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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/dartssh2.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'src/ssh_algorithm.dart' show SSHAlgorithms;
export 'src/ssh_agent.dart';
export 'src/ssh_client.dart';
export 'src/ssh_errors.dart';
export 'src/ssh_forward.dart';
Expand Down
1 change: 1 addition & 0 deletions lib/src/message/msg_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ abstract class SSHChannelRequestType {
static const shell = 'shell';
static const exec = 'exec';
static const subsystem = 'subsystem';
static const authAgent = 'auth-agent-req@openssh.com';
static const windowChange = 'window-change';
static const xon = 'xon-xoff';
static const signal = 'signal';
Expand Down
234 changes: 234 additions & 0 deletions lib/src/ssh_agent.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:dartssh2/src/hostkey/hostkey_rsa.dart';
import 'package:dartssh2/src/ssh_channel.dart';
import 'package:dartssh2/src/ssh_hostkey.dart';
import 'package:dartssh2/src/ssh_key_pair.dart';
import 'package:dartssh2/src/ssh_message.dart';
import 'package:dartssh2/src/ssh_transport.dart';
import 'package:pointycastle/api.dart' hide Signature;
import 'package:pointycastle/asymmetric/api.dart' as asymmetric;
import 'package:pointycastle/digests/sha1.dart';
import 'package:pointycastle/digests/sha256.dart';
import 'package:pointycastle/digests/sha512.dart';
import 'package:pointycastle/signers/rsa_signer.dart';

abstract class SSHAgentHandler {
Future<Uint8List> handleRequest(Uint8List request);
}

class SSHKeyPairAgent implements SSHAgentHandler {
SSHKeyPairAgent(this._identities, {this.comment});

final List<SSHKeyPair> _identities;
final String? comment;

@override
Future<Uint8List> handleRequest(Uint8List request) async {
if (request.isEmpty) {
return _failure();
}
final reader = SSHMessageReader(request);
final messageType = reader.readUint8();
switch (messageType) {
case SSHAgentProtocol.requestIdentities:
return _handleRequestIdentities();
case SSHAgentProtocol.signRequest:
return _handleSignRequest(reader);
default:
return _failure();
}
}

Uint8List _handleRequestIdentities() {
final writer = SSHMessageWriter();
writer.writeUint8(SSHAgentProtocol.identitiesAnswer);
writer.writeUint32(_identities.length);
for (final identity in _identities) {
final publicKey = identity.toPublicKey().encode();
writer.writeString(publicKey);
writer.writeUtf8(comment ?? '');
}
return writer.takeBytes();
}

Uint8List _handleSignRequest(SSHMessageReader reader) {
final keyBlob = reader.readString();
final data = reader.readString();
final flags = reader.readUint32();

final identity = _findIdentity(keyBlob);
if (identity == null) {
return _failure();
}

final signature = _sign(identity, data, flags);
final writer = SSHMessageWriter();
writer.writeUint8(SSHAgentProtocol.signResponse);
writer.writeString(signature.encode());
return writer.takeBytes();
}

SSHSignature _sign(SSHKeyPair identity, Uint8List data, int flags) {
if (identity is OpenSSHRsaKeyPair || identity is RsaPrivateKey) {
final signatureType = _rsaSignatureTypeForFlags(flags);
return _signRsa(identity, data, signatureType);
}
return identity.sign(data);
}

String _rsaSignatureTypeForFlags(int flags) {
if (flags & SSHAgentProtocol.rsaSha2_512 != 0) {
return SSHRsaSignatureType.sha512;
}
if (flags & SSHAgentProtocol.rsaSha2_256 != 0) {
return SSHRsaSignatureType.sha256;
}
return SSHRsaSignatureType.sha1;
}

SSHRsaSignature _signRsa(
SSHKeyPair identity,
Uint8List data,
String signatureType,
) {
final key = _rsaKeyFrom(identity);
if (key == null) {
return identity.sign(data) as SSHRsaSignature;
}

final signer = _rsaSignerFor(signatureType);
signer.init(true, PrivateKeyParameter<asymmetric.RSAPrivateKey>(key));
return SSHRsaSignature(signatureType, signer.generateSignature(data).bytes);
}

asymmetric.RSAPrivateKey? _rsaKeyFrom(SSHKeyPair identity) {
if (identity is OpenSSHRsaKeyPair) {
return asymmetric.RSAPrivateKey(identity.n, identity.d, identity.p, identity.q);
}
if (identity is RsaPrivateKey) {
return asymmetric.RSAPrivateKey(identity.n, identity.d, identity.p, identity.q);
}
return null;
}

RSASigner _rsaSignerFor(String signatureType) {
switch (signatureType) {
case SSHRsaSignatureType.sha1:
return RSASigner(SHA1Digest(), '06052b0e03021a');
case SSHRsaSignatureType.sha256:
return RSASigner(SHA256Digest(), '0609608648016503040201');
case SSHRsaSignatureType.sha512:
return RSASigner(SHA512Digest(), '0609608648016503040203');
default:
return RSASigner(SHA256Digest(), '0609608648016503040201');
}
}

SSHKeyPair? _findIdentity(Uint8List keyBlob) {
for (final identity in _identities) {
final publicKey = identity.toPublicKey().encode();
if (_bytesEqual(publicKey, keyBlob)) {
return identity;
}
}
return null;
}

Uint8List _failure() {
final writer = SSHMessageWriter();
writer.writeUint8(SSHAgentProtocol.failure);
return writer.takeBytes();
}

bool _bytesEqual(Uint8List a, Uint8List b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
}

class SSHAgentChannel {
SSHAgentChannel(this._channel, this._handler, {this.printDebug}) {
_subscription = _channel.stream.listen(
_handleData,
onDone: _handleDone,
onError: (_, __) => _handleDone(),
);
}

final SSHChannel _channel;
final SSHAgentHandler _handler;
final SSHPrintHandler? printDebug;

StreamSubscription<SSHChannelData>? _subscription;
Uint8List _buffer = Uint8List(0);
bool _processing = false;

void _handleDone() {
_subscription?.cancel();
}

void _handleData(SSHChannelData data) {
_buffer = _appendBytes(_buffer, data.bytes);
_drainRequests();
}

void _drainRequests() {
if (_processing) return;
_processing = true;
_processQueue().whenComplete(() => _processing = false);
}

Future<void> _processQueue() async {
while (_buffer.length >= 4) {
final length = ByteData.sublistView(_buffer, 0, 4).getUint32(0);
if (_buffer.length < 4 + length) return;
final payload = _buffer.sublist(4, 4 + length);
_buffer = _buffer.sublist(4 + length);
Uint8List response;
try {
response = await _handler.handleRequest(payload);
} catch (error) {
printDebug?.call('SSH agent handler error: $error');
response = _failureResponse();
}
_sendResponse(response);
}
}

Uint8List _failureResponse() {
final writer = SSHMessageWriter();
writer.writeUint8(SSHAgentProtocol.failure);
return writer.takeBytes();
}

void _sendResponse(Uint8List payload) {
final writer = SSHMessageWriter();
writer.writeUint32(payload.length);
writer.writeBytes(payload);
_channel.addData(writer.takeBytes());
}

Uint8List _appendBytes(Uint8List a, Uint8List b) {
if (a.isEmpty) return b;
if (b.isEmpty) return a;
final combined = Uint8List(a.length + b.length);
combined.setAll(0, a);
combined.setAll(a.length, b);
return combined;
}
}

abstract class SSHAgentProtocol {
static const int failure = 5;
static const int requestIdentities = 11;
static const int identitiesAnswer = 12;
static const int signRequest = 13;
static const int signResponse = 14;
static const int rsaSha2_256 = 2;
static const int rsaSha2_512 = 4;
}
11 changes: 11 additions & 0 deletions lib/src/ssh_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ class SSHChannelController {
return await _requestReplyQueue.next;
}

Future<bool> sendAgentForwardingRequest() async {
sendMessage(
SSH_Message_Channel_Request(
recipientChannel: remoteId,
requestType: SSHChannelRequestType.authAgent,
wantReply: true,
),
);
return await _requestReplyQueue.next;
}

Future<bool> sendSubsystem(String subsystem) async {
sendMessage(
SSH_Message_Channel_Request.subsystem(
Expand Down
51 changes: 51 additions & 0 deletions lib/src/ssh_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dart:typed_data';
import 'package:dartssh2/src/http/http_client.dart';
import 'package:dartssh2/src/sftp/sftp_client.dart';
import 'package:dartssh2/src/ssh_algorithm.dart';
import 'package:dartssh2/src/ssh_agent.dart';
import 'package:dartssh2/src/ssh_channel.dart';
import 'package:dartssh2/src/ssh_channel_id.dart';
import 'package:dartssh2/src/ssh_errors.dart';
Expand Down Expand Up @@ -122,6 +123,9 @@ class SSHClient {
/// Function called when authentication is complete.
final SSHAuthenticatedHandler? onAuthenticated;

/// Optional handler for SSH agent forwarding requests.
final SSHAgentHandler? agentHandler;

/// The interval at which to send a keep-alive message through the [ping]
/// method. Set this to null to disable automatic keep-alive messages.
final Duration? keepAliveInterval;
Expand Down Expand Up @@ -154,6 +158,7 @@ class SSHClient {
this.onUserInfoRequest,
this.onUserauthBanner,
this.onAuthenticated,
this.agentHandler,
this.keepAliveInterval = const Duration(seconds: 10),
this.disableHostkeyVerification = false,
}) {
Expand Down Expand Up @@ -314,6 +319,10 @@ class SSHClient {
}
}

if (agentHandler != null) {
await channelController.sendAgentForwardingRequest();
}

if (pty != null) {
final ptyOk = await channelController.sendPtyReq(
terminalType: pty.type,
Expand Down Expand Up @@ -353,6 +362,10 @@ class SSHClient {
}
}

if (agentHandler != null) {
await channelController.sendAgentForwardingRequest();
}

if (pty != null) {
final ok = await channelController.sendPtyReq(
terminalType: pty.type,
Expand Down Expand Up @@ -676,6 +689,8 @@ class SSHClient {
switch (message.channelType) {
case 'forwarded-tcpip':
return _handleForwardedTcpipChannelOpen(message);
case 'auth-agent@openssh.com':
return _handleAgentChannelOpen(message);
}

printDebug?.call('unknown channelType: ${message.channelType}');
Expand Down Expand Up @@ -740,6 +755,42 @@ class SSHClient {
);
}

void _handleAgentChannelOpen(SSH_Message_Channel_Open message) {
final handler = agentHandler;
if (handler == null) {
final reply = SSH_Message_Channel_Open_Failure(
recipientChannel: message.senderChannel,
reasonCode: SSH_Message_Channel_Open_Failure.codeUnknownChannelType,
description: 'agent forwarding not enabled',
);
_sendMessage(reply);
return;
}

final localChannelId = _channelIdAllocator.allocate();
final confirmation = SSH_Message_Channel_Confirmation(
recipientChannel: message.senderChannel,
senderChannel: localChannelId,
initialWindowSize: _initialWindowSize,
maximumPacketSize: _maximumPacketSize,
data: Uint8List(0),
);
_sendMessage(confirmation);

final channelController = _acceptChannel(
localChannelId: localChannelId,
remoteChannelId: message.senderChannel,
remoteInitialWindowSize: message.initialWindowSize,
remoteMaximumPacketSize: message.maximumPacketSize,
);

SSHAgentChannel(
channelController.channel,
handler,
printDebug: printDebug,
);
}

/// Finds a remote forward that matches the given host and port.
SSHRemoteForward? _findRemoteForward(String host, int port) {
final result = _remoteForwards.where(
Expand Down
Loading