├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── README.md ├── dartssh.dart ├── dartsshd.dart └── pubspec.yaml ├── lib ├── agent.dart ├── client.dart ├── http.dart ├── http_html.dart ├── http_io.dart ├── identity.dart ├── kex.dart ├── pem.dart ├── protocol.dart ├── serializable.dart ├── server.dart ├── socket.dart ├── socket_html.dart ├── socket_io.dart ├── ssh.dart ├── transport.dart ├── websocket_html.dart ├── websocket_io.dart └── zlib.dart ├── pubspec.lock ├── pubspec.yaml ├── test-coverage.sh └── test ├── dartssh_test.dart ├── ecdsa-sha2-nistp384 ├── id_ecdsa ├── id_ecdsa.pub ├── ssh_host_ecdsa_key └── ssh_host_ecdsa_key.pub ├── ecdsa-sha2-nistp521 ├── id_ecdsa ├── id_ecdsa.pub ├── ssh_host_ecdsa_key └── ssh_host_ecdsa_key.pub ├── id_ecdsa ├── id_ecdsa.pub ├── id_ed25519 ├── id_ed25519.pub ├── id_rsa ├── id_rsa.openssh ├── id_rsa.pub ├── ssh_host_ecdsa_key ├── ssh_host_ecdsa_key.pub ├── ssh_host_ed25519_key ├── ssh_host_ed25519_key.pub ├── ssh_host_rsa_key └── ssh_host_rsa_key.pub /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | language: dart 3 | 4 | addons: 5 | apt: 6 | # Flutter dependencies 7 | sources: 8 | - ubuntu-toolchain-r-test 9 | packages: 10 | - libstdc++6 11 | 12 | before_install: 13 | - echo $TRAVIS_OS_NAME 14 | - echo ${HOME} 15 | - echo ${TRAVIS_BUILD_DIR} 16 | - echo "repo_token:" $COVERALLS_REPO_TOKEN > .coveralls.yml 17 | - gem install coveralls-lcov 18 | - git clone https://github.com/flutter/flutter.git -b stable --depth 1 ${HOME}/flutter 19 | - ${HOME}/flutter/bin/flutter doctor -v 20 | - ${HOME}/flutter/bin/flutter packages get 21 | - ${HOME}/flutter/bin/flutter pub global activate coverage 0.13.3 22 | 23 | script: 24 | - ${HOME}/flutter/bin/flutter pub run test 25 | - ./test-coverage.sh 26 | 27 | after_success: 28 | - echo "success" 29 | - coveralls-lcov coverage/lcov.info 30 | 31 | after_failure: 32 | - echo "failure" 33 | 34 | env: 35 | global: 36 | secure: DIw0G0dfxPTGpTo1W8f9vswPoKLbT2S2G5wc1kq2zbrS8fpGk2wIQJzLfZn2I0e4V4nrTCRHfi8F2lCnLPNrGSXDtbRoDo6cSGIV+/jnJXqc0C6WC1H6Uz3Woo+DedoX7mp6XpUj20+AFOaQkecK9nyjZYmlzCi74YKXcEOgdkXAzTFm5P2xv3UNKjW7EYGx0CQBpuW5lKxRbhiLLy2d5DZ3NRKKCM7OuDsqJOEIcwLd0O+5at/W1IKDHvoDTadXdc/2Vm3pgBFWiAvFOkRPI8B8bDq+8Pd9oRyjYWEo0CRDREc+pORPaHscT9TfauuK2QxVeOc+iOCWLUEmIYgT5OhwagpMPpXZ168XnA64WcZXK1vEaa4dyUJaafw5NSR6AeBttc+qA2l2TYmKdL+DUALIlIsUXWI1MidvG71cYwapT6HmWu4Y50r17dAA96nkKXnCJFgBQi31wQxFJFd0HOOVBkmP7EuBsLLgmsv5cxiXsHy3P87O0/K/CCGVDDcEt5eCJpHb5cGiCoQOtYyX0LFHZ9qRiVaHacpOSF5l8DhzyfmeHYAXVCw47OYHAKxK9oJTELQvwUJW0o7C9GT91cXDVAt073+rBwn43hRKbcT8C6qs5Ucgl2VmVulI5VOKIn+Mz8vcNen5Cf5QV5ir9KNZ3evJlF1rzaqrffV3WhU= 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0+0 2 | 3 | - Initial release. 4 | 5 | ## 1.0.1+1 6 | 7 | - Add SSHTunneledSocketImpl, SSHTunneledWebSocketImpl, and SSHTunneledBaseClient. 8 | 9 | ## 1.0.2+2 10 | 11 | - Add example/README.md 12 | 13 | ## 1.0.3+3 14 | 15 | - Fix tunneled WebSocket issue. 16 | 17 | ## 1.0.4+4 18 | 19 | - Increase test coverage and documentation. 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Green Appers, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dartssh [![pub package](https://img.shields.io/pub/v/dartssh.svg)](https://pub.dartlang.org/packages/dartssh) [![Build Status](https://travis-ci.org/GreenAppers/dartssh.svg?branch=master)](https://travis-ci.org/GreenAppers/dartssh) [![Coverage Status](https://coveralls.io/repos/github/GreenAppers/dartssh/badge.svg?branch=master)](https://coveralls.io/github/GreenAppers/dartssh?branch=master) [![documentation](https://img.shields.io/badge/Documentation-dartssh-blue.svg)](https://www.dartdocs.org/documentation/dartssh/latest/) 2 | 3 | Dart SSH package providing First-class tunnelling primitives. 4 | 5 | ## Feature support 6 | 7 | Keys: Ed25519, ECDSA, RSA. 8 | KEX: X25519DH, ECDH, DHGEX, DH. 9 | Cipher: AES-CTR, AES-CBC. 10 | MAC: MD5, SHA. 11 | Compression: not yet supported. 12 | Forwarding: TCP/IP, Agent. 13 | Tunneling drop-ins for: Socket, WebSocket, package:http. 14 | 15 | ## Example 16 | 17 | See [example/dartssh.dart](example/dartssh.dart). 18 | 19 | ## Build 20 | 21 | Follow the same procedure as [the continuous integration](.travis.yml). 22 | 23 | ## License 24 | 25 | dartssh is released under the terms of the MIT license. See [LICENSE](LICENSE). 26 | 27 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.1.8.0.yaml 2 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # dartssh example 2 | 3 | Library providing a pure Dart SSH implementation. 4 | 5 | ## ssh 6 | 7 | [dartssh.dart](dartssh.dart) 8 | 9 | dart dartssh.dart -l user 10.0.0.1 10 | 11 | ## sshd 12 | 13 | [dartsshd.dart](dartsshd.dart) 14 | 15 | dart dartsshd.dart 16 | 17 | -------------------------------------------------------------------------------- /example/dartssh.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | import 'dart:typed_data'; 7 | 8 | import 'package:args/args.dart'; 9 | 10 | import 'package:dartssh/client.dart'; 11 | import 'package:dartssh/identity.dart'; 12 | import 'package:dartssh/pem.dart'; 13 | import 'package:dartssh/ssh.dart'; 14 | import 'package:dartssh/transport.dart'; 15 | 16 | Identity identity; 17 | SSHClient client; 18 | Channel forwardChannel; 19 | 20 | void main(List arguments) async { 21 | stdin.lineMode = false; 22 | stdin.echoMode = false; 23 | ProcessSignal.sigint.watch().listen((_) { 24 | if (client != null) send(Uint8List.fromList([3])); 25 | }); 26 | ProcessSignal.sigwinch.watch().listen((_) { 27 | if (client != null) { 28 | client.setTerminalWindowSize( 29 | stdout.terminalColumns, stdout.terminalLines); 30 | } 31 | }); 32 | exitCode = await ssh( 33 | arguments, stdin, (_, String v) => stdout.write(v), () => exit(0), 34 | termWidth: stdout.terminalColumns, termHeight: stdout.terminalLines); 35 | } 36 | 37 | void send(Uint8List x) { 38 | if (forwardChannel != null) { 39 | client.sendToChannel(forwardChannel, x); 40 | } else { 41 | client.sendChannelData(x); 42 | } 43 | } 44 | 45 | Future ssh(List arguments, Stream> input, 46 | ResponseCallback response, VoidCallback done, 47 | {int termWidth = 80, int termHeight = 25}) async { 48 | final argParser = ArgParser() 49 | ..addOption('login', abbr: 'l') 50 | ..addOption('identity', abbr: 'i') 51 | ..addOption('password') 52 | ..addOption('tunnel') 53 | ..addOption('kex') 54 | ..addOption('key') 55 | ..addOption('cipher') 56 | ..addOption('mac') 57 | ..addFlag('debug') 58 | ..addFlag('trace') 59 | ..addFlag('agentForwarding', abbr: 'A'); 60 | 61 | final ArgResults args = argParser.parse(arguments); 62 | 63 | identity = null; 64 | client = null; 65 | forwardChannel = null; 66 | 67 | if (args.rest.length != 1) { 68 | print('usage: ssh -l login url [args]'); 69 | print(argParser.usage); 70 | return 1; 71 | } 72 | 73 | final String host = args.rest.first, 74 | login = args['login'], 75 | identityFile = args['identity'], 76 | tunnel = args['tunnel']; 77 | 78 | if (login == null || login.isEmpty) { 79 | print('no login specified'); 80 | return 1; 81 | } 82 | 83 | if (tunnel != null && tunnel.split(':').length != 2) { 84 | print('tunnel target should be specified host:port'); 85 | return 2; 86 | } 87 | 88 | applyCipherSuiteOverrides( 89 | args['kex'], args['key'], args['cipher'], args['mac']); 90 | 91 | try { 92 | client = SSHClient( 93 | hostport: parseUri(host), 94 | login: login, 95 | print: print, 96 | termWidth: termWidth, 97 | termHeight: termHeight, 98 | termvar: Platform.environment['TERM'] ?? 'xterm', 99 | agentForwarding: args['agentForwarding'], 100 | debugPrint: args['debug'] ? print : null, 101 | tracePrint: args['trace'] ? print : null, 102 | getPassword: ((args['password'] != null) 103 | ? () => utf8.encode(args['password']) 104 | : null), 105 | response: response, 106 | loadIdentity: () { 107 | if (identity == null && identityFile != null) { 108 | identity = parsePem(File(identityFile).readAsStringSync()); 109 | } 110 | return identity; 111 | }, 112 | disconnected: done, 113 | startShell: tunnel == null, 114 | success: tunnel == null 115 | ? null 116 | : () { 117 | List tunnelTarget = tunnel.split(':'); 118 | forwardChannel = client.openTcpChannel( 119 | '127.0.0.1', 120 | 1234, 121 | tunnelTarget[0], 122 | int.parse(tunnelTarget[1]), 123 | (_, Uint8List m) => response(client, utf8.decode(m))); 124 | }); 125 | 126 | await for (String x in input.transform(utf8.decoder)) { 127 | send(utf8.encode(x)); 128 | } 129 | } catch (error, stacktrace) { 130 | print('ssh: exception: $error: $stacktrace'); 131 | return -1; 132 | } 133 | 134 | return 0; 135 | } 136 | -------------------------------------------------------------------------------- /example/dartsshd.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:convert'; 6 | import 'dart:io'; 7 | import 'dart:math'; 8 | import 'dart:typed_data'; 9 | 10 | import 'package:args/args.dart'; 11 | import 'package:stack_trace/stack_trace.dart'; 12 | 13 | import 'package:dartssh/agent.dart'; 14 | import 'package:dartssh/identity.dart'; 15 | import 'package:dartssh/pem.dart'; 16 | import 'package:dartssh/protocol.dart'; 17 | import 'package:dartssh/socket_io.dart'; 18 | import 'package:dartssh/server.dart'; 19 | import 'package:dartssh/ssh.dart'; 20 | import 'package:dartssh/transport.dart'; 21 | 22 | SSHServer server; 23 | 24 | void main(List arguments) async { 25 | exitCode = 0; 26 | await sshd(arguments); 27 | } 28 | 29 | Future sshd(List arguments) async { 30 | final argParser = ArgParser() 31 | ..addOption('port', abbr: 'p') 32 | ..addOption('config', abbr: 'f') 33 | ..addOption('hostkey', abbr: 'h') 34 | ..addOption('password') 35 | ..addOption('kex') 36 | ..addOption('key') 37 | ..addOption('cipher') 38 | ..addOption('mac') 39 | ..addFlag('debug') 40 | ..addFlag('trace') 41 | ..addFlag('forwardTcp'); 42 | 43 | final ArgResults args = argParser.parse(arguments); 44 | final int port = int.parse(args['port'] ?? '22'); 45 | final bool debug = args['debug'], forwardTcp = args['forwardTcp']; 46 | final Identity hostkey = loadHostKey(path: args['hostkey']); 47 | 48 | server = null; 49 | 50 | applyCipherSuiteOverrides( 51 | args['kex'], args['key'], args['cipher'], args['mac']); 52 | 53 | if (forwardTcp) { 54 | print( 55 | 'WARNING: Forwarding TCP connections is in effect running an open proxy.'); 56 | } 57 | 58 | try { 59 | await Chain.capture(() async { 60 | final ServerSocket listener = await ServerSocket.bind('0.0.0.0', port); 61 | 62 | await for (Socket socket in listener) { 63 | final String hostport = 64 | '${socket.remoteAddress.host}:${socket.remotePort}'; 65 | print('accepted $hostport'); 66 | StreamController input = StreamController(); 67 | bool done = false; 68 | Future pending; 69 | 70 | server = SSHServer( 71 | hostkey, 72 | socket: SocketImpl()..socket = socket, 73 | hostport: parseUri(hostport), 74 | print: print, 75 | debugPrint: debug ? print : null, 76 | tracePrint: args['trace'] ? print : null, 77 | response: (SSHTransport server, String v) { 78 | input.add(v); 79 | server.sendChannelData(utf8.encode(v)); 80 | }, 81 | userAuthRequest: (MSG_USERAUTH_REQUEST msg) { 82 | String requirePassword = args['password']; 83 | if ((requirePassword ?? '').isEmpty) { 84 | /// Graciously accept all authorization requests. 85 | return true; 86 | } else { 87 | return (msg.methodName ?? '') == 'password' && 88 | utf8.decode(msg.secret ?? '') == requirePassword; 89 | } 90 | }, 91 | sessionChannelRequest: (SSHServer server, String req) { 92 | if (req == 'shell') { 93 | server.sendChannelData(utf8.encode('\$ ')); 94 | return true; 95 | } else if (req == 'pty-req') { 96 | return true; 97 | } else { 98 | return false; 99 | } 100 | }, 101 | disconnected: () { 102 | if (debug) { 103 | print('dartsshd: $hostport: disconnected'); 104 | listener.close(); 105 | } 106 | }, 107 | directTcpRequest: forwardTcp ? forwardTcpChannel : null, 108 | ); 109 | 110 | input.stream.transform(LineSplitter()).listen((String line) { 111 | if (done) return; 112 | if (line == 'exit') { 113 | done = true; 114 | pending = chainWork(pending, 115 | () async => server.closeChannel(server.sessionChannel)); 116 | } else if (line == 'testAgent') { 117 | pending = chainWork(pending, () => testAgentForwarding()); 118 | } else if (line == 'testDebug') { 119 | server.writeCipher(MSG_DEBUG()); 120 | server.writeCipher(MSG_IGNORE()); 121 | server.sendChannelData(utf8.encode('success\n')); 122 | } 123 | }); 124 | } 125 | }); 126 | } catch (error, stacktrace) { 127 | print('sshd: exception: $error: $stacktrace'); 128 | exitCode = -1; 129 | } 130 | } 131 | 132 | Future chainWork(Future chain, FutureFunction x) => 133 | chain == null ? x() : chain.then((_) async => await x()); 134 | 135 | Identity loadHostKey({StringFunction getPassword, String path}) { 136 | Identity hostkey = Identity(); 137 | path ??= '/etc/ssh/ssh_host_'; 138 | try { 139 | parsePem(File('${path}ecdsa_key').readAsStringSync(), 140 | identity: hostkey, getPassword: getPassword); 141 | } catch (error) { 142 | print('open ${path}ecdsa_key failed'); 143 | } 144 | try { 145 | parsePem(File('${path}ed25519_key').readAsStringSync(), 146 | identity: hostkey, getPassword: getPassword); 147 | } catch (error) { 148 | print('open ${path}ed25519_key failed'); 149 | } 150 | try { 151 | parsePem(File('${path}rsa_key').readAsStringSync(), 152 | identity: hostkey, getPassword: getPassword); 153 | } catch (error) { 154 | print('open ${path}rsa_key failed'); 155 | } 156 | return hostkey; 157 | } 158 | 159 | Future forwardTcpChannel(Channel channel, String sourceHost, 160 | int sourcePort, String targetHost, int targetPort) async { 161 | SocketImpl tunneledSocket = SocketImpl(); 162 | Completer connectCompleter = Completer(); 163 | print('dartsshd: Forwarding connection to $targetHost:$targetPort'); 164 | tunneledSocket.connect( 165 | Uri.parse('tcp://$targetHost:$targetPort'), 166 | () => connectCompleter.complete(null), 167 | (String error) => connectCompleter.complete('$error')); 168 | String connectError = await connectCompleter.future; 169 | if (connectError != null) return connectError; 170 | 171 | StringCallback closeTunneledSocket = (String error) { 172 | print( 173 | "dartsshd: Closing forwarded connection to $targetHost:$targetPort: $error"); 174 | tunneledSocket.close(); 175 | server.closeChannel(channel); 176 | }; 177 | tunneledSocket.listen((Uint8List m) => server.sendToChannel(channel, m)); 178 | tunneledSocket.handleError(closeTunneledSocket); 179 | tunneledSocket.handleDone(closeTunneledSocket); 180 | 181 | channel.cb = (_, Uint8List m) => tunneledSocket.sendRaw(m); 182 | channel.error = closeTunneledSocket; 183 | channel.closed = () => closeTunneledSocket('remote closed'); 184 | return null; 185 | } 186 | 187 | Future testAgentForwarding() async { 188 | Channel agentChannel; 189 | Uint8List key, challenge; 190 | Completer openCompleter = Completer(); 191 | Completer doneCompleter = Completer(); 192 | agentChannel = server.openAgentChannel( 193 | (_, Uint8List read) => SSHAgentForwarding.dispatchAgentRead( 194 | agentChannel, read, (_, agentPacketS) { 195 | int agentPacketId = agentPacketS.getUint8(); 196 | switch (agentPacketId) { 197 | case AGENT_IDENTITIES_ANSWER.ID: 198 | AGENT_IDENTITIES_ANSWER msg = AGENT_IDENTITIES_ANSWER() 199 | ..deserialize(agentPacketS); 200 | assert(msg.keys.isNotEmpty); 201 | key = msg.keys.first.key; 202 | challenge = randBytes(Random.secure(), 16); 203 | server.sendToChannel( 204 | agentChannel, AGENTC_SIGN_REQUEST(key, challenge).toRaw()); 205 | break; 206 | 207 | case AGENT_SIGN_RESPONSE.ID: 208 | AGENT_SIGN_RESPONSE msg = AGENT_SIGN_RESPONSE() 209 | ..deserialize(agentPacketS); 210 | assert(msg.sig.isNotEmpty); 211 | doneCompleter.complete(null); 212 | break; 213 | 214 | default: 215 | break; 216 | } 217 | }), 218 | connected: () => openCompleter.complete(null), 219 | error: (String error) => openCompleter.complete('$error'), 220 | ); 221 | String openError = await openCompleter.future; 222 | if (openError != null) { 223 | server.sendChannelData(utf8.encode('error: $openError\n')); 224 | return; 225 | } 226 | server.sendToChannel(agentChannel, AGENTC_REQUEST_IDENTITIES().toRaw()); 227 | String doneError = await doneCompleter.future; 228 | if (doneError == null) { 229 | server.sendChannelData(utf8.encode('success\n')); 230 | } else { 231 | server.sendChannelData(utf8.encode('error: $doneError\n')); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ssh 2 | version: 1.0.0+0 3 | description: Command line SSH client with dartssh. 4 | author: Green Appers, Inc. 5 | homepage: https://github.com/GreenAppers/cruzawl 6 | documentation: https://pub.dev/documentation/dartssh/latest/ 7 | 8 | environment: 9 | sdk: ">=2.1.0 <3.0.0" 10 | 11 | dependencies: 12 | args: ^1.5.2 13 | dartssh: 14 | path: ../ 15 | stack_trace: ^1.9.3 16 | -------------------------------------------------------------------------------- /lib/agent.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:typed_data'; 5 | 6 | import 'package:dartssh/protocol.dart'; 7 | import 'package:dartssh/serializable.dart'; 8 | import 'package:dartssh/ssh.dart'; 9 | import 'package:dartssh/transport.dart'; 10 | 11 | /// Mixin providing SSH Agent forwarding. 12 | mixin SSHAgentForwarding on SSHTransport { 13 | /// Frames SSH Agent [channel] data into packets. 14 | void handleAgentRead(Channel channel, Uint8List msg) => 15 | dispatchAgentRead(channel, msg, handleAgentPacket); 16 | 17 | static void dispatchAgentRead( 18 | Channel channel, Uint8List msg, ChannelInputCallback handleAgentPacket) { 19 | channel.buf.add(msg); 20 | while (channel.buf.data.length > 4) { 21 | SerializableInput input = SerializableInput(channel.buf.data); 22 | int agentPacketLen = input.getUint32(); 23 | if (input.remaining < agentPacketLen) break; 24 | handleAgentPacket( 25 | channel, 26 | SerializableInput( 27 | input.viewOffset(input.offset, input.offset + agentPacketLen))); 28 | channel.buf.flush(agentPacketLen + 4); 29 | } 30 | } 31 | 32 | // Dispatches SSH Agent messages to handlers. 33 | void handleAgentPacket(Channel channel, SerializableInput agentPacketS) { 34 | int agentPacketId = agentPacketS.getUint8(); 35 | switch (agentPacketId) { 36 | case AGENTC_REQUEST_IDENTITIES.ID: 37 | handleAGENTC_REQUEST_IDENTITIES(channel); 38 | break; 39 | 40 | case AGENTC_SIGN_REQUEST.ID: 41 | handleAGENTC_SIGN_REQUEST( 42 | channel, AGENTC_SIGN_REQUEST()..deserialize(agentPacketS)); 43 | break; 44 | 45 | default: 46 | if (print != null) { 47 | print('$hostport: unknown agent packet number: $agentPacketId'); 48 | } 49 | break; 50 | } 51 | } 52 | 53 | /// Responds with any identities we're forwarding. 54 | void handleAGENTC_REQUEST_IDENTITIES(Channel channel) { 55 | if (tracePrint != null) { 56 | tracePrint('$hostport: agent channel: AGENTC_REQUEST_IDENTITIES'); 57 | } 58 | AGENT_IDENTITIES_ANSWER reply = AGENT_IDENTITIES_ANSWER(); 59 | if (identity != null) { 60 | reply.keys = identity.getRawPublicKeyList(); 61 | } 62 | sendToChannel(channel, reply.toRaw()); 63 | } 64 | 65 | /// Signs challenge authenticating a descendent channel. 66 | void handleAGENTC_SIGN_REQUEST(Channel channel, AGENTC_SIGN_REQUEST msg) { 67 | if (tracePrint != null) { 68 | tracePrint('$hostport: agent channel: AGENTC_SIGN_REQUEST'); 69 | } 70 | SerializableInput keyStream = SerializableInput(msg.key); 71 | String keyType = deserializeString(keyStream); 72 | Uint8List sig = 73 | identity.signMessage(Key.id(keyType), msg.data, getSecureRandom()); 74 | if (sig != null) { 75 | sendToChannel(channel, AGENT_SIGN_RESPONSE(sig).toRaw()); 76 | } else { 77 | sendToChannel(channel, AGENT_FAILURE().toRaw()); 78 | } 79 | } 80 | } 81 | 82 | /// https://tools.ietf.org/html/draft-miller-ssh-agent-03#section-3 83 | abstract class AgentMessage extends Serializable { 84 | int id; 85 | AgentMessage(this.id); 86 | 87 | Uint8List toRaw({Endian endian = Endian.big}) { 88 | Uint8List buffer = Uint8List(5 + serializedSize); 89 | SerializableOutput output = SerializableOutput(buffer); 90 | output.addUint32(buffer.length - 4); 91 | output.addUint8(id); 92 | serialize(output); 93 | if (!output.done) { 94 | throw FormatException('${output.offset}/${output.buffer.length}'); 95 | } 96 | return buffer; 97 | } 98 | } 99 | 100 | /// https://tools.ietf.org/html/draft-miller-ssh-agent-03#section-4.1 101 | class AGENT_FAILURE extends AgentMessage { 102 | static const int ID = 5; 103 | AGENT_FAILURE() : super(ID); 104 | 105 | @override 106 | int get serializedHeaderSize => 0; 107 | 108 | @override 109 | int get serializedSize => serializedHeaderSize; 110 | 111 | @override 112 | void deserialize(SerializableInput input) {} 113 | 114 | @override 115 | void serialize(SerializableOutput output) {} 116 | } 117 | 118 | /// https://tools.ietf.org/html/draft-miller-ssh-agent-03#section-4.4 119 | class AGENTC_REQUEST_IDENTITIES extends AgentMessage { 120 | static const int ID = 11; 121 | AGENTC_REQUEST_IDENTITIES() : super(ID); 122 | 123 | @override 124 | int get serializedHeaderSize => 0; 125 | 126 | @override 127 | int get serializedSize => serializedHeaderSize; 128 | 129 | @override 130 | void deserialize(SerializableInput input) {} 131 | 132 | @override 133 | void serialize(SerializableOutput output) {} 134 | } 135 | 136 | /// https://tools.ietf.org/html/draft-miller-ssh-agent-03#section-4.4 137 | class AGENT_IDENTITIES_ANSWER extends AgentMessage { 138 | static const int ID = 12; 139 | List> keys = List>(); 140 | AGENT_IDENTITIES_ANSWER() : super(ID); 141 | 142 | @override 143 | int get serializedHeaderSize => 4; 144 | 145 | @override 146 | int get serializedSize => keys.fold( 147 | serializedHeaderSize, (v, e) => v + 8 + e.key.length + e.value.length); 148 | 149 | @override 150 | void deserialize(SerializableInput input) { 151 | keys.clear(); 152 | int length = input.getUint32(); 153 | for (int i = 0; i < length; i++) { 154 | Uint8List key = deserializeStringBytes(input); 155 | keys.add(MapEntry(key, deserializeString(input))); 156 | } 157 | } 158 | 159 | @override 160 | void serialize(SerializableOutput output) { 161 | output.addUint32(keys.length); 162 | for (MapEntry key in keys) { 163 | serializeString(output, key.key); 164 | serializeString(output, key.value); 165 | } 166 | } 167 | } 168 | 169 | /// https://tools.ietf.org/html/draft-miller-ssh-agent-03#section-4.5 170 | class AGENTC_SIGN_REQUEST extends AgentMessage { 171 | static const int ID = 13; 172 | Uint8List key, data; 173 | int flags; 174 | AGENTC_SIGN_REQUEST([this.key, this.data, this.flags = 0]) : super(ID); 175 | 176 | @override 177 | int get serializedHeaderSize => 4 * 3; 178 | 179 | @override 180 | int get serializedSize => serializedHeaderSize + key.length + data.length; 181 | 182 | @override 183 | void deserialize(SerializableInput input) { 184 | key = deserializeStringBytes(input); 185 | data = deserializeStringBytes(input); 186 | flags = input.getUint32(); 187 | } 188 | 189 | @override 190 | void serialize(SerializableOutput output) { 191 | serializeString(output, key); 192 | serializeString(output, data); 193 | output.addUint32(flags); 194 | } 195 | } 196 | 197 | /// On success, the agent shall reply with: 198 | class AGENT_SIGN_RESPONSE extends AgentMessage { 199 | static const int ID = 14; 200 | Uint8List sig; 201 | AGENT_SIGN_RESPONSE([this.sig]) : super(ID); 202 | 203 | @override 204 | int get serializedHeaderSize => 4; 205 | 206 | @override 207 | int get serializedSize => serializedHeaderSize + sig.length; 208 | 209 | @override 210 | void deserialize(SerializableInput input) => 211 | sig = deserializeStringBytes(input); 212 | 213 | @override 214 | void serialize(SerializableOutput output) => serializeString(output, sig); 215 | } 216 | -------------------------------------------------------------------------------- /lib/client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:math'; 5 | import 'dart:convert'; 6 | import 'dart:typed_data'; 7 | 8 | import "package:pointycastle/api.dart"; 9 | 10 | import 'package:dartssh/agent.dart'; 11 | import 'package:dartssh/identity.dart'; 12 | import 'package:dartssh/pem.dart'; 13 | import 'package:dartssh/protocol.dart'; 14 | import 'package:dartssh/serializable.dart'; 15 | import 'package:dartssh/socket.dart'; 16 | import 'package:dartssh/socket_html.dart' 17 | if (dart.library.io) 'package:dartssh/socket_io.dart'; 18 | import 'package:dartssh/ssh.dart'; 19 | import 'package:dartssh/transport.dart'; 20 | import 'package:dartssh/websocket_html.dart' 21 | if (dart.library.io) 'package:dartssh/websocket_io.dart'; 22 | 23 | /// The Secure Shell (SSH) is a protocol for secure remote login and 24 | /// other secure network services over an insecure network. 25 | class SSHClient extends SSHTransport with SSHAgentForwarding { 26 | // Parameters 27 | String login, termvar, startupCommand; 28 | bool agentForwarding, closeOnDisconnect, startShell; 29 | FingerprintCallback acceptHostFingerprint; 30 | Uint8ListFunction getPassword; 31 | IdentityFunction loadIdentity; 32 | List success = []; 33 | 34 | // State 35 | int loginPrompts = 0, passwordPrompts = 0, userauthFail = 0; 36 | bool acceptedHostkey = false, loadedPw = false, wrotePw = false; 37 | Uint8List pw; 38 | int termWidth, termHeight; 39 | 40 | SSHClient( 41 | {Uri hostport, 42 | this.login, 43 | this.termvar = '', 44 | this.termWidth = 80, 45 | this.termHeight = 25, 46 | this.startupCommand, 47 | bool compress = false, 48 | this.agentForwarding = false, 49 | this.closeOnDisconnect, 50 | this.startShell = true, 51 | List forwardLocal, 52 | List forwardRemote, 53 | VoidCallback disconnected, 54 | ResponseCallback response, 55 | StringCallback print, 56 | StringCallback debugPrint, 57 | StringCallback tracePrint, 58 | VoidCallback success, 59 | this.acceptHostFingerprint, 60 | this.loadIdentity, 61 | this.getPassword, 62 | SocketInterface socketInput, 63 | Random random, 64 | SecureRandom secureRandom}) 65 | : super(false, 66 | hostport: hostport, 67 | compress: compress, 68 | forwardLocal: forwardLocal, 69 | forwardRemote: forwardRemote, 70 | disconnected: disconnected, 71 | response: response, 72 | print: print, 73 | debugPrint: debugPrint, 74 | tracePrint: tracePrint, 75 | socket: socketInput, 76 | random: random, 77 | secureRandom: secureRandom) { 78 | if (success != null) { 79 | this.success.add(success); 80 | } 81 | if (socket == null) { 82 | if (debugPrint != null) { 83 | debugPrint('Connecting to $hostport'); 84 | } 85 | socket = (hostport.hasScheme && 86 | (hostport.scheme == 'ws' || hostport.scheme == 'wss')) 87 | ? WebSocketImpl() 88 | : SocketImpl(); 89 | 90 | socket.connect( 91 | hostport, onConnected, (error) => disconnect('connect error')); 92 | } 93 | } 94 | 95 | /// https://tools.ietf.org/html/rfc4253#section-6 96 | @override 97 | void handlePacket(Uint8List packet) { 98 | packetId = packetS.getUint8(); 99 | switch (packetId) { 100 | case MSG_KEXINIT.ID: 101 | state = state == SSHTransportState.FIRST_KEXINIT 102 | ? SSHTransportState.FIRST_KEXREPLY 103 | : SSHTransportState.KEXREPLY; 104 | handleMSG_KEXINIT(MSG_KEXINIT()..deserialize(packetS), packet); 105 | break; 106 | 107 | case MSG_KEXDH_REPLY.ID: 108 | case MSG_KEX_DH_GEX_REPLY.ID: 109 | handleMSG_KEXDH_REPLY(packetId, packet); 110 | break; 111 | 112 | case MSG_NEWKEYS.ID: 113 | handleMSG_NEWKEYS(); 114 | writeCipher(MSG_SERVICE_REQUEST('ssh-userauth')); 115 | break; 116 | 117 | case MSG_SERVICE_ACCEPT.ID: 118 | handleMSG_SERVICE_ACCEPT(); 119 | break; 120 | 121 | case MSG_USERAUTH_FAILURE.ID: 122 | handleMSG_USERAUTH_FAILURE( 123 | MSG_USERAUTH_FAILURE()..deserialize(packetS)); 124 | break; 125 | 126 | case MSG_USERAUTH_SUCCESS.ID: 127 | handleMSG_USERAUTH_SUCCESS(); 128 | break; 129 | 130 | case MSG_USERAUTH_INFO_REQUEST.ID: 131 | handleMSG_USERAUTH_INFO_REQUEST( 132 | MSG_USERAUTH_INFO_REQUEST()..deserialize(packetS)); 133 | break; 134 | 135 | case MSG_GLOBAL_REQUEST.ID: 136 | handleMSG_GLOBAL_REQUEST(MSG_GLOBAL_REQUEST()..deserialize(packetS)); 137 | break; 138 | 139 | case MSG_CHANNEL_OPEN.ID: 140 | handleMSG_CHANNEL_OPEN( 141 | MSG_CHANNEL_OPEN()..deserialize(packetS), packetS); 142 | break; 143 | 144 | case MSG_CHANNEL_OPEN_CONFIRMATION.ID: 145 | handleMSG_CHANNEL_OPEN_CONFIRMATION( 146 | MSG_CHANNEL_OPEN_CONFIRMATION()..deserialize(packetS)); 147 | break; 148 | 149 | case MSG_CHANNEL_OPEN_FAILURE.ID: 150 | handleMSG_CHANNEL_OPEN_FAILURE( 151 | MSG_CHANNEL_OPEN_FAILURE()..deserialize(packetS)); 152 | break; 153 | 154 | case MSG_CHANNEL_WINDOW_ADJUST.ID: 155 | handleMSG_CHANNEL_WINDOW_ADJUST( 156 | MSG_CHANNEL_WINDOW_ADJUST()..deserialize(packetS)); 157 | break; 158 | 159 | case MSG_CHANNEL_DATA.ID: 160 | handleMSG_CHANNEL_DATA(MSG_CHANNEL_DATA()..deserialize(packetS)); 161 | break; 162 | 163 | case MSG_CHANNEL_EOF.ID: 164 | handleMSG_CHANNEL_EOF(MSG_CHANNEL_EOF()..deserialize(packetS)); 165 | break; 166 | 167 | case MSG_CHANNEL_CLOSE.ID: 168 | handleMSG_CHANNEL_CLOSE(MSG_CHANNEL_CLOSE()..deserialize(packetS)); 169 | break; 170 | 171 | case MSG_CHANNEL_REQUEST.ID: 172 | handleMSG_CHANNEL_REQUEST(MSG_CHANNEL_REQUEST()..deserialize(packetS)); 173 | break; 174 | 175 | case MSG_CHANNEL_SUCCESS.ID: 176 | if (tracePrint != null) { 177 | tracePrint('$hostport: MSG_CHANNEL_SUCCESS'); 178 | } 179 | break; 180 | 181 | case MSG_CHANNEL_FAILURE.ID: 182 | if (tracePrint != null) { 183 | tracePrint('$hostport: MSG_CHANNEL_FAILURE'); 184 | } 185 | break; 186 | 187 | case MSG_DISCONNECT.ID: 188 | handleMSG_DISCONNECT(MSG_DISCONNECT()..deserialize(packetS)); 189 | break; 190 | 191 | case MSG_IGNORE.ID: 192 | handleMSG_IGNORE(MSG_IGNORE()..deserialize(packetS)); 193 | break; 194 | 195 | case MSG_DEBUG.ID: 196 | handleMSG_DEBUG(MSG_DEBUG()..deserialize(packetS)); 197 | break; 198 | 199 | default: 200 | if (print != null) { 201 | print('$hostport: unknown packet number: $packetId, len $packetLen'); 202 | } 203 | break; 204 | } 205 | } 206 | 207 | /// Initialize a shared-secret negotiation culminating with [MSG_NEWKEYS]. 208 | @override 209 | void sendDiffileHellmanInit() { 210 | initializeDiffieHellman(kexMethod, random); 211 | if (KEX.x25519DiffieHellman(kexMethod)) { 212 | writeClearOrEncrypted(MSG_KEX_ECDH_INIT(x25519dh.myPubKey)); 213 | } else if (KEX.ellipticCurveDiffieHellman(kexMethod)) { 214 | writeClearOrEncrypted(MSG_KEX_ECDH_INIT(ecdh.cText)); 215 | } else if (KEX.diffieHellmanGroupExchange(kexMethod)) { 216 | writeClearOrEncrypted( 217 | MSG_KEX_DH_GEX_REQUEST(dh.gexMin, dh.gexMax, dh.gexPref)); 218 | } else if (KEX.diffieHellman(kexMethod)) { 219 | writeClearOrEncrypted(MSG_KEXDH_INIT(dh.e)); 220 | } else { 221 | throw FormatException('$hostport: unknown kex method: $kexMethod'); 222 | } 223 | } 224 | 225 | /// https://tools.ietf.org/html/rfc4253#section-8 226 | void handleMSG_KEXDH_REPLY(int packetId, Uint8List packet) { 227 | if (state != SSHTransportState.FIRST_KEXREPLY && 228 | state != SSHTransportState.KEXREPLY) { 229 | throw FormatException('$hostport: unexpected state $state'); 230 | } 231 | if (guessedS && !guessedRightS) { 232 | guessedS = false; 233 | if (print != null) { 234 | print('$hostport: server guessed wrong, ignoring packet'); 235 | } 236 | return; 237 | } 238 | 239 | Uint8List fingerprint; 240 | if (packetId == MSG_KEX_ECDH_REPLY.ID && 241 | KEX.x25519DiffieHellman(kexMethod)) { 242 | fingerprint = handleX25519MSG_KEX_ECDH_REPLY( 243 | MSG_KEX_ECDH_REPLY()..deserialize(packetS)) ?? 244 | fingerprint; 245 | } else if (packetId == MSG_KEXDH_REPLY.ID && 246 | KEX.ellipticCurveDiffieHellman(kexMethod)) { 247 | fingerprint = handleEcDhMSG_KEX_ECDH_REPLY( 248 | MSG_KEX_ECDH_REPLY()..deserialize(packetS)) ?? 249 | fingerprint; 250 | } else if (packetId == MSG_KEXDH_REPLY.ID && 251 | KEX.diffieHellmanGroupExchange(kexMethod)) { 252 | handleDhGroupMSG_KEX_DH_GEX_GROUP( 253 | MSG_KEX_DH_GEX_GROUP()..deserialize(packetS)); 254 | return; 255 | } else { 256 | fingerprint = 257 | handleDhMSG_KEXDH_REPLY(MSG_KEXDH_REPLY()..deserialize(packetS)) ?? 258 | fingerprint; 259 | } 260 | 261 | if (state == SSHTransportState.FIRST_KEXREPLY) { 262 | if (acceptHostFingerprint != null) { 263 | acceptedHostkey = acceptHostFingerprint(hostkeyType, fingerprint); 264 | } else { 265 | acceptedHostkey = true; 266 | } 267 | } 268 | 269 | sendNewKeys(); 270 | } 271 | 272 | /// Completes X25519 key exchange. 273 | Uint8List handleX25519MSG_KEX_ECDH_REPLY(MSG_KEX_ECDH_REPLY msg) { 274 | Uint8List fingerprint; 275 | if (tracePrint != null) { 276 | tracePrint('$hostport: MSG_KEX_ECDH_REPLY for X25519DH'); 277 | } 278 | if (!acceptedHostkey) fingerprint = msg.kS; 279 | 280 | K = x25519dh.computeSecret(msg.qS); 281 | if (!computeExchangeHashAndVerifyHostKey(msg.kS, msg.hSig)) { 282 | throw FormatException('$hostport: verify hostkey failed'); 283 | } 284 | 285 | return fingerprint; 286 | } 287 | 288 | /// Completes Elliptic-curve Diffie–Hellman key exchange. 289 | Uint8List handleEcDhMSG_KEX_ECDH_REPLY(MSG_KEX_ECDH_REPLY msg) { 290 | Uint8List fingerprint; 291 | if (tracePrint != null) { 292 | tracePrint('$hostport: MSG_KEX_ECDH_REPLY for ECDH'); 293 | } 294 | if (!acceptedHostkey) fingerprint = msg.kS; 295 | 296 | K = ecdh.computeSecret(msg.qS); 297 | if (!computeExchangeHashAndVerifyHostKey(msg.kS, msg.hSig)) { 298 | throw FormatException('$hostport: verify hostkey failed'); 299 | } 300 | 301 | return fingerprint; 302 | } 303 | 304 | /// Completes Diffie-Hellman Group Exchange and begins key exchange. 305 | void handleDhGroupMSG_KEX_DH_GEX_GROUP(MSG_KEX_DH_GEX_GROUP msg) { 306 | if (tracePrint != null) { 307 | tracePrint('$hostport: MSG_KEX_DH_GEX_GROUP'); 308 | } 309 | initializeDiffieHellmanGroup(msg.p, msg.g, random); 310 | writeClearOrEncrypted(MSG_KEX_DH_GEX_INIT(dh.e)); 311 | } 312 | 313 | /// Completes Diffie-Hellman key exchange. 314 | Uint8List handleDhMSG_KEXDH_REPLY(MSG_KEXDH_REPLY msg) { 315 | Uint8List fingerprint; 316 | if (tracePrint != null) { 317 | tracePrint('$hostport: MSG_KEXDH_REPLY'); 318 | } 319 | if (!acceptedHostkey) fingerprint = msg.kS; 320 | 321 | K = dh.computeSecret(msg.f); 322 | if (!computeExchangeHashAndVerifyHostKey(msg.kS, msg.hSig)) { 323 | throw FormatException('$hostport: verify hostkey failed'); 324 | } 325 | 326 | return fingerprint; 327 | } 328 | 329 | /// Handle accepted [MSG_SERVICE_REQUEST] sent in response to [MSG_NEWKEYS]. 330 | void handleMSG_SERVICE_ACCEPT() { 331 | if (tracePrint != null) tracePrint('$hostport: MSG_SERVICE_ACCEPT'); 332 | if (login == null || login.isEmpty) { 333 | loginPrompts = 1; 334 | response(this, 'login: '); 335 | } 336 | if (identity == null && loadIdentity != null) { 337 | identity = loadIdentity(); 338 | } 339 | sendAuthenticationRequest(); 340 | } 341 | 342 | /// If key authentication failed, then try password authentication. 343 | void handleMSG_USERAUTH_FAILURE(MSG_USERAUTH_FAILURE msg) { 344 | if (tracePrint != null) { 345 | tracePrint( 346 | '$hostport: MSG_USERAUTH_FAILURE: auth_left="${msg.authLeft}" loadedPw=$loadedPw useauthFail=$userauthFail'); 347 | } 348 | if (!loadedPw) clearPassword(); 349 | userauthFail++; 350 | if (userauthFail == 1 && !wrotePw) { 351 | response(this, 'Password:'); 352 | passwordPrompts = 1; 353 | getThenSendPassword(); 354 | } else { 355 | throw FormatException('$hostport: authorization failed'); 356 | } 357 | } 358 | 359 | /// After successfull authentication, open the session channel and start compression. 360 | void handleMSG_USERAUTH_SUCCESS() { 361 | if (tracePrint != null) { 362 | tracePrint('$hostport: MSG_USERAUTH_SUCCESS'); 363 | } 364 | sessionChannel = 365 | Channel(localId: nextChannelId, windowS: initialWindowSize); 366 | channels[nextChannelId] = sessionChannel; 367 | nextChannelId++; 368 | 369 | if (compressIdC2s == Compression.OpenSSHZLib) { 370 | // zwriter = ArchiveDeflateWriter(); 371 | throw FormatException('compression not supported'); 372 | } 373 | if (compressIdS2c == Compression.OpenSSHZLib) { 374 | // zreader = ArchiveInflateReader(); 375 | throw FormatException('compression not supported'); 376 | } 377 | writeCipher(MSG_CHANNEL_OPEN( 378 | 'session', sessionChannel.localId, initialWindowSize, maxPacketSize)); 379 | for (VoidCallback successCallback in success) { 380 | successCallback(); 381 | } 382 | } 383 | 384 | /// The server can optionally request authentication information from the client. 385 | void handleMSG_USERAUTH_INFO_REQUEST(MSG_USERAUTH_INFO_REQUEST msg) { 386 | if (tracePrint != null) { 387 | tracePrint( 388 | '$hostport: MSG_USERAUTH_INFO_REQUEST prompts=${msg.prompts.length}'); 389 | } 390 | if (msg.instruction.isNotEmpty) { 391 | if (tracePrint != null) { 392 | tracePrint('$hostport: instruction: ${msg.instruction}'); 393 | } 394 | response(this, msg.instruction); 395 | } 396 | 397 | for (MapEntry prompt in msg.prompts) { 398 | if (tracePrint != null) { 399 | tracePrint('$hostport: prompt: ${prompt.key}'); 400 | } 401 | response(this, prompt.key); 402 | } 403 | 404 | if (msg.prompts.isNotEmpty) { 405 | passwordPrompts = msg.prompts.length; 406 | getThenSendPassword(); 407 | } else { 408 | writeCipher(MSG_USERAUTH_INFO_RESPONSE(List())); 409 | } 410 | } 411 | 412 | /// Logs any (unhandled) channel specific requests from server. 413 | void handleMSG_CHANNEL_REQUEST(MSG_CHANNEL_REQUEST msg) { 414 | if (tracePrint != null) { 415 | tracePrint( 416 | '$hostport: MSG_CHANNEL_REQUEST ${msg.requestType} wantReply=${msg.wantReply}'); 417 | } 418 | } 419 | 420 | /// Handles server-initiated [Channel] to client. e.g. for remote port forwarding, 421 | /// or SSH agent request. 422 | void handleMSG_CHANNEL_OPEN(MSG_CHANNEL_OPEN msg, SerializableInput packetS) { 423 | if (tracePrint != null) { 424 | tracePrint('$hostport: MSG_CHANNEL_OPEN type=${msg.channelType}'); 425 | } 426 | if (msg.channelType == 'auth-agent@openssh.com' && agentForwarding) { 427 | Channel channel = acceptChannel(msg); 428 | channel.agentChannel = true; 429 | writeCipher(MSG_CHANNEL_OPEN_CONFIRMATION( 430 | channel.remoteId, channel.localId, channel.windowS, maxPacketSize)); 431 | } else if (msg.channelType == 'forwarded-tcpip') { 432 | MSG_CHANNEL_OPEN_TCPIP openTcpIp = MSG_CHANNEL_OPEN_TCPIP() 433 | ..deserialize(packetS); 434 | Forward forward = 435 | forwardingRemote == null ? null : forwardingRemote[openTcpIp.dstPort]; 436 | if (forward == null || remoteForward == null) { 437 | if (print != null) { 438 | print('unknown port open ${openTcpIp.dstPort}'); 439 | } 440 | writeCipher(MSG_CHANNEL_OPEN_FAILURE(msg.senderChannel, 0, '', '')); 441 | } else { 442 | Channel channel = acceptChannel(msg); 443 | remoteForward(channel, forward.targetHost, forward.targetPort, 444 | openTcpIp.srcHost, openTcpIp.srcPort); 445 | writeCipher(MSG_CHANNEL_OPEN_CONFIRMATION( 446 | channel.remoteId, channel.localId, channel.windowS, maxPacketSize)); 447 | } 448 | } else { 449 | if (print != null) { 450 | print('unknown channel open ${msg.channelType}'); 451 | } 452 | writeCipher(MSG_CHANNEL_OPEN_FAILURE(msg.senderChannel, 0, '', '')); 453 | } 454 | } 455 | 456 | /// Handles successfully opened client-initiated [Channel]. 457 | void handleChannelOpenConfirmation(Channel channel) { 458 | if (channel == sessionChannel) { 459 | handleSessionStarted(); 460 | } else if (channel.connected != null) { 461 | channel.connected(); 462 | } 463 | } 464 | 465 | /// After the session is established, initialize channel state. 466 | void handleSessionStarted() { 467 | if (agentForwarding) { 468 | writeCipher(MSG_CHANNEL_REQUEST.exec( 469 | sessionChannel.remoteId, 'auth-agent-req@openssh.com', '', true)); 470 | } 471 | 472 | if (forwardRemote != null) { 473 | for (Forward forward in forwardRemote) { 474 | writeCipher(MSG_GLOBAL_REQUEST_TCPIP('', forward.port)); 475 | forwardingRemote[forward.port] = forward; 476 | } 477 | } 478 | 479 | if (startShell) { 480 | writeCipher(MSG_CHANNEL_REQUEST.ptyReq( 481 | sessionChannel.remoteId, 482 | 'pty-req', 483 | Point(termWidth, termHeight), 484 | Point(termWidth * 8, termHeight * 12), 485 | termvar, 486 | '', 487 | true)); 488 | 489 | writeCipher( 490 | MSG_CHANNEL_REQUEST.exec(sessionChannel.remoteId, 'shell', '', true)); 491 | 492 | if ((startupCommand ?? '').isNotEmpty) { 493 | sendToChannel(sessionChannel, utf8.encode(startupCommand)); 494 | } 495 | } 496 | } 497 | 498 | /// Handles all [Channel] data for this session. 499 | @override 500 | void handleChannelData(Channel chan, Uint8List data) { 501 | if (chan == sessionChannel) { 502 | response(this, utf8.decode(data)); 503 | } else if (chan.cb != null) { 504 | chan.cb(chan, data); 505 | } else if (chan.agentChannel) { 506 | handleAgentRead(chan, data); 507 | } 508 | } 509 | 510 | /// Handles [Channel] closed by server. 511 | @override 512 | void handleChannelClose(Channel chan, [String description]) { 513 | if (chan == sessionChannel) { 514 | writeCipher(MSG_DISCONNECT()); 515 | sessionChannel = null; 516 | } else if (chan.cb != null) { 517 | chan.opened = false; 518 | if (chan.error != null) { 519 | chan.error(description); 520 | } else { 521 | chan.cb(chan, Uint8List(0)); 522 | } 523 | } 524 | } 525 | 526 | /// Updates [exH] and verifies [kS]'s [hSig]. On success [MSG_NEWKEYS] is sent. 527 | bool computeExchangeHashAndVerifyHostKey(Uint8List kS, Uint8List hSig) { 528 | updateExchangeHash(kS); 529 | return verifyHostKey(exH, hostkeyType, kS, hSig); 530 | } 531 | 532 | /// Calls [sendPassword()] if [getPassword] succeeds. 533 | void getThenSendPassword() { 534 | if (getPassword != null && (pw = getPassword()) != null) sendPassword(); 535 | } 536 | 537 | /// "Securely" clears the local password storage. 538 | void clearPassword() { 539 | if (pw == null) return; 540 | for (int i = 0; i < pw.length; i++) { 541 | pw[i] ^= random.nextInt(255); 542 | } 543 | pw = null; 544 | } 545 | 546 | /// Sends [MSG_USERAUTH_REQUEST] with password [pw]. 547 | void sendPassword() { 548 | response(this, '\r\n'); 549 | wrotePw = true; 550 | if (userauthFail != 0) { 551 | writeCipher(MSG_USERAUTH_REQUEST( 552 | login, 'ssh-connection', 'password', '', pw, Uint8List(0))); 553 | } else { 554 | List prompt = 555 | List.filled(passwordPrompts, Uint8List(0)); 556 | prompt.last = pw; 557 | writeCipher(MSG_USERAUTH_INFO_RESPONSE(prompt)); 558 | } 559 | passwordPrompts = 0; 560 | clearPassword(); 561 | } 562 | 563 | /// Sends [MSG_USERAUTH_REQUEST] optionally using [identity] or keyboard-interactive. 564 | void sendAuthenticationRequest() { 565 | if (identity == null) { 566 | if (debugPrint != null) { 567 | debugPrint('$hostport: Keyboard interactive'); 568 | } 569 | writeCipher(MSG_USERAUTH_REQUEST(login, 'ssh-connection', 570 | 'keyboard-interactive', '', Uint8List(0), Uint8List(0))); 571 | } else if (identity.ed25519 != null) { 572 | if (debugPrint != null) { 573 | debugPrint('$hostport: Sending Ed25519 authorization request'); 574 | } 575 | Uint8List pubkey = identity.getEd25519PublicKey().toRaw(); 576 | Uint8List challenge = deriveChallenge(sessionId, login, 'ssh-connection', 577 | 'publickey', 'ssh-ed25519', pubkey); 578 | Ed25519Signature sig = identity.signWithEd25519Key(challenge); 579 | writeCipher(MSG_USERAUTH_REQUEST(login, 'ssh-connection', 'publickey', 580 | 'ssh-ed25519', pubkey, sig.toRaw())); 581 | } else if (identity.ecdsaPrivate != null) { 582 | if (debugPrint != null) { 583 | debugPrint('$hostport: Sending ECDSA authorization request'); 584 | } 585 | String keyType = Key.name(identity.ecdsaKeyType); 586 | Uint8List pubkey = identity.getECDSAPublicKey().toRaw(); 587 | Uint8List challenge = deriveChallenge( 588 | sessionId, login, 'ssh-connection', 'publickey', keyType, pubkey); 589 | ECDSASignature sig = 590 | identity.signWithECDSAKey(challenge, getSecureRandom()); 591 | writeCipher(MSG_USERAUTH_REQUEST( 592 | login, 'ssh-connection', 'publickey', keyType, pubkey, sig.toRaw())); 593 | } else if (identity.rsaPrivate != null) { 594 | if (debugPrint != null) { 595 | debugPrint('$hostport: Sending RSA authorization request'); 596 | } 597 | Uint8List pubkey = identity.getRSAPublicKey().toRaw(); 598 | Uint8List challenge = deriveChallenge( 599 | sessionId, login, 'ssh-connection', 'publickey', 'ssh-rsa', pubkey); 600 | RSASignature sig = identity.signWithRSAKey(challenge); 601 | writeCipher(MSG_USERAUTH_REQUEST(login, 'ssh-connection', 'publickey', 602 | 'ssh-rsa', pubkey, sig.toRaw())); 603 | } 604 | } 605 | 606 | /// Sends channel data [b] on [sessionChannel]. 607 | /// Optionally [b] is captured by [loginPrompts] or [passwordPrompts]. 608 | @override 609 | void sendChannelData(Uint8List b) { 610 | if (loginPrompts != 0) { 611 | response(this, utf8.decode(b)); 612 | bool cr = b.isNotEmpty && b.last == '\n'.codeUnits[0]; 613 | login += String.fromCharCodes(b, 0, b.length - (cr ? 1 : 0)); 614 | if (cr) { 615 | response(this, '\n'); 616 | loginPrompts = 0; 617 | sendAuthenticationRequest(); 618 | } 619 | } else if (passwordPrompts != 0) { 620 | bool cr = b.isNotEmpty && b.last == '\n'.codeUnits[0]; 621 | pw = appendUint8List( 622 | pw ?? Uint8List(0), viewUint8List(b, 0, b.length - (cr ? 1 : 0))); 623 | if (cr) sendPassword(); 624 | } else { 625 | if (sessionChannel != null) { 626 | sendToChannel(sessionChannel, b); 627 | } 628 | } 629 | } 630 | 631 | /// Sends window-change [MSG_CHANNEL_REQUEST]. 632 | void setTerminalWindowSize(int w, int h) { 633 | termWidth = w; 634 | termHeight = h; 635 | if (socket == null || sessionChannel == null) return; 636 | writeCipher(MSG_CHANNEL_REQUEST.ptyReq( 637 | sessionChannel.remoteId, 638 | 'window-change', 639 | Point(termWidth, termHeight), 640 | Point(termWidth * 8, termHeight * 12), 641 | termvar, 642 | '', 643 | false)); 644 | } 645 | 646 | void exec(String command, {bool wantReply = true}) { 647 | assert(socket != null && sessionChannel != null); 648 | if (socket == null || sessionChannel == null) return; 649 | writeCipher(MSG_CHANNEL_REQUEST.exec( 650 | sessionChannel.remoteId, 'exec', command, wantReply)); 651 | } 652 | } 653 | 654 | /// Implement same [SocketInterface] as actual [Socket] but over [SSHClient] tunnel. 655 | class SSHTunneledSocketImpl extends SocketInterface { 656 | bool clientOwner, shutdownSend = false, shutdownRecv = false; 657 | SSHClient client; 658 | Identity identity; 659 | Channel channel; 660 | String sourceHost, tunnelToHost; 661 | int sourcePort, tunnelToPort; 662 | VoidCallback connectHandler; 663 | StringCallback connectError, onError, onDone; 664 | Uint8ListCallback onMessage; 665 | 666 | SSHTunneledSocketImpl.fromClient(this.client) : clientOwner = false; 667 | 668 | SSHTunneledSocketImpl(Uri url, String login, String key, String password, 669 | {StringCallback print, StringCallback debugPrint}) 670 | : clientOwner = true { 671 | identity = key == null ? null : parsePem(key); 672 | client = SSHClient( 673 | socketInput: SocketImpl(), 674 | hostport: url, 675 | login: login, 676 | getPassword: password == null ? null : () => utf8.encode(password), 677 | loadIdentity: () => identity, 678 | response: (_, m) {}, 679 | disconnected: () { 680 | if (onDone != null) { 681 | onDone('SSHTunnelledSocketImpl.client disconnected'); 682 | } 683 | }, 684 | startShell: false, 685 | success: openTunnel, 686 | print: print, 687 | debugPrint: debugPrint); 688 | } 689 | 690 | @override 691 | bool get connected => channel != null; 692 | 693 | @override 694 | bool get connecting => connectHandler != null; 695 | 696 | @override 697 | void handleError(StringCallback errorHandler) => onError = errorHandler; 698 | 699 | @override 700 | void handleDone(StringCallback doneHandler) => onDone = doneHandler; 701 | 702 | @override 703 | void listen(Uint8ListCallback messageHandler) => onMessage = messageHandler; 704 | 705 | @override 706 | void send(String text) => sendRaw(utf8.encode(text)); 707 | 708 | @override 709 | void sendRaw(Uint8List raw) { 710 | if (shutdownSend) return; 711 | //client.debugPrint('DEBUG SSHTunneledSocketImpl.send: ${String.fromCharCodes(raw)}'); 712 | client.sendToChannel(channel, raw); 713 | } 714 | 715 | @override 716 | void close() { 717 | if (clientOwner) { 718 | client.disconnect('close'); 719 | } else if (channel != null) { 720 | client.closeChannel(channel); 721 | } 722 | } 723 | 724 | /// Connects to [address] over SSH tunnel provided by [client]. 725 | @override 726 | void connect( 727 | Uri address, VoidCallback connectCallback, StringCallback errorHandler, 728 | {int timeoutSeconds = 15, bool ignoreBadCert = false}) { 729 | tunnelToHost = address.host; 730 | tunnelToPort = address.port; 731 | connectHandler = connectCallback; 732 | connectError = errorHandler; 733 | if (clientOwner) { 734 | client.socket.connect(client.hostport, client.onConnected, (error) { 735 | client.disconnect('connect error'); 736 | if (connectError != null) connectError(error); 737 | }); 738 | } else { 739 | if (client.sessionChannel == null) { 740 | client.success.add(openTunnel); 741 | } else { 742 | openTunnel(); 743 | } 744 | } 745 | } 746 | 747 | /// Sends [MSG_CHANNEL_OPEN_TCPIP] for [tunnelToHost]:[tunnelToPort]. 748 | void openTunnel([String sourceHost = '127.0.0.1', int sourcePort = 1234]) { 749 | this.sourceHost = sourceHost; 750 | this.sourcePort = sourcePort; 751 | channel = client.openTcpChannel( 752 | sourceHost, sourcePort, tunnelToHost, tunnelToPort, (_, Uint8List m) { 753 | //client.debugPrint('DEBUG SSHTunneledSocketImpl.recv: ${String.fromCharCodes(m)}'); 754 | //client.debugPrint('DEBUG SSHTunneledSocketImpl.recvRaw(${m.length}) = $m'); 755 | onMessage(m); 756 | }, connected: () { 757 | if (connectHandler != null) connectHandler(); 758 | connectHandler = null; 759 | connectError = null; 760 | }, error: (String description) { 761 | if (connectError != null) { 762 | connectError(description); 763 | } else { 764 | onError(description); 765 | } 766 | connectHandler = null; 767 | connectError = null; 768 | }); 769 | } 770 | } 771 | -------------------------------------------------------------------------------- /lib/http.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:collection'; 6 | import 'dart:convert'; 7 | import 'dart:typed_data'; 8 | 9 | import 'package:http/http.dart' as http; 10 | 11 | import 'package:dartssh/client.dart'; 12 | import 'package:dartssh/serializable.dart'; 13 | import 'package:dartssh/socket.dart'; 14 | import 'package:dartssh/transport.dart'; 15 | 16 | typedef HttpClientFactory = http.Client Function(); 17 | typedef SocketFilter = Future Function(SocketInterface); 18 | 19 | /// Asynchronous HTTP request 20 | class HttpRequest { 21 | String url, method, data; 22 | Map headers; 23 | Completer completer = Completer(); 24 | HttpRequest(this.url, this.method, {this.data, this.headers}); 25 | } 26 | 27 | /// HTTP response integrating [io.HttpClient] and [html.HttpRequest]. 28 | class HttpResponse { 29 | int status, contentLength; 30 | String text, reason; 31 | Map headers; 32 | Stream> contentStream; 33 | HttpResponse(this.status, 34 | {this.text, 35 | this.reason, 36 | this.headers, 37 | this.contentStream, 38 | this.contentLength}); 39 | } 40 | 41 | /// HTTP client integrating [io.HttpClient] and [html.HttpRequest]. 42 | abstract class HttpClient { 43 | int numOutstanding = 0; 44 | StringCallback debugPrint; 45 | HttpClient({this.debugPrint}); 46 | 47 | Future request(String url, 48 | {String method, String data, Map headers}); 49 | } 50 | 51 | /// Shim [HttpClient] for testing 52 | class TestHttpClient extends HttpClient { 53 | Queue requests = Queue(); 54 | 55 | @override 56 | Future request(String url, 57 | {String method, String data, Map headers}) { 58 | HttpRequest httpRequest = HttpRequest(url, method, data: data); 59 | requests.add(httpRequest); 60 | return httpRequest.completer.future; 61 | } 62 | } 63 | 64 | /// package:http based implementation of [HttpClient]. 65 | class HttpClientImpl extends HttpClient { 66 | HttpClientFactory clientFactory; 67 | HttpClientImpl( 68 | {this.clientFactory, StringCallback debugPrint, StringFilter userAgent}) 69 | : super(debugPrint: debugPrint) { 70 | clientFactory ??= () => UserAgentBaseClient( 71 | userAgent == null ? null : userAgent('HttpClientImpl'), http.Client()); 72 | } 73 | 74 | @override 75 | Future request(String url, 76 | {String method, String data, Map headers}) async { 77 | numOutstanding++; 78 | if (debugPrint != null) debugPrint('HTTP Request: $url'); 79 | 80 | http.Client client = clientFactory(); 81 | var uriResponse; 82 | switch (method) { 83 | case 'POST': 84 | uriResponse = await client.post(url, body: data, headers: headers); 85 | break; 86 | 87 | default: 88 | uriResponse = await client.get(url, headers: headers); 89 | break; 90 | } 91 | 92 | HttpResponse ret = 93 | HttpResponse(uriResponse.statusCode, text: uriResponse.body); 94 | if (debugPrint != null) { 95 | debugPrint('HTTP Response=${ret.status}: ${ret.text}'); 96 | } 97 | numOutstanding--; 98 | return ret; 99 | } 100 | 101 | /*void requestWithIncrementalHandler(String url, 102 | {String method, String data}) async { 103 | var request = http.Request(method ?? 'GET', Uri.parse(url)); 104 | var response = await request.send(); 105 | var lineStream = 106 | response.stream.transform(Utf8Decoder()).transform(LineSplitter()); 107 | 108 | /// https://github.com/llamadonica/dart-json-stream-parser/tree/master/test 109 | await for (String line in lineStream) { 110 | print(line); 111 | } 112 | }*/ 113 | } 114 | 115 | /// [http.BaseClient] with [userAgent] header. 116 | /// Reference: https://github.com/dart-lang/http/blob/master/README.md 117 | class UserAgentBaseClient extends http.BaseClient { 118 | final String userAgent; 119 | final http.Client inner; 120 | UserAgentBaseClient(this.userAgent, this.inner); 121 | 122 | Future send(http.BaseRequest request) { 123 | if (userAgent != null) { 124 | request.headers['user-agent'] = userAgent; 125 | } 126 | return inner.send(request); 127 | } 128 | } 129 | 130 | /// [http.BaseClient] running over [SSHTunneledSocketImpl]. 131 | class SSHTunneledBaseClient extends http.BaseClient { 132 | final String userAgent; 133 | final SSHClient client; 134 | SSHTunneledBaseClient(this.client, {this.userAgent}); 135 | 136 | @override 137 | Future send(http.BaseRequest request) async { 138 | if (userAgent != null) { 139 | request.headers['user-agent'] = userAgent; 140 | } 141 | 142 | HttpResponse response = await httpRequest( 143 | request.url, 144 | request.method, 145 | SSHTunneledSocketImpl.fromClient(client), 146 | requestHeaders: request.headers, 147 | body: await request.finalize().toBytes(), 148 | debugPrint: client.debugPrint, 149 | persistentConnection: request.persistentConnection, 150 | ); 151 | 152 | return http.StreamedResponse( 153 | response.contentStream, 154 | response.status, 155 | contentLength: response.contentLength, 156 | request: request, 157 | headers: response.headers, 158 | reasonPhrase: response.reason, 159 | ); 160 | } 161 | } 162 | 163 | /// In basic HTTP authentication, a request contains a header field in the form of 164 | /// Authorization: Basic , where credentials is the base64 encoding of id 165 | /// and password joined by a single colon. 166 | /// https://en.wikipedia.org/wiki/Basic_access_authentication 167 | Map addBasicAuthenticationHeader( 168 | Map headers, String username, String password) { 169 | headers['authorization'] = 170 | 'Basic ' + base64.encode(utf8.encode('$username:$password')); 171 | return headers; 172 | } 173 | 174 | Future connectUri(Uri uri, SocketInterface socket, 175 | {SocketFilter secureUpgrade}) async { 176 | /// We might be asking the remote to open an SSH tunnel to [uri]. 177 | Completer connectCompleter = Completer(); 178 | socket.connect(uri, () => connectCompleter.complete(null), 179 | (error) => connectCompleter.complete('$error')); 180 | String connectError = await connectCompleter.future; 181 | if (connectError != null) throw FormatException(connectError); 182 | 183 | if (secureUpgrade != null && 184 | uri.hasScheme && 185 | (uri.scheme == 'https' || uri.scheme == 'wss')) { 186 | socket = await secureUpgrade(socket); 187 | } 188 | 189 | return socket; 190 | } 191 | 192 | /// Makes HTTP request over [SocketInterface], e.g. [SSHTunneledSocketImpl]. 193 | Future httpRequest(Uri uri, String method, SocketInterface socket, 194 | {Map requestHeaders, 195 | Uint8List body, 196 | StringCallback debugPrint, 197 | bool persistentConnection = true}) async { 198 | /// Initialize connection state. 199 | String headerText; 200 | List statusLine; 201 | Map headers; 202 | int contentLength = 0, contentRead = 0; 203 | QueueBuffer buffer = QueueBuffer(Uint8List(0)); 204 | Completer readHeadersCompleter = Completer(); 205 | StreamController> contentController = StreamController>(); 206 | 207 | if (!socket.connected && !socket.connecting) { 208 | socket = await connectUri(uri, socket); 209 | } 210 | socket.handleDone((String reason) { 211 | if (debugPrint != null) { 212 | debugPrint('SSHTunneledBaseClient.socket.handleDone'); 213 | } 214 | socket.close(); 215 | contentController.close(); 216 | if (headerText == null) readHeadersCompleter.complete('done'); 217 | }); 218 | 219 | socket.handleError((error) { 220 | if (debugPrint != null) { 221 | debugPrint('SSHTunneledBaseClient.socket.handleError'); 222 | } 223 | socket.close(); 224 | contentController.close(); 225 | if (headerText == null) readHeadersCompleter.complete('$error'); 226 | }); 227 | 228 | socket.listen((Uint8List m) { 229 | if (debugPrint != null) { 230 | debugPrint('SSHTunneledBaseClient.socket.listen: read ${m.length} bytes'); 231 | } 232 | if (headerText == null) { 233 | buffer.add(m); 234 | int headersEnd = searchUint8List( 235 | buffer.data, Uint8List.fromList('\r\n\r\n'.codeUnits)); 236 | 237 | /// Parse HTTP headers. 238 | if (headersEnd != -1) { 239 | headerText = utf8.decode(viewUint8List(buffer.data, 0, headersEnd)); 240 | buffer.flush(headersEnd + 4); 241 | var lines = LineSplitter.split(headerText); 242 | statusLine = lines.first.split(' '); 243 | headers = Map.fromIterable(lines.skip(1), 244 | key: (h) => h.substring(0, h.indexOf(': ')), 245 | value: (h) => h.substring(h.indexOf(': ') + 2).trim()); 246 | headers.forEach((key, value) { 247 | if (key.toLowerCase() == 'content-length') { 248 | contentLength = int.parse(value); 249 | } 250 | }); 251 | readHeadersCompleter.complete(null); 252 | 253 | /// If there's no content then we're already done. 254 | if (contentLength == 0) { 255 | if (debugPrint != null) { 256 | debugPrint( 257 | 'SSHTunneledBaseClient.socket.listen: Content-Length: 0, remaining=${buffer.data.length}'); 258 | } 259 | contentController.close(); 260 | if (!persistentConnection) { 261 | socket.close(); 262 | } 263 | return; 264 | } 265 | 266 | /// Handle any remaining data in the read buffer. 267 | if (buffer.data.isEmpty) return; 268 | m = buffer.data; 269 | } 270 | } 271 | 272 | /// Add content to the stream until completed. 273 | contentController.add(m); 274 | contentRead += m.length; 275 | if (contentRead >= contentLength) { 276 | if (debugPrint != null) { 277 | debugPrint( 278 | 'SSHTunneledBaseClient.socket.listen: done $contentRead / $contentLength'); 279 | } 280 | contentController.close(); 281 | if (!persistentConnection || contentRead > contentLength) { 282 | socket.close(); 283 | } 284 | } 285 | }); 286 | 287 | requestHeaders['Host'] = '${uri.host}'; 288 | if (method == 'POST') { 289 | requestHeaders['Content-Length'] = '${body.length}'; 290 | } 291 | socket.send('${method} /${uri.path} HTTP/1.1\r\n' + 292 | requestHeaders.entries 293 | .map((header) => '${header.key}: ${header.value}') 294 | .join('\r\n') + 295 | '\r\n\r\n'); 296 | if (method == 'POST') socket.sendRaw(body); 297 | 298 | String readHeadersError = await readHeadersCompleter.future; 299 | if (readHeadersError != null) throw FormatException(readHeadersError); 300 | 301 | return HttpResponse(int.parse(statusLine[1]), 302 | reason: statusLine.sublist(2).join(' '), 303 | headers: headers, 304 | contentLength: contentLength, 305 | contentStream: contentController.stream); 306 | } 307 | -------------------------------------------------------------------------------- /lib/http_html.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:html' as html; 6 | 7 | import 'package:dartssh/http.dart'; 8 | import 'package:dartssh/transport.dart'; 9 | 10 | /// dart:html based alternative [HttpClient] implementation. 11 | class HttpClientImpl extends HttpClient { 12 | static const String type = 'html'; 13 | HttpClientImpl({StringCallback debugPrint, StringFilter userAgent}) 14 | : super(debugPrint: debugPrint); 15 | 16 | @override 17 | Future request(String url, 18 | {String method, String data, Map headers}) { 19 | numOutstanding++; 20 | Completer completer = Completer(); 21 | html.HttpRequest.request(url, method: method, requestHeaders: headers) 22 | .then((r) { 23 | numOutstanding--; 24 | completer.complete(HttpResponse(r.status, text: r.responseText)); 25 | }); 26 | return completer.future; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/http_io.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:convert'; 6 | import 'dart:io' as io; 7 | 8 | import 'package:dartssh/http.dart'; 9 | import 'package:dartssh/transport.dart'; 10 | 11 | /// dart:io based alternative [HttpClient] implementation. 12 | class HttpClientImpl extends HttpClient { 13 | static const String type = 'io'; 14 | io.HttpClient client = io.HttpClient(); 15 | 16 | HttpClientImpl({StringCallback debugPrint, StringFilter userAgent}) 17 | : super(debugPrint: debugPrint) { 18 | if (userAgent != null) client.userAgent = userAgent(client.userAgent); 19 | } 20 | 21 | @override 22 | Future request(String url, 23 | {String method, String data, Map headers}) async { 24 | numOutstanding++; 25 | 26 | Uri uri = Uri.parse(url); 27 | var request; 28 | switch (method) { 29 | case 'POST': 30 | request = await client.postUrl(uri); 31 | break; 32 | 33 | default: 34 | request = await client.getUrl(uri); 35 | break; 36 | } 37 | 38 | if (headers != null) { 39 | headers 40 | .forEach((String key, String value) => request.headers[key] = value); 41 | } 42 | 43 | if (debugPrint != null) debugPrint('HTTP Request: ${request.uri}'); 44 | var response = await request.close(); 45 | HttpResponse ret = HttpResponse(response.statusCode); 46 | await for (var contents in response.transform(Utf8Decoder())) { 47 | if (ret.text == null) { 48 | ret.text = contents; 49 | } else { 50 | ret.text += contents; 51 | } 52 | } 53 | 54 | if (debugPrint != null) { 55 | debugPrint('HTTP Response=${ret.status}: ${ret.text}'); 56 | } 57 | numOutstanding--; 58 | return ret; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/identity.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:typed_data'; 5 | 6 | import 'package:pointycastle/api.dart' hide Signature; 7 | import 'package:pointycastle/asymmetric/api.dart' as asymmetric; 8 | import 'package:pointycastle/digests/sha1.dart'; 9 | import 'package:pointycastle/ecc/api.dart'; 10 | import 'package:pointycastle/signers/ecdsa_signer.dart'; 11 | import 'package:pointycastle/signers/rsa_signer.dart'; 12 | import 'package:tweetnacl/tweetnacl.dart' as tweetnacl; 13 | 14 | import 'package:dartssh/protocol.dart'; 15 | import 'package:dartssh/serializable.dart'; 16 | import 'package:dartssh/ssh.dart'; 17 | 18 | class Identity { 19 | tweetnacl.KeyPair ed25519; 20 | int ecdsaKeyType; 21 | ECPublicKey ecdsaPublic; 22 | ECPrivateKey ecdsaPrivate; 23 | asymmetric.RSAPublicKey rsaPublic; 24 | asymmetric.RSAPrivateKey rsaPrivate; 25 | 26 | Ed25519Key getEd25519PublicKey() => Ed25519Key(ed25519.publicKey); 27 | 28 | Ed25519Signature signWithEd25519Key(Uint8List m) => 29 | Ed25519Signature(tweetnacl.Signature(null, ed25519.secretKey) 30 | .sign(m) 31 | .buffer 32 | .asUint8List(0, 64)); 33 | 34 | ECDSAKey getECDSAPublicKey() => ECDSAKey(Key.name(ecdsaKeyType), 35 | Key.ellipticCurveName(ecdsaKeyType), ecdsaPublic.Q.getEncoded(false)); 36 | 37 | ECDSASignature signWithECDSAKey(Uint8List m, SecureRandom secureRandom) { 38 | ECDSASigner signer = ECDSASigner(Key.ellipticCurveHash(ecdsaKeyType)); 39 | signer.init( 40 | true, 41 | ParametersWithRandom( 42 | PrivateKeyParameter(ecdsaPrivate), 43 | secureRandom, 44 | )); 45 | ECSignature sig = signer.generateSignature(m); 46 | return ECDSASignature(Key.name(ecdsaKeyType), sig.r, sig.s); 47 | } 48 | 49 | RSAKey getRSAPublicKey() => RSAKey(rsaPublic.exponent, rsaPublic.modulus); 50 | 51 | RSASignature signWithRSAKey(Uint8List m) { 52 | RSASigner signer = RSASigner(SHA1Digest(), '06052b0e03021a'); 53 | signer.init( 54 | true, PrivateKeyParameter(rsaPrivate)); 55 | return RSASignature(signer.generateSignature(m).bytes); 56 | } 57 | 58 | Uint8List getRawPublicKey(int keyType) { 59 | if (Key.ellipticCurveDSA(keyType)) return getECDSAPublicKey().toRaw(); 60 | switch (keyType) { 61 | case Key.ED25519: 62 | return getEd25519PublicKey().toRaw(); 63 | case Key.RSA: 64 | return getRSAPublicKey().toRaw(); 65 | default: 66 | throw FormatException('key type $keyType'); 67 | } 68 | } 69 | 70 | Uint8List signMessage(int keyType, Uint8List m, [SecureRandom secureRandom]) { 71 | if (Key.ellipticCurveDSA(keyType)) { 72 | return signWithECDSAKey(m, secureRandom).toRaw(); 73 | } 74 | switch (keyType) { 75 | case Key.ED25519: 76 | return signWithEd25519Key(m).toRaw(); 77 | case Key.RSA: 78 | return signWithRSAKey(m).toRaw(); 79 | default: 80 | throw FormatException('key type $keyType'); 81 | } 82 | } 83 | 84 | List> getRawPublicKeyList() { 85 | List> ret = List>(); 86 | if (ed25519 != null) { 87 | ret.add(MapEntry(getEd25519PublicKey().toRaw(), '')); 88 | } 89 | if (ecdsaPublic != null) { 90 | ret.add(MapEntry(getECDSAPublicKey().toRaw(), '')); 91 | } 92 | if (rsaPublic != null) { 93 | ret.add(MapEntry(getRSAPublicKey().toRaw(), '')); 94 | } 95 | return ret; 96 | } 97 | } 98 | 99 | /// https://tools.ietf.org/html/rfc4253#section-6.6 100 | class RSAKey with Serializable { 101 | String formatId = 'ssh-rsa'; 102 | BigInt e, n; 103 | RSAKey([this.e, this.n]); 104 | 105 | @override 106 | int get serializedHeaderSize => 3 * 4; 107 | 108 | @override 109 | int get serializedSize => 110 | serializedHeaderSize + formatId.length + mpIntLength(e) + mpIntLength(n); 111 | 112 | @override 113 | void deserialize(SerializableInput input) { 114 | formatId = deserializeString(input); 115 | if (formatId != Key.name(Key.RSA)) throw FormatException(formatId); 116 | e = deserializeMpInt(input); 117 | n = deserializeMpInt(input); 118 | } 119 | 120 | @override 121 | void serialize(SerializableOutput output) { 122 | serializeString(output, formatId); 123 | serializeMpInt(output, e); 124 | serializeMpInt(output, n); 125 | } 126 | } 127 | 128 | /// https://tools.ietf.org/html/rfc4253#section-6.6 129 | class RSASignature with Serializable { 130 | String formatId = 'ssh-rsa'; 131 | Uint8List sig; 132 | RSASignature([this.sig]); 133 | 134 | @override 135 | int get serializedHeaderSize => 4 * 2 + 7; 136 | 137 | @override 138 | int get serializedSize => serializedHeaderSize + sig.length; 139 | 140 | @override 141 | void deserialize(SerializableInput input) { 142 | formatId = deserializeString(input); 143 | sig = deserializeStringBytes(input); 144 | if (formatId != 'ssh-rsa') throw FormatException(formatId); 145 | } 146 | 147 | @override 148 | void serialize(SerializableOutput output) { 149 | serializeString(output, formatId); 150 | serializeString(output, sig); 151 | } 152 | } 153 | 154 | /// https://tools.ietf.org/html/rfc5656#section-3.1 155 | class ECDSAKey with Serializable { 156 | String formatId, curveId; 157 | Uint8List q; 158 | ECDSAKey([this.formatId, this.curveId, this.q]); 159 | 160 | @override 161 | int get serializedHeaderSize => 3 * 4; 162 | 163 | @override 164 | int get serializedSize => 165 | serializedHeaderSize + formatId.length + curveId.length + q.length; 166 | 167 | @override 168 | void deserialize(SerializableInput input) { 169 | formatId = deserializeString(input); 170 | if (!formatId.startsWith('ecdsa-sha2-')) throw FormatException(formatId); 171 | curveId = deserializeString(input); 172 | q = deserializeStringBytes(input); 173 | } 174 | 175 | @override 176 | void serialize(SerializableOutput output) { 177 | serializeString(output, formatId); 178 | serializeString(output, curveId); 179 | serializeString(output, q); 180 | } 181 | } 182 | 183 | /// https://tools.ietf.org/html/rfc5656#section-3.1.2 184 | class ECDSASignature with Serializable { 185 | String formatId; 186 | BigInt r, s; 187 | ECDSASignature([this.formatId, this.r, this.s]); 188 | 189 | @override 190 | int get serializedHeaderSize => 4 * 4; 191 | 192 | @override 193 | int get serializedSize => 194 | serializedHeaderSize + formatId.length + mpIntLength(r) + mpIntLength(s); 195 | 196 | @override 197 | void deserialize(SerializableInput input) { 198 | formatId = deserializeString(input); 199 | Uint8List blob = deserializeStringBytes(input); 200 | if (!formatId.startsWith('ecdsa-sha2-')) throw FormatException(formatId); 201 | SerializableInput blobInput = SerializableInput(blob); 202 | r = deserializeMpInt(blobInput); 203 | s = deserializeMpInt(blobInput); 204 | if (!blobInput.done) throw FormatException('${blobInput.offset}'); 205 | } 206 | 207 | @override 208 | void serialize(SerializableOutput output) { 209 | serializeString(output, formatId); 210 | Uint8List blob = Uint8List(4 * 2 + mpIntLength(r) + mpIntLength(s)); 211 | SerializableOutput blobOutput = SerializableOutput(blob); 212 | serializeMpInt(blobOutput, r); 213 | serializeMpInt(blobOutput, s); 214 | if (!blobOutput.done) throw FormatException('${blobOutput.offset}'); 215 | serializeString(output, blob); 216 | } 217 | } 218 | 219 | /// https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-02#section-4 220 | class Ed25519Key with Serializable { 221 | String formatId = 'ssh-ed25519'; 222 | Uint8List key; 223 | Ed25519Key([this.key]); 224 | 225 | @override 226 | int get serializedHeaderSize => 4 * 2 + 11; 227 | 228 | @override 229 | int get serializedSize => serializedHeaderSize + key.length; 230 | 231 | @override 232 | void deserialize(SerializableInput input) { 233 | formatId = deserializeString(input); 234 | key = deserializeStringBytes(input); 235 | if (formatId != 'ssh-ed25519') throw FormatException(formatId); 236 | } 237 | 238 | @override 239 | void serialize(SerializableOutput output) { 240 | serializeString(output, formatId); 241 | serializeString(output, key); 242 | } 243 | } 244 | 245 | /// https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-02#section-6 246 | class Ed25519Signature with Serializable { 247 | String formatId = 'ssh-ed25519'; 248 | Uint8List sig; 249 | Ed25519Signature([this.sig]); 250 | 251 | @override 252 | int get serializedHeaderSize => 4 * 2 + 11; 253 | 254 | @override 255 | int get serializedSize => serializedHeaderSize + sig.length; 256 | 257 | @override 258 | void deserialize(SerializableInput input) { 259 | formatId = deserializeString(input); 260 | sig = deserializeStringBytes(input); 261 | if (formatId != 'ssh-ed25519') throw FormatException(formatId); 262 | } 263 | 264 | @override 265 | void serialize(SerializableOutput output) { 266 | serializeString(output, formatId); 267 | serializeString(output, sig); 268 | } 269 | } 270 | 271 | /// Verifies Ed25519 [signature] on [message] with private key matching [publicKey]. 272 | bool verifyEd25519Signature( 273 | Ed25519Key publicKey, Ed25519Signature signature, Uint8List message) => 274 | tweetnacl.Signature(publicKey.key, null) 275 | .detached_verify(message, signature.sig); 276 | 277 | /// Verifies ECDSA [signature] on [message] with private key matching [publicKey]. 278 | bool verifyECDSASignature(int keyType, ECDSAKey publicKey, 279 | ECDSASignature signature, Uint8List message) { 280 | ECDSASigner signer = ECDSASigner(Key.ellipticCurveHash(keyType)); 281 | ECDomainParameters curve = Key.ellipticCurve(keyType); 282 | signer.init( 283 | false, 284 | PublicKeyParameter( 285 | ECPublicKey(curve.curve.decodePoint(publicKey.q), curve))); 286 | return signer.verifySignature(message, ECSignature(signature.r, signature.s)); 287 | } 288 | 289 | /// Verifies RSA [signature] on [message] with private key matching [publicKey]. 290 | bool verifyRSASignature( 291 | RSAKey publicKey, RSASignature signature, Uint8List message) { 292 | RSASigner signer = RSASigner(SHA1Digest(), '06052b0e03021a'); 293 | signer.init( 294 | false, 295 | ParametersWithRandom( 296 | PublicKeyParameter( 297 | asymmetric.RSAPublicKey(publicKey.n, publicKey.e)), 298 | null)); 299 | return signer.verifySignature( 300 | message, asymmetric.RSASignature(signature.sig)); 301 | } 302 | -------------------------------------------------------------------------------- /lib/kex.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:math'; 5 | import 'dart:typed_data'; 6 | 7 | import "package:pointycastle/api.dart"; 8 | import 'package:pointycastle/digests/sha1.dart'; 9 | import "package:pointycastle/digests/sha256.dart"; 10 | import 'package:pointycastle/ecc/api.dart'; 11 | import 'package:pointycastle/src/utils.dart'; 12 | import 'package:tweetnacl/tweetnacl.dart'; 13 | 14 | import 'package:dartssh/protocol.dart'; 15 | import 'package:dartssh/ssh.dart'; 16 | 17 | /// Mixin providing a suite of key exchange methods. 18 | mixin SSHDiffieHellman { 19 | DiffieHellman dh = DiffieHellman(); 20 | EllipticCurveDiffieHellman ecdh = EllipticCurveDiffieHellman(); 21 | X25519DiffieHellman x25519dh = X25519DiffieHellman(); 22 | Digest kexHash; 23 | BigInt K; 24 | 25 | void initializeDiffieHellman(int kexMethod, Random random) { 26 | if (KEX.x25519DiffieHellman(kexMethod)) { 27 | kexHash = SHA256Digest(); 28 | x25519dh.generatePair(random); 29 | } else if (KEX.ellipticCurveDiffieHellman(kexMethod)) { 30 | kexHash = KEX.ellipticCurveHash(kexMethod); 31 | ecdh = EllipticCurveDiffieHellman( 32 | KEX.ellipticCurve(kexMethod), KEX.ellipticCurveSecretBits(kexMethod)); 33 | ecdh.generatePair(random); 34 | } else if (KEX.diffieHellmanGroupExchange(kexMethod)) { 35 | if (kexMethod == KEX.DHGEX_SHA1) { 36 | kexHash = SHA1Digest(); 37 | } else if (kexMethod == KEX.DHGEX_SHA256) { 38 | kexHash = SHA256Digest(); 39 | } 40 | } else if (KEX.diffieHellman(kexMethod)) { 41 | if (kexMethod == KEX.DH14_SHA1) { 42 | dh = DiffieHellman.group14(); 43 | } else if (kexMethod == KEX.DH1_SHA1) { 44 | dh = DiffieHellman.group1(); 45 | } 46 | kexHash = SHA1Digest(); 47 | dh.generatePair(random); 48 | } else { 49 | throw FormatException('unknown kex method: $kexMethod'); 50 | } 51 | } 52 | 53 | void initializeDiffieHellmanGroup(BigInt p, BigInt g, Random random) { 54 | dh = DiffieHellman(p, g, 256); 55 | dh.generatePair(random); 56 | } 57 | } 58 | 59 | /// https://tools.ietf.org/html/rfc7748#section-6 60 | class X25519DiffieHellman { 61 | Uint8List myPrivKey, myPubKey, remotePubKey; 62 | 63 | void generatePair(Random random) { 64 | myPrivKey = randBytes(random, 32); 65 | myPubKey = ScalarMult.scalseMult_base(myPrivKey); 66 | } 67 | 68 | BigInt computeSecret(Uint8List remotePubKey) { 69 | this.remotePubKey = remotePubKey; 70 | return decodeBigInt(ScalarMult.scalseMult(myPrivKey, remotePubKey)); 71 | } 72 | } 73 | 74 | /// The Elliptic Curve Diffie-Hellman (ECDH) key exchange method 75 | /// generates a shared secret from an ephemeral local elliptic curve 76 | /// private key and ephemeral remote elliptic curve public key. 77 | class EllipticCurveDiffieHellman { 78 | ECDomainParameters curve; 79 | int secretBits; 80 | BigInt x; 81 | Uint8List cText, sText; 82 | EllipticCurveDiffieHellman([this.curve, this.secretBits]); 83 | 84 | /// Generate ephemeral key pair. 85 | void generatePair(Random random) { 86 | do { 87 | x = decodeBigInt(randBits(random, secretBits)) % curve.n; 88 | } while (x == BigInt.zero); 89 | ECPoint c = curve.G * x; 90 | cText = c.getEncoded(false); 91 | } 92 | 93 | /// Compute shared secret. 94 | BigInt computeSecret(Uint8List sText) { 95 | this.sText = sText; 96 | ECPoint s = curve.curve.decodePoint(sText); 97 | return (s * x).x.toBigInteger(); 98 | } 99 | } 100 | 101 | /// The Diffie-Hellman (DH) key exchange provides a shared secret that 102 | /// cannot be determined by either party alone. 103 | /// https://tools.ietf.org/html/rfc4253#section-8 104 | class DiffieHellman { 105 | int gexMin = 1024, gexMax = 8192, gexPref = 2048, secretBits; 106 | BigInt g, p, x, e, f; 107 | DiffieHellman([this.p, this.g, this.secretBits]); 108 | 109 | /// https://tools.ietf.org/html/rfc2409 Second Oakley Group 110 | DiffieHellman.group1() 111 | : secretBits = 160, 112 | g = BigInt.from(2), 113 | p = decodeBigInt(Uint8List.fromList([ 114 | 0xff, 115 | 0xff, 116 | 0xff, 117 | 0xff, 118 | 0xff, 119 | 0xff, 120 | 0xff, 121 | 0xff, 122 | 0xc9, 123 | 0x0f, 124 | 0xda, 125 | 0xa2, 126 | 0x21, 127 | 0x68, 128 | 0xc2, 129 | 0x34, 130 | 0xc4, 131 | 0xc6, 132 | 0x62, 133 | 0x8b, 134 | 0x80, 135 | 0xdc, 136 | 0x1c, 137 | 0xd1, 138 | 0x29, 139 | 0x02, 140 | 0x4e, 141 | 0x08, 142 | 0x8a, 143 | 0x67, 144 | 0xcc, 145 | 0x74, 146 | 0x02, 147 | 0x0b, 148 | 0xbe, 149 | 0xa6, 150 | 0x3b, 151 | 0x13, 152 | 0x9b, 153 | 0x22, 154 | 0x51, 155 | 0x4a, 156 | 0x08, 157 | 0x79, 158 | 0x8e, 159 | 0x34, 160 | 0x04, 161 | 0xdd, 162 | 0xef, 163 | 0x95, 164 | 0x19, 165 | 0xb3, 166 | 0xcd, 167 | 0x3a, 168 | 0x43, 169 | 0x1b, 170 | 0x30, 171 | 0x2b, 172 | 0x0a, 173 | 0x6d, 174 | 0xf2, 175 | 0x5f, 176 | 0x14, 177 | 0x37, 178 | 0x4f, 179 | 0xe1, 180 | 0x35, 181 | 0x6d, 182 | 0x6d, 183 | 0x51, 184 | 0xc2, 185 | 0x45, 186 | 0xe4, 187 | 0x85, 188 | 0xb5, 189 | 0x76, 190 | 0x62, 191 | 0x5e, 192 | 0x7e, 193 | 0xc6, 194 | 0xf4, 195 | 0x4c, 196 | 0x42, 197 | 0xe9, 198 | 0xa6, 199 | 0x37, 200 | 0xed, 201 | 0x6b, 202 | 0x0b, 203 | 0xff, 204 | 0x5c, 205 | 0xb6, 206 | 0xf4, 207 | 0x06, 208 | 0xb7, 209 | 0xed, 210 | 0xee, 211 | 0x38, 212 | 0x6b, 213 | 0xfb, 214 | 0x5a, 215 | 0x89, 216 | 0x9f, 217 | 0xa5, 218 | 0xae, 219 | 0x9f, 220 | 0x24, 221 | 0x11, 222 | 0x7c, 223 | 0x4b, 224 | 0x1f, 225 | 0xe6, 226 | 0x49, 227 | 0x28, 228 | 0x66, 229 | 0x51, 230 | 0xec, 231 | 0xe6, 232 | 0x53, 233 | 0x81, 234 | 0xff, 235 | 0xff, 236 | 0xff, 237 | 0xff, 238 | 0xff, 239 | 0xff, 240 | 0xff, 241 | 0xff 242 | ])); 243 | 244 | /// https://tools.ietf.org/html/rfc3526 Oakley Group 14 245 | DiffieHellman.group14() 246 | : secretBits = 224, 247 | g = BigInt.from(2), 248 | p = decodeBigInt(Uint8List.fromList([ 249 | 0xff, 250 | 0xff, 251 | 0xff, 252 | 0xff, 253 | 0xff, 254 | 0xff, 255 | 0xff, 256 | 0xff, 257 | 0xc9, 258 | 0x0f, 259 | 0xda, 260 | 0xa2, 261 | 0x21, 262 | 0x68, 263 | 0xc2, 264 | 0x34, 265 | 0xc4, 266 | 0xc6, 267 | 0x62, 268 | 0x8b, 269 | 0x80, 270 | 0xdc, 271 | 0x1c, 272 | 0xd1, 273 | 0x29, 274 | 0x02, 275 | 0x4e, 276 | 0x08, 277 | 0x8a, 278 | 0x67, 279 | 0xcc, 280 | 0x74, 281 | 0x02, 282 | 0x0b, 283 | 0xbe, 284 | 0xa6, 285 | 0x3b, 286 | 0x13, 287 | 0x9b, 288 | 0x22, 289 | 0x51, 290 | 0x4a, 291 | 0x08, 292 | 0x79, 293 | 0x8e, 294 | 0x34, 295 | 0x04, 296 | 0xdd, 297 | 0xef, 298 | 0x95, 299 | 0x19, 300 | 0xb3, 301 | 0xcd, 302 | 0x3a, 303 | 0x43, 304 | 0x1b, 305 | 0x30, 306 | 0x2b, 307 | 0x0a, 308 | 0x6d, 309 | 0xf2, 310 | 0x5f, 311 | 0x14, 312 | 0x37, 313 | 0x4f, 314 | 0xe1, 315 | 0x35, 316 | 0x6d, 317 | 0x6d, 318 | 0x51, 319 | 0xc2, 320 | 0x45, 321 | 0xe4, 322 | 0x85, 323 | 0xb5, 324 | 0x76, 325 | 0x62, 326 | 0x5e, 327 | 0x7e, 328 | 0xc6, 329 | 0xf4, 330 | 0x4c, 331 | 0x42, 332 | 0xe9, 333 | 0xa6, 334 | 0x37, 335 | 0xed, 336 | 0x6b, 337 | 0x0b, 338 | 0xff, 339 | 0x5c, 340 | 0xb6, 341 | 0xf4, 342 | 0x06, 343 | 0xb7, 344 | 0xed, 345 | 0xee, 346 | 0x38, 347 | 0x6b, 348 | 0xfb, 349 | 0x5a, 350 | 0x89, 351 | 0x9f, 352 | 0xa5, 353 | 0xae, 354 | 0x9f, 355 | 0x24, 356 | 0x11, 357 | 0x7c, 358 | 0x4b, 359 | 0x1f, 360 | 0xe6, 361 | 0x49, 362 | 0x28, 363 | 0x66, 364 | 0x51, 365 | 0xec, 366 | 0xe4, 367 | 0x5b, 368 | 0x3d, 369 | 0xc2, 370 | 0x00, 371 | 0x7c, 372 | 0xb8, 373 | 0xa1, 374 | 0x63, 375 | 0xbf, 376 | 0x05, 377 | 0x98, 378 | 0xda, 379 | 0x48, 380 | 0x36, 381 | 0x1c, 382 | 0x55, 383 | 0xd3, 384 | 0x9a, 385 | 0x69, 386 | 0x16, 387 | 0x3f, 388 | 0xa8, 389 | 0xfd, 390 | 0x24, 391 | 0xcf, 392 | 0x5f, 393 | 0x83, 394 | 0x65, 395 | 0x5d, 396 | 0x23, 397 | 0xdc, 398 | 0xa3, 399 | 0xad, 400 | 0x96, 401 | 0x1c, 402 | 0x62, 403 | 0xf3, 404 | 0x56, 405 | 0x20, 406 | 0x85, 407 | 0x52, 408 | 0xbb, 409 | 0x9e, 410 | 0xd5, 411 | 0x29, 412 | 0x07, 413 | 0x70, 414 | 0x96, 415 | 0x96, 416 | 0x6d, 417 | 0x67, 418 | 0x0c, 419 | 0x35, 420 | 0x4e, 421 | 0x4a, 422 | 0xbc, 423 | 0x98, 424 | 0x04, 425 | 0xf1, 426 | 0x74, 427 | 0x6c, 428 | 0x08, 429 | 0xca, 430 | 0x18, 431 | 0x21, 432 | 0x7c, 433 | 0x32, 434 | 0x90, 435 | 0x5e, 436 | 0x46, 437 | 0x2e, 438 | 0x36, 439 | 0xce, 440 | 0x3b, 441 | 0xe3, 442 | 0x9e, 443 | 0x77, 444 | 0x2c, 445 | 0x18, 446 | 0x0e, 447 | 0x86, 448 | 0x03, 449 | 0x9b, 450 | 0x27, 451 | 0x83, 452 | 0xa2, 453 | 0xec, 454 | 0x07, 455 | 0xa2, 456 | 0x8f, 457 | 0xb5, 458 | 0xc5, 459 | 0x5d, 460 | 0xf0, 461 | 0x6f, 462 | 0x4c, 463 | 0x52, 464 | 0xc9, 465 | 0xde, 466 | 0x2b, 467 | 0xcb, 468 | 0xf6, 469 | 0x95, 470 | 0x58, 471 | 0x17, 472 | 0x18, 473 | 0x39, 474 | 0x95, 475 | 0x49, 476 | 0x7c, 477 | 0xea, 478 | 0x95, 479 | 0x6a, 480 | 0xe5, 481 | 0x15, 482 | 0xd2, 483 | 0x26, 484 | 0x18, 485 | 0x98, 486 | 0xfa, 487 | 0x05, 488 | 0x10, 489 | 0x15, 490 | 0x72, 491 | 0x8e, 492 | 0x5a, 493 | 0x8a, 494 | 0xac, 495 | 0xaa, 496 | 0x68, 497 | 0xff, 498 | 0xff, 499 | 0xff, 500 | 0xff, 501 | 0xff, 502 | 0xff, 503 | 0xff, 504 | 0xff 505 | ])); 506 | 507 | void generatePair(Random random) { 508 | if (secretBits % 8 != 0) throw FormatException(); 509 | x = decodeBigInt(randBytes(random, secretBits ~/ 8)); 510 | e = g.modPow(x, p); 511 | } 512 | 513 | BigInt computeSecret(BigInt f) => (this.f = f).modPow(x, p); 514 | } 515 | -------------------------------------------------------------------------------- /lib/pem.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:convert'; 5 | import 'dart:typed_data'; 6 | 7 | import 'package:asn1lib/asn1lib.dart'; 8 | import 'package:pointycastle/api.dart' hide Signature; 9 | import 'package:pointycastle/asymmetric/api.dart' as asymmetric; 10 | import 'package:pointycastle/ecc/api.dart'; 11 | import 'package:tweetnacl/tweetnacl.dart' as tweetnacl; 12 | 13 | import 'package:dartssh/identity.dart'; 14 | import 'package:dartssh/protocol.dart'; 15 | import 'package:dartssh/serializable.dart'; 16 | import 'package:dartssh/ssh.dart'; 17 | import 'package:dartssh/transport.dart'; 18 | 19 | /// Privacy-Enhanced Mail (PEM) is a de facto file format for storing and sending 20 | /// cryptographic keys, certificates, and other data. 21 | Identity parsePem(String text, 22 | {StringFunction getPassword, Identity identity}) { 23 | identity ??= Identity(); 24 | const String beginText = '-----BEGIN ', 25 | endText = '-----END ', 26 | termText = '-----'; 27 | int beginBegin, beginEnd, endBegin, endEnd; 28 | if ((beginBegin = text.indexOf(beginText)) == -1) { 29 | throw FormatException('missing $beginText'); 30 | } 31 | if ((beginEnd = text.indexOf(termText, beginBegin + beginText.length)) == 32 | -1) { 33 | throw FormatException('missing $termText'); 34 | } 35 | if ((endBegin = text.indexOf(endText, beginEnd + termText.length)) == -1) { 36 | throw FormatException('missing $endText'); 37 | } 38 | if ((endEnd = text.indexOf(termText, endBegin + endText.length)) == -1) { 39 | throw FormatException('missing $termText'); 40 | } 41 | 42 | String type = text.substring(beginBegin + beginText.length, beginEnd); 43 | if (type != text.substring(endBegin + endText.length, endEnd)) { 44 | throw FormatException('type disagreement: $type'); 45 | } 46 | 47 | int start = beginEnd + termText.length, end = endBegin; 48 | if (start < text.length && text[start] == '\r') start++; 49 | if (start < text.length && text[start] == '\n') start++; 50 | 51 | String headersEndText = '\n\n', procType; 52 | int headersStart = -1, headersEnd = text.indexOf(headersEndText, start); 53 | if (headersEnd == -1 || headersEnd >= end) { 54 | headersEndText = '\r\n\r\n'; 55 | headersEnd = text.indexOf(headersEndText, start); 56 | } 57 | if (headersEnd != -1 && headersEnd < end) { 58 | headersStart = start; 59 | start = headersEnd + headersEndText.length; 60 | for (String header 61 | in LineSplitter().convert(text.substring(headersStart, headersEnd))) { 62 | if (header.startsWith('Proc-Type: ')) { 63 | procType = header.substring(11); 64 | } else if (header.startsWith('DEK-Info: ')) { 65 | throw FormatException('not supported'); 66 | } 67 | } 68 | } 69 | 70 | String base64text = ''; 71 | for (String line in LineSplitter().convert(text.substring(start, end))) { 72 | base64text += line.trim(); 73 | } 74 | Uint8List payload = base64.decode(base64text); 75 | 76 | switch (type) { 77 | case 'OPENSSH PRIVATE KEY': 78 | OpenSSHKey openssh = OpenSSHKey() 79 | ..deserialize(SerializableInput(payload)); 80 | Uint8List privateKey; 81 | switch (openssh.kdfname) { 82 | case 'bcrypt': 83 | OpenSSHBCryptKDFOptions kdfoptions = OpenSSHBCryptKDFOptions() 84 | ..deserialize(SerializableInput(openssh.kdfoptions)); 85 | int cipherAlgo; 86 | if (openssh.ciphername == 'aes256-cbc') { 87 | cipherAlgo = Cipher.AES256_CBC; 88 | } else { 89 | throw FormatException('cipher ${openssh.ciphername}'); 90 | } 91 | privateKey = opensshKeyCrypt( 92 | false, 93 | (getPassword != null ? getPassword() : '').codeUnits, 94 | kdfoptions.salt, 95 | kdfoptions.rounds, 96 | openssh.privatekey, 97 | cipherAlgo); 98 | break; 99 | 100 | case 'none': 101 | privateKey = openssh.privatekey; 102 | break; 103 | 104 | default: 105 | throw FormatException('kdf ${openssh.kdfname}'); 106 | } 107 | SerializableInput input = SerializableInput(privateKey); 108 | OpenSSHPrivateKeyHeader().deserialize(input); 109 | String type = deserializeString(SerializableInput(input.viewRemaining())); 110 | switch (type) { 111 | case 'ssh-ed25519': 112 | OpenSSHEd25519PrivateKey ed25519 = OpenSSHEd25519PrivateKey() 113 | ..deserialize(input); 114 | if (identity.ed25519 != null) throw FormatException(); 115 | identity.ed25519 = 116 | tweetnacl.Signature.keyPair_fromSecretKey(ed25519.privkey); 117 | if (!equalUint8List(identity.ed25519.publicKey, ed25519.pubkey)) { 118 | throw FormatException(); 119 | } 120 | return identity; 121 | 122 | case 'ssh-rsa': 123 | OpenSSHRSAPrivateKey rsaPrivateKey = OpenSSHRSAPrivateKey() 124 | ..deserialize(input); 125 | if (identity.rsaPublic != null || identity.rsaPrivate != null) { 126 | throw FormatException(); 127 | } 128 | return identity 129 | ..rsaPublic = 130 | asymmetric.RSAPublicKey(rsaPrivateKey.n, rsaPrivateKey.e) 131 | ..rsaPrivate = asymmetric.RSAPrivateKey(rsaPrivateKey.n, 132 | rsaPrivateKey.d, rsaPrivateKey.p, rsaPrivateKey.q); 133 | 134 | default: 135 | if (type.startsWith('ecdsa-')) { 136 | OpenSSHECDSAPrivateKey ecdsaPrivateKey = OpenSSHECDSAPrivateKey() 137 | ..deserialize(input); 138 | ECDomainParameters curve = 139 | Key.ellipticCurve(ecdsaPrivateKey.keyTypeId); 140 | if (identity.ecdsaPublic != null || identity.ecdsaPrivate != null) { 141 | throw FormatException(); 142 | } 143 | identity 144 | ..ecdsaKeyType = ecdsaPrivateKey.keyTypeId 145 | ..ecdsaPublic = 146 | ECPublicKey(curve.curve.decodePoint(ecdsaPrivateKey.q), curve) 147 | ..ecdsaPrivate = ECPrivateKey(ecdsaPrivateKey.d, curve); 148 | 149 | if (curve.G * identity.ecdsaPrivate.d != identity.ecdsaPublic.Q) { 150 | throw FormatException(); 151 | } 152 | return identity; 153 | } else { 154 | throw FormatException('type $type'); 155 | } 156 | } 157 | break; 158 | 159 | case 'RSA PRIVATE KEY': 160 | RSAPrivateKey rsaPrivateKey = RSAPrivateKey() 161 | ..deserialize(SerializableInput(payload)); 162 | if (identity.rsaPublic != null || identity.rsaPrivate != null) { 163 | throw FormatException(); 164 | } 165 | return identity 166 | ..rsaPublic = asymmetric.RSAPublicKey(rsaPrivateKey.n, rsaPrivateKey.e) 167 | ..rsaPrivate = asymmetric.RSAPrivateKey( 168 | rsaPrivateKey.n, rsaPrivateKey.d, rsaPrivateKey.p, rsaPrivateKey.q); 169 | 170 | default: 171 | throw FormatException('type not supported: $type'); 172 | } 173 | } 174 | 175 | /// https://tools.ietf.org/html/rfc3447#appendix-A.1.2 176 | class RSAPrivateKey extends Serializable { 177 | BigInt version, n, e, d, p, q, exponent1, exponent2, coefficient; 178 | 179 | @override 180 | int get serializedSize => null; 181 | 182 | /// https://gist.github.com/proteye/982d9991922276ccfb011dfc55443d74 183 | @override 184 | void deserialize(SerializableInput input) { 185 | ASN1Parser asn1Parser = ASN1Parser(input.viewRemaining()); 186 | ASN1Sequence pkSeq = asn1Parser.nextObject(); 187 | version = (pkSeq.elements[0] as ASN1Integer).valueAsBigInteger; 188 | n = (pkSeq.elements[1] as ASN1Integer).valueAsBigInteger; 189 | e = (pkSeq.elements[2] as ASN1Integer).valueAsBigInteger; 190 | d = (pkSeq.elements[3] as ASN1Integer).valueAsBigInteger; 191 | p = (pkSeq.elements[4] as ASN1Integer).valueAsBigInteger; 192 | q = (pkSeq.elements[5] as ASN1Integer).valueAsBigInteger; 193 | exponent1 = (pkSeq.elements[6] as ASN1Integer).valueAsBigInteger; 194 | exponent2 = (pkSeq.elements[7] as ASN1Integer).valueAsBigInteger; 195 | coefficient = (pkSeq.elements[8] as ASN1Integer).valueAsBigInteger; 196 | } 197 | 198 | @override 199 | void serialize(SerializableOutput output) {} 200 | 201 | String toString() => 'version: $version, n: $n, d: $d, e: $e, p: $p, q: $q'; 202 | } 203 | 204 | /// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key 205 | class OpenSSHKey extends Serializable { 206 | String magic = 'openssh-key-v1', ciphername, kdfname; 207 | Uint8List kdfoptions, privatekey; 208 | List publickeys; 209 | OpenSSHKey([this.ciphername, this.kdfname, this.kdfoptions, this.privatekey]); 210 | 211 | @override 212 | int get serializedHeaderSize => 5 * 4 + 15; 213 | 214 | @override 215 | int get serializedSize => 216 | serializedHeaderSize + 217 | ciphername.length + 218 | kdfname.length + 219 | kdfoptions.length + 220 | privatekey.length + 221 | publickeys.fold(0, (v, e) => v += e.length); 222 | 223 | @override 224 | void deserialize(SerializableInput input) { 225 | Uint8List nullTerminatedMagic = input.getBytes(15); 226 | magic = String.fromCharCodes(nullTerminatedMagic, 0, 14); 227 | if (magic != 'openssh-key-v1') throw FormatException('wrong magic: $magic'); 228 | 229 | ciphername = deserializeString(input); 230 | kdfname = deserializeString(input); 231 | kdfoptions = deserializeStringBytes(input); 232 | publickeys = List(input.getUint32()); 233 | for (int i = 0; i < publickeys.length; i++) { 234 | publickeys[i] = deserializeStringBytes(input); 235 | } 236 | privatekey = deserializeStringBytes(input); 237 | } 238 | 239 | @override 240 | void serialize(SerializableOutput output) { 241 | output.addBytes(magic.codeUnits); 242 | output.addUint8(0); 243 | 244 | serializeString(output, ciphername); 245 | serializeString(output, kdfname); 246 | serializeString(output, kdfoptions); 247 | output.addUint32(publickeys.length); 248 | for (Uint8List publickey in publickeys) { 249 | serializeString(output, publickey); 250 | } 251 | serializeString(output, privatekey); 252 | } 253 | } 254 | 255 | /// Before the key is encrypted, a random integer is assigned to both checkint fields so successful 256 | /// decryption can be quickly checked by verifying that both checkint fields hold the same value. 257 | class OpenSSHPrivateKeyHeader extends Serializable { 258 | int checkint1 = 0, checkint2 = 0; 259 | OpenSSHPrivateKeyHeader(); 260 | 261 | @override 262 | int get serializedHeaderSize => 2 * 4; 263 | 264 | @override 265 | int get serializedSize => serializedHeaderSize; 266 | 267 | @override 268 | void deserialize(SerializableInput input) { 269 | checkint1 = input.getUint32(); 270 | checkint2 = input.getUint32(); 271 | if (checkint1 != checkint2) { 272 | throw FormatException('$checkint1 != $checkint2'); 273 | } 274 | } 275 | 276 | @override 277 | void serialize(SerializableOutput output) { 278 | output.addUint32(checkint1); 279 | output.addUint32(checkint2); 280 | } 281 | } 282 | 283 | /// https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L3274 284 | class OpenSSHRSAPrivateKey extends Serializable { 285 | String keytype = 'ssh-rsa', comment; 286 | BigInt n, e, d, iqmp, p, q; 287 | OpenSSHRSAPrivateKey(); 288 | 289 | @override 290 | int get serializedHeaderSize => 7 * 4; 291 | 292 | @override 293 | int get serializedSize => 294 | serializedHeaderSize + 295 | mpIntLength(n) + 296 | mpIntLength(e) + 297 | mpIntLength(d) + 298 | mpIntLength(iqmp) + 299 | mpIntLength(p) + 300 | mpIntLength(q) + 301 | comment.length; 302 | 303 | @override 304 | void deserialize(SerializableInput input) { 305 | keytype = deserializeString(input); 306 | if (keytype != 'ssh-rsa') throw FormatException('$keytype'); 307 | n = deserializeMpInt(input); 308 | e = deserializeMpInt(input); 309 | d = deserializeMpInt(input); 310 | iqmp = deserializeMpInt(input); 311 | p = deserializeMpInt(input); 312 | q = deserializeMpInt(input); 313 | comment = deserializeString(input); 314 | } 315 | 316 | @override 317 | void serialize(SerializableOutput output) {} 318 | 319 | String toString() => 'n: $n, d: $d, e: $e, p: $p, q: $q'; 320 | } 321 | 322 | /// https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L3223 323 | class OpenSSHECDSAPrivateKey extends Serializable { 324 | String keytype, curveName, comment; 325 | int keyTypeId; 326 | Uint8List q; 327 | BigInt d; 328 | OpenSSHECDSAPrivateKey(); 329 | 330 | @override 331 | int get serializedHeaderSize => 4 * 5; 332 | 333 | @override 334 | int get serializedSize => 335 | serializedHeaderSize + 336 | keytype.length + 337 | curveName.length + 338 | q.length + 339 | mpIntLength(d); 340 | 341 | @override 342 | void deserialize(SerializableInput input) { 343 | keytype = deserializeString(input); 344 | if (!keytype.startsWith('ecdsa-sha2-')) throw FormatException('$keytype'); 345 | keyTypeId = Key.id(keytype); 346 | if (!Key.ellipticCurveDSA(keyTypeId)) throw FormatException(); 347 | curveName = deserializeString(input); 348 | q = deserializeStringBytes(input); 349 | d = deserializeMpInt(input); 350 | comment = deserializeString(input); 351 | } 352 | 353 | @override 354 | void serialize(SerializableOutput output) {} 355 | } 356 | 357 | /// https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L2446 358 | class OpenSSHEd25519PrivateKey extends Serializable { 359 | String keytype = 'ssh-ed25519', comment; 360 | Uint8List pubkey, privkey; 361 | OpenSSHEd25519PrivateKey([this.pubkey, this.privkey, this.comment]); 362 | 363 | @override 364 | int get serializedHeaderSize => 4 * 4; 365 | 366 | @override 367 | int get serializedSize => 368 | serializedHeaderSize + 369 | keytype.length + 370 | pubkey.length + 371 | privkey.length + 372 | comment.length; 373 | 374 | @override 375 | void deserialize(SerializableInput input) { 376 | keytype = deserializeString(input); 377 | if (keytype != 'ssh-ed25519') throw FormatException('$keytype'); 378 | pubkey = deserializeStringBytes(input); 379 | if (pubkey.length != 32) throw FormatException('${pubkey.length}'); 380 | privkey = deserializeStringBytes(input); 381 | if (privkey.length != 64) throw FormatException('${privkey.length}'); 382 | comment = deserializeString(input); 383 | } 384 | 385 | @override 386 | void serialize(SerializableOutput output) { 387 | serializeString(output, keytype); 388 | serializeString(output, pubkey); 389 | serializeString(output, privkey); 390 | serializeString(output, comment); 391 | } 392 | } 393 | 394 | /// The options: string salt, uint32 rounds are concatenated and represented as a string. 395 | class OpenSSHBCryptKDFOptions extends Serializable { 396 | Uint8List salt; 397 | int rounds; 398 | OpenSSHBCryptKDFOptions([this.salt, this.rounds]); 399 | 400 | @override 401 | int get serializedHeaderSize => 2 * 4; 402 | 403 | @override 404 | int get serializedSize => serializedHeaderSize + salt.length; 405 | 406 | @override 407 | void deserialize(SerializableInput input) { 408 | salt = deserializeStringBytes(input); 409 | rounds = input.getUint32(); 410 | } 411 | 412 | @override 413 | void serialize(SerializableOutput output) { 414 | serializeString(output, salt); 415 | output.addUint32(rounds); 416 | } 417 | } 418 | 419 | Uint8List opensshKeyCrypt(bool forEncryption, Uint8List password, 420 | Uint8List salt, int rounds, Uint8List input, int cipherAlgo) { 421 | int keySize = Cipher.keySize(cipherAlgo), 422 | blockSize = Cipher.blockSize(cipherAlgo); 423 | Uint8List key = bcryptPbkdf(password, salt, keySize + blockSize, rounds); 424 | BlockCipher cipher = Cipher.cipher(cipherAlgo); 425 | cipher.init( 426 | forEncryption, 427 | ParametersWithIV(KeyParameter(viewUint8List(key, 0, keySize)), 428 | viewUint8List(key, keySize, blockSize))); 429 | return applyBlockCipher(cipher, input); 430 | } 431 | 432 | Uint8List bcryptHash(Uint8List pass, Uint8List salt) { 433 | throw FormatException('bcryptHash not implemented'); 434 | } 435 | 436 | Uint8List bcryptPbkdf( 437 | Uint8List password, Uint8List salt, int length, int rounds) { 438 | throw FormatException('bcryptPbkdf not implemented'); 439 | } 440 | -------------------------------------------------------------------------------- /lib/serializable.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:typed_data'; 5 | 6 | /// Rounds [input] up to the nearest [n]th. 7 | int nextMultipleOfN(int input, int n) => 8 | (input % n != 0) ? (input ~/ n + 1) * n : input; 9 | 10 | /// Returns concatenation of [x] and [y]. 11 | Uint8List appendUint8List(Uint8List x, Uint8List y) => 12 | Uint8List.fromList(x + y); 13 | 14 | /// Returns view of [x], accounting for when [x] is another view. 15 | Uint8List viewUint8List(Uint8List x, [int offset = 0, int length]) => 16 | Uint8List.view(x.buffer, x.offsetInBytes + offset, length ?? x.length); 17 | 18 | /// Returns the position of the first match of [needle] in [haystack] or -1. 19 | int searchUint8List(Uint8List haystack, Uint8List needle) { 20 | if (needle.isEmpty) return -1; 21 | for (int i = 0; i < haystack.length - needle.length + 1; i++) { 22 | int j = 0; 23 | while (j < needle.length && haystack[i + j] == needle[j]) { 24 | j++; 25 | } 26 | if (j == needle.length) return i; 27 | } 28 | return -1; 29 | } 30 | 31 | /// Returns true if [x] and [y] are equivalent. 32 | bool equalUint8List(Uint8List x, Uint8List y) { 33 | if (x.length != y.length) return false; 34 | for (int i = 0; i < x.length; ++i) { 35 | if (x[i] != y[i]) return false; 36 | } 37 | return true; 38 | } 39 | 40 | /// A [Uint8List] deque for consuming binary protocol. 41 | class QueueBuffer { 42 | Uint8List data; 43 | QueueBuffer(this.data); 44 | 45 | /// Appends [x] to [data]. 46 | void add(Uint8List x) => data = Uint8List.fromList((data ?? []) + x); 47 | 48 | /// Removes [0..x] of [data]. 49 | void flush(int x) => data = data.sublist(x); 50 | } 51 | 52 | /// Base class for advancing [offset] view of Uint8List [data]. 53 | abstract class SerializableBuffer { 54 | int offset = 0; 55 | final Uint8List buffer; 56 | final ByteData data; 57 | final Endian endian; 58 | SerializableBuffer(this.buffer, {this.endian = Endian.big}) 59 | : this.data = 60 | ByteData.view(buffer.buffer, buffer.offsetInBytes, buffer.length); 61 | 62 | bool get done => offset == buffer.length; 63 | int get remaining => buffer.length - offset; 64 | 65 | Uint8List view() => viewOffset(0, offset); 66 | Uint8List viewRemaining() => viewOffset(offset, buffer.length); 67 | Uint8List viewOffset(int start, int end) => 68 | viewUint8List(buffer, start, end - start); 69 | } 70 | 71 | /// Consumes [SerializableBuffer] to deserialized input. 72 | class SerializableInput extends SerializableBuffer { 73 | SerializableInput(Uint8List buffer, {Endian endian = Endian.big}) 74 | : super(buffer, endian: endian); 75 | 76 | bool getBool() => getUint8() == 0 ? false : true; 77 | 78 | int getUint8() { 79 | offset++; 80 | return data.getUint8(offset - 1); 81 | } 82 | 83 | int getUint16() { 84 | offset += 2; 85 | return data.getUint16(offset - 2, endian); 86 | } 87 | 88 | int getUint32() { 89 | offset += 4; 90 | return data.getUint32(offset - 4, endian); 91 | } 92 | 93 | int getUint64() { 94 | offset += 8; 95 | return data.getUint64(offset - 8, endian); 96 | } 97 | 98 | Uint8List getBytes(int length) { 99 | offset += length; 100 | return viewOffset(offset - length, offset); 101 | } 102 | } 103 | 104 | /// Fills [SerializableBuffer] with serialized output. 105 | class SerializableOutput extends SerializableBuffer { 106 | SerializableOutput(Uint8List buffer, {Endian endian = Endian.big}) 107 | : super(buffer, endian: endian); 108 | 109 | void addUint8(int x) { 110 | data.setUint8(offset, x); 111 | offset++; 112 | } 113 | 114 | void addUint16(int x) { 115 | data.setUint16(offset, x, endian); 116 | offset += 2; 117 | } 118 | 119 | void addUint32(int x) { 120 | data.setUint32(offset, x, endian); 121 | offset += 4; 122 | } 123 | 124 | void addUint64(int x) { 125 | data.setUint64(offset, x, endian); 126 | offset += 8; 127 | } 128 | 129 | void addBytes(Uint8List x) { 130 | buffer.setRange(offset, offset + x.length, x); 131 | offset += x.length; 132 | } 133 | } 134 | 135 | // Interface implemented by serializable objects. 136 | abstract class Serializable { 137 | /// Minimum size for this serialized object. 138 | int get serializedHeaderSize => null; 139 | 140 | /// Exact size for this serialized object. 141 | int get serializedSize; 142 | 143 | /// Interface for output serialization. 144 | void serialize(SerializableOutput output); 145 | 146 | /// Interface for intput serialization. 147 | void deserialize(SerializableInput input); 148 | 149 | /// Serializes this [Serializable] to a [Uint8List]. 150 | Uint8List toRaw({Endian endian = Endian.big}) { 151 | SerializableOutput ret = 152 | SerializableOutput(Uint8List(serializedSize), endian: endian); 153 | serialize(ret); 154 | if (!ret.done) { 155 | throw FormatException('${ret.offset}/${ret.buffer.length}'); 156 | } 157 | return ret.buffer; 158 | } 159 | 160 | /// Deserializes this [Serializable] from a [SerializableInput]. 161 | void fromRaw(SerializableInput input) { 162 | deserialize(input); 163 | if (!input.done) { 164 | throw FormatException('${input.offset}/${input.buffer.length}'); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/server.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:convert'; 5 | import 'dart:math'; 6 | import 'dart:typed_data'; 7 | 8 | import "package:pointycastle/api.dart"; 9 | 10 | import 'package:dartssh/identity.dart'; 11 | import 'package:dartssh/kex.dart'; 12 | import 'package:dartssh/socket.dart'; 13 | import 'package:dartssh/ssh.dart'; 14 | import 'package:dartssh/protocol.dart'; 15 | import 'package:dartssh/serializable.dart'; 16 | import 'package:dartssh/transport.dart'; 17 | 18 | typedef ChannelRequest = bool Function(SSHServer server, String request); 19 | typedef UserAuthRequest = bool Function(MSG_USERAUTH_REQUEST msg); 20 | typedef GexRequest = MapEntry Function(MSG_KEX_DH_GEX_REQUEST); 21 | 22 | class SSHServer extends SSHTransport { 23 | // Parameters 24 | RemoteForwardCallback directTcpRequest; 25 | UserAuthRequest userAuthRequest; 26 | ChannelRequest sessionChannelRequest; 27 | GexRequest gexRequest; 28 | 29 | SSHServer(Identity hostkey, 30 | {Uri hostport, 31 | bool compress = false, 32 | List forwardLocal, 33 | List forwardRemote, 34 | VoidCallback disconnected, 35 | ResponseCallback response, 36 | StringCallback print, 37 | StringCallback debugPrint, 38 | StringCallback tracePrint, 39 | SocketInterface socket, 40 | Random random, 41 | SecureRandom secureRandom, 42 | this.directTcpRequest, 43 | this.userAuthRequest, 44 | this.sessionChannelRequest, 45 | this.gexRequest}) 46 | : super(true, 47 | identity: hostkey, 48 | hostport: hostport, 49 | compress: compress, 50 | forwardLocal: forwardLocal, 51 | forwardRemote: forwardRemote, 52 | disconnected: disconnected, 53 | response: response, 54 | print: print, 55 | debugPrint: debugPrint, 56 | tracePrint: tracePrint, 57 | socket: socket, 58 | random: random, 59 | secureRandom: secureRandom) { 60 | onConnected(); 61 | } 62 | 63 | /// Does nothing. The client initializes Diffie Hellman. 64 | @override 65 | void sendDiffileHellmanInit() {} 66 | 67 | @override 68 | void handlePacket(Uint8List packet) { 69 | packetId = packetS.getUint8(); 70 | switch (packetId) { 71 | case MSG_KEXINIT.ID: 72 | state = state == SSHTransportState.FIRST_KEXINIT 73 | ? SSHTransportState.FIRST_KEXREPLY 74 | : SSHTransportState.KEXREPLY; 75 | handleMSG_KEXINIT(MSG_KEXINIT()..deserialize(packetS), packet); 76 | break; 77 | 78 | case MSG_KEXDH_INIT.ID: 79 | case MSG_KEX_DH_GEX_INIT.ID: 80 | handleMSG_KEXDH_INIT(packetId, packet); 81 | break; 82 | 83 | case MSG_KEX_DH_GEX_REQUEST.ID: 84 | handleMSG_KEX_DH_GEX_REQUEST( 85 | MSG_KEX_DH_GEX_REQUEST()..deserialize(packetS)); 86 | break; 87 | 88 | case MSG_NEWKEYS.ID: 89 | handleMSG_NEWKEYS(); 90 | break; 91 | 92 | case MSG_SERVICE_REQUEST.ID: 93 | handleMSG_SERVICE_REQUEST(MSG_SERVICE_REQUEST()..deserialize(packetS)); 94 | break; 95 | 96 | case MSG_USERAUTH_REQUEST.ID: 97 | handleMSG_USERAUTH_REQUEST( 98 | MSG_USERAUTH_REQUEST()..deserialize(packetS)); 99 | break; 100 | 101 | case MSG_CHANNEL_OPEN.ID: 102 | handleMSG_CHANNEL_OPEN( 103 | MSG_CHANNEL_OPEN()..deserialize(packetS), packetS); 104 | break; 105 | 106 | case MSG_CHANNEL_REQUEST.ID: 107 | handleMSG_CHANNEL_REQUEST(MSG_CHANNEL_REQUEST()..deserialize(packetS)); 108 | break; 109 | 110 | case MSG_CHANNEL_OPEN_CONFIRMATION.ID: 111 | handleMSG_CHANNEL_OPEN_CONFIRMATION( 112 | MSG_CHANNEL_OPEN_CONFIRMATION()..deserialize(packetS)); 113 | break; 114 | 115 | case MSG_CHANNEL_DATA.ID: 116 | handleMSG_CHANNEL_DATA(MSG_CHANNEL_DATA()..deserialize(packetS)); 117 | break; 118 | 119 | case MSG_CHANNEL_EOF.ID: 120 | handleMSG_CHANNEL_EOF(MSG_CHANNEL_EOF()..deserialize(packetS)); 121 | break; 122 | 123 | case MSG_CHANNEL_CLOSE.ID: 124 | handleMSG_CHANNEL_CLOSE(MSG_CHANNEL_CLOSE()..deserialize(packetS)); 125 | break; 126 | 127 | case MSG_DISCONNECT.ID: 128 | handleMSG_DISCONNECT(MSG_DISCONNECT()..deserialize(packetS)); 129 | break; 130 | 131 | default: 132 | if (print != null) { 133 | print('$hostport: unknown packet number: $packetId, len $packetLen'); 134 | } 135 | break; 136 | } 137 | } 138 | 139 | void handleMSG_KEXDH_INIT(int packetId, Uint8List packet) { 140 | if (packetId == MSG_KEX_ECDH_INIT.ID && 141 | KEX.x25519DiffieHellman(kexMethod)) { 142 | handleX25519MSG_KEX_ECDH_INIT(MSG_KEX_ECDH_INIT()..deserialize(packetS)); 143 | } else if (packetId == MSG_KEX_ECDH_INIT.ID && 144 | KEX.ellipticCurveDiffieHellman(kexMethod)) { 145 | handleEcDhMSG_KEX_ECDH_INIT(MSG_KEX_ECDH_INIT()..deserialize(packetS)); 146 | } else if ((packetId == MSG_KEXDH_INIT.ID && 147 | KEX.diffieHellman(kexMethod)) || 148 | (packetId == MSG_KEX_DH_GEX_INIT.ID && 149 | KEX.diffieHellmanGroupExchange(kexMethod))) { 150 | handleDhMSG_KEXDH_INIT(packetId, MSG_KEXDH_INIT()..deserialize(packetS)); 151 | } else { 152 | throw FormatException('unknown kex: $kexMethod'); 153 | } 154 | } 155 | 156 | void handleX25519MSG_KEX_ECDH_INIT(MSG_KEX_ECDH_INIT msg) { 157 | initializeDiffieHellman(kexMethod, random); 158 | K = x25519dh.computeSecret(msg.qC); 159 | Uint8List kS = identity.getRawPublicKey(hostkeyType); 160 | updateExchangeHash(kS); 161 | writeClearOrEncrypted(MSG_KEX_ECDH_REPLY(x25519dh.myPubKey, kS, 162 | identity.signMessage(hostkeyType, exH, getSecureRandom()))); 163 | sendNewKeys(); 164 | } 165 | 166 | void handleEcDhMSG_KEX_ECDH_INIT(MSG_KEX_ECDH_INIT msg) { 167 | initializeDiffieHellman(kexMethod, random); 168 | K = ecdh.computeSecret(msg.qC); 169 | Uint8List kS = identity.getRawPublicKey(hostkeyType); 170 | updateExchangeHash(kS); 171 | writeClearOrEncrypted(MSG_KEX_ECDH_REPLY(ecdh.cText, kS, 172 | identity.signMessage(hostkeyType, exH, getSecureRandom()))); 173 | sendNewKeys(); 174 | } 175 | 176 | void handleDhMSG_KEXDH_INIT(int packetId, MSG_KEXDH_INIT msg) { 177 | if (packetId != MSG_KEX_DH_GEX_INIT.ID) { 178 | initializeDiffieHellman(kexMethod, random); 179 | } 180 | K = dh.computeSecret(msg.e); 181 | Uint8List kS = identity.getRawPublicKey(hostkeyType); 182 | updateExchangeHash(kS); 183 | Uint8List hSig = identity.signMessage(hostkeyType, exH, getSecureRandom()); 184 | writeClearOrEncrypted(packetId == MSG_KEX_DH_GEX_INIT.ID 185 | ? MSG_KEX_DH_GEX_REPLY(dh.e, kS, hSig) 186 | : MSG_KEXDH_REPLY(dh.e, kS, hSig)); 187 | sendNewKeys(); 188 | } 189 | 190 | void handleMSG_KEX_DH_GEX_REQUEST(MSG_KEX_DH_GEX_REQUEST msg) { 191 | MapEntry group = 192 | gexRequest == null ? null : gexRequest(msg); 193 | if (group == null) { 194 | DiffieHellman group14 = DiffieHellman.group14(); 195 | group = MapEntry(group14.p, group14.g); 196 | } 197 | initializeDiffieHellman(kexMethod, random); 198 | initializeDiffieHellmanGroup(group.key, group.value, random); 199 | writeClearOrEncrypted(MSG_KEX_DH_GEX_GROUP(group.key, group.value)); 200 | } 201 | 202 | void handleMSG_SERVICE_REQUEST(MSG_SERVICE_REQUEST msg) { 203 | switch (msg.serviceName) { 204 | case 'ssh-userauth': 205 | writeCipher(MSG_SERVICE_ACCEPT(msg.serviceName)); 206 | break; 207 | 208 | default: 209 | throw FormatException('service name ${msg.serviceName}'); 210 | } 211 | } 212 | 213 | void handleMSG_USERAUTH_REQUEST(MSG_USERAUTH_REQUEST msg) { 214 | if (tracePrint != null) { 215 | tracePrint('$hostport: MSG_USERAUTH_REQUEST: $msg'); 216 | } 217 | 218 | if (userAuthRequest != null && userAuthRequest(msg)) { 219 | writeCipher(MSG_USERAUTH_SUCCESS()); 220 | } else { 221 | writeCipher(MSG_USERAUTH_FAILURE()); 222 | } 223 | } 224 | 225 | void handleMSG_CHANNEL_OPEN(MSG_CHANNEL_OPEN msg, SerializableInput packetS) { 226 | if (tracePrint != null) { 227 | tracePrint('$hostport: MSG_CHANNEL_OPEN type=${msg.channelType}'); 228 | } 229 | if (msg.channelType == 'session') { 230 | if (sessionChannel != null) { 231 | throw FormatException('already started session'); 232 | } 233 | sessionChannel = acceptChannel(msg); 234 | writeCipher(MSG_CHANNEL_OPEN_CONFIRMATION(sessionChannel.remoteId, 235 | sessionChannel.localId, sessionChannel.windowS, maxPacketSize)); 236 | } else if (msg.channelType == 'direct-tcpip' && directTcpRequest != null) { 237 | MSG_CHANNEL_OPEN_TCPIP tcpip = MSG_CHANNEL_OPEN_TCPIP() 238 | ..deserialize(packetS); 239 | Channel tcpipChannel = acceptChannel(msg); 240 | directTcpRequest(tcpipChannel, tcpip.srcHost, tcpip.srcPort, 241 | tcpip.dstHost, tcpip.dstPort) 242 | .then((String error) { 243 | if (error == null) { 244 | writeCipher(MSG_CHANNEL_OPEN_CONFIRMATION(tcpipChannel.remoteId, 245 | tcpipChannel.localId, tcpipChannel.windowS, maxPacketSize)); 246 | } else { 247 | writeCipher(MSG_CHANNEL_OPEN_FAILURE(msg.senderChannel, 0, '', '')); 248 | } 249 | }); 250 | } else { 251 | if (print != null) { 252 | print('unknown channel open ${msg.channelType}'); 253 | } 254 | writeCipher(MSG_CHANNEL_OPEN_FAILURE(msg.senderChannel, 0, '', '')); 255 | } 256 | } 257 | 258 | void handleMSG_CHANNEL_REQUEST(MSG_CHANNEL_REQUEST msg) { 259 | if (tracePrint != null) { 260 | tracePrint( 261 | '$hostport: MSG_CHANNEL_REQUEST ${msg.requestType} wantReply=${msg.wantReply}'); 262 | } 263 | Channel chan = channels[msg.recipientChannel]; 264 | if (chan == sessionChannel && 265 | sessionChannelRequest != null && 266 | sessionChannelRequest(this, msg.requestType)) { 267 | if (msg.wantReply) { 268 | writeCipher(MSG_CHANNEL_SUCCESS(chan.remoteId)); 269 | } 270 | } else { 271 | if (msg.wantReply) { 272 | writeCipher(MSG_CHANNEL_FAILURE(chan != null ? chan.remoteId : 0)); 273 | } 274 | } 275 | } 276 | 277 | @override 278 | void handleChannelOpenConfirmation(Channel channel) { 279 | if (channel.connected != null) { 280 | channel.connected(); 281 | } 282 | } 283 | 284 | @override 285 | void handleChannelData(Channel channel, Uint8List data) { 286 | if (channel == sessionChannel) { 287 | response(this, utf8.decode(data)); 288 | } else if (channel.cb != null) { 289 | channel.cb(channel, data); 290 | } 291 | } 292 | 293 | @override 294 | void handleChannelClose(Channel channel, [String description]) { 295 | if (channel == sessionChannel) { 296 | sessionChannel = null; 297 | } else if (channel.cb != null) { 298 | channel.opened = false; 299 | channel.cb(channel, Uint8List(0)); 300 | } 301 | } 302 | 303 | @override 304 | void sendChannelData(Uint8List b) { 305 | if (sessionChannel != null) { 306 | sendToChannel(sessionChannel, b); 307 | } 308 | } 309 | 310 | Channel openAgentChannel(ChannelCallback cb, 311 | {VoidCallback connected, StringCallback error}) { 312 | if (debugPrint != null) debugPrint('openAgentChannel'); 313 | if (socket == null || state <= SSHTransportState.FIRST_NEWKEYS) return null; 314 | Channel chan = channels[nextChannelId] = Channel( 315 | localId: nextChannelId++, 316 | windowS: initialWindowSize, 317 | cb: cb, 318 | error: error, 319 | connected: connected) 320 | ..agentChannel = true; 321 | nextChannelId++; 322 | writeCipher(MSG_CHANNEL_OPEN( 323 | 'auth-agent@openssh.com', chan.localId, chan.windowS, maxPacketSize)); 324 | return chan; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /lib/socket.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:collection'; 5 | import 'dart:typed_data'; 6 | 7 | import 'package:dartssh/socket_html.dart' 8 | if (dart.library.io) 'package:dartssh/socket_io.dart'; 9 | import 'package:dartssh/transport.dart'; 10 | 11 | enum ConnectionDirection { receive, send, both } 12 | 13 | /// Interface for connections, e.g. Socket or WebSocket. 14 | abstract class ConnectionInterface { 15 | /// Invokes [messageHandler] upon reading input from the connection. 16 | void listen(Uint8ListCallback messageHandler); 17 | 18 | /// Invokes [errorHandler] if a connection error occurs. 19 | void handleError(StringCallback errorHandler); 20 | 21 | /// Involes [handleDone] if the connection is closed normally. 22 | void handleDone(StringCallback doneHandler); 23 | 24 | /// Closed the connection. 25 | void close(); 26 | } 27 | 28 | /// Websocket style interface for BSD sockets and/or RFC6455 WebSockets. 29 | abstract class SocketInterface extends ConnectionInterface { 30 | // True if this socket is connected. 31 | bool get connected; 32 | 33 | // True if this socket is connecting. 34 | bool get connecting; 35 | 36 | /// Connects the socket to [uri] then invokes [onConnected] or [onError]. 37 | void connect(Uri uri, VoidCallback onConnected, StringCallback onError, 38 | {int timeoutSeconds = 15, bool ignoreBadCert = false}); 39 | 40 | /// Sends [text] over the socket. 41 | void send(String text); 42 | 43 | /// Sends [raw] over the socket. 44 | void sendRaw(Uint8List raw); 45 | 46 | void shutdown(ConnectionDirection direction) {} 47 | } 48 | 49 | /// Mixin for testing with shim [ConnectionInterface]s. 50 | mixin TestConnection { 51 | bool connected = false, connecting = false, closed = false; 52 | Uint8ListCallback messageHandler; 53 | StringCallback errorHandler, doneHandler; 54 | Queue sent = Queue(); 55 | 56 | void close() => closed = true; 57 | void handleError(StringCallback errorHandler) => 58 | this.errorHandler = errorHandler; 59 | void handleDone(StringCallback doneHandler) => this.doneHandler = doneHandler; 60 | void listen(Uint8ListCallback messageHandler) => 61 | this.messageHandler = messageHandler; 62 | } 63 | 64 | /// Shim [Socket] for testing 65 | class TestSocket extends SocketInterface with TestConnection { 66 | void connect(Uri address, VoidCallback onConnected, StringCallback onError, 67 | {int timeoutSeconds = 15, bool ignoreBadCert = false}) { 68 | connected = true; 69 | closed = false; 70 | onConnected(); 71 | } 72 | 73 | void send(String text) => sent.add(text); 74 | void sendRaw(Uint8List raw) => sent.add(String.fromCharCodes(raw)); 75 | } 76 | -------------------------------------------------------------------------------- /lib/socket_html.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'package:dartssh/socket.dart'; 5 | 6 | /// No socket implementation is possible in browser. 7 | class SocketImpl extends TestSocket {} 8 | -------------------------------------------------------------------------------- /lib/socket_io.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:convert'; 6 | import 'dart:io'; 7 | import 'dart:typed_data'; 8 | 9 | import 'package:dartssh/socket.dart'; 10 | import 'package:dartssh/transport.dart'; 11 | 12 | /// dart:io [Socket] based implementation of [SocketInterface]. 13 | class SocketImpl extends SocketInterface { 14 | Socket socket; 15 | StreamSubscription messageSubscription; 16 | Uint8ListCallback messageHandler; 17 | StringCallback onError, onDone; 18 | 19 | @override 20 | bool get connected => socket != null; 21 | 22 | @override 23 | bool connecting = false; 24 | 25 | SocketImpl([this.socket]); 26 | 27 | @override 28 | void close() { 29 | connecting = false; 30 | messageHandler = null; 31 | onError = onDone = null; 32 | if (messageSubscription != null) { 33 | messageSubscription.cancel(); 34 | messageSubscription = null; 35 | } 36 | if (socket != null) { 37 | socket.close(); 38 | socket = null; 39 | } 40 | } 41 | 42 | @override 43 | void connect(Uri uri, VoidCallback onConnected, StringCallback onError, 44 | {int timeoutSeconds = 15, bool ignoreBadCert = false}) { 45 | assert(!connecting); 46 | connecting = true; 47 | if (socket != null) { 48 | if (socket is SocketAdaptor) { 49 | (socket as SocketAdaptor).impl.connect( 50 | uri, () => connectSucceeded(onConnected), onError, 51 | timeoutSeconds: timeoutSeconds, ignoreBadCert: ignoreBadCert); 52 | } else { 53 | throw FormatException(); 54 | } 55 | } else { 56 | Socket.connect(uri.host, uri.port, 57 | timeout: Duration(seconds: timeoutSeconds)) 58 | .then((Socket x) { 59 | if (x == null) { 60 | onError(null); 61 | } else { 62 | socket = x; 63 | connectSucceeded(onConnected); 64 | } 65 | }); 66 | } 67 | } 68 | 69 | void connectSucceeded(VoidCallback onConnected) { 70 | connecting = false; 71 | onConnected(); 72 | } 73 | 74 | @override 75 | void handleError(StringCallback errorHandler) => onError = errorHandler; 76 | 77 | @override 78 | void handleDone(StringCallback doneHandler) => onDone = doneHandler; 79 | 80 | @override 81 | void listen(Uint8ListCallback newMessageHandler) { 82 | messageHandler = newMessageHandler; 83 | if (messageSubscription == null) { 84 | messageSubscription = socket.listen((Uint8List m) { 85 | if (messageHandler != null) { 86 | messageHandler(m); 87 | } 88 | }, onDone: () { 89 | if (onDone != null) { 90 | onDone(null); 91 | } 92 | }, onError: (error, stacktrace) { 93 | if (onError != null) { 94 | onError('$error: $stacktrace'); 95 | } 96 | }); 97 | } 98 | } 99 | 100 | @override 101 | void send(String text) => sendRaw(utf8.encode(text)); 102 | 103 | @override 104 | void sendRaw(Uint8List raw) => socket.add(raw); 105 | } 106 | 107 | /// https://github.com/dart-lang/sdk/blob/master/sdk/lib/_internal/vm/bin/socket_patch.dart#L1651 108 | class SocketAdaptor extends Stream implements Socket { 109 | SocketInterface impl; 110 | StreamController controller; 111 | SocketAdaptorStreamConsumer consumer; 112 | IOSink sink; 113 | StringCallback debugPrint; 114 | // var _detachReady; 115 | 116 | @override 117 | InternetAddress address; 118 | 119 | @override 120 | InternetAddress remoteAddress; 121 | 122 | @override 123 | int port; 124 | 125 | @override 126 | int remotePort; 127 | 128 | @override 129 | Encoding get encoding => sink.encoding; 130 | 131 | @override 132 | set encoding(Encoding value) => sink.encoding = value; 133 | 134 | SocketAdaptor(this.impl, 135 | {this.address, 136 | this.remoteAddress, 137 | this.port, 138 | this.remotePort, 139 | this.debugPrint}) { 140 | controller = StreamController(sync: true); 141 | consumer = SocketAdaptorStreamConsumer(this); 142 | sink = IOSink(consumer); 143 | 144 | /// https://github.com/dart-lang/sdk/issues/39589 145 | impl.listen((Uint8List m) => controller.add(Uint8List.fromList(m))); 146 | impl.handleError((error) => controller.addError(error)); 147 | impl.handleDone((String reason) => controller.addError(reason)); 148 | } 149 | 150 | @override 151 | void destroy() { 152 | consumer.stop(); 153 | impl.close(); 154 | controller.close(); 155 | } 156 | 157 | @override 158 | void add(List bytes) => sink.add(bytes); 159 | 160 | @override 161 | void write(Object obj) => sink.write(obj); 162 | 163 | @override 164 | void writeAll(Iterable objects, [String separator = ""]) => 165 | sink.writeAll(objects, separator); 166 | 167 | @override 168 | void writeln([Object obj = ""]) => sink.writeln(obj); 169 | 170 | @override 171 | void writeCharCode(int charCode) => sink.writeCharCode(charCode); 172 | 173 | @override 174 | void addError(error, [StackTrace stackTrace]) { 175 | throw UnsupportedError("Cannot send errors on sockets"); 176 | } 177 | 178 | @override 179 | Future addStream(Stream> stream) => sink.addStream(stream); 180 | 181 | @override 182 | Future flush() => sink.flush(); 183 | 184 | @override 185 | Future close() => sink.close(); 186 | 187 | @override 188 | Future get done => sink.done; 189 | 190 | @override 191 | bool setOption(SocketOption option, bool enabled) => false; 192 | 193 | @override 194 | Uint8List getRawOption(RawSocketOption option) => null; 195 | 196 | @override 197 | void setRawOption(RawSocketOption option) {} 198 | 199 | @override 200 | StreamSubscription listen(void onData(Uint8List event), 201 | {Function onError, void onDone(), bool cancelOnError}) { 202 | //debugPrint('DEBUG SocketAdaptor.listen $remoteAddress:$remotePort'); 203 | return controller.stream.listen((m) { 204 | //debugPrint('DEBUG SocketAdaptor.read $m'); 205 | onData(m); 206 | }, onError: onError, onDone: onDone, cancelOnError: cancelOnError); 207 | } 208 | 209 | /*void _consumerDone() { 210 | if (_detachReady != null) { 211 | _detachReady.complete(null); 212 | } else { 213 | if (impl != null) { 214 | impl.shutdown(ConnectionDirection.send); 215 | } 216 | } 217 | }*/ 218 | 219 | /*Future _detachRaw() { 220 | _detachReady = new Completer(); 221 | sink.close(); 222 | return _detachReady.future.then((_) { 223 | var raw = impl; 224 | impl = null; 225 | return [ 226 | RawSocketAdaptor(raw, 227 | address: address, 228 | remoteAddress: remoteAddress, 229 | port: port, 230 | remotePort: remotePort, 231 | debugPrint: debugPrint), 232 | null 233 | ]; 234 | }); 235 | }*/ 236 | } 237 | 238 | /// Copied from https://github.com/dart-lang/sdk/blob/master/sdk/lib/_internal/vm/bin/socket_patch.dart 239 | class SocketAdaptorStreamConsumer extends StreamConsumer> { 240 | final SocketAdaptor socket; 241 | StreamSubscription subscription; 242 | Completer streamCompleter; 243 | SocketAdaptorStreamConsumer(this.socket); 244 | 245 | Future close() { 246 | //socket._consumerDone(); 247 | return Future.value(socket); 248 | } 249 | 250 | void stop() { 251 | if (subscription == null) return; 252 | subscription.cancel(); 253 | subscription = null; 254 | } 255 | 256 | void done([error, stackTrace]) { 257 | if (streamCompleter != null) { 258 | if (error != null) { 259 | streamCompleter.completeError(error, stackTrace); 260 | } else { 261 | streamCompleter.complete(socket); 262 | } 263 | streamCompleter = null; 264 | } 265 | } 266 | 267 | Future addStream(Stream> stream) { 268 | streamCompleter = Completer(); 269 | if (socket.impl != null) { 270 | subscription = stream.listen((data) { 271 | try { 272 | if (subscription != null) { 273 | assert(data != null); 274 | socket.impl.sendRaw(data); 275 | } 276 | } catch (e) { 277 | socket.destroy(); 278 | stop(); 279 | done(e); 280 | } 281 | }, onError: (error, [stackTrace]) { 282 | socket.destroy(); 283 | done(error, stackTrace); 284 | }, onDone: () { 285 | done(); 286 | }, cancelOnError: true); 287 | } 288 | return streamCompleter.future; 289 | } 290 | } 291 | 292 | /// https://github.com/dart-lang/sdk/issues/39690 293 | /* 294 | /// https://github.com/dart-lang/sdk/blob/master/sdk/lib/_internal/vm/bin/socket_patch.dart#L1651 295 | class RawSocketAdaptor extends Stream implements RawSocket { 296 | final SocketInterface socket; 297 | StreamController controller; 298 | QueueBuffer readBuffer = QueueBuffer(Uint8List(0)); 299 | StringCallback debugPrint; 300 | bool _readEventsEnabled = true; 301 | bool _writeEventsEnabled = true; 302 | bool _paused = false; 303 | 304 | @override 305 | InternetAddress address; 306 | 307 | @override 308 | InternetAddress remoteAddress; 309 | 310 | @override 311 | int port; 312 | 313 | @override 314 | int remotePort; 315 | 316 | RawSocketAdaptor(this.socket, 317 | {this.address, 318 | this.remoteAddress, 319 | this.port, 320 | this.remotePort, 321 | this.debugPrint}) { 322 | controller = StreamController( 323 | sync: true, 324 | onListen: _onSubscriptionStateChange, 325 | onCancel: _onSubscriptionStateChange, 326 | onPause: _onPauseStateChange, 327 | onResume: _onPauseStateChange); 328 | 329 | socket.listen((Uint8List m) { 330 | readBuffer.add(m); 331 | if (!_paused && _readEventsEnabled) { 332 | controller.add(RawSocketEvent.read); 333 | } 334 | }); 335 | 336 | socket.handleDone((String reason) { 337 | controller.add(RawSocketEvent.readClosed); 338 | //controller.add(RawSocketEvent.closed); 339 | //controller.close(); 340 | }); 341 | 342 | socket.handleError((error) => controller.addError(error)); 343 | } 344 | 345 | @override 346 | StreamSubscription listen(void onData(RawSocketEvent event), 347 | {Function onError, void onDone(), bool cancelOnError}) { 348 | return controller.stream.listen(onData, 349 | onError: onError, onDone: onDone, cancelOnError: cancelOnError); 350 | } 351 | 352 | @override 353 | int available() => readBuffer.data.length; 354 | 355 | @override 356 | Uint8List read([int len]) { 357 | int readSize = len != null ? min(len, available()) : available(); 358 | if (readSize == null) return null; 359 | Uint8List data = readBuffer.data.sublist(0, readSize); 360 | readBuffer.flush(readSize); 361 | return data; 362 | } 363 | 364 | @override 365 | int write(List buffer, [int offset, int count]) { 366 | socket.sendRaw(Uint8List.fromList((offset == null && count == null) 367 | ? buffer 368 | : buffer.sublist(offset, offset + count))); 369 | return count ?? buffer.length; 370 | } 371 | 372 | @override 373 | Future close() { 374 | socket.close(); 375 | return Future.value(this); 376 | } 377 | 378 | @override 379 | void shutdown(SocketDirection direction) {} 380 | 381 | @override 382 | bool get readEventsEnabled => _readEventsEnabled; 383 | 384 | @override 385 | set readEventsEnabled(bool value) { 386 | if (value != _readEventsEnabled) { 387 | _readEventsEnabled = value; 388 | if (!controller.isPaused) _resume(); 389 | } 390 | } 391 | 392 | @override 393 | bool get writeEventsEnabled => _writeEventsEnabled; 394 | 395 | @override 396 | set writeEventsEnabled(bool value) { 397 | if (value != _writeEventsEnabled) { 398 | _writeEventsEnabled = value; 399 | if (!controller.isPaused) _resume(); 400 | } 401 | } 402 | 403 | @override 404 | bool setOption(SocketOption option, bool enabled) => false; 405 | 406 | @override 407 | Uint8List getRawOption(RawSocketOption option) => null; 408 | 409 | @override 410 | void setRawOption(RawSocketOption option) {} 411 | 412 | void _onPauseStateChange() { 413 | if (controller.isPaused) { 414 | _pause(); 415 | } else { 416 | _resume(); 417 | } 418 | } 419 | 420 | void _onSubscriptionStateChange() { 421 | if (controller.hasListener) { 422 | _resume(); 423 | } else { 424 | socket.close(); 425 | } 426 | } 427 | 428 | void _pause() => _paused = true; 429 | void _resume() => _paused = false; 430 | } 431 | */ 432 | 433 | InternetAddress tryParseInternetAddress(String x) { 434 | try { 435 | return InternetAddress(x); 436 | } catch (error) { 437 | return null; 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /lib/ssh.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:typed_data'; 5 | 6 | import 'package:pointycastle/api.dart' hide Signature; 7 | import 'package:pointycastle/block/aes_fast.dart'; 8 | import 'package:pointycastle/block/modes/cbc.dart'; 9 | import 'package:pointycastle/block/modes/ctr.dart'; 10 | import 'package:pointycastle/digests/md5.dart'; 11 | import 'package:pointycastle/digests/sha1.dart'; 12 | import 'package:pointycastle/digests/sha256.dart'; 13 | import 'package:pointycastle/digests/sha384.dart'; 14 | import 'package:pointycastle/digests/sha512.dart'; 15 | import 'package:pointycastle/ecc/api.dart'; 16 | import 'package:pointycastle/ecc/curves/secp256r1.dart'; 17 | import 'package:pointycastle/ecc/curves/secp384r1.dart'; 18 | import 'package:pointycastle/ecc/curves/secp521r1.dart'; 19 | import 'package:pointycastle/macs/hmac.dart'; 20 | import 'package:pointycastle/src/utils.dart'; 21 | import 'package:pointycastle/stream/ctr.dart'; 22 | 23 | import 'package:dartssh/identity.dart'; 24 | import 'package:dartssh/kex.dart'; 25 | import 'package:dartssh/protocol.dart'; 26 | import 'package:dartssh/serializable.dart'; 27 | 28 | typedef NameFunction = String Function(int); 29 | typedef SupportedFunction = bool Function(int); 30 | 31 | /// Valid URLs include 127.0.0.1, 127.0.0.1:22, wss://webssh. 32 | Uri parseUri(String uriText) { 33 | Uri uri; 34 | try { 35 | uri = Uri.parse(uriText); 36 | } catch (_) { 37 | uri = Uri.parse('ssh://$uriText'); 38 | } 39 | if (!uri.hasScheme) uri = uri = Uri.parse('ssh://$uriText'); 40 | if (uri.scheme == 'ssh' && !uri.hasPort) uri = Uri.parse('$uri:22'); 41 | return uri; 42 | } 43 | 44 | /// Each of the algorithm name-lists MUST be a comma-separated list of algorithm names. 45 | /// Each supported (allowed) algorithm MUST be listed in order of preference, from most to least. 46 | /// https://tools.ietf.org/html/rfc4253#section-7.1 47 | String buildPreferenceCsv( 48 | NameFunction name, SupportedFunction supported, int end, 49 | [int startAfter = 0]) { 50 | String ret = ''; 51 | for (int i = 1 + startAfter; i <= end; i++) { 52 | if (supported(i)) ret += (ret.isEmpty ? '' : ',') + name(i); 53 | } 54 | return ret; 55 | } 56 | 57 | /// Choose the first algorithm that satisfies the conditions. 58 | String preferenceIntersection(String intersectCsv, String supportedCsv, 59 | [bool server = false]) { 60 | if (server) { 61 | String swapCsv = intersectCsv; 62 | intersectCsv = supportedCsv; 63 | supportedCsv = swapCsv; 64 | } 65 | Set supported = Set.of(supportedCsv.split(',')); 66 | for (String intersect in intersectCsv.split(',')) { 67 | if (supported.contains(intersect)) return intersect; 68 | } 69 | return ''; 70 | } 71 | 72 | /// Limits cipher suite support to the specified parameter, if not null. 73 | void applyCipherSuiteOverrides( 74 | String kex, String key, String cipher, String mac) { 75 | if (kex != null) { 76 | final int kexOverride = KEX.id(kex); 77 | if (kexOverride == 0) { 78 | throw FormatException( 79 | 'unknown kex: $kex, supported: ${KEX.preferenceCsv()}'); 80 | } 81 | KEX.supported = (int id) => id == kexOverride; 82 | } 83 | if (key != null) { 84 | final int keyOverride = Key.id(key); 85 | if (keyOverride == 0) { 86 | throw FormatException( 87 | 'unknown key: $key, supported: ${Key.preferenceCsv()}'); 88 | } 89 | Key.supported = (int id) => id == keyOverride; 90 | } 91 | if (cipher != null) { 92 | final int cipherOverride = Cipher.id(cipher); 93 | if (cipherOverride == 0) { 94 | throw FormatException( 95 | 'unknown cipher: $cipher, supported: ${Cipher.preferenceCsv()}'); 96 | } 97 | Cipher.supported = (int id) => id == cipherOverride; 98 | } 99 | if (mac != null) { 100 | final int macOverride = MAC.id(mac); 101 | if (macOverride == 0) { 102 | throw FormatException( 103 | 'unknown mac: $mac, supported: ${MAC.preferenceCsv()}'); 104 | } 105 | MAC.supported = (int id) => id == macOverride; 106 | } 107 | } 108 | 109 | /// This protocol has been designed to operate with almost any public key 110 | /// format, encoding, and algorithm (signature and/or encryption). 111 | class Key { 112 | static const int ED25519 = 1, 113 | ECDSA_SHA2_NISTP256 = 2, 114 | ECDSA_SHA2_NISTP384 = 3, 115 | ECDSA_SHA2_NISTP521 = 4, 116 | RSA = 5, 117 | End = 5; 118 | 119 | static int id(String name) { 120 | if (name == null) return 0; 121 | switch (name) { 122 | case 'ssh-ed25519': 123 | return ED25519; 124 | case 'ecdsa-sha2-nistp256': 125 | return ECDSA_SHA2_NISTP256; 126 | case 'ecdsa-sha2-nistp384': 127 | return ECDSA_SHA2_NISTP384; 128 | case 'ecdsa-sha2-nistp521': 129 | return ECDSA_SHA2_NISTP521; 130 | case 'ssh-rsa': 131 | return RSA; 132 | default: 133 | return 0; 134 | } 135 | } 136 | 137 | static String name(int id) { 138 | switch (id) { 139 | case ED25519: 140 | return 'ssh-ed25519'; 141 | case ECDSA_SHA2_NISTP256: 142 | return 'ecdsa-sha2-nistp256'; 143 | case ECDSA_SHA2_NISTP384: 144 | return 'ecdsa-sha2-nistp384'; 145 | case ECDSA_SHA2_NISTP521: 146 | return 'ecdsa-sha2-nistp521'; 147 | case RSA: 148 | return 'ssh-rsa'; 149 | default: 150 | return ''; 151 | } 152 | } 153 | 154 | static SupportedFunction supported = (int id) => true; 155 | 156 | static bool ellipticCurveDSA(int id) => 157 | id == ECDSA_SHA2_NISTP256 || 158 | id == ECDSA_SHA2_NISTP384 || 159 | id == ECDSA_SHA2_NISTP521; 160 | 161 | static ECDomainParameters ellipticCurve(int id) { 162 | switch (id) { 163 | case ECDSA_SHA2_NISTP256: 164 | return ECCurve_secp256r1(); 165 | case ECDSA_SHA2_NISTP384: 166 | return ECCurve_secp384r1(); 167 | case ECDSA_SHA2_NISTP521: 168 | return ECCurve_secp521r1(); 169 | default: 170 | return null; 171 | } 172 | } 173 | 174 | static String ellipticCurveName(int id) { 175 | switch (id) { 176 | case ECDSA_SHA2_NISTP256: 177 | return 'nistp256'; 178 | case ECDSA_SHA2_NISTP384: 179 | return 'nistp384'; 180 | case ECDSA_SHA2_NISTP521: 181 | return 'nistp521'; 182 | default: 183 | return null; 184 | } 185 | } 186 | 187 | static int ellipticCurveSecretBits(int id) { 188 | switch (id) { 189 | case ECDSA_SHA2_NISTP256: 190 | return 256; 191 | case ECDSA_SHA2_NISTP384: 192 | return 384; 193 | case ECDSA_SHA2_NISTP521: 194 | return 521; 195 | default: 196 | return null; 197 | } 198 | } 199 | 200 | static Digest ellipticCurveHash(int id) { 201 | switch (id) { 202 | case ECDSA_SHA2_NISTP256: 203 | return SHA256Digest(); 204 | case ECDSA_SHA2_NISTP384: 205 | return SHA384Digest(); 206 | case ECDSA_SHA2_NISTP521: 207 | return SHA512Digest(); 208 | default: 209 | return null; 210 | } 211 | } 212 | 213 | static String preferenceCsv([int startAfter = 0]) => 214 | buildPreferenceCsv(name, supported, End, startAfter); 215 | 216 | static int preferenceIntersect(String intersectCsv, 217 | [bool server = false, int startAfter = 0]) => 218 | id(preferenceIntersection( 219 | preferenceCsv(startAfter), intersectCsv, server)); 220 | } 221 | 222 | /// The key exchange method specifies how one-time session keys are generated for 223 | /// encryption and for authentication, and how the server authentication is done. 224 | class KEX { 225 | static const int ECDH_SHA2_X25519 = 1, 226 | ECDH_SHA2_NISTP256 = 2, 227 | ECDH_SHA2_NISTP384 = 3, 228 | ECDH_SHA2_NISTP521 = 4, 229 | DHGEX_SHA256 = 5, 230 | DHGEX_SHA1 = 6, 231 | DH14_SHA1 = 7, 232 | DH1_SHA1 = 8, 233 | End = 8; 234 | 235 | static int id(String name) { 236 | if (name == null) return 0; 237 | switch (name) { 238 | case 'curve25519-sha256@libssh.org': 239 | return ECDH_SHA2_X25519; 240 | case 'ecdh-sha2-nistp256': 241 | return ECDH_SHA2_NISTP256; 242 | case 'ecdh-sha2-nistp384': 243 | return ECDH_SHA2_NISTP384; 244 | case 'ecdh-sha2-nistp521': 245 | return ECDH_SHA2_NISTP521; 246 | case 'diffie-hellman-group-exchange-sha256': 247 | return DHGEX_SHA256; 248 | case 'diffie-hellman-group-exchange-sha1': 249 | return DHGEX_SHA1; 250 | case 'diffie-hellman-group14-sha1': 251 | return DH14_SHA1; 252 | case 'diffie-hellman-group1-sha1': 253 | return DH1_SHA1; 254 | default: 255 | return 0; 256 | } 257 | } 258 | 259 | static String name(int id) { 260 | switch (id) { 261 | case ECDH_SHA2_X25519: 262 | return 'curve25519-sha256@libssh.org'; 263 | case ECDH_SHA2_NISTP256: 264 | return 'ecdh-sha2-nistp256'; 265 | case ECDH_SHA2_NISTP384: 266 | return 'ecdh-sha2-nistp384'; 267 | case ECDH_SHA2_NISTP521: 268 | return 'ecdh-sha2-nistp521'; 269 | case DHGEX_SHA256: 270 | return 'diffie-hellman-group-exchange-sha256'; 271 | case DHGEX_SHA1: 272 | return 'diffie-hellman-group-exchange-sha1'; 273 | case DH14_SHA1: 274 | return 'diffie-hellman-group14-sha1'; 275 | case DH1_SHA1: 276 | return 'diffie-hellman-group1-sha1'; 277 | default: 278 | return ''; 279 | } 280 | } 281 | 282 | static SupportedFunction supported = (int id) => true; 283 | 284 | static bool x25519DiffieHellman(int id) => id == ECDH_SHA2_X25519; 285 | 286 | static bool ellipticCurveDiffieHellman(int id) => 287 | id == ECDH_SHA2_NISTP256 || 288 | id == ECDH_SHA2_NISTP384 || 289 | id == ECDH_SHA2_NISTP521; 290 | 291 | static ECDomainParameters ellipticCurve(int id) { 292 | switch (id) { 293 | case ECDH_SHA2_NISTP256: 294 | return ECCurve_secp256r1(); 295 | case ECDH_SHA2_NISTP384: 296 | return ECCurve_secp384r1(); 297 | case ECDH_SHA2_NISTP521: 298 | return ECCurve_secp521r1(); 299 | default: 300 | return null; 301 | } 302 | } 303 | 304 | static int ellipticCurveSecretBits(int id) { 305 | switch (id) { 306 | case ECDH_SHA2_NISTP256: 307 | return 256; 308 | case ECDH_SHA2_NISTP384: 309 | return 384; 310 | case ECDH_SHA2_NISTP521: 311 | return 521; 312 | default: 313 | return null; 314 | } 315 | } 316 | 317 | static Digest ellipticCurveHash(int id) { 318 | switch (id) { 319 | case ECDH_SHA2_NISTP256: 320 | return SHA256Digest(); 321 | case ECDH_SHA2_NISTP384: 322 | return SHA384Digest(); 323 | case ECDH_SHA2_NISTP521: 324 | return SHA512Digest(); 325 | default: 326 | return null; 327 | } 328 | } 329 | 330 | static bool diffieHellmanGroupExchange(int id) => 331 | id == DHGEX_SHA256 || id == DHGEX_SHA1; 332 | 333 | static bool diffieHellman(int id) => 334 | id == DHGEX_SHA256 || 335 | id == DHGEX_SHA1 || 336 | id == DH14_SHA1 || 337 | id == DH1_SHA1; 338 | 339 | static String preferenceCsv([int startAfter = 0]) => 340 | buildPreferenceCsv(name, supported, End, startAfter); 341 | 342 | static int preferenceIntersect(String intersectCsv, 343 | [bool server = false, int startAfter = 0]) => 344 | id(preferenceIntersection( 345 | preferenceCsv(startAfter), intersectCsv, server)); 346 | } 347 | 348 | // When encryption is in effect, the packet length, padding length, payload, 349 | // and padding fields of each packet MUST be encrypted with the given algorithm. 350 | class Cipher { 351 | static const int AES128_CTR = 1, 352 | AES128_CBC = 2, 353 | AES256_CTR = 3, 354 | AES256_CBC = 4, 355 | End = 4; 356 | 357 | static int id(String name) { 358 | if (name == null) return 0; 359 | switch (name) { 360 | case 'aes128-ctr': 361 | return AES128_CTR; 362 | case 'aes128-cbc': 363 | return AES128_CBC; 364 | case 'aes256-ctr': 365 | return AES256_CTR; 366 | case 'aes256-cbc': 367 | return AES256_CBC; 368 | default: 369 | return 0; 370 | } 371 | } 372 | 373 | static String name(int id) { 374 | switch (id) { 375 | case AES128_CTR: 376 | return 'aes128-ctr'; 377 | case AES128_CBC: 378 | return 'aes128-cbc'; 379 | case AES256_CTR: 380 | return 'aes256-ctr'; 381 | case AES256_CBC: 382 | return 'aes256-cbc'; 383 | default: 384 | return ''; 385 | } 386 | } 387 | 388 | static SupportedFunction supported = (int id) => true; 389 | 390 | static String preferenceCsv([int startAfter = 0]) => 391 | buildPreferenceCsv(name, supported, End, startAfter); 392 | 393 | static int preferenceIntersect(String intersectCsv, 394 | [bool server = false, int startAfter = 0]) => 395 | id(preferenceIntersection( 396 | preferenceCsv(startAfter), intersectCsv, server)); 397 | 398 | static int keySize(int id) { 399 | switch (id) { 400 | case AES128_CTR: 401 | return 16; 402 | case AES128_CBC: 403 | return 16; 404 | case AES256_CTR: 405 | return 32; 406 | case AES256_CBC: 407 | return 32; 408 | default: 409 | throw FormatException('$id'); 410 | } 411 | } 412 | 413 | static int blockSize(int id) { 414 | switch (id) { 415 | case AES128_CTR: 416 | return 16; 417 | case AES128_CBC: 418 | return 16; 419 | case AES256_CTR: 420 | return 16; 421 | case AES256_CBC: 422 | return 16; 423 | default: 424 | throw FormatException('$id'); 425 | } 426 | } 427 | 428 | static BlockCipher cipher(int id) { 429 | switch (id) { 430 | case AES128_CTR: 431 | AESFastEngine aes = AESFastEngine(); 432 | return CTRBlockCipher(aes.blockSize, CTRStreamCipher(aes)); 433 | case AES128_CBC: 434 | return CBCBlockCipher(AESFastEngine()); 435 | case AES256_CTR: 436 | AESFastEngine aes = AESFastEngine(); 437 | return CTRBlockCipher(aes.blockSize, CTRStreamCipher(aes)); 438 | case AES256_CBC: 439 | return CBCBlockCipher(AESFastEngine()); 440 | default: 441 | throw FormatException('$id'); 442 | } 443 | } 444 | } 445 | 446 | /// Data integrity is protected by including with each packet a MAC that is computed 447 | /// from a shared secret, packet sequence number, and the contents of the packet. 448 | class MAC { 449 | static const int MD5 = 1, 450 | SHA1 = 2, 451 | SHA1_96 = 3, 452 | MD5_96 = 4, 453 | SHA256 = 5, 454 | SHA256_96 = 6, 455 | SHA512 = 7, 456 | SHA512_96 = 8, 457 | End = 8; 458 | 459 | static int id(String name) { 460 | if (name == null) return 0; 461 | switch (name) { 462 | case 'hmac-md5': 463 | return MD5; 464 | case 'hmac-md5-96': 465 | return MD5_96; 466 | case 'hmac-sha1': 467 | return SHA1; 468 | case 'hmac-sha1-96': 469 | return SHA1_96; 470 | case 'hmac-sha2-256': 471 | return SHA256; 472 | case 'hmac-sha2-256-96': 473 | return SHA256_96; 474 | case 'hmac-sha2-512': 475 | return SHA512; 476 | case 'hmac-sha2-512-96': 477 | return SHA512_96; 478 | default: 479 | return 0; 480 | } 481 | } 482 | 483 | static String name(int id) { 484 | switch (id) { 485 | case MD5: 486 | return 'hmac-md5'; 487 | case MD5_96: 488 | return 'hmac-md5-96'; 489 | case SHA1: 490 | return 'hmac-sha1'; 491 | case SHA1_96: 492 | return 'hmac-sha1-96'; 493 | case SHA256: 494 | return 'hmac-sha2-256'; 495 | case SHA256_96: 496 | return 'hmac-sha2-256-96'; 497 | case SHA512: 498 | return 'hmac-sha2-512'; 499 | case SHA512_96: 500 | return 'hmac-sha2-512-96'; 501 | default: 502 | return ''; 503 | } 504 | } 505 | 506 | static int hashSize(int id) { 507 | switch (id) { 508 | case MD5: 509 | return 16; 510 | case MD5_96: 511 | return 16; 512 | case SHA1: 513 | return 20; 514 | case SHA1_96: 515 | return 20; 516 | case SHA256: 517 | return 32; 518 | case SHA256_96: 519 | return 32; 520 | case SHA512: 521 | return 64; 522 | case SHA512_96: 523 | return 64; 524 | default: 525 | return 0; 526 | } 527 | } 528 | 529 | static SupportedFunction supported = (int id) => true; 530 | 531 | static String preferenceCsv([int startAfter = 0]) => 532 | buildPreferenceCsv(name, supported, End, startAfter); 533 | 534 | static int preferenceIntersect(String intersectCsv, 535 | [bool server = false, int startAfter = 0]) => 536 | id(preferenceIntersection( 537 | preferenceCsv(startAfter), intersectCsv, server)); 538 | 539 | static int prefixBytes(int id) { 540 | switch (id) { 541 | case MD5: 542 | return 0; 543 | case MD5_96: 544 | return 12; 545 | case SHA1: 546 | return 0; 547 | case SHA1_96: 548 | return 12; 549 | case SHA256: 550 | return 0; 551 | case SHA256_96: 552 | return 12; 553 | case SHA512: 554 | return 0; 555 | case SHA512_96: 556 | return 12; 557 | default: 558 | throw FormatException('$id'); 559 | } 560 | } 561 | 562 | static HMac mac(int id) { 563 | switch (id) { 564 | case MD5: 565 | return HMac(MD5Digest(), 64); 566 | case MD5_96: 567 | return HMac(MD5Digest(), 64); 568 | case SHA1: 569 | return HMac(SHA1Digest(), 64); 570 | case SHA1_96: 571 | return HMac(SHA1Digest(), 64); 572 | case SHA256: 573 | return HMac(SHA256Digest(), 64); 574 | case SHA256_96: 575 | return HMac(SHA256Digest(), 64); 576 | case SHA512: 577 | return HMac(SHA512Digest(), 128); 578 | case SHA512_96: 579 | return HMac(SHA512Digest(), 128); 580 | default: 581 | throw FormatException('$id'); 582 | } 583 | } 584 | } 585 | 586 | /// If compression has been negotiated, the 'payload' field (and only it) 587 | /// will be compressed using the negotiated algorithm. 588 | class Compression { 589 | static const int OpenSSHZLib = 1, None = 2, End = 2; 590 | 591 | static int id(String name) { 592 | if (name == null) return 0; 593 | switch (name) { 594 | case 'zlib@openssh.com': 595 | return OpenSSHZLib; 596 | case 'none': 597 | return None; 598 | default: 599 | return 0; 600 | } 601 | } 602 | 603 | static String name(int id) { 604 | switch (id) { 605 | case OpenSSHZLib: 606 | return 'zlib@openssh.com'; 607 | case None: 608 | return 'none'; 609 | default: 610 | return ''; 611 | } 612 | } 613 | 614 | static SupportedFunction supported = (int id) => true; 615 | 616 | static String preferenceCsv([int startAfter = 0]) => 617 | buildPreferenceCsv(name, supported, End, startAfter); 618 | 619 | static int preferenceIntersect(String intersectCsv, 620 | [bool server = false, int startAfter = 0]) => 621 | id(preferenceIntersection( 622 | preferenceCsv(startAfter), intersectCsv, server)); 623 | } 624 | 625 | /// Hashes SSH protocol data without first serializing it. 626 | class Digester { 627 | Digest digest; 628 | Digester(this.digest) { 629 | digest.reset(); 630 | } 631 | 632 | void updateByte(int x) => digest.updateByte(x); 633 | 634 | void updateString(String x) => update(Uint8List.fromList(x.codeUnits)); 635 | 636 | void update(Uint8List x) => updateOffset(x, 0, x.length); 637 | 638 | void updateRaw(Uint8List x) => updateRawOffset(x, 0, x.length); 639 | 640 | void updateOffset(Uint8List x, int offset, int length) { 641 | updateInt(length); 642 | updateRawOffset(x, offset, length); 643 | } 644 | 645 | void updateRawOffset(Uint8List x, int offset, int length) => 646 | digest.update(x, offset, length); 647 | 648 | void updateInt(int x) { 649 | Uint8List buf = Uint8List(4); 650 | ByteData.view(buf.buffer).setUint32(0, x, Endian.big); 651 | digest.update(buf, 0, buf.length); 652 | } 653 | 654 | void updateBigInt(BigInt x) { 655 | Uint8List xBytes = encodeBigInt(x); 656 | bool padX = x.bitLength > 0 && x.bitLength % 8 == 0; 657 | updateInt(xBytes.length + (padX ? 1 : 0)); 658 | if (padX) digest.updateByte(0); 659 | digest.update(xBytes, 0, xBytes.length); 660 | } 661 | 662 | Uint8List finish() { 663 | Uint8List ret = Uint8List(digest.digestSize); 664 | int finalLength = digest.doFinal(ret, 0); 665 | if (finalLength != ret.length) throw FormatException(); 666 | return ret; 667 | } 668 | } 669 | 670 | /// The exchange hash is used to authenticate the key exchange and SHOULD be kept secret. 671 | Uint8List computeExchangeHash( 672 | bool server, 673 | int kexMethod, 674 | Digest algo, 675 | String verC, 676 | String verS, 677 | Uint8List kexInitC, 678 | Uint8List kexInitS, 679 | Uint8List kS, 680 | BigInt K, 681 | DiffieHellman dh, 682 | EllipticCurveDiffieHellman ecdh, 683 | X25519DiffieHellman x25519dh) { 684 | BinaryPacket kexCPacket = BinaryPacket(kexInitC), 685 | kexSPacket = BinaryPacket(kexInitS); 686 | int kexCPacketLen = 4 + kexCPacket.length, 687 | kexSPacketLen = 4 + kexSPacket.length; 688 | 689 | Digester H = Digester(algo); 690 | if (server) H.updateString(verS); 691 | H.updateString(verC); 692 | if (!server) H.updateString(verS); 693 | H.updateOffset(kexInitC, 5, kexCPacketLen - 5 - kexCPacket.padding); 694 | H.updateOffset(kexInitS, 5, kexSPacketLen - 5 - kexSPacket.padding); 695 | H.update(kS); 696 | 697 | if (KEX.diffieHellmanGroupExchange(kexMethod)) { 698 | H.updateInt(dh.gexMin); 699 | H.updateInt(dh.gexPref); 700 | H.updateInt(dh.gexMax); 701 | H.updateBigInt(dh.p); 702 | H.updateBigInt(dh.g); 703 | } 704 | if (KEX.x25519DiffieHellman(kexMethod)) { 705 | if (server) H.update(x25519dh.remotePubKey); 706 | H.update(x25519dh.myPubKey); 707 | if (!server) H.update(x25519dh.remotePubKey); 708 | } else if (KEX.ellipticCurveDiffieHellman(kexMethod)) { 709 | if (server) H.update(ecdh.sText); 710 | H.update(ecdh.cText); 711 | if (!server) H.update(ecdh.sText); 712 | } else { 713 | if (server) H.updateBigInt(dh.f); 714 | H.updateBigInt(dh.e); 715 | if (!server) H.updateBigInt(dh.f); 716 | } 717 | H.updateBigInt(K); 718 | return H.finish(); 719 | } 720 | 721 | /// Verifies that [key] signed [exH] producing [sig]. 722 | bool verifyHostKey( 723 | Uint8List exH, int hostkeyType, Uint8List key, Uint8List sig) { 724 | if (hostkeyType == Key.RSA) { 725 | return verifyRSASignature(RSAKey()..deserialize(SerializableInput(key)), 726 | RSASignature()..deserialize(SerializableInput(sig)), exH); 727 | } else if (Key.ellipticCurveDSA(hostkeyType)) { 728 | return verifyECDSASignature( 729 | hostkeyType, 730 | ECDSAKey()..deserialize(SerializableInput(key)), 731 | ECDSASignature()..deserialize(SerializableInput(sig)), 732 | exH); 733 | } else if (hostkeyType == Key.ED25519) { 734 | return verifyEd25519Signature( 735 | Ed25519Key()..deserialize(SerializableInput(key)), 736 | Ed25519Signature()..deserialize(SerializableInput(sig)), 737 | exH); 738 | } else { 739 | return false; 740 | } 741 | } 742 | 743 | /// https://tools.ietf.org/html/rfc4253#section-7.2 744 | Uint8List deriveKey(Digest algo, Uint8List sessionId, Uint8List exH, BigInt K, 745 | int id, int bytes) { 746 | Uint8List ret = Uint8List(0); 747 | while (ret.length < bytes) { 748 | Digester digest = Digester(algo); 749 | digest.updateBigInt(K); 750 | digest.updateRaw(exH); 751 | if (ret.isEmpty) { 752 | digest.updateByte(id); 753 | digest.updateRaw(sessionId); 754 | } else { 755 | digest.updateRaw(ret); 756 | } 757 | ret = Uint8List.fromList(ret + digest.finish()); 758 | } 759 | return viewUint8List(ret, 0, bytes); 760 | } 761 | 762 | /// https://tools.ietf.org/html/rfc4252#section-7 763 | Uint8List deriveChallenge(Uint8List sessionId, String userName, 764 | String serviceName, String methodName, String algoName, Uint8List secret) { 765 | SerializableOutput output = SerializableOutput(Uint8List(2 + 766 | 4 * 6 + 767 | sessionId.length + 768 | userName.length + 769 | serviceName.length + 770 | methodName.length + 771 | algoName.length + 772 | secret.length)); 773 | serializeString(output, sessionId); 774 | output.addUint8(MSG_USERAUTH_REQUEST.ID); 775 | serializeString(output, userName); 776 | serializeString(output, serviceName); 777 | serializeString(output, methodName); 778 | output.addUint8(1); 779 | serializeString(output, algoName); 780 | serializeString(output, secret); 781 | if (!output.done) { 782 | throw FormatException('${output.offset}/${output.buffer.length}'); 783 | } 784 | return output.buffer; 785 | } 786 | 787 | /// Transforms [m] by [cipher] provided [m.length] is a multiple of [cipher.blockSize]. 788 | Uint8List applyBlockCipher(BlockCipher cipher, Uint8List m) { 789 | Uint8List out = Uint8List(m.length); 790 | if (m.length % cipher.blockSize != 0) { 791 | throw FormatException('${m.length} not multiple of ${cipher.blockSize}'); 792 | } 793 | for (int offset = 0; offset < m.length; offset += cipher.blockSize) { 794 | cipher.processBlock(m, offset, out, offset); 795 | } 796 | return out; 797 | } 798 | 799 | /// Signs [seq] | [m] with [k] using [mac]. 800 | Uint8List computeMAC( 801 | HMac mac, int macLen, Uint8List m, int seq, Uint8List k, int prefix) { 802 | mac.init(KeyParameter(k)); 803 | 804 | Uint8List buf = Uint8List(4); 805 | ByteData.view(buf.buffer).setUint32(0, seq, Endian.big); 806 | mac.update(buf, 0, buf.length); 807 | mac.update(m, 0, m.length); 808 | 809 | if (macLen != mac.macSize) throw FormatException(); 810 | Uint8List ret = Uint8List(macLen); 811 | int finalLen = mac.doFinal(ret, 0); 812 | if (finalLen != macLen) throw FormatException(); 813 | 814 | if (prefix != 0) { 815 | return viewUint8List(ret, 0, prefix); 816 | } else { 817 | return ret; 818 | } 819 | } 820 | -------------------------------------------------------------------------------- /lib/websocket_html.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:html' as html; 6 | import 'dart:typed_data'; 7 | 8 | import 'package:dartssh/socket.dart'; 9 | import 'package:dartssh/transport.dart'; 10 | 11 | /// dart:html [WebSocket] based implementation of [SocketInterface]. 12 | class WebSocketImpl extends SocketInterface { 13 | static const String type = 'html'; 14 | 15 | html.WebSocket socket; 16 | Uint8ListCallback messageHandler; 17 | StringCallback errorHandler, doneHandler; 18 | VoidCallback connectCallback; 19 | StreamSubscription connectErrorSubscription, 20 | messageSubscription, 21 | errorSubscription, 22 | doneSubscription; 23 | 24 | @override 25 | bool get connected => socket != null && !connecting; 26 | 27 | @override 28 | bool get connecting => connectErrorSubscription != null; 29 | 30 | @override 31 | void close() { 32 | messageHandler = null; 33 | errorHandler = null; 34 | doneHandler = null; 35 | if (errorSubscription != null) { 36 | errorSubscription.cancel(); 37 | errorSubscription = null; 38 | } 39 | if (doneSubscription != null) { 40 | doneSubscription.cancel(); 41 | doneSubscription = null; 42 | } 43 | if (messageSubscription != null) { 44 | messageSubscription.cancel(); 45 | messageSubscription = null; 46 | } 47 | if (socket != null) { 48 | socket.close(); 49 | socket == null; 50 | } 51 | } 52 | 53 | @override 54 | void connect(Uri uri, VoidCallback onConnected, StringCallback onError, 55 | {int timeoutSeconds = 15, bool ignoreBadCert = false}) { 56 | assert(!connecting); 57 | 58 | /// No way to allow self-signed certificates. 59 | assert(!ignoreBadCert); 60 | try { 61 | connectCallback = onConnected; 62 | socket = html.WebSocket('$uri'); 63 | socket.onOpen.listen(connectSucceeded); 64 | connectErrorSubscription = 65 | socket.onError.listen((error) => onError('$error')); 66 | } catch (error) { 67 | onError('$error'); 68 | } 69 | } 70 | 71 | void connectSucceeded(dynamic x) { 72 | connectErrorSubscription.cancel(); 73 | connectErrorSubscription = null; 74 | connectCallback(); 75 | } 76 | 77 | @override 78 | void handleError(StringCallback newErrorHandler) => 79 | errorHandler = newErrorHandler; 80 | 81 | @override 82 | void handleDone(StringCallback newDoneHandler) => 83 | doneHandler = newDoneHandler; 84 | 85 | @override 86 | void listen(Uint8ListCallback newMessageHandler) { 87 | messageHandler = newMessageHandler; 88 | 89 | if (errorSubscription == null) { 90 | errorSubscription = socket.onError.listen((error) { 91 | if (errorHandler != null) { 92 | errorHandler('$error'); 93 | } 94 | }); 95 | } 96 | 97 | if (doneSubscription == null) { 98 | doneSubscription = socket.onClose.listen((closeEvent) { 99 | if (doneHandler != null) { 100 | doneHandler('$closeEvent'); 101 | } 102 | }); 103 | } 104 | 105 | if (messageSubscription == null) { 106 | messageSubscription = socket.onMessage.listen((e) { 107 | if (messageHandler != null) { 108 | messageHandler(e.data); 109 | } 110 | }); 111 | } 112 | } 113 | 114 | @override 115 | void send(String text) => socket.sendString(text); 116 | 117 | @override 118 | void sendRaw(Uint8List raw) => socket.send(raw); 119 | } 120 | -------------------------------------------------------------------------------- /lib/websocket_io.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:convert'; 6 | import 'dart:io' as io; 7 | import 'dart:math'; 8 | import 'dart:typed_data'; 9 | 10 | import 'package:dartssh/client.dart'; 11 | import 'package:dartssh/http.dart'; 12 | import 'package:dartssh/protocol.dart'; 13 | import 'package:dartssh/socket.dart'; 14 | import 'package:dartssh/socket_io.dart'; 15 | import 'package:dartssh/transport.dart'; 16 | 17 | /// dart:io [WebSocket] based implementation of [SocketInterface]. 18 | class WebSocketImpl extends SocketInterface { 19 | static const String type = 'io'; 20 | 21 | io.WebSocket socket; 22 | StreamSubscription messageSubscription; 23 | Uint8ListCallback messageHandler; 24 | StringCallback errorHandler, doneHandler; 25 | 26 | @override 27 | bool get connected => socket != null; 28 | 29 | @override 30 | bool connecting = false; 31 | 32 | @override 33 | void close() { 34 | connecting = false; 35 | messageHandler = null; 36 | errorHandler = null; 37 | doneHandler = null; 38 | if (socket != null) { 39 | socket.close(); 40 | socket == null; 41 | } 42 | } 43 | 44 | @override 45 | void connect(Uri uri, VoidCallback onConnected, StringCallback onError, 46 | {int timeoutSeconds = 15, bool ignoreBadCert = false}) async { 47 | assert(!connecting); 48 | connecting = true; 49 | 50 | if (!ignoreBadCert || !uri.hasScheme || uri.scheme != 'wss') { 51 | return io.WebSocket.connect('$uri') 52 | .timeout(Duration(seconds: timeoutSeconds)) 53 | .then((io.WebSocket x) { 54 | socket = x; 55 | connectSucceeded(onConnected); 56 | }, onError: (error, _) => onError(error)); 57 | } 58 | 59 | io.HttpClient client = io.HttpClient(); 60 | client.badCertificateCallback = 61 | (io.X509Certificate cert, String host, int port) => true; 62 | 63 | /// Upgrade https to wss using [badCertificateCallback] to allow 64 | /// self-signed certificates. This still gains you stream encryption. 65 | try { 66 | io.HttpClientRequest request = 67 | await client.getUrl(Uri.parse('https' + '$uri'.substring(3))); 68 | request.headers.add('Connection', 'upgrade'); 69 | request.headers.add('Upgrade', 'websocket'); 70 | request.headers.add('sec-websocket-version', '13'); 71 | request.headers.add( 72 | 'sec-websocket-key', base64.encode(randBytes(Random.secure(), 8))); 73 | 74 | io.HttpClientResponse response = await request.close() 75 | ..timeout(Duration(seconds: timeoutSeconds)); 76 | 77 | socket = io.WebSocket.fromUpgradedSocket(await response.detachSocket(), 78 | serverSide: false); 79 | connectSucceeded(onConnected); 80 | } catch (error) { 81 | onError(error); 82 | } 83 | } 84 | 85 | void connectSucceeded(VoidCallback onConnected) { 86 | connecting = false; 87 | onConnected(); 88 | } 89 | 90 | @override 91 | void handleError(StringCallback newErrorHandler) => 92 | errorHandler = newErrorHandler; 93 | 94 | @override 95 | void handleDone(StringCallback newDoneHandler) => 96 | doneHandler = newDoneHandler; 97 | 98 | @override 99 | void listen(Uint8ListCallback newMessageHandler) { 100 | messageHandler = newMessageHandler; 101 | 102 | if (messageSubscription == null) { 103 | messageSubscription = socket.listen((m) { 104 | //print("WebSocketImpl.read: $m"); 105 | if (messageHandler != null) { 106 | messageHandler(utf8.encode(m)); 107 | } 108 | }); 109 | 110 | socket.done.then((_) { 111 | if (doneHandler != null) { 112 | doneHandler( 113 | 'WebSocketImpl.handleDone: ${socket.closeCode} ${socket.closeReason}'); 114 | } 115 | return null; 116 | }); 117 | 118 | socket.handleError((error, _) { 119 | if (errorHandler != null) { 120 | errorHandler(error); 121 | } 122 | }); 123 | } 124 | } 125 | 126 | @override 127 | void send(String text) => socket.addUtf8Text(utf8.encode(text)); 128 | 129 | @override 130 | void sendRaw(Uint8List raw) => socket.add(raw); 131 | } 132 | 133 | /// The initial [SSHTunneledSocketImpl] (which implements same [SocketInteface] 134 | /// as [SSHTunneledWebSocketImpl]), is bridged via [SSHTunneledSocket] adaptor 135 | /// to initialize [io.WebSocket.fromUpgradedSocket()]. 136 | class SSHTunneledWebSocketImpl extends WebSocketImpl { 137 | SocketInterface tunneledSocket; 138 | final String sourceHost, tunnelToHost; 139 | final int sourcePort, tunnelToPort; 140 | final StringCallback debugPrint; 141 | 142 | SSHTunneledWebSocketImpl(SSHTunneledSocketImpl inputSocket) 143 | : tunneledSocket = inputSocket, 144 | sourceHost = inputSocket.sourceHost, 145 | tunnelToHost = inputSocket.tunnelToHost, 146 | sourcePort = inputSocket.sourcePort, 147 | tunnelToPort = inputSocket.tunnelToPort, 148 | debugPrint = inputSocket.client.debugPrint; 149 | 150 | @override 151 | void connect(Uri uri, VoidCallback onConnected, StringCallback onError, 152 | {int timeoutSeconds = 15, bool ignoreBadCert = false}) async { 153 | uri = '$uri'.startsWith('wss') 154 | ? Uri.parse('https' + '$uri'.substring(3)) 155 | : Uri.parse('http' + '$uri'.substring(2)); 156 | 157 | if (!tunneledSocket.connected && !tunneledSocket.connecting) { 158 | tunneledSocket = await connectUri(uri, tunneledSocket, 159 | secureUpgrade: (SocketInterface x) async => 160 | SocketImpl(await io.SecureSocket.secure( 161 | SocketAdaptor(x), 162 | 163 | /// https://github.com/dart-lang/sdk/issues/39690 164 | /*io.Socket.fromRaw(RawSocketAdaptor( 165 | x, 166 | address: tryParseInternetAddress('127.0.0.1'), 167 | remoteAddress: (await io.InternetAddress.lookup(uri.host)).first, 168 | port: 1234, 169 | remotePort: uri.port, 170 | debugPrint: debugPrint, 171 | )),*/ 172 | onBadCertificate: (io.X509Certificate certificate) => 173 | ignoreBadCert, 174 | ))); 175 | } 176 | 177 | HttpResponse response = await httpRequest( 178 | uri, 179 | 'GET', 180 | tunneledSocket, 181 | requestHeaders: { 182 | 'Connection': 'upgrade', 183 | 'Upgrade': 'websocket', 184 | 'sec-websocket-version': '13', 185 | 'sec-websocket-key': base64.encode(randBytes(Random.secure(), 8)) 186 | }, 187 | debugPrint: debugPrint, 188 | ); 189 | if (response.status == 101) { 190 | socket = io.WebSocket.fromUpgradedSocket( 191 | SocketAdaptor( 192 | tunneledSocket, 193 | address: tryParseInternetAddress('127.0.0.1'), 194 | remoteAddress: (await io.InternetAddress.lookup(uri.host)).first, 195 | port: 1234, 196 | remotePort: uri.port, 197 | debugPrint: debugPrint, 198 | ), 199 | 200 | /// https://github.com/dart-lang/sdk/issues/39690 201 | /*io.Socket.socketFromRaw(RawSocketAdaptor( 202 | tunneledSocket, 203 | address: tryParseInternetAddress('127.0.0.1'), 204 | remoteAddress: (await io.InternetAddress.lookup(uri.host)).first, 205 | port: 1234, 206 | remotePort: uri.port, 207 | debugPrint: debugPrint, 208 | )),*/ 209 | serverSide: false); 210 | onConnected(); 211 | } else { 212 | onError('status ${response.status} ${response.reason}'); 213 | } 214 | tunneledSocket = null; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /lib/zlib.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | /* 5 | import 'dart:typed_data'; 6 | 7 | import 'package:archive/src/zlib/deflate.dart'; 8 | import 'package:archive/src/zlib/inflate.dart'; 9 | 10 | class ArchiveInflateReader { 11 | Inflate zreader = Inflate.stream(); 12 | Uint8List convert(Uint8List input) { 13 | zreader.streamInput(input); 14 | return Uint8List.fromList(zreader.inflateNext()); 15 | } 16 | } 17 | 18 | class ArchiveDeflateWriter { 19 | Deflate zwriter = Deflate.buffer(null); 20 | Uint8List convert(Uint8List input) { 21 | zwriter.addBytes(input, flush: Deflate.PARTIAL_FLUSH); 22 | return zwriter.takeBytes(); 23 | } 24 | }*/ 25 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | analyzer: 5 | dependency: transitive 6 | description: 7 | name: analyzer 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "0.38.5" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.5.2" 18 | asn1lib: 19 | dependency: "direct main" 20 | description: 21 | name: asn1lib 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "0.5.11" 25 | async: 26 | dependency: transitive 27 | description: 28 | name: async 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.4.0" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.0.5" 39 | build: 40 | dependency: transitive 41 | description: 42 | name: build 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.2.0" 46 | build_config: 47 | dependency: transitive 48 | description: 49 | name: build_config 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.4.1+1" 53 | build_daemon: 54 | dependency: transitive 55 | description: 56 | name: build_daemon 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.1.0" 60 | build_resolvers: 61 | dependency: transitive 62 | description: 63 | name: build_resolvers 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.2.1" 67 | build_runner: 68 | dependency: "direct dev" 69 | description: 70 | name: build_runner 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.7.1" 74 | build_runner_core: 75 | dependency: transitive 76 | description: 77 | name: build_runner_core 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "4.1.0" 81 | built_collection: 82 | dependency: transitive 83 | description: 84 | name: built_collection 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "4.2.2" 88 | built_value: 89 | dependency: transitive 90 | description: 91 | name: built_value 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "6.7.1" 95 | charcode: 96 | dependency: transitive 97 | description: 98 | name: charcode 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "1.1.2" 102 | checked_yaml: 103 | dependency: transitive 104 | description: 105 | name: checked_yaml 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "1.0.2" 109 | code_builder: 110 | dependency: transitive 111 | description: 112 | name: code_builder 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "3.2.0" 116 | collection: 117 | dependency: transitive 118 | description: 119 | name: collection 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "1.14.12" 123 | convert: 124 | dependency: "direct main" 125 | description: 126 | name: convert 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "2.1.1" 130 | coverage: 131 | dependency: transitive 132 | description: 133 | name: coverage 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "0.13.3" 137 | crypto: 138 | dependency: transitive 139 | description: 140 | name: crypto 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "2.1.3" 144 | csslib: 145 | dependency: transitive 146 | description: 147 | name: csslib 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "0.16.1" 151 | dart_style: 152 | dependency: transitive 153 | description: 154 | name: dart_style 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "1.3.2" 158 | fixnum: 159 | dependency: transitive 160 | description: 161 | name: fixnum 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "0.10.9" 165 | front_end: 166 | dependency: transitive 167 | description: 168 | name: front_end 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "0.1.27" 172 | glob: 173 | dependency: transitive 174 | description: 175 | name: glob 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "1.2.0" 179 | graphs: 180 | dependency: transitive 181 | description: 182 | name: graphs 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "0.2.0" 186 | html: 187 | dependency: transitive 188 | description: 189 | name: html 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "0.14.0+3" 193 | http: 194 | dependency: "direct main" 195 | description: 196 | name: http 197 | url: "https://pub.dartlang.org" 198 | source: hosted 199 | version: "0.12.0+2" 200 | http_multi_server: 201 | dependency: transitive 202 | description: 203 | name: http_multi_server 204 | url: "https://pub.dartlang.org" 205 | source: hosted 206 | version: "2.1.0" 207 | http_parser: 208 | dependency: transitive 209 | description: 210 | name: http_parser 211 | url: "https://pub.dartlang.org" 212 | source: hosted 213 | version: "3.1.3" 214 | io: 215 | dependency: transitive 216 | description: 217 | name: io 218 | url: "https://pub.dartlang.org" 219 | source: hosted 220 | version: "0.3.3" 221 | js: 222 | dependency: transitive 223 | description: 224 | name: js 225 | url: "https://pub.dartlang.org" 226 | source: hosted 227 | version: "0.6.1+1" 228 | json_annotation: 229 | dependency: transitive 230 | description: 231 | name: json_annotation 232 | url: "https://pub.dartlang.org" 233 | source: hosted 234 | version: "3.0.0" 235 | kernel: 236 | dependency: transitive 237 | description: 238 | name: kernel 239 | url: "https://pub.dartlang.org" 240 | source: hosted 241 | version: "0.3.27" 242 | logging: 243 | dependency: transitive 244 | description: 245 | name: logging 246 | url: "https://pub.dartlang.org" 247 | source: hosted 248 | version: "0.11.3+2" 249 | matcher: 250 | dependency: transitive 251 | description: 252 | name: matcher 253 | url: "https://pub.dartlang.org" 254 | source: hosted 255 | version: "0.12.5" 256 | meta: 257 | dependency: "direct main" 258 | description: 259 | name: meta 260 | url: "https://pub.dartlang.org" 261 | source: hosted 262 | version: "1.1.7" 263 | mime: 264 | dependency: transitive 265 | description: 266 | name: mime 267 | url: "https://pub.dartlang.org" 268 | source: hosted 269 | version: "0.9.6+3" 270 | multi_server_socket: 271 | dependency: transitive 272 | description: 273 | name: multi_server_socket 274 | url: "https://pub.dartlang.org" 275 | source: hosted 276 | version: "1.0.2" 277 | node_interop: 278 | dependency: transitive 279 | description: 280 | name: node_interop 281 | url: "https://pub.dartlang.org" 282 | source: hosted 283 | version: "1.0.3" 284 | node_io: 285 | dependency: transitive 286 | description: 287 | name: node_io 288 | url: "https://pub.dartlang.org" 289 | source: hosted 290 | version: "1.0.1+2" 291 | node_preamble: 292 | dependency: transitive 293 | description: 294 | name: node_preamble 295 | url: "https://pub.dartlang.org" 296 | source: hosted 297 | version: "1.4.8" 298 | package_config: 299 | dependency: transitive 300 | description: 301 | name: package_config 302 | url: "https://pub.dartlang.org" 303 | source: hosted 304 | version: "1.1.0" 305 | package_resolver: 306 | dependency: transitive 307 | description: 308 | name: package_resolver 309 | url: "https://pub.dartlang.org" 310 | source: hosted 311 | version: "1.0.10" 312 | path: 313 | dependency: transitive 314 | description: 315 | name: path 316 | url: "https://pub.dartlang.org" 317 | source: hosted 318 | version: "1.6.4" 319 | pedantic: 320 | dependency: "direct main" 321 | description: 322 | name: pedantic 323 | url: "https://pub.dartlang.org" 324 | source: hosted 325 | version: "1.8.0+1" 326 | pointycastle: 327 | dependency: "direct main" 328 | description: 329 | name: pointycastle 330 | url: "https://pub.dartlang.org" 331 | source: hosted 332 | version: "1.0.1" 333 | pool: 334 | dependency: transitive 335 | description: 336 | name: pool 337 | url: "https://pub.dartlang.org" 338 | source: hosted 339 | version: "1.4.0" 340 | pub_semver: 341 | dependency: transitive 342 | description: 343 | name: pub_semver 344 | url: "https://pub.dartlang.org" 345 | source: hosted 346 | version: "1.4.2" 347 | pubspec_parse: 348 | dependency: transitive 349 | description: 350 | name: pubspec_parse 351 | url: "https://pub.dartlang.org" 352 | source: hosted 353 | version: "0.1.5" 354 | quiver: 355 | dependency: transitive 356 | description: 357 | name: quiver 358 | url: "https://pub.dartlang.org" 359 | source: hosted 360 | version: "2.0.5" 361 | shelf: 362 | dependency: transitive 363 | description: 364 | name: shelf 365 | url: "https://pub.dartlang.org" 366 | source: hosted 367 | version: "0.7.5" 368 | shelf_packages_handler: 369 | dependency: transitive 370 | description: 371 | name: shelf_packages_handler 372 | url: "https://pub.dartlang.org" 373 | source: hosted 374 | version: "1.0.4" 375 | shelf_static: 376 | dependency: transitive 377 | description: 378 | name: shelf_static 379 | url: "https://pub.dartlang.org" 380 | source: hosted 381 | version: "0.2.8" 382 | shelf_web_socket: 383 | dependency: transitive 384 | description: 385 | name: shelf_web_socket 386 | url: "https://pub.dartlang.org" 387 | source: hosted 388 | version: "0.2.3" 389 | source_map_stack_trace: 390 | dependency: transitive 391 | description: 392 | name: source_map_stack_trace 393 | url: "https://pub.dartlang.org" 394 | source: hosted 395 | version: "1.1.5" 396 | source_maps: 397 | dependency: transitive 398 | description: 399 | name: source_maps 400 | url: "https://pub.dartlang.org" 401 | source: hosted 402 | version: "0.10.8" 403 | source_span: 404 | dependency: transitive 405 | description: 406 | name: source_span 407 | url: "https://pub.dartlang.org" 408 | source: hosted 409 | version: "1.5.5" 410 | stack_trace: 411 | dependency: transitive 412 | description: 413 | name: stack_trace 414 | url: "https://pub.dartlang.org" 415 | source: hosted 416 | version: "1.9.3" 417 | stream_channel: 418 | dependency: transitive 419 | description: 420 | name: stream_channel 421 | url: "https://pub.dartlang.org" 422 | source: hosted 423 | version: "2.0.0" 424 | stream_transform: 425 | dependency: transitive 426 | description: 427 | name: stream_transform 428 | url: "https://pub.dartlang.org" 429 | source: hosted 430 | version: "0.0.19" 431 | string_scanner: 432 | dependency: transitive 433 | description: 434 | name: string_scanner 435 | url: "https://pub.dartlang.org" 436 | source: hosted 437 | version: "1.0.5" 438 | term_glyph: 439 | dependency: transitive 440 | description: 441 | name: term_glyph 442 | url: "https://pub.dartlang.org" 443 | source: hosted 444 | version: "1.1.0" 445 | test: 446 | dependency: "direct dev" 447 | description: 448 | name: test 449 | url: "https://pub.dartlang.org" 450 | source: hosted 451 | version: "1.9.2" 452 | test_api: 453 | dependency: transitive 454 | description: 455 | name: test_api 456 | url: "https://pub.dartlang.org" 457 | source: hosted 458 | version: "0.2.9" 459 | test_core: 460 | dependency: transitive 461 | description: 462 | name: test_core 463 | url: "https://pub.dartlang.org" 464 | source: hosted 465 | version: "0.2.13" 466 | timing: 467 | dependency: transitive 468 | description: 469 | name: timing 470 | url: "https://pub.dartlang.org" 471 | source: hosted 472 | version: "0.1.1+2" 473 | tweetnacl: 474 | dependency: "direct main" 475 | description: 476 | path: "." 477 | ref: HEAD 478 | resolved-ref: "14c6386326ad46f7e6d3c62f4796c07403f918a9" 479 | url: "https://github.com/GreenAppers/tweetnacl-dart" 480 | source: git 481 | version: "0.3.2" 482 | typed_data: 483 | dependency: transitive 484 | description: 485 | name: typed_data 486 | url: "https://pub.dartlang.org" 487 | source: hosted 488 | version: "1.1.6" 489 | validators: 490 | dependency: "direct main" 491 | description: 492 | name: validators 493 | url: "https://pub.dartlang.org" 494 | source: hosted 495 | version: "2.0.0+1" 496 | vm_service: 497 | dependency: transitive 498 | description: 499 | name: vm_service 500 | url: "https://pub.dartlang.org" 501 | source: hosted 502 | version: "1.2.0" 503 | watcher: 504 | dependency: transitive 505 | description: 506 | name: watcher 507 | url: "https://pub.dartlang.org" 508 | source: hosted 509 | version: "0.9.7+12" 510 | web_socket_channel: 511 | dependency: transitive 512 | description: 513 | name: web_socket_channel 514 | url: "https://pub.dartlang.org" 515 | source: hosted 516 | version: "1.1.0" 517 | yaml: 518 | dependency: transitive 519 | description: 520 | name: yaml 521 | url: "https://pub.dartlang.org" 522 | source: hosted 523 | version: "2.2.0" 524 | sdks: 525 | dart: ">=2.3.0 <3.0.0" 526 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dartssh 2 | version: 1.0.4+4 3 | description: Dart SSH package providing First-class tunnelling primitives. 4 | author: Green Appers, Inc. 5 | homepage: https://github.com/GreenAppers/dartssh 6 | 7 | environment: 8 | sdk: ">=2.1.0 <3.0.0" 9 | 10 | dependencies: 11 | asn1lib: ^0.5.11 12 | convert: ^2.1.1 13 | meta: ^1.1.6 14 | http: ^0.12.0+2 15 | pedantic: ^1.8.0 16 | pointycastle: ^1.0.1 17 | tweetnacl: 18 | git: 19 | url: https://github.com/GreenAppers/tweetnacl-dart 20 | validators: ^2.0.0+1 21 | 22 | dev_dependencies: 23 | build_runner: ^1.4.0 24 | test: ^1.6.5 25 | -------------------------------------------------------------------------------- /test-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | OBS_PORT=9292 4 | 5 | dart --disable-service-auth-codes \ 6 | --enable-vm-service=$OBS_PORT \ 7 | --pause-isolates-on-exit \ 8 | test/dartssh_test.dart & 9 | 10 | pub global run coverage:collect_coverage \ 11 | --port=$OBS_PORT \ 12 | --out=coverage/coverage.json \ 13 | --wait-paused \ 14 | --resume-isolates 15 | 16 | pub global run coverage:format_coverage \ 17 | --lcov \ 18 | --in=coverage/coverage.json \ 19 | --out=coverage/lcov.info \ 20 | --packages=.packages \ 21 | --report-on=lib 22 | -------------------------------------------------------------------------------- /test/dartssh_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 dartssh developers 2 | // Use of this source code is governed by a MIT-style license that can be found in the LICENSE file. 3 | 4 | import 'dart:async'; 5 | import 'dart:convert'; 6 | import 'dart:io'; 7 | import 'dart:math'; 8 | import 'dart:typed_data'; 9 | 10 | import 'package:convert/convert.dart'; 11 | import 'package:test/test.dart'; 12 | 13 | import 'package:dartssh/client.dart'; 14 | import 'package:dartssh/http.dart'; 15 | import 'package:dartssh/identity.dart'; 16 | import 'package:dartssh/pem.dart'; 17 | import 'package:dartssh/protocol.dart'; 18 | import 'package:dartssh/serializable.dart'; 19 | import 'package:dartssh/socket.dart'; 20 | import 'package:dartssh/ssh.dart'; 21 | import 'package:dartssh/websocket_io.dart'; 22 | 23 | import '../example/dartssh.dart' as ssh; 24 | import '../example/dartsshd.dart' as sshd; 25 | 26 | void main() { 27 | /// https://www.ietf.org/rfc/rfc4251.txt 28 | test('mpint', () { 29 | Uint8List buffer = Uint8List(4096); 30 | SerializableInput input = SerializableInput(buffer, endian: Endian.big); 31 | SerializableOutput output = SerializableOutput(buffer, endian: Endian.big); 32 | 33 | BigInt x = BigInt.from(0); 34 | serializeMpInt(output, x); 35 | expect(hex.encode(output.view()), '00000000'); 36 | expect(deserializeMpInt(input), x); 37 | expect(input.offset, output.offset); 38 | 39 | x = (BigInt.from(0x9a378f9) << 32) | BigInt.from(0xb2e332a7); 40 | input.offset = output.offset = 0; 41 | serializeMpInt(output, x); 42 | expect(hex.encode(output.view()), '0000000809a378f9b2e332a7'); 43 | expect(deserializeMpInt(input), x); 44 | expect(input.offset, output.offset); 45 | 46 | x = BigInt.from(0x80); 47 | input.offset = output.offset = 0; 48 | serializeMpInt(output, x); 49 | expect(hex.encode(output.view()), '000000020080'); 50 | expect(deserializeMpInt(input), x); 51 | expect(input.offset, output.offset); 52 | }); 53 | 54 | test('pem', () { 55 | Identity rsa1 = parsePem(File('test/id_rsa').readAsStringSync()); 56 | Identity rsa2 = parsePem(File('test/id_rsa.openssh').readAsStringSync()); 57 | expect(rsa1.rsaPublic.exponent, rsa2.rsaPublic.exponent); 58 | }); 59 | 60 | test('TestSocket', () { 61 | TestSocket socket = TestSocket(); 62 | SSHClient ssh = SSHClient( 63 | socketInput: socket, 64 | print: print, 65 | debugPrint: print, 66 | tracePrint: print); 67 | socket.connect(Uri.parse('tcp://foobar:22'), ssh.onConnected, (_) {}); 68 | expect(socket.sent.removeFirst(), 'SSH-2.0-dartssh_1.0\r\n'); 69 | ssh.disconnect('done'); 70 | expect(socket.closed, true); 71 | }); 72 | 73 | test('TestHttpClient', () async { 74 | TestHttpClient httpClient = TestHttpClient(); 75 | Future httpTestResult = httpTest(httpClient); 76 | httpClient.requests.removeFirst().completer.complete( 77 | HttpResponse(200, text: 'support@greenappers.com')); 78 | expect(httpTestResult, completion(equals(true))); 79 | }); 80 | 81 | test('cipher suite', () async { 82 | int kexIndex = 1, keyIndex = 1, cipherIndex = 1, macIndex = 1; 83 | bool kexLooped = false, 84 | keyLooped = false, 85 | cipherLooped = false, 86 | macLooped = false; 87 | KEX.supported = (int id) => id == kexIndex; 88 | Key.supported = (int id) => id == keyIndex; 89 | Cipher.supported = (int id) => id == cipherIndex; 90 | MAC.supported = (int id) => id == macIndex; 91 | while (!kexLooped || !keyLooped || !cipherLooped || !macLooped) { 92 | print( 93 | '=== suite begin ${KEX.name(kexIndex)}, ${Key.name(keyIndex)}, ${Cipher.name(cipherIndex)}, ${MAC.name(macIndex)} ==='); 94 | 95 | StreamController> sshInput = StreamController>(); 96 | String sshResponse = '', identityFile; 97 | 98 | switch (keyIndex) { 99 | case Key.ED25519: 100 | identityFile = 'test/id_ed25519'; 101 | break; 102 | case Key.ECDSA_SHA2_NISTP256: 103 | identityFile = 'test/id_ecdsa'; 104 | break; 105 | case Key.ECDSA_SHA2_NISTP384: 106 | identityFile = 'test/ecdsa-sha2-nistp384/id_ecdsa'; 107 | break; 108 | case Key.ECDSA_SHA2_NISTP521: 109 | identityFile = 'test/ecdsa-sha2-nistp521/id_ecdsa'; 110 | break; 111 | case Key.RSA: 112 | identityFile = 'test/id_rsa'; 113 | break; 114 | default: 115 | throw FormatException('key $keyIndex'); 116 | } 117 | 118 | Future sshdMain = sshd.sshd([ 119 | '-p 42022', 120 | '-h', 121 | (keyIndex == Key.ECDSA_SHA2_NISTP384 || 122 | keyIndex == Key.ECDSA_SHA2_NISTP521) 123 | ? 'test/${Key.name(keyIndex)}/ssh_host_' 124 | : 'test/ssh_host_', 125 | '--debug', 126 | '--trace', 127 | ]); 128 | 129 | Future sshMain = ssh.ssh([ 130 | '-A', 131 | '-l', 132 | 'root', 133 | '127.0.0.1:42022', 134 | '-i', 135 | identityFile, 136 | '--debug', 137 | '--trace', 138 | ], sshInput.stream, (_, String v) => sshResponse += v, 139 | () => sshInput.close()); 140 | 141 | while (ssh.client.sessionChannel == null) { 142 | await Future.delayed(const Duration(seconds: 1)); 143 | } 144 | ssh.client.sendChannelData(utf8.encode('testAgent\nexit\n')); 145 | await sshMain; 146 | await sshdMain; 147 | expect(sshResponse, '\$ testAgent\nexit\nsuccess\n'); 148 | 149 | print( 150 | '=== suite end ${KEX.name(kexIndex)}, ${Key.name(keyIndex)}, ${Cipher.name(cipherIndex)}, ${MAC.name(macIndex)} ==='); 151 | kexIndex++; 152 | if (kexIndex > KEX.End) { 153 | kexLooped = true; 154 | kexIndex = 1; 155 | } 156 | keyIndex++; 157 | if (keyIndex > Key.End) { 158 | keyLooped = true; 159 | keyIndex = 1; 160 | } 161 | cipherIndex++; 162 | if (cipherIndex > Cipher.End) { 163 | cipherLooped = true; 164 | cipherIndex = 1; 165 | } 166 | macIndex++; 167 | if (macIndex > MAC.End) { 168 | macLooped = true; 169 | macIndex = 1; 170 | } 171 | } 172 | }); 173 | 174 | test('tunneled http test', () async { 175 | expect(httpTest(HttpClientImpl()), completion(equals(true))); 176 | String password = 'foobar123'; 177 | 178 | KEX.supported = 179 | Key.supported = Cipher.supported = MAC.supported = (_) => true; 180 | Future sshdMain = sshd.sshd([ 181 | '-p 42022', 182 | '-h', 183 | 'test/ssh_host_', 184 | '--debug', 185 | '--trace', 186 | '--forwardTcp', 187 | '--password', 188 | password 189 | ]); 190 | 191 | String sshResponse = ''; 192 | StreamController> sshInput = StreamController>(); 193 | Future sshMain = ssh.ssh([ 194 | '-l', 195 | 'root', 196 | '127.0.0.1:42022', 197 | '--password', 198 | password, 199 | '--debug', 200 | '--trace', 201 | ], sshInput.stream, (_, String v) => sshResponse += v, 202 | () => sshInput.close()); 203 | 204 | while (ssh.client.sessionChannel == null) { 205 | await Future.delayed(const Duration(seconds: 1)); 206 | } 207 | ssh.client.setTerminalWindowSize(80, 25); 208 | ssh.client.exec('ls'); 209 | 210 | bool tunneledHttpTest = await httpTest( 211 | HttpClientImpl(clientFactory: () => SSHTunneledBaseClient(ssh.client)), 212 | proto: 'http'); 213 | expect(tunneledHttpTest, true); 214 | 215 | ssh.client.sendChannelData(utf8.encode('debugTest\nexit\n')); 216 | await sshMain; 217 | await sshdMain; 218 | expect(sshResponse, 'Password:\r\n\$ debugTest\nexit\n'); 219 | }); 220 | 221 | test('tunneled websocket test', () async { 222 | expect(websocketEchoTest(WebSocketImpl(), proto: 'ws'), 223 | completion(equals(true))); 224 | 225 | expect(websocketEchoTest(WebSocketImpl(), ignoreBadCert: true), 226 | completion(equals(true))); 227 | 228 | KEX.supported = 229 | Key.supported = Cipher.supported = MAC.supported = (_) => true; 230 | Future sshdMain = sshd.sshd([ 231 | '-p 42022', 232 | '-h', 233 | 'test/ssh_host_', 234 | '--debug', 235 | '--trace', 236 | '--forwardTcp', 237 | ]); 238 | 239 | String sshResponse = ''; 240 | StreamController> sshInput = StreamController>(); 241 | Future sshMain = ssh.ssh([ 242 | '-l', 243 | 'root', 244 | '127.0.0.1:42022', 245 | '--debug', 246 | '--trace', 247 | ], sshInput.stream, (_, String v) => sshResponse += v, 248 | () => sshInput.close()); 249 | 250 | while (ssh.client.sessionChannel == null) { 251 | await Future.delayed(const Duration(seconds: 1)); 252 | } 253 | 254 | bool tunneledWebsocketTest = await websocketEchoTest( 255 | SSHTunneledWebSocketImpl(SSHTunneledSocketImpl.fromClient(ssh.client)), 256 | proto: 'ws'); 257 | expect(tunneledWebsocketTest, true); 258 | 259 | ssh.client.sendChannelData(utf8.encode('exit\n')); 260 | await sshMain; 261 | await sshdMain; 262 | expect(sshResponse, '\$ exit\n'); 263 | }); 264 | } 265 | 266 | Future httpTest(HttpClient httpClient, {String proto = 'https'}) async { 267 | var response = await httpClient.request('$proto://www.greenappers.com/'); 268 | return response != null && response.text.contains('support@greenappers.com'); 269 | } 270 | 271 | Future websocketEchoTest(WebSocketImpl websocket, 272 | {bool ignoreBadCert = false, String proto = 'wss'}) async { 273 | final Completer connectCompleter = Completer(); 274 | websocket.connect( 275 | Uri.parse('$proto://echo.websocket.org'), 276 | () => connectCompleter.complete(null), 277 | (String error) => connectCompleter.complete(error), 278 | ignoreBadCert: ignoreBadCert); 279 | final String error = await connectCompleter.future; 280 | if (error != null) return false; 281 | 282 | final Completer responseCompleter = Completer(); 283 | final String challenge = 284 | 'websocketEchoTest ${base64.encode(randBytes(Random.secure(), 16))}'; 285 | websocket.listen((Uint8List m) => responseCompleter.complete(utf8.decode(m))); 286 | websocket.handleError((String m) => responseCompleter.complete(m)); 287 | websocket.handleDone((String m) => responseCompleter.complete(m)); 288 | websocket.send(challenge); 289 | final String response = await responseCompleter.future; 290 | websocket.close(); 291 | return response == challenge; 292 | } 293 | -------------------------------------------------------------------------------- /test/ecdsa-sha2-nistp384/id_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRSnxUwiMy7mxlPapzJSLYiONwDGnz+ 4 | auFw1m5FQ8+5INYrhUZF3WPFRkoObFFHrRMWyfUCGdxAgnbftSGWRWPFli1Lf60ylQTI9i 5 | 0dfUaUIhP2flMR2gDWvDrh5Czn7/AAAADo9vJYo/byWKMAAAATZWNkc2Etc2hhMi1uaXN0 6 | cDM4NAAAAAhuaXN0cDM4NAAAAGEEUp8VMIjMu5sZT2qcyUi2IjjcAxp8/mrhcNZuRUPPuS 7 | DWK4VGRd1jxUZKDmxRR60TFsn1AhncQIJ237UhlkVjxZYtS3+tMpUEyPYtHX1GlCIT9n5T 8 | EdoA1rw64eQs5+/wAAAAMQDz4scmpkYxgdTcvgTgaWMSnDrXIWNWFEGfIZOJTxITvoAT6f 9 | UJig+lGSNXRGdFw6EAAAAZamZvdXR0c0BVU0FKRk9VVFRTTS5sb2NhbAECAwQFBg== 10 | -----END OPENSSH PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /test/ecdsa-sha2-nistp384/id_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBFKfFTCIzLubGU9qnMlItiI43AMafP5q4XDWbkVDz7kg1iuFRkXdY8VGSg5sUUetExbJ9QIZ3ECCdt+1IZZFY8WWLUt/rTKVBMj2LR19RpQiE/Z+UxHaANa8OuHkLOfv8A== jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/ecdsa-sha2-nistp384/ssh_host_ecdsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQT0F6vCdx5FhEP4/Ydl1AOwYwJgpGeu 4 | iDLxbqe11tAtauIFHMNEGPWOr8N0b+1iARQhIHO++iY8IBjQCGva3FT3jf14I+VS1bW1hN 5 | Xo7Fpq+4zuZjWzIQOf/gQ+0ArY7yYAAADowut4MMLreDAAAAATZWNkc2Etc2hhMi1uaXN0 6 | cDM4NAAAAAhuaXN0cDM4NAAAAGEE9BerwnceRYRD+P2HZdQDsGMCYKRnrogy8W6ntdbQLW 7 | riBRzDRBj1jq/DdG/tYgEUISBzvvomPCAY0Ahr2txU9439eCPlUtW1tYTV6OxaavuM7mY1 8 | syEDn/4EPtAK2O8mAAAAMQDcQgSdqB9LNIKBwvQKUIpp+VvXiWspnuxjcutKmhO0sbHR5j 9 | wq+yO8ZkBR8ZPyE8AAAAAZamZvdXR0c0BVU0FKRk9VVFRTTS5sb2NhbAECAwQFBg== 10 | -----END OPENSSH PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /test/ecdsa-sha2-nistp384/ssh_host_ecdsa_key.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBPQXq8J3HkWEQ/j9h2XUA7BjAmCkZ66IMvFup7XW0C1q4gUcw0QY9Y6vw3Rv7WIBFCEgc776JjwgGNAIa9rcVPeN/Xgj5VLVtbWE1ejsWmr7jO5mNbMhA5/+BD7QCtjvJg== jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/ecdsa-sha2-nistp521/id_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQAs81G8m/qvEcIv9vtU+5MLmnH7OSe 4 | jnXT+SQP1rVN07Cz/Q04gk2GaCf9xDCRoS+61sd5ebxRUBONJZsZPoVxxoMA81EcO3g0H2 5 | YmYCgQ4EAZgEuFhEXybt/MygZnXleDrsv+bFSBo09b5ANUXsZ4/fLVLk9CI4N3mTivkmyP 6 | K6f0F8cAAAEYD8pDVg/KQ1YAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ 7 | AAAIUEALPNRvJv6rxHCL/b7VPuTC5px+zkno510/kkD9a1TdOws/0NOIJNhmgn/cQwkaEv 8 | utbHeXm8UVATjSWbGT6FccaDAPNRHDt4NB9mJmAoEOBAGYBLhYRF8m7fzMoGZ15Xg67L/m 9 | xUgaNPW+QDVF7GeP3y1S5PQiODd5k4r5Jsjyun9BfHAAAAQgE/WWa/K2kJn8zctAwyBBPI 10 | cycvhKpRovCKnXLAxOv0tKGQki98S/FEswfF2pVbcMIysss7ujM/HIiZQExXZvzPOAAAAB 11 | lqZm91dHRzQFVTQUpGT1VUVFNNLmxvY2FsAQ== 12 | -----END OPENSSH PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /test/ecdsa-sha2-nistp521/id_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACzzUbyb+q8Rwi/2+1T7kwuacfs5J6OddP5JA/WtU3TsLP9DTiCTYZoJ/3EMJGhL7rWx3l5vFFQE40lmxk+hXHGgwDzURw7eDQfZiZgKBDgQBmAS4WERfJu38zKBmdeV4Ouy/5sVIGjT1vkA1Rexnj98tUuT0Ijg3eZOK+SbI8rp/QXxw== jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/ecdsa-sha2-nistp521/ssh_host_ecdsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBO/iOTkpxtAp+EHd/zEfEhou8B2nR 4 | SqAe37MX/LoJc4KsBIkUwKGPF0g3uMpI3KREsV7U/MeI0dxTuxRwCuY/h9cBv0y02GDkxo 5 | V0JVIH2xTuxvHyMZRa59NWTKTHpSCMquAGomjG6ohZWQxmWTdVRo73Qqm5FiQ9juGF/C/X 6 | 8G/F9OcAAAEYXiXViV4l1YkAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ 7 | AAAIUEATv4jk5KcbQKfhB3f8xHxIaLvAdp0UqgHt+zF/y6CXOCrASJFMChjxdIN7jKSNyk 8 | RLFe1PzHiNHcU7sUcArmP4fXAb9MtNhg5MaFdCVSB9sU7sbx8jGUWufTVkykx6UgjKrgBq 9 | JoxuqIWVkMZlk3VUaO90KpuRYkPY7hhfwv1/BvxfTnAAAAQgHLFn0sOZzvMDmSZQYK5LaB 10 | gK5dDqeiBhygJLciE/KPYFlrHAYPJpzduApUulnIbK6fPMbM4snm6eTwAtFDTDcxQwAAAB 11 | lqZm91dHRzQFVTQUpGT1VUVFNNLmxvY2FsAQ== 12 | -----END OPENSSH PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /test/ecdsa-sha2-nistp521/ssh_host_ecdsa_key.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAE7+I5OSnG0Cn4Qd3/MR8SGi7wHadFKoB7fsxf8uglzgqwEiRTAoY8XSDe4ykjcpESxXtT8x4jR3FO7FHAK5j+H1wG/TLTYYOTGhXQlUgfbFO7G8fIxlFrn01ZMpMelIIyq4AaiaMbqiFlZDGZZN1VGjvdCqbkWJD2O4YX8L9fwb8X05w== jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/id_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQSA8zUPX0QP2Gd20hm2u4XA1ZwTAVe 4 | zH/o9x8/RUji9BzzEL5k+apPwKHV6iXe7coRlun8ZeYqB0VjqodqgM5yAAAAuNM7tzfTO7 5 | c3AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBIDzNQ9fRA/YZ3b 6 | SGba7hcDVnBMBV7Mf+j3Hz9FSOL0HPMQvmT5qk/AodXqJd7tyhGW6fxl5ioHRWOqh2qAzn 7 | IAAAAgdgkPZYI74SpmM9mhEAlquDKlPh7lejy7eEXMshzBTYgAAAAZamZvdXR0c0BVU0FK 8 | Rk9VVFRTTS5sb2NhbAECAwQFBgc= 9 | -----END OPENSSH PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /test/id_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBIDzNQ9fRA/YZ3bSGba7hcDVnBMBV7Mf+j3Hz9FSOL0HPMQvmT5qk/AodXqJd7tyhGW6fxl5ioHRWOqh2qAznI= jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/id_ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACBZnnnYZjFQ7Zt0gMyJ2YYmDINTucLFWY81/Wuv2aOIpAAAAKBQ6gOSUOoD 4 | kgAAAAtzc2gtZWQyNTUxOQAAACBZnnnYZjFQ7Zt0gMyJ2YYmDINTucLFWY81/Wuv2aOIpA 5 | AAAEAP8fq0hjlR3jhL7pg+26PSaMiC1V/RrinVbo/4eBMRNFmeedhmMVDtm3SAzInZhiYM 6 | g1O5wsVZjzX9a6/Zo4ikAAAAGWpmb3V0dHNAVVNBSkZPVVRUU00ubG9jYWwBAgME 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test/id_ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFmeedhmMVDtm3SAzInZhiYMg1O5wsVZjzX9a6/Zo4ik jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAt8OMf6QYihSbrrpbk964PKCkv7VkLz6C5OG1SHtHBIGasSf/ 3 | 101VEQhPlKoiEQuLBMLtChM99BTY1a853P3vu6DfBfN5cggKNQEaGdCWT8WgKWu8 4 | CJEkDQsMp2Dl/y7j8ZlH4Z0GViZIfALTf3keo3i1mULYC7dd96i7QbaAQai7pVLy 5 | bxFRU7kGh6aziZDDDz8qUIKZEFIZTxD8dcXRURMFCa5SAYVTF0yxDtLyHKt3rtOi 6 | ErH8RMdjzXvzvd2Nmub7KeWkCieTYq2qa/SWpjeNkKdhYiZvWbWQIEmKMSSbowdP 7 | wusN2cgPHcBiiGheiDWvgcG8heyA0o/xhExlkwIDAQABAoIBAAUzjMOEIomuAaYV 8 | ckgiMrbMmT0v5jEMJOpTlS0DCESRYo6HIk+2yaScxpvfoO04lWvCFLGHT+abvHN3 9 | 7TxbF7EI4acqeBKJNbjAWjvG0qpZXqoteXoImaug12/ZZ0ksy5joDMfPCQHhPYX9 10 | En12MS7RWNqIsNLMEuXSqdI3VzQEhVL9TttHrcOyFoQYTd4gmXPVgMhO8/RgG2ha 11 | Kl30/sw+CeATOlnMquFrROf4rvbgXVyjHfsa8kPvfFgAZGudHhbHccplhIDygzjV 12 | QoUbst7laKkuGj792fvAgOCMmsE3TiuKzN/1Y3b0B4sGr82ABLg9P15SlT7h9sgL 13 | jG97ONECgYEA6dhHfBADBieIeTifrLq8iTpAhKLLI9JVv6BDsI7CmfjePvDEtkCa 14 | Bvtq/MQnInsfJKTObUHrlluGXT+hTCJB1X7dGf8DAEffJT2vJ6+QJlpAWd1vmatk 15 | 1wiypS9H1CwFIFp+GLq07uU6uFaDezixxYXYmbR0TA5t6+JvKXq95SsCgYEAySyY 16 | Yxt/UF16Wwyr8rzS52IK2DbuNbbetYfxfh6UJ9rregpoYGhRMH7y6Pn+n4ckyZXk 17 | RpZopCr6mrRk4L3CZtavrvSbTf5QIpucwRBE+fGr6YKI3lVy73PcDSjVF+P+Tpc0 18 | AhI/3GO3XLRv/Tp8MOroP61KvSItnnWIohTrnTkCgYBOeVcb6h+prBCfZYIoLA5j 19 | GtpV6G+1WLuP8A9nK7FgjGTAyHmrE6jc2PiBVK4xYxIDcQ8ZGTsfHR3NIzJU41Ym 20 | eElolOyD5pqa28Vw0vjT9guMXMQ71Imlo/SXfHNlX1RlFBkm4Vkgpmp7PAUpj9AQ 21 | kicrBaTVdS4sL7PQPrGFOwKBgQCMmep3egRlOrAFarnSkT4mEVPbAalDSWgmH7kc 22 | mGqb4FmrlhKVLtNvvYowYmkfPejsKyK8YusHsjIsZeALYxS3o1xuPu88d32ycmgA 23 | V0qeFdY9Acp++eG2kZc+a0djxyk57FuhBvgzJE5HMKUEqxeZaNShjJAr1/NiSGsS 24 | POTfoQKBgGnsKDLpfKrem3CjISnPhTExkLV6oipcLdL9vhimUOWeoXPp5ckrkkdC 25 | qfLaJTu1VpiMYCjmGVvlTDZzVhryqpAnYBdVOrZsOOZi3+Z5pR7JlWZd4PCpWb7Q 26 | Ldc5E6XkJW/RmMjKAzZsadNAIfyLwqI9kmaEGtkKSJpsWdqamjvm 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/id_rsa.openssh: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEAt8OMf6QYihSbrrpbk964PKCkv7VkLz6C5OG1SHtHBIGasSf/101V 4 | EQhPlKoiEQuLBMLtChM99BTY1a853P3vu6DfBfN5cggKNQEaGdCWT8WgKWu8CJEkDQsMp2 5 | Dl/y7j8ZlH4Z0GViZIfALTf3keo3i1mULYC7dd96i7QbaAQai7pVLybxFRU7kGh6aziZDD 6 | Dz8qUIKZEFIZTxD8dcXRURMFCa5SAYVTF0yxDtLyHKt3rtOiErH8RMdjzXvzvd2Nmub7Ke 7 | WkCieTYq2qa/SWpjeNkKdhYiZvWbWQIEmKMSSbowdPwusN2cgPHcBiiGheiDWvgcG8heyA 8 | 0o/xhExlkwAAA9CdjgBfnY4AXwAAAAdzc2gtcnNhAAABAQC3w4x/pBiKFJuuuluT3rg8oK 9 | S/tWQvPoLk4bVIe0cEgZqxJ//XTVURCE+UqiIRC4sEwu0KEz30FNjVrznc/e+7oN8F83ly 10 | CAo1ARoZ0JZPxaApa7wIkSQNCwynYOX/LuPxmUfhnQZWJkh8AtN/eR6jeLWZQtgLt133qL 11 | tBtoBBqLulUvJvEVFTuQaHprOJkMMPPypQgpkQUhlPEPx1xdFREwUJrlIBhVMXTLEO0vIc 12 | q3eu06ISsfxEx2PNe/O93Y2a5vsp5aQKJ5Nirapr9JamN42Qp2FiJm9ZtZAgSYoxJJujB0 13 | /C6w3ZyA8dwGKIaF6INa+BwbyF7IDSj/GETGWTAAAAAwEAAQAAAQAFM4zDhCKJrgGmFXJI 14 | IjK2zJk9L+YxDCTqU5UtAwhEkWKOhyJPtsmknMab36DtOJVrwhSxh0/mm7xzd+08WxexCO 15 | GnKngSiTW4wFo7xtKqWV6qLXl6CJmroNdv2WdJLMuY6AzHzwkB4T2F/RJ9djEu0VjaiLDS 16 | zBLl0qnSN1c0BIVS/U7bR63DshaEGE3eIJlz1YDITvP0YBtoWipd9P7MPgngEzpZzKrha0 17 | Tn+K724F1cox37GvJD73xYAGRrnR4Wx3HKZYSA8oM41UKFG7Le5WipLho+/dn7wIDgjJrB 18 | N04riszf9WN29AeLBq/NgAS4PT9eUpU+4fbIC4xvezjRAAAAgGnsKDLpfKrem3CjISnPhT 19 | ExkLV6oipcLdL9vhimUOWeoXPp5ckrkkdCqfLaJTu1VpiMYCjmGVvlTDZzVhryqpAnYBdV 20 | OrZsOOZi3+Z5pR7JlWZd4PCpWb7QLdc5E6XkJW/RmMjKAzZsadNAIfyLwqI9kmaEGtkKSJ 21 | psWdqamjvmAAAAgQDp2Ed8EAMGJ4h5OJ+suryJOkCEossj0lW/oEOwjsKZ+N4+8MS2QJoG 22 | +2r8xCciex8kpM5tQeuWW4ZdP6FMIkHVft0Z/wMAR98lPa8nr5AmWkBZ3W+Zq2TXCLKlL0 23 | fULAUgWn4YurTu5Tq4VoN7OLHFhdiZtHRMDm3r4m8per3lKwAAAIEAySyYYxt/UF16Wwyr 24 | 8rzS52IK2DbuNbbetYfxfh6UJ9rregpoYGhRMH7y6Pn+n4ckyZXkRpZopCr6mrRk4L3CZt 25 | avrvSbTf5QIpucwRBE+fGr6YKI3lVy73PcDSjVF+P+Tpc0AhI/3GO3XLRv/Tp8MOroP61K 26 | vSItnnWIohTrnTkAAAAZamZvdXR0c0BVU0FKRk9VVFRTTS5sb2NhbAEC 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3w4x/pBiKFJuuuluT3rg8oKS/tWQvPoLk4bVIe0cEgZqxJ//XTVURCE+UqiIRC4sEwu0KEz30FNjVrznc/e+7oN8F83lyCAo1ARoZ0JZPxaApa7wIkSQNCwynYOX/LuPxmUfhnQZWJkh8AtN/eR6jeLWZQtgLt133qLtBtoBBqLulUvJvEVFTuQaHprOJkMMPPypQgpkQUhlPEPx1xdFREwUJrlIBhVMXTLEO0vIcq3eu06ISsfxEx2PNe/O93Y2a5vsp5aQKJ5Nirapr9JamN42Qp2FiJm9ZtZAgSYoxJJujB0/C6w3ZyA8dwGKIaF6INa+BwbyF7IDSj/GETGWT jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/ssh_host_ecdsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR6CRv2GHU23gai7jw8YQ5IwOj09tOS 4 | MoG6b6hRQnC9BseB427xHD2y4xYUZeOoeD+k5vAgWzG5XSis/U1mEWf0AAAAuOLU2yri1N 5 | sqAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHoJG/YYdTbeBqLu 6 | PDxhDkjA6PT205IygbpvqFFCcL0Gx4HjbvEcPbLjFhRl46h4P6Tm8CBbMbldKKz9TWYRZ/ 7 | QAAAAhAMOqFkndoggSsAYprv9Ur+sHi4LVhJ0VjTp4CHi9+nriAAAAGWpmb3V0dHNAVVNB 8 | SkZPVVRUU00ubG9jYWwBAgMEBQY= 9 | -----END OPENSSH PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /test/ssh_host_ecdsa_key.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHoJG/YYdTbeBqLuPDxhDkjA6PT205IygbpvqFFCcL0Gx4HjbvEcPbLjFhRl46h4P6Tm8CBbMbldKKz9TWYRZ/Q= jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/ssh_host_ed25519_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACDQJ65EwdaizNomzJhQcqzhsAhA+Td3X//b2D07tkuH5QAAAKAk6jnzJOo5 4 | 8wAAAAtzc2gtZWQyNTUxOQAAACDQJ65EwdaizNomzJhQcqzhsAhA+Td3X//b2D07tkuH5Q 5 | AAAECPVGo7eCpp7D0hyrw+wKTI2LGyPud3JhnexFpn+vhuhdAnrkTB1qLM2ibMmFByrOGw 6 | CED5N3df/9vYPTu2S4flAAAAGWpmb3V0dHNAVVNBSkZPVVRUU00ubG9jYWwBAgME 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test/ssh_host_ed25519_key.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINAnrkTB1qLM2ibMmFByrOGwCED5N3df/9vYPTu2S4fl jfoutts@USAJFOUTTSM.local 2 | -------------------------------------------------------------------------------- /test/ssh_host_rsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEA8438q4ZuPdkyk/eC1A4Zx/9WjFX7CXqHxTqNzK3j2ozhZN901j6s 4 | knwThU+wuqXT6cqPStspvIiARcK4CsecSnvxXEEeHjzjG4hYRoDSiEczcrUlgGUWZED64E 5 | nhZupmSdezJP6v+69e4BnL5zKd9BXIU5YD6w/b4CV9x9GxduY3VCeleAzzT5+yaI+6wtnx 6 | b0xt2wzPL6DsRALL3UdttVTexDBA205nN8xX2Uf1LSGfd41VUuaWLssZT5NCkVeNoqPQWc 7 | IvQEWOSjWNzvoLsiEI+UmREtBJP4E9pQvFrwce/rzke9Tr5fbRJV7C7DX7acU6kCnowRTf 8 | KCanBSubuQAAA9C6EgSZuhIEmQAAAAdzc2gtcnNhAAABAQDzjfyrhm492TKT94LUDhnH/1 9 | aMVfsJeofFOo3MrePajOFk33TWPqySfBOFT7C6pdPpyo9K2ym8iIBFwrgKx5xKe/FcQR4e 10 | POMbiFhGgNKIRzNytSWAZRZkQPrgSeFm6mZJ17Mk/q/7r17gGcvnMp30FchTlgPrD9vgJX 11 | 3H0bF25jdUJ6V4DPNPn7Joj7rC2fFvTG3bDM8voOxEAsvdR221VN7EMEDbTmc3zFfZR/Ut 12 | IZ93jVVS5pYuyxlPk0KRV42io9BZwi9ARY5KNY3O+guyIQj5SZES0Ek/gT2lC8WvBx7+vO 13 | R71Ovl9tElXsLsNftpxTqQKejBFN8oJqcFK5u5AAAAAwEAAQAAAQEA25xOFwQSZ6ZvWsi1 14 | qSxFxvbQPZ5RzAw2XHsd3U92w1yA6IotOfotdbB3kZ93xfU9DfReHKteCOg0cbLQbLfsj6 15 | UOz5bP54gTaIIaxwflzogVNfttI0cDV8bX8GHt4vS84xyiJluYp6NMM1pPZ9tWXf8+MVB1 16 | nAEizAxCTGkiUggl0FXYIednoiHe7vuGmRhDptMZRBBWr6HcKkV+RRK2qa4Tyw6yvep0iC 17 | kIZW4GalDMmk32QU6rg2x+eiV3HqICw3MNPXmkeuldaJ7ZQpmHFSVCbOKfWiSZxrPjUBtp 18 | SK0VuRh/Dimg0tOpjjjuLxHO1cp+Horjdw0BnfkcMs2eYQAAAIEAkiZm0IfSBl2nqVRri1 19 | 6Ugs+3ybbg6YiYqmC7ExaWHISubq0DBzMVurV9l94X2RDAFcQYIRXNykAnrMqlk5z+6xz3 20 | qSM+mrzfBlebzhUXGTaHkKzPrp1wVlBJ+zEmg6Yt/O7qKEk8FSOr6YEeWCSnHMgnP7wc4/ 21 | jCh9QrNduIhyoAAACBAPnEgU5iGlQC0uTDKUsa/cSLIjxYp0WAQ4BPi2ReP40HxdeO+h37 22 | L63Tmj6IA8gj6tPcsKceRGEMBfHyp5oXz1888hPy2b9Q71FE0KTw2mcsx7tkbLsXx1wn6K 23 | 14KpqyvCfDwA+jwNO2i5lxtrPKPWUGkObvtC8iyQ2Ep8EEYWUVAAAAgQD5octDyq8l1Jh3 24 | lwY48o8I+3WhwdiTy9U/ePpD9A/gNVlSQH114ldt3zPj1OG8AHmtvlJuxQ+qwpSgT9i4r1 25 | lNVKTd3o5kC5dkN8IBrykSFcDBSDcFJR3trPnp5IoR2bQy7Pu0VVDlMdWCA8oJxzs3HAXG 26 | RnGKTbngeeMiJmtNFQAAABlqZm91dHRzQFVTQUpGT1VUVFNNLmxvY2Fs 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/ssh_host_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDzjfyrhm492TKT94LUDhnH/1aMVfsJeofFOo3MrePajOFk33TWPqySfBOFT7C6pdPpyo9K2ym8iIBFwrgKx5xKe/FcQR4ePOMbiFhGgNKIRzNytSWAZRZkQPrgSeFm6mZJ17Mk/q/7r17gGcvnMp30FchTlgPrD9vgJX3H0bF25jdUJ6V4DPNPn7Joj7rC2fFvTG3bDM8voOxEAsvdR221VN7EMEDbTmc3zFfZR/UtIZ93jVVS5pYuyxlPk0KRV42io9BZwi9ARY5KNY3O+guyIQj5SZES0Ek/gT2lC8WvBx7+vOR71Ovl9tElXsLsNftpxTqQKejBFN8oJqcFK5u5 jfoutts@USAJFOUTTSM.local 2 | --------------------------------------------------------------------------------