Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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].
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<SSHKeyPair> decryptKeyPairs(List<String> args) {
return SSHKeyPair.fromPem(args[0], args[1]);
List<SSHKeyPair> decryptKeyPairs((String pem, String passphrase) args) {
return SSHKeyPair.fromPem(args.$1, args.$2);
}

final keypairs = await compute(decryptKeyPairs, ['<pem text>', '<passphrase>']);
final keypairs = await compute(decryptKeyPairs, ('<pem text>', '<passphrase>'));
}
```

Expand Down
53 changes: 53 additions & 0 deletions lib/src/kex/kex_dh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -45,10 +54,54 @@ class SSHKexDH implements SSHKex {
return SSHKexDH(p: _group14Prime, g: BigInt.from(2), secretBits: 224);
}

static Future<SSHKexDH> 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<SSHKexDH> group1Async() {
return createAsync(p: _group1Prime, g: BigInt.from(2), secretBits: 160);
}

static Future<SSHKexDH> 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<BigInt> 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(
Expand Down
93 changes: 93 additions & 0 deletions lib/src/kex/kex_nist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -31,19 +32,53 @@ 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<SSHKexNist> p256Async() => createAsync('p256');

static Future<SSHKexNist> p384Async() => createAsync('p384');

static Future<SSHKexNist> p521Async() => createAsync('p521');

static Future<SSHKexNist> 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) {
final s = curve.curve.decodePoint(remotePubilcKey)!;
return (s * privateKey)!.x!.toBigInteger()!;
}

Future<BigInt> computeSecretAsync(Uint8List remotePublicKey) async {
final curveName = _getNameByCurve(curve);
return sshCompute(
_computeNistSecret,
(curveName, privateKey, remotePublicKey),
);
}

BigInt _generatePrivateKey() {
late BigInt x;
do {
Expand All @@ -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()!;
}
44 changes: 39 additions & 5 deletions lib/src/kex/kex_x25519.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,62 @@
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<SSHKexX25519> createAsync() async {
final (privateKey, publicKey) =
await sshCompute(_computeX25519KeyPair, null);
return SSHKexX25519._(
privateKey: privateKey,
publicKey: publicKey,
);
}

@override
BigInt computeSecret(Uint8List remotePublicKey) {
final secret = _ScalarMult.scalseMult(privateKey, remotePublicKey);
return decodeBigIntWithSign(1, secret);
}

Future<BigInt> 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.
Expand Down
2 changes: 1 addition & 1 deletion lib/src/ssh_key_pair.dart
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ class OpenSSHBcryptKdfOptions implements OpenSSHKdfOptions {
}
}

abstract class OpenSSHKeyPair implements SSHKeyPair {
abstract mixin class OpenSSHKeyPair implements SSHKeyPair {
void writeTo(SSHMessageWriter writer);

@override
Expand Down
Loading
Loading