From d770cf32e311838d6c54e67682099f9cb4704020 Mon Sep 17 00:00:00 2001 From: Sudipto Chandra Date: Thu, 18 Apr 2024 11:03:37 +0400 Subject: [PATCH] Enable digest generator with stream for ChaCha20 and Salsa20 --- README.md | 14 +-- benchmark/chacha20.dart | 2 +- benchmark/salsa20.dart | 2 +- benchmark/xor.dart | 2 +- example/cipherlib_example.dart | 4 +- lib/src/algorithms/chacha20.dart | 6 +- lib/src/algorithms/chacha20_poly1305.dart | 125 ++++++++++++++++++++-- lib/src/algorithms/poly1305.dart | 97 ++++++++--------- lib/src/algorithms/salsa20.dart | 2 +- lib/src/algorithms/salsa20_poly1305.dart | 117 +++++++++++++++++++- lib/src/algorithms/xor.dart | 2 +- lib/src/chacha20.dart | 30 +++--- lib/src/chacha20_poly1305.dart | 101 ++++++++++++----- lib/src/core/auth_cipher.dart | 30 ------ lib/src/core/authenticator.dart | 58 ++++++++++ lib/src/core/cipher.dart | 33 ++++-- lib/src/salsa20.dart | 18 ++-- lib/src/salsa20_poly1305.dart | 101 ++++++++++++----- lib/src/xor.dart | 3 +- pubspec.yaml | 2 +- test/chacha20_poly1305_test.dart | 102 +++++++++++++++++- test/chacha20_test.dart | 8 +- test/salsa20_poly1305_test.dart | 61 +++++++++++ test/salsa20_test.dart | 4 +- test/xor_test.dart | 4 +- 25 files changed, 716 insertions(+), 212 deletions(-) delete mode 100644 lib/src/core/auth_cipher.dart create mode 100644 lib/src/core/authenticator.dart create mode 100644 test/salsa20_poly1305_test.dart diff --git a/README.md b/README.md index c1b2f54..94071a4 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ Implementations of cryptographic algorithms for encryption and decryption in Dar ## Features -| Ciphers | Public class and methods | Source | -| ----------------- | ---------------------------------------------------------------- | :----------: | -| XOR | `XOR`, `xor`, `xorPipe` | Wikipedia | -| ChaCha20 | `ChaCha20`, `chacha20`, `chacha20Pipe` | RFC-8439 | -| ChaCha20/Poly1305 | `ChaCha20Poly1305`, `chacha20poly1305`, `chacha20poly1305digest` | RFC-8439 | -| Salsa20 | `Salsa20`, `salsa20`, `salsa20Pipe` | Snuffle 2005 | -| Salsa20/Poly1305 | `Salsa20Poly1305`, `salsa20poly1305`, `salsa20poly1305digest` | Snuffle 2005 | +| Ciphers | Public class and methods | Source | +| ----------------- | -------------------------------------------------------------------------------------------------------------------- | :----------: | +| XOR | `XOR`, `xor`, `xorStream` | Wikipedia | +| ChaCha20 | `ChaCha20`, `chacha20`, `chacha20Stream` | RFC-8439 | +| ChaCha20/Poly1305 | `ChaCha20Poly1305`, `chacha20poly1305digest`, `chacha20poly1305verify`, `chacha20poly1305`, `chacha20poly1305Stream` | RFC-8439 | +| Salsa20 | `Salsa20`, `salsa20`, `salsa20Stream` | Snuffle 2005 | +| Salsa20/Poly1305 | `Salsa20Poly1305`, `salsa20poly1305digest`, `salsa20poly1305verify`, `salsa20poly1305`, `salsa20poly1305Stream` | Snuffle 2005 | ## Getting started diff --git a/benchmark/chacha20.dart b/benchmark/chacha20.dart index e9c451b..3e8928d 100644 --- a/benchmark/chacha20.dart +++ b/benchmark/chacha20.dart @@ -37,7 +37,7 @@ class CipherlibStreamBenchmark extends AsyncBenchmark { @override Future run() async { - await cipher.chacha20Pipe(inputStream, key).drain(); + await cipher.chacha20Stream(inputStream, key).drain(); } } diff --git a/benchmark/salsa20.dart b/benchmark/salsa20.dart index c1c950e..f3f32ff 100644 --- a/benchmark/salsa20.dart +++ b/benchmark/salsa20.dart @@ -37,7 +37,7 @@ class CipherlibStreamBenchmark extends AsyncBenchmark { @override Future run() async { - await cipher.salsa20Pipe(inputStream, key).drain(); + await cipher.salsa20Stream(inputStream, key).drain(); } } diff --git a/benchmark/xor.dart b/benchmark/xor.dart index 0fd8b85..126e455 100644 --- a/benchmark/xor.dart +++ b/benchmark/xor.dart @@ -33,7 +33,7 @@ class CipherlibStreamBenchmark extends AsyncBenchmark { @override Future run() async { - await cipher.xorPipe(inputStream, key).drain(); + await cipher.xorStream(inputStream, key).drain(); } } diff --git a/example/cipherlib_example.dart b/example/cipherlib_example.dart index 7da44e5..8011e3d 100644 --- a/example/cipherlib_example.dart +++ b/example/cipherlib_example.dart @@ -27,13 +27,13 @@ void main() { result.cipher, key, nonce: nonce, - tag: result.tag.bytes, + mac: result.mac.bytes, ); print(' Text: $text'); print(' Key: ${toHex(key)}'); print(' Nonce: ${toHex(nonce)}'); print('Cipher: ${toHex(result.cipher)}'); - print(' Tag: ${result.tag.hex()}'); + print(' Tag: ${result.mac.hex()}'); print(' Plain: ${utf8.decode(plain.cipher)}'); } } diff --git a/lib/src/algorithms/chacha20.dart b/lib/src/algorithms/chacha20.dart index 9674e54..d545afa 100644 --- a/lib/src/algorithms/chacha20.dart +++ b/lib/src/algorithms/chacha20.dart @@ -27,7 +27,7 @@ class ChaCha20 extends SymmetricCipher { Uint8List convert( List message, { List? nonce, - int blockCount = 1, + int blockId = 1, }) { if (message.isEmpty) { return Uint8List(0); @@ -42,7 +42,7 @@ class ChaCha20 extends SymmetricCipher { var result = Uint8List.fromList(message); for (int i = 0; i < message.length; ++i) { if (pos == 0 || pos == 64) { - _block(state, key32, nonce32, blockCount++); + _block(state, key32, nonce32, blockId++); pos = 0; } result[i] ^= state8[pos++]; @@ -51,7 +51,7 @@ class ChaCha20 extends SymmetricCipher { } @override - Stream pipe( + Stream bind( Stream stream, { List? nonce, int blockId = 1, diff --git a/lib/src/algorithms/chacha20_poly1305.dart b/lib/src/algorithms/chacha20_poly1305.dart index 4b18c4c..769740c 100644 --- a/lib/src/algorithms/chacha20_poly1305.dart +++ b/lib/src/algorithms/chacha20_poly1305.dart @@ -1,29 +1,140 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:async'; import 'dart:typed_data'; +import 'package:cipherlib/src/core/authenticator.dart'; +import 'package:cipherlib/src/core/chunk_stream.dart'; +import 'package:hashlib/hashlib.dart' show HashDigest; + import 'chacha20.dart'; import 'poly1305.dart'; /// ChaCha20-Poly1305 is a cryptographic algorithm combining the [ChaCha20] -/// stream cipher for encryption and the [Poly1305Authenticator] for message -/// authentication. It provides both confidentiality and integrity protection, -/// making it a popular choice for secure communication protocols like TLS. +/// stream cipher for encryption andthe [Poly1305Mac] for generating message +/// authentication code. +/// It provides both confidentiality and integrity protection, making it a +/// popular choice for secure communication protocols like TLS. /// /// This implementation is based on the [RFC-8439][rfc] /// /// [rfc]: https://www.rfc-editor.org/rfc/rfc8439.html -class ChaCha20Poly1305 extends ChaCha20 with Poly1305Authenticator { +class ChaCha20Poly1305 extends ChaCha20 with Authenticator { @override String get name => "${super.name}/Poly1305"; const ChaCha20Poly1305(List key) : super(key); - @override - Uint8List generateOTK([List? nonce]) => convert( + /// Generate One-Time-Key for Poly1305 + @pragma('vm:prefer-inline') + Uint8List _generateOTK([List? nonce]) => convert( Uint8List(32), nonce: nonce, - blockCount: 0, + blockId: 0, ); + + @override + HashDigest digest( + List message, { + List? nonce, + List? aad, + }) => + Poly1305Mac( + _generateOTK(nonce), + aad: aad, + ).convert(message); + + @override + bool verify( + List message, + List mac, { + List? nonce, + List? aad, + }) => + digest( + message, + nonce: nonce, + aad: aad, + ).isEqual(mac); + + @override + CipherMAC convertWithDigest( + List message, { + List? mac, + List? nonce, + List? aad, + int blockId = 1, + }) { + var otk = _generateOTK(nonce); + if (mac != null) { + var digest = Poly1305Mac(otk, aad: aad).convert(message); + if (!digest.isEqual(mac)) { + throw StateError('Invalid MAC'); + } + } + var cipher = convert( + message, + nonce: nonce, + blockId: blockId, + ); + var digest = Poly1305Mac(otk, aad: aad).convert(cipher); + return CipherMAC(cipher, digest); + } + + @override + AsyncCipherMAC streamWithDigest( + Stream stream, { + Future? mac, + List? nonce, + List? aad, + int blockId = 1, + }) { + var controller = StreamController(sync: true); + return AsyncCipherMAC( + controller.stream, + $buildDigest( + controller, + stream, + mac: mac, + nonce: nonce, + aad: aad, + blockId: blockId, + ), + ); + } + + Future $buildDigest( + StreamController controller, + Stream stream, { + Future? mac, + List? nonce, + List? aad, + int blockId = 1, + }) async { + var otk = _generateOTK(nonce); + var sink = mac != null ? Poly1305Mac(otk, aad: aad).createSink() : null; + // create digest sink for cipher + var cipherSink = Poly1305Mac(otk, aad: aad).createSink(); + // cipher stream + var it = generate(nonce, blockId).iterator; + await for (var buffer in asChunkedStream(4096, stream)) { + sink?.add(buffer); + for (int p = 0; p < buffer.length; ++p) { + it.moveNext(); + buffer[p] ^= it.current; + controller.add(buffer[p]); + } + cipherSink.add(buffer); + } + controller.close(); + // message digest + if (sink != null && mac != null) { + if (!sink.digest().isEqual(await mac)) { + throw StateError('Invalid MAC'); + } + } + // cipher digest + return cipherSink.digest(); + } } diff --git a/lib/src/algorithms/poly1305.dart b/lib/src/algorithms/poly1305.dart index e40898c..229445e 100644 --- a/lib/src/algorithms/poly1305.dart +++ b/lib/src/algorithms/poly1305.dart @@ -3,69 +3,66 @@ import 'dart:typed_data'; -import 'package:cipherlib/src/core/auth_cipher.dart'; -import 'package:hashlib/hashlib.dart' show HashDigest, Poly1305; +import 'package:hashlib/hashlib.dart' show HashDigest, Poly1305, Poly1305Sink; /// [Poly1305] is an authentication algorithm used for verifying the integrity /// of messages. It generates a short, fixed-length tag based on a secret key /// and the message, providing assurance that the message has not been /// tampered with. -/// -/// This is intended to be used as a mixin with the original ChaCha20 or Salsa20 -/// algorithms to generate message digests. -abstract class Poly1305Authenticator implements Authenticator { - // Generate a 32-bytes long One-Time-Key for Poly1305 digest - Uint8List generateOTK([List? nonce]); +class Poly1305Mac extends Poly1305 { + final List? aad; - @override - HashDigest digest( - List message, { - List? nonce, - List? aad, - }) { - // create key - var otk = generateOTK(nonce); + /// Creates a new instance + /// + /// Parameters: + /// - [keypair] : A 32-bytes long key. + /// - [aad] : Additional authenticated data. + const Poly1305Mac( + List keypair, { + this.aad, + }) : super(keypair); - // create sink - var sink = Poly1305(otk).createSink(); + @override + Poly1305AuthenticatorSink createSink() => + Poly1305AuthenticatorSink()..init(key, aad); +} - // add AAD - int aadLength = aad?.length ?? 0; - if (aad != null && aadLength > 0) { - sink.add(aad); - sink.add(Uint8List(16 - (aadLength & 15))); - } +/// Extends the base [Poly1305Sink] to generate message digest for cipher +/// algorithms. +class Poly1305AuthenticatorSink extends Poly1305Sink { + int _aadLength = 0; + int _messageLength = 0; - // add cipher text - int messageLength = message.length; - if (messageLength > 0) { - sink.add(message); - sink.add(Uint8List(16 - (messageLength & 15))); + @override + void init(List keypair, [List? aad]) { + super.init(keypair); + _aadLength = aad?.length ?? 0; + if (aad != null && _aadLength > 0) { + super.add(aad); + super.add(Uint8List(16 - (_aadLength & 15))); } + _messageLength = 0; + } - // add lengths - sink.add(Uint32List.fromList([ - aadLength, - aadLength >>> 32, - messageLength, - messageLength >>> 32, - ]).buffer.asUint8List()); - - return sink.digest(); + @override + void add(List data, [int start = 0, int? end]) { + end ??= data.length; + _messageLength += end - start; + super.add(data, start, end); } @override - bool verify( - List message, - List tag, { - List? nonce, - List? aad, - }) { - var current = digest( - message, - nonce: nonce, - aad: aad, - ); - return current.isEqual(tag); + HashDigest digest() { + if (_messageLength > 0) { + super.add(Uint8List(16 - (_messageLength & 15))); + } + + super.add(Uint32List.fromList([ + _aadLength, + _aadLength >>> 32, + _messageLength, + _messageLength >>> 32, + ]).buffer.asUint8List()); + return super.digest(); } } diff --git a/lib/src/algorithms/salsa20.dart b/lib/src/algorithms/salsa20.dart index 605564d..3c1b72d 100644 --- a/lib/src/algorithms/salsa20.dart +++ b/lib/src/algorithms/salsa20.dart @@ -50,7 +50,7 @@ class Salsa20 extends SymmetricCipher { } @override - Stream pipe( + Stream bind( Stream stream, { List? nonce, }) async* { diff --git a/lib/src/algorithms/salsa20_poly1305.dart b/lib/src/algorithms/salsa20_poly1305.dart index 67c4949..cfa9975 100644 --- a/lib/src/algorithms/salsa20_poly1305.dart +++ b/lib/src/algorithms/salsa20_poly1305.dart @@ -1,21 +1,128 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:async'; import 'dart:typed_data'; +import 'package:cipherlib/src/core/authenticator.dart'; +import 'package:cipherlib/src/core/chunk_stream.dart'; +import 'package:hashlib/hashlib.dart' show HashDigest; + import 'salsa20.dart'; import 'poly1305.dart'; /// Salsa20-Poly1305 is a cryptographic algorithm combining the [Salsa20] -/// stream cipher for encryption and the [Poly1305Authenticator] for message -/// authentication. -class Salsa20Poly1305 extends Salsa20 with Poly1305Authenticator { +/// stream cipher for encryption and the [Poly1305Mac] for generating message +/// authentication code. +class Salsa20Poly1305 extends Salsa20 with Authenticator { @override String get name => "${super.name}/Poly1305"; const Salsa20Poly1305(List key) : super(key); + /// Generate One-Time-Key for Poly1305 + @pragma('vm:prefer-inline') + Uint8List _generateOTK([List? nonce]) => convert( + Uint8List(32), + nonce: nonce, + ); + @override - Uint8List generateOTK([List? nonce]) => - convert(Uint8List(32), nonce: nonce); + HashDigest digest( + List message, { + List? nonce, + List? aad, + }) => + Poly1305Mac( + _generateOTK(nonce), + aad: aad, + ).convert(message); + + @override + bool verify( + List message, + List mac, { + List? nonce, + List? aad, + }) => + digest( + message, + nonce: nonce, + aad: aad, + ).isEqual(mac); + + @override + CipherMAC convertWithDigest( + List message, { + List? mac, + List? nonce, + List? aad, + }) { + var otk = _generateOTK(nonce); + if (mac != null) { + var digest = Poly1305Mac(otk, aad: aad).convert(message); + if (!digest.isEqual(mac)) { + throw StateError('Invalid MAC'); + } + } + var cipher = convert( + message, + nonce: nonce, + ); + var digest = Poly1305Mac(otk, aad: aad).convert(cipher); + return CipherMAC(cipher, digest); + } + + @override + AsyncCipherMAC streamWithDigest( + Stream stream, { + Future? mac, + List? nonce, + List? aad, + }) { + var controller = StreamController(sync: true); + return AsyncCipherMAC( + controller.stream, + $buildDigest( + controller, + stream, + mac: mac, + nonce: nonce, + aad: aad, + ), + ); + } + + Future $buildDigest( + StreamController controller, + Stream stream, { + Future? mac, + List? nonce, + List? aad, + }) async { + var otk = _generateOTK(nonce); + var sink = mac != null ? Poly1305Mac(otk, aad: aad).createSink() : null; + // create digest sink for cipher + var cipherSink = Poly1305Mac(otk, aad: aad).createSink(); + // cipher stream + var it = generate(nonce).iterator; + await for (var buffer in asChunkedStream(4096, stream)) { + sink?.add(buffer); + for (int p = 0; p < buffer.length; ++p) { + it.moveNext(); + buffer[p] ^= it.current; + controller.add(buffer[p]); + } + cipherSink.add(buffer); + } + controller.close(); + // message digest + if (sink != null && mac != null) { + if (!sink.digest().isEqual(await mac)) { + throw StateError('Invalid MAC'); + } + } + // cipher digest + return cipherSink.digest(); + } } diff --git a/lib/src/algorithms/xor.dart b/lib/src/algorithms/xor.dart index 98d6731..ef3b294 100644 --- a/lib/src/algorithms/xor.dart +++ b/lib/src/algorithms/xor.dart @@ -45,7 +45,7 @@ class XOR extends SymmetricCipher { } @override - Stream pipe(Stream stream) async* { + Stream bind(Stream stream) async* { int i = 0; await for (var x in stream) { yield x ^ key[i++]; diff --git a/lib/src/chacha20.dart b/lib/src/chacha20.dart index 289ac4d..c01a5a5 100644 --- a/lib/src/chacha20.dart +++ b/lib/src/chacha20.dart @@ -9,10 +9,11 @@ export 'algorithms/chacha20.dart' show ChaCha20; /// Apply [ChaCha20] cipher with the follwing parameters: /// -/// - Arbitrary length plaintext [message] to transform. -/// - A 16 or 32-bytes long [key]. -/// - (Optional) A 16-bytes long [nonce]. Default: 0 -/// - (Optional) The initial block number as [blockCount]. Default: 1. +/// Parameters: +/// - [message] : arbitrary length plain-text. +/// - [key] : A 32-bytes long key. +/// - [nonce] : A 12-bytes long nonce. Deafult: 0 +/// - [blockId] : The initial block number. Default: 1. /// /// Both the encryption and decryption can be done using this same method. @pragma('vm:prefer-inline') @@ -20,31 +21,32 @@ Uint8List chacha20( List message, List key, [ List? nonce, - int blockCount = 1, + int blockId = 1, ]) => ChaCha20(key).convert( message, nonce: nonce, - blockCount: blockCount, + blockId: blockId, ); /// Apply [ChaCha20] cipher with the follwing parameters: /// -/// - Plaintext message [stream] to transform. -/// - A 16 or 32-bytes long [key]. -/// - (Optional) A 16-bytes long [nonce]. Default: 0 -/// - (Optional) The initial block number as [blockCount]. Default: 1. +/// Parameters: +/// - [stream] : arbitrary length plain-text. +/// - [key] : A 32-bytes long key. +/// - [nonce] : A 12-bytes long nonce. Deafult: 0 +/// - [blockId] : The initial block number. Default: 1. /// /// Both the encryption and decryption can be done using this same method. @pragma('vm:prefer-inline') -Stream chacha20Pipe( +Stream chacha20Stream( Stream stream, List key, [ List? nonce, - int blockCount = 1, + int blockId = 1, ]) => - ChaCha20(key).pipe( + ChaCha20(key).bind( stream, nonce: nonce, - blockId: blockCount, + blockId: blockId, ); diff --git a/lib/src/chacha20_poly1305.dart b/lib/src/chacha20_poly1305.dart index 849505b..363e422 100644 --- a/lib/src/chacha20_poly1305.dart +++ b/lib/src/chacha20_poly1305.dart @@ -2,54 +2,103 @@ // All rights reserved. Check LICENSE file for details. import 'package:cipherlib/src/algorithms/chacha20_poly1305.dart'; -import 'package:cipherlib/src/core/auth_cipher.dart'; -import 'package:hashlib/hashlib.dart'; +import 'package:cipherlib/src/core/authenticator.dart'; +import 'package:hashlib/hashlib.dart' show HashDigest; export 'algorithms/chacha20_poly1305.dart' show ChaCha20Poly1305; +/// Generate only the [message] digest using [ChaCha20Poly1305]. +/// +/// Parameters: +/// - [message] : arbitrary length plain-text. +/// - [key] : A 32-bytes long key. +/// - [nonce] : A 12-bytes long nonce. Deafult: 0 +/// - [aad] : Additional authenticated data. +HashDigest chacha20poly1305digest( + List message, + List key, { + List? nonce, + List? aad, +}) => + ChaCha20Poly1305(key).digest( + message, + nonce: nonce, + aad: aad, + ); + +/// Verify the [message] digest using [ChaCha20Poly1305]. +/// +/// Parameters: +/// - [message] : arbitrary length plain-text. +/// - [key] : A 32-bytes long key. +/// - [nonce] : A 12-bytes long nonce. Deafult: 0 +/// - [aad] : Additional authenticated data. +bool chacha20poly1305verify( + List message, + List key, + List mac, { + List? nonce, + List? aad, +}) => + ChaCha20Poly1305(key).verify( + message, + mac, + nonce: nonce, + aad: aad, + ); + /// Transforms [message] with ChaCha20 algorithm and generates the message /// digest with Poly1305 authentication code generator. /// /// Parameters: /// - [message] : arbitrary length plain-text. -/// - [key] : A 256-bit or 32-bytes long key. -/// - [nonce] : A 96-bit or 12-bytes long nonce. +/// - [key] : A 32-bytes long key. +/// - [nonce] : A 12-bytes long nonce. Deafult: 0 /// - [aad] : Additional authenticated data. -/// - [tag] : A 128-bit or 16-bytes long authentication tag for verification. +/// - [mac] : A 128-bit or 16-bytes long authentication tag for verification. +/// - [blockId] : The initial block number. Default: 1. /// /// Both the encryption and decryption can be done using this same method. CipherMAC chacha20poly1305( List message, List key, { - List? tag, + List? mac, List? nonce, List? aad, -}) { - var instance = ChaCha20Poly1305(key); - if (tag != null) { - if (!instance.verify(message, tag, nonce: nonce, aad: aad)) { - throw StateError('Invalid tag'); - } - } - var cipher = instance.convert(message, nonce: nonce); - var cipherTag = instance.digest(cipher, nonce: nonce, aad: aad); - return CipherMAC(cipher, cipherTag); -} + int blockId = 1, +}) => + ChaCha20Poly1305(key).convertWithDigest( + message, + mac: mac, + nonce: nonce, + aad: aad, + blockId: blockId, + ); -/// Generate only the [message] digest using [ChaCha20Poly1305]. +/// Transforms [stream] with ChaCha20 algorithm and generates the message +/// digest with Poly1305 authentication code generator. /// /// Parameters: -/// - [message] : arbitrary length plain-text. -/// - [key] : A 256-bit or 32-bytes long key. -/// - [nonce] : A 96-bit or 12-bytes long nonce. +/// - [stream] : arbitrary length plain-text. +/// - [key] : A 32-bytes long key. +/// - [nonce] : A 12-bytes long nonce. Deafult: 0 /// - [aad] : Additional authenticated data. +/// - [mac] : A 128-bit or 16-bytes long authentication tag for verification. +/// - [blockId] : The initial block number. Default: 1. /// /// Both the encryption and decryption can be done using this same method. -HashDigest chacha20poly1305digest( - List message, +AsyncCipherMAC chacha20poly1305Stream( + Stream stream, List key, { + Future? mac, List? nonce, List? aad, -}) { - return ChaCha20Poly1305(key).digest(message, nonce: nonce, aad: aad); -} + int blockId = 1, +}) => + ChaCha20Poly1305(key).streamWithDigest( + stream, + nonce: nonce, + mac: mac, + aad: aad, + blockId: blockId, + ); diff --git a/lib/src/core/auth_cipher.dart b/lib/src/core/auth_cipher.dart deleted file mode 100644 index a9df0a0..0000000 --- a/lib/src/core/auth_cipher.dart +++ /dev/null @@ -1,30 +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 HashDigest; - -/// Mixin for ciphers relying on authentication tag. -abstract class Authenticator { - /// Generates the authentication tag for the [message]. - HashDigest digest(List message); - - /// Verify the [message] against the authentication [tag]. - bool verify(List message, List tag) { - var current = digest(message); - return current.isEqual(tag); - } -} - -/// Combined result of encrypted [cipher] text with the authentication [tag]. -class CipherMAC { - /// The authentication tag. - final HashDigest tag; - - /// The cipher text. - final Uint8List cipher; - - /// Creates a new instance of [CipherMAC] - const CipherMAC(this.cipher, this.tag); -} diff --git a/lib/src/core/authenticator.dart b/lib/src/core/authenticator.dart new file mode 100644 index 0000000..e90a13f --- /dev/null +++ b/lib/src/core/authenticator.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2024, Sudipto Chandra +// All rights reserved. Check LICENSE file for details. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:hashlib/hashlib.dart' show HashDigest; + +/// Mixin for ciphers relying on authentication tag. +abstract class Authenticator { + /// Generates the authentication tag for the [message]. + HashDigest digest(List message); + + /// Verify the [message] against the authentication [mac]. + bool verify( + List message, + List mac, + ) { + var current = digest(message); + return current.isEqual(mac); + } + + /// Transforms the [message] with an authentication tag. + /// If [mac] is provided, it verifies the message integrity first. + CipherMAC convertWithDigest( + List message, { + List? mac, + }); + + /// Transforms the [stream] with an autentication tag. + /// If [mac] is provided, it verifies the message integrity first. + AsyncCipherMAC streamWithDigest( + Stream stream, { + Future? mac, + }); +} + +/// Combined result of encrypted [cipher] text with an authentication [mac]. +class CipherMAC { + /// The cipher text. + final Uint8List cipher; + + /// The authentication tag. + final HashDigest mac; + + const CipherMAC(this.cipher, this.mac); +} + +/// Combined result of encrypted [cipher] text with an authentication [mac]. +class AsyncCipherMAC { + /// The cipher text. + final Stream cipher; + + /// The authentication tag. + final Future mac; + + const AsyncCipherMAC(this.cipher, this.mac); +} diff --git a/lib/src/core/cipher.dart b/lib/src/core/cipher.dart index b494d9f..34fbc05 100644 --- a/lib/src/core/cipher.dart +++ b/lib/src/core/cipher.dart @@ -1,9 +1,10 @@ // Copyright (c) 2023, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:async'; import 'dart:typed_data'; -/// A template for encryption and decryption algorithms. +/// Template for encryption and decryption algorithms. abstract class Cipher { const Cipher(); @@ -11,23 +12,27 @@ abstract class Cipher { String get name; } -/// A template for Symmetric Ciphers that use the same operation for both +/// Template for Symmetric Ciphers that uses the same operation for both /// encryption and decryption. -abstract class SymmetricCipher extends Cipher { +abstract class SymmetricCipher extends Cipher + implements StreamTransformer { + const SymmetricCipher(); + /// The symmetric key for both encryption and decryption List get key; - /// Create new instance - const SymmetricCipher(); - /// Transforms the [message]. Uint8List convert(List message); - /// Transforms the message [stream]. - Stream pipe(Stream stream); + @override + Stream bind(Stream stream); + + @override + StreamTransformer cast() => + StreamTransformer.castFrom(this); } -/// A template for Asymmetric Ciphers that use different operations for +/// Template for Asymmetric Ciphers that uses different operations for /// encryption and decryption. abstract class AsymmetricCipher extends Cipher { const AsymmetricCipher(); @@ -38,9 +43,15 @@ abstract class AsymmetricCipher extends Cipher { /// The cipher algorithm for decryption. SymmetricCipher get decryptor; - /// Transforms the plain text [message] into encrypted cipher code. + /// Encrypts the [message] using the algorithm Uint8List encrypt(List message) => encryptor.convert(message); - /// Transforms the encrypted [cipher] code back to the plain text. + /// Decrypts the [cipher] using the algorithm Uint8List decrypt(List cipher) => decryptor.convert(cipher); + + /// Encrypts the [stream] using the algorithm + Stream encryptStream(Stream stream) => encryptor.bind(stream); + + /// Decrypts the [stream] using the algorithm + Stream decryptStream(Stream stream) => decryptor.bind(stream); } diff --git a/lib/src/salsa20.dart b/lib/src/salsa20.dart index 27e1bfe..0f55118 100644 --- a/lib/src/salsa20.dart +++ b/lib/src/salsa20.dart @@ -9,9 +9,10 @@ export 'algorithms/salsa20.dart' show Salsa20; /// Apply [Salsa20] cipher with the follwing parameters: /// -/// - Arbitrary length plaintext [message] to transform. -/// - A 16 or 32-bytes long [key]. -/// - (Optional) A 16-bytes long [nonce]. Default: 0 +/// Parameters: +/// - [message] : arbitrary length plain-text. +/// - [key] : A 16 or 32-bytes long key. +/// - [nonce] : A 16-bytes long nonce. Deafult: 0 /// /// Both the encryption and decryption can be done using this same method. @pragma('vm:prefer-inline') @@ -27,18 +28,19 @@ Uint8List salsa20( /// Apply [Salsa20] cipher with the follwing parameters: /// -/// - Plaintext message [stream] to transform. -/// - A 16 or 32-bytes long [key]. -/// - (Optional) A 16-bytes long [nonce]. Default: 0 +/// Parameters: +/// - [stream] : arbitrary length plain-text. +/// - [key] : A 16 or 32-bytes long key. +/// - [nonce] : A 16-bytes long nonce. Deafult: 0 /// /// Both the encryption and decryption can be done using this same method. @pragma('vm:prefer-inline') -Stream salsa20Pipe( +Stream salsa20Stream( Stream stream, List key, [ List? nonce, ]) => - Salsa20(key).pipe( + Salsa20(key).bind( stream, nonce: nonce, ); diff --git a/lib/src/salsa20_poly1305.dart b/lib/src/salsa20_poly1305.dart index ae75c50..2d49dab 100644 --- a/lib/src/salsa20_poly1305.dart +++ b/lib/src/salsa20_poly1305.dart @@ -2,54 +2,97 @@ // All rights reserved. Check LICENSE file for details. import 'package:cipherlib/src/algorithms/salsa20_poly1305.dart'; -import 'package:cipherlib/src/core/auth_cipher.dart'; -import 'package:hashlib/hashlib.dart'; +import 'package:cipherlib/src/core/authenticator.dart'; +import 'package:hashlib/hashlib.dart' show HashDigest; -export 'algorithms/chacha20_poly1305.dart' show ChaCha20Poly1305; +export 'algorithms/salsa20_poly1305.dart' show Salsa20Poly1305; -/// Transforms [message] with Salsa20 algorithm and generates the message digest -/// with Poly1305 authentication code generator. +/// Generate only the [message] digest using [Salsa20Poly1305]. /// /// Parameters: /// - [message] : arbitrary length plain-text. -/// - [key] : A 256-bit or 32-bytes long key. -/// - [nonce] : A 96-bit or 12-bytes long nonce. +/// - [key] : A 16 or 32-bytes long key. +/// - [nonce] : A 16-bytes long nonce. Deafult: 0 /// - [aad] : Additional authenticated data. -/// - [tag] : A 128-bit or 16-bytes long authentication tag for verification. +HashDigest salsa20poly1305digest( + List message, + List key, { + List? nonce, + List? aad, +}) => + Salsa20Poly1305(key).digest( + message, + nonce: nonce, + aad: aad, + ); + +/// Verify the [message] digest using [Salsa20Poly1305]. +/// +/// Parameters: +/// - [message] : arbitrary length plain-text. +/// - [key] : A 16 or 32-bytes long key. +/// - [nonce] : A 16-bytes long nonce. Deafult: 0 +/// - [aad] : Additional authenticated data. +bool salsa20poly1305verify( + List message, + List key, + List mac, { + List? nonce, + List? aad, +}) => + Salsa20Poly1305(key).verify( + message, + mac, + nonce: nonce, + aad: aad, + ); + +/// Transforms [message] with Salsa20 algorithm and generates the message +/// digest with Poly1305 authentication code generator. +/// +/// Parameters: +/// - [message] : arbitrary length plain-text. +/// - [key] : A 16 or 32-bytes long key. +/// - [nonce] : A 16-bytes long nonce. Deafult: 0 +/// - [aad] : Additional authenticated data. +/// - [mac] : A 128-bit or 16-bytes long authentication tag for verification. /// /// Both the encryption and decryption can be done using this same method. CipherMAC salsa20poly1305( List message, List key, { - List? tag, + List? mac, List? nonce, List? aad, -}) { - var instance = Salsa20Poly1305(key); - if (tag != null) { - if (!instance.verify(message, tag, nonce: nonce, aad: aad)) { - throw StateError('Invalid tag'); - } - } - var cipher = instance.convert(message, nonce: nonce); - var cipherTag = instance.digest(cipher, nonce: nonce, aad: aad); - return CipherMAC(cipher, cipherTag); -} +}) => + Salsa20Poly1305(key).convertWithDigest( + message, + mac: mac, + nonce: nonce, + aad: aad, + ); -/// Generate only the [message] digest using [Salsa20Poly1305]. +/// Transforms [stream] with Salsa20 algorithm and generates the message +/// digest with Poly1305 authentication code generator. /// /// Parameters: -/// - [message] : arbitrary length plain-text. -/// - [key] : A 256-bit or 32-bytes long key. -/// - [nonce] : A 96-bit or 12-bytes long nonce. +/// - [stream] : arbitrary length plain-text. +/// - [key] : A 16 or 32-bytes long key. +/// - [nonce] : A 16-bytes long nonce. Deafult: 0 /// - [aad] : Additional authenticated data. +/// - [mac] : A 128-bit or 16-bytes long authentication tag for verification. /// /// Both the encryption and decryption can be done using this same method. -HashDigest salsa20poly1305digest( - List message, +AsyncCipherMAC salsa20poly1305Stream( + Stream stream, List key, { + Future? mac, List? nonce, List? aad, -}) { - return Salsa20Poly1305(key).digest(message, nonce: nonce, aad: aad); -} +}) => + Salsa20Poly1305(key).streamWithDigest( + stream, + mac: mac, + nonce: nonce, + aad: aad, + ); diff --git a/lib/src/xor.dart b/lib/src/xor.dart index 455cd29..5e1d133 100644 --- a/lib/src/xor.dart +++ b/lib/src/xor.dart @@ -21,4 +21,5 @@ Uint8List xor(List message, List key) => XOR(key).convert(message); /// /// **WARNING**: This is not intended to be used for security purposes. @pragma('vm:prefer-inline') -Stream xorPipe(Stream stream, List key) => XOR(key).pipe(stream); +Stream xorStream(Stream stream, List key) => + XOR(key).bind(stream); diff --git a/pubspec.yaml b/pubspec.yaml index b3cf106..d0f4e51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ environment: sdk: '>=2.14.0 <4.0.0' dependencies: - hashlib: ^1.13.0 + hashlib: ^1.13.1 hashlib_codecs: ^2.2.0 dev_dependencies: diff --git a/test/chacha20_poly1305_test.dart b/test/chacha20_poly1305_test.dart index cfb757e..eb6fbdc 100644 --- a/test/chacha20_poly1305_test.dart +++ b/test/chacha20_poly1305_test.dart @@ -1,10 +1,15 @@ // Copyright (c) 2024, Sudipto Chandra // All rights reserved. Check LICENSE file for details. +import 'dart:async'; +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('Test ChaCha20/Poly1305 cipher', () { group('RFC 8439 example', () { @@ -27,7 +32,6 @@ void main() { "3ff4def08e4b7a9de576d26586cec64b" "6116", ); - var tag = fromHex("1ae10b594f09e26a7e902ecbd0600691"); test('convert', () { var res = chacha20poly1305( sample.codeUnits, @@ -36,9 +40,30 @@ void main() { aad: aad, ); expect(cipher, equals(res.cipher)); - expect(tag, equals(res.tag)); + expect('1ae10b594f09e26a7e902ecbd0600691', equals(res.mac.hex())); + }); + test('stream', () async { + var stream = Stream.fromIterable(sample.codeUnits); + var res = chacha20poly1305Stream( + stream, + key, + nonce: nonce, + aad: aad, + ); + var mac = await res.mac; + expect(cipher, equals(await res.cipher.toList())); + expect('1ae10b594f09e26a7e902ecbd0600691', equals(mac.hex())); + }); + test('convert without aad', () { + var res = chacha20poly1305( + sample.codeUnits, + key, + nonce: nonce, + ); + expect(cipher, equals(res.cipher)); + expect('6a23a4681fd59456aea1d29f82477216', equals(res.mac.hex())); }); - test('verify', () { + test('verify and decrypt', () { var res = chacha20poly1305( sample.codeUnits, key, @@ -48,12 +73,79 @@ void main() { var verified = chacha20poly1305( res.cipher, key, - tag: res.tag.bytes, + mac: res.mac.bytes, nonce: nonce, aad: aad, ); - expect(sample.codeUnits, equals(verified.cipher)); + expect(verified.cipher, equals(sample.codeUnits)); + expect('661e943467edb1963bfe9015190609f0', equals(verified.mac.hex())); }); + test('stream verify and decrypt', () async { + var stream = Stream.fromIterable(sample.codeUnits); + var res = chacha20poly1305Stream( + stream, + key, + nonce: nonce, + aad: aad, + ); + var verified = chacha20poly1305Stream( + res.cipher, + key, + mac: res.mac, + nonce: nonce, + aad: aad, + ); + var finalMac = await verified.mac; + expect(sample.codeUnits, equals(await verified.cipher.toList())); + expect('1ae10b594f09e26a7e902ecbd0600691', equals(await res.mac)); + expect('661e943467edb1963bfe9015190609f0', equals(finalMac.hex())); + }); + }); + test('encryption <-> decryption (convert)', () { + for (int i = 1; i < 100; ++i) { + var key = randomNumbers(32); + var nonce = randomBytes(12); + for (int j = 0; j < 100; ++j) { + var text = randomNumbers(j); + var plain = Uint8List.fromList(text); + var res = chacha20poly1305( + plain, + key, + nonce: nonce, + ); + var verified = chacha20poly1305( + res.cipher, + key, + mac: res.mac.bytes, + nonce: nonce, + ); + expect(plain, equals(verified.cipher), reason: '[key: $i, text: $j]'); + } + } + }); + test('encryption <-> decryption (stream)', () async { + for (int i = 1; i < 10; ++i) { + var key = randomNumbers(32); + var nonce = randomBytes(12); + for (int j = 0; j < 100; ++j) { + var text = randomNumbers(j); + var bytes = Uint8List.fromList(text); + var stream = Stream.fromIterable(text); + var res = chacha20poly1305Stream( + stream, + key, + nonce: nonce, + ); + var verified = chacha20poly1305Stream( + res.cipher, + key, + nonce: nonce, + mac: res.mac, + ); + var plain = await verified.cipher.toList(); + expect(bytes, equals(plain), reason: '[key: $i, text: $j]'); + } + } }); }); } diff --git a/test/chacha20_test.dart b/test/chacha20_test.dart index ccd83e8..46fffbe 100644 --- a/test/chacha20_test.dart +++ b/test/chacha20_test.dart @@ -42,8 +42,8 @@ void main() { var iv = randomNumbers(12); var text = randomBytes(100); var instance = ChaCha20(key); - var cipher = instance.convert(text, nonce: iv, blockCount: nos); - var plain = instance.convert(cipher, nonce: iv, blockCount: nos); + var cipher = instance.convert(text, nonce: iv, blockId: nos); + var plain = instance.convert(cipher, nonce: iv, blockId: nos); expect(text, equals(plain)); }); test('RFC 8439 example-1', () { @@ -109,8 +109,8 @@ void main() { var text = randomNumbers(j); var bytes = Uint8List.fromList(text); var stream = Stream.fromIterable(text); - var cipherStream = chacha20Pipe(stream, key, nonce); - var plainStream = chacha20Pipe(cipherStream, key, nonce); + var cipherStream = chacha20Stream(stream, key, nonce); + var plainStream = chacha20Stream(cipherStream, key, nonce); var plain = await plainStream.toList(); expect(bytes, equals(plain), reason: '[key: $i, text: $j]'); } diff --git a/test/salsa20_poly1305_test.dart b/test/salsa20_poly1305_test.dart new file mode 100644 index 0000000..0d285e7 --- /dev/null +++ b/test/salsa20_poly1305_test.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2024, Sudipto Chandra +// All rights reserved. Check LICENSE file for details. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:cipherlib/cipherlib.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('Test Salsa20/Poly1305 cipher', () { + test('encryption <-> decryption (convert)', () { + for (int i = 1; i < 100; ++i) { + var key = randomNumbers(32); + var nonce = randomBytes(12); + for (int j = 0; j < 100; ++j) { + var text = randomNumbers(j); + var plain = Uint8List.fromList(text); + var res = chacha20poly1305( + plain, + key, + nonce: nonce, + ); + var verified = chacha20poly1305( + res.cipher, + key, + mac: res.mac.bytes, + nonce: nonce, + ); + expect(plain, equals(verified.cipher), reason: '[key: $i, text: $j]'); + } + } + }); + test('encryption <-> decryption (stream)', () async { + for (int i = 1; i < 10; ++i) { + var key = randomNumbers(32); + var nonce = randomBytes(12); + for (int j = 0; j < 100; ++j) { + var text = randomNumbers(j); + var bytes = Uint8List.fromList(text); + var stream = Stream.fromIterable(text); + var res = chacha20poly1305Stream( + stream, + key, + nonce: nonce, + ); + var verified = chacha20poly1305Stream( + res.cipher, + key, + nonce: nonce, + mac: res.mac, + ); + var plain = await verified.cipher.toList(); + expect(bytes, equals(plain), reason: '[key: $i, text: $j]'); + } + } + }); + }); +} diff --git a/test/salsa20_test.dart b/test/salsa20_test.dart index 2fa7e4c..179aeda 100644 --- a/test/salsa20_test.dart +++ b/test/salsa20_test.dart @@ -87,8 +87,8 @@ void main() { var text = randomNumbers(j); var bytes = Uint8List.fromList(text); var stream = Stream.fromIterable(text); - var cipherStream = salsa20Pipe(stream, key, nonce); - var plainStream = salsa20Pipe(cipherStream, key, nonce); + var cipherStream = salsa20Stream(stream, key, nonce); + var plainStream = salsa20Stream(cipherStream, key, nonce); var plain = await plainStream.toList(); expect(bytes, equals(plain), reason: '[key: $i, text: $j]'); } diff --git a/test/xor_test.dart b/test/xor_test.dart index 8f0ee06..02cad52 100644 --- a/test/xor_test.dart +++ b/test/xor_test.dart @@ -35,8 +35,8 @@ void main() { var text = randomNumbers(j); var bytes = Uint8List.fromList(text); var stream = Stream.fromIterable(text); - var cipherStream = xorPipe(stream, key); - var plainStream = xorPipe(cipherStream, key); + var cipherStream = xorStream(stream, key); + var plainStream = xorStream(cipherStream, key); var plain = await plainStream.toList(); expect(bytes, equals(plain), reason: '[key: $i, text: $j]'); }