From 96b1f5647fff0c70c002fc65bce011000ff602bb Mon Sep 17 00:00:00 2001 From: Sudipto Chandra Date: Thu, 12 Sep 2024 02:59:49 +0400 Subject: [PATCH] Adds SaltedCipher mixin to XChaCha20, XSalsa20, and all AEAD ciphers --- benchmark/chacha20.dart | 2 +- benchmark/chacha20_poly1305.dart | 2 +- benchmark/salsa20_poly1305.dart | 2 +- lib/src/algorithms/aead_cipher.dart | 9 +- lib/src/algorithms/aes/cbc.dart | 28 +++-- lib/src/algorithms/aes/cfb.dart | 30 +++-- lib/src/algorithms/aes/ctr.dart | 17 ++- lib/src/algorithms/aes/gcm.dart | 28 +++-- lib/src/algorithms/aes/ige.dart | 28 +++-- lib/src/algorithms/aes/ofb.dart | 19 ++- lib/src/algorithms/aes/pcbc.dart | 28 +++-- lib/src/algorithms/aes/xts.dart | 28 +++-- lib/src/algorithms/chacha20.dart | 95 ++++++++++----- lib/src/algorithms/salsa20.dart | 75 ++++++++---- lib/src/chacha20_poly1305.dart | 31 +++-- lib/src/cipherlib_base.dart | 1 - lib/src/core/cipher.dart | 12 ++ lib/src/core/cipher_sink.dart | 2 - lib/src/core/salted_cipher.dart | 36 ------ lib/src/salsa20_poly1305.dart | 30 +++-- lib/src/xchacha20_poly1305.dart | 31 +++-- lib/src/xsalsa20_poly1305.dart | 31 +++-- test/chacha20_poly1305_test.dart | 20 +++- test/chacha20_test.dart | 58 +++++++++ test/cipher_test.dart | 7 +- test/salsa20_poly1305_test.dart | 94 +++++++++++++-- test/salsa20_test.dart | 48 ++++++++ test/xchacha20_poly1305_test.dart | 176 ++++++++++++++++++++++++++++ test/xchacha20_test.dart | 101 ++++++++++------ test/xsalsa20_poly1305_test.dart | 111 ++++++++++++++++++ test/xsalsa20_test.dart | 112 +++++++----------- 31 files changed, 965 insertions(+), 327 deletions(-) delete mode 100644 lib/src/core/salted_cipher.dart create mode 100644 test/xchacha20_poly1305_test.dart create mode 100644 test/xsalsa20_poly1305_test.dart diff --git a/benchmark/chacha20.dart b/benchmark/chacha20.dart index cfe73cc..37ecf32 100644 --- a/benchmark/chacha20.dart +++ b/benchmark/chacha20.dart @@ -23,7 +23,7 @@ class CipherlibBenchmark extends Benchmark { @override void run() { - cipher.chacha20(input, key, nonce: nonce); + cipher.ChaCha20(key, nonce).convert(input); } } diff --git a/benchmark/chacha20_poly1305.dart b/benchmark/chacha20_poly1305.dart index b0e389e..e7d4bcc 100644 --- a/benchmark/chacha20_poly1305.dart +++ b/benchmark/chacha20_poly1305.dart @@ -23,7 +23,7 @@ class CipherlibBenchmark extends Benchmark { @override void run() { - cipher.ChaCha20Poly1305(key: key, nonce: nonce).convert(input); + cipher.ChaCha20Poly1305(key, nonce: nonce).convert(input); } } diff --git a/benchmark/salsa20_poly1305.dart b/benchmark/salsa20_poly1305.dart index e5b125a..da29133 100644 --- a/benchmark/salsa20_poly1305.dart +++ b/benchmark/salsa20_poly1305.dart @@ -21,7 +21,7 @@ class CipherlibBenchmark extends Benchmark { @override void run() { - cipher.Salsa20Poly1305(key: key, nonce: nonce).convert(input); + cipher.Salsa20Poly1305(key, nonce: nonce).convert(input); } } diff --git a/lib/src/algorithms/aead_cipher.dart b/lib/src/algorithms/aead_cipher.dart index cae7afe..8379fb4 100644 --- a/lib/src/algorithms/aead_cipher.dart +++ b/lib/src/algorithms/aead_cipher.dart @@ -174,7 +174,8 @@ abstract class AEADCipher /// Transforms the [message]. Alias for [sign]. @pragma('vm:prefer-inline') - AEADResult convert(List message) => sign(message); + Uint8List convert(List message, [bool verifyMode = false]) => + createSink(verifyMode).add(message, true); /// Signs the [message] with an authentication tag. AEADResult sign(List message) { @@ -193,8 +194,9 @@ abstract class AEADCipher Stream bind( Stream> stream, [ Function(HashDigest tag)? onDigest, + bool verifyMode = false, ]) async* { - var sink = createSink(); + var sink = createSink(verifyMode); List? cache; await for (var data in stream) { if (cache != null) { @@ -212,9 +214,10 @@ abstract class AEADCipher Stream stream( Stream stream, [ Function(HashDigest tag)? onDigest, + bool verifyMode = false, ]) async* { int p = 0; - var sink = createSink(); + var sink = createSink(verifyMode); var chunk = Uint8List(1024); await for (var x in stream) { chunk[p++] = x; diff --git a/lib/src/algorithms/aes/cbc.dart b/lib/src/algorithms/aes/cbc.dart index 4a4ad2e..78b2940 100644 --- a/lib/src/algorithms/aes/cbc.dart +++ b/lib/src/algorithms/aes/cbc.dart @@ -3,11 +3,12 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/algorithms/padding.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; +import 'package:cipherlib/src/core/collate_cipher.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; +import '../padding.dart'; import '_core.dart'; /// The sink used for encryption by the [AESInCBCModeEncrypt] algorithm. @@ -215,7 +216,7 @@ class AESInCBCModeDecryptSink implements CipherSink { } /// Provides encryption for AES cipher in CBC mode. -class AESInCBCModeEncrypt extends SaltedCipher { +class AESInCBCModeEncrypt extends Cipher with SaltedCipher { @override String get name => "AES#encrypt/CBC/${padding.name}"; @@ -225,11 +226,14 @@ class AESInCBCModeEncrypt extends SaltedCipher { /// Padding scheme for the input message final Padding padding; + @override + final Uint8List iv; + const AESInCBCModeEncrypt( this.key, - Uint8List iv, [ + this.iv, [ this.padding = Padding.pkcs7, - ]) : super(iv); + ]); @override @pragma('vm:prefer-inline') @@ -238,7 +242,7 @@ class AESInCBCModeEncrypt extends SaltedCipher { } /// Provides decryption for AES cipher in CBC mode. -class AESInCBCModeDecrypt extends SaltedCipher { +class AESInCBCModeDecrypt extends Cipher with SaltedCipher { @override String get name => "AES#decrypt/CBC/${padding.name}"; @@ -248,11 +252,14 @@ class AESInCBCModeDecrypt extends SaltedCipher { /// Padding scheme for the output message final Padding padding; + @override + final Uint8List iv; + const AESInCBCModeDecrypt( this.key, - Uint8List iv, [ + this.iv, [ this.padding = Padding.pkcs7, - ]) : super(iv); + ]); @override @pragma('vm:prefer-inline') @@ -261,7 +268,7 @@ class AESInCBCModeDecrypt extends SaltedCipher { } /// Provides encryption and decryption for AES cipher in CBC mode. -class AESInCBCMode extends SaltedCollateCipher { +class AESInCBCMode extends CollateCipher with SaltedCipher { @override String get name => "AES/CBC/${padding.name}"; @@ -276,6 +283,9 @@ class AESInCBCMode extends SaltedCollateCipher { required this.decryptor, }); + @override + Uint8List get iv => encryptor.iv; + /// Creates AES cipher in CBC mode. /// /// Parameters: diff --git a/lib/src/algorithms/aes/cfb.dart b/lib/src/algorithms/aes/cfb.dart index 58a29ad..bdb3e92 100644 --- a/lib/src/algorithms/aes/cfb.dart +++ b/lib/src/algorithms/aes/cfb.dart @@ -3,11 +3,12 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/algorithms/padding.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; +import 'package:cipherlib/src/core/collate_cipher.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; +import '../padding.dart'; import '_core.dart'; /// The sink used for encryption by the [AESInCFBModeEncrypt] algorithm. @@ -164,7 +165,7 @@ class AESInCFBModeDecryptSink implements CipherSink { } /// Provides encryption for AES cipher in CFB mode. -class AESInCFBModeEncrypt extends SaltedCipher { +class AESInCFBModeEncrypt extends Cipher with SaltedCipher { @override String get name => "AES#encrypt/CFB/${Padding.none.name}"; @@ -174,11 +175,14 @@ class AESInCFBModeEncrypt extends SaltedCipher { /// Number of bytes to use per block final int sbyte; + @override + final Uint8List iv; + const AESInCFBModeEncrypt( this.key, - Uint8List iv, + this.iv, this.sbyte, - ) : super(iv); + ); @override @pragma('vm:prefer-inline') @@ -187,7 +191,7 @@ class AESInCFBModeEncrypt extends SaltedCipher { } /// Provides decryption for AES cipher in CFB mode. -class AESInCFBModeDecrypt extends SaltedCipher { +class AESInCFBModeDecrypt extends Cipher with SaltedCipher { @override String get name => "AES#decrypt/CFB/${Padding.none.name}"; @@ -197,7 +201,14 @@ class AESInCFBModeDecrypt extends SaltedCipher { /// Number of bytes to use per block final int sbyte; - const AESInCFBModeDecrypt(this.key, Uint8List iv, this.sbyte) : super(iv); + @override + final Uint8List iv; + + const AESInCFBModeDecrypt( + this.key, + this.iv, + this.sbyte, + ); @override @pragma('vm:prefer-inline') @@ -206,7 +217,7 @@ class AESInCFBModeDecrypt extends SaltedCipher { } /// Provides encryption and decryption for AES cipher in CFB mode. -class AESInCFBMode extends SaltedCollateCipher { +class AESInCFBMode extends CollateCipher with SaltedCipher { @override String get name => "AES/CFB/${Padding.none.name}"; @@ -221,6 +232,9 @@ class AESInCFBMode extends SaltedCollateCipher { required this.decryptor, }); + @override + Uint8List get iv => encryptor.iv; + /// Creates AES cipher in CFB mode. /// /// Parameters: diff --git a/lib/src/algorithms/aes/ctr.dart b/lib/src/algorithms/aes/ctr.dart index f4df71d..6d1b106 100644 --- a/lib/src/algorithms/aes/ctr.dart +++ b/lib/src/algorithms/aes/ctr.dart @@ -3,12 +3,13 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/algorithms/padding.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; +import 'package:cipherlib/src/core/collate_cipher.dart'; import 'package:cipherlib/src/utils/nonce.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; +import '../padding.dart'; import '_core.dart'; const int _mask32 = 0xFFFFFFFF; @@ -99,14 +100,17 @@ class AESInCTRModeSink implements CipherSink { } /// Provides AES cipher in CTR mode. -class AESInCTRModeCipher extends SaltedCipher { +class AESInCTRModeCipher extends Cipher with SaltedCipher { @override String get name => "AES#cipher/CTR/${Padding.none.name}"; /// Key for the cipher final Uint8List key; - const AESInCTRModeCipher(this.key, Uint8List iv) : super(iv); + @override + final Uint8List iv; + + const AESInCTRModeCipher(this.key, this.iv); @override @pragma('vm:prefer-inline') @@ -114,7 +118,7 @@ class AESInCTRModeCipher extends SaltedCipher { } /// Provides encryption and decryption for AES cipher in CTR mode. -class AESInCTRMode extends SaltedCollateCipher { +class AESInCTRMode extends CollateCipher with SaltedCipher { @override String get name => "AES/CTR/${Padding.none.name}"; @@ -129,6 +133,9 @@ class AESInCTRMode extends SaltedCollateCipher { required this.decryptor, }); + @override + Uint8List get iv => encryptor.iv; + /// Creates AES cipher in CTR mode. /// /// The [iv] parameter combines the 64-bit nonce, and 64-bit counter diff --git a/lib/src/algorithms/aes/gcm.dart b/lib/src/algorithms/aes/gcm.dart index eabe401..bbf465b 100644 --- a/lib/src/algorithms/aes/gcm.dart +++ b/lib/src/algorithms/aes/gcm.dart @@ -3,11 +3,12 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/algorithms/padding.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; +import 'package:cipherlib/src/core/collate_cipher.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; +import '../padding.dart'; import '_core.dart'; const List _pow2 = [ @@ -400,13 +401,16 @@ class AESInGCMModeDecryptSink extends _AESInGCMModeSinkBase { } /// Provides AES cipher in GCM mode for encryption. -class AESInGCMModeEncrypt extends SaltedCipher { +class AESInGCMModeEncrypt extends Cipher with SaltedCipher { @override String get name => "AES#encrypt/GCM/${Padding.none.name}"; /// Key for the cipher final Uint8List key; + @override + final Uint8List iv; + /// The length of the message authentication tag in bytes final int tagSize; @@ -415,10 +419,10 @@ class AESInGCMModeEncrypt extends SaltedCipher { const AESInGCMModeEncrypt( this.key, - Uint8List iv, { + this.iv, { this.aad, this.tagSize = 16, - }) : super(iv); + }); @override @pragma('vm:prefer-inline') @@ -427,13 +431,16 @@ class AESInGCMModeEncrypt extends SaltedCipher { } /// Provides AES cipher in GCM mode for decryption. -class AESInGCMModeDecrypt extends SaltedCipher { +class AESInGCMModeDecrypt extends Cipher with SaltedCipher { @override String get name => "AES#decrypt/GCM/${Padding.none.name}"; /// Key for the cipher final Uint8List key; + @override + final Uint8List iv; + /// The length of the message authentication tag in bytes final int tagSize; @@ -442,10 +449,10 @@ class AESInGCMModeDecrypt extends SaltedCipher { const AESInGCMModeDecrypt( this.key, - Uint8List iv, { + this.iv, { this.aad, this.tagSize = 16, - }) : super(iv); + }); @override @pragma('vm:prefer-inline') @@ -454,7 +461,7 @@ class AESInGCMModeDecrypt extends SaltedCipher { } /// Provides encryption and decryption for AES cipher in GCM mode. -class AESInGCMMode extends SaltedCollateCipher { +class AESInGCMMode extends CollateCipher with SaltedCipher { @override String get name => "AES/GCM/${Padding.none.name}"; @@ -469,6 +476,9 @@ class AESInGCMMode extends SaltedCollateCipher { required this.decryptor, }); + @override + Uint8List get iv => encryptor.iv; + /// Creates AES cipher in GCM mode. /// /// Parameters: diff --git a/lib/src/algorithms/aes/ige.dart b/lib/src/algorithms/aes/ige.dart index 2df11d9..6e49dee 100644 --- a/lib/src/algorithms/aes/ige.dart +++ b/lib/src/algorithms/aes/ige.dart @@ -3,11 +3,12 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/algorithms/padding.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; +import 'package:cipherlib/src/core/collate_cipher.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; +import '../padding.dart'; import '_core.dart'; /// The sink used for encryption by the [AESInIGEModeEncrypt] algorithm. @@ -224,21 +225,24 @@ class AESInIGEModeDecryptSink implements CipherSink { } /// Provides encryption for AES cipher in IGE mode. -class AESInIGEModeEncrypt extends SaltedCipher { +class AESInIGEModeEncrypt extends Cipher with SaltedCipher { @override String get name => "AES#encrypt/IGE/${padding.name}"; /// Key for the cipher final Uint8List key; + @override + final Uint8List iv; + /// Padding scheme for the input message final Padding padding; const AESInIGEModeEncrypt( this.key, - Uint8List iv, [ + this.iv, [ this.padding = Padding.pkcs7, - ]) : super(iv); + ]); @override @pragma('vm:prefer-inline') @@ -247,21 +251,24 @@ class AESInIGEModeEncrypt extends SaltedCipher { } /// Provides decryption for AES cipher in IGE mode. -class AESInIGEModeDecrypt extends SaltedCipher { +class AESInIGEModeDecrypt extends Cipher with SaltedCipher { @override String get name => "AES#decrypt/IGE/${padding.name}"; /// Key for the cipher final Uint8List key; + @override + final Uint8List iv; + /// Padding scheme for the output message final Padding padding; const AESInIGEModeDecrypt( this.key, - Uint8List iv, [ + this.iv, [ this.padding = Padding.pkcs7, - ]) : super(iv); + ]); @override @pragma('vm:prefer-inline') @@ -270,7 +277,7 @@ class AESInIGEModeDecrypt extends SaltedCipher { } /// Provides encryption and decryption for AES cipher in IGE mode. -class AESInIGEMode extends SaltedCollateCipher { +class AESInIGEMode extends CollateCipher with SaltedCipher { @override String get name => "AES/IGE/${padding.name}"; @@ -285,6 +292,9 @@ class AESInIGEMode extends SaltedCollateCipher { required this.decryptor, }); + @override + Uint8List get iv => encryptor.iv; + /// Creates AES cipher in IGE mode. /// /// Parameters: diff --git a/lib/src/algorithms/aes/ofb.dart b/lib/src/algorithms/aes/ofb.dart index 9b5f92a..5bead4d 100644 --- a/lib/src/algorithms/aes/ofb.dart +++ b/lib/src/algorithms/aes/ofb.dart @@ -3,11 +3,12 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/algorithms/padding.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; +import 'package:cipherlib/src/core/collate_cipher.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; +import '../padding.dart'; import '_core.dart'; /// The sink used for encryption by the [AESInOFBModeCipher] algorithm. @@ -88,21 +89,24 @@ class AESInOFBModeSink implements CipherSink { } /// Provides encryption for AES cipher in OFB mode. -class AESInOFBModeCipher extends SaltedCipher { +class AESInOFBModeCipher extends Cipher with SaltedCipher { @override String get name => "AES#cipher/OFB/${Padding.none.name}"; /// Key for the cipher final Uint8List key; + @override + final Uint8List iv; + /// Number of bytes to use per block final int sbyte; const AESInOFBModeCipher( this.key, - Uint8List iv, + this.iv, this.sbyte, - ) : super(iv); + ); @override @pragma('vm:prefer-inline') @@ -110,7 +114,7 @@ class AESInOFBModeCipher extends SaltedCipher { } /// Provides encryption and decryption for AES cipher in OFB mode. -class AESInOFBMode extends SaltedCollateCipher { +class AESInOFBMode extends CollateCipher with SaltedCipher { @override String get name => "AES/OFB/${Padding.none.name}"; @@ -125,6 +129,9 @@ class AESInOFBMode extends SaltedCollateCipher { required this.decryptor, }); + @override + Uint8List get iv => encryptor.iv; + /// Creates AES cipher in OFB mode. /// /// Parameters: diff --git a/lib/src/algorithms/aes/pcbc.dart b/lib/src/algorithms/aes/pcbc.dart index d25d430..5e9c014 100644 --- a/lib/src/algorithms/aes/pcbc.dart +++ b/lib/src/algorithms/aes/pcbc.dart @@ -3,11 +3,12 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/algorithms/padding.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; +import 'package:cipherlib/src/core/collate_cipher.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; +import '../padding.dart'; import '_core.dart'; /// The sink used for encryption by the [AESInPCBCModeEncrypt] algorithm. @@ -213,21 +214,24 @@ class AESInPCBCModeDecryptSink implements CipherSink { } /// Provides encryption for AES cipher in PCBC mode. -class AESInPCBCModeEncrypt extends SaltedCipher { +class AESInPCBCModeEncrypt extends Cipher with SaltedCipher { @override String get name => "AES#encrypt/PCBC/${padding.name}"; /// Key for the cipher final Uint8List key; + @override + final Uint8List iv; + /// Padding scheme for the input message final Padding padding; const AESInPCBCModeEncrypt( this.key, - Uint8List iv, [ + this.iv, [ this.padding = Padding.pkcs7, - ]) : super(iv); + ]); @override @pragma('vm:prefer-inline') @@ -236,21 +240,24 @@ class AESInPCBCModeEncrypt extends SaltedCipher { } /// Provides decryption for AES cipher in PCBC mode. -class AESInPCBCModeDecrypt extends SaltedCipher { +class AESInPCBCModeDecrypt extends Cipher with SaltedCipher { @override String get name => "AES#decrypt/PCBC/${padding.name}"; /// Key for the cipher final Uint8List key; + @override + final Uint8List iv; + /// Padding scheme for the output message final Padding padding; const AESInPCBCModeDecrypt( this.key, - Uint8List iv, [ + this.iv, [ this.padding = Padding.pkcs7, - ]) : super(iv); + ]); @override @pragma('vm:prefer-inline') @@ -259,7 +266,7 @@ class AESInPCBCModeDecrypt extends SaltedCipher { } /// Provides encryption and decryption for AES cipher in PCBC mode. -class AESInPCBCMode extends SaltedCollateCipher { +class AESInPCBCMode extends CollateCipher with SaltedCipher { @override String get name => "AES/PCBC/${padding.name}"; @@ -274,6 +281,9 @@ class AESInPCBCMode extends SaltedCollateCipher { required this.decryptor, }); + @override + Uint8List get iv => encryptor.iv; + /// Creates AES cipher in PCBC mode. /// /// Parameters: diff --git a/lib/src/algorithms/aes/xts.dart b/lib/src/algorithms/aes/xts.dart index 3ea6ca6..4fa3526 100644 --- a/lib/src/algorithms/aes/xts.dart +++ b/lib/src/algorithms/aes/xts.dart @@ -3,12 +3,13 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/algorithms/padding.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; +import 'package:cipherlib/src/core/collate_cipher.dart'; import 'package:cipherlib/src/utils/nonce.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; +import '../padding.dart'; import '_core.dart'; /// Multiply [T] by `alpha` = `0x87` in 128-bit Galois Field @@ -328,7 +329,7 @@ class AESInXTSModeDecryptSink implements CipherSink { } /// Provides encryption for AES cipher in XTS mode. -class AESInXTSModeEncrypt extends SaltedCipher { +class AESInXTSModeEncrypt extends Cipher with SaltedCipher { @override String get name => "AES#encrypt/XTS/${Padding.none.name}"; @@ -338,11 +339,14 @@ class AESInXTSModeEncrypt extends SaltedCipher { /// Key for the tweak encryption final Uint8List tkey; + @override + final Uint8List iv; + const AESInXTSModeEncrypt( this.ekey, this.tkey, - Uint8List iv, - ) : super(iv); + this.iv, + ); @override @pragma('vm:prefer-inline') @@ -351,7 +355,7 @@ class AESInXTSModeEncrypt extends SaltedCipher { } /// Provides decryption for AES cipher in XTS mode. -class AESInXTSModeDecrypt extends SaltedCipher { +class AESInXTSModeDecrypt extends Cipher with SaltedCipher { @override String get name => "AES#decrypt/XTS/${Padding.none.name}"; @@ -361,11 +365,14 @@ class AESInXTSModeDecrypt extends SaltedCipher { /// Key for the tweak encryption final Uint8List tkey; + @override + final Uint8List iv; + const AESInXTSModeDecrypt( this.ekey, this.tkey, - Uint8List iv, - ) : super(iv); + this.iv, + ); @override @pragma('vm:prefer-inline') @@ -374,7 +381,7 @@ class AESInXTSModeDecrypt extends SaltedCipher { } /// Provides encryption and decryption for AES cipher in XTS mode. -class AESInXTSMode extends SaltedCollateCipher { +class AESInXTSMode extends CollateCipher with SaltedCipher { @override String get name => "AES/XTS/${Padding.none.name}"; @@ -389,6 +396,9 @@ class AESInXTSMode extends SaltedCollateCipher { required this.decryptor, }); + @override + Uint8List get iv => encryptor.iv; + /// Creates AES cipher in XTS mode. /// /// Parameters: diff --git a/lib/src/algorithms/chacha20.dart b/lib/src/algorithms/chacha20.dart index 14e3df9..455665a 100644 --- a/lib/src/algorithms/chacha20.dart +++ b/lib/src/algorithms/chacha20.dart @@ -3,8 +3,8 @@ import 'dart:typed_data'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; import 'package:cipherlib/src/utils/nonce.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; @@ -57,7 +57,8 @@ class ChaCha20Sink implements CipherSink { for (int i = start; i < end; i++) { if (_pos == 0) { _process(_state32, _key32, _iv32); - if ((++_iv32[0]) == 0 && _counterBytes == 8) { + ++_iv32[0]; + if (_iv32[0] == 0 && _counterBytes == 8) { ++_iv32[1]; } } @@ -250,7 +251,8 @@ class ChaCha20Sink implements CipherSink { /// /// See also: /// - [XChaCha20] for better security with 192-bit nonce -class ChaCha20 extends SaltedCipher { +class ChaCha20 extends Cipher with SaltedCipher { + final Uint8List _nonce; final int _counterBytes; @override @@ -261,13 +263,16 @@ class ChaCha20 extends SaltedCipher { const ChaCha20._( this.key, - Uint8List nonce, [ + this._nonce, [ this._counterBytes = 8, - ]) : super(nonce); + ]); + + @override + Uint8List get iv => _nonce; @override @pragma('vm:prefer-inline') - CipherSink createSink() => ChaCha20Sink(key, iv, _counterBytes); + CipherSink createSink() => ChaCha20Sink(key, _nonce, _counterBytes); /// Creates an instance with a [key], [nonce], and [counter] containing a /// list of bytes. @@ -320,7 +325,7 @@ class ChaCha20 extends SaltedCipher { var state32 = Uint32List(16); var state = Uint8List.view(state32.buffer); var key32 = Uint32List.view(key.buffer); - var iv32 = Uint32List.view(iv.buffer); + var iv32 = Uint32List.view(_nonce.buffer); var nonce32 = Uint32List(4); if (_counterBytes == 4) { nonce32[1] = iv32[1]; @@ -342,14 +347,27 @@ class ChaCha20 extends SaltedCipher { /// See also: /// - [ChaCha20] class XChaCha20 extends ChaCha20 { + final Uint8List _xkey; + final Uint8List _xnonce; + final Nonce64? _xcounter; + @override String get name => "XChaCha20"; const XChaCha20._( + this._xkey, + this._xnonce, + this._xcounter, Uint8List key, - Uint8List nonce, [ - int counterBytes = 8, - ]) : super._(key, nonce, counterBytes); + Uint8List iv, + int counterBytes, + ) : super._(key, iv, counterBytes); + + @override + Uint8List get iv => _xnonce; + + /// The IV used by the base algorithm + Uint8List get activeIV => _nonce; /// Creates a [XChaCha20] with [key], and [nonce]. /// @@ -379,10 +397,29 @@ class XChaCha20 extends ChaCha20 { } var nonce8 = nonce is Uint8List ? nonce : Uint8List.fromList(nonce); + var instance = XChaCha20._( + key8, + nonce8, + counter, + Uint8List(32), + Uint8List(16), + counterSize, + ); + instance._hchacha20(); + return instance; + } + + @override + void resetIV() { + super.resetIV(); + _hchacha20(); + } + + void _hchacha20() { // HChaCha20 State from key and first 16 byte of nonce var state32 = Uint32List(16); - var key32 = Uint32List.view(key8.buffer); - var nonce32 = Uint32List.view(nonce8.buffer); + var key32 = Uint32List.view(_xkey.buffer); + var nonce32 = Uint32List.view(_xnonce.buffer); ChaCha20Sink._process(state32, key32, nonce32, true); // Take first 128-bit and last 128-bit from state as subkey @@ -396,32 +433,32 @@ class XChaCha20 extends ChaCha20 { state32[14], state32[15], ]); + key.setAll(0, Uint8List.view(subkey32.buffer)); // Use the subkey and remaining 8 byte of nonce - var subkey = Uint8List.view(subkey32.buffer); - var iv32 = Uint32List(4); - var iv = Uint8List.view(iv32.buffer); - iv32[2] = nonce32[4]; - iv32[3] = nonce32[5]; - if (nonce.length == 28) { - iv32[1] = nonce32[4]; - iv32[2] = nonce32[5]; - iv32[3] = nonce32[6]; - } else if (nonce.length == 32) { + var iv32 = Uint32List.view(_nonce.buffer); + if (_xnonce.length == 32) { iv32[0] = nonce32[4]; iv32[1] = nonce32[5]; iv32[2] = nonce32[6]; iv32[3] = nonce32[7]; - } - if (counter == null) { - iv[0] = 1; } else { - var counter32 = Uint32List.view(counter.bytes.buffer); - iv32[0] = counter32[0]; - if (counterSize == 8) { + if (_xcounter == null) { + iv32[0] = 1; + iv32[1] = 0; + } else { + var counter32 = Uint32List.view(_xcounter!.bytes.buffer); + iv32[0] = counter32[0]; iv32[1] = counter32[1]; } + if (_xnonce.length == 28) { + iv32[1] = nonce32[4]; + iv32[2] = nonce32[5]; + iv32[3] = nonce32[6]; + } else { + iv32[2] = nonce32[4]; + iv32[3] = nonce32[5]; + } } - return XChaCha20._(subkey, iv, counterSize); } } diff --git a/lib/src/algorithms/salsa20.dart b/lib/src/algorithms/salsa20.dart index b413613..eebc226 100644 --- a/lib/src/algorithms/salsa20.dart +++ b/lib/src/algorithms/salsa20.dart @@ -3,8 +3,8 @@ import 'dart:typed_data'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/core/cipher_sink.dart'; -import 'package:cipherlib/src/core/salted_cipher.dart'; import 'package:cipherlib/src/utils/nonce.dart'; import 'package:hashlib/hashlib.dart' show randomBytes; @@ -56,7 +56,8 @@ class Salsa20Sink implements CipherSink { for (int i = start; i < end; i++) { if (_pos == 0) { _process(_state32, _key32, _iv32); - if ((++_iv32[2]) == 0) { + ++_iv32[2]; + if (_iv32[2] == 0) { ++_iv32[3]; } } @@ -216,21 +217,26 @@ class Salsa20Sink implements CipherSink { /// /// See also: /// - [XSalsa20] for better security with 192-bit nonce -class Salsa20 extends SaltedCipher { - @override - String get name => "Salsa20"; +class Salsa20 extends Cipher with SaltedCipher { + final Uint8List _nonce; /// Key for the cipher final Uint8List key; + @override + String get name => "Salsa20"; + const Salsa20._( this.key, - Uint8List nonce, - ) : super(nonce); + this._nonce, + ); + + @override + Uint8List get iv => _nonce; @override @pragma('vm:prefer-inline') - Salsa20Sink createSink() => Salsa20Sink(key, iv); + Salsa20Sink createSink() => Salsa20Sink(key, _nonce); /// Creates an instance with a [key], [nonce], and [counter] containing a /// list of bytes. @@ -271,7 +277,7 @@ class Salsa20 extends SaltedCipher { var state32 = Uint32List(16); var state = Uint8List.view(state32.buffer); var key32 = Uint32List.view(key.buffer); - var nonce32 = Uint32List.view(iv.buffer); + var nonce32 = Uint32List.view(_nonce.buffer); Salsa20Sink._process(state32, key32, nonce32); return state.sublist(0, 32); } @@ -288,13 +294,26 @@ class Salsa20 extends SaltedCipher { /// See also: /// - [Salsa20] class XSalsa20 extends Salsa20 { + final Uint8List _xkey; + final Uint8List _xnonce; + final Nonce64? _xcounter; + @override String get name => "XSalsa20"; const XSalsa20._( + this._xkey, + this._xnonce, + this._xcounter, Uint8List key, - Uint8List nonce, - ) : super._(key, nonce); + Uint8List iv, + ) : super._(key, iv); + + @override + Uint8List get iv => _xnonce; + + /// The IV used by the base algorithm + Uint8List get activeIV => _nonce; /// Creates a [XSalsa20] with [key], and [nonce]. /// @@ -321,10 +340,28 @@ class XSalsa20 extends Salsa20 { } var nonce8 = nonce is Uint8List ? nonce : Uint8List.fromList(nonce); + var instance = XSalsa20._( + key8, + nonce8, + counter, + Uint8List(32), + Uint8List(16), + ); + instance._hsalsa20(); + return instance; + } + + @override + void resetIV() { + super.resetIV(); + _hsalsa20(); + } + + void _hsalsa20() { // HSalsa20 state from key and first 128-bit of nonce var state32 = Uint32List(16); - var key32 = Uint32List.view(key8.buffer); - var nonce32 = Uint32List.view(nonce8.buffer); + var key32 = Uint32List.view(_xkey.buffer); + var nonce32 = Uint32List.view(_xnonce.buffer); Salsa20Sink._process(state32, key32, nonce32, true); // Take first 128-bit and last 128-bit from state as subkey @@ -338,21 +375,19 @@ class XSalsa20 extends Salsa20 { state32[8], state32[9], ]); + key.setAll(0, Uint8List.view(subkey32.buffer)); // Use the subkey and last 128-bit of nonce or 96-bit nonce and counter. - var iv32 = Uint32List(4); - var iv = Uint8List.view(iv32.buffer); + var iv32 = Uint32List.view(_nonce.buffer); iv32[0] = nonce32[4]; iv32[1] = nonce32[5]; - if (nonce.length == 32) { + if (_xnonce.length == 32) { iv32[2] = nonce32[6]; iv32[3] = nonce32[7]; - } else if (counter != null) { - var counter32 = Uint32List.view(counter.bytes.buffer); + } else if (_xcounter != null) { + var counter32 = Uint32List.view(_xcounter!.bytes.buffer); iv32[2] = counter32[0]; iv32[3] = counter32[1]; } - var subkey = Uint8List.view(subkey32.buffer); - return XSalsa20._(subkey, iv); } } diff --git a/lib/src/chacha20_poly1305.dart b/lib/src/chacha20_poly1305.dart index b9acf3d..440ef78 100644 --- a/lib/src/chacha20_poly1305.dart +++ b/lib/src/chacha20_poly1305.dart @@ -1,8 +1,11 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:typed_data'; + import 'package:cipherlib/src/algorithms/aead_cipher.dart'; import 'package:cipherlib/src/algorithms/chacha20.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/utils/nonce.dart'; import 'package:hashlib/hashlib.dart' show Poly1305; @@ -15,7 +18,8 @@ import 'package:hashlib/hashlib.dart' show Poly1305; /// This implementation is based on the [RFC-8439][rfc] /// /// [rfc]: https://www.rfc-editor.org/rfc/rfc8439.html -class ChaCha20Poly1305 extends AEADCipher { +class ChaCha20Poly1305 extends AEADCipher + with SaltedCipher { const ChaCha20Poly1305._( ChaCha20 cipher, Poly1305 mac, @@ -29,22 +33,27 @@ class ChaCha20Poly1305 extends AEADCipher { /// - [nonce] : Either 8 or 12 bytes nonce. /// - [aad] : Additional authenticated data. /// - [counter] : Initial block number. - factory ChaCha20Poly1305({ - required List key, + factory ChaCha20Poly1305( + List key, { List? nonce, Nonce64? counter, List? aad, }) => ChaCha20(key, nonce, counter).poly1305(aad); - @override - @pragma('vm:prefer-inline') - AEADResultWithIV convert(List message) => sign(message); - @override @pragma('vm:prefer-inline') AEADResultWithIV sign(List message) => super.sign(message).withIV(cipher.iv); + + @override + Uint8List get iv => cipher.iv; + + @override + void resetIV() { + cipher.resetIV(); + mac.keypair.setAll(0, cipher.$otk()); + } } /// Adds [poly1305] to [ChaCha20] to create an instance of [ChaCha20Poly1305] @@ -59,8 +68,8 @@ extension ChaCha20ExtentionForPoly1305 on ChaCha20 { ChaCha20Poly1305._(this, Poly1305($otk()), aad); } -/// Transforms [message] with ChaCha20 algorithm and generates the message -/// digest with Poly1305 authentication code generator. +/// Encrypts or Decrypts the [message] using ChaCha20 cipher and generates an +/// authentication tag with Poly1305. /// /// Parameters: /// - [message] : arbitrary length plain-text. @@ -82,7 +91,7 @@ AEADResultWithIV chacha20poly1305( Nonce64? counter, }) { var algo = ChaCha20Poly1305( - key: key, + key, nonce: nonce, counter: counter, aad: aad, @@ -90,5 +99,5 @@ AEADResultWithIV chacha20poly1305( if (mac != null && !algo.verify(message, mac)) { throw AssertionError('Message authenticity check failed'); } - return algo.convert(message); + return algo.sign(message); } diff --git a/lib/src/cipherlib_base.dart b/lib/src/cipherlib_base.dart index 67e7992..84e6d4e 100644 --- a/lib/src/cipherlib_base.dart +++ b/lib/src/cipherlib_base.dart @@ -9,7 +9,6 @@ export 'chacha20_poly1305.dart'; export 'core/cipher.dart'; export 'core/cipher_sink.dart'; export 'core/collate_cipher.dart'; -export 'core/salted_cipher.dart'; export 'salsa20.dart'; export 'salsa20_poly1305.dart'; export 'utils/nonce.dart'; diff --git a/lib/src/core/cipher.dart b/lib/src/core/cipher.dart index 33f30e1..ce46164 100644 --- a/lib/src/core/cipher.dart +++ b/lib/src/core/cipher.dart @@ -4,6 +4,8 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:hashlib/hashlib.dart' show fillRandom; + import 'cipher_sink.dart'; abstract class CipherBase { @@ -76,3 +78,13 @@ abstract class Cipher extends StreamCipherBase { } } } + +/// Mixin to use a random initialization vector or salt with the Cipher +abstract class SaltedCipher implements CipherBase { + /// The salt or initialization vector + Uint8List get iv; + + /// Replaces current IV with a new random one + @pragma('vm:prefer-inline') + void resetIV() => fillRandom(iv.buffer); +} diff --git a/lib/src/core/cipher_sink.dart b/lib/src/core/cipher_sink.dart index 770c90a..fab902d 100644 --- a/lib/src/core/cipher_sink.dart +++ b/lib/src/core/cipher_sink.dart @@ -5,8 +5,6 @@ import 'dart:typed_data'; /// Template for Cipher algorithm sink. abstract class CipherSink implements Sink> { - const CipherSink(); - /// Returns true if the sink is closed, false otherwise bool get closed; diff --git a/lib/src/core/salted_cipher.dart b/lib/src/core/salted_cipher.dart deleted file mode 100644 index dcb02ca..0000000 --- a/lib/src/core/salted_cipher.dart +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2024, Sudipto Chandra -// All rights reserved. Check LICENSE file for details. - -import 'dart:typed_data'; - -import 'package:hashlib/hashlib.dart' show fillRandom; - -import 'cipher.dart'; -import 'collate_cipher.dart'; - -/// Template for Ciphers accepting a random initialization vector or salt -abstract class SaltedCipher extends Cipher { - /// The salt or initialization vector - final Uint8List iv; - - /// Creates the cipher with a random initialization vector - const SaltedCipher(this.iv); - - /// Replaces current IV with a new random one - @pragma('vm:prefer-inline') - void resetIV() => fillRandom(iv.buffer); -} - -/// Template for Cipher algorithm accepting an IV (Initialization Vector) which -/// does not use the same logic for encryption and decryption. -abstract class SaltedCollateCipher extends CollateCipher { - const SaltedCollateCipher(); - - /// IV for the cipher - Uint8List get iv => encryptor.iv; - - /// Replaces current IV with a new random one - @pragma('vm:prefer-inline') - void resetIV() => fillRandom(iv.buffer); -} diff --git a/lib/src/salsa20_poly1305.dart b/lib/src/salsa20_poly1305.dart index f6c180e..63d7124 100644 --- a/lib/src/salsa20_poly1305.dart +++ b/lib/src/salsa20_poly1305.dart @@ -1,15 +1,18 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:typed_data'; + import 'package:cipherlib/src/algorithms/aead_cipher.dart'; import 'package:cipherlib/src/algorithms/salsa20.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/utils/nonce.dart'; import 'package:hashlib/hashlib.dart' show Poly1305; /// Salsa20-Poly1305 is a cryptographic algorithm combining the [Salsa20] /// stream cipher for encryption and the [Poly1305] for generating message /// authentication code. -class Salsa20Poly1305 extends AEADCipher { +class Salsa20Poly1305 extends AEADCipher with SaltedCipher { const Salsa20Poly1305._( Salsa20 cipher, Poly1305 mac, @@ -23,22 +26,27 @@ class Salsa20Poly1305 extends AEADCipher { /// - [nonce] : Either 8 or 16 bytes nonce. /// - [aad] : Additional authenticated data. /// - [counter] : Initial block number. - factory Salsa20Poly1305({ - required List key, + factory Salsa20Poly1305( + List key, { List? nonce, Nonce64? counter, List? aad, }) => Salsa20(key, nonce, counter).poly1305(aad); - @override - @pragma('vm:prefer-inline') - AEADResultWithIV convert(List message) => sign(message); - @override @pragma('vm:prefer-inline') AEADResultWithIV sign(List message) => super.sign(message).withIV(cipher.iv); + + @override + Uint8List get iv => cipher.iv; + + @override + void resetIV() { + cipher.resetIV(); + mac.keypair.setAll(0, cipher.$otk()); + } } /// Adds [poly1305] to [Salsa20] to create an instance of [Salsa20Poly1305] @@ -54,8 +62,8 @@ extension Salsa20ExtentionForPoly1305 on Salsa20 { } } -/// Transforms [message] with Salsa20 algorithm and generates the message -/// digest with Poly1305 authentication code generator. +/// Encrypts or Decrypts the [message] using Salsa20 cipher and generates an +/// authentication tag with Poly1305. /// /// Parameters: /// - [message] : arbitrary length plain-text. @@ -77,7 +85,7 @@ AEADResultWithIV salsa20poly1305( Nonce64? counter, }) { var algo = Salsa20Poly1305( - key: key, + key, nonce: nonce, counter: counter, aad: aad, @@ -85,5 +93,5 @@ AEADResultWithIV salsa20poly1305( if (mac != null && !algo.verify(message, mac)) { throw AssertionError('Message authenticity check failed'); } - return algo.convert(message); + return algo.sign(message); } diff --git a/lib/src/xchacha20_poly1305.dart b/lib/src/xchacha20_poly1305.dart index cbffd19..08b2a2c 100644 --- a/lib/src/xchacha20_poly1305.dart +++ b/lib/src/xchacha20_poly1305.dart @@ -1,8 +1,11 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:typed_data'; + import 'package:cipherlib/src/algorithms/aead_cipher.dart'; import 'package:cipherlib/src/algorithms/chacha20.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/utils/nonce.dart'; import 'package:hashlib/hashlib.dart' show Poly1305; @@ -13,7 +16,8 @@ import 'package:hashlib/hashlib.dart' show Poly1305; /// This implementation is based on the [RFC-8439][rfc] /// /// [rfc]: https://www.rfc-editor.org/rfc/rfc8439.html -class XChaCha20Poly1305 extends AEADCipher { +class XChaCha20Poly1305 extends AEADCipher + with SaltedCipher { const XChaCha20Poly1305._( XChaCha20 cipher, Poly1305 mac, @@ -27,22 +31,27 @@ class XChaCha20Poly1305 extends AEADCipher { /// - [nonce] : Either 8 or 12 bytes nonce. /// - [aad] : Additional authenticated data. /// - [counter] : Initial block number. - factory XChaCha20Poly1305({ - required List key, + factory XChaCha20Poly1305( + List key, { List? nonce, Nonce64? counter, List? aad, }) => XChaCha20(key, nonce, counter).poly1305(aad); - @override - @pragma('vm:prefer-inline') - AEADResultWithIV convert(List message) => sign(message); - @override @pragma('vm:prefer-inline') AEADResultWithIV sign(List message) => super.sign(message).withIV(cipher.iv); + + @override + Uint8List get iv => cipher.iv; + + @override + void resetIV() { + cipher.resetIV(); + mac.keypair.setAll(0, cipher.$otk()); + } } /// Adds [poly1305] to [XChaCha20] to create an instance of [XChaCha20Poly1305] @@ -57,8 +66,8 @@ extension XChaCha20ExtentionForPoly1305 on XChaCha20 { XChaCha20Poly1305._(this, Poly1305($otk()), aad); } -/// Transforms [message] with XChaCha20 algorithm and generates the message -/// digest with Poly1305 authentication code generator. +/// Encrypts or Decrypts the [message] using XChaCha20 cipher and generates an +/// authentication tag with Poly1305. /// /// Parameters: /// - [message] : arbitrary length plain-text. @@ -80,7 +89,7 @@ AEADResultWithIV xchacha20poly1305( Nonce64? counter, }) { var algo = XChaCha20Poly1305( - key: key, + key, nonce: nonce, counter: counter, aad: aad, @@ -88,5 +97,5 @@ AEADResultWithIV xchacha20poly1305( if (mac != null && !algo.verify(message, mac)) { throw AssertionError('Message authenticity check failed'); } - return algo.convert(message); + return algo.sign(message); } diff --git a/lib/src/xsalsa20_poly1305.dart b/lib/src/xsalsa20_poly1305.dart index dd4e124..f7fae54 100644 --- a/lib/src/xsalsa20_poly1305.dart +++ b/lib/src/xsalsa20_poly1305.dart @@ -1,15 +1,19 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:typed_data'; + import 'package:cipherlib/src/algorithms/aead_cipher.dart'; import 'package:cipherlib/src/algorithms/salsa20.dart'; +import 'package:cipherlib/src/core/cipher.dart'; import 'package:cipherlib/src/utils/nonce.dart'; import 'package:hashlib/hashlib.dart' show Poly1305; /// XSalsa20-Poly1305 is a cryptographic algorithm combining the [XSalsa20] /// stream cipher for encryption and the [Poly1305] for generating message /// authentication code. -class XSalsa20Poly1305 extends AEADCipher { +class XSalsa20Poly1305 extends AEADCipher + with SaltedCipher { const XSalsa20Poly1305._( XSalsa20 cipher, Poly1305 mac, @@ -23,22 +27,27 @@ class XSalsa20Poly1305 extends AEADCipher { /// - [nonce] : Either 8 or 16 bytes nonce. /// - [aad] : Additional authenticated data. /// - [counter] : Initial block number. - factory XSalsa20Poly1305({ - required List key, + factory XSalsa20Poly1305( + List key, { List? nonce, Nonce64? counter, List? aad, }) => XSalsa20(key, nonce, counter).poly1305(aad); - @override - @pragma('vm:prefer-inline') - AEADResultWithIV convert(List message) => sign(message); - @override @pragma('vm:prefer-inline') AEADResultWithIV sign(List message) => super.sign(message).withIV(cipher.iv); + + @override + Uint8List get iv => cipher.iv; + + @override + void resetIV() { + cipher.resetIV(); + mac.keypair.setAll(0, cipher.$otk()); + } } /// Adds [poly1305] to [XSalsa20] to create an instance of [XSalsa20Poly1305] @@ -54,8 +63,8 @@ extension XSalsa20ExtentionForPoly1305 on XSalsa20 { } } -/// Transforms [message] with XSalsa20 algorithm and generates the message -/// digest with Poly1305 authentication code generator. +/// Encrypts or Decrypts the [message] using XSalsa20 cipher and generates an +/// authentication tag with Poly1305. /// /// Parameters: /// - [message] : arbitrary length plain-text. @@ -77,7 +86,7 @@ AEADResultWithIV xsalsa20poly1305( Nonce64? counter, }) { var algo = XSalsa20Poly1305( - key: key, + key, nonce: nonce, counter: counter, aad: aad, @@ -85,5 +94,5 @@ AEADResultWithIV xsalsa20poly1305( if (mac != null && !algo.verify(message, mac)) { throw AssertionError('Message authenticity check failed'); } - return algo.convert(message); + return algo.sign(message); } diff --git a/test/chacha20_poly1305_test.dart b/test/chacha20_poly1305_test.dart index d1633d9..204d96f 100644 --- a/test/chacha20_poly1305_test.dart +++ b/test/chacha20_poly1305_test.dart @@ -113,12 +113,23 @@ void main() { final iv = randomBytes(16); final aad = randomBytes(key[0]); final message = randomBytes(i); - final instance = ChaCha20Poly1305(key: key, nonce: iv, aad: aad); + final instance = ChaCha20Poly1305(key, nonce: iv, aad: aad); final res = instance.sign(message); expect(instance.verify(res.data, res.tag.bytes), isTrue); } }); + test('reset iv', () { + var x = ChaCha20Poly1305(Uint8List(32)); + var iv = [...x.iv]; + var key1 = [...x.cipher.key]; + var key2 = [...x.mac.keypair]; + x.resetIV(); + expect(iv, isNot(equals(x.iv))); + expect(key1, equals(x.cipher.key)); + expect(key2, isNot(equals(x.mac.keypair))); + }); + group('functionality tests', () { var key = fromHex( "808182838485868788898a8b8c8d8e8f" @@ -141,7 +152,7 @@ void main() { "6116", ); var algo = ChaCha20Poly1305( - key: key, + key, aad: aad, nonce: nonce, ); @@ -158,7 +169,7 @@ void main() { var input = List.generate(1200, (index) => index); var stream = Stream.fromIterable(input); var output = await algo.stream(stream).toList(); - var expected = algo.convert(input).data; + var expected = algo.convert(input); expect(output, equals(expected)); }); test('accepts integer stream with onDigest callback', () async { @@ -192,7 +203,7 @@ void main() { await for (var out in algo.bind(stream)) { output.addAll(out); } - var expected = algo.convert(input).data; + var expected = algo.convert(input); expect(output, equals(expected)); }); test('binds stream with onDigest call', () async { @@ -224,6 +235,7 @@ void main() { var out = cipher.skip(i).take(step).toList(); expect(sink.add(inp), equals(out)); } + expect(() => sink.digest(), throwsStateError); expect(sink.close(), equals([])); expect(sink.closed, true); expect(sink.digest().hex(), '1ae10b594f09e26a7e902ecbd0600691'); diff --git a/test/chacha20_test.dart b/test/chacha20_test.dart index 134774c..683bc31 100644 --- a/test/chacha20_test.dart +++ b/test/chacha20_test.dart @@ -11,6 +11,9 @@ import 'utils.dart'; void main() { group('Functionality test', () { + test('name', () { + expect(ChaCha20(Uint8List(32)).name, "ChaCha20"); + }); test('accepts empty message', () { var key = randomNumbers(32); var nonce = randomBytes(12); @@ -162,6 +165,61 @@ void main() { }); }); + group('counter increment', () { + test('at 32-bit with 8-byte nonce', () { + var key = randomBytes(32); + var iv = fromHex('3122331221327845'); + var counter1 = Nonce64.int32(0xFFFFFFFF, 0x0F0F0FFF); + var counter2 = Nonce64.int32(1, 0x0F0F1000); + var message = Uint8List(256); + var out1 = chacha20(message, key, nonce: iv, counter: counter1); + var out2 = chacha20(message, key, nonce: iv, counter: counter2); + expect(out1.skip(128), equals(out2.take(128))); + }); + + test('at 64-bit with 8-byte nonce', () { + var key = randomBytes(32); + var iv = fromHex('3122331221327845'); + var counter1 = Nonce64.int32(0xFFFFFFFF, 0xFFFFFFFF); + var counter2 = Nonce64.int32(1); + var message = Uint8List(256); + var out1 = chacha20(message, key, nonce: iv, counter: counter1); + var out2 = chacha20(message, key, nonce: iv, counter: counter2); + expect(out1.skip(128), equals(out2.take(128))); + }); + + test('at 32-bit with 12-byte nonce', () { + var key = randomBytes(32); + var iv = fromHex('FF0F0F0F3122331221327845'); + var counter1 = Nonce64.int32(0xFFFFFFFF, 0xFFFFFFFF); + var counter2 = Nonce64.int32(1, 0xFFFFFFFF); + var message = Uint8List(256); + var out1 = chacha20(message, key, nonce: iv, counter: counter1); + var out2 = chacha20(message, key, nonce: iv, counter: counter2); + expect(out1.skip(128), equals(out2.take(128))); + }); + + test('at 32-bit with 16-byte nonce', () { + var key = randomBytes(32); + var nonce1 = fromHex('FFFFFFFFFF0F0F0F3122331221327845'); + var nonce2 = fromHex('0100000000100F0F3122331221327845'); + var message = Uint8List(256); + var out1 = chacha20(message, key, nonce: nonce1); + var out2 = chacha20(message, key, nonce: nonce2); + expect(out1.skip(128), equals(out2.take(128))); + }); + + test('at 64-bit with 16-byte nonce', () { + var key = randomBytes(32); + var nonce1 = fromHex('FFFFFFFFFFFFFFFF3122331221327845'); + var nonce2 = fromHex('01000000000000003122331221327845'); + var message = Uint8List(256); + var out1 = chacha20(message, key, nonce: nonce1); + var out2 = chacha20(message, key, nonce: nonce2); + expect(out1.skip(128), equals(out2.take(128))); + }); + }); + test('Sink operations', () { var key = fromHex( "000102030405060708090a0b0c0d0e0f" diff --git a/test/cipher_test.dart b/test/cipher_test.dart index 25779c7..4d86522 100644 --- a/test/cipher_test.dart +++ b/test/cipher_test.dart @@ -25,11 +25,14 @@ class TestCipher extends Cipher { } // Concrete implementation of SaltedCipher for testing -class TestSaltedCipher extends SaltedCipher { +class TestSaltedCipher extends Cipher with SaltedCipher { @override String get name => 'TestSaltedCipher'; - const TestSaltedCipher(Uint8List iv) : super(iv); + @override + final Uint8List iv; + + const TestSaltedCipher(this.iv); @override CipherSink createSink() => MockCipherSink(); diff --git a/test/salsa20_poly1305_test.dart b/test/salsa20_poly1305_test.dart index 02e5ad0..0dd0e76 100644 --- a/test/salsa20_poly1305_test.dart +++ b/test/salsa20_poly1305_test.dart @@ -9,23 +9,80 @@ import 'package:test/test.dart'; import 'utils.dart'; void main() { + group('Functionality test', () { + test('name', () { + expect(XSalsa20Poly1305(Uint8List(32)).name, "XSalsa20/Poly1305"); + }); + test('accepts empty message', () { + final key = randomNumbers(32); + final res = xsalsa20poly1305([], key); + expect(res.data, equals([])); + expect(res.tag.bytes.length, equals(16)); + final out = xsalsa20poly1305([], key, nonce: res.iv, mac: res.tag.bytes); + expect(out.data, equals([])); + expect(out.tag.hex(), equals(res.tag.hex())); + }); + test('The key should be either 16 or 32 bytes', () { + for (int i = 0; i < 100; ++i) { + void cb() => xsalsa20poly1305([1], Uint8List(i)); + if (i == 16 || i == 32) { + cb(); + } else { + expect(cb, throwsArgumentError, reason: 'length: $i'); + } + } + }); + test('Counter is not expected with 32-byte nonce', () { + final key = Uint8List(32); + final iv = Uint8List(32); + final c = Nonce64.zero(); + expect(() => XSalsa20Poly1305(key, nonce: iv, counter: c), + throwsArgumentError); + }); + test('The nonce should be either 24 or 32 bytes', () { + var key = Uint8List(32); + for (int i = 0; i < 100; ++i) { + void cb() => xsalsa20poly1305([1], key, nonce: Uint8List(i)); + if (i == 24 || i == 32) { + cb(); + } else { + expect(cb, throwsArgumentError, reason: 'length: $i'); + } + } + }); + test('returns the original nonce', () { + final key = Uint8List(32); + final nonce = List.filled(32, 1); + final algo = XSalsa20Poly1305(key, nonce: nonce); + expect(algo.cipher.iv, equals(nonce)); + }); + test('random nonce is used if nonce is null, ', () { + var key = randomNumbers(32); + var text = randomBytes(100); + xsalsa20poly1305(text, key); + }); + test('reset iv', () { + var x = XSalsa20Poly1305(Uint8List(32)); + var iv = [...x.iv]; + var key1 = [...x.cipher.key]; + var key2 = [...x.mac.keypair]; + var activeIV = [...x.cipher.activeIV]; + x.resetIV(); + expect(iv, isNot(equals(x.iv))); + expect(key1, isNot(equals(x.cipher.key))); + expect(key2, isNot(equals(x.mac.keypair))); + expect(activeIV, isNot(equals(x.cipher.activeIV))); + }); + }); + test('encryption <-> decryption (convert)', () { var key = randomNumbers(32); var nonce = randomBytes(16); for (int j = 0; j < 100; ++j) { var text = randomBytes(j); - var res = salsa20poly1305( - text, - key, - nonce: nonce, - ); - var verified = salsa20poly1305( - res.data, - key, - mac: res.tag.bytes, - nonce: nonce, - ); - expect(verified.data, equals(text), reason: '[text size: $j]'); + var res = Salsa20Poly1305(key, nonce: nonce).convert(text); + var verified = Salsa20Poly1305(key, nonce: nonce).convert(res); + expect(verified, equals(text), reason: '[text size: $j]'); } }); @@ -35,12 +92,23 @@ void main() { final iv = randomBytes(16); final aad = randomBytes(key[0]); final message = randomBytes(i); - final instance = Salsa20Poly1305(key: key, nonce: iv, aad: aad); + final instance = Salsa20Poly1305(key, nonce: iv, aad: aad); final res = instance.sign(message); expect(instance.verify(res.data, res.tag.bytes), isTrue); } }); + test('reset iv', () { + var x = Salsa20Poly1305(Uint8List(32)); + var iv = [...x.iv]; + var key1 = [...x.cipher.key]; + var key2 = [...x.mac.keypair]; + x.resetIV(); + expect(iv, isNot(equals(x.iv))); + expect(key1, equals(x.cipher.key)); + expect(key2, isNot(equals(x.mac.keypair))); + }); + test('decrypt with invalid mac', () { var key = Uint8List(32); var nonce = Uint8List(16); diff --git a/test/salsa20_test.dart b/test/salsa20_test.dart index cca171a..51dc54d 100644 --- a/test/salsa20_test.dart +++ b/test/salsa20_test.dart @@ -4,12 +4,16 @@ import 'dart:typed_data'; import 'package:cipherlib/cipherlib.dart'; +import 'package:hashlib_codecs/hashlib_codecs.dart'; import 'package:test/test.dart'; import 'utils.dart'; void main() { group('Functionality test', () { + test('name', () { + expect(Salsa20(Uint8List(32)).name, "Salsa20"); + }); test('accepts empty message', () { var key = randomNumbers(32); var nonce = randomBytes(16); @@ -131,6 +135,50 @@ void main() { }); }); + group('counter increment', () { + test('at 32-bit with 8-byte nonce', () { + var key = randomBytes(32); + var iv = fromHex('3122331221327845'); + var counter1 = Nonce64.int32(0xFFFFFFFF, 0x0F0F0FFF); + var counter2 = Nonce64.int32(1, 0x0F0F1000); + var message = Uint8List(256); + var out1 = salsa20(message, key, nonce: iv, counter: counter1); + var out2 = salsa20(message, key, nonce: iv, counter: counter2); + expect(out1.skip(128), equals(out2.take(128))); + }); + + test('at 64-bit with 8-byte nonce', () { + var key = randomBytes(32); + var iv = fromHex('3122331221327845'); + var counter1 = Nonce64.int32(0xFFFFFFFF, 0xFFFFFFFF); + var counter2 = Nonce64.int32(1); + var message = Uint8List(256); + var out1 = salsa20(message, key, nonce: iv, counter: counter1); + var out2 = salsa20(message, key, nonce: iv, counter: counter2); + expect(out1.skip(128), equals(out2.take(128))); + }); + + test('at 32-bit with 16-byte nonce', () { + var key = randomBytes(32); + var nonce1 = fromHex('3122331221327845FFFFFFFFFF0F0F0F'); + var nonce2 = fromHex('31223312213278450100000000100F0F'); + var message = Uint8List(256); + var out1 = salsa20(message, key, nonce: nonce1); + var out2 = salsa20(message, key, nonce: nonce2); + expect(out1.skip(128), equals(out2.take(128))); + }); + + test('at 64-bit with 16-byte nonce', () { + var key = randomBytes(32); + var nonce1 = fromHex('3122331221327845FFFFFFFFFFFFFFFF'); + var nonce2 = fromHex('31223312213278450100000000000000'); + var message = Uint8List(256); + var out1 = salsa20(message, key, nonce: nonce1); + var out2 = salsa20(message, key, nonce: nonce2); + expect(out1.skip(128), equals(out2.take(128))); + }); + }); + test('Sink operations', () { var key = Uint8List.fromList( List.generate(16, (i) => i + 1), diff --git a/test/xchacha20_poly1305_test.dart b/test/xchacha20_poly1305_test.dart new file mode 100644 index 0000000..1f98c31 --- /dev/null +++ b/test/xchacha20_poly1305_test.dart @@ -0,0 +1,176 @@ +// Copyright (c) 2024, Sudipto Chandra +// All rights reserved. Check LICENSE file for details. + +import 'dart:typed_data'; + +import 'package:cipherlib/cipherlib.dart'; +import 'package:cipherlib/src/cipherlib_base.dart'; +import 'package:hashlib_codecs/hashlib_codecs.dart'; +import 'package:test/test.dart'; + +import 'fixures/xchacha20_vectors.dart'; +import 'utils.dart'; + +void main() { + group('Functionality test', () { + test('name', () { + expect(XChaCha20Poly1305(Uint8List(32)).name, "XChaCha20/Poly1305"); + }); + test('accepts empty message', () { + final key = randomNumbers(32); + final res = xchacha20poly1305([], key); + expect(res.data, equals([])); + expect(res.tag.bytes.length, equals(16)); + final out = xchacha20poly1305([], key, nonce: res.iv, mac: res.tag.bytes); + expect(out.data, equals([])); + expect(out.tag.hex(), equals(res.tag.hex())); + }); + test('The key should be either 16 or 32 bytes', () { + for (int i = 0; i < 100; ++i) { + void cb() => xchacha20poly1305([1], Uint8List(i)); + if (i == 16 || i == 32) { + cb(); + } else { + expect(cb, throwsArgumentError, reason: 'length: $i'); + } + } + }); + test('The nonce should be 24, 28, 32 bytes', () { + var key = Uint8List(32); + for (int i = 0; i < 100; ++i) { + void cb() => xchacha20poly1305([1], key, nonce: Uint8List(i)); + if (i == 24 || i == 28 || i == 32) { + cb(); + } else { + expect(cb, throwsArgumentError, reason: 'length: $i'); + } + } + }); + test('Counter is not expected with 32-byte nonce', () { + final key = Uint8List(32); + final c = Nonce64.zero(); + expect(() => XChaCha20Poly1305(key, nonce: Uint8List(32), counter: c), + throwsArgumentError); + }); + test('returns the original nonce', () { + final key = Uint8List(32); + final nonce = List.filled(32, 1); + final algo = XChaCha20Poly1305(key, nonce: nonce); + expect(algo.cipher.iv, equals(nonce)); + }); + test('random nonce is used if nonce is null, ', () { + var key = randomNumbers(32); + var text = randomBytes(100); + xchacha20(text, key); + }); + test('reset iv', () { + var x = XChaCha20Poly1305(Uint8List(32)); + var iv = [...x.iv]; + var key1 = [...x.cipher.key]; + var key2 = [...x.mac.keypair]; + var activeIV = [...x.cipher.activeIV]; + x.resetIV(); + expect(iv, isNot(equals(x.iv))); + expect(key1, isNot(equals(x.cipher.key))); + expect(key2, isNot(equals(x.mac.keypair))); + expect(activeIV, isNot(equals(x.cipher.activeIV))); + }); + }); + + test('sign and verify', () { + for (int i = 1; i < 100; ++i) { + final key = randomBytes(32); + final iv = randomBytes(24); + final aad = randomBytes(key[0]); + final message = randomBytes(i); + final res = xchacha20poly1305( + message, + key, + nonce: iv, + aad: aad, + ); + final verify = xchacha20poly1305( + res.data, + key, + nonce: iv, + aad: aad, + mac: res.tag.bytes, + ); + expect(verify.data, equals(message)); + expect(res.tag.hex(), isNot(equals(verify.tag.hex()))); + expect( + () => xchacha20poly1305( + res.data, + key, + nonce: iv, + aad: aad, + mac: verify.tag.bytes, + ), + throwsA(isA())); + } + }); + + // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03 + group('Example A.3.1 - draft-irtf-cfrg-xchacha-03 (A.3.1)', () { + final key = fromHex( + '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', + ); + final iv = fromHex( + '404142434445464748494a4b4c4d4e4f5051525354555657', + ); + final aad = fromHex( + '50515253c0c1c2c3c4c5c6c7', + ); + final plain = fromHex( + '4c616469657320616e642047656e746c656d656e206f662074686520636c6173' + '73206f66202739393a204966204920636f756c64206f6666657220796f75206f' + '6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73' + '637265656e20776f756c642062652069742e', + ); + final cipher = fromHex( + 'bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb' + '731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452' + '2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9' + '21f9664c97637da9768812f615c68b13b52e', + ); + final tag = fromHex( + 'c0875924c1c7987947deafd8780acf49', + ); + + test('sign', () { + final output = xchacha20poly1305( + plain, + key, + nonce: iv, + aad: aad, + ); + expect(output.data, equals(cipher)); + expect(output.tag.bytes, equals(tag)); + }); + test('decrypt', () { + final output = xchacha20poly1305( + cipher, + key, + nonce: iv, + aad: aad, + mac: tag, + ); + expect(output.data, equals(plain)); + }); + }); + + // https://github.com/golang/crypto/blob/master/chacha20poly1305/chacha20poly1305_vectors_test.go + test('golang-crypto test vectors for XChaCha20-Poly1305', () { + for (final item in xchacha20_vectors) { + final inp = fromHex(item['plain']!); + final aad = fromHex(item['aad']!); + final key = fromHex(item['key']!); + final iv = fromHex(item['nonce']!); + final out = fromHex(item['out']!).take(inp.length).toList(); + final tag = fromHex(item['out']!).skip(inp.length).toList(); + final res = xchacha20poly1305(inp, key, nonce: iv, aad: aad); + expect(toHex(res.data), equals(toHex(out))); + expect(res.tag.hex(), equals(toHex(tag))); + } + }); +} diff --git a/test/xchacha20_test.dart b/test/xchacha20_test.dart index 710be14..f859bff 100644 --- a/test/xchacha20_test.dart +++ b/test/xchacha20_test.dart @@ -12,6 +12,9 @@ import 'utils.dart'; void main() { group('Functionality test', () { + test('name', () { + expect(XChaCha20(Uint8List(32)).name, "XChaCha20"); + }); test('accepts empty message', () { final key = randomNumbers(32); expect(xchacha20([], key), equals([])); @@ -37,47 +40,82 @@ void main() { } } }); - test('If counter is not provided, default one is used', () { + test('If counter is not provided, default one is used (24 byte nonce)', () { final key = Uint8List(32); final nonce = List.filled(24, 1); final algo = XChaCha20(key, nonce); - expect(algo.iv, equals([1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1])); + expect(algo.activeIV, + equals([1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1])); }); - test('Counter is set correctly when provided', () { + test('Counter is set correctly when provided (24 byte nonce)', () { final key = Uint8List(32); final nonce = List.filled(24, 1); final counter = Nonce64.bytes([2, 2, 2, 2, 2, 2, 2, 2]); final algo = XChaCha20(key, nonce, counter); - expect(algo.iv, equals([2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1])); + expect(algo.activeIV, + equals([2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1])); + }); + test('If counter is not provided, default one is used (28 byte nonce)', () { + final key = Uint8List(32); + final nonce = List.filled(28, 1); + final algo = XChaCha20(key, nonce); + expect(algo.activeIV, + equals([1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])); + }); + test('Counter is set correctly when provided (28 byte nonce)', () { + final key = Uint8List(32); + final nonce = List.filled(28, 1); + final counter = Nonce64.bytes([2, 2, 2, 2, 2, 2, 2, 2]); + final algo = XChaCha20(key, nonce, counter); + expect(algo.activeIV, + equals([2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])); + }); + test('Counter is not expected with 32-byte nonce', () { + final key = Uint8List(32); + final c = Nonce64.zero(); + expect(() => XChaCha20(key, Uint8List(32), c), throwsArgumentError); }); test('random nonce is used if nonce is null, ', () { var key = randomNumbers(32); var text = randomBytes(100); xchacha20(text, key); }); + test('reset iv', () { + var x = XChaCha20(Uint8List(32)); + var iv = [...x.iv]; + var key = [...x.key]; + var activeIV = [...x.activeIV]; + x.resetIV(); + expect(iv, isNot(equals(x.iv))); + expect(key, isNot(equals(x.key))); + expect(activeIV, isNot(equals(x.activeIV))); + }); }); - test('XChaCha20: encryption <=> decryption', () { - for (int i = 0; i < 100; ++i) { - final key = randomBytes(32); - final iv = randomBytes(24); - final message = randomBytes(i); - final cipher = xchacha20(message, key, nonce: iv); - final plain = xchacha20(cipher, key, nonce: iv); - expect(plain, equals(message)); - } - }); - - test('XChaCha20Poly1305: sign and verify', () { - for (int i = 0; i < 100; ++i) { - final key = randomBytes(32); - final iv = randomBytes(24); - final aad = randomBytes(key[0]); - final message = randomBytes(i); - final instance = XChaCha20Poly1305(key: key, nonce: iv, aad: aad); - final res = instance.sign(message); - expect(instance.verify(res.data, res.tag.bytes), isTrue); - } + group('correctness', () { + test('XChaCha20: encryption <=> decryption', () { + for (int i = 0; i < 100; ++i) { + final key = randomBytes(32); + final iv = randomBytes(24); + final message = randomBytes(i); + final cipher = xchacha20(message, key, nonce: iv); + final plain = xchacha20(cipher, key, nonce: iv); + expect(plain, equals(message)); + } + }); + test('XChaCha20: encryption <-> decryption (stream)', () async { + for (int j = 0; j < 100; ++j) { + var key = randomNumbers(16); + var nonce = randomBytes(24); + var text = randomNumbers(j); + var bytes = Uint8List.fromList(text); + var stream = Stream.fromIterable(text); + var cipherStream = xchacha20Stream(stream, key, nonce: nonce); + var plainStream = xchacha20Stream(cipherStream, key, nonce: nonce); + var plain = await plainStream.toList(); + expect(bytes, equals(plain), reason: '[text: $j]'); + } + }); }); test('HChaCha20 subkey', () { @@ -221,17 +259,4 @@ void main() { expect(toHex(res), equals(toHex(out))); } }); - test('golang-crypto test vectors for XChaCha20-Poly1305', () { - for (final item in xchacha20_vectors) { - final inp = fromHex(item['plain']!); - final aad = fromHex(item['aad']!); - final key = fromHex(item['key']!); - final iv = fromHex(item['nonce']!); - final out = fromHex(item['out']!).take(inp.length).toList(); - final tag = fromHex(item['out']!).skip(inp.length).toList(); - final res = xchacha20poly1305(inp, key, nonce: iv, aad: aad); - expect(toHex(res.data), equals(toHex(out))); - expect(res.tag.hex(), equals(toHex(tag))); - } - }); } diff --git a/test/xsalsa20_poly1305_test.dart b/test/xsalsa20_poly1305_test.dart new file mode 100644 index 0000000..46038e5 --- /dev/null +++ b/test/xsalsa20_poly1305_test.dart @@ -0,0 +1,111 @@ +// Copyright (c) 2024, Sudipto Chandra +// All rights reserved. Check LICENSE file for details. + +import 'dart:typed_data'; + +import 'package:cipherlib/cipherlib.dart'; +import 'package:cipherlib/src/cipherlib_base.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('Functionality test', () { + test('name', () { + expect(XSalsa20Poly1305(Uint8List(32)).name, "XSalsa20/Poly1305"); + }); + test('accepts empty message', () { + final key = randomNumbers(32); + final res = xsalsa20poly1305([], key); + expect(res.data, equals([])); + expect(res.tag.bytes.length, equals(16)); + final out = xsalsa20poly1305([], key, nonce: res.iv, mac: res.tag.bytes); + expect(out.data, equals([])); + expect(out.tag.hex(), equals(res.tag.hex())); + }); + test('The key should be either 16 or 32 bytes', () { + for (int i = 0; i < 100; ++i) { + void cb() => xsalsa20poly1305([1], Uint8List(i)); + if (i == 16 || i == 32) { + cb(); + } else { + expect(cb, throwsArgumentError, reason: 'length: $i'); + } + } + }); + test('Counter is not expected with 32-byte nonce', () { + final key = Uint8List(32); + final iv = Uint8List(32); + final c = Nonce64.zero(); + expect(() => XSalsa20Poly1305(key, nonce: iv, counter: c), + throwsArgumentError); + }); + test('The nonce should be either 24 or 32 bytes', () { + var key = Uint8List(32); + for (int i = 0; i < 100; ++i) { + void cb() => xsalsa20poly1305([1], key, nonce: Uint8List(i)); + if (i == 24 || i == 32) { + cb(); + } else { + expect(cb, throwsArgumentError, reason: 'length: $i'); + } + } + }); + test('returns the original nonce', () { + final key = Uint8List(32); + final nonce = List.filled(32, 1); + final algo = XSalsa20Poly1305(key, nonce: nonce); + expect(algo.cipher.iv, equals(nonce)); + }); + test('random nonce is used if nonce is null, ', () { + var key = randomNumbers(32); + var text = randomBytes(100); + xsalsa20poly1305(text, key); + }); + test('reset iv', () { + var x = XSalsa20Poly1305(Uint8List(32)); + var iv = [...x.iv]; + var key1 = [...x.cipher.key]; + var key2 = [...x.mac.keypair]; + var activeIV = [...x.cipher.activeIV]; + x.resetIV(); + expect(iv, isNot(equals(x.iv))); + expect(key1, isNot(equals(x.cipher.key))); + expect(key2, isNot(equals(x.mac.keypair))); + expect(activeIV, isNot(equals(x.cipher.activeIV))); + }); + }); + + test('sign and verify', () { + for (int i = 1; i < 100; ++i) { + final key = randomBytes(32); + final iv = randomBytes(24); + final aad = randomBytes(key[0]); + final message = randomBytes(i); + final res = xsalsa20poly1305( + message, + key, + nonce: iv, + aad: aad, + ); + final verify = xsalsa20poly1305( + res.data, + key, + nonce: iv, + aad: aad, + mac: res.tag.bytes, + ); + expect(verify.data, equals(message)); + expect(res.tag.hex(), isNot(equals(verify.tag.hex()))); + expect( + () => xsalsa20poly1305( + res.data, + key, + nonce: iv, + aad: aad, + mac: verify.tag.bytes, + ), + throwsA(isA())); + } + }); +} diff --git a/test/xsalsa20_test.dart b/test/xsalsa20_test.dart index e74b282..8794663 100644 --- a/test/xsalsa20_test.dart +++ b/test/xsalsa20_test.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:cipherlib/cipherlib.dart'; +import 'package:cipherlib/src/cipherlib_base.dart'; import 'package:hashlib_codecs/hashlib_codecs.dart'; import 'package:test/test.dart'; @@ -11,6 +12,9 @@ import 'utils.dart'; void main() { group('Functionality test', () { + test('name', () { + expect(XSalsa20(Uint8List(32)).name, "XSalsa20"); + }); test('accepts empty message', () { final key = randomNumbers(32); expect(xsalsa20([], key), equals([])); @@ -28,7 +32,7 @@ void main() { test('Counter is not expected with 32-byte nonce', () { final key = Uint8List(32); final c = Nonce64.zero(); - expect(() => Salsa20(key, Uint8List(32), c), throwsArgumentError); + expect(() => XSalsa20(key, Uint8List(32), c), throwsArgumentError); }); test('The nonce should be either 24 or 32 bytes', () { var key = Uint8List(32); @@ -45,90 +49,62 @@ void main() { final key = Uint8List(32); final nonce = List.filled(24, 1); final algo = XSalsa20(key, nonce); - expect(algo.iv, equals([1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0])); + expect(algo.activeIV, + equals([1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0])); }); test('Counter is set correctly when provided', () { final key = Uint8List(32); final nonce = List.filled(24, 1); final counter = Nonce64.bytes([2, 2, 2, 2, 2, 2, 2, 2]); final algo = XSalsa20(key, nonce, counter); - expect(algo.iv, equals([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2])); + expect(algo.activeIV, + equals([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2])); }); test('random nonce is used if nonce is null, ', () { var key = randomNumbers(32); var text = randomBytes(100); xsalsa20(text, key); }); - }); - - test('correctness: encryption <=> decryption', () { - for (int i = 0; i < 20; ++i) { - final key = randomBytes(32); - final iv = randomBytes(24); - final message = randomBytes(96); - final cipher = xsalsa20(message, key, nonce: iv); - final plain = xsalsa20(cipher, key, nonce: iv); - expect(plain, equals(message)); - } - }); - - test('XSalsa20Poly1305: sign and verify', () { - for (int i = 0; i < 100; ++i) { - final key = randomBytes(32); - final iv = randomBytes(24); - final aad = randomBytes(key[0]); - final message = randomBytes(i); - final instance = XSalsa20Poly1305(key: key, nonce: iv, aad: aad); - final res = instance.sign(message); - expect(instance.verify(res.data, res.tag.bytes), isTrue); - } - }); - - // https://github.com/golang/crypto/blob/master/salsa20/salsa20_test.go - group('Test 1', () { - final key = 'this is 32-byte key for xsalsa20'.codeUnits; - final iv = '24-byte nonce for xsalsa'.codeUnits; - final plain = "Hello world!"; - final cipher = [ - 0x00, 0x2d, 0x45, 0x13, 0x84, 0x3f, 0xc2, 0x40, 0xc4, 0x01, 0xe5, 0x41 // - ]; - - test('encrypt', () { - final output = xsalsa20(plain.codeUnits, key, nonce: iv); - expect(toHex(output), equals(toHex(cipher))); - }); - test('decrypt', () { - final output = xsalsa20(cipher, key, nonce: iv); - expect(String.fromCharCodes(output), equals(plain)); + test('reset iv', () { + var x = XSalsa20(Uint8List(32)); + var iv = [...x.iv]; + var key = [...x.key]; + var activeIV = [...x.activeIV]; + x.resetIV(); + expect(iv, isNot(equals(x.iv))); + expect(key, isNot(equals(x.key))); + expect(activeIV, isNot(equals(x.activeIV))); }); }); - group('Test 2', () { - final key = 'this is 32-byte key for xsalsa20'.codeUnits; - final iv = '24-byte nonce for xsalsa'.codeUnits; - final plain = Uint8List(64); - final cipher = [ - 0x48, 0x48, 0x29, 0x7f, 0xeb, 0x1f, 0xb5, 0x2f, 0xb6, // - 0x6d, 0x81, 0x60, 0x9b, 0xd5, 0x47, 0xfa, 0xbc, 0xbe, 0x70, - 0x26, 0xed, 0xc8, 0xb5, 0xe5, 0xe4, 0x49, 0xd0, 0x88, 0xbf, - 0xa6, 0x9c, 0x08, 0x8f, 0x5d, 0x8d, 0xa1, 0xd7, 0x91, 0x26, - 0x7c, 0x2c, 0x19, 0x5a, 0x7f, 0x8c, 0xae, 0x9c, 0x4b, 0x40, - 0x50, 0xd0, 0x8c, 0xe6, 0xd3, 0xa1, 0x51, 0xec, 0x26, 0x5f, - 0x3a, 0x58, 0xe4, 0x76, 0x48, - ]; - - test('encrypt', () { - final output = xsalsa20(plain, key, nonce: iv); - expect(toHex(output), equals(toHex(cipher))); + group('correctness', () { + test('encryption <=> decryption', () { + for (int i = 0; i < 20; ++i) { + final key = randomBytes(32); + final iv = randomBytes(24); + final message = randomBytes(96); + final cipher = xsalsa20(message, key, nonce: iv); + final plain = xsalsa20(cipher, key, nonce: iv); + expect(plain, equals(message)); + } }); - test('decrypt', () { - final output = xsalsa20(cipher, key, nonce: iv); - expect(toHex(output), equals(toHex(plain))); + test('encryption <-> decryption (stream)', () async { + for (int j = 0; j < 100; ++j) { + var key = randomNumbers(16); + var nonce = randomBytes(24); + var text = randomNumbers(j); + var bytes = Uint8List.fromList(text); + var stream = Stream.fromIterable(text); + var cipherStream = xsalsa20Stream(stream, key, nonce: nonce); + var plainStream = xsalsa20Stream(cipherStream, key, nonce: nonce); + var plain = await plainStream.toList(); + expect(bytes, equals(plain), reason: '[text: $j]'); + } }); }); // https://github.com/golang/crypto/blob/master/salsa20/salsa20_test.go - group('XSalsa20: Test 1', () { + group('Test 1', () { final key = 'this is 32-byte key for xsalsa20'.codeUnits; final iv = '24-byte nonce for xsalsa'.codeUnits; final plain = "Hello world!"; @@ -146,7 +122,7 @@ void main() { }); }); - group('XSalsa20: Test 2', () { + group('Test 2', () { final key = 'this is 32-byte key for xsalsa20'.codeUnits; final iv = '24-byte nonce for xsalsa'.codeUnits; final plain = Uint8List(64); @@ -170,7 +146,7 @@ void main() { }); }); - group('XSalsa20: Test 3', () { + group('Test 3', () { final key = fromHex( '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', ); @@ -193,7 +169,7 @@ void main() { }); }); - group('XSalsa20: Test 4', () { + group('Test 4', () { final key = fromHex( '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', );