server: Run crypto methods in seperate isolate

This commit is contained in:
Elias Projahn 2020-05-11 18:08:09 +02:00
parent fa2e9ebacd
commit e897465fd7
3 changed files with 132 additions and 16 deletions

View file

@ -1,12 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:aqueduct/aqueduct.dart'; import 'package:aqueduct/aqueduct.dart';
import 'package:corsac_jwt/corsac_jwt.dart'; import 'package:corsac_jwt/corsac_jwt.dart';
import 'package:steel_crypt/steel_crypt.dart';
import 'compute.dart';
import 'crypt.dart';
import 'database.dart'; import 'database.dart';
/// Information on the rights of the user making the request. /// Information on the rights of the user making the request.
@ -54,9 +53,6 @@ class RequestUser {
class RegisterController extends Controller { class RegisterController extends Controller {
final ServerDatabase db; final ServerDatabase db;
final _crypt = PassCrypt();
final _rand = Random.secure();
RegisterController(this.db); RegisterController(this.db);
@override @override
@ -69,21 +65,20 @@ class RegisterController extends Controller {
final existingUser = await db.getUser(requestUser.name); final existingUser = await db.getUser(requestUser.name);
if (existingUser != null) { if (existingUser != null) {
// Returning something different than 200 here has the security // Returning something different than 200 here has the security
// implication that an attacker can check for existing user names. At the // implication that an attacker can check for existing user names. At
// moment, I don't see any alternatives, because we don't use email // the moment, I don't see any alternatives, because we don't use email
// addresses for identification. The client needs to know, whether the // addresses for identification. The client needs to know, whether the
// user name is already given. // user name is already given.
return Response.conflict(); return Response.conflict();
} else { } else {
final bytes = List.generate(32, (i) => _rand.nextInt(256)); // This will take a long time, so we run it in a new isolate.
final salt = base64UrlEncode(bytes); final result = await compute(Crypt.hashPassword, requestUser.password);
final hash = _crypt.hashPass(salt, requestUser.password);
db.updateUser(User( db.updateUser(User(
name: requestUser.name, name: requestUser.name,
email: requestUser.email, email: requestUser.email,
salt: salt, salt: result.salt,
hash: hash, hash: result.hash,
mayUpload: true, mayUpload: true,
mayEdit: false, mayEdit: false,
mayDelete: false, mayDelete: false,
@ -106,7 +101,6 @@ class LoginController extends Controller {
/// The secret that will be used for signing the token. /// The secret that will be used for signing the token.
final String secret; final String secret;
final _crypt = PassCrypt();
final JWTHmacSha256Signer _signer; final JWTHmacSha256Signer _signer;
LoginController(this.db, this.secret) : _signer = JWTHmacSha256Signer(secret); LoginController(this.db, this.secret) : _signer = JWTHmacSha256Signer(secret);
@ -119,8 +113,16 @@ class LoginController extends Controller {
final realUser = await db.getUser(requestUser.name); final realUser = await db.getUser(requestUser.name);
if (realUser != null) { if (realUser != null) {
if (_crypt.checkPassKey( // We check the password in a new isolate, because this can take a long
realUser.salt, requestUser.password, realUser.hash)) { // time.
if (await compute(
Crypt.checkPassword,
CheckPasswordRequest(
password: requestUser.password,
salt: realUser.salt,
hash: realUser.hash,
),
)) {
final builder = JWTBuilder() final builder = JWTBuilder()
..expiresAt = DateTime.now().add(Duration(minutes: 30)) ..expiresAt = DateTime.now().add(Duration(minutes: 30))
..setClaim('user', requestUser.name); ..setClaim('user', requestUser.name);

View file

@ -0,0 +1,52 @@
import 'dart:isolate';
import 'package:meta/meta.dart';
/// This function will run within the new isolate.
void _isolateEntrypoint<T, S>(_ComputeRequest<T, S> request) {
final result = request.compute();
request.sendPort.send(result);
}
/// Bundle of information to pass to the isolate.
class _ComputeRequest<T, S> {
/// The function to call.
T Function(S parameter) function;
/// The parameter to pass to the function.
S parameter;
/// The port through which the result will be sent.
SendPort sendPort;
_ComputeRequest({
@required this.function,
@required this.parameter,
@required this.sendPort,
});
/// Call [function] with [parameter] and return the result.
///
/// This function exists to avoid type errors within the isolate.
T compute() => function(parameter);
}
/// Call a function in a new isolate and await the result.
///
/// The function has to be a static function. If the result is not a primitive
/// value or a list or map of such, this won't work
/// (see https://api.dart.dev/stable/2.8.1/dart-isolate/SendPort/send.html).
Future<T> compute<T, S>(T Function(S parameter) function, S parameter) async {
final receivePort = ReceivePort();
Isolate.spawn(
_isolateEntrypoint,
_ComputeRequest<T, S>(
function: function,
parameter: parameter,
sendPort: receivePort.sendPort,
),
);
return await receivePort.first as T;
}

62
server/lib/src/crypt.dart Normal file
View file

@ -0,0 +1,62 @@
import 'dart:convert';
import 'dart:math';
import 'package:meta/meta.dart';
import 'package:steel_crypt/steel_crypt.dart';
/// Result of [hashPassword].
class HashPasswordResult {
/// The computed hash.
final String hash;
/// A randomly generated string.
final String salt;
HashPasswordResult({
@required this.hash,
@required this.salt,
});
}
/// Parameters for [checkPassword].
class CheckPasswordRequest {
/// The password to check.
final String password;
/// The salt that was used for computing the hash.
final String salt;
/// The hash value to check against.
final String hash;
CheckPasswordRequest({
@required this.password,
@required this.salt,
@required this.hash,
});
}
/// Methods for handling passwords.
class Crypt {
static final _crypt = PassCrypt();
static final _rand = Random.secure();
/// Compute a hash for a password.
///
/// The result will contain the hash and a randomly generated salt.
static HashPasswordResult hashPassword(String password) {
final bytes = List.generate(32, (i) => _rand.nextInt(256));
final salt = base64UrlEncode(bytes);
final hash = _crypt.hashPass(salt, password);
return HashPasswordResult(
hash: hash,
salt: salt,
);
}
/// Check whether a password matches a hash value.
static bool checkPassword(CheckPasswordRequest request) {
return _crypt.checkPassKey(request.salt, request.password, request.hash);
}
}