@microsoft/dev-tunnels-ssh
Version:
SSH library for Dev Tunnels
445 lines • 25.6 kB
JavaScript
"use strict";
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var AuthenticationService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthenticationService = void 0;
const sshService_1 = require("./sshService");
const authenticationMessages_1 = require("../messages/authenticationMessages");
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const sshData_1 = require("../io/sshData");
const transportMessages_1 = require("../messages/transportMessages");
const sshAuthenticatingEventArgs_1 = require("../events/sshAuthenticatingEventArgs");
const connectionService_1 = require("./connectionService");
const serviceActivation_1 = require("./serviceActivation");
const queue_1 = require("../util/queue");
const trace_1 = require("../trace");
const errors_1 = require("../errors");
/**
* Handles SSH protocol messages related to client authentication.
*/
let AuthenticationService = AuthenticationService_1 = class AuthenticationService extends sshService_1.SshService {
constructor(session) {
var _a;
super(session);
this.currentRequestMessage = null;
this.authenticationFailureCount = 0;
this.disposeCancellationSource = new vscode_jsonrpc_1.CancellationTokenSource();
const algorithmName = (_a = session.algorithms) === null || _a === void 0 ? void 0 : _a.publicKeyAlgorithmName;
if (!algorithmName) {
throw new Error('Algorithms not initialized.');
}
this.publicKeyAlgorithmName = algorithmName;
}
handleMessage(message, cancellation) {
if (message instanceof authenticationMessages_1.AuthenticationSuccessMessage) {
return this.handleSuccessMessage(message);
}
else if (message instanceof authenticationMessages_1.AuthenticationFailureMessage) {
return this.handleFailureMessage(message);
}
else if (message instanceof authenticationMessages_1.AuthenticationRequestMessage) {
return this.handleAuthenticationRequestMessage(message, cancellation);
}
else if (message instanceof authenticationMessages_1.AuthenticationInfoRequestMessage) {
return this.handleInfoRequestMessage(message, cancellation);
}
else if (message instanceof authenticationMessages_1.AuthenticationInfoResponseMessage) {
return this.handleInfoResponseMessage(message, cancellation);
}
else if (message instanceof authenticationMessages_1.PublicKeyOKMessage) {
// Not handled.
}
else {
// Ignore unrecognized authentication messages.
}
}
async handleAuthenticationRequestMessage(message, cancellation) {
this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionAuthenticating, `Authentication request: ${message.methodName}`);
let methodName = message.methodName;
if (!this.session.config.authenticationMethods.includes(methodName)) {
methodName = null;
}
if (methodName === "publickey" /* AuthenticationMethod.publicKey */ ||
methodName === "hostbased" /* AuthenticationMethod.hostBased */) {
const publicKeymessage = message.convertTo(new authenticationMessages_1.PublicKeyRequestMessage());
this.setCurrentRequest(publicKeymessage);
return this.handlePublicKeyRequestMessage(publicKeymessage, cancellation);
}
else if (methodName === "password" /* AuthenticationMethod.password */) {
const passwordMessage = message.convertTo(new authenticationMessages_1.PasswordRequestMessage());
this.setCurrentRequest(passwordMessage);
return this.handlePasswordRequestMessage(passwordMessage, cancellation);
}
else if (methodName === "keyboard-interactive" /* AuthenticationMethod.keyboardInteractive */) {
this.setCurrentRequest(message);
return this.beginInteractiveAuthentication(message, cancellation);
}
else if (methodName === "none" /* AuthenticationMethod.none */) {
this.setCurrentRequest(message);
return this.handleAuthenticating(new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.clientNone, {
username: message.username,
}), cancellation);
}
else {
this.setCurrentRequest(null);
const failureMessage = new authenticationMessages_1.AuthenticationFailureMessage();
failureMessage.methodNames = [
"publickey" /* AuthenticationMethod.publicKey */,
"password" /* AuthenticationMethod.password */,
"hostbased" /* AuthenticationMethod.hostBased */,
];
await this.session.sendMessage(failureMessage, cancellation);
}
}
setCurrentRequest(message) {
var _a;
this.currentRequestMessage = message;
const protocol = this.session.protocol;
if (protocol) {
protocol.messageContext = (_a = message === null || message === void 0 ? void 0 : message.methodName) !== null && _a !== void 0 ? _a : null;
}
}
async handlePublicKeyRequestMessage(message, cancellation) {
var _a, _b, _c;
const publicKeyAlg = this.session.config.getPublicKeyAlgorithm(message.keyAlgorithmName);
if (!publicKeyAlg) {
const failureMessage = new authenticationMessages_1.AuthenticationFailureMessage();
failureMessage.methodNames = [
"publickey" /* AuthenticationMethod.publicKey */,
"password" /* AuthenticationMethod.password */,
];
await this.session.sendMessage(failureMessage, cancellation);
return;
}
const publicKey = publicKeyAlg.createKeyPair();
await publicKey.setPublicKeyBytes(message.publicKey);
let args;
if (message.methodName === "hostbased" /* AuthenticationMethod.hostBased */) {
args = new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.clientHostBased, {
username: (_a = message.username) !== null && _a !== void 0 ? _a : '',
publicKey: publicKey,
clientHostname: message.clientHostname,
clientUsername: message.clientUsername,
});
}
else if (!message.hasSignature) {
args = new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.clientPublicKeyQuery, {
username: (_b = message.username) !== null && _b !== void 0 ? _b : '',
publicKey: publicKey,
});
}
else {
// Verify that the signature matches the public key.
const signature = publicKeyAlg.readSignatureData(message.signature);
const sessionId = this.session.sessionId;
if (sessionId == null) {
throw new Error('Session ID not initialized.');
}
const writer = new sshData_1.SshDataWriter(Buffer.alloc(sessionId.length + message.payloadWithoutSignature.length + 20));
writer.writeBinary(sessionId);
writer.write(message.payloadWithoutSignature);
const signedData = writer.toBuffer();
const verifier = publicKeyAlg.createVerifier(publicKey);
const verified = await verifier.verify(signedData, signature);
if (!verified) {
await this.handleAuthenticationFailure('Public key authentication failed: invalid signature.', cancellation);
}
args = new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.clientPublicKey, {
username: (_c = message.username) !== null && _c !== void 0 ? _c : '',
publicKey: publicKey,
});
}
// Raise an Authenticating event that allows handlers to do additional verification
// of the client's username and public key.
await this.handleAuthenticating(args, cancellation);
}
async handlePasswordRequestMessage(message, cancellation) {
var _a, _b;
// Raise an Authenticating event that allows handlers to do verification
// of the client's username and password.
const args = new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.clientPassword, {
username: (_a = message.username) !== null && _a !== void 0 ? _a : '',
password: (_b = message.password) !== null && _b !== void 0 ? _b : '',
});
await this.handleAuthenticating(args, cancellation);
}
async beginInteractiveAuthentication(message, cancellation) {
// Raise an Authenticating event that allows the server to interactively prompt for
// information from the client.
const args = new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.clientInteractive, {
username: message.username,
});
await this.handleAuthenticating(args, cancellation);
}
async handleInfoRequestMessage(message, cancellation) {
// Raise an Authenticating event that allows the client to respond to interactive prompts
// and provide requested information to the server.
const args = new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.clientInteractive, {
infoRequest: message,
});
await this.handleAuthenticating(args, cancellation);
}
async handleInfoResponseMessage(message, cancellation) {
var _a;
// Raise an Authenticating event that allows the server to process the client's responses
// to interactive prompts, and request further info if necessary.
const args = new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.clientInteractive, {
username: (_a = this.currentRequestMessage) === null || _a === void 0 ? void 0 : _a.username,
infoResponse: message,
});
await this.handleAuthenticating(args, cancellation);
}
async handleAuthenticating(args, cancellation) {
var _a;
if (!this.currentRequestMessage) {
throw new errors_1.SshConnectionError('No current authentication request.', transportMessages_1.SshDisconnectReason.protocolError);
}
args.cancellation = this.disposeCancellationSource.token;
let authenticatedPrincipal = null;
try {
authenticatedPrincipal = await this.session.raiseAuthenticatingEvent(args);
}
catch (e) {
if (!(e instanceof Error))
throw e;
this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.authenticationError, `Error while authenticating client: ${e.message}`, e);
authenticatedPrincipal = null;
}
if (authenticatedPrincipal) {
if (args.authenticationType === sshAuthenticatingEventArgs_1.SshAuthenticationType.clientPublicKeyQuery) {
const publicKeyRequest = this.currentRequestMessage;
const okMessage = new authenticationMessages_1.PublicKeyOKMessage();
okMessage.keyAlgorithmName = publicKeyRequest.keyAlgorithmName;
okMessage.publicKey = publicKeyRequest.publicKey;
this.setCurrentRequest(null);
await this.session.sendMessage(okMessage, cancellation);
}
else {
this.session.principal = authenticatedPrincipal;
const serviceName = this.currentRequestMessage.serviceName;
if (serviceName) {
this.session.activateService(serviceName);
}
this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionAuthenticated, `${sshAuthenticatingEventArgs_1.SshAuthenticationType[args.authenticationType]} authentication succeeded.`);
this.setCurrentRequest(null);
await this.session.sendMessage(new authenticationMessages_1.AuthenticationSuccessMessage(), cancellation);
(_a = this.session) === null || _a === void 0 ? void 0 : _a.handleClientAuthenticated();
}
}
else if (args.authenticationType === sshAuthenticatingEventArgs_1.SshAuthenticationType.clientInteractive &&
!this.session.isClientSession &&
args.infoRequest) {
// Server authenticating event-handler supplied an info request.
await this.sendMessage(args.infoRequest, cancellation);
}
else if (args.authenticationType === sshAuthenticatingEventArgs_1.SshAuthenticationType.clientInteractive &&
this.session.isClientSession &&
args.infoResponse) {
// Client authenticating event-handler supplied an info response.
await this.sendMessage(args.infoResponse, cancellation);
}
else {
this.setCurrentRequest(null);
await this.handleAuthenticationFailure(`${sshAuthenticatingEventArgs_1.SshAuthenticationType[args.authenticationType]} authentication failed.`);
}
}
async handleAuthenticationFailure(message, cancellation) {
this.authenticationFailureCount++;
this.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.clientAuthenticationFailed, message);
const failureMessage = new authenticationMessages_1.AuthenticationFailureMessage();
failureMessage.methodNames = this.session.config.authenticationMethods;
await this.session.sendMessage(failureMessage, cancellation);
// Allow trying again with another authentication method. But prevent unlimited tries.
if (this.authenticationFailureCount >= this.session.config.maxClientAuthenticationAttempts) {
await this.session.close(transportMessages_1.SshDisconnectReason.noMoreAuthMethodsAvailable, 'Authentication failed.');
}
}
async authenticateClient(credentials, cancellation) {
var _a, _b, _c, _d;
this.clientAuthenticationMethods = new queue_1.Queue();
const configuredMethods = this.session.config.authenticationMethods;
if (configuredMethods.includes("publickey" /* AuthenticationMethod.publicKey */)) {
for (const publicKey of (_a = credentials.publicKeys) !== null && _a !== void 0 ? _a : []) {
if (!publicKey)
continue;
const username = (_b = credentials.username) !== null && _b !== void 0 ? _b : '';
let privateKey = publicKey;
const privateKeyProvider = credentials.privateKeyProvider;
this.clientAuthenticationMethods.enqueue({
method: "publickey" /* AuthenticationMethod.publicKey */,
handler: async (cancellation2) => {
if (!privateKey.hasPrivateKey) {
if (privateKeyProvider == null) {
throw new Error('A private key provider is required.');
}
privateKey = await privateKeyProvider(publicKey, cancellation2 !== null && cancellation2 !== void 0 ? cancellation2 : vscode_jsonrpc_1.CancellationToken.None);
}
if (privateKey) {
await this.requestPublicKeyAuthentication(username, privateKey, cancellation2);
}
else {
await this.session.close(transportMessages_1.SshDisconnectReason.authCancelledByUser);
}
},
});
}
}
if (configuredMethods.includes("password" /* AuthenticationMethod.password */)) {
const passwordCredentialProvider = credentials.passwordProvider;
if (passwordCredentialProvider) {
this.clientAuthenticationMethods.enqueue({
method: "password" /* AuthenticationMethod.password */,
handler: async (cancellation2) => {
var _a;
const passwordCredentialPromise = passwordCredentialProvider(cancellation2 !== null && cancellation2 !== void 0 ? cancellation2 : vscode_jsonrpc_1.CancellationToken.None);
const passwordCredential = passwordCredentialPromise
? await passwordCredentialPromise
: null;
if (passwordCredential) {
await this.requestPasswordAuthentication((_a = passwordCredential[0]) !== null && _a !== void 0 ? _a : '', passwordCredential[1], cancellation2);
}
else {
await this.session.close(transportMessages_1.SshDisconnectReason.authCancelledByUser);
}
},
});
}
else if (credentials.password) {
const username = (_c = credentials.username) !== null && _c !== void 0 ? _c : '';
const password = credentials.password;
this.clientAuthenticationMethods.enqueue({
method: "password" /* AuthenticationMethod.password */,
handler: async (cancellation2) => {
await this.requestPasswordAuthentication(username, password, cancellation2);
},
});
}
}
// Only add None or Interactive methods if no client credentials were supplied.
if (this.clientAuthenticationMethods.size === 0) {
const username = (_d = credentials.username) !== null && _d !== void 0 ? _d : '';
if (configuredMethods.includes("none" /* AuthenticationMethod.none */)) {
this.clientAuthenticationMethods.enqueue({
method: "none" /* AuthenticationMethod.none */,
handler: async (cancellation2) => {
await this.requestUsernameAuthentication(username, cancellation2);
},
});
}
if (configuredMethods.includes("keyboard-interactive" /* AuthenticationMethod.keyboardInteractive */)) {
this.clientAuthenticationMethods.enqueue({
method: "keyboard-interactive" /* AuthenticationMethod.keyboardInteractive */,
handler: async (cancellation2) => {
await this.requestInteractiveAuthentication(username, cancellation2);
},
});
}
if (this.clientAuthenticationMethods.size === 0) {
throw new Error('Could not prepare request for authentication method(s): ' +
configuredMethods.join(', ') +
'. Supply client credentials or enable none or interactive authentication methods.');
}
}
// Auth request messages all include a request the for the server to activate the connection
// service . Go ahead and activate it on the client side too; if authentication fails then
// a following channel open request will fail anyway.
this.session.activateService(connectionService_1.ConnectionService);
const firstAuthMethod = this.clientAuthenticationMethods.dequeue();
await firstAuthMethod.handler(cancellation);
}
async requestUsernameAuthentication(username, cancellation) {
const authMessage = new authenticationMessages_1.AuthenticationRequestMessage();
authMessage.serviceName = connectionService_1.ConnectionService.serviceName;
authMessage.methodName = "none" /* AuthenticationMethod.none */;
authMessage.username = username;
this.setCurrentRequest(authMessage);
await this.session.sendMessage(authMessage, cancellation);
}
async requestPublicKeyAuthentication(username, key, cancellation) {
const algorithm = this.session.config.publicKeyAlgorithms.find((a) => (a === null || a === void 0 ? void 0 : a.keyAlgorithmName) === key.keyAlgorithmName);
if (!algorithm) {
throw new Error(`Public key algorithm '${key.keyAlgorithmName}' is not in session config.`);
}
const authMessage = new authenticationMessages_1.PublicKeyRequestMessage();
authMessage.serviceName = connectionService_1.ConnectionService.serviceName;
authMessage.username = username;
authMessage.keyAlgorithmName = algorithm.name;
authMessage.publicKey = (await key.getPublicKeyBytes(algorithm.name));
authMessage.signature = await this.createAuthenticationSignature(authMessage, algorithm, key);
this.setCurrentRequest(authMessage);
await this.session.sendMessage(authMessage, cancellation);
}
async requestPasswordAuthentication(username, password, cancellation) {
const authMessage = new authenticationMessages_1.PasswordRequestMessage();
authMessage.serviceName = connectionService_1.ConnectionService.serviceName;
authMessage.username = username;
authMessage.password = password;
this.setCurrentRequest(authMessage);
await this.session.sendMessage(authMessage, cancellation);
}
async requestInteractiveAuthentication(username, cancellation) {
const authMessage = new authenticationMessages_1.AuthenticationRequestMessage();
authMessage.serviceName = connectionService_1.ConnectionService.serviceName;
authMessage.methodName = "keyboard-interactive" /* AuthenticationMethod.keyboardInteractive */;
authMessage.username = username;
this.setCurrentRequest(authMessage);
await this.session.sendMessage(authMessage, cancellation);
}
async handleFailureMessage(message) {
var _a, _b;
this.setCurrentRequest(null);
while ((_a = this.clientAuthenticationMethods) === null || _a === void 0 ? void 0 : _a.size) {
const nextAuthMethod = this.clientAuthenticationMethods.dequeue();
// Skip client auth methods that the server did not suggest.
if ((_b = message.methodNames) === null || _b === void 0 ? void 0 : _b.includes(nextAuthMethod.method)) {
await nextAuthMethod.handler(this.disposeCancellationSource.token);
return;
}
}
this.session.onAuthenticationComplete(false);
}
handleSuccessMessage(message) {
this.setCurrentRequest(null);
this.session.onAuthenticationComplete(true);
}
async createAuthenticationSignature(requestMessage, algorithm, key) {
const sessionId = this.session.sessionId;
if (sessionId == null) {
throw new Error('Session ID not initialized.');
}
const writer = new sshData_1.SshDataWriter(Buffer.alloc(requestMessage.publicKey.length + (requestMessage.username || '').length + 400));
writer.writeBinary(sessionId);
writer.writeByte(requestMessage.messageType);
writer.writeString(requestMessage.username || '', 'utf8');
writer.writeString(requestMessage.serviceName || '', 'ascii');
writer.writeString("publickey" /* AuthenticationMethod.publicKey */, 'ascii');
writer.writeBoolean(true);
writer.writeString(requestMessage.keyAlgorithmName, 'ascii');
writer.writeBinary(requestMessage.publicKey);
const signer = algorithm.createSigner(key);
const signature = await signer.sign(writer.toBuffer());
return algorithm.createSignatureData(signature);
}
dispose() {
try {
this.disposeCancellationSource.cancel();
this.disposeCancellationSource.dispose();
}
catch (_a) { }
super.dispose();
}
};
AuthenticationService.serviceName = 'ssh-userauth';
AuthenticationService = AuthenticationService_1 = __decorate([
(0, serviceActivation_1.serviceActivation)({ serviceRequest: AuthenticationService_1.serviceName })
], AuthenticationService);
exports.AuthenticationService = AuthenticationService;
//# sourceMappingURL=authenticationService.js.map