diff --git a/packages/simplexmq/lib/simplexmq.dart b/packages/simplexmq/lib/simplexmq.dart index 2d08580d0..fcec668b9 100644 --- a/packages/simplexmq/lib/simplexmq.dart +++ b/packages/simplexmq/lib/simplexmq.dart @@ -4,4 +4,4 @@ library simplexmq; export 'src/protocol.dart'; -export 'src/transport.dart' show Transport; +export 'src/transport.dart' show Transport, SMPTransportClient; diff --git a/packages/simplexmq/lib/src/buffer.dart b/packages/simplexmq/lib/src/buffer.dart index c700a95a5..4f5cbe13b 100644 --- a/packages/simplexmq/lib/src/buffer.dart +++ b/packages/simplexmq/lib/src/buffer.dart @@ -130,3 +130,15 @@ Uint8List? decode64(Uint8List b64) { return bytes; } + +Uint8List encodeInt32(int n) { + final data = Uint8List(4); + ByteData.sublistView(data).setInt32(0, n); + return data; +} + +Uint8List encodeInt16(int n) { + final data = Uint8List(2); + ByteData.sublistView(data).setInt16(0, n); + return data; +} diff --git a/packages/simplexmq/lib/src/crypto.dart b/packages/simplexmq/lib/src/crypto.dart index 9cd168487..5a184c114 100644 --- a/packages/simplexmq/lib/src/crypto.dart +++ b/packages/simplexmq/lib/src/crypto.dart @@ -19,7 +19,7 @@ class AESKey { static AESKey decode(Uint8List rawKey) => AESKey._make(rawKey); - Uint8List encode() => _key; + Uint8List get bytes => _key; } Uint8List randomIV() { diff --git a/packages/simplexmq/lib/src/transport.dart b/packages/simplexmq/lib/src/transport.dart index 93444d12a..baa030a43 100644 --- a/packages/simplexmq/lib/src/transport.dart +++ b/packages/simplexmq/lib/src/transport.dart @@ -1,5 +1,9 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:pointycastle/asymmetric/api.dart'; +import 'buffer.dart'; +import 'crypto.dart'; +import 'rsa_keys.dart'; abstract class Transport { Future read(int n); @@ -15,3 +19,127 @@ Stream blockStream(Transport t, int blockSize) async* { return; } } + +class SMPServer { + final String host; + final int? port; + final Uint8List? keyHash; + SMPServer(this.host, [this.port, this.keyHash]); +} + +class SessionKey { + final AESKey aesKey; + final Uint8List baseIV; + int _counter = 0; + SessionKey._(this.aesKey, this.baseIV); + static SessionKey create() => SessionKey._(AESKey.random(), randomIV()); + Uint8List serialize() => concat(aesKey.bytes, baseIV); +} + +class ServerHeader { + final int blockSize; + final int keySize; + ServerHeader(this.blockSize, this.keySize); +} + +class ServerHandshake { + final RSAPublicKey publicKey; + final int blockSize; + ServerHandshake(this.publicKey, this.blockSize); +} + +typedef SMPVersion = List; + +// SMPVersion _currentSMPVersion = const [0, 4, 1, 0]; +const int _serverHeaderSize = 8; +const int _binaryRsaTransport = 0; +const int _transportBlockSize = 4096; +const int _maxTransportBlockSize = 65536; + +class SMPTransportClient { + final Transport _conn; + final _sndKey = SessionKey.create(); + final _rcvKey = SessionKey.create(); + final int blockSize; + SMPTransportClient._(this._conn, this.blockSize); + + static Future connect(Transport conn, + {Uint8List? keyHash, int? blockSize}) { + return _clientHandshake(conn, keyHash, blockSize); + } + + static Future _clientHandshake( + Transport conn, Uint8List? keyHash, int? blkSize) async { + final srv = await _getHeaderAndPublicKey_1_2(conn, keyHash); + final t = SMPTransportClient._(conn, blkSize ?? srv.blockSize); + await t._sendEncryptedKeys_4(srv.publicKey); + _checkVersion(await t._getWelcome_6()); + return t; + } + + static Future _getHeaderAndPublicKey_1_2( + Transport conn, Uint8List? keyHash) async { + final srvHeader = parseServerHeader(await conn.read(_serverHeaderSize)); + final blkSize = srvHeader.blockSize; + if (blkSize < _transportBlockSize || blkSize > _maxTransportBlockSize) { + throw Exception('smp handshake header error: bad block size $blkSize'); + } + final rawKey = await conn.read(srvHeader.keySize); + if (keyHash != null) validateKeyHash_2(rawKey, keyHash); + final serverKey = decodeRsaPubKey(rawKey); + return ServerHandshake(serverKey, blkSize); + } + + static void validateKeyHash_2(Uint8List rawKey, Uint8List keyHash) { + // todo + } + + static ServerHeader parseServerHeader(Uint8List a) { + if (a.length != 8) { + throw Exception('smp handshake error: bad header size ${a.length}'); + } + final v = ByteData.sublistView(a); + final blockSize = v.getUint32(0); + final transportMode = v.getUint16(4); + if (transportMode != _binaryRsaTransport) { + throw Exception('smp handshake error: bad transport mode $transportMode'); + } + final keySize = v.getUint16(6); + return ServerHeader(blockSize, keySize); + } + + Future _sendEncryptedKeys_4(RSAPublicKey serverKey) => + _conn.write(encryptOAEP(serverKey, _clientHeader())); + + Uint8List _clientHeader() => concatN([ + encodeInt32(blockSize), + encodeInt16(_binaryRsaTransport), + _sndKey.serialize(), + _rcvKey.serialize() + ]); + + Future _getWelcome_6() async => + _parseSMPVersion(await _readEncrypted()); + + static SMPVersion _parseSMPVersion(Uint8List block) { + return []; + } + + static void _checkVersion(SMPVersion version) {} + + Future _readEncrypted() async { + final block = await _conn.read(blockSize); + final iv = _nextIV(_rcvKey); + return decryptAES(_rcvKey.aesKey, iv, block); + } + + static Uint8List _nextIV(SessionKey sk) { + final c = encodeInt32(sk._counter++); + final start = sk.baseIV.sublist(0, 4); + final rest = sk.baseIV.sublist(4); + for (int i = 0; i < 4; i++) { + start[i] ^= c[i]; + } + return concat(start, rest); + } +} diff --git a/packages/simplexmq_io/lib/src/socket.dart b/packages/simplexmq_io/lib/src/socket.dart index c418ac397..148f50508 100644 --- a/packages/simplexmq_io/lib/src/socket.dart +++ b/packages/simplexmq_io/lib/src/socket.dart @@ -20,16 +20,16 @@ class SocketTransport implements Transport { final int _bufferSize; Uint8List _buffer = Uint8List(0); final ListQueue<_STReaders> _readers = ListQueue(16); - SocketTransport._new(this._socket, this._timeout, this._bufferSize); + SocketTransport._(this._socket, this._timeout, this._bufferSize); static Future connect(String host, int port, {Duration timeout = const Duration(seconds: 1), int bufferSize = 16384}) async { final socket = await Socket.connect(host, port, timeout: timeout); - final t = SocketTransport._new(socket, timeout, bufferSize); + final t = SocketTransport._(socket, timeout, bufferSize); // ignore: cancel_subscriptions final subscription = socket.listen(t._onData, - onError: (Object e) => t._finalize, onDone: t._finalize); + onError: (Object e) => t.close, onDone: t.close); t._subscription = subscription; return t; } @@ -71,7 +71,8 @@ class SocketTransport implements Transport { } } - void _finalize() { + /// Close the client transport + void close() { _subscription.cancel(); _socket.destroy(); while (_readers.isNotEmpty) { @@ -79,9 +80,4 @@ class SocketTransport implements Transport { r.completer.completeError(Exception('socket closed')); } } - - /// Allow closing the client transport. - void close() { - _finalize(); - } } diff --git a/packages/simplexmq_io/test/transport_test.dart b/packages/simplexmq_io/test/transport_test.dart new file mode 100644 index 000000000..de3835319 --- /dev/null +++ b/packages/simplexmq_io/test/transport_test.dart @@ -0,0 +1,15 @@ +import 'package:simplexmq/simplexmq.dart'; +import 'package:simplexmq_io/simplexmq_io.dart'; +import 'package:test/test.dart'; + +void main() { + group('SMP transport', () { + test('establish connection (expects SMP server on localhost:5423)', + () async { + final conn = await SocketTransport.connect('localhost', 5423); + final smp = await SMPTransportClient.connect(conn); + print('connected'); + print(smp); + }, skip: 'socket does not connect'); + }); +}