UNPKG

@microsoft/dev-tunnels-ssh

Version:

SSH library for Dev Tunnels

445 lines 25.6 kB
"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