diff --git a/CHANGELOG.md b/CHANGELOG.md index 8159d65..fb2e599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [2.20.0] - 2026-06-30 +- **BREAKING**: Bumped the minimum Dart SDK constraint to `3.0.0` [#23]. Thanks [@vicajilau]. +- **BREAKING**: Declared `OpenSSHKeyPair` as a `mixin class` to comply with Dart 3.0 class modifier rules [#23]. Thanks [@vicajilau]. +- Offloaded all cryptographic key exchange (KEX) calculations to background isolates using `Isolate.run` on platforms that support it, preventing the Flutter main thread from blocking/freezing during connection [#23]. Thanks [@vicajilau]. +- Refactored internal key exchange isolate communication payloads (X25519, NIST Curves, DH) to use Dart 3.0 type-safe Records [#23]. Thanks [@vicajilau]. + ## [2.19.0] - 2026-06-30 - Added tolerant HTTP-date parsing to accept all RFC 7231 §7.1.1.1 HTTP-date formats (`IMF-fixdate`, `RFC 850`, `asctime`) for HTTP response headers [#170]. Thanks [@GT-610]. - Added chunked transfer-encoding decoding for HTTP response bodies according to RFC 7230 §4.1, improving interoperability with HTTP/1.1 servers [#171]. Thanks [@GT-610]. @@ -241,6 +247,7 @@ [#71]: https://github.com/TerminalStudio/dartssh2/issues/71 [#50]: https://github.com/TerminalStudio/dartssh2/issues/50 [#24]: https://github.com/TerminalStudio/dartssh2/issues/24 +[#23]: https://github.com/TerminalStudio/dartssh2/issues/23 [#21]: https://github.com/TerminalStudio/dartssh2/issues/21 [#18]: https://github.com/TerminalStudio/dartssh2/issues/18 [#17]: https://github.com/TerminalStudio/dartssh2/issues/17 diff --git a/README.md b/README.md index 6c6dae3..24f79da 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e - **Authentication**: Supports password, private key and interactive authentication method. - **Forwarding**: Supports local forwarding, remote forwarding, and dynamic forwarding (SOCKS5 CONNECT). - **SFTP**: Supports all operations defined in [SFTPv3 protocol](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02) including upload, download, list, link, remove, rename, etc. +- **Non-blocking Key Exchange**: Automatically offloads heavy key exchange calculations (X25519, NIST Curves, DH) to background isolates on supported VM platforms, preventing the main UI thread from freezing during connection. ## 🧬 Built with dartssh2 @@ -370,15 +371,15 @@ void main() async { } ``` -Decrypt PEM file with [`compute`](https://api.flutter.dev/flutter/foundation/compute-constant.html) in Flutter +Decrypt PEM file with [`compute`](https://api.flutter.dev/flutter/foundation/compute-constant.html) in Flutter: ```dart void main() async { - List decryptKeyPairs(List args) { - return SSHKeyPair.fromPem(args[0], args[1]); + List decryptKeyPairs((String pem, String passphrase) args) { + return SSHKeyPair.fromPem(args.$1, args.$2); } - final keypairs = await compute(decryptKeyPairs, ['', '']); + final keypairs = await compute(decryptKeyPairs, ('', '')); } ``` diff --git a/lib/src/kex/kex_dh.dart b/lib/src/kex/kex_dh.dart index bf83cd4..c493e44 100644 --- a/lib/src/kex/kex_dh.dart +++ b/lib/src/kex/kex_dh.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:dartssh2/src/ssh_kex.dart'; import 'package:dartssh2/src/utils/bigint.dart'; import 'package:dartssh2/src/utils/list.dart'; +import 'package:dartssh2/src/utils/compute.dart'; /// The Diffie-Hellman (DH) key exchange provides a shared secret that /// cannot be determined by either party alone. @@ -35,6 +36,14 @@ class SSHKexDH implements SSHKex { e = g.modPow(x, p); } + SSHKexDH._({ + required this.p, + required this.g, + required this.secretBits, + required this.x, + required this.e, + }); + /// https://tools.ietf.org/html/rfc2409 Second Oakley Group factory SSHKexDH.group1() { return SSHKexDH(p: _group1Prime, g: BigInt.from(2), secretBits: 160); @@ -45,10 +54,54 @@ class SSHKexDH implements SSHKex { return SSHKexDH(p: _group14Prime, g: BigInt.from(2), secretBits: 224); } + static Future createAsync({ + required BigInt p, + required BigInt g, + required int secretBits, + }) async { + final (x, e) = await sshCompute(_computeDHKeyPair, (p, g, secretBits)); + return SSHKexDH._( + p: p, + g: g, + secretBits: secretBits, + x: x, + e: e, + ); + } + + static Future group1Async() { + return createAsync(p: _group1Prime, g: BigInt.from(2), secretBits: 160); + } + + static Future group14Async() { + return createAsync(p: _group14Prime, g: BigInt.from(2), secretBits: 224); + } + /// Compute the shared secret K BigInt computeSecret(BigInt f) { return f.modPow(x, p); } + + Future computeSecretAsync(BigInt f) async { + return sshCompute(_computeDHSecret, (f, x, p)); + } +} + +(BigInt, BigInt) _computeDHKeyPair((BigInt, BigInt, int) args) { + final p = args.$1; + final g = args.$2; + final secretBits = args.$3; + + final x = decodeBigIntWithSign(1, randomBytes(secretBits ~/ 8)); + final e = g.modPow(x, p); + return (x, e); +} + +BigInt _computeDHSecret((BigInt, BigInt, BigInt) args) { + final f = args.$1; + final x = args.$2; + final p = args.$3; + return f.modPow(x, p); } final _group1Prime = decodeBigIntWithSign( diff --git a/lib/src/kex/kex_nist.dart b/lib/src/kex/kex_nist.dart index 23df1a8..b53e70b 100644 --- a/lib/src/kex/kex_nist.dart +++ b/lib/src/kex/kex_nist.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:dartssh2/src/ssh_kex.dart'; import 'package:dartssh2/src/utils/bigint.dart'; import 'package:dartssh2/src/utils/list.dart'; +import 'package:dartssh2/src/utils/compute.dart'; import 'package:pointycastle/ecc/curves/secp256r1.dart'; import 'package:pointycastle/ecc/curves/secp384r1.dart'; import 'package:pointycastle/ecc/curves/secp521r1.dart'; @@ -31,12 +32,38 @@ class SSHKexNist implements SSHKexECDH { publicKey = c!.getEncoded(false); } + SSHKexNist._({ + required this.curve, + required this.secretBits, + required this.privateKey, + required this.publicKey, + }); + SSHKexNist.p256() : this(curve: ECCurve_secp256r1(), secretBits: 256); SSHKexNist.p384() : this(curve: ECCurve_secp384r1(), secretBits: 384); SSHKexNist.p521() : this(curve: ECCurve_secp521r1(), secretBits: 521); + static Future p256Async() => createAsync('p256'); + + static Future p384Async() => createAsync('p384'); + + static Future p521Async() => createAsync('p521'); + + static Future createAsync(String curveName) async { + final (privateKey, publicKey) = + await sshCompute(_computeNistKeyPair, curveName); + final curve = _getCurveByName(curveName); + final secretBits = _getSecretBitsByName(curveName); + return SSHKexNist._( + curve: curve, + secretBits: secretBits, + privateKey: privateKey, + publicKey: publicKey, + ); + } + /// Compute shared secret. @override BigInt computeSecret(Uint8List remotePubilcKey) { @@ -44,6 +71,14 @@ class SSHKexNist implements SSHKexECDH { return (s * privateKey)!.x!.toBigInteger()!; } + Future computeSecretAsync(Uint8List remotePublicKey) async { + final curveName = _getNameByCurve(curve); + return sshCompute( + _computeNistSecret, + (curveName, privateKey, remotePublicKey), + ); + } + BigInt _generatePrivateKey() { late BigInt x; do { @@ -52,3 +87,61 @@ class SSHKexNist implements SSHKexECDH { return x; } } + +ECDomainParameters _getCurveByName(String name) { + switch (name) { + case 'p256': + return ECCurve_secp256r1(); + case 'p384': + return ECCurve_secp384r1(); + case 'p521': + return ECCurve_secp521r1(); + default: + throw ArgumentError('Unknown curve name: $name'); + } +} + +int _getSecretBitsByName(String name) { + switch (name) { + case 'p256': + return 256; + case 'p384': + return 384; + case 'p521': + return 521; + default: + throw ArgumentError('Unknown curve name: $name'); + } +} + +String _getNameByCurve(ECDomainParameters curve) { + if (curve is ECCurve_secp256r1) return 'p256'; + if (curve is ECCurve_secp384r1) return 'p384'; + if (curve is ECCurve_secp521r1) return 'p521'; + throw ArgumentError('Unknown curve type: $curve'); +} + +(BigInt, Uint8List) _computeNistKeyPair(String curveName) { + final curve = _getCurveByName(curveName); + final secretBits = _getSecretBitsByName(curveName); + + late BigInt x; + do { + x = decodeBigIntWithSign(1, randomBytes(secretBits ~/ 8)) % curve.n; + } while (x == BigInt.zero); + + final c = curve.G * x; + final publicKey = c!.getEncoded(false); + + return (x, publicKey); +} + +BigInt _computeNistSecret((String, BigInt, Uint8List) args) { + final curveName = args.$1; + final privateKey = args.$2; + final remotePublicKey = args.$3; + + final curve = _getCurveByName(curveName); + final s = curve.curve.decodePoint(remotePublicKey)!; + return (s * privateKey)!.x!.toBigInteger()!; +} diff --git a/lib/src/kex/kex_x25519.dart b/lib/src/kex/kex_x25519.dart index 77a8ff6..5803cc2 100644 --- a/lib/src/kex/kex_x25519.dart +++ b/lib/src/kex/kex_x25519.dart @@ -1,21 +1,37 @@ import 'dart:typed_data'; import 'package:dartssh2/src/ssh_kex.dart'; +import 'package:dartssh2/src/utils/compute.dart'; import 'package:dartssh2/src/utils/bigint.dart'; import 'package:dartssh2/src/utils/list.dart'; import 'package:pinenacl/tweetnacl.dart'; class SSHKexX25519 implements SSHKexECDH { /// Randomly generated private key. - late final Uint8List privateKey; + final Uint8List privateKey; /// Public key computed from the private key. @override - late final Uint8List publicKey; + final Uint8List publicKey; + + factory SSHKexX25519() { + final privateKey = randomBytes(32); + final publicKey = _ScalarMult.scalseMultBase(privateKey); + return SSHKexX25519._( + privateKey: privateKey, + publicKey: publicKey, + ); + } + + SSHKexX25519._({required this.privateKey, required this.publicKey}); - SSHKexX25519() { - privateKey = randomBytes(32); - publicKey = _ScalarMult.scalseMultBase(privateKey); + static Future createAsync() async { + final (privateKey, publicKey) = + await sshCompute(_computeX25519KeyPair, null); + return SSHKexX25519._( + privateKey: privateKey, + publicKey: publicKey, + ); } @override @@ -23,6 +39,24 @@ class SSHKexX25519 implements SSHKexECDH { final secret = _ScalarMult.scalseMult(privateKey, remotePublicKey); return decodeBigIntWithSign(1, secret); } + + Future computeSecretAsync(Uint8List remotePublicKey) async { + final secret = await sshCompute( + _computeX25519Secret, + (privateKey, remotePublicKey), + ); + return decodeBigIntWithSign(1, secret); + } +} + +(Uint8List, Uint8List) _computeX25519KeyPair(void _) { + final privateKey = randomBytes(32); + final publicKey = _ScalarMult.scalseMultBase(privateKey); + return (privateKey, publicKey); +} + +Uint8List _computeX25519Secret((Uint8List, Uint8List) data) { + return _ScalarMult.scalseMult(data.$1, data.$2); } /// Scalar multiplication, Implements curve25519. diff --git a/lib/src/ssh_key_pair.dart b/lib/src/ssh_key_pair.dart index 52c2a17..fef8782 100644 --- a/lib/src/ssh_key_pair.dart +++ b/lib/src/ssh_key_pair.dart @@ -280,7 +280,7 @@ class OpenSSHBcryptKdfOptions implements OpenSSHKdfOptions { } } -abstract class OpenSSHKeyPair implements SSHKeyPair { +abstract mixin class OpenSSHKeyPair implements SSHKeyPair { void writeTo(SSHMessageWriter writer); @override diff --git a/lib/src/ssh_transport.dart b/lib/src/ssh_transport.dart index d05a769..410e0c3 100644 --- a/lib/src/ssh_transport.dart +++ b/lib/src/ssh_transport.dart @@ -128,6 +128,9 @@ class SSHTransport { /// transport is closed. StreamSubscription? _socketSubscription; + /// Guards asynchronous packet processing to preserve message order. + var _isProcessingData = false; + /// Identification string sent by us without trailing \r\n. For example, /// "SSH-2.0-DartSSH_2.0". String get _localVersion => 'SSH-2.0-$version'; @@ -472,13 +475,7 @@ class SSHTransport { /// Callback triggered when new raw bytes are received from the socket. void _onSocketData(Uint8List data) { _buffer.add(data); - try { - _processData(); - } on SSHError catch (e, stackTrace) { - closeWithError(e, stackTrace); - } catch (e) { - rethrow; - } + _scheduleProcessData(); } /// Callback triggered when an error occurs on the socket stream. @@ -493,12 +490,33 @@ class SSHTransport { close(); } - /// Orchestrates processing of the current buffered data. - void _processData() { + void _scheduleProcessData() { + if (_isProcessingData || isClosed) { + return; + } + + _isProcessingData = true; + + _processDataAsync().catchError((error, stackTrace) { + if (error is SSHError) { + closeWithError(error, stackTrace); + } else { + closeWithError(SSHInternalError(error), stackTrace); + } + }).whenComplete(() { + _isProcessingData = false; + if (_buffer.isNotEmpty && !isClosed) { + _scheduleProcessData(); + } + }); + } + + Future _processDataAsync() async { if (_remoteVersion == null) { _processVersionExchange(); - } else { - _processPackets(); + } + if (_remoteVersion != null) { + await _processPackets(); } } @@ -541,12 +559,12 @@ class SSHTransport { _sendKexInit(); } - // There maybe more data in the buffer, so process it. - _processPackets(); + // There maybe more data in the buffer, so it will be consumed by the + // asynchronous packet processing queue. } /// Process one or more SSH packets queued in [_buffer]. - void _processPackets() { + Future _processPackets() async { printDebug?.call('SSHTransport._processPackets'); while (_buffer.isNotEmpty && !isClosed) { @@ -559,7 +577,7 @@ class SSHTransport { // throw SSHPacketError('Packet too long: ${payload.length}'); // } - _handleMessage(payload); + await _handleMessage(payload); _remotePacketSN.increase(); } @@ -1071,7 +1089,7 @@ class SSHTransport { } /// Dispatches the incoming decrypted packet payload to the appropriate message handler. - void _handleMessage(Uint8List message) { + Future _handleMessage(Uint8List message) async { final messageId = SSHMessage.readMessageId(message); switch (messageId) { case SSH_Message_KexInit.messageId: @@ -1087,7 +1105,7 @@ class SSHTransport { } /// Processes the KEXINIT message received from the remote peer and negotiates algorithms. - void _handleMessageKexInit(Uint8List payload) { + Future _handleMessageKexInit(Uint8List payload) async { printDebug?.call('SSHTransport._handleMessageKexInit'); // If this message initiates a new key-exchange round from the remote @@ -1165,23 +1183,23 @@ class SSHTransport { switch (_kexType) { case SSHKexType.x25519: - _kex = SSHKexX25519(); + _kex = await SSHKexX25519.createAsync(); break; case SSHKexType.nistp256: - _kex = SSHKexNist.p256(); + _kex = await SSHKexNist.p256Async(); break; case SSHKexType.nistp384: - _kex = SSHKexNist.p384(); + _kex = await SSHKexNist.p384Async(); break; case SSHKexType.nistp521: - _kex = SSHKexNist.p521(); + _kex = await SSHKexNist.p521Async(); break; case SSHKexType.dh14Sha1: case SSHKexType.dh14Sha256: - _kex = SSHKexDH.group14(); + _kex = await SSHKexDH.group14Async(); break; case SSHKexType.dh1Sha1: - _kex = SSHKexDH.group1(); + _kex = await SSHKexDH.group1Async(); break; case SSHKexType.dhGexSha1: case SSHKexType.dhGexSha256: @@ -1199,7 +1217,7 @@ class SSHTransport { /// When client receives [SSH_Message_KexECDH_Reply], it should verify the /// server's signature with the server's public key. Then send NEW_KEYS /// message back to the server. - void _handleMessageKexReply(Uint8List payload) { + Future _handleMessageKexReply(Uint8List payload) async { printDebug?.call('SSHTransport._handleMessageKexReply'); if (isServer) throw SSHStateError('Unexpected KEX_REPLY'); @@ -1233,7 +1251,7 @@ class SSHTransport { hostSignature = message.signature; serverKexKey = encodeBigInt(message.f); clientKexKey = encodeBigInt(kex.e); - sharedSecret = kex.computeSecret(message.f); + sharedSecret = await kex.computeSecretAsync(message.f); } else if (kex is SSHKexECDH) { final message = SSH_Message_KexECDH_Reply.decode(payload); printTrace?.call('<- $socket: $message'); @@ -1241,7 +1259,13 @@ class SSHTransport { hostSignature = message.signature; serverKexKey = message.ecdhPublicKey; clientKexKey = kex.publicKey; - sharedSecret = kex.computeSecret(message.ecdhPublicKey); + if (kex is SSHKexX25519) { + sharedSecret = await kex.computeSecretAsync(message.ecdhPublicKey); + } else if (kex is SSHKexNist) { + sharedSecret = await kex.computeSecretAsync(message.ecdhPublicKey); + } else { + sharedSecret = kex.computeSecret(message.ecdhPublicKey); + } } else { throw UnimplementedError('$kex'); } @@ -1281,40 +1305,35 @@ class SSHTransport { } final userVerified = onVerifyHostKey != null - ? onVerifyHostKey!(_hostkeyType!.name, fingerprint) + ? await Future.value(onVerifyHostKey!(_hostkeyType!.name, fingerprint)) : true; - Future.value(userVerified).then( - (verified) { - if (!verified) { - closeWithError(SSHHostkeyError('Hostkey verification failed')); - } else { - _hostkeyVerified = true; - _sendNewKeys(); - _applyLocalKeys(); - onReady?.call(); - } - }, - onError: (error) { - closeWithError(error); - }, - ); + if (!userVerified) { + closeWithError(SSHHostkeyError('Hostkey verification failed')); + return; + } + + _hostkeyVerified = true; + _sendNewKeys(); + _applyLocalKeys(); + onReady?.call(); } /// Processes the Group Exchange Reply (GEX Group) message containing Diffie-Hellman params. - void _handleMessageKexGexReply(Uint8List payload) { + Future _handleMessageKexGexReply(Uint8List payload) async { printDebug?.call('SSHTransport._handleMessageKexGexReply'); if (isServer) throw SSHStateError('Unexpected KEX_GEX_REPLY'); final message = SSH_Message_KexDH_GexGroup.decode(payload); printTrace?.call('<- $socket: $message'); - _kex = SSHKexDH(p: message.p, g: message.g, secretBits: 256); + _kex = + await SSHKexDH.createAsync(p: message.p, g: message.g, secretBits: 256); _sendKexDHGexInit(); } /// Handles the NEWKEYS message, activating the remote decryption keys and flushing queued packets. - void _handleMessageNewKeys(Uint8List message) { + Future _handleMessageNewKeys(Uint8List message) async { printDebug?.call('SSHTransport._handleMessageNewKeys'); printTrace?.call('<- $socket: SSH_Message_NewKeys'); diff --git a/lib/src/utils/compute.dart b/lib/src/utils/compute.dart new file mode 100644 index 0000000..9bc081c --- /dev/null +++ b/lib/src/utils/compute.dart @@ -0,0 +1,7 @@ +import 'compute_stub.dart' if (dart.library.isolate) 'compute_io.dart'; + +typedef SSHComputeCallback = R Function(M message); + +Future sshCompute(SSHComputeCallback callback, M message) { + return sshComputeImpl(callback, message); +} diff --git a/lib/src/utils/compute_io.dart b/lib/src/utils/compute_io.dart new file mode 100644 index 0000000..47622af --- /dev/null +++ b/lib/src/utils/compute_io.dart @@ -0,0 +1,8 @@ +import 'dart:isolate'; + +Future sshComputeImpl( + R Function(M message) callback, + M message, +) { + return Isolate.run(() => callback(message)); +} diff --git a/lib/src/utils/compute_stub.dart b/lib/src/utils/compute_stub.dart new file mode 100644 index 0000000..5fdd1a2 --- /dev/null +++ b/lib/src/utils/compute_stub.dart @@ -0,0 +1,6 @@ +Future sshComputeImpl( + R Function(M message) callback, + M message, +) { + return Future.sync(() => callback(message)); +} diff --git a/pubspec.yaml b/pubspec.yaml index 0af7c6a..27c6933 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: dartssh2 -version: 2.19.0 +version: 2.20.0 description: SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as easy to use. homepage: https://github.com/TerminalStudio/dartssh2 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: asn1lib: ^1.5.8 diff --git a/test/src/channel/ssh_channel_test.dart b/test/src/channel/ssh_channel_test.dart index 17183e1..9f6420a 100644 --- a/test/src/channel/ssh_channel_test.dart +++ b/test/src/channel/ssh_channel_test.dart @@ -1,5 +1,5 @@ @Tags(['integration']) -library ssh_channel_test; +library; import 'package:dartssh2/dartssh2.dart'; import 'package:test/test.dart'; diff --git a/test/src/kex/kex_dh_test.dart b/test/src/kex/kex_dh_test.dart index 8f9990c..e32d193 100644 --- a/test/src/kex/kex_dh_test.dart +++ b/test/src/kex/kex_dh_test.dart @@ -58,6 +58,38 @@ void main() { expect(computedSecret, equals(isA())); }); }); + + group('SSHKexDH (Async)', () { + test('generate keys and compute shared secret asynchronously (Group 1)', + () async { + final kex1 = await SSHKexDH.group1Async(); + final kex2 = await SSHKexDH.group1Async(); + final secret1 = await kex1.computeSecretAsync(kex2.e); + final secret2 = await kex2.computeSecretAsync(kex1.e); + expect(secret1, equals(secret2)); + }); + + test('generate keys and compute shared secret asynchronously (Group 14)', + () async { + final kex1 = await SSHKexDH.group14Async(); + final kex2 = await SSHKexDH.group14Async(); + final secret1 = await kex1.computeSecretAsync(kex2.e); + final secret2 = await kex2.computeSecretAsync(kex1.e); + expect(secret1, equals(secret2)); + }); + + test( + 'generate keys and compute shared secret asynchronously (Custom Group)', + () async { + final kex1 = await SSHKexDH.createAsync( + p: _group1Prime, g: BigInt.from(2), secretBits: 160); + final kex2 = await SSHKexDH.createAsync( + p: _group1Prime, g: BigInt.from(2), secretBits: 160); + final secret1 = await kex1.computeSecretAsync(kex2.e); + final secret2 = await kex2.computeSecretAsync(kex1.e); + expect(secret1, equals(secret2)); + }); + }); } // Predefined group 1 prime diff --git a/test/src/kex/kex_nist_test.dart b/test/src/kex/kex_nist_test.dart index ebe24dd..9205114 100644 --- a/test/src/kex/kex_nist_test.dart +++ b/test/src/kex/kex_nist_test.dart @@ -67,4 +67,33 @@ void main() { reason: 'Private key should be less than curve order.'); }); }); + + group('SSHKexNist (Async)', () { + test('generate keys and compute shared secret asynchronously (P-256)', + () async { + final kex1 = await SSHKexNist.p256Async(); + final kex2 = await SSHKexNist.p256Async(); + final secret1 = await kex1.computeSecretAsync(kex2.publicKey); + final secret2 = await kex2.computeSecretAsync(kex1.publicKey); + expect(secret1, equals(secret2)); + }); + + test('generate keys and compute shared secret asynchronously (P-384)', + () async { + final kex1 = await SSHKexNist.p384Async(); + final kex2 = await SSHKexNist.p384Async(); + final secret1 = await kex1.computeSecretAsync(kex2.publicKey); + final secret2 = await kex2.computeSecretAsync(kex1.publicKey); + expect(secret1, equals(secret2)); + }); + + test('generate keys and compute shared secret asynchronously (P-521)', + () async { + final kex1 = await SSHKexNist.p521Async(); + final kex2 = await SSHKexNist.p521Async(); + final secret1 = await kex1.computeSecretAsync(kex2.publicKey); + final secret2 = await kex2.computeSecretAsync(kex1.publicKey); + expect(secret1, equals(secret2)); + }); + }); } diff --git a/test/src/kex/kex_x25519_test.dart b/test/src/kex/kex_x25519_test.dart index f6ded39..9794ec9 100644 --- a/test/src/kex/kex_x25519_test.dart +++ b/test/src/kex/kex_x25519_test.dart @@ -61,4 +61,17 @@ void main() { expect(validSharedSecret.bitLength, greaterThan(0)); }); }); + + group('SSHKexX25519 (Async)', () { + test('generate keys and compute shared secret asynchronously', () async { + final kex1 = await SSHKexX25519.createAsync(); + final kex2 = await SSHKexX25519.createAsync(); + expect(kex1.privateKey.length, equals(32)); + expect(kex1.publicKey.length, equals(32)); + + final secret1 = await kex1.computeSecretAsync(kex2.publicKey); + final secret2 = await kex2.computeSecretAsync(kex1.publicKey); + expect(secret1, equals(secret2)); + }); + }); } diff --git a/test/src/sftp/sftp_client_test.dart b/test/src/sftp/sftp_client_test.dart index 8cd8bc7..486d475 100644 --- a/test/src/sftp/sftp_client_test.dart +++ b/test/src/sftp/sftp_client_test.dart @@ -1,5 +1,5 @@ @Tags(['integration']) -library sftp_client_test; +library; import 'dart:io'; diff --git a/test/src/sftp/sftp_stream_io_test.dart b/test/src/sftp/sftp_stream_io_test.dart index 84b149f..4e6f77c 100644 --- a/test/src/sftp/sftp_stream_io_test.dart +++ b/test/src/sftp/sftp_stream_io_test.dart @@ -1,5 +1,5 @@ @Tags(['integration']) -library sftp_stream_io_test; +library; import 'package:dartssh2/dartssh2.dart'; import 'package:test/test.dart'; diff --git a/test/src/socket/ssh_socket_io_test.dart b/test/src/socket/ssh_socket_io_test.dart index 9ae9fe5..f08b6ae 100644 --- a/test/src/socket/ssh_socket_io_test.dart +++ b/test/src/socket/ssh_socket_io_test.dart @@ -1,5 +1,5 @@ @Tags(['integration']) -library ssh_socket_io_test; +library; import 'package:dartssh2/dartssh2.dart'; import 'package:test/test.dart'; diff --git a/test/src/ssh_client_test.dart b/test/src/ssh_client_test.dart index 555ce4e..12a6f2e 100644 --- a/test/src/ssh_client_test.dart +++ b/test/src/ssh_client_test.dart @@ -1,5 +1,5 @@ @Tags(['integration']) -library ssh_client_test; +library; import 'dart:convert'; diff --git a/test/src/ssh_transport_aead_test.dart b/test/src/ssh_transport_aead_test.dart index 096d125..36b4f7f 100644 --- a/test/src/ssh_transport_aead_test.dart +++ b/test/src/ssh_transport_aead_test.dart @@ -311,7 +311,7 @@ void main() { transport.close(); }); - test('kexinit allows missing MAC when AEAD cipher is selected', () { + test('kexinit allows missing MAC when AEAD cipher is selected', () async { for (final cipherType in [ SSHCipherType.aes128gcm, SSHCipherType.aes256gcm @@ -340,11 +340,9 @@ void main() { firstKexPacketFollows: false, ).encode(); - expect( - () => reflect(transport) - .invoke(privateSymbol('_handleMessageKexInit'), [payload]), - returnsNormally, - ); + final result = reflect(transport).invoke( + privateSymbol('_handleMessageKexInit'), [payload]).reflectee; + await expectLater(result, completes); transport.close(); } @@ -435,7 +433,8 @@ void main() { transport.close(); }); - test('kexinit requires client MAC when non-AEAD cipher is selected', () { + test('kexinit requires client MAC when non-AEAD cipher is selected', + () async { final socket = _CaptureSSHSocket(); final transport = SSHTransport( socket, @@ -460,16 +459,22 @@ void main() { firstKexPacketFollows: false, ).encode(); - expect( - () => reflect(transport) - .invoke(privateSymbol('_handleMessageKexInit'), [payload]), + await expectLater( + () async { + final result = reflect(transport).invoke( + privateSymbol('_handleMessageKexInit'), [payload]).reflectee; + if (result is Future) { + await result; + } + }, throwsA(isA()), ); transport.close(); }); - test('kexinit requires server MAC when non-AEAD cipher is selected', () { + test('kexinit requires server MAC when non-AEAD cipher is selected', + () async { final socket = _CaptureSSHSocket(); final transport = SSHTransport( socket, @@ -494,9 +499,14 @@ void main() { firstKexPacketFollows: false, ).encode(); - expect( - () => reflect(transport) - .invoke(privateSymbol('_handleMessageKexInit'), [payload]), + await expectLater( + () async { + final result = reflect(transport).invoke( + privateSymbol('_handleMessageKexInit'), [payload]).reflectee; + if (result is Future) { + await result; + } + }, throwsA(isA()), );