diff --git a/README.md b/README.md index 5d9f1f7d..5ebf319c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # libsodium_dart_bindings +[![Continous Integration for package sodium](https://github.com/Skycoder42/libsodium_dart_bindings/actions/workflows/sodium_ci.yaml/badge.svg)](https://github.com/Skycoder42/libsodium_dart_bindings/actions/workflows/sodium_ci.yaml) +[![Pub Version](https://img.shields.io/pub/v/sodium)](https://pub.dev/packages/sodium) + This repository is a multi package repository for dart bindings of [libsodium](https://libsodium.gitbook.io/doc/). It consists of the following packages. Please check the READMEs of the specific packages for more details on @@ -8,5 +11,5 @@ If you just landed here and don't know where to start, simply read the [sodium README](packages/sodium), as that is the primary package of this repository. -- [sodium](packages/sodium): Dart bindings for libsodium, supporting both the -VM and JS without flutter dependencies. \ No newline at end of file +- **[sodium](packages/sodium)**: Dart bindings for libsodium, supporting both the VM and JS without flutter +dependencies. \ No newline at end of file diff --git a/packages/sodium/CHANGELOG.md b/packages/sodium/CHANGELOG.md index 36524b6a..a5a11fdc 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.4] - 2021-05-21 +### Added +- New libsodium API: crypto_box_seal + ## [0.1.3] - 2021-05-21 ### Added - New libsodium APIs: diff --git a/packages/sodium/README.md b/packages/sodium/README.md index 15c62e17..1d2efb27 100644 --- a/packages/sodium/README.md +++ b/packages/sodium/README.md @@ -50,7 +50,7 @@ API based on libsodium version: *1.0.18* crypto_auth | ✔️ | ✔️ | https://libsodium.gitbook.io/doc/secret-key_cryptography/secret-key_authentication 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_box_seal | ✔️ | ✔️ | https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes 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 diff --git a/packages/sodium/lib/src/api/box.dart b/packages/sodium/lib/src/api/box.dart index 7e1788c5..7e8aec31 100644 --- a/packages/sodium/lib/src/api/box.dart +++ b/packages/sodium/lib/src/api/box.dart @@ -10,7 +10,8 @@ import 'secure_key.dart'; /// A meta class that provides access to all libsodium box APIs. /// /// This class provides the dart interface for the crypto operations documented -/// in https://libsodium.gitbook.io/doc/public-key_cryptography/authenticated_encryption. +/// in https://libsodium.gitbook.io/doc/public-key_cryptography/authenticated_encryption +/// and https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes. /// Please refer to that documentation for more details about these APIs. abstract class Box { const Box._(); // coverage:ignore-line @@ -40,6 +41,11 @@ abstract class Box { /// See https://libsodium.gitbook.io/doc/public-key_cryptography/authenticated_encryption#constants int get seedBytes; + /// Provides crypto_box_SEALBYTES. + /// + /// See https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes#constants + int get sealBytes; + /// Provides crypto_box_keypair. /// /// See https://libsodium.gitbook.io/doc/public-key_cryptography/authenticated_encryption#key-pair-generation @@ -90,6 +96,23 @@ abstract class Box { required Uint8List senderPublicKey, required SecureKey recipientSecretKey, }); + + /// Provides crypto_box_seal. + /// + /// See https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes#usage + Uint8List seal({ + required Uint8List message, + required Uint8List recipientPublicKey, + }); + + /// Provides crypto_box_seal_open. + /// + /// See https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes#usage + Uint8List sealOpen({ + required Uint8List cipherText, + required Uint8List recipientPublicKey, + required SecureKey recipientSecretKey, + }); } @internal @@ -129,4 +152,10 @@ mixin BoxValidations implements Box { macBytes, 'cipherText', ); + + void validateSealCipherText(Uint8List cipherText) => Validations.checkAtLeast( + cipherText.length, + sealBytes, + 'cipherText', + ); } diff --git a/packages/sodium/lib/src/ffi/api/box_ffi.dart b/packages/sodium/lib/src/ffi/api/box_ffi.dart index 3a167c95..98f7c0ed 100644 --- a/packages/sodium/lib/src/ffi/api/box_ffi.dart +++ b/packages/sodium/lib/src/ffi/api/box_ffi.dart @@ -35,6 +35,9 @@ class BoxFFI with BoxValidations implements Box { @override int get seedBytes => sodium.crypto_box_seedbytes(); + @override + int get sealBytes => sodium.crypto_box_sealbytes(); + @override KeyPair keyPair() { SecureKeyFFI? secretKey; @@ -304,4 +307,78 @@ class BoxFFI with BoxValidations implements Box { publicKeyPtr?.dispose(); } } + + @override + Uint8List seal({ + required Uint8List message, + required Uint8List recipientPublicKey, + }) { + validatePublicKey(recipientPublicKey); + + SodiumPointer? dataPtr; + SodiumPointer? publicKeyPtr; + try { + dataPtr = SodiumPointer.alloc( + sodium, + count: message.length + sealBytes, + ) + ..fill(List.filled(sealBytes, 0)) + ..fill(message, offset: sealBytes); + publicKeyPtr = recipientPublicKey.toSodiumPointer( + sodium, + memoryProtection: MemoryProtection.readOnly, + ); + + final result = sodium.crypto_box_seal( + dataPtr.ptr, + dataPtr.viewAt(sealBytes).ptr, + message.length, + publicKeyPtr.ptr, + ); + SodiumException.checkSucceededInt(result); + + return dataPtr.copyAsList(); + } finally { + dataPtr?.dispose(); + publicKeyPtr?.dispose(); + } + } + + @override + Uint8List sealOpen({ + required Uint8List cipherText, + required Uint8List recipientPublicKey, + required SecureKey recipientSecretKey, + }) { + validateSealCipherText(cipherText); + validatePublicKey(recipientPublicKey); + validateSecretKey(recipientSecretKey); + + SodiumPointer? dataPtr; + SodiumPointer? publicKeyPtr; + try { + dataPtr = cipherText.toSodiumPointer(sodium); + publicKeyPtr = recipientPublicKey.toSodiumPointer( + sodium, + memoryProtection: MemoryProtection.readOnly, + ); + + final result = recipientSecretKey.runUnlockedNative( + sodium, + (secretKeyPtr) => sodium.crypto_box_seal_open( + dataPtr!.viewAt(sealBytes).ptr, + dataPtr.ptr, + dataPtr.count, + publicKeyPtr!.ptr, + secretKeyPtr.ptr, + ), + ); + SodiumException.checkSucceededInt(result); + + return dataPtr.viewAt(sealBytes).copyAsList(); + } finally { + dataPtr?.dispose(); + publicKeyPtr?.dispose(); + } + } } diff --git a/packages/sodium/lib/src/js/api/box_js.dart b/packages/sodium/lib/src/js/api/box_js.dart index ed6fa3c5..8826bcb0 100644 --- a/packages/sodium/lib/src/js/api/box_js.dart +++ b/packages/sodium/lib/src/js/api/box_js.dart @@ -29,6 +29,9 @@ class BoxJS with BoxValidations implements Box { @override int get seedBytes => sodium.crypto_box_SEEDBYTES.toSafeUInt32(); + @override + int get sealBytes => sodium.crypto_box_SEALBYTES.toSafeUInt32(); + @override KeyPair keyPair() { final keyPair = JsError.wrap(() => sodium.crypto_box_keypair()); @@ -155,4 +158,40 @@ class BoxJS with BoxValidations implements Box { ), ); } + + @override + Uint8List seal({ + required Uint8List message, + required Uint8List recipientPublicKey, + }) { + validatePublicKey(recipientPublicKey); + + return JsError.wrap( + () => sodium.crypto_box_seal( + message, + recipientPublicKey, + ), + ); + } + + @override + Uint8List sealOpen({ + required Uint8List cipherText, + required Uint8List recipientPublicKey, + required SecureKey recipientSecretKey, + }) { + validateSealCipherText(cipherText); + validatePublicKey(recipientPublicKey); + validateSecretKey(recipientSecretKey); + + return JsError.wrap( + () => recipientSecretKey.runUnlockedSync( + (secretKeyData) => sodium.crypto_box_seal_open( + cipherText, + recipientPublicKey, + secretKeyData, + ), + ), + ); + } } diff --git a/packages/sodium/pubspec.yaml b/packages/sodium/pubspec.yaml index 376a90b5..818dbda5 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.3 +version: 0.1.4 homepage: https://github.com/Skycoder42/libsodium_dart_bindings environment: @@ -13,13 +13,13 @@ dependencies: meta: ^1.3.0 dev_dependencies: - build_runner: ^2.0.3 + build_runner: ^2.0.4 coverage: ^1.0.2 dart_pre_commit: ^2.3.1 ffigen: ^3.0.0 freezed: ^0.14.2 lint: ^1.5.3 - mocktail: ^0.1.2 + mocktail: ^0.1.3 path: ^1.8.0 test: ^1.17.4 tuple: ^2.0.0 diff --git a/packages/sodium/test/integration/cases/box_test_case.dart b/packages/sodium/test/integration/cases/box_test_case.dart index 680940eb..ecb7e264 100644 --- a/packages/sodium/test/integration/cases/box_test_case.dart +++ b/packages/sodium/test/integration/cases/box_test_case.dart @@ -20,6 +20,7 @@ class BoxTestCase extends TestCase { expect(sut.macBytes, 16, reason: 'macBytes'); expect(sut.nonceBytes, 24, reason: 'nonceBytes'); expect(sut.seedBytes, 32, reason: 'seedBytes'); + expect(sut.sealBytes, 48, reason: 'sealBytes'); }); test('keyPair generates different correct length keys', () { @@ -245,5 +246,63 @@ class BoxTestCase extends TestCase { ); }); }); + + group('seal', () { + test('can encrypt and decrypt data', () { + final recipientKey = sut.keyPair(); + final message = Uint8List.fromList( + List.generate(32, (index) => index * 2), + ); + + printOnFailure( + 'recipientKey.secretKey: ${recipientKey.secretKey.extractBytes()}', + ); + printOnFailure('recipientKey.publicKey: ${recipientKey.publicKey}'); + printOnFailure('message: $message'); + + final cipherText = sut.seal( + message: message, + recipientPublicKey: recipientKey.publicKey, + ); + + printOnFailure('cipherText: $cipherText'); + + final restored = sut.sealOpen( + cipherText: cipherText, + recipientPublicKey: recipientKey.publicKey, + recipientSecretKey: recipientKey.secretKey, + ); + + printOnFailure('restored: $restored'); + + expect(restored, message); + }); + + test('fails if data is invalid', () { + final recipientKey = sut.keyPair(); + + printOnFailure( + 'recipientKey.secretKey: ${recipientKey.secretKey.extractBytes()}', + ); + printOnFailure('recipientKey.publicKey: ${recipientKey.publicKey}'); + + final cipherText = sut.seal( + message: Uint8List.fromList(const [1, 2, 3]), + recipientPublicKey: recipientKey.publicKey, + ); + + printOnFailure('cipherText: $cipherText'); + cipherText[0] = cipherText[0] ^ 0xFF; + + expect( + () => sut.sealOpen( + cipherText: cipherText, + recipientPublicKey: recipientKey.publicKey, + recipientSecretKey: recipientKey.secretKey, + ), + throwsA(isA()), + ); + }); + }); } } diff --git a/packages/sodium/test/unit/api/box_test.dart b/packages/sodium/test/unit/api/box_test.dart index 9573a240..96ce6087 100644 --- a/packages/sodium/test/unit/api/box_test.dart +++ b/packages/sodium/test/unit/api/box_test.dart @@ -52,5 +52,11 @@ void main() { source: () => sutMock.macBytes, sut: (value) => sutMock.validateEasyCipherText(Uint8List(value)), ); + + testCheckAtLeast( + 'validateSealCipherText', + source: () => sutMock.sealBytes, + sut: (value) => sutMock.validateSealCipherText(Uint8List(value)), + ); }); } diff --git a/packages/sodium/test/unit/ffi/api/box_ffi_test.dart b/packages/sodium/test/unit/ffi/api/box_ffi_test.dart index 176eb843..c314c309 100644 --- a/packages/sodium/test/unit/ffi/api/box_ffi_test.dart +++ b/packages/sodium/test/unit/ffi/api/box_ffi_test.dart @@ -58,6 +58,11 @@ void main() { () => sut.seedBytes, 'seedBytes', ), + Tuple3( + () => mockSodium.crypto_box_sealbytes(), + () => sut.sealBytes, + 'sealBytes', + ), ]); group('methods', () { @@ -67,6 +72,7 @@ void main() { when(() => mockSodium.crypto_box_macbytes()).thenReturn(5); when(() => mockSodium.crypto_box_noncebytes()).thenReturn(5); when(() => mockSodium.crypto_box_seedbytes()).thenReturn(5); + when(() => mockSodium.crypto_box_sealbytes()).thenReturn(5); }); group('keypair', () { @@ -358,7 +364,7 @@ void main() { verify(() => mockSodium.crypto_box_noncebytes()); }); - test('asserts if recipientPublicKey is invalid', () { + test('asserts if senderPublicKey is invalid', () { expect( () => sut.openEasy( cipherText: Uint8List(20), @@ -372,7 +378,7 @@ void main() { verify(() => mockSodium.crypto_box_publickeybytes()); }); - test('asserts if senderSecretKey is invalid', () { + test('asserts if recipientSecretKey is invalid', () { expect( () => sut.openEasy( cipherText: Uint8List(20), @@ -670,7 +676,7 @@ void main() { verify(() => mockSodium.crypto_box_noncebytes()); }); - test('asserts if recipientPublicKey is invalid', () { + test('asserts if senderPublicKey is invalid', () { expect( () => sut.openDetached( cipherText: Uint8List(10), @@ -685,7 +691,7 @@ void main() { verify(() => mockSodium.crypto_box_publickeybytes()); }); - test('asserts if senderSecretKey is invalid', () { + test('asserts if recipientSecretKey is invalid', () { expect( () => sut.openDetached( cipherText: Uint8List(10), @@ -809,5 +815,224 @@ void main() { verify(() => mockSodium.sodium_free(any())).called(5); }); }); + + group('seal', () { + test('asserts if recipientPublicKey is invalid', () { + expect( + () => sut.seal( + message: Uint8List(20), + recipientPublicKey: Uint8List(10), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_box_publickeybytes()); + }); + + test('calls crypto_box_seal with correct arguments', () { + when( + () => mockSodium.crypto_box_seal( + any(), + any(), + any(), + any(), + ), + ).thenReturn(0); + + final message = List.generate(20, (index) => index * 2); + final recipientPublicKey = List.generate(5, (index) => 20 + index); + final seal = List.filled(5, 0); + + sut.seal( + message: Uint8List.fromList(message), + recipientPublicKey: Uint8List.fromList(recipientPublicKey), + ); + + verifyInOrder([ + () => mockSodium.sodium_mprotect_readonly( + any(that: hasRawData(recipientPublicKey)), + ), + () => mockSodium.crypto_box_seal( + any(that: hasRawData(seal + message)), + any(that: hasRawData(message)), + message.length, + any(that: hasRawData(recipientPublicKey)), + ), + ]); + }); + + test('returns sealed data', () { + final cipher = List.generate(25, (index) => 100 - index); + when( + () => mockSodium.crypto_box_seal( + any(), + any(), + any(), + any(), + ), + ).thenAnswer((i) { + fillPointer(i.positionalArguments.first as Pointer, cipher); + return 0; + }); + + final result = sut.seal( + message: Uint8List(20), + recipientPublicKey: Uint8List(5), + ); + + expect(result, cipher); + + verify(() => mockSodium.sodium_free(any())).called(2); + }); + + test('throws exception on failure', () { + when( + () => mockSodium.crypto_box_seal( + any(), + any(), + any(), + any(), + ), + ).thenReturn(1); + + expect( + () => sut.seal( + message: Uint8List(10), + recipientPublicKey: Uint8List(5), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.sodium_free(any())).called(2); + }); + }); + + group('sealOpen', () { + test('asserts if cipherText is invalid', () { + expect( + () => sut.sealOpen( + cipherText: Uint8List(3), + recipientPublicKey: Uint8List(5), + recipientSecretKey: SecureKeyFake.empty(5), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_box_sealbytes()); + }); + + test('asserts if recipientPublicKey is invalid', () { + expect( + () => sut.sealOpen( + cipherText: Uint8List(20), + recipientPublicKey: Uint8List(10), + recipientSecretKey: SecureKeyFake.empty(5), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_box_publickeybytes()); + }); + + test('asserts if recipientSecretKey is invalid', () { + expect( + () => sut.sealOpen( + cipherText: Uint8List(20), + recipientPublicKey: Uint8List(5), + recipientSecretKey: SecureKeyFake.empty(10), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_box_secretkeybytes()); + }); + + test('calls crypto_box_seal_open with correct arguments', () { + when( + () => mockSodium.crypto_box_seal_open( + any(), + any(), + any(), + any(), + any(), + ), + ).thenReturn(0); + + final cipherText = List.generate(20, (index) => index * 2); + final recipientPublicKey = List.generate(5, (index) => 20 + index); + final recipientSecretKey = List.generate(5, (index) => 30 + index); + + sut.sealOpen( + cipherText: Uint8List.fromList(cipherText), + recipientPublicKey: Uint8List.fromList(recipientPublicKey), + recipientSecretKey: SecureKeyFake(recipientSecretKey), + ); + + verifyInOrder([ + () => mockSodium.sodium_mprotect_readonly( + any(that: hasRawData(recipientPublicKey)), + ), + () => mockSodium.sodium_mprotect_readonly( + any(that: hasRawData(recipientSecretKey)), + ), + () => mockSodium.crypto_box_seal_open( + any(that: hasRawData(cipherText.sublist(5))), + any(that: hasRawData(cipherText)), + cipherText.length, + any(that: hasRawData(recipientPublicKey)), + any(that: hasRawData(recipientSecretKey)), + ), + ]); + }); + + test('returns decrypted data', () { + final message = List.generate(8, (index) => index * 5); + when( + () => mockSodium.crypto_box_seal_open( + any(), + any(), + any(), + any(), + any(), + ), + ).thenAnswer((i) { + fillPointer(i.positionalArguments.first as Pointer, message); + return 0; + }); + + final result = sut.sealOpen( + cipherText: Uint8List(13), + recipientPublicKey: Uint8List(5), + recipientSecretKey: SecureKeyFake.empty(5), + ); + + expect(result, message); + + verify(() => mockSodium.sodium_free(any())).called(3); + }); + + test('throws exception on failure', () { + when( + () => mockSodium.crypto_box_seal_open( + any(), + any(), + any(), + any(), + any(), + ), + ).thenReturn(1); + + expect( + () => sut.sealOpen( + cipherText: Uint8List(13), + recipientPublicKey: Uint8List(5), + recipientSecretKey: SecureKeyFake.empty(5), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.sodium_free(any())).called(3); + }); + }); }); } diff --git a/packages/sodium/test/unit/js/api/box_js_test.dart b/packages/sodium/test/unit/js/api/box_js_test.dart index 4136d822..414f15ae 100644 --- a/packages/sodium/test/unit/js/api/box_js_test.dart +++ b/packages/sodium/test/unit/js/api/box_js_test.dart @@ -55,6 +55,11 @@ void main() { () => sut.seedBytes, 'seedBytes', ), + Tuple3( + () => mockSodium.crypto_box_SEALBYTES, + () => sut.sealBytes, + 'sealBytes', + ), ]); group('methods', () { @@ -64,6 +69,7 @@ void main() { when(() => mockSodium.crypto_box_MACBYTES).thenReturn(5); when(() => mockSodium.crypto_box_NONCEBYTES).thenReturn(5); when(() => mockSodium.crypto_box_SEEDBYTES).thenReturn(5); + when(() => mockSodium.crypto_box_SEALBYTES).thenReturn(5); }); group('keypair', () { @@ -301,7 +307,7 @@ void main() { verify(() => mockSodium.crypto_box_NONCEBYTES); }); - test('asserts if recipientPublicKey is invalid', () { + test('asserts if senderPublicKey is invalid', () { expect( () => sut.openEasy( cipherText: Uint8List(20), @@ -315,7 +321,7 @@ void main() { verify(() => mockSodium.crypto_box_PUBLICKEYBYTES); }); - test('asserts if senderSecretKey is invalid', () { + test('asserts if recipientSecretKey is invalid', () { expect( () => sut.openEasy( cipherText: Uint8List(20), @@ -566,7 +572,7 @@ void main() { verify(() => mockSodium.crypto_box_NONCEBYTES); }); - test('asserts if recipientPublicKey is invalid', () { + test('asserts if senderPublicKey is invalid', () { expect( () => sut.openDetached( cipherText: Uint8List(10), @@ -581,7 +587,7 @@ void main() { verify(() => mockSodium.crypto_box_PUBLICKEYBYTES); }); - test('asserts if senderSecretKey is invalid', () { + test('asserts if recipientSecretKey is invalid', () { expect( () => sut.openDetached( cipherText: Uint8List(10), @@ -678,5 +684,184 @@ void main() { ); }); }); + + group('seal', () { + test('asserts if recipientPublicKey is invalid', () { + expect( + () => sut.seal( + message: Uint8List(20), + recipientPublicKey: Uint8List(10), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_box_PUBLICKEYBYTES); + }); + + test('calls crypto_box_seal with correct arguments', () { + when( + () => mockSodium.crypto_box_seal( + any(), + any(), + ), + ).thenReturn(Uint8List(0)); + + final message = List.generate(20, (index) => index * 2); + final recipientPublicKey = List.generate(5, (index) => 20 + index); + + sut.seal( + message: Uint8List.fromList(message), + recipientPublicKey: Uint8List.fromList(recipientPublicKey), + ); + + verify( + () => mockSodium.crypto_box_seal( + Uint8List.fromList(message), + Uint8List.fromList(recipientPublicKey), + ), + ); + }); + + test('returns encrypted data', () { + final cipher = List.generate(25, (index) => 100 - index); + when( + () => mockSodium.crypto_box_seal( + any(), + any(), + ), + ).thenReturn(Uint8List.fromList(cipher)); + + final result = sut.seal( + message: Uint8List(20), + recipientPublicKey: Uint8List(5), + ); + + expect(result, cipher); + }); + + test('throws exception on failure', () { + when( + () => mockSodium.crypto_box_seal( + any(), + any(), + ), + ).thenThrow(JsError()); + + expect( + () => sut.seal( + message: Uint8List(10), + recipientPublicKey: Uint8List(5), + ), + throwsA(isA()), + ); + }); + }); + + group('sealOpen', () { + test('asserts if cipherText is invalid', () { + expect( + () => sut.sealOpen( + cipherText: Uint8List(3), + recipientPublicKey: Uint8List(5), + recipientSecretKey: SecureKeyFake.empty(5), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_box_SEALBYTES); + }); + + test('asserts if recipientPublicKey is invalid', () { + expect( + () => sut.sealOpen( + cipherText: Uint8List(20), + recipientPublicKey: Uint8List(10), + recipientSecretKey: SecureKeyFake.empty(5), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_box_PUBLICKEYBYTES); + }); + + test('asserts if recipientSecretKey is invalid', () { + expect( + () => sut.sealOpen( + cipherText: Uint8List(20), + recipientPublicKey: Uint8List(5), + recipientSecretKey: SecureKeyFake.empty(10), + ), + throwsA(isA()), + ); + + verify(() => mockSodium.crypto_box_SECRETKEYBYTES); + }); + + test('calls crypto_box_seal_open with correct arguments', () { + when( + () => mockSodium.crypto_box_seal_open( + any(), + any(), + any(), + ), + ).thenReturn(Uint8List(0)); + + final cipherText = List.generate(20, (index) => index * 2); + final recipientPublicKey = List.generate(5, (index) => 20 + index); + final recipientSecretKey = List.generate(5, (index) => 30 + index); + + sut.sealOpen( + cipherText: Uint8List.fromList(cipherText), + recipientPublicKey: Uint8List.fromList(recipientPublicKey), + recipientSecretKey: SecureKeyFake(recipientSecretKey), + ); + + verify( + () => mockSodium.crypto_box_seal_open( + Uint8List.fromList(cipherText), + Uint8List.fromList(recipientPublicKey), + Uint8List.fromList(recipientSecretKey), + ), + ); + }); + + test('returns decrypted data', () { + final message = List.generate(8, (index) => index * 5); + when( + () => mockSodium.crypto_box_seal_open( + any(), + any(), + any(), + ), + ).thenReturn(Uint8List.fromList(message)); + + final result = sut.sealOpen( + cipherText: Uint8List(13), + recipientPublicKey: Uint8List(5), + recipientSecretKey: SecureKeyFake.empty(5), + ); + + expect(result, message); + }); + + test('throws exception on failure', () { + when( + () => mockSodium.crypto_box_seal_open( + any(), + any(), + any(), + ), + ).thenThrow(JsError()); + + expect( + () => sut.sealOpen( + cipherText: Uint8List(10), + recipientPublicKey: Uint8List(5), + recipientSecretKey: SecureKeyFake.empty(5), + ), + throwsA(isA()), + ); + }); + }); }); }