diff --git a/packages/sodium/CHANGELOG.md b/packages/sodium/CHANGELOG.md index a5a11fdc..175d7d22 100644 --- a/packages/sodium/CHANGELOG.md +++ b/packages/sodium/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.5] - 2021-05-27 +### Added +- New libsodium API: crypto_generichash + ## [0.1.4] - 2021-05-21 ### Added - New libsodium API: crypto_box_seal diff --git a/packages/sodium/README.md b/packages/sodium/README.md index 464d9885..c20bb38e 100644 --- a/packages/sodium/README.md +++ b/packages/sodium/README.md @@ -51,7 +51,7 @@ API based on libsodium version: *1.0.18* crypto_box | ✔️ | ✔️ | https://libsodium.gitbook.io/doc/public-key_cryptography/authenticated_encryption crypto_sign | ✔️ | ✔️ | https://libsodium.gitbook.io/doc/public-key_cryptography/public-key_signatures crypto_box_seal | ✔️ | ✔️ | https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes - crypto_generichash | 🚧 | 🚧 | https://libsodium.gitbook.io/doc/hashing/generic_hashing + crypto_generichash | ✔️ | ✔️ | https://libsodium.gitbook.io/doc/hashing/generic_hashing crypto_shorthash | 🚧 | 🚧 | https://libsodium.gitbook.io/doc/hashing/short-input_hashing crypto_pwhash | ✔️ | ✔️ | https://libsodium.gitbook.io/doc/password_hashing/default_phf crypto_kdf | 🚧 | 🚧 | https://libsodium.gitbook.io/doc/key_derivation diff --git a/packages/sodium/lib/sodium.dart b/packages/sodium/lib/sodium.dart index 1531c8d6..b5b3a9d4 100644 --- a/packages/sodium/lib/sodium.dart +++ b/packages/sodium/lib/sodium.dart @@ -2,6 +2,7 @@ export 'src/api/auth.dart' hide AuthValidations; export 'src/api/box.dart' hide BoxValidations; export 'src/api/crypto.dart'; export 'src/api/detached_cipher_result.dart'; +export 'src/api/generic_hash.dart' hide GenericHashValidations; export 'src/api/key_pair.dart'; export 'src/api/pwhash.dart' hide PwHashValidations; export 'src/api/randombytes.dart'; diff --git a/packages/sodium/lib/src/api/crypto.dart b/packages/sodium/lib/src/api/crypto.dart index 05f31a80..66256d3d 100644 --- a/packages/sodium/lib/src/api/crypto.dart +++ b/packages/sodium/lib/src/api/crypto.dart @@ -1,5 +1,6 @@ import 'auth.dart'; import 'box.dart'; +import 'generic_hash.dart'; import 'pwhash.dart'; import 'secret_box.dart'; import 'secret_stream.dart'; @@ -34,6 +35,11 @@ abstract class Crypto { /// This provides all APIs that start with `crypto_sign`. Sign get sign; + /// An instance of [GenericHash]. + /// + /// This provides all APIs that start with `crypto_generichash`. + GenericHash get genericHash; + /// An instance of [Pwhash]. /// /// This provides all APIs that start with `crypto_pwhash`. diff --git a/packages/sodium/lib/src/api/generic_hash.dart b/packages/sodium/lib/src/api/generic_hash.dart new file mode 100644 index 00000000..c767cd59 --- /dev/null +++ b/packages/sodium/lib/src/api/generic_hash.dart @@ -0,0 +1,152 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import 'helpers/validations.dart'; +import 'secure_key.dart'; + +/// A typed [StreamConsumer], which is used to generate a hash from a stream of +/// data. +/// +/// See [GenericHash.createConsumer] for more details. +abstract class GenericHashConsumer implements StreamConsumer { + const GenericHashConsumer._(); // coverage:ignore-line + + /// A future that resolves to the hash of the data. + /// + /// This is the same future as returned by [close]. It will be resolved as + /// soon as the consumer is closed and will either produce the actual + /// hash of the consumed data, or an error if something went wrong. + Future get hash; + + /// Closes the consumer and calculates the hash. + /// + /// This internally finalizes the consumer and calculates the hash over all + /// the received data. Once done, the hash is returned, or an error thrown, if + /// it failed. The returned future is the same as the on provided via [hash]. + /// + /// After having been closed, no more streams can be added to the consumer. + /// See [StreamConsumer.close] for more details. + @override + Future close(); +} + +/// A meta class that provides access to all libsodium generichash APIs. +/// +/// This class provides the dart interface for the crypto operations documented +/// in https://libsodium.gitbook.io/doc/hashing/generic_hashing. +/// Please refer to that documentation for more details about these APIs. +abstract class GenericHash { + const GenericHash._(); // coverage:ignore-line + + /// Provides crypto_generichash_BYTES. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#constants + int get bytes; + + /// Provides crypto_generichash_BYTES_MIN. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#constants + int get bytesMin; + + /// Provides crypto_generichash_BYTES_MAX. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#constants + int get bytesMax; + + /// Provides crypto_generichash_KEYBYTES. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#constants + int get keyBytes; + + /// Provides crypto_generichash_KEYBYTES_MIN. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#constants + int get keyBytesMin; + + /// Provides crypto_generichash_KEYBYTES_MAX. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#constants + int get keyBytesMax; + + /// Provides crypto_generichash_keygen. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#usage + SecureKey keygen(); + + /// Provides crypto_generichash. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#usage + Uint8List call({ + required Uint8List message, + int? outLen, + SecureKey? key, + }); + + /// Creates a [StreamConsumer] for generating a hash from a stream. + /// + /// The returned [GenericHashConsumer] is basically a typed [StreamConsumer], + /// that wraps the generichash streaming APIs. Creating the consumer will call + /// crypto_generichash_init, adding messages to it via + /// [GenericHashConsumer.addStream] will call crypto_generichash_update for + /// every event in the stream. After you are done adding messages, you can + /// [GenericHashConsumer.close] it, which will call crypto_generichash_final + /// internally and return the hash of the data. + /// + /// Optionally, you can pass [outLen] to modify the length of the generated + /// hash and [key] if you want to use the hash as MAC. + /// + /// For simpler usage, if you only have a single input [Stream] and simply + /// want to get the hash from it, you ca use [stream] instead. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#usage + GenericHashConsumer createConsumer({ + int? outLen, + SecureKey? key, + }); + + /// Get the hash from an aynchronous stream of data. + /// + /// This is a shortcut for [createConsumer], which simply calls [Stream.pipe] + /// on the [messages] stream and pipes it into the consumer. The returned + /// result is the hash over all the data in the [messages] stream, optionally + /// with a modified [outLen] or a [key]. + /// + /// See https://libsodium.gitbook.io/doc/hashing/generic_hashing#usage + Future stream({ + required Stream messages, + int? outLen, + SecureKey? key, + }); +} + +@internal +mixin GenericHashValidations implements GenericHash { + void validateOutLen(int outLen) => Validations.checkInRange( + outLen, + bytesMin, + bytesMax, + 'outLen', + ); + + void validateKey(SecureKey key) => Validations.checkInRange( + key.length, + keyBytesMin, + keyBytesMax, + 'key', + ); + + @override + Future stream({ + required Stream messages, + int? outLen, + SecureKey? key, + }) => + messages + .pipe(createConsumer( + outLen: outLen, + key: key, + )) + .then((dynamic value) => value as Uint8List); +} diff --git a/packages/sodium/lib/src/ffi/api/crypto_ffi.dart b/packages/sodium/lib/src/ffi/api/crypto_ffi.dart index df2dba19..66c78335 100644 --- a/packages/sodium/lib/src/ffi/api/crypto_ffi.dart +++ b/packages/sodium/lib/src/ffi/api/crypto_ffi.dart @@ -3,6 +3,7 @@ import 'package:meta/meta.dart'; import '../../api/auth.dart'; import '../../api/box.dart'; import '../../api/crypto.dart'; +import '../../api/generic_hash.dart'; import '../../api/pwhash.dart'; import '../../api/secret_box.dart'; import '../../api/secret_stream.dart'; @@ -10,6 +11,7 @@ import '../../api/sign.dart'; import '../bindings/libsodium.ffi.dart'; import 'auth_ffi.dart'; import 'box_ffi.dart'; +import 'generic_hash_ffi.dart'; import 'pwhash_ffi.dart'; import 'secret_box_ffi.dart'; import 'secret_stream_ffi.dart'; @@ -36,6 +38,9 @@ class CryptoFFI implements Crypto { @override late final Sign sign = SignFFI(sodium); + @override + late final GenericHash genericHash = GenericHashFFI(sodium); + @override late final Pwhash pwhash = PwhashFFI(sodium); } diff --git a/packages/sodium/lib/src/ffi/api/generic_hash_ffi.dart b/packages/sodium/lib/src/ffi/api/generic_hash_ffi.dart new file mode 100644 index 00000000..f200043d --- /dev/null +++ b/packages/sodium/lib/src/ffi/api/generic_hash_ffi.dart @@ -0,0 +1,109 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import '../../api/generic_hash.dart'; +import '../../api/secure_key.dart'; +import '../../api/sodium_exception.dart'; +import '../bindings/libsodium.ffi.dart'; +import '../bindings/memory_protection.dart'; +import '../bindings/secure_key_native.dart'; +import '../bindings/sodium_pointer.dart'; +import 'helpers/generic_hash/generic_hash_consumer_ffi.dart'; +import 'helpers/keygen_mixin.dart'; + +class GenericHashFFI + with GenericHashValidations, KeygenMixin + implements GenericHash { + final LibSodiumFFI sodium; + + GenericHashFFI(this.sodium); + + @override + int get bytes => sodium.crypto_generichash_bytes(); + + @override + int get bytesMin => sodium.crypto_generichash_bytes_min(); + + @override + int get bytesMax => sodium.crypto_generichash_bytes_max(); + + @override + int get keyBytes => sodium.crypto_generichash_keybytes(); + + @override + int get keyBytesMin => sodium.crypto_generichash_keybytes_min(); + + @override + int get keyBytesMax => sodium.crypto_generichash_keybytes_max(); + + @override + SecureKey keygen() => keygenImpl( + sodium: sodium, + keyBytes: keyBytes, + implementation: sodium.crypto_generichash_keygen, + ); + + @override + Uint8List call({ + required Uint8List message, + int? outLen, + SecureKey? key, + }) { + if (outLen != null) { + validateOutLen(outLen); + } + if (key != null) { + validateKey(key); + } + + SodiumPointer? outPtr; + SodiumPointer? inPtr; + try { + outPtr = SodiumPointer.alloc( + sodium, + count: outLen ?? bytes, + ); + inPtr = message.toSodiumPointer( + sodium, + memoryProtection: MemoryProtection.readOnly, + ); + + final result = key.runMaybeUnlockedNative( + sodium, + (keyPtr) => sodium.crypto_generichash( + outPtr!.ptr, + outPtr.count, + inPtr!.ptr, + inPtr.count, + keyPtr?.ptr ?? nullptr, + keyPtr?.count ?? 0, + ), + ); + SodiumException.checkSucceededInt(result); + + return outPtr.copyAsList(); + } finally { + outPtr?.dispose(); + inPtr?.dispose(); + } + } + + @override + GenericHashConsumer createConsumer({ + int? outLen, + SecureKey? key, + }) { + if (outLen != null) { + validateOutLen(outLen); + } + if (key != null) { + validateKey(key); + } + + return GenericHashConsumerFFI( + sodium: sodium, + outLen: outLen ?? bytes, + key: key, + ); + } +} diff --git a/packages/sodium/lib/src/ffi/api/helpers/generic_hash/generic_hash_consumer_ffi.dart b/packages/sodium/lib/src/ffi/api/helpers/generic_hash/generic_hash_consumer_ffi.dart new file mode 100644 index 00000000..30800e3a --- /dev/null +++ b/packages/sodium/lib/src/ffi/api/helpers/generic_hash/generic_hash_consumer_ffi.dart @@ -0,0 +1,113 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import '../../../../api/generic_hash.dart'; +import '../../../../api/secure_key.dart'; +import '../../../../api/sodium_exception.dart'; +import '../../../bindings/libsodium.ffi.dart'; +import '../../../bindings/memory_protection.dart'; +import '../../../bindings/secure_key_native.dart'; +import '../../../bindings/sodium_pointer.dart'; + +@internal +class GenericHashConsumerFFI implements GenericHashConsumer { + final LibSodiumFFI sodium; + final int outLen; + + final _hashCompleter = Completer(); + late final SodiumPointer _state; + + @override + Future get hash => _hashCompleter.future; + + GenericHashConsumerFFI({ + required this.sodium, + required this.outLen, + SecureKey? key, + }) { + _state = SodiumPointer.alloc( + sodium, + count: sodium.crypto_generichash_statebytes(), + zeroMemory: true, + ); + + try { + final result = key.runMaybeUnlockedNative( + sodium, + (keyPtr) => sodium.crypto_generichash_init( + _state.ptr.cast(), + keyPtr?.ptr ?? nullptr, + keyPtr?.count ?? 0, + outLen, + ), + ); + SodiumException.checkSucceededInt(result); + } catch (e) { + _state.dispose(); + rethrow; + } + } + + @override + Future addStream(Stream stream) { + _ensureNotCompleted(); + + return stream.map((event) { + SodiumPointer? messagePtr; + try { + messagePtr = event.toSodiumPointer( + sodium, + memoryProtection: MemoryProtection.readOnly, + ); + + final result = sodium.crypto_generichash_update( + _state.ptr.cast(), + messagePtr.ptr, + messagePtr.count, + ); + SodiumException.checkSucceededInt(result); + } finally { + messagePtr?.dispose(); + } + }).drain(); + } + + @override + Future close() { + _ensureNotCompleted(); + + SodiumPointer? outPtr; + try { + outPtr = SodiumPointer.alloc( + sodium, + count: outLen, + zeroMemory: true, + ); + + final result = sodium.crypto_generichash_final( + _state.ptr.cast(), + outPtr.ptr, + outPtr.count, + ); + SodiumException.checkSucceededInt(result); + + _hashCompleter.complete(outPtr.copyAsList()); + } catch (e, s) { + _hashCompleter.completeError(e, s); + } finally { + outPtr?.dispose(); + _state.dispose(); + } + + return _hashCompleter.future; + } + + void _ensureNotCompleted() { + if (_hashCompleter.isCompleted) { + throw StateError('Hash has already been finalized'); + } + } +} diff --git a/packages/sodium/lib/src/js/api/crypto_js.dart b/packages/sodium/lib/src/js/api/crypto_js.dart index e73874e6..db01ca53 100644 --- a/packages/sodium/lib/src/js/api/crypto_js.dart +++ b/packages/sodium/lib/src/js/api/crypto_js.dart @@ -3,6 +3,7 @@ import 'package:meta/meta.dart'; import '../../api/auth.dart'; import '../../api/box.dart'; import '../../api/crypto.dart'; +import '../../api/generic_hash.dart'; import '../../api/pwhash.dart'; import '../../api/secret_box.dart'; import '../../api/secret_stream.dart'; @@ -10,6 +11,7 @@ import '../../api/sign.dart'; import '../bindings/sodium.js.dart' hide SecretBox; import 'auth_js.dart'; import 'box_js.dart'; +import 'generic_hash_js.dart'; import 'pwhash_js.dart'; import 'secret_box_js.dart'; import 'secret_stream_js.dart'; @@ -36,6 +38,9 @@ class CryptoJS implements Crypto { @override late final Sign sign = SignJS(sodium); + @override + late final GenericHash genericHash = GenericHashJS(sodium); + @override late final Pwhash pwhash = PwhashJS(sodium); } diff --git a/packages/sodium/lib/src/js/api/generic_hash_js.dart b/packages/sodium/lib/src/js/api/generic_hash_js.dart new file mode 100644 index 00000000..f84d5be9 --- /dev/null +++ b/packages/sodium/lib/src/js/api/generic_hash_js.dart @@ -0,0 +1,85 @@ +import 'dart:typed_data'; + +import '../../api/generic_hash.dart'; +import '../../api/secure_key.dart'; +import '../bindings/js_error.dart'; +import '../bindings/secure_key_nullable_x.dart'; +import '../bindings/sodium.js.dart'; +import '../bindings/to_safe_int.dart'; +import 'helpers/generic_hash/generic_hash_consumer_js.dart'; +import 'secure_key_js.dart'; + +class GenericHashJS with GenericHashValidations implements GenericHash { + final LibSodiumJS sodium; + + GenericHashJS(this.sodium); + + @override + int get bytes => sodium.crypto_generichash_BYTES.toSafeUInt32(); + + @override + int get bytesMin => sodium.crypto_generichash_BYTES_MIN.toSafeUInt32(); + + @override + int get bytesMax => sodium.crypto_generichash_BYTES_MAX.toSafeUInt32(); + + @override + int get keyBytes => sodium.crypto_generichash_KEYBYTES.toSafeUInt32(); + + @override + int get keyBytesMin => sodium.crypto_generichash_KEYBYTES_MIN.toSafeUInt32(); + + @override + int get keyBytesMax => sodium.crypto_generichash_KEYBYTES_MAX.toSafeUInt32(); + + @override + SecureKey keygen() => SecureKeyJS( + sodium, + JsError.wrap( + () => sodium.crypto_generichash_keygen(), + ), + ); + + @override + Uint8List call({ + required Uint8List message, + int? outLen, + SecureKey? key, + }) { + if (outLen != null) { + validateOutLen(outLen); + } + if (key != null) { + validateKey(key); + } + + return JsError.wrap( + () => key.runMaybeUnlockedSync( + (keyData) => sodium.crypto_generichash( + outLen ?? bytes, + message, + keyData, + ), + ), + ); + } + + @override + GenericHashConsumer createConsumer({ + int? outLen, + SecureKey? key, + }) { + if (outLen != null) { + validateOutLen(outLen); + } + if (key != null) { + validateKey(key); + } + + return GenericHashConsumerJS( + sodium: sodium, + outLen: outLen ?? bytes, + key: key, + ); + } +} diff --git a/packages/sodium/lib/src/js/api/helpers/generic_hash/generic_hash_consumer_js.dart b/packages/sodium/lib/src/js/api/helpers/generic_hash/generic_hash_consumer_js.dart new file mode 100644 index 00000000..28797a00 --- /dev/null +++ b/packages/sodium/lib/src/js/api/helpers/generic_hash/generic_hash_consumer_js.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import '../../../../api/generic_hash.dart'; +import '../../../../api/secure_key.dart'; +import '../../../bindings/js_error.dart'; +import '../../../bindings/secure_key_nullable_x.dart'; +import '../../../bindings/sodium.js.dart'; + +class GenericHashConsumerJS implements GenericHashConsumer { + final LibSodiumJS sodium; + final int outLen; + + final _hashCompleter = Completer(); + late final GenerichashState _state; + + @override + Future get hash => _hashCompleter.future; + + GenericHashConsumerJS({ + required this.sodium, + required this.outLen, + SecureKey? key, + }) { + _state = JsError.wrap( + () => key.runMaybeUnlockedSync( + (keyData) => sodium.crypto_generichash_init(keyData, outLen), + ), + ); + } + + @override + Future addStream(Stream stream) { + _ensureNotCompleted(); + + return stream + .map( + (event) => JsError.wrap( + () => sodium.crypto_generichash_update(_state, event), + ), + ) + .drain(); + } + + @override + Future close() { + _ensureNotCompleted(); + + try { + final result = JsError.wrap( + () => sodium.crypto_generichash_final( + _state, + outLen, + ), + ); + _hashCompleter.complete(result); + } catch (e, s) { + _hashCompleter.completeError(e, s); + } + + return _hashCompleter.future; + } + + void _ensureNotCompleted() { + if (_hashCompleter.isCompleted) { + throw StateError('Hash has already been finalized'); + } + } +} diff --git a/packages/sodium/lib/src/js/bindings/secure_key_nullable_x.dart b/packages/sodium/lib/src/js/bindings/secure_key_nullable_x.dart new file mode 100644 index 00000000..72e06083 --- /dev/null +++ b/packages/sodium/lib/src/js/bindings/secure_key_nullable_x.dart @@ -0,0 +1,15 @@ +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import '../../api/secure_key.dart'; + +typedef SecureNullableCallbackFn = T Function(Uint8List? data); + +@internal +extension SecureKeyNullableX on SecureKey? { + T runMaybeUnlockedSync(SecureNullableCallbackFn callback) => + this != null + ? this!.runUnlockedSync((data) => callback(data)) + : callback(null); +} diff --git a/packages/sodium/pubspec.yaml b/packages/sodium/pubspec.yaml index 4bfa82c7..e5ef4363 100644 --- a/packages/sodium/pubspec.yaml +++ b/packages/sodium/pubspec.yaml @@ -1,6 +1,6 @@ name: sodium description: Dart bindings for libsodium, for the Dart-VM and for the Web -version: 0.1.4 +version: 0.1.5 homepage: https://github.com/Skycoder42/libsodium_dart_bindings environment: diff --git a/packages/sodium/test/integration/cases/auth_test_case.dart b/packages/sodium/test/integration/cases/auth_test_case.dart index 7d50204f..0cfc5b31 100644 --- a/packages/sodium/test/integration/cases/auth_test_case.dart +++ b/packages/sodium/test/integration/cases/auth_test_case.dart @@ -26,8 +26,8 @@ class AuthTestCase extends TestCase { printOnFailure('key1: ${key1.extractBytes()}'); printOnFailure('key2: ${key2.extractBytes()}'); - expect(key1, hasLength(32)); - expect(key2, hasLength(32)); + expect(key1, hasLength(sut.keyBytes)); + expect(key2, hasLength(sut.keyBytes)); expect(key1, isNot(key2)); }); diff --git a/packages/sodium/test/integration/cases/generic_hash_test_case.dart b/packages/sodium/test/integration/cases/generic_hash_test_case.dart new file mode 100644 index 00000000..1bcec7a5 --- /dev/null +++ b/packages/sodium/test/integration/cases/generic_hash_test_case.dart @@ -0,0 +1,260 @@ +import 'dart:typed_data'; + +// dart_pre_commit:ignore-library-import +import 'package:sodium/sodium.dart'; +import 'package:test/test.dart'; + +import '../test_case.dart'; + +class GenericHashTestCase extends TestCase { + @override + String get name => 'generichash'; + + GenericHash get sut => sodium.crypto.genericHash; + + @override + void setupTests() { + test('constants return correct values', () { + expect(sut.bytes, 32, reason: 'bytes'); + expect(sut.bytesMin, 16, reason: 'bytesMin'); + expect(sut.bytesMax, 64, reason: 'bytesMax'); + expect(sut.keyBytes, 32, reason: 'keyBytes'); + expect(sut.keyBytesMin, 16, reason: 'keyBytesMin'); + expect(sut.keyBytesMax, 64, reason: 'keyBytesMax'); + }); + + test('keygen generates different correct length keys', () { + final key1 = sut.keygen(); + final key2 = sut.keygen(); + + printOnFailure('key1: ${key1.extractBytes()}'); + printOnFailure('key2: ${key2.extractBytes()}'); + + expect(key1, hasLength(sut.keyBytes)); + expect(key2, hasLength(sut.keyBytes)); + + expect(key1, isNot(key2)); + }); + + group('hash', () { + test('generates same hash for same data', () { + final message = Uint8List.fromList( + List.generate(64, (index) => index + 32), + ); + + printOnFailure('message: $message'); + + final hash1 = sut(message: message); + final hash2 = sut(message: message); + + printOnFailure('hash1: $hash1'); + printOnFailure('hash2: $hash2'); + + expect(hash1, hasLength(sut.bytes)); + expect(hash2, hasLength(sut.bytes)); + + expect(hash1, hash2); + }); + + test('generates different hashes for different data', () { + final message1 = Uint8List.fromList( + List.generate(64, (index) => index + 32), + ); + final message2 = Uint8List.fromList( + List.generate(64, (index) => index - 32), + ); + + printOnFailure('message1: $message1'); + printOnFailure('message2: $message2'); + + final hash1 = sut(message: message1); + final hash2 = sut(message: message2); + + printOnFailure('hash1: $hash1'); + printOnFailure('hash2: $hash2'); + + expect(hash1, hasLength(sut.bytes)); + expect(hash2, hasLength(sut.bytes)); + + expect(hash1, isNot(hash2)); + }); + + test('generates same hash for same key', () { + final key = sut.keygen(); + final message = Uint8List.fromList( + List.generate(64, (index) => index), + ); + + printOnFailure('message: $message'); + + final hash1 = sut( + message: message, + outLen: sut.bytesMax, + key: key, + ); + final hash2 = sut( + message: message, + outLen: sut.bytesMax, + key: key, + ); + + printOnFailure('hash1: $hash1'); + printOnFailure('hash2: $hash2'); + + expect(hash1, hasLength(sut.bytesMax)); + expect(hash2, hasLength(sut.bytesMax)); + + expect(hash1, hash2); + }); + + test('generates different hashes for different keys', () { + final key1 = sut.keygen(); + final key2 = sut.keygen(); + final message = Uint8List.fromList( + List.generate(64, (index) => index), + ); + + printOnFailure('message: $message'); + + final hash1 = sut( + message: message, + outLen: sut.bytesMin, + key: key1, + ); + final hash2 = sut( + message: message, + outLen: sut.bytesMin, + key: key2, + ); + + printOnFailure('hash1: $hash1'); + printOnFailure('hash2: $hash2'); + + expect(hash1, hasLength(sut.bytesMin)); + expect(hash2, hasLength(sut.bytesMin)); + + expect(hash1, isNot(hash2)); + }); + }); + + group('stream', () { + test('generates same hash for same data', () async { + final messages = List.generate( + 10, + (i) => Uint8List.fromList( + List.generate(32, (j) => i + j), + ), + ); + + printOnFailure('message: $messages'); + + final hash1 = await sut.stream(messages: Stream.fromIterable(messages)); + final hash2 = await sut.stream(messages: Stream.fromIterable(messages)); + + printOnFailure('hash1: $hash1'); + printOnFailure('hash2: $hash2'); + + expect(hash1, hasLength(sut.bytes)); + expect(hash2, hasLength(sut.bytes)); + + expect(hash1, hash2); + }); + + test('generates different hashes for different data', () async { + final messages1 = List.generate( + 10, + (i) => Uint8List.fromList( + List.generate(20, (j) => i + j), + ), + ); + final messages2 = List.generate( + 10, + (i) => Uint8List.fromList( + List.generate(20, (j) => i * j), + ), + ); + + printOnFailure('message1: $messages1'); + printOnFailure('message2: $messages2'); + + final hash1 = await sut.stream( + messages: Stream.fromIterable(messages1), + ); + final hash2 = await sut.stream( + messages: Stream.fromIterable(messages2), + ); + + printOnFailure('hash1: $hash1'); + printOnFailure('hash2: $hash2'); + + expect(hash1, hasLength(sut.bytes)); + expect(hash2, hasLength(sut.bytes)); + + expect(hash1, isNot(hash2)); + }); + + test('generates same hash for same key', () async { + final key = sut.keygen(); + final messages = List.generate( + 10, + (i) => Uint8List.fromList( + List.generate(32, (j) => i + j), + ), + ); + + printOnFailure('message: $messages'); + + final hash1 = await sut.stream( + messages: Stream.fromIterable(messages), + outLen: sut.bytesMax, + key: key, + ); + final hash2 = await sut.stream( + messages: Stream.fromIterable(messages), + outLen: sut.bytesMax, + key: key, + ); + + printOnFailure('hash1: $hash1'); + printOnFailure('hash2: $hash2'); + + expect(hash1, hasLength(sut.bytesMax)); + expect(hash2, hasLength(sut.bytesMax)); + + expect(hash1, hash2); + }); + + test('generates different hashes for different keys', () async { + final key1 = sut.keygen(); + final key2 = sut.keygen(); + final messages = List.generate( + 10, + (i) => Uint8List.fromList( + List.generate(32, (j) => i + j), + ), + ); + + printOnFailure('message: $messages'); + + final hash1 = await sut.stream( + messages: Stream.fromIterable(messages), + outLen: sut.bytesMin, + key: key1, + ); + final hash2 = await sut.stream( + messages: Stream.fromIterable(messages), + outLen: sut.bytesMin, + key: key2, + ); + + printOnFailure('hash1: $hash1'); + printOnFailure('hash2: $hash2'); + + expect(hash1, hasLength(sut.bytesMin)); + expect(hash2, hasLength(sut.bytesMin)); + + expect(hash1, isNot(hash2)); + }); + }); + } +} diff --git a/packages/sodium/test/integration/test_runner.dart b/packages/sodium/test/integration/test_runner.dart index f1330a95..4ac83b68 100644 --- a/packages/sodium/test/integration/test_runner.dart +++ b/packages/sodium/test/integration/test_runner.dart @@ -4,6 +4,7 @@ import 'package:test/test.dart'; import 'cases/auth_test_case.dart'; import 'cases/box_test_case.dart'; +import 'cases/generic_hash_test_case.dart'; import 'cases/pwhash_test_case.dart'; import 'cases/randombytes_test_case.dart'; import 'cases/secret_box_test_case.dart'; @@ -21,6 +22,7 @@ abstract class TestRunner { AuthTestCase(), BoxTestCase(), SignTestCase(), + GenericHashTestCase(), PwhashTestCase(), ]; diff --git a/packages/sodium/test/unit/api/generic_hash_test.dart b/packages/sodium/test/unit/api/generic_hash_test.dart new file mode 100644 index 00000000..09c14d2f --- /dev/null +++ b/packages/sodium/test/unit/api/generic_hash_test.dart @@ -0,0 +1,79 @@ +import 'dart:typed_data'; + +import 'package:mocktail/mocktail.dart'; +import 'package:sodium/src/api/generic_hash.dart'; +import 'package:test/test.dart'; + +import '../../secure_key_fake.dart'; +import '../../test_validator.dart'; + +class MockGenericHash extends Mock + with GenericHashValidations + implements GenericHash {} + +class MockGenericHashConsumer extends Mock implements GenericHashConsumer {} + +void main() { + setUpAll(() { + registerFallbackValue(const Stream.empty()); + }); + + group('GenericHashValidations', () { + late MockGenericHash sutMock; + + setUp(() { + sutMock = MockGenericHash(); + }); + + testCheckInRange( + 'validateOutLen', + minSource: () => sutMock.bytesMin, + maxSource: () => sutMock.bytesMax, + sut: (value) => sutMock.validateOutLen(value), + ); + + testCheckInRange( + 'validateKey', + minSource: () => sutMock.keyBytesMin, + maxSource: () => sutMock.keyBytesMax, + sut: (value) => sutMock.validateKey(SecureKeyFake.empty(value)), + ); + + test('stream pipes messages into consumer', () async { + final mockConsumer = MockGenericHashConsumer(); + + const messageStream = Stream.empty(); + const outLen = 42; + final key = SecureKeyFake( + List.generate(15, (index) => index), + ); + final hash = Uint8List.fromList( + List.generate(5, (index) => 100 + index), + ); + + when(() => sutMock.createConsumer( + outLen: any(named: 'outLen'), + key: any(named: 'key'), + )).thenReturn(mockConsumer); + when(() => mockConsumer.addStream(any())) + .thenAnswer((i) async {}); + when(() => mockConsumer.close()).thenAnswer((i) async => hash); + + final res = await sutMock.stream( + messages: messageStream, + outLen: outLen, + key: key, + ); + + expect(res, hash); + verifyInOrder([ + () => sutMock.createConsumer( + outLen: outLen, + key: key, + ), + () => mockConsumer.addStream(messageStream), + () => mockConsumer.close(), + ]); + }); + }); +} diff --git a/packages/sodium/test/unit/ffi/api/crypto_ffi_test.dart b/packages/sodium/test/unit/ffi/api/crypto_ffi_test.dart index a2f00245..ef228c8b 100644 --- a/packages/sodium/test/unit/ffi/api/crypto_ffi_test.dart +++ b/packages/sodium/test/unit/ffi/api/crypto_ffi_test.dart @@ -2,6 +2,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:sodium/src/ffi/api/auth_ffi.dart'; import 'package:sodium/src/ffi/api/box_ffi.dart'; import 'package:sodium/src/ffi/api/crypto_ffi.dart'; +import 'package:sodium/src/ffi/api/generic_hash_ffi.dart'; import 'package:sodium/src/ffi/api/pwhash_ffi.dart'; import 'package:sodium/src/ffi/api/secret_box_ffi.dart'; import 'package:sodium/src/ffi/api/secret_stream_ffi.dart'; @@ -77,6 +78,17 @@ void main() { ); }); + test('genericHash returns GenericHashFFI instance', () { + expect( + sut.genericHash, + isA().having( + (p) => p.sodium, + 'sodium', + mockSodium, + ), + ); + }); + test('pwhash returns PwhashFFI instance', () { expect( sut.pwhash, diff --git a/packages/sodium/test/unit/ffi/api/generic_hash_ffi_test.dart b/packages/sodium/test/unit/ffi/api/generic_hash_ffi_test.dart new file mode 100644 index 00000000..a1ffccfb --- /dev/null +++ b/packages/sodium/test/unit/ffi/api/generic_hash_ffi_test.dart @@ -0,0 +1,343 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:mocktail/mocktail.dart'; +import 'package:sodium/src/api/sodium_exception.dart'; +import 'package:sodium/src/ffi/api/generic_hash_ffi.dart'; +import 'package:sodium/src/ffi/api/helpers/generic_hash/generic_hash_consumer_ffi.dart'; +import 'package:sodium/src/ffi/bindings/libsodium.ffi.dart'; +import 'package:test/test.dart'; +import 'package:tuple/tuple.dart'; + +import '../../../secure_key_fake.dart'; +import '../../../test_constants_mapping.dart'; +import '../keygen_test_helpers.dart'; +import '../pointer_test_helpers.dart'; + +class MockSodiumFFI extends Mock implements LibSodiumFFI {} + +void main() { + final mockSodium = MockSodiumFFI(); + + late GenericHashFFI sut; + + setUpAll(() { + registerPointers(); + registerFallbackValue>(nullptr); + }); + + setUp(() { + reset(mockSodium); + + mockAllocArray(mockSodium); + + sut = GenericHashFFI(mockSodium); + }); + + testConstantsMapping([ + Tuple3( + () => mockSodium.crypto_generichash_bytes(), + () => sut.bytes, + 'bytes', + ), + Tuple3( + () => mockSodium.crypto_generichash_bytes_min(), + () => sut.bytesMin, + 'bytesMin', + ), + Tuple3( + () => mockSodium.crypto_generichash_bytes_max(), + () => sut.bytesMax, + 'bytesMax', + ), + Tuple3( + () => mockSodium.crypto_generichash_keybytes(), + () => sut.keyBytes, + 'keyBytes', + ), + Tuple3( + () => mockSodium.crypto_generichash_keybytes_min(), + () => sut.keyBytesMin, + 'keyBytesMin', + ), + Tuple3( + () => mockSodium.crypto_generichash_keybytes_max(), + () => sut.keyBytesMax, + 'keyBytesMax', + ), + ]); + + group('methods', () { + setUp(() { + when(() => mockSodium.crypto_generichash_bytes_min()).thenReturn(5); + when(() => mockSodium.crypto_generichash_bytes_max()).thenReturn(5); + when(() => mockSodium.crypto_generichash_keybytes_min()).thenReturn(5); + when(() => mockSodium.crypto_generichash_keybytes_max()).thenReturn(5); + when(() => mockSodium.crypto_generichash_statebytes()).thenReturn(10); + }); + + testKeygen( + mockSodium: mockSodium, + runKeygen: () => sut.keygen(), + keyBytesNative: mockSodium.crypto_generichash_keybytes, + keygenNative: mockSodium.crypto_generichash_keygen, + ); + + group('call', () { + test('asserts if outLen is invalid', () { + expect( + () => sut( + message: Uint8List(0), + outLen: 10, + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_generichash_bytes_min()); + verify(() => mockSodium.crypto_generichash_bytes_max()); + }); + + test('asserts if key is invalid', () { + expect( + () => sut( + message: Uint8List(0), + key: SecureKeyFake.empty(10), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_generichash_keybytes_min()); + verify(() => mockSodium.crypto_generichash_keybytes_max()); + }); + + test('calls crypto_generichash with correct defaults', () { + const hashBytes = 15; + when(() => mockSodium.crypto_generichash_bytes()).thenReturn(hashBytes); + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + any(), + any(), + any(), + ), + ).thenReturn(0); + + final message = List.generate(20, (index) => index * 2); + + sut(message: Uint8List.fromList(message)); + + verifyInOrder([ + () => mockSodium.sodium_allocarray(hashBytes, 1), + () => mockSodium.sodium_mprotect_readonly( + any(that: hasRawData(message)), + ), + () => mockSodium.crypto_generichash( + any(that: isNot(nullptr)), + hashBytes, + any(that: hasRawData(message)), + message.length, + any(that: equals(nullptr)), + 0, + ), + ]); + }); + + test('calls crypto_generichash with all arguments', () { + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + any(), + any(), + any(), + ), + ).thenReturn(0); + + const outLen = 5; + final key = List.generate(5, (index) => index * 10); + final message = List.generate(20, (index) => index * 2); + + sut( + message: Uint8List.fromList(message), + outLen: outLen, + key: SecureKeyFake(key), + ); + + verifyInOrder([ + () => mockSodium.sodium_allocarray(outLen, 1), + () => mockSodium.sodium_mprotect_readonly( + any(that: hasRawData(message)), + ), + () => mockSodium.sodium_mprotect_readonly( + any(that: hasRawData(key)), + ), + () => mockSodium.crypto_generichash( + any(that: isNot(nullptr)), + outLen, + any(that: hasRawData(message)), + message.length, + any(that: hasRawData(key)), + key.length, + ), + ]); + }); + + test('returns calculated default hash', () { + final hash = List.generate(25, (index) => 10 + index); + when(() => mockSodium.crypto_generichash_bytes()) + .thenReturn(hash.length); + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + any(), + any(), + any(), + ), + ).thenAnswer((i) { + fillPointer(i.positionalArguments.first as Pointer, hash); + return 0; + }); + + final result = sut(message: Uint8List(10)); + + expect(result, hash); + + verify(() => mockSodium.sodium_free(any())).called(2); + }); + + test('returns calculated custom hash', () { + final hash = List.generate(5, (index) => 10 + index); + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + any(), + any(), + any(), + ), + ).thenAnswer((i) { + fillPointer(i.positionalArguments.first as Pointer, hash); + return 0; + }); + + final result = sut( + message: Uint8List(10), + outLen: hash.length, + key: SecureKeyFake.empty(5), + ); + + expect(result, hash); + + verify(() => mockSodium.sodium_free(any())).called(3); + }); + + test('throws exception on failure', () { + when(() => mockSodium.crypto_generichash_bytes()).thenReturn(10); + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + any(), + any(), + any(), + ), + ).thenReturn(1); + + expect( + () => sut( + message: Uint8List(15), + key: SecureKeyFake.empty(5), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.sodium_free(any())).called(3); + }); + }); + + group('createConsumer', () { + test('asserts if outLen is invalid', () { + expect( + () => sut.createConsumer( + outLen: 10, + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_generichash_bytes_min()); + verify(() => mockSodium.crypto_generichash_bytes_max()); + }); + + test('asserts if key is invalid', () { + expect( + () => sut.createConsumer( + key: SecureKeyFake.empty(10), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_generichash_keybytes_min()); + verify(() => mockSodium.crypto_generichash_keybytes_max()); + }); + + test('returns GenericHashConsumerFFI with defaults', () { + const outLen = 55; + when(() => mockSodium.crypto_generichash_bytes()).thenReturn(outLen); + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + any(), + any(), + )).thenReturn(0); + + final result = sut.createConsumer(); + + expect( + result, + isA() + .having((c) => c.sodium, 'sodium', mockSodium) + .having( + (c) => c.outLen, + 'outLen', + outLen, + ), + ); + }); + + test('returns GenericHashConsumerFFI with key', () { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + any(), + any(), + )).thenReturn(0); + + const outLen = 5; + final secretKey = List.generate(5, (index) => index * index); + + final result = sut.createConsumer( + outLen: outLen, + key: SecureKeyFake(secretKey), + ); + + expect( + result, + isA() + .having((c) => c.sodium, 'sodium', mockSodium) + .having( + (c) => c.outLen, + 'outLen', + outLen, + ), + ); + }); + }); + }); +} diff --git a/packages/sodium/test/unit/ffi/api/helpers/generic_hash/generic_hash_consumer_ffi_test.dart b/packages/sodium/test/unit/ffi/api/helpers/generic_hash/generic_hash_consumer_ffi_test.dart new file mode 100644 index 00000000..73619f63 --- /dev/null +++ b/packages/sodium/test/unit/ffi/api/helpers/generic_hash/generic_hash_consumer_ffi_test.dart @@ -0,0 +1,281 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:mocktail/mocktail.dart'; +import 'package:sodium/src/api/sodium_exception.dart'; +import 'package:sodium/src/ffi/api/helpers/generic_hash/generic_hash_consumer_ffi.dart'; +import 'package:sodium/src/ffi/bindings/libsodium.ffi.dart'; +import 'package:test/test.dart'; + +import '../../../../../secure_key_fake.dart'; +import '../../../pointer_test_helpers.dart'; + +class MockSodiumFFI extends Mock implements LibSodiumFFI {} + +void main() { + const outLen = 42; + + final mockSodium = MockSodiumFFI(); + + setUpAll(() { + registerPointers(); + registerFallbackValue>(nullptr); + }); + + setUp(() { + reset(mockSodium); + + mockAllocArray(mockSodium); + + when(() => mockSodium.crypto_generichash_statebytes()).thenReturn(5); + when(() => mockSodium.crypto_generichash_keybytes()).thenReturn(15); + }); + + group('constructor', () { + test('initializes hash state', () { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + any(), + any(), + )).thenReturn(0); + + GenericHashConsumerFFI( + sodium: mockSodium, + outLen: outLen, + ); + + verifyInOrder([ + () => mockSodium.crypto_generichash_statebytes(), + () => mockSodium.sodium_allocarray(5, 1), + () => mockSodium.sodium_memzero(any(that: isNot(nullptr)), 5), + () => mockSodium.crypto_generichash_init( + any(that: isNot(nullptr)), + any(that: equals(nullptr)), + 0, + outLen, + ), + ]); + }); + + test('initializes hash state with key', () { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + any(), + any(), + )).thenReturn(0); + + final key = List.generate(15, (index) => index + 5); + + GenericHashConsumerFFI( + sodium: mockSodium, + outLen: outLen, + key: SecureKeyFake(key), + ); + + verifyInOrder([ + () => mockSodium.crypto_generichash_statebytes(), + () => mockSodium.sodium_allocarray(5, 1), + () => mockSodium.sodium_memzero(any(that: isNot(nullptr)), 5), + () => mockSodium.sodium_mprotect_readonly(any(that: hasRawData(key))), + () => mockSodium.crypto_generichash_init( + any(that: isNot(nullptr)), + any(that: hasRawData(key)), + key.length, + outLen, + ), + ]); + }); + + test('disposes sign state on error', () { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + any(), + any(), + )).thenReturn(1); + + expect( + () => GenericHashConsumerFFI( + sodium: mockSodium, + outLen: outLen, + ), + throwsA(isA()), + ); + + verifyInOrder([ + () => mockSodium.crypto_generichash_statebytes(), + () => mockSodium.sodium_allocarray(5, 1), + () => mockSodium.sodium_memzero(any(that: isNot(nullptr)), 5), + () => mockSodium.crypto_generichash_init( + any(that: isNot(nullptr)), + any(that: equals(nullptr)), + 0, + outLen, + ), + () => mockSodium.sodium_free(any(that: isNot(nullptr))), + ]); + }); + }); + + group('members', () { + late GenericHashConsumerFFI sut; + + setUp(() { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + any(), + any(), + )).thenReturn(0); + + sut = GenericHashConsumerFFI( + sodium: mockSodium, + outLen: outLen, + ); + + clearInteractions(mockSodium); + }); + + group('addStream', () { + test('calls crypto_generichash_update on stream events', () async { + when(() => mockSodium.crypto_generichash_update(any(), any(), any())) + .thenReturn(0); + + final message = List.generate(25, (index) => index * 3); + + await sut.addStream(Stream.value(Uint8List.fromList(message))); + + verifyInOrder([ + () => mockSodium.sodium_mprotect_readonly( + any(that: hasRawData(message)), + ), + () => mockSodium.crypto_generichash_update( + any(that: isNot(nullptr)), + any(that: hasRawData(message)), + message.length, + ), + () => mockSodium.sodium_free( + any(that: hasRawData(message)), + ), + ]); + }); + + test('throws exception and cancels addStream on error', () async { + when(() => mockSodium.crypto_generichash_update(any(), any(), any())) + .thenReturn(1); + + final message = List.generate(25, (index) => index * 3); + + await expectLater( + () => sut.addStream(Stream.value(Uint8List.fromList(message))), + throwsA(isA()), + ); + + verify( + () => mockSodium.sodium_free( + any(that: hasRawData(message)), + ), + ); + }); + + test('throws StateError when adding a stream after completition', + () async { + when(() => mockSodium.crypto_generichash_final(any(), any(), any())) + .thenReturn(0); + + await sut.close(); + + expect( + () => sut.addStream(const Stream.empty()), + throwsA(isA()), + ); + }); + }); + + group('close', () { + test('calls crypto_generichash_final with correct arguments', () async { + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + any(), + )).thenReturn(0); + + await sut.close(); + + verifyInOrder([ + () => mockSodium.sodium_allocarray(outLen, 1), + () => mockSodium.sodium_memzero(any(that: isNot(nullptr)), outLen), + () => mockSodium.crypto_generichash_final( + any(that: isNot(nullptr)), + any(that: isNot(nullptr)), + outLen, + ), + ]); + }); + + test('returns hash on success', () async { + final hash = List.generate(outLen, (index) => index * 12); + + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + any(), + )).thenAnswer((i) { + fillPointer(i.positionalArguments[1] as Pointer, hash); + return 0; + }); + + final result = await sut.close(); + + expect(result, Uint8List.fromList(hash)); + verify(() => mockSodium.sodium_free(any())).called(2); + }); + + test('throws exception if hashing fails', () async { + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + any(), + )).thenReturn(1); + + await expectLater( + () => sut.close(), + throwsA(isA()), + ); + + verify(() => mockSodium.sodium_free(any())).called(2); + }); + + test('throws state error if close is called a second time', () async { + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + any(), + )).thenReturn(0); + + await sut.close(); + + await expectLater( + () => sut.close(), + throwsA(isA()), + ); + }); + + test('returns same future as hash', () async { + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + any(), + )).thenReturn(0); + + final hash = sut.hash; + final closed = sut.close(); + + expect(hash, closed); + expect(await hash, await closed); + }); + }); + }); +} diff --git a/packages/sodium/test/unit/ffi/api/helpers/sign/sign_consumer_ffi_mixin_test_helpers.dart b/packages/sodium/test/unit/ffi/api/helpers/sign/sign_consumer_ffi_mixin_test_helpers.dart index 7d7365d9..d6cabd34 100644 --- a/packages/sodium/test/unit/ffi/api/helpers/sign/sign_consumer_ffi_mixin_test_helpers.dart +++ b/packages/sodium/test/unit/ffi/api/helpers/sign/sign_consumer_ffi_mixin_test_helpers.dart @@ -97,6 +97,12 @@ void addStreamTests({ () => sut.addStream(Stream.value(Uint8List.fromList(message))), throwsA(isA()), ); + + verify( + () => mockSodium.sodium_free( + any(that: hasRawData(message)), + ), + ); }); test('throws StateError when adding a stream after completition', () async { diff --git a/packages/sodium/test/unit/js/api/crypto_js_test.dart b/packages/sodium/test/unit/js/api/crypto_js_test.dart index 22504114..6fb059e5 100644 --- a/packages/sodium/test/unit/js/api/crypto_js_test.dart +++ b/packages/sodium/test/unit/js/api/crypto_js_test.dart @@ -2,6 +2,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:sodium/src/js/api/auth_js.dart'; import 'package:sodium/src/js/api/box_js.dart'; import 'package:sodium/src/js/api/crypto_js.dart'; +import 'package:sodium/src/js/api/generic_hash_js.dart'; import 'package:sodium/src/js/api/pwhash_js.dart'; import 'package:sodium/src/js/api/secret_box_js.dart'; import 'package:sodium/src/js/api/secret_stream_js.dart'; @@ -77,6 +78,17 @@ void main() { ); }); + test('genericHash returns GenericHashJS instance', () { + expect( + sut.genericHash, + isA().having( + (p) => p.sodium, + 'sodium', + mockSodium, + ), + ); + }); + test('pwhash returns PwhashJS instance', () { expect( sut.pwhash, diff --git a/packages/sodium/test/unit/js/api/generic_hash_js_test.dart b/packages/sodium/test/unit/js/api/generic_hash_js_test.dart new file mode 100644 index 00000000..bed949ec --- /dev/null +++ b/packages/sodium/test/unit/js/api/generic_hash_js_test.dart @@ -0,0 +1,269 @@ +import 'dart:typed_data'; + +import 'package:mocktail/mocktail.dart'; +import 'package:sodium/src/api/sodium_exception.dart'; +import 'package:sodium/src/js/api/generic_hash_js.dart'; +import 'package:sodium/src/js/api/helpers/generic_hash/generic_hash_consumer_js.dart'; +import 'package:sodium/src/js/bindings/js_error.dart'; +import 'package:sodium/src/js/bindings/sodium.js.dart'; +import 'package:test/test.dart'; +import 'package:tuple/tuple.dart'; + +import '../../../secure_key_fake.dart'; +import '../../../test_constants_mapping.dart'; +import '../keygen_test_helpers.dart'; + +class MockLibSodiumJS extends Mock implements LibSodiumJS {} + +void main() { + final mockSodium = MockLibSodiumJS(); + + late GenericHashJS sut; + + setUpAll(() { + registerFallbackValue(Uint8List(0)); + }); + + setUp(() { + reset(mockSodium); + + sut = GenericHashJS(mockSodium); + }); + + testConstantsMapping([ + Tuple3( + () => mockSodium.crypto_generichash_BYTES, + () => sut.bytes, + 'bytes', + ), + Tuple3( + () => mockSodium.crypto_generichash_BYTES_MIN, + () => sut.bytesMin, + 'bytesMin', + ), + Tuple3( + () => mockSodium.crypto_generichash_BYTES_MAX, + () => sut.bytesMax, + 'bytesMax', + ), + Tuple3( + () => mockSodium.crypto_generichash_KEYBYTES, + () => sut.keyBytes, + 'keyBytes', + ), + Tuple3( + () => mockSodium.crypto_generichash_KEYBYTES_MIN, + () => sut.keyBytesMin, + 'keyBytesMin', + ), + Tuple3( + () => mockSodium.crypto_generichash_KEYBYTES_MAX, + () => sut.keyBytesMax, + 'keyBytesMax', + ), + ]); + + group('methods', () { + setUp(() { + when(() => mockSodium.crypto_generichash_BYTES_MIN).thenReturn(5); + when(() => mockSodium.crypto_generichash_BYTES_MAX).thenReturn(5); + when(() => mockSodium.crypto_generichash_KEYBYTES_MIN).thenReturn(5); + when(() => mockSodium.crypto_generichash_KEYBYTES_MAX).thenReturn(5); + }); + + testKeygen( + mockSodium: mockSodium, + runKeygen: () => sut.keygen(), + keygenNative: mockSodium.crypto_generichash_keygen, + ); + + group('call', () { + test('asserts if outLen is invalid', () { + expect( + () => sut( + message: Uint8List(0), + outLen: 10, + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_generichash_BYTES_MIN); + verify(() => mockSodium.crypto_generichash_BYTES_MAX); + }); + + test('asserts if key is invalid', () { + expect( + () => sut( + message: Uint8List(0), + key: SecureKeyFake.empty(10), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_generichash_KEYBYTES_MIN); + verify(() => mockSodium.crypto_generichash_KEYBYTES_MAX); + }); + + test('calls crypto_generichash with correct defaults', () { + const hashBytes = 15; + when(() => mockSodium.crypto_generichash_BYTES).thenReturn(hashBytes); + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + ), + ).thenReturn(Uint8List(0)); + + final message = List.generate(20, (index) => index * 2); + + sut(message: Uint8List.fromList(message)); + + verify( + () => mockSodium.crypto_generichash( + hashBytes, + Uint8List.fromList(message), + null, + ), + ); + }); + + test('calls crypto_generichash with all arguments', () { + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + ), + ).thenReturn(Uint8List(0)); + + const outLen = 5; + final key = List.generate(5, (index) => index * 10); + final message = List.generate(20, (index) => index * 2); + + sut( + message: Uint8List.fromList(message), + outLen: outLen, + key: SecureKeyFake(key), + ); + + verify( + () => mockSodium.crypto_generichash( + outLen, + Uint8List.fromList(message), + Uint8List.fromList(key), + ), + ); + }); + + test('returns calculated hash', () { + final hash = List.generate(25, (index) => 10 + index); + when(() => mockSodium.crypto_generichash_BYTES).thenReturn(hash.length); + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + ), + ).thenReturn(Uint8List.fromList(hash)); + + final result = sut(message: Uint8List(10)); + + expect(result, hash); + }); + + test('throws exception on failure', () { + when(() => mockSodium.crypto_generichash_BYTES).thenReturn(10); + when( + () => mockSodium.crypto_generichash( + any(), + any(), + any(), + ), + ).thenThrow(JsError()); + + expect( + () => sut( + message: Uint8List(15), + key: SecureKeyFake.empty(5), + ), + throwsA(isA()), + ); + }); + }); + + group('createConsumer', () { + test('asserts if outLen is invalid', () { + expect( + () => sut.createConsumer( + outLen: 10, + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_generichash_BYTES_MIN); + verify(() => mockSodium.crypto_generichash_BYTES_MAX); + }); + + test('asserts if key is invalid', () { + expect( + () => sut.createConsumer( + key: SecureKeyFake.empty(10), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_generichash_KEYBYTES_MIN); + verify(() => mockSodium.crypto_generichash_KEYBYTES_MAX); + }); + + test('returns GenericHashConsumerJS with defaults', () { + const outLen = 55; + when(() => mockSodium.crypto_generichash_BYTES).thenReturn(outLen); + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + )).thenReturn(0); + + final result = sut.createConsumer(); + + expect( + result, + isA() + .having((c) => c.sodium, 'sodium', mockSodium) + .having( + (c) => c.outLen, + 'outLen', + outLen, + ), + ); + }); + + test('returns GenericHashConsumerJS with key', () { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + )).thenReturn(0); + + const outLen = 5; + final secretKey = List.generate(5, (index) => index * index); + + final result = sut.createConsumer( + outLen: outLen, + key: SecureKeyFake(secretKey), + ); + + expect( + result, + isA() + .having((c) => c.sodium, 'sodium', mockSodium) + .having( + (c) => c.outLen, + 'outLen', + outLen, + ), + ); + }); + }); + }); +} diff --git a/packages/sodium/test/unit/js/api/helpers/generic_hash/generic_hash_consumer_js_test.dart b/packages/sodium/test/unit/js/api/helpers/generic_hash/generic_hash_consumer_js_test.dart new file mode 100644 index 00000000..d74c0470 --- /dev/null +++ b/packages/sodium/test/unit/js/api/helpers/generic_hash/generic_hash_consumer_js_test.dart @@ -0,0 +1,215 @@ +import 'dart:typed_data'; + +import 'package:mocktail/mocktail.dart'; +import 'package:sodium/src/api/sodium_exception.dart'; +import 'package:sodium/src/js/api/helpers/generic_hash/generic_hash_consumer_js.dart'; +import 'package:sodium/src/js/bindings/js_error.dart'; +import 'package:sodium/src/js/bindings/sodium.js.dart'; +import 'package:test/test.dart'; + +import '../../../../../secure_key_fake.dart'; + +class MockSodiumJS extends Mock implements LibSodiumJS {} + +void main() { + const state = 234; + const outLen = 42; + + final mockSodium = MockSodiumJS(); + + setUpAll(() { + registerFallbackValue(Uint8List(0)); + }); + + setUp(() { + reset(mockSodium); + + when(() => mockSodium.crypto_generichash_KEYBYTES).thenReturn(15); + }); + + group('constructor', () { + test('initializes hash state', () { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + )).thenReturn(state); + + GenericHashConsumerJS( + sodium: mockSodium, + outLen: outLen, + ); + + verify( + () => mockSodium.crypto_generichash_init( + null, + outLen, + ), + ); + }); + + test('initializes hash state with key', () { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + )).thenReturn(state); + + final key = List.generate(15, (index) => index + 5); + + GenericHashConsumerJS( + sodium: mockSodium, + outLen: outLen, + key: SecureKeyFake(key), + ); + + verify( + () => mockSodium.crypto_generichash_init( + Uint8List.fromList(key), + outLen, + ), + ); + }); + + test('throws SodiumException on error', () { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + )).thenThrow(JsError()); + + expect( + () => GenericHashConsumerJS( + sodium: mockSodium, + outLen: outLen, + ), + throwsA(isA()), + ); + }); + }); + + group('members', () { + late GenericHashConsumerJS sut; + + setUp(() { + when(() => mockSodium.crypto_generichash_init( + any(), + any(), + )).thenReturn(state); + + sut = GenericHashConsumerJS( + sodium: mockSodium, + outLen: outLen, + ); + + clearInteractions(mockSodium); + }); + + group('addStream', () { + test('calls crypto_generichash_update on stream events', () async { + final message = List.generate(25, (index) => index * 3); + + await sut.addStream(Stream.value(Uint8List.fromList(message))); + + verify( + () => mockSodium.crypto_generichash_update( + state, + Uint8List.fromList(message), + ), + ); + }); + + test('throws exception and cancels addStream on error', () async { + when(() => mockSodium.crypto_generichash_update(any(), any())) + .thenThrow(JsError()); + + final message = List.generate(25, (index) => index * 3); + + await expectLater( + () => sut.addStream(Stream.value(Uint8List.fromList(message))), + throwsA(isA()), + ); + }); + + test('throws StateError when adding a stream after completition', + () async { + when(() => mockSodium.crypto_generichash_final(any(), any())) + .thenReturn(Uint8List(0)); + + await sut.close(); + + expect( + () => sut.addStream(const Stream.empty()), + throwsA(isA()), + ); + }); + }); + + group('close', () { + test('calls crypto_generichash_final with correct arguments', () async { + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + )).thenReturn(Uint8List(0)); + + await sut.close(); + + verify( + () => mockSodium.crypto_generichash_final( + state, + outLen, + ), + ); + }); + + test('returns hash on success', () async { + final hash = List.generate(outLen, (index) => index * 12); + + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + )).thenReturn(Uint8List.fromList(hash)); + + final result = await sut.close(); + + expect(result, Uint8List.fromList(hash)); + }); + + test('throws exception if hashing fails', () async { + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + )).thenThrow(JsError()); + + await expectLater( + () => sut.close(), + throwsA(isA()), + ); + }); + + test('throws state error if close is called a second time', () async { + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + )).thenReturn(Uint8List(0)); + + await sut.close(); + + await expectLater( + () => sut.close(), + throwsA(isA()), + ); + }); + + test('returns same future as hash', () async { + when(() => mockSodium.crypto_generichash_final( + any(), + any(), + )).thenReturn(Uint8List(0)); + + final hash = sut.hash; + final closed = sut.close(); + + expect(hash, closed); + expect(await hash, await closed); + }); + }); + }); +}