diff --git a/server/lib/src/auth.dart b/server/lib/src/auth.dart index a84a8d5..bcc0303 100644 --- a/server/lib/src/auth.dart +++ b/server/lib/src/auth.dart @@ -1,12 +1,11 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'dart:math'; import 'package:aqueduct/aqueduct.dart'; import 'package:corsac_jwt/corsac_jwt.dart'; -import 'package:steel_crypt/steel_crypt.dart'; +import 'compute.dart'; +import 'crypt.dart'; import 'database.dart'; /// Information on the rights of the user making the request. @@ -54,9 +53,6 @@ class RequestUser { class RegisterController extends Controller { final ServerDatabase db; - final _crypt = PassCrypt(); - final _rand = Random.secure(); - RegisterController(this.db); @override @@ -69,21 +65,20 @@ class RegisterController extends Controller { final existingUser = await db.getUser(requestUser.name); if (existingUser != null) { // Returning something different than 200 here has the security - // implication that an attacker can check for existing user names. At the - // moment, I don't see any alternatives, because we don't use email + // implication that an attacker can check for existing user names. At + // the moment, I don't see any alternatives, because we don't use email // addresses for identification. The client needs to know, whether the // user name is already given. return Response.conflict(); } else { - final bytes = List.generate(32, (i) => _rand.nextInt(256)); - final salt = base64UrlEncode(bytes); - final hash = _crypt.hashPass(salt, requestUser.password); + // This will take a long time, so we run it in a new isolate. + final result = await compute(Crypt.hashPassword, requestUser.password); db.updateUser(User( name: requestUser.name, email: requestUser.email, - salt: salt, - hash: hash, + salt: result.salt, + hash: result.hash, mayUpload: true, mayEdit: false, mayDelete: false, @@ -106,7 +101,6 @@ class LoginController extends Controller { /// The secret that will be used for signing the token. final String secret; - final _crypt = PassCrypt(); final JWTHmacSha256Signer _signer; LoginController(this.db, this.secret) : _signer = JWTHmacSha256Signer(secret); @@ -119,8 +113,16 @@ class LoginController extends Controller { final realUser = await db.getUser(requestUser.name); if (realUser != null) { - if (_crypt.checkPassKey( - realUser.salt, requestUser.password, realUser.hash)) { + // We check the password in a new isolate, because this can take a long + // time. + if (await compute( + Crypt.checkPassword, + CheckPasswordRequest( + password: requestUser.password, + salt: realUser.salt, + hash: realUser.hash, + ), + )) { final builder = JWTBuilder() ..expiresAt = DateTime.now().add(Duration(minutes: 30)) ..setClaim('user', requestUser.name); diff --git a/server/lib/src/compute.dart b/server/lib/src/compute.dart new file mode 100644 index 0000000..c29c5ef --- /dev/null +++ b/server/lib/src/compute.dart @@ -0,0 +1,52 @@ +import 'dart:isolate'; + +import 'package:meta/meta.dart'; + +/// This function will run within the new isolate. +void _isolateEntrypoint(_ComputeRequest request) { + final result = request.compute(); + request.sendPort.send(result); +} + +/// Bundle of information to pass to the isolate. +class _ComputeRequest { + /// 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 compute(T Function(S parameter) function, S parameter) async { + final receivePort = ReceivePort(); + + Isolate.spawn( + _isolateEntrypoint, + _ComputeRequest( + function: function, + parameter: parameter, + sendPort: receivePort.sendPort, + ), + ); + + return await receivePort.first as T; +} diff --git a/server/lib/src/crypt.dart b/server/lib/src/crypt.dart new file mode 100644 index 0000000..dc46639 --- /dev/null +++ b/server/lib/src/crypt.dart @@ -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); + } +}