diff --git a/.eslint-plugins/eslint-plugin-mscdex/index.js b/.eslint-plugins/eslint-plugin-mscdex/index.js new file mode 100644 index 00000000..9d9d4c05 --- /dev/null +++ b/.eslint-plugins/eslint-plugin-mscdex/index.js @@ -0,0 +1,22 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const RULES_DIR = path.join(__dirname, 'rules'); + +let cache; +module.exports = { + get rules() { + if (!cache) { + cache = {}; + const files = fs.readdirSync(RULES_DIR) + .filter(filename => filename.endsWith('.js')); + for (const file of files) { + const name = file.slice(0, -3); + cache[name] = require(path.resolve(RULES_DIR, file)); + } + } + return cache; + }, +}; diff --git a/.eslint-plugins/eslint-plugin-mscdex/rules/curly.js b/.eslint-plugins/eslint-plugin-mscdex/rules/curly.js new file mode 100644 index 00000000..b28d2817 --- /dev/null +++ b/.eslint-plugins/eslint-plugin-mscdex/rules/curly.js @@ -0,0 +1,500 @@ +/** + * @fileoverview Rule to flag statements without curly braces + * @author Nicholas C. Zakas + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "suggestion", + + docs: { + description: "enforce consistent brace style for all control statements", + category: "Best Practices", + recommended: false, + url: "https://eslint.org/docs/rules/curly" + }, + + schema: { + anyOf: [ + { + type: "array", + items: [ + { + enum: ["all"] + } + ], + minItems: 0, + maxItems: 1 + }, + { + type: "array", + items: [ + { + enum: ["multi", "multi-line", "multi-or-nest"] + }, + { + enum: ["consistent"] + } + ], + minItems: 0, + maxItems: 2 + } + ] + }, + + fixable: "code", + + messages: { + missingCurlyAfter: "Expected { after '{{name}}'.", + missingCurlyAfterCondition: "Expected { after '{{name}}' condition.", + unexpectedCurlyAfter: "Unnecessary { after '{{name}}'.", + unexpectedCurlyAfterCondition: "Unnecessary { after '{{name}}' condition." + } + }, + + create(context) { + + const multiOnly = (context.options[0] === "multi"); + const multiLine = (context.options[0] === "multi-line"); + const multiOrNest = (context.options[0] === "multi-or-nest"); + const consistent = (context.options[1] === "consistent"); + + const sourceCode = context.getSourceCode(); + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Determines if a given node is a one-liner that's on the same line as it's preceding code. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code. + * @private + */ + function isCollapsedOneLiner(node) { + const before = sourceCode.getTokenBefore(node); + const last = sourceCode.getLastToken(node); + const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last; + + return before.loc.start.line === lastExcludingSemicolon.loc.end.line; + } + + /** + * Determines if a given node is a one-liner. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is a one-liner. + * @private + */ + function isOneLiner(node) { + if (node.type === "EmptyStatement") { + return true; + } + + const first = sourceCode.getFirstToken(node); + const last = sourceCode.getLastToken(node); + const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last; + + return first.loc.start.line === lastExcludingSemicolon.loc.end.line; + } + + /** + * Determines if the given node is a lexical declaration (let, const, function, or class) + * @param {ASTNode} node The node to check + * @returns {boolean} True if the node is a lexical declaration + * @private + */ + function isLexicalDeclaration(node) { + if (node.type === "VariableDeclaration") { + return node.kind === "const" || node.kind === "let"; + } + + return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration"; + } + + /** + * Checks if the given token is an `else` token or not. + * @param {Token} token The token to check. + * @returns {boolean} `true` if the token is an `else` token. + */ + function isElseKeywordToken(token) { + return token.value === "else" && token.type === "Keyword"; + } + + /** + * Gets the `else` keyword token of a given `IfStatement` node. + * @param {ASTNode} node A `IfStatement` node to get. + * @returns {Token} The `else` keyword token. + */ + function getElseKeyword(node) { + return node.alternate && sourceCode.getFirstTokenBetween(node.consequent, node.alternate, isElseKeywordToken); + } + + /** + * Determines whether the given node has an `else` keyword token as the first token after. + * @param {ASTNode} node The node to check. + * @returns {boolean} `true` if the node is followed by an `else` keyword token. + */ + function isFollowedByElseKeyword(node) { + const nextToken = sourceCode.getTokenAfter(node); + + return Boolean(nextToken) && isElseKeywordToken(nextToken); + } + + /** + * Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError. + * @param {Token} closingBracket The } token + * @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block. + */ + function needsSemicolon(closingBracket) { + const tokenBefore = sourceCode.getTokenBefore(closingBracket); + const tokenAfter = sourceCode.getTokenAfter(closingBracket); + const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]); + + if (astUtils.isSemicolonToken(tokenBefore)) { + + // If the last statement already has a semicolon, don't add another one. + return false; + } + + if (!tokenAfter) { + + // If there are no statements after this block, there is no need to add a semicolon. + return false; + } + + if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") { + + /* + * If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression), + * don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause + * a SyntaxError if it was followed by `else`. + */ + return false; + } + + if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) { + + // If the next token is on the same line, insert a semicolon. + return true; + } + + if (/^[([/`+-]/u.test(tokenAfter.value)) { + + // If the next token starts with a character that would disrupt ASI, insert a semicolon. + return true; + } + + if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) { + + // If the last token is ++ or --, insert a semicolon to avoid disrupting ASI. + return true; + } + + // Otherwise, do not insert a semicolon. + return false; + } + + /** + * Determines whether the code represented by the given node contains an `if` statement + * that would become associated with an `else` keyword directly appended to that code. + * + * Examples where it returns `true`: + * + * if (a) + * foo(); + * + * if (a) { + * foo(); + * } + * + * if (a) + * foo(); + * else if (b) + * bar(); + * + * while (a) + * if (b) + * if(c) + * foo(); + * else + * bar(); + * + * Examples where it returns `false`: + * + * if (a) + * foo(); + * else + * bar(); + * + * while (a) { + * if (b) + * if(c) + * foo(); + * else + * bar(); + * } + * + * while (a) + * if (b) { + * if(c) + * foo(); + * } + * else + * bar(); + * @param {ASTNode} node Node representing the code to check. + * @returns {boolean} `true` if an `if` statement within the code would become associated with an `else` appended to that code. + */ + function hasUnsafeIf(node) { + switch (node.type) { + case "IfStatement": + if (!node.alternate) { + return true; + } + return hasUnsafeIf(node.alternate); + case "ForStatement": + case "ForInStatement": + case "ForOfStatement": + case "LabeledStatement": + case "WithStatement": + case "WhileStatement": + return hasUnsafeIf(node.body); + default: + return false; + } + } + + /** + * Determines whether the existing curly braces around the single statement are necessary to preserve the semantics of the code. + * The braces, which make the given block body, are necessary in either of the following situations: + * + * 1. The statement is a lexical declaration. + * 2. Without the braces, an `if` within the statement would become associated with an `else` after the closing brace: + * + * if (a) { + * if (b) + * foo(); + * } + * else + * bar(); + * + * if (a) + * while (b) + * while (c) { + * while (d) + * if (e) + * while(f) + * foo(); + * } + * else + * bar(); + * @param {ASTNode} node `BlockStatement` body with exactly one statement directly inside. The statement can have its own nested statements. + * @returns {boolean} `true` if the braces are necessary - removing them (replacing the given `BlockStatement` body with its single statement content) + * would change the semantics of the code or produce a syntax error. + */ + const blockParentTypes = ['IfStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement', 'WhileStatement']; + function areBracesNecessary(node) { + const statement = node.body[0]; + let parent; + if (node.type === 'BlockStatement' && (parent = node.parent) && blockParentTypes.includes(parent.type) && (node.loc.start.line - parent.loc.start.line) > 0) + return true; + return isLexicalDeclaration(statement) || + hasUnsafeIf(statement) && isFollowedByElseKeyword(node); + } + + /** + * Prepares to check the body of a node to see if it's a block statement. + * @param {ASTNode} node The node to report if there's a problem. + * @param {ASTNode} body The body node to check for blocks. + * @param {string} name The name to report if there's a problem. + * @param {{ condition: boolean }} opts Options to pass to the report functions + * @returns {Object} a prepared check object, with "actual", "expected", "check" properties. + * "actual" will be `true` or `false` whether the body is already a block statement. + * "expected" will be `true` or `false` if the body should be a block statement or not, or + * `null` if it doesn't matter, depending on the rule options. It can be modified to change + * the final behavior of "check". + * "check" will be a function reporting appropriate problems depending on the other + * properties. + */ + function prepareCheck(node, body, name, opts) { + const hasBlock = (body.type === "BlockStatement"); + let expected = null; + + if (hasBlock && (body.body.length !== 1 || areBracesNecessary(body))) { + expected = true; + } else if (multiOnly) { + expected = false; + } else if (multiLine) { + if (!isCollapsedOneLiner(body)) { + expected = true; + } + + // otherwise, the body is allowed to have braces or not to have braces + + } else if (multiOrNest) { + if (hasBlock) { + const statement = body.body[0]; + const leadingCommentsInBlock = sourceCode.getCommentsBefore(statement); + + expected = !isOneLiner(statement) || leadingCommentsInBlock.length > 0; + } else { + const leadingCommentsInBlock = sourceCode.getCommentsBefore(body); + + expected = !isOneLiner(body) || (leadingCommentsInBlock.length > 0 && (leadingCommentsInBlock[0].loc.end.line - leadingCommentsInBlock[0].loc.start.line) > 0); + } + } else { + + // default "all" + expected = true; + } + + return { + actual: hasBlock, + expected, + check() { + if (this.expected !== null && this.expected !== this.actual) { + if (this.expected) { + context.report({ + node, + loc: (name !== "else" ? node : getElseKeyword(node)).loc.start, + messageId: opts && opts.condition ? "missingCurlyAfterCondition" : "missingCurlyAfter", + data: { + name + }, + fix: fixer => fixer.replaceText(body, `{${sourceCode.getText(body)}}`) + }); + } else { + context.report({ + node, + loc: (name !== "else" ? node : getElseKeyword(node)).loc.start, + messageId: opts && opts.condition ? "unexpectedCurlyAfterCondition" : "unexpectedCurlyAfter", + data: { + name + }, + fix(fixer) { + + /* + * `do while` expressions sometimes need a space to be inserted after `do`. + * e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)` + */ + const needsPrecedingSpace = node.type === "DoWhileStatement" && + sourceCode.getTokenBefore(body).range[1] === body.range[0] && + !astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(body, { skip: 1 })); + + const openingBracket = sourceCode.getFirstToken(body); + const closingBracket = sourceCode.getLastToken(body); + const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket); + + if (needsSemicolon(closingBracket)) { + + /* + * If removing braces would cause a SyntaxError due to multiple statements on the same line (or + * change the semantics of the code due to ASI), don't perform a fix. + */ + return null; + } + + const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) + + sourceCode.getText(lastTokenInBlock) + + sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]); + + return fixer.replaceText(body, (needsPrecedingSpace ? " " : "") + resultingBodyText); + } + }); + } + } + } + }; + } + + /** + * Prepares to check the bodies of a "if", "else if" and "else" chain. + * @param {ASTNode} node The first IfStatement node of the chain. + * @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more + * information. + */ + function prepareIfChecks(node) { + const preparedChecks = []; + + for (let currentNode = node; currentNode; currentNode = currentNode.alternate) { + preparedChecks.push(prepareCheck(currentNode, currentNode.consequent, "if", { condition: true })); + if (currentNode.alternate && currentNode.alternate.type !== "IfStatement") { + preparedChecks.push(prepareCheck(currentNode, currentNode.alternate, "else")); + break; + } + } + + if (consistent) { + + /* + * If any node should have or already have braces, make sure they + * all have braces. + * If all nodes shouldn't have braces, make sure they don't. + */ + const expected = preparedChecks.some(preparedCheck => { + if (preparedCheck.expected !== null) { + return preparedCheck.expected; + } + return preparedCheck.actual; + }); + + preparedChecks.forEach(preparedCheck => { + preparedCheck.expected = expected; + }); + } + + return preparedChecks; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + IfStatement(node) { + const parent = node.parent; + const isElseIf = parent.type === "IfStatement" && parent.alternate === node; + + if (!isElseIf) { + + // This is a top `if`, check the whole `if-else-if` chain + prepareIfChecks(node).forEach(preparedCheck => { + preparedCheck.check(); + }); + } + + // Skip `else if`, it's already checked (when the top `if` was visited) + }, + + WhileStatement(node) { + prepareCheck(node, node.body, "while", { condition: true }).check(); + }, + + DoWhileStatement(node) { + prepareCheck(node, node.body, "do").check(); + }, + + ForStatement(node) { + prepareCheck(node, node.body, "for", { condition: true }).check(); + }, + + ForInStatement(node) { + prepareCheck(node, node.body, "for-in").check(); + }, + + ForOfStatement(node) { + prepareCheck(node, node.body, "for-of").check(); + } + }; + } +}; diff --git a/.eslint-plugins/eslint-plugin-mscdex/rules/quotes.js b/.eslint-plugins/eslint-plugin-mscdex/rules/quotes.js new file mode 100644 index 00000000..683a2465 --- /dev/null +++ b/.eslint-plugins/eslint-plugin-mscdex/rules/quotes.js @@ -0,0 +1,332 @@ +/** + * @fileoverview A rule to choose between single and double quote marks + * @author Matt DuVall , Brandon Payton + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +const QUOTE_SETTINGS = { + double: { + quote: "\"", + alternateQuote: "'", + description: "doublequote" + }, + single: { + quote: "'", + alternateQuote: "\"", + description: "singlequote" + }, + backtick: { + quote: "`", + alternateQuote: "\"", + description: "backtick" + } +}; + +// An unescaped newline is a newline preceded by an even number of backslashes. +const UNESCAPED_LINEBREAK_PATTERN = new RegExp(String.raw`(^|[^\\])(\\\\)*[${Array.from(astUtils.LINEBREAKS).join("")}]`, "u"); + +/** + * Switches quoting of javascript string between ' " and ` + * escaping and unescaping as necessary. + * Only escaping of the minimal set of characters is changed. + * Note: escaping of newlines when switching from backtick to other quotes is not handled. + * @param {string} str A string to convert. + * @returns {string} The string with changed quotes. + * @private + */ +QUOTE_SETTINGS.double.convert = +QUOTE_SETTINGS.single.convert = +QUOTE_SETTINGS.backtick.convert = function(str) { + const newQuote = this.quote; + const oldQuote = str[0]; + + if (newQuote === oldQuote) { + return str; + } + return newQuote + str.slice(1, -1).replace(/\\(\$\{|\r\n?|\n|.)|["'`]|\$\{|(\r\n?|\n)/gu, (match, escaped, newline) => { + if (escaped === oldQuote || oldQuote === "`" && escaped === "${") { + return escaped; // unescape + } + if (match === newQuote || newQuote === "`" && match === "${") { + return `\\${match}`; // escape + } + if (newline && oldQuote === "`") { + return "\\n"; // escape newlines + } + return match; + }) + newQuote; +}; + +const AVOID_ESCAPE = "avoid-escape"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "layout", + + docs: { + description: "enforce the consistent use of either backticks, double, or single quotes", + category: "Stylistic Issues", + recommended: false, + url: "https://eslint.org/docs/rules/quotes" + }, + + fixable: "code", + + schema: [ + { + enum: ["single", "double", "backtick"] + }, + { + anyOf: [ + { + enum: ["avoid-escape"] + }, + { + type: "object", + properties: { + avoidEscape: { + type: "boolean" + }, + allowTemplateLiterals: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + ], + + messages: { + wrongQuotes: "Strings must use {{description}}." + } + }, + + create(context) { + + const quoteOption = context.options[0], + settings = QUOTE_SETTINGS[quoteOption || "double"], + options = context.options[1], + allowTemplateLiterals = options && options.allowTemplateLiterals === true, + sourceCode = context.getSourceCode(); + let avoidEscape = options && options.avoidEscape === true; + + // deprecated + if (options === AVOID_ESCAPE) { + avoidEscape = true; + } + + /** + * Determines if a given node is part of JSX syntax. + * + * This function returns `true` in the following cases: + * + * - `
` ... If the literal is an attribute value, the parent of the literal is `JSXAttribute`. + * - `
foo
` ... If the literal is a text content, the parent of the literal is `JSXElement`. + * - `<>foo` ... If the literal is a text content, the parent of the literal is `JSXFragment`. + * + * In particular, this function returns `false` in the following cases: + * + * - `
` + * - `
{"foo"}
` + * + * In both cases, inside of the braces is handled as normal JavaScript. + * The braces are `JSXExpressionContainer` nodes. + * @param {ASTNode} node The Literal node to check. + * @returns {boolean} True if the node is a part of JSX, false if not. + * @private + */ + function isJSXLiteral(node) { + return node.parent.type === "JSXAttribute" || node.parent.type === "JSXElement" || node.parent.type === "JSXFragment"; + } + + /** + * Checks whether or not a given node is a directive. + * The directive is a `ExpressionStatement` which has only a string literal. + * @param {ASTNode} node A node to check. + * @returns {boolean} Whether or not the node is a directive. + * @private + */ + function isDirective(node) { + return ( + node.type === "ExpressionStatement" && + node.expression.type === "Literal" && + typeof node.expression.value === "string" + ); + } + + /** + * Checks whether or not a given node is a part of directive prologues. + * See also: http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive + * @param {ASTNode} node A node to check. + * @returns {boolean} Whether or not the node is a part of directive prologues. + * @private + */ + function isPartOfDirectivePrologue(node) { + const block = node.parent.parent; + + if (block.type !== "Program" && (block.type !== "BlockStatement" || !astUtils.isFunction(block.parent))) { + return false; + } + + // Check the node is at a prologue. + for (let i = 0; i < block.body.length; ++i) { + const statement = block.body[i]; + + if (statement === node.parent) { + return true; + } + if (!isDirective(statement)) { + break; + } + } + + return false; + } + + /** + * Checks whether or not a given node is allowed as non backtick. + * @param {ASTNode} node A node to check. + * @returns {boolean} Whether or not the node is allowed as non backtick. + * @private + */ + function isAllowedAsNonBacktick(node) { + const parent = node.parent; + + switch (parent.type) { + + // Directive Prologues. + case "ExpressionStatement": + return isPartOfDirectivePrologue(node); + + // LiteralPropertyName. + case "Property": + case "MethodDefinition": + return parent.key === node && !parent.computed; + + // ModuleSpecifier. + case "ImportDeclaration": + case "ExportNamedDeclaration": + case "ExportAllDeclaration": + return parent.source === node; + + // Others don't allow. + default: + return false; + } + } + + /** + * Checks whether or not a given TemplateLiteral node is actually using any of the special features provided by template literal strings. + * @param {ASTNode} node A TemplateLiteral node to check. + * @returns {boolean} Whether or not the TemplateLiteral node is using any of the special features provided by template literal strings. + * @private + */ + function isUsingFeatureOfTemplateLiteral(node) { + const hasTag = node.parent.type === "TaggedTemplateExpression" && node === node.parent.quasi; + + if (hasTag) { + return true; + } + + const hasStringInterpolation = node.expressions.length > 0; + + if (hasStringInterpolation) { + return true; + } + + const isMultilineString = node.quasis.length >= 1 && UNESCAPED_LINEBREAK_PATTERN.test(node.quasis[0].value.raw); + + if (isMultilineString) { + return true; + } + + return false; + } + + return { + + Literal(node) { + const val = node.value, + rawVal = node.raw; + + if (settings && typeof val === "string") { + let isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) || + isJSXLiteral(node) || + astUtils.isSurroundedBy(rawVal, settings.quote); + + if (!isValid && avoidEscape) { + isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0; + } + + if (!isValid) { + context.report({ + node, + messageId: "wrongQuotes", + data: { + description: settings.description + }, + fix(fixer) { + if (quoteOption === "backtick" && astUtils.hasOctalEscapeSequence(rawVal)) { + + // An octal escape sequence in a template literal would produce syntax error, even in non-strict mode. + return null; + } + + return fixer.replaceText(node, settings.convert(node.raw)); + } + }); + } + } + }, + + TemplateLiteral(node) { + // Don't throw an error if backticks are expected or a template literal feature is in use. + if ( + allowTemplateLiterals || + quoteOption === "backtick" || + isUsingFeatureOfTemplateLiteral(node) || + settings && avoidEscape && node.quasis[0].value.raw.indexOf(settings.quote) >= 0 + ) { + return; + } + + context.report({ + node, + messageId: "wrongQuotes", + data: { + description: settings.description + }, + fix(fixer) { + if (isPartOfDirectivePrologue(node)) { + + /* + * TemplateLiterals in a directive prologue aren't actually directives, but if they're + * in the directive prologue, then fixing them might turn them into directives and change + * the behavior of the code. + */ + return null; + } + return fixer.replaceText(node, settings.convert(sourceCode.getText(node))); + } + }); + } + }; + + } +}; diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..73ed104a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +node_modules +lib/protocol/crypto/poly1305.js +.eslint-plugins +!.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..135fbb27 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,212 @@ +'use strict'; + +/* eslint-env node */ + +const Module = require('module'); +const path = require('path'); + +const ModuleFindPath = Module._findPath; +const hacks = [ + 'eslint-plugin-mscdex', +]; +const eslintRulesPath = + path.join(path.dirname(process.mainModule.filename), '..', 'lib', 'rules'); +Module._findPath = (request, paths, isMain) => { + const r = ModuleFindPath(request, paths.concat(eslintRulesPath), isMain); + if (!r) { + if (hacks.includes(request)) + return path.join(__dirname, '.eslint-plugins', request); + } + return r; +}; + +module.exports = { + root: true, + env: { node: true, es6: true }, + plugins: ['mscdex'], + parserOptions: { sourceType: 'script', ecmaVersion: '2020' }, + rules: { + // ESLint built-in rules + // https://eslint.org/docs/rules/ + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'arrow-parens': ['error', 'always'], + 'arrow-spacing': ['error', { before: true, after: true }], + 'block-scoped-var': 'error', + 'block-spacing': 'error', + 'brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'capitalized-comments': ['error', 'always', { + line: { + // Ignore all lines that have less characters than 20 and all lines that + // start with something that looks like a variable name or code. + // eslint-disable-next-line max-len + ignorePattern: '.{0,20}$|[a-z]+ ?[0-9A-Z_.(/=:[#-]|std|http|ssh|ftp|(let|var|const) [a-z_A-Z0-9]+ =|[b-z] |[a-z]*[0-9].* ', + ignoreInlineComments: true, + ignoreConsecutiveComments: true, + }, + block: { + ignorePattern: '.*', + }, + }], + 'comma-dangle': ['error', 'only-multiline'], + 'comma-spacing': 'error', + 'comma-style': 'error', + 'computed-property-spacing': 'error', + 'constructor-super': 'error', + 'default-case-last': 'error', + 'dot-location': ['error', 'property'], + 'dot-notation': 'error', + 'eol-last': 'error', + 'eqeqeq': ['error', 'smart'], + 'for-direction': 'error', + 'func-call-spacing': 'error', + 'func-name-matching': 'error', + 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], + 'getter-return': 'error', + 'key-spacing': ['error', { mode: 'strict' }], + 'keyword-spacing': 'error', + 'linebreak-style': ['error', 'unix'], + 'max-len': ['error', { + code: 80, + ignorePattern: '^// Flags:', + ignoreRegExpLiterals: true, + ignoreUrls: true, + tabWidth: 2, + }], + 'new-parens': 'error', + 'no-async-promise-executor': 'error', + 'no-class-assign': 'error', + 'no-confusing-arrow': 'error', + 'no-const-assign': 'error', + 'no-constructor-return': 'error', + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-delete-var': 'error', + 'no-dupe-args': 'error', + 'no-dupe-class-members': 'error', + 'no-dupe-keys': 'error', + 'no-dupe-else-if': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-else-return': ['error', { allowElseIf: true }], + 'no-empty-character-class': 'error', + 'no-ex-assign': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-parens': ['error', 'functions'], + 'no-extra-semi': 'error', + 'no-fallthrough': 'error', + 'no-func-assign': 'error', + 'no-global-assign': 'error', + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-lonely-if': 'error', + 'no-misleading-character-class': 'error', + 'no-mixed-requires': 'error', + 'no-mixed-spaces-and-tabs': 'error', + 'no-multi-spaces': ['error', { ignoreEOLComments: true }], + 'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 0, maxBOF: 0 }], + 'no-new-require': 'error', + 'no-new-symbol': 'error', + 'no-obj-calls': 'error', + 'no-octal': 'error', + 'no-path-concat': 'error', + 'no-proto': 'error', + 'no-redeclare': 'error', + /* eslint-disable max-len */ + 'no-restricted-syntax': [ + 'error', + { + selector: "CallExpression[callee.name='setTimeout'][arguments.length<2]", + message: '`setTimeout()` must be invoked with at least two arguments.', + }, + { + selector: "CallExpression[callee.name='setInterval'][arguments.length<2]", + message: '`setInterval()` must be invoked with at least two arguments.', + }, + { + selector: 'ThrowStatement > CallExpression[callee.name=/Error$/]', + message: 'Use `new` keyword when throwing an `Error`.', + } + ], + /* eslint-enable max-len */ + 'no-return-await': 'error', + 'no-self-assign': 'error', + 'no-self-compare': 'error', + 'no-setter-return': 'error', + 'no-shadow-restricted-names': 'error', + 'no-tabs': 'error', + 'no-template-curly-in-string': 'error', + 'no-this-before-super': 'error', + 'no-throw-literal': 'error', + 'no-trailing-spaces': 'error', + 'no-undef': ['error', { typeof: true }], + 'no-undef-init': 'error', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-unsafe-negation': 'error', + 'no-unused-labels': 'error', + 'no-unused-vars': ['error', { args: 'none', caughtErrors: 'all' }], + 'no-use-before-define': ['error', { + classes: true, + functions: false, + variables: false, + }], + 'no-useless-backreference': 'error', + 'no-useless-call': 'error', + 'no-useless-catch': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-escape': 'error', + 'no-useless-return': 'error', + 'no-var': 'error', + 'no-void': 'error', + 'no-whitespace-before-property': 'error', + 'no-with': 'error', + 'object-curly-spacing': ['error', 'always'], + 'one-var': ['error', { initialized: 'never' }], + 'one-var-declaration-per-line': 'error', + 'operator-linebreak': ['error', 'before', { overrides: { '=': 'after' } }], + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: 'function', next: 'function' }, + ], + 'prefer-const': ['error', { ignoreReadBeforeAssign: true }], + 'quote-props': ['error', 'consistent'], + 'rest-spread-spacing': 'error', + 'semi': 'error', + 'semi-spacing': 'error', + 'space-before-blocks': ['error', 'always'], + 'space-before-function-paren': ['error', { + anonymous: 'never', + named: 'never', + asyncArrow: 'always', + }], + 'space-in-parens': ['error', 'never'], + 'space-infix-ops': 'error', + 'space-unary-ops': 'error', + 'spaced-comment': ['error', 'always', { + 'block': { 'balanced': true }, + 'exceptions': ['-'], + }], + 'strict': ['error', 'global'], + 'symbol-description': 'error', + 'template-curly-spacing': 'error', + 'unicode-bom': 'error', + 'use-isnan': 'error', + 'valid-typeof': 'error', + + // Custom rules + 'mscdex/curly': ['error', 'multi-or-nest', 'consistent'], + 'mscdex/quotes': ['error', 'single', { avoidEscape: true }], + }, + globals: { + Atomics: 'readable', + BigInt: 'readable', + BigInt64Array: 'readable', + BigUint64Array: 'readable', + TextEncoder: 'readable', + TextDecoder: 'readable', + globalThis: 'readable', + }, +}; diff --git a/.travis.yml b/.travis.yml index 3d549631..f7cbeb2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,9 @@ notifications: email: false env: matrix: - - TRAVIS_NODE_VERSION="6" - - TRAVIS_NODE_VERSION="8" - TRAVIS_NODE_VERSION="10" - TRAVIS_NODE_VERSION="12" - - TRAVIS_NODE_VERSION="13" + - TRAVIS_NODE_VERSION="14" install: - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION - node --version diff --git a/README.md b/README.md index c048fb88..d94a2c12 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ +# WARNING: This documentation is for an upcoming, unreleased version of `ssh2`. You are probably looking for documentation for `ssh2` v0.8.x [here](https://github.com/mscdex/ssh2/blob/v0.8.x/README.md) + # Description SSH2 client and server modules written in pure JavaScript for [node.js](http://nodejs.org/). -Development/testing is done against OpenSSH (7.6 currently). +Development/testing is done against OpenSSH (8.0 currently). [![Build Status](https://travis-ci.org/mscdex/ssh2.svg?branch=master)](https://travis-ci.org/mscdex/ssh2) @@ -11,7 +13,7 @@ Development/testing is done against OpenSSH (7.6 currently). * [Requirements](#requirements) * [Installation](#installation) * [Client Examples](#client-examples) - * [Execute `uptime` on a server](#execute-uptime-on-a-server) + * [Execute 'uptime' on a server](#execute-uptime-on-a-server) * [Start an interactive shell session](#start-an-interactive-shell-session) * [Send a raw HTTP request to port 80 on the server](#send-a-raw-http-request-to-port-80-on-the-server) * [Forward local connections to port 8000 on the server to us](#forward-local-connections-to-port-8000-on-the-server-to-us) @@ -39,10 +41,12 @@ Development/testing is done against OpenSSH (7.6 currently). * [Terminal modes](#terminal-modes) * [HTTPAgent](#httpagent) * [HTTPAgent methods](#httpagent-methods) + * [HTTPSAgent](#httpsagent) + * [HTTPSAgent methods](#httpsagent-methods) ## Requirements -* [node.js](http://nodejs.org/) -- v5.10.0 or newer +* [node.js](http://nodejs.org/) -- v10.16.0 or newer * node v12.0.0 or newer for Ed25519 key support ## Installation @@ -51,22 +55,24 @@ Development/testing is done against OpenSSH (7.6 currently). ## Client Examples -### Execute `uptime` on a server +### Execute 'uptime' on a server ```js -var Client = require('ssh2').Client; +const { readFileSync } = require('fs'); + +const { Client } = require('ssh2'); -var conn = new Client(); -conn.on('ready', function() { +const conn = new Client(); +conn.on('ready', () => { console.log('Client :: ready'); - conn.exec('uptime', function(err, stream) { + conn.exec('uptime', (err, stream) => { if (err) throw err; - stream.on('close', function(code, signal) { + stream.on('close', (code, signal) => { console.log('Stream :: close :: code: ' + code + ', signal: ' + signal); conn.end(); - }).on('data', function(data) { + }).on('data', (data) => { console.log('STDOUT: ' + data); - }).stderr.on('data', function(data) { + }).stderr.on('data', (data) => { console.log('STDERR: ' + data); }); }); @@ -74,7 +80,7 @@ conn.on('ready', function() { host: '192.168.100.100', port: 22, username: 'frylock', - privateKey: require('fs').readFileSync('/here/is/my/key') + privateKey: readFileSync('/path/to/my/key') }); // example output: @@ -88,17 +94,19 @@ conn.on('ready', function() { ### Start an interactive shell session ```js -var Client = require('ssh2').Client; +const { readFileSync } = require('fs'); -var conn = new Client(); -conn.on('ready', function() { +const { Client } = require('ssh2'); + +const conn = new Client(); +conn.on('ready', () => { console.log('Client :: ready'); - conn.shell(function(err, stream) { + conn.shell((err, stream) => { if (err) throw err; - stream.on('close', function() { + stream.on('close', () => { console.log('Stream :: close'); conn.end(); - }).on('data', function(data) { + }).on('data', (data) => { console.log('OUTPUT: ' + data); }); stream.end('ls -l\nexit\n'); @@ -107,7 +115,7 @@ conn.on('ready', function() { host: '192.168.100.100', port: 22, username: 'frylock', - privateKey: require('fs').readFileSync('/here/is/my/key') + privateKey: readFileSync('/path/to/my/key') }); // example output: @@ -135,17 +143,17 @@ conn.on('ready', function() { ### Send a raw HTTP request to port 80 on the server ```js -var Client = require('ssh2').Client; +const { Client } = require('ssh2'); -var conn = new Client(); -conn.on('ready', function() { +const conn = new Client(); +conn.on('ready', () => { console.log('Client :: ready'); - conn.forwardOut('192.168.100.102', 8000, '127.0.0.1', 80, function(err, stream) { + conn.forwardOut('192.168.100.102', 8000, '127.0.0.1', 80, (err, stream) => { if (err) throw err; - stream.on('close', function() { + stream.on('close', () => { console.log('TCP :: CLOSED'); conn.end(); - }).on('data', function(data) { + }).on('data', (data) => { console.log('TCP :: DATA: ' + data); }).end([ 'HEAD / HTTP/1.1', @@ -183,21 +191,21 @@ conn.on('ready', function() { ### Forward local connections to port 8000 on the server to us ```js -var Client = require('ssh2').Client; +const { Client } = require('ssh2'); -var conn = new Client(); -conn.on('ready', function() { +const conn = new Client(); +conn.on('ready', () => { console.log('Client :: ready'); - conn.forwardIn('127.0.0.1', 8000, function(err) { + conn.forwardIn('127.0.0.1', 8000, (err) => { if (err) throw err; console.log('Listening for connections on server on port 8000!'); }); -}).on('tcp connection', function(info, accept, reject) { +}).on('tcp connection', (info, accept, reject) => { console.log('TCP :: INCOMING CONNECTION:'); console.dir(info); - accept().on('close', function() { + accept().on('close', () => { console.log('TCP :: CLOSED'); - }).on('data', function(data) { + }).on('data', (data) => { console.log('TCP :: DATA: ' + data); }).end([ 'HTTP/1.1 404 Not Found', @@ -235,14 +243,14 @@ conn.on('ready', function() { ### Get a directory listing via SFTP ```js -var Client = require('ssh2').Client; +const { Client } = require('ssh2'); -var conn = new Client(); -conn.on('ready', function() { +const conn = new Client(); +conn.on('ready', () => { console.log('Client :: ready'); - conn.sftp(function(err, sftp) { + conn.sftp((err, sftp) => { if (err) throw err; - sftp.readdir('foo', function(err, list) { + sftp.readdir('foo', (err, list) => { if (err) throw err; console.dir(list); conn.end(); @@ -280,18 +288,18 @@ conn.on('ready', function() { ### Connection hopping ```js -var Client = require('ssh2').Client; +const { Client } = require('ssh2'); -var conn1 = new Client(); -var conn2 = new Client(); +const conn1 = new Client(); +const conn2 = new Client(); // Checks uptime on 10.1.1.40 via 192.168.1.1 -conn1.on('ready', function() { +conn1.on('ready', () => { console.log('FIRST :: connection ready'); - // Alternatively, you could use netcat or socat with exec() instead of - // forwardOut() - conn1.forwardOut('127.0.0.1', 12345, '10.1.1.40', 22, function(err, stream) { + // Alternatively, you could use something like netcat or socat with exec() + // instead of forwardOut(), depending on what the server allows + conn1.forwardOut('127.0.0.1', 12345, '10.1.1.40', 22, (err, stream) => { if (err) { console.log('FIRST :: forwardOut error: ' + err); return conn1.end(); @@ -308,16 +316,18 @@ conn1.on('ready', function() { password: 'password1', }); -conn2.on('ready', function() { +conn2.on('ready', () => { + // This connection is the one to 10.1.1.40 + console.log('SECOND :: connection ready'); - conn2.exec('uptime', function(err, stream) { + conn2.exec('uptime', (err, stream) => { if (err) { console.log('SECOND :: exec error: ' + err); return conn1.end(); } - stream.on('end', function() { + stream.on('close', () => { conn1.end(); // close parent (and this) connection - }).on('data', function(data) { + }).on('data', (data) => { console.log(data.toString()); }); }); @@ -327,31 +337,31 @@ conn2.on('ready', function() { ### Forward remote X11 connections ```js -var net = require('net'); +const { Socket } = require('net'); -var Client = require('ssh2').Client; +const { Client } = require('ssh2'); -var conn = new Client(); +const conn = new Client(); -conn.on('x11', function(info, accept, reject) { - var xserversock = new net.Socket(); - xserversock.on('connect', function() { - var xclientsock = accept(); +conn.on('x11', (info, accept, reject) => { + const xserversock = new net.Socket(); + xserversock.on('connect', () => { + const xclientsock = accept(); xclientsock.pipe(xserversock).pipe(xclientsock); }); // connects to localhost:0.0 xserversock.connect(6000, 'localhost'); }); -conn.on('ready', function() { - conn.exec('xeyes', { x11: true }, function(err, stream) { +conn.on('ready', () => { + conn.exec('xeyes', { x11: true }, (err, stream) => { if (err) throw err; - var code = 0; - stream.on('end', function() { + let code = 0; + stream.on('close', () => { if (code !== 0) console.log('Do you have X11 forwarding enabled on your SSH server?'); conn.end(); - }).on('exit', function(exitcode) { + }).on('exit', (exitcode) => { code = exitcode; }); }); @@ -365,44 +375,45 @@ conn.on('ready', function() { ### Dynamic (1:1) port forwarding using a SOCKSv5 proxy (using [socksv5](https://github.com/mscdex/socksv5)) ```js -var socks = require('socksv5'); -var Client = require('ssh2').Client; +const socks = require('socksv5'); +const { Client } = require('ssh2'); -var ssh_config = { +const sshConfig = { host: '192.168.100.1', port: 22, username: 'nodejs', password: 'rules' }; -socks.createServer(function(info, accept, deny) { +socks.createServer((info, accept, deny) => { // NOTE: you could just use one ssh2 client connection for all forwards, but // you could run into server-imposed limits if you have too many forwards open // at any given time - var conn = new Client(); - conn.on('ready', function() { + const conn = new Client(); + conn.on('ready', () => { conn.forwardOut(info.srcAddr, info.srcPort, info.dstAddr, info.dstPort, - function(err, stream) { + (err, stream) => { if (err) { conn.end(); return deny(); } - var clientSocket; - if (clientSocket = accept(true)) { - stream.pipe(clientSocket).pipe(stream).on('close', function() { + const clientSocket = accept(true); + if (clientSocket) { + stream.pipe(clientSocket).pipe(stream).on('close', () => { conn.end(); }); - } else + } else { conn.end(); + } }); - }).on('error', function(err) { + }).on('error', (err) => { deny(); - }).connect(ssh_config); -}).listen(1080, 'localhost', function() { + }).connect(sshConfig); +}).listen(1080, 'localhost', () => { console.log('SOCKSv5 proxy server started on port 1080'); }).useAuth(socks.auth.None()); @@ -413,19 +424,19 @@ socks.createServer(function(info, accept, deny) { ### Make HTTP(S) connections easily using a custom http(s).Agent ```js -var http = require('http'); +const http = require('http'); -var HTTPAgent = require('ssh2').HTTPAgent; -var Client = require('ssh2').Client; +const { Client, HTTPAgent, HTTPSAgent } = require('ssh2'); -var ssh_config = { +const sshConfig = { host: '192.168.100.1', port: 22, username: 'nodejs', password: 'rules' }; -var agent = new HTTPAgent(ssh_config); +// Use `HTTPSAgent` instead for an HTTPS request +const agent = new HTTPAgent(sshConfig); http.get({ host: '192.168.200.1', agent, @@ -441,21 +452,23 @@ http.get({ ### Invoke an arbitrary subsystem ```js -var Client = require('ssh2').Client; -var xmlhello = '' + - '' + - ' ' + - ' urn:ietf:params:netconf:base:1.0' + - ' ' + - ']]>]]>'; +const { Client } = require('ssh2'); + +const xmlhello = ` + + + + urn:ietf:params:netconf:base:1.0 + + ]]>]]>`; -var conn = new Client(); +const conn = new Client(); -conn.on('ready', function() { +conn.on('ready', () => { console.log('Client :: ready'); - conn.subsys('netconf', function(err, stream) { + conn.subsys('netconf', (err, stream) => { if (err) throw err; - stream.on('data', function(data) { + stream.on('data', (data) => { console.log(data); }).write(xmlhello); }); @@ -472,42 +485,45 @@ conn.on('ready', function() { ### Password and public key authentication and non-interactive (exec) command execution ```js -var fs = require('fs'); -var crypto = require('crypto'); -var inspect = require('util').inspect; - -var ssh2 = require('ssh2'); -var utils = ssh2.utils; - -var allowedUser = Buffer.from('foo'); -var allowedPassword = Buffer.from('bar'); -var allowedPubKey = utils.parseKey(fs.readFileSync('foo.pub')); - -new ssh2.Server({ - hostKeys: [fs.readFileSync('host.key')] -}, function(client) { +const { timingSafeEqual } = require('crypto'); +const { readFileSync } = require('fs'); +const { inspect } = require('util'); + +const { parseKey, Server } = require('ssh2'); + +const allowedUser = Buffer.from('foo'); +const allowedPassword = Buffer.from('bar'); +const allowedPubKey = parseKey(readFileSync('foo.pub')); + +function checkValue(input, allowed) { + const autoReject = (input.length !== allowed.length); + if (autoReject) { + // Prevent leaking length information by always making a comparison with the + // same input when lengths don't match what we expect ... + allowed = input; + } + const isMatch = timingSafeEqual(input, allowed); + return (!autoReject && isMatch); +} + +new Server({ + hostKeys: [readFileSync('host.key')] +}, (client) => { console.log('Client connected!'); - client.on('authentication', function(ctx) { - var user = Buffer.from(ctx.username); - if (user.length !== allowedUser.length - || !crypto.timingSafeEqual(user, allowedUser)) { - return ctx.reject(); - } + client.on('authentication', (ctx) => { + let allowed = true; + if (!checkValue(Buffer.from(ctx.username), allowedUser)) + allowed = false; switch (ctx.method) { case 'password': - var password = Buffer.from(ctx.password); - if (password.length !== allowedPassword.length - || !crypto.timingSafeEqual(password, allowedPassword)) { + if (!checkValue(Buffer.from(ctx.password), allowedPassword)) return ctx.reject(); - } break; case 'publickey': - var allowedPubSSHKey = allowedPubKey.getPublicSSH(); if (ctx.key.algo !== allowedPubKey.type - || ctx.key.data.length !== allowedPubSSHKey.length - || !crypto.timingSafeEqual(ctx.key.data, allowedPubSSHKey) + || !checkValue(ctx.key.data, allowedPubKey.getPublicSSH()) || (ctx.signature && allowedPubKey.verify(ctx.blob, ctx.signature) !== true)) { return ctx.reject(); } @@ -516,22 +532,25 @@ new ssh2.Server({ return ctx.reject(); } - ctx.accept(); - }).on('ready', function() { + if (allowed) + ctx.accept(); + else + ctx.reject(); + }).on('ready', () => { console.log('Client authenticated!'); - client.on('session', function(accept, reject) { - var session = accept(); - session.once('exec', function(accept, reject, info) { + client.on('session', (accept, reject) => { + const session = accept(); + session.once('exec', (accept, reject, info) => { console.log('Client wants to execute: ' + inspect(info.command)); - var stream = accept(); + const stream = accept(); stream.stderr.write('Oh no, the dreaded errors!\n'); stream.write('Just kidding about the errors!\n'); stream.exit(0); stream.end(); }); }); - }).on('end', function() { + }).on('close', () => { console.log('Client disconnected'); }); }).listen(0, '127.0.0.1', function() { @@ -542,83 +561,107 @@ new ssh2.Server({ ### SFTP-only server ```js -var fs = require('fs'); -var crypto = require('crypto'); - -var ssh2 = require('ssh2'); -var OPEN_MODE = ssh2.SFTP_OPEN_MODE; -var STATUS_CODE = ssh2.SFTP_STATUS_CODE; - -var allowedUser = Buffer.from('foo'); -var allowedPassword = Buffer.from('bar'); +const { timingSafeEqual } = require('crypto'); +const { readFileSync } = require('fs'); +const { inspect } = require('util'); + +const { + Server, + sftp: { + OPEN_MODE, + STATUS_CODE, + }, +} = require('ssh2'); + +const allowedUser = Buffer.from('foo'); +const allowedPassword = Buffer.from('bar'); + +function checkValue(input, allowed) { + const autoReject = (input.length !== allowed.length); + if (autoReject) { + // Prevent leaking length information by always making a comparison with the + // same input when lengths don't match what we expect ... + allowed = input; + } + const isMatch = timingSafeEqual(input, allowed); + return (!autoReject && isMatch); +} + +// This simple SFTP server implements file uploading where the contents get +// ignored ... new ssh2.Server({ - hostKeys: [fs.readFileSync('host.key')] -}, function(client) { + hostKeys: [readFileSync('host.key')] +}, (client) => { console.log('Client connected!'); - client.on('authentication', function(ctx) { - var user = Buffer.from(ctx.username); - if (user.length !== allowedUser.length - || !crypto.timingSafeEqual(user, allowedUser)) { - return ctx.reject(); - } + client.on('authentication', (ctx) => { + let allowed = true; + if (!checkValue(Buffer.from(ctx.username), allowedUser)) + allowed = false; switch (ctx.method) { case 'password': - var password = Buffer.from(ctx.password); - if (password.length !== allowedPassword.length - || !crypto.timingSafeEqual(password, allowedPassword)) { + if (!checkValue(Buffer.from(ctx.password), allowedPassword)) return ctx.reject(); - } break; default: return ctx.reject(); } - ctx.accept(); - }).on('ready', function() { + if (allowed) + ctx.accept(); + else + ctx.reject(); + }).on('ready', () => { console.log('Client authenticated!'); - client.on('session', function(accept, reject) { - var session = accept(); - session.on('sftp', function(accept, reject) { + client.on('session', (accept, reject) => { + const session = accept(); + session.on('sftp', (accept, reject) => { console.log('Client SFTP session'); - var openFiles = {}; - var handleCount = 0; - // `sftpStream` is an `SFTPStream` instance in server mode - // see: https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md - var sftpStream = accept(); - sftpStream.on('OPEN', function(reqid, filename, flags, attrs) { - // only allow opening /tmp/foo.txt for writing + const openFiles = new Map(); + let handleCount = 0; + const sftp = accept(); + sftp.on('OPEN', (reqid, filename, flags, attrs) => { + // Only allow opening /tmp/foo.txt for writing if (filename !== '/tmp/foo.txt' || !(flags & OPEN_MODE.WRITE)) - return sftpStream.status(reqid, STATUS_CODE.FAILURE); - // create a fake handle to return to the client, this could easily + return sftp.status(reqid, STATUS_CODE.FAILURE); + + // Create a fake handle to return to the client, this could easily // be a real file descriptor number for example if actually opening - // the file on the disk - var handle = new Buffer(4); - openFiles[handleCount] = true; - handle.writeUInt32BE(handleCount++, 0, true); - sftpStream.handle(reqid, handle); + // a file on disk + const handle = Buffer.alloc(4); + openFiles.set(handleCount, true); + handle.writeUInt32BE(handleCount++, 0); + console.log('Opening file for write') - }).on('WRITE', function(reqid, handle, offset, data) { - if (handle.length !== 4 || !openFiles[handle.readUInt32BE(0, true)]) - return sftpStream.status(reqid, STATUS_CODE.FAILURE); - // fake the write - sftpStream.status(reqid, STATUS_CODE.OK); - var inspected = require('util').inspect(data); - console.log('Write to file at offset %d: %s', offset, inspected); - }).on('CLOSE', function(reqid, handle) { - var fnum; - if (handle.length !== 4 || !openFiles[(fnum = handle.readUInt32BE(0, true))]) - return sftpStream.status(reqid, STATUS_CODE.FAILURE); - delete openFiles[fnum]; - sftpStream.status(reqid, STATUS_CODE.OK); + sftp.handle(reqid, handle); + }).on('WRITE', (reqid, handle, offset, data) => { + if (handle.length !== 4 + || !openFiles.has(handle.readUInt32BE(0))) { + return sftp.status(reqid, STATUS_CODE.FAILURE); + } + + // Fake the write operation + sftp.status(reqid, STATUS_CODE.OK); + + console.log('Write to file at offset ${offset}: ${inspect(data)}'); + }).on('CLOSE', (reqid, handle) => { + let fnum; + if (handle.length !== 4 + || !openFiles.has(fnum = handle.readUInt32BE(0))) { + return sftp.status(reqid, STATUS_CODE.FAILURE); + } + console.log('Closing file'); + openFiles.delete(fnum); + + sftp.status(reqid, STATUS_CODE.OK); }); }); }); - }).on('end', function() { + }).on('close', () => { console.log('Client disconnected'); }); }).listen(0, '127.0.0.1', function() { @@ -630,20 +673,16 @@ You can find more examples in the `examples` directory of this repository. ## API -`require('ssh2').Client` returns a **_Client_** constructor. +`require('ssh2').Client` returns the **_Client_** constructor. -`require('ssh2').Server` returns a **_Server_** constructor. +`require('ssh2').Server` returns the **_Server_** constructor. -`require('ssh2').utils` returns the [utility methods from `ssh2-streams`](https://github.com/mscdex/ssh2-streams#utility-methods). +`require('ssh2').utils` returns an object containing some useful [utilities](#utilities). `require('ssh2').HTTPAgent` returns an [`http.Agent`](https://nodejs.org/docs/latest/api/http.html#http_class_http_agent) constructor. `require('ssh2').HTTPSAgent` returns an [`https.Agent`](https://nodejs.org/docs/latest/api/https.html#https_class_https_agent) constructor. Its API is the same as `HTTPAgent` except it's for HTTPS connections. -`require('ssh2').SFTP_STATUS_CODE` returns the [`SFTPStream.STATUS_CODE` from `ssh2-streams`](https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md#sftpstream-static-constants). - -`require('ssh2').SFTP_OPEN_MODE` returns the [`SFTPStream.OPEN_MODE` from `ssh2-streams`](https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md#sftpstream-static-constants). - ### Client #### Client events @@ -676,7 +715,29 @@ You can find more examples in the `examples` directory of this repository. * **change password**(< _string_ >message, < _string_ >language, < _function_ >done) - If using password-based user authentication, the server has requested that the user's password be changed. Call `done` with the new password. -* **continue**() - Emitted when more requests/data can be sent to the server (after a `Client` method returned `false`). +* **handshake**(< _object_ >negotiated) - Emitted when a handshake has completed (either initial or rekey). `negotiated` contains the negotiated details of the handshake and is of the form: + +```js + // In this particular case `mac` is empty because there is no separate MAC + // because it's integrated into AES in GCM mode + { kex: 'ecdh-sha2-nistp256', + srvHostKey: 'rsa-sha2-512', + cs: { // Client to server algorithms + cipher: 'aes128-gcm', + mac: '', + compress: 'none', + lang: '' + }, + sc: { // Server to client algorithms + cipher: 'aes128-gcm', + mac: '', + compress: 'none', + lang: '' + } + } +``` + +* **rekey**() - Emitted when a rekeying operation has completed (either client or server-initiated). * **error**(< _Error_ >err) - An error occurred. A 'level' property indicates 'client-socket' for socket-level errors and 'client-ssh' for SSH disconnection messages. In the case of 'client-ssh' messages, there may be a 'description' property that provides more detail. @@ -702,7 +763,7 @@ You can find more examples in the `examples` directory of this repository. * **forceIPv6** - _boolean_ - Only connect via resolved IPv6 address for `host`. **Default:** `false` - * **hostHash** - _string_ - Any valid hash algorithm supported by node. The host's key is hashed using this algorithm and passed to the **hostVerifier** function. **Default:** (none) + * **hostHash** - _string_ - Any valid hash algorithm supported by node. The host's key is hashed using this algorithm and passed to the **hostVerifier** function as a hex string. **Default:** (none) * **hostVerifier** - _function_ - Function with parameters `(hashedKey[, callback])` where `hashedKey` is a string hex hash of the host's key for verification purposes. Return `true` to continue with the handshake or `false` to reject and disconnect, or call `callback()` with `true` or `false` if you need to perform asynchronous verification. **Default:** (auto-accept if `hostVerifier` is not set) @@ -736,25 +797,86 @@ You can find more examples in the `examples` directory of this repository. * **strictVendor** - _boolean_ - Performs a strict server vendor check before sending vendor-specific requests, etc. (e.g. check for OpenSSH server when using `openssh_noMoreSessions()`) **Default:** `true` - * **algorithms** - _object_ - This option allows you to explicitly override the default transport layer algorithms used for the connection. Each value must be an array of valid algorithms for that category. The order of the algorithms in the arrays are important, with the most favorable being first. For a list of valid and default algorithm names, please review the documentation for the version of `ssh2-streams` used by this module. Valid keys: + * **algorithms** - _object_ - This option allows you to explicitly override the default transport layer algorithms used for the connection. The value for each category must either be an array of valid algorithm names or an object containing `append`, `prepend`, and/or `remove` properties that each contain an _array_ of algorithm names or RegExps to match to adjust default lists for each category. For arrays, the order of the algorithm names matters, with the most favorable being first. Valid keys: + + * **kex** - _mixed_ - Key exchange algorithms. + * Default list (in order from most to least preferrable): + * `curve25519-sha256 (node v14.0.0+)` + * `curve25519-sha256@libssh.org (node v14.0.0+)` + * `ecdh-sha2-nistp256` + * `ecdh-sha2-nistp384` + * `ecdh-sha2-nistp521` + * `diffie-hellman-group-exchange-sha256` + * `diffie-hellman-group14-sha256` + * `diffie-hellman-group15-sha512` + * `diffie-hellman-group16-sha512` + * `diffie-hellman-group17-sha512` + * `diffie-hellman-group18-sha512` + * Other supported names: + * `diffie-hellman-group-exchange-sha1` + * `diffie-hellman-group14-sha1` + * `diffie-hellman-group1-sha1` + + * **serverHostKey** - _mixed_ - Server host key formats. + * Default list (in order from most to least preferrable): + * `ssh-ed25519` (node v12.0.0+) + * `ecdsa-sha2-nistp256` + * `ecdsa-sha2-nistp384` + * `ecdsa-sha2-nistp521` + * `rsa-sha2-512` + * `rsa-sha2-256` + * `ssh-rsa` + * Other supported names: + * `ssh-dss` + + * **cipher** - _mixed_ - Ciphers. + * Default list (in order from most to least preferrable): + * `chacha20-poly1305@openssh.com` (priority of chacha20-poly1305 may vary depending upon CPU and/or optional binding availability) + * `aes128-gcm` + * `aes128-gcm@openssh.com` + * `aes256-gcm` + * `aes256-gcm@openssh.com` + * `aes128-ctr` + * `aes192-ctr` + * `aes256-ctr` + * Other supported names: + * `3des-cbc` + * `aes256-cbc` + * `aes192-cbc` + * `aes128-cbc` + * `arcfour256` + * `arcfour128` + * `arcfour` + * `blowfish-cbc` + * `cast128-cbc` + + * **hmac** - _mixed_ - (H)MAC algorithms. + * Default list (in order from most to least preferrable): + * `hmac-sha2-256-etm@openssh.com` + * `hmac-sha2-512-etm@openssh.com` + * `hmac-sha1-etm@openssh.com` + * `hmac-sha2-256` + * `hmac-sha2-512` + * `hmac-sha1` + * Other supported names: + * hmac-md5 + * hmac-sha2-256-96 + * hmac-sha2-512-96 + * hmac-ripemd160 + * hmac-sha1-96 + * hmac-md5-96 + + * **compress** - _mixed_ - Compression algorithms. + * Default list (in order from most to least preferrable): + * `none` + * `zlib@openssh.com` + * `zlib` + * Other supported names: - * **kex** - _array_ - Key exchange algorithms. - - * **cipher** - _array_ - Ciphers. - - * **serverHostKey** - _array_ - Server host key formats. - - * **hmac** - _array_ - (H)MAC algorithms. - - * **compress** - _array_ - Compression algorithms. - - * **compress** - _mixed_ - Set to `true` to enable compression if server supports it, `'force'` to force compression (disconnecting if server does not support it), or `false` to explicitly opt out of compression all of the time. Note: this setting is overridden when explicitly setting a compression algorithm in the `algorithms` configuration option. **Default:** (only use compression if that is only what the server supports) * **debug** - _function_ - Set this to a function that receives a single string argument to get detailed (local) debug information. **Default:** (none) -**Default authentication method order:** None -> Password -> Private Key -> Agent (-> keyboard-interactive if `tryKeyboard` is `true`) -> Hostbased - -* **exec**(< _string_ >command[, < _object_ >options], < _function_ >callback) - _boolean_ - Executes `command` on the server. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Valid `options` properties are: +* **exec**(< _string_ >command[, < _object_ >options], < _function_ >callback) - _(void)_ - Executes `command` on the server. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Valid `options` properties are: * **env** - _object_ - An environment to use for the execution of the command. @@ -770,9 +892,9 @@ You can find more examples in the `examples` directory of this repository. * **cookie** - _mixed_ - The authentication cookie. Can be a hex _string_ or a _Buffer_ containing the raw cookie value (which will be converted to a hex string). **Default:** (random 16 byte value) -* **shell**([[< _mixed_ >window,] < _object_ >options]< _function_ >callback) - _boolean_ - Starts an interactive shell session on the server, with an optional `window` object containing pseudo-tty settings (see 'Pseudo-TTY settings'). If `window === false`, then no pseudo-tty is allocated. `options` supports the `x11` and `env` options as described in `exec()`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **shell**([[< _mixed_ >window,] < _object_ >options]< _function_ >callback) - _(void)_ - Starts an interactive shell session on the server, with an optional `window` object containing pseudo-tty settings (see 'Pseudo-TTY settings'). If `window === false`, then no pseudo-tty is allocated. `options` supports the `x11` and `env` options as described in `exec()`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. -* **forwardIn**(< _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _boolean_ - Bind to `remoteAddr` on `remotePort` on the server and forward incoming TCP connections. `callback` has 2 parameters: < _Error_ >err, < _integer_ >port (`port` is the assigned port number if `remotePort` was 0). Returns `false` if you should wait for the `continue` event before sending any more traffic. Here are some special values for `remoteAddr` and their associated binding behaviors: +* **forwardIn**(< _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _(void)_ - Bind to `remoteAddr` on `remotePort` on the server and forward incoming TCP connections. `callback` has 2 parameters: < _Error_ >err, < _integer_ >port (`port` is the assigned port number if `remotePort` was 0). Here are some special values for `remoteAddr` and their associated binding behaviors: * '' - Connections are to be accepted on all protocol families supported by the server. @@ -784,23 +906,25 @@ You can find more examples in the `examples` directory of this repository. * '127.0.0.1' and '::1' - Listen on the loopback interfaces for IPv4 and IPv6, respectively. -* **unforwardIn**(< _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _boolean_ - Unbind from `remoteAddr` on `remotePort` on the server and stop forwarding incoming TCP connections. Until `callback` is called, more connections may still come in. `callback` has 1 parameter: < _Error_ >err. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **unforwardIn**(< _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _(void)_ - Unbind from `remoteAddr` on `remotePort` on the server and stop forwarding incoming TCP connections. Until `callback` is called, more connections may still come in. `callback` has 1 parameter: < _Error_ >err. -* **forwardOut**(< _string_ >srcIP, < _integer_ >srcPort, < _string_ >dstIP, < _integer_ >dstPort, < _function_ >callback) - _boolean_ - Open a connection with `srcIP` and `srcPort` as the originating address and port and `dstIP` and `dstPort` as the remote destination address and port. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **forwardOut**(< _string_ >srcIP, < _integer_ >srcPort, < _string_ >dstIP, < _integer_ >dstPort, < _function_ >callback) - _(void)_ - Open a connection with `srcIP` and `srcPort` as the originating address and port and `dstIP` and `dstPort` as the remote destination address and port. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. -* **sftp**(< _function_ >callback) - _boolean_ - Starts an SFTP session. `callback` has 2 parameters: < _Error_ >err, < _SFTPStream_ >sftp. For methods available on `sftp`, see the [`SFTPStream` client documentation](https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md) (except `read()` and `write()` are used instead of `readData()` and `writeData()` respectively, for convenience). Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **sftp**(< _function_ >callback) - _(void)_ - Starts an SFTP session. `callback` has 2 parameters: < _Error_ >err, < _SFTP_ >sftp. For methods available on `sftp`, see the [`SFTP` client documentation](https://github.com/mscdex/ssh2/blob/master/SFTP.md). -* **subsys**(< _string_ >subsystem, < _function_ >callback) - _boolean_ - Invokes `subsystem` on the server. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **subsys**(< _string_ >subsystem, < _function_ >callback) - _(void)_ - Invokes `subsystem` on the server. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. + +* **rekey**([< _function_ >callback]) - _(void)_ - Initiates a rekey with the server. If `callback` is supplied, it is added as a one-time handler for the `rekey` event. * **end**() - _(void)_ - Disconnects the socket. -* **openssh_noMoreSessions**(< _function_ >callback) - _boolean_ - OpenSSH extension that sends a request to reject any new sessions (e.g. exec, shell, sftp, subsys) for this connection. `callback` has 1 parameter: < _Error_ >err. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **openssh_noMoreSessions**(< _function_ >callback) - _(void)_ - OpenSSH extension that sends a request to reject any new sessions (e.g. exec, shell, sftp, subsys) for this connection. `callback` has 1 parameter: < _Error_ >err. -* **openssh_forwardInStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _boolean_ - OpenSSH extension that binds to a UNIX domain socket at `socketPath` on the server and forwards incoming connections. `callback` has 1 parameter: < _Error_ >err. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **openssh_forwardInStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _(void)_ - OpenSSH extension that binds to a UNIX domain socket at `socketPath` on the server and forwards incoming connections. `callback` has 1 parameter: < _Error_ >err. -* **openssh_unforwardInStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _boolean_ - OpenSSH extension that unbinds from a UNIX domain socket at `socketPath` on the server and stops forwarding incoming connections. `callback` has 1 parameter: < _Error_ >err. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **openssh_unforwardInStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _(void)_ - OpenSSH extension that unbinds from a UNIX domain socket at `socketPath` on the server and stops forwarding incoming connections. `callback` has 1 parameter: < _Error_ >err. -* **openssh_forwardOutStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _boolean_ - OpenSSH extension that opens a connection to a UNIX domain socket at `socketPath` on the server. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **openssh_forwardOutStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _(void)_ - OpenSSH extension that opens a connection to a UNIX domain socket at `socketPath` on the server. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. ### Server @@ -843,7 +967,7 @@ You can find more examples in the `examples` directory of this repository. * **hostKeys** - _array_ - An array of either Buffers/strings that contain host private keys or objects in the format of `{ key: , passphrase: }` for encrypted private keys. (**Required**) **Default:** (none) - * **algorithms** - _object_ - This option allows you to explicitly override the default transport layer algorithms used for incoming client connections. Each value must be an array of valid algorithms for that category. The order of the algorithms in the arrays are important, with the most favorable being first. For a list of valid and default algorithm names, please review the documentation for the version of `ssh2-streams` used by this module. Valid keys: + * **algorithms** - _object_ - This option allows you to explicitly override the default transport layer algorithms used for incoming client connections. Each value must be an array of valid algorithms for that category. The order of the algorithms in the arrays are important, with the most favorable being first. For a list of valid and default algorithm names, please review the documentation for the version of `ssh2` used by this module. Valid keys: * **kex** - _array_ - Key exchange algorithms. @@ -891,13 +1015,13 @@ You can find more examples in the `examples` directory of this repository. * **submethods** - _array_ - A list of preferred authentication "sub-methods" sent by the client. This may be used to determine what (if any) prompts to send to the client. - * **prompt**(< _array_ >prompts[, < _string_ >title[, < _string_ >instructions]], < _function_ >callback) - _boolean_ - Send prompts to the client. `prompts` is an array of `{ prompt: 'Prompt text', echo: true }` objects (`prompt` being the prompt text and `echo` indicating whether the client's response to the prompt should be echoed to their display). `callback` is called with `(err, responses)`, where `responses` is an array of string responses matching up to the `prompts`. + * **prompt**(< _array_ >prompts[, < _string_ >title[, < _string_ >instructions]], < _function_ >callback) - _(void)_ - Send prompts to the client. `prompts` is an array of `{ prompt: 'Prompt text', echo: true }` objects (`prompt` being the prompt text and `echo` indicating whether the client's response to the prompt should be echoed to their display). `callback` is called with `(err, responses)`, where `responses` is an array of string responses matching up to the `prompts`. * **ready**() - Emitted when the client has been successfully authenticated. -* **session**(< _function_ >accept, < _function_ >reject) - Emitted when the client has requested a new session. Sessions are used to start interactive shells, execute commands, request X11 forwarding, etc. `accept()` returns a new _Session_ instance. `reject()` Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **session**(< _function_ >accept, < _function_ >reject) - Emitted when the client has requested a new session. Sessions are used to start interactive shells, execute commands, request X11 forwarding, etc. `accept()` returns a new _Session_ instance. -* **tcpip**(< _function_ >accept, < _function_ >reject, < _object_ >info) - Emitted when the client has requested an outbound (TCP) connection. `accept()` returns a new _Channel_ instance representing the connection. `reject()` Returns `false` if you should wait for the `continue` event before sending any more traffic. `info` contains: +* **tcpip**(< _function_ >accept, < _function_ >reject, < _object_ >info) - Emitted when the client has requested an outbound (TCP) connection. `accept()` returns a new _Channel_ instance representing the connection. `info` contains: * **srcIP** - _string_ - Source IP address of outgoing connection. @@ -907,7 +1031,7 @@ You can find more examples in the `examples` directory of this repository. * **destPort** - _string_ - Destination port of outgoing connection. -* **openssh.streamlocal**(< _function_ >accept, < _function_ >reject, < _object_ >info) - Emitted when the client has requested a connection to a UNIX domain socket. `accept()` returns a new _Channel_ instance representing the connection. `reject()` Returns `false` if you should wait for the `continue` event before sending any more traffic. `info` contains: +* **openssh.streamlocal**(< _function_ >accept, < _function_ >reject, < _object_ >info) - Emitted when the client has requested a connection to a UNIX domain socket. `accept()` returns a new _Channel_ instance representing the connection. `info` contains: * **socketPath** - _string_ - Destination socket path of outgoing connection. @@ -923,9 +1047,29 @@ You can find more examples in the `examples` directory of this repository. * **socketPath** - _string_ - The socket path to start/stop binding to. -* **rekey**() - Emitted when the client has finished rekeying (either client or server initiated). +* **handshake**(< _object_ >negotiated) - Emitted when a handshake has completed (either initial or rekey). `negotiated` contains the negotiated details of the handshake and is of the form: -* **continue**() - Emitted when more requests/data can be sent to the client (after a `Connection` method returned `false`). +```js + // In this particular case `mac` is empty because there is no separate MAC + // because it's integrated into AES in GCM mode + { kex: 'ecdh-sha2-nistp256', + srvHostKey: 'rsa-sha2-512', + cs: { // Client to server algorithms + cipher: 'aes128-gcm', + mac: '', + compress: 'none', + lang: '' + }, + sc: { // Server to client algorithms + cipher: 'aes128-gcm', + mac: '', + compress: 'none', + lang: '' + } + } +``` + +* **rekey**() - Emitted when a rekeying operation has completed (either client or server-initiated). * **error**(< _Error_ >err) - An error occurred. @@ -935,19 +1079,19 @@ You can find more examples in the `examples` directory of this repository. #### Connection methods -* **end**() - _boolean_ - Closes the client connection. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **end**() - _(void)_ - Closes the client connection. -* **x11**(< _string_ >originAddr, < _integer_ >originPort, < _function_ >callback) - _boolean_ - Alert the client of an incoming X11 client connection from `originAddr` on port `originPort`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **x11**(< _string_ >originAddr, < _integer_ >originPort, < _function_ >callback) - _(void)_ - Alert the client of an incoming X11 client connection from `originAddr` on port `originPort`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. -* **forwardOut**(< _string_ >boundAddr, < _integer_ >boundPort, < _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _boolean_ - Alert the client of an incoming TCP connection on `boundAddr` on port `boundPort` from `remoteAddr` on port `remotePort`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **forwardOut**(< _string_ >boundAddr, < _integer_ >boundPort, < _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _(void)_ - Alert the client of an incoming TCP connection on `boundAddr` on port `boundPort` from `remoteAddr` on port `remotePort`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. -* **openssh_forwardOutStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _boolean_ - Alert the client of an incoming UNIX domain socket connection on `socketPath`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **openssh_forwardOutStreamLocal**(< _string_ >socketPath, < _function_ >callback) - _(void)_ - Alert the client of an incoming UNIX domain socket connection on `socketPath`. `callback` has 2 parameters: < _Error_ >err, < _Channel_ >stream. -* **rekey**([< _function_ >callback]) - _boolean_ - Initiates a rekeying with the client. If `callback` is supplied, it is added as a one-time handler for the `rekey` event. Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **rekey**([< _function_ >callback]) - _(void)_ - Initiates a rekey with the client. If `callback` is supplied, it is added as a one-time handler for the `rekey` event. #### Session events -* **pty**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client requested allocation of a pseudo-TTY for this session. `accept` and `reject` are functions if the client requested a response and return `false` if you should wait for the `continue` event before sending any more traffic. `info` has these properties: +* **pty**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client requested allocation of a pseudo-TTY for this session. `accept` and `reject` are functions if the client requested a response. `info` has these properties: * **cols** - _integer_ - The number of columns for the pseudo-TTY. @@ -959,7 +1103,7 @@ You can find more examples in the `examples` directory of this repository. * **modes** - _object_ - Contains the requested terminal modes of the pseudo-TTY keyed on the mode name with the value being the mode argument. (See the table at the end for valid names). -* **window-change**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client reported a change in window dimensions during this session. `accept` and `reject` are functions if the client requested a response and return `false` if you should wait for the `continue` event before sending any more traffic. `info` has these properties: +* **window-change**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client reported a change in window dimensions during this session. `accept` and `reject` are functions if the client requested a response. `info` has these properties: * **cols** - _integer_ - The new number of columns for the client window. @@ -969,7 +1113,7 @@ You can find more examples in the `examples` directory of this repository. * **height** - _integer_ - The new height of the client window in pixels. -* **x11**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client requested X11 forwarding. `accept` and `reject` are functions if the client requested a response and return `false` if you should wait for the `continue` event before sending any more traffic. `info` has these properties: +* **x11**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client requested X11 forwarding. `accept` and `reject` are functions if the client requested a response. `info` has these properties: * **single** - _boolean_ - `true` if only a single connection should be forwarded. @@ -979,27 +1123,27 @@ You can find more examples in the `examples` directory of this repository. * **screen** - _integer_ - The screen number to forward X11 connections for. -* **env**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client requested an environment variable to be set for this session. `accept` and `reject` are functions if the client requested a response and return `false` if you should wait for the `continue` event before sending any more traffic. `info` has these properties: +* **env**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client requested an environment variable to be set for this session. `accept` and `reject` are functions if the client requested a response. `info` has these properties: * **key** - _string_ - The environment variable's name. * **value** - _string_ - The environment variable's value. -* **signal**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client has sent a signal. `accept` and `reject` are functions if the client requested a response and return `false` if you should wait for the `continue` event before sending any more traffic. `info` has these properties: +* **signal**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client has sent a signal. `accept` and `reject` are functions if the client requested a response. `info` has these properties: * **name** - _string_ - The signal name (e.g. `SIGUSR1`). -* **auth-agent**(< _mixed_ >accept, < _mixed_ >reject) - The client has requested incoming ssh-agent requests be forwarded to them. `accept` and `reject` are functions if the client requested a response and return `false` if you should wait for the `continue` event before sending any more traffic. +* **auth-agent**(< _mixed_ >accept, < _mixed_ >reject) - The client has requested incoming ssh-agent requests be forwarded to them. `accept` and `reject` are functions if the client requested a response. -* **shell**(< _mixed_ >accept, < _mixed_ >reject) - The client has requested an interactive shell. `accept` and `reject` are functions if the client requested a response. `accept()` returns a _Channel_ for the interactive shell. `reject()` Returns `false` if you should wait for the `continue` event before sending any more traffic. +* **shell**(< _mixed_ >accept, < _mixed_ >reject) - The client has requested an interactive shell. `accept` and `reject` are functions if the client requested a response. `accept()` returns a _Channel_ for the interactive shell. -* **exec**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client has requested execution of a command string. `accept` and `reject` are functions if the client requested a response. `accept()` returns a _Channel_ for the command execution. `reject()` Returns `false` if you should wait for the `continue` event before sending any more traffic. `info` has these properties: +* **exec**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client has requested execution of a command string. `accept` and `reject` are functions if the client requested a response. `accept()` returns a _Channel_ for the command execution. `info` has these properties: * **command** - _string_ - The command line to be executed. -* **sftp**(< _mixed_ >accept, < _mixed_ >reject) - The client has requested the SFTP subsystem. `accept` and `reject` are functions if the client requested a response. `accept()` returns an _SFTPStream_ in server mode (see the [`SFTPStream` documentation](https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md) for details). `reject()` Returns `false` if you should wait for the `continue` event before sending any more traffic. `info` has these properties: +* **sftp**(< _mixed_ >accept, < _mixed_ >reject) - The client has requested the SFTP subsystem. `accept` and `reject` are functions if the client requested a response. `accept()` returns an _SFTP_ instance in server mode (see the [`SFTP` documentation](https://github.com/mscdex/ssh2/blob/master/SFTP.md) for details). `info` has these properties: -* **subsystem**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client has requested an arbitrary subsystem. `accept` and `reject` are functions if the client requested a response. `accept()` returns a _Channel_ for the subsystem. `reject()` Returns `false` if you should wait for the `continue` event before sending any more traffic. `info` has these properties: +* **subsystem**(< _mixed_ >accept, < _mixed_ >reject, < _object_ >info) - The client has requested an arbitrary subsystem. `accept` and `reject` are functions if the client requested a response. `accept()` returns a _Channel_ for the subsystem. `info` has these properties: * **name** - _string_ - The name of the subsystem. @@ -1027,17 +1171,17 @@ This is a normal **streams2** Duplex Stream (used both by clients and servers), * The readable side represents stdout and the writable side represents stdin. - * **signal**(< _string_ >signalName) - _boolean_ - Sends a POSIX signal to the current process on the server. Valid signal names are: 'ABRT', 'ALRM', 'FPE', 'HUP', 'ILL', 'INT', 'KILL', 'PIPE', 'QUIT', 'SEGV', 'TERM', 'USR1', and 'USR2'. Some server implementations may ignore this request if they do not support signals. Note: If you are trying to send SIGINT and you find `signal()` doesn't work, try writing `'\x03'` to the Channel stream instead. Returns `false` if you should wait for the `continue` event before sending any more traffic. + * **signal**(< _string_ >signalName) - _(void)_ - Sends a POSIX signal to the current process on the server. Valid signal names are: 'ABRT', 'ALRM', 'FPE', 'HUP', 'ILL', 'INT', 'KILL', 'PIPE', 'QUIT', 'SEGV', 'TERM', 'USR1', and 'USR2'. Some server implementations may ignore this request if they do not support signals. Note: If you are trying to send SIGINT and you find `signal()` doesn't work, try writing `'\x03'` to the Channel stream instead. - * **setWindow**(< _integer_ >rows, < _integer_ >cols, < _integer_ >height, < _integer_ >width) - _boolean_ - Lets the server know that the local terminal window has been resized. The meaning of these arguments are described in the 'Pseudo-TTY settings' section. Returns `false` if you should wait for the `continue` event before sending any more traffic. + * **setWindow**(< _integer_ >rows, < _integer_ >cols, < _integer_ >height, < _integer_ >width) - _(void)_ - Lets the server know that the local terminal window has been resized. The meaning of these arguments are described in the 'Pseudo-TTY settings' section. * Server-specific: * For exec-enabled channel instances there is an additional method available that may be called right before you close the channel. It has two different signatures: - * **exit**(< _integer_ >exitCode) - _boolean_ - Sends an exit status code to the client. Returns `false` if you should wait for the `continue` event before sending any more traffic. + * **exit**(< _integer_ >exitCode) - _(void)_ - Sends an exit status code to the client. - * **exit**(< _string_ >signalName[, < _boolean_ >coreDumped[, < _string_ >errorMsg]]) - _boolean_ - Sends an exit status code to the client. Returns `false` if you should wait for the `continue` event before sending any more traffic. + * **exit**(< _string_ >signalName[, < _boolean_ >coreDumped[, < _string_ >errorMsg]]) - _(void)_ - Sends an exit status code to the client. * For exec and shell-enabled channel instances, `channel.stderr` is a writable stream. @@ -1126,3 +1270,35 @@ TTY_OP_OSPEED | Specifies the output baud rate in bits per second. #### HTTPAgent methods * **(constructor)**(< _object_ >sshConfig[, < _object_ >agentConfig]) - Creates and returns a new `http.Agent` instance used to tunnel an HTTP connection over SSH. `sshConfig` is what is passed to `client.connect()` and `agentOptions` is passed to the `http.Agent` constructor. + +### HTTPSAgent + +#### HTTPSAgent methods + +* **(constructor)**(< _object_ >sshConfig[, < _object_ >agentConfig]) - Creates and returns a new `https.Agent` instance used to tunnel an HTTP connection over SSH. `sshConfig` is what is passed to `client.connect()` and `agentOptions` is passed to the `https.Agent` constructor. + +### Utilities + +* **parseKey**(< _mixed_ >keyData[, < _string_ >passphrase]) - _mixed_ - Parses a private/public key in OpenSSH, RFC4716, or PPK format. For encrypted private keys, the key will be decrypted with the given `passphrase`. `keyData` can be a _Buffer_ or _string_ value containing the key contents. The returned value will be an array of objects (currently in the case of modern OpenSSH keys) or an object with these properties and methods: + + * **type** - _string_ - The full key type (e.g. `'ssh-rsa'`) + + * **comment** - _string_ - The comment for the key + + * **getPrivatePEM**() - _string_ - This returns the PEM version of a private key + + * **getPublicPEM**() - _string_ - This returns the PEM version of a public key (for either public key or derived from a private key) + + * **getPublicSSH**() - _string_ - This returns the SSH version of a public key (for either public key or derived from a private key) + + * **sign**(< _mixed_ >data) - _mixed_ - This signs the given `data` using this key and returns a _Buffer_ containing the signature on success. On failure, an _Error_ will be returned. `data` can be anything accepted by node's [`sign.update()`](https://nodejs.org/docs/latest/api/crypto.html#crypto_sign_update_data_inputencoding). + + * **verify**(< _mixed_ >data, < _Buffer_ >signature) - _mixed_ - This verifies a `signature` of the given `data` using this key and returns `true` if the signature could be verified. On failure, either `false` will be returned or an _Error_ will be returned upon a more critical failure. `data` can be anything accepted by node's [`verify.update()`](https://nodejs.org/docs/latest/api/crypto.html#crypto_verify_update_data_inputencoding). + +* **sftp.OPEN_MODE** - [`OPEN_MODE`](https://github.com/mscdex/ssh2/blob/master/SFTP.md#useful-standalone-data-structures) + +* **sftp.STATUS_CODE** - [`STATUS_CODE`](https://github.com/mscdex/ssh2/blob/master/SFTP.md#useful-standalone-data-structures) + +* **sftp.flagsToString** - [`flagsToString()`](https://github.com/mscdex/ssh2/blob/master/SFTP.md#useful-standalone-methods) + +* **sftp.stringToFlags** - [`stringToFlags()`](https://github.com/mscdex/ssh2/blob/master/SFTP.md#useful-standalone-methods) diff --git a/SFTP.md b/SFTP.md new file mode 100644 index 00000000..f5a26192 --- /dev/null +++ b/SFTP.md @@ -0,0 +1,403 @@ +SFTP events +----------- + +**Client/Server events** + +* **ready**() - Emitted after initial protocol version check has passed. + +**Server-only events** + +_Responses to these client requests are sent using one of the methods listed further in this document under `Server-only methods`. The valid response(s) for each request are documented below._ + +* **OPEN**(< _integer_ >reqID, < _string_ >filename, < _integer_ >flags, < _ATTRS_ >attrs) + + `flags` is a bitfield containing any of the flags defined in `OPEN_MODE`. + Use the static method `flagsToString()` to convert the value to a mode + string to be used by `fs.open()` (e.g. `'r'`). + + Respond using one of the following: + + * `handle()` - This indicates a successful opening of the file and passes + the given handle back to the client to use to refer to this open file for + future operations (e.g. reading, writing, closing). + + * `status()` - Use this to indicate a failure to open the requested file. + +* **READ**(< _integer_ >reqID, < _Buffer_ >handle, < _integer_ >offset, < _integer_ >length) + + Respond using one of the following: + + * `data()` - Use this to send the requested chunk of data back to the client. + The amount of data sent is allowed to be less than the `length` requested, + for example if the file ends between `offset` and `offset + length`. + + * `status()` - Use this to indicate either end of file (`STATUS_CODE.EOF`) + has been reached (`offset` is past the end of the file) or if an error + occurred while reading the requested part of the file. + +* **WRITE**(< _integer_ >reqID, < _Buffer_ >handle, < _integer_ >offset, < _Buffer_ >data) + + Respond using: + + * `status()` - Use this to indicate success/failure of the write to the file. + +* **FSTAT**(< _integer_ >reqID, < _Buffer_ >handle) + + Respond using one of the following: + + * `attrs()` - Use this to send the attributes for the requested + file/directory back to the client. + + * `status()` - Use this to indicate an error occurred while accessing the + file/directory. + +* **FSETSTAT**(< _integer_ >reqID, < _Buffer_ >handle, < _ATTRS_ >attrs) + + Respond using: + + * `status()` - Use this to indicates success/failure of the setting of the + given file/directory attributes. + +* **CLOSE**(< _integer_ >reqID, < _Buffer_ >handle) + + Respond using: + + * `status()` - Use this to indicate success (`STATUS_CODE.OK`) or failure of + the closing of the file identified by `handle`. + +* **OPENDIR**(< _integer_ >reqID, < _string_ >path) + + Respond using one of the following: + + * `handle()` - This indicates a successful opening of the directory and + passes the given handle back to the client to use to refer to this open + directory for future operations (e.g. reading directory contents, closing). + + * `status()` - Use this to indicate a failure to open the requested + directory. + +* **READDIR**(< _integer_ >reqID, < _Buffer_ >handle) + + Respond using one of the following: + + * `name()` - Use this to send one or more directory listings for the open + directory back to the client. + + * `status()` - Use this to indicate either end of directory contents + (`STATUS_CODE.EOF`) or if an error occurred while reading the directory + contents. + +* **LSTAT**(< _integer_ >reqID, < _string_ >path) + + Respond using one of the following: + + * `attrs()` - Use this to send the attributes for the requested + file/directory back to the client. + + * `status()` - Use this to indicate an error occurred while accessing the + file/directory. + +* **STAT**(< _integer_ >reqID, < _string_ >path) + + Respond using one of the following: + + * `attrs()` - Use this to send the attributes for the requested + file/directory back to the client. + + * `status()` - Use this to indicate an error occurred while accessing the + file/directory. + +* **REMOVE**(< _integer_ >reqID, < _string_ >path) + + Respond using: + + * `status()` - Use this to indicate success/failure of the removal of the + file at `path`. + +* **RMDIR**(< _integer_ >reqID, < _string_ >path) + + Respond using: + + * `status()` - Use this to indicate success/failure of the removal of the + directory at `path`. + +* **REALPATH**(< _integer_ >reqID, < _string_ >path) + + Respond using one of the following: + + * `name()` - Use this to respond with a normalized version of `path`. + No file/directory attributes are required to be sent in this response. + + * `status()` - Use this to indicate a failure in normalizing `path`. + +* **READLINK**(< _integer_ >reqID, < _string_ >path) + + Respond using one of the following: + + * `name()` - Use this to respond with the target of the symlink at `path`. + No file/directory attributes are required to be sent in this response. + + * `status()` - Use this to indicate a failure in reading the symlink at + `path`. + +* **SETSTAT**(< _integer_ >reqID, < _string_ >path, < _ATTRS_ >attrs) + + Respond using: + + * `status()` - Use this to indicates success/failure of the setting of the + given file/directory attributes. + +* **MKDIR**(< _integer_ >reqID, < _string_ >path, < _ATTRS_ >attrs) + + Respond using: + + * `status()` - Use this to indicate success/failure of the creation of the + directory at `path`. + +* **RENAME**(< _integer_ >reqID, < _string_ >oldPath, < _string_ >newPath) + + Respond using: + + * `status()` - Use this to indicate success/failure of the renaming of the + file/directory at `oldPath` to `newPath`. + +* **SYMLINK**(< _integer_ >reqID, < _string_ >linkPath, < _string_ >targetPath) + + Respond using: + + * `status()` - Use this to indicate success/failure of the symlink creation. + + +Useful standalone data structures +--------------------------------- + +* **STATUS_CODE** - _object_ - Contains the various status codes (for use especially with `status()`): + + * `OK` + + * `EOF` + + * `NO_SUCH_FILE` + + * `PERMISSION_DENIED` + + * `FAILURE` + + * `BAD_MESSAGE` + + * `OP_UNSUPPORTED` + +* **OPEN_MODE** - _object_ - Contains the various open file flags: + + * `READ` + + * `WRITE` + + * `APPEND` + + * `CREAT` + + * `TRUNC` + + * `EXCL` + + +Useful standalone methods +------------------------- + +* **stringToFlags**(< _string_ >flagsStr) - _integer_ - Converts string flags (e.g. `'r'`, `'a+'`, etc.) to the appropriate `OPEN_MODE` flag mask. Returns `null` if conversion failed. + +* **flagsToString**(< _integer_ >flagsMask) - _string_ - Converts flag mask (e.g. number containing `OPEN_MODE` values) to the appropriate string value. Returns `null` if conversion failed. + + +SFTP methods +------------ + +* **(constructor)**(< _object_ >config[, < _string_ >remoteIdentRaw]) - Creates and returns a new SFTP instance. `remoteIdentRaw` can be the raw SSH identification string of the remote party. This is used to change internal behavior based on particular SFTP implementations. `config` can contain: + + * **server** - _boolean_ - Set to `true` to create an instance in server mode. **Default:** `false` + + * **debug** - _function_ - Set this to a function that receives a single string argument to get detailed (local) debug information. **Default:** (none) + + + +**Client-only methods** + +* **fastGet**(< _string_ >remotePath, < _string_ >localPath[, < _object_ >options], < _function_ >callback) - _(void)_ - Downloads a file at `remotePath` to `localPath` using parallel reads for faster throughput. `options` can have the following properties: + + * **concurrency** - _integer_ - Number of concurrent reads **Default:** `64` + + * **chunkSize** - _integer_ - Size of each read in bytes **Default:** `32768` + + * **step** - _function_(< _integer_ >total_transferred, < _integer_ >chunk, < _integer_ >total) - Called every time a part of a file was transferred + + `callback` has 1 parameter: < _Error_ >err. + +* **fastPut**(< _string_ >localPath, < _string_ >remotePath[, < _object_ >options], < _function_ >callback) - _(void)_ - Uploads a file from `localPath` to `remotePath` using parallel reads for faster throughput. `options` can have the following properties: + + * **concurrency** - _integer_ - Number of concurrent reads **Default:** `64` + + * **chunkSize** - _integer_ - Size of each read in bytes **Default:** `32768` + + * **step** - _function_(< _integer_ >total_transferred, < _integer_ >chunk, < _integer_ >total) - Called every time a part of a file was transferred + + * **mode** - _mixed_ - Integer or string representing the file mode to set for the uploaded file. + + `callback` has 1 parameter: < _Error_ >err. + +* **createReadStream**(< _string_ >path[, < _object_ >options]) - _ReadStream_ - Returns a new readable stream for `path`. `options` has the following defaults: + + ```javascript + { flags: 'r', + encoding: null, + handle: null, + mode: 0o666, + autoClose: true + } + ``` + + `options` can include `start` and `end` values to read a range of bytes from the file instead of the entire file. Both `start` and `end` are inclusive and start at 0. The `encoding` can be `'utf8'`, `'ascii'`, or `'base64'`. + + If `autoClose` is false, then the file handle won't be closed, even if there's an error. It is your responsiblity to close it and make sure there's no file handle leak. If `autoClose` is set to true (default behavior), on `error` or `end` the file handle will be closed automatically. + + An example to read the last 10 bytes of a file which is 100 bytes long: + + ```javascript + sftp.createReadStream('sample.txt', {start: 90, end: 99}); + ``` + +* **createWriteStream**(< _string_ >path[, < _object_ >options]) - _WriteStream_ - Returns a new writable stream for `path`. `options` has the following defaults: + + ```javascript + { + flags: 'w', + encoding: null, + mode: 0o666, + autoClose: true + } + ``` + + `options` may also include a `start` option to allow writing data at some position past the beginning of the file. Modifying a file rather than replacing it may require a flags mode of 'r+' rather than the default mode 'w'. + + If 'autoClose' is set to false and you pipe to this stream, this stream will not automatically close after there is no more data upstream -- allowing future pipes and/or manual writes. + +* **open**(< _string_ >filename, < _string_ >flags, [< _mixed_ >attrs_mode, ]< _function_ >callback) - _boolean_ - Opens a file `filename` with `flags` with optional _ATTRS_ object or file mode `attrs_mode`. `flags` is any of the flags supported by `fs.open` (except sync flag). Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _Buffer_ >handle. + +* **close**(< _Buffer_ >handle, < _function_ >callback) - _boolean_ - Closes the resource associated with `handle` given by open() or opendir(). Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **read**(< _Buffer_ >handle, < _Buffer_ >buffer, < _integer_ >offset, < _integer_ >length, < _integer_ >position, < _function_ >callback) - _boolean_ - Reads `length` bytes from the resource associated with `handle` starting at `position` and stores the bytes in `buffer` starting at `offset`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 4 parameters: < _Error_ >err, < _integer_ >bytesRead, < _Buffer_ >buffer (offset adjusted), < _integer_ >position. + +* **write**(< _Buffer_ >handle, < _Buffer_ >buffer, < _integer_ >offset, < _integer_ >length, < _integer_ >position, < _function_ >callback) - _boolean_ - Writes `length` bytes from `buffer` starting at `offset` to the resource associated with `handle` starting at `position`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **fstat**(< _Buffer_ >handle, < _function_ >callback) - _boolean_ - Retrieves attributes for the resource associated with `handle`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _Stats_ >stats. + +* **fsetstat**(< _Buffer_ >handle, < _ATTRS_ >attributes, < _function_ >callback) - _boolean_ - Sets the attributes defined in `attributes` for the resource associated with `handle`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **futimes**(< _Buffer_ >handle, < _mixed_ >atime, < _mixed_ >mtime, < _function_ >callback) - _boolean_ - Sets the access time and modified time for the resource associated with `handle`. `atime` and `mtime` can be Date instances or UNIX timestamps. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **fchown**(< _Buffer_ >handle, < _integer_ >uid, < _integer_ >gid, < _function_ >callback) - _boolean_ - Sets the owner for the resource associated with `handle`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **fchmod**(< _Buffer_ >handle, < _mixed_ >mode, < _function_ >callback) - _boolean_ - Sets the mode for the resource associated with `handle`. `mode` can be an integer or a string containing an octal number. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **opendir**(< _string_ >path, < _function_ >callback) - _boolean_ - Opens a directory `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _Buffer_ >handle. + +* **readdir**(< _mixed_ >location, < _function_ >callback) - _boolean_ - Retrieves a directory listing. `location` can either be a _Buffer_ containing a valid directory handle from opendir() or a _string_ containing the path to a directory. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _mixed_ >list. `list` is an _Array_ of `{ filename: 'foo', longname: '....', attrs: {...} }` style objects (attrs is of type _ATTR_). If `location` is a directory handle, this function may need to be called multiple times until `list` is boolean false, which indicates that no more directory entries are available for that directory handle. + +* **unlink**(< _string_ >path, < _function_ >callback) - _boolean_ - Removes the file/symlink at `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **rename**(< _string_ >srcPath, < _string_ >destPath, < _function_ >callback) - _boolean_ - Renames/moves `srcPath` to `destPath`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **mkdir**(< _string_ >path, [< _ATTRS_ >attributes, ]< _function_ >callback) - _boolean_ - Creates a new directory `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **rmdir**(< _string_ >path, < _function_ >callback) - _boolean_ - Removes the directory at `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **stat**(< _string_ >path, < _function_ >callback) - _boolean_ - Retrieves attributes for `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameter: < _Error_ >err, < _Stats_ >stats. + +* **lstat**(< _string_ >path, < _function_ >callback) - _boolean_ - Retrieves attributes for `path`. If `path` is a symlink, the link itself is stat'ed instead of the resource it refers to. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _Stats_ >stats. + +* **setstat**(< _string_ >path, < _ATTRS_ >attributes, < _function_ >callback) - _boolean_ - Sets the attributes defined in `attributes` for `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **utimes**(< _string_ >path, < _mixed_ >atime, < _mixed_ >mtime, < _function_ >callback) - _boolean_ - Sets the access time and modified time for `path`. `atime` and `mtime` can be Date instances or UNIX timestamps. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **chown**(< _string_ >path, < _integer_ >uid, < _integer_ >gid, < _function_ >callback) - _boolean_ - Sets the owner for `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **chmod**(< _string_ >path, < _mixed_ >mode, < _function_ >callback) - _boolean_ - Sets the mode for `path`. `mode` can be an integer or a string containing an octal number. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **readlink**(< _string_ >path, < _function_ >callback) - _boolean_ - Retrieves the target for a symlink at `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _string_ >target. + +* **symlink**(< _string_ >targetPath, < _string_ >linkPath, < _function_ >callback) - _boolean_ - Creates a symlink at `linkPath` to `targetPath`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **realpath**(< _string_ >path, < _function_ >callback) - _boolean_ - Resolves `path` to an absolute path. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _string_ >absPath. + +* **ext_openssh_rename**(< _string_ >srcPath, < _string_ >destPath, < _function_ >callback) - _boolean_ - **OpenSSH extension** Performs POSIX rename(3) from `srcPath` to `destPath`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **ext_openssh_statvfs**(< _string_ >path, < _function_ >callback) - _boolean_ - **OpenSSH extension** Performs POSIX statvfs(2) on `path`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _object_ >fsInfo. `fsInfo` contains the information as found in the [statvfs struct](http://linux.die.net/man/2/statvfs). + +* **ext_openssh_fstatvfs**(< _Buffer_ >handle, < _function_ >callback) - _boolean_ - **OpenSSH extension** Performs POSIX fstatvfs(2) on open handle `handle`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 2 parameters: < _Error_ >err, < _object_ >fsInfo. `fsInfo` contains the information as found in the [statvfs struct](http://linux.die.net/man/2/statvfs). + +* **ext_openssh_hardlink**(< _string_ >targetPath, < _string_ >linkPath, < _function_ >callback) - _boolean_ - **OpenSSH extension** Performs POSIX link(2) to create a hard link to `targetPath` at `linkPath`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + +* **ext_openssh_fsync**(< _Buffer_ >handle, < _function_ >callback) - _boolean_ - **OpenSSH extension** Performs POSIX fsync(3) on the open handle `handle`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `callback` has 1 parameter: < _Error_ >err. + + +**Server-only methods** + +* **status**(< _integer_ >reqID, < _integer_ >statusCode[, < _string_ >message]) - _boolean_ - Sends a status response for the request identified by `id`. Returns `false` if you should wait for the `continue` event before sending any more traffic. + +* **handle**(< _integer_ >reqID, < _Buffer_ >handle) - _boolean_ - Sends a handle response for the request identified by `id`. `handle` must be less than 256 bytes and is an opaque value that could merely contain the value of a backing file descriptor or some other unique, custom value. Returns `false` if you should wait for the `continue` event before sending any more traffic. + +* **data**(< _integer_ >reqID, < _mixed_ >data[, < _string_ >encoding]) - _boolean_ - Sends a data response for the request identified by `id`. `data` can be a _Buffer_ or _string_. If `data` is a string, `encoding` is the encoding of `data`. Returns `false` if you should wait for the `continue` event before sending any more traffic. + +* **name**(< _integer_ >reqID, < _array_ >names) - _boolean_ - Sends a name response for the request identified by `id`. Returns `false` if you should wait for the `continue` event before sending any more traffic. `names` must be an _array_ of _object_ where each _object_ can contain: + + * **filename** - _string_ - The entry's name. + + * **longname** - _string_ - This is the `ls -l`-style format for the entry (e.g. `-rwxr--r-- 1 bar bar 718 Dec 8 2009 foo`) + + * **attrs** - _ATTRS_ - This is an optional _ATTRS_ object that contains requested/available attributes for the entry. + +* **attrs**(< _integer_ >reqID, < _ATTRS_ >attrs) - _boolean_ - Sends an attrs response for the request identified by `id`. `attrs` contains the requested/available attributes. + + +ATTRS +----- + +An object with the following valid properties: + +* **mode** - _integer_ - Mode/permissions for the resource. + +* **uid** - _integer_ - User ID of the resource. + +* **gid** - _integer_ - Group ID of the resource. + +* **size** - _integer_ - Resource size in bytes. + +* **atime** - _integer_ - UNIX timestamp of the access time of the resource. + +* **mtime** - _integer_ - UNIX timestamp of the modified time of the resource. + +When supplying an ATTRS object to one of the SFTP methods: + +* `atime` and `mtime` can be either a Date instance or a UNIX timestamp. + +* `mode` can either be an integer or a string containing an octal number. + + +Stats +----- + +An object with the same attributes as an ATTRS object with the addition of the following methods: + +* `stats.isDirectory()` + +* `stats.isFile()` + +* `stats.isBlockDevice()` + +* `stats.isCharacterDevice()` + +* `stats.isSymbolicLink()` + +* `stats.isFIFO()` + +* `stats.isSocket()` diff --git a/examples/server-chat.js b/examples/server-chat.js index 18e51080..a82a9550 100644 --- a/examples/server-chat.js +++ b/examples/server-chat.js @@ -4,21 +4,23 @@ // terminal types of client connections // 2. Install `blessed`: `npm install blessed` // 3. Create a server host key in this same directory and name it `host.key` +'use strict'; -var fs = require('fs'); +const { readFileSync } = require('fs'); -var blessed = require('blessed'); -var Server = require('ssh2').Server; +const blessed = require('blessed'); +const { Server } = require('ssh2'); -var RE_SPECIAL = /[\x00-\x1F\x7F]+|(?:\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K])/g; -var MAX_MSG_LEN = 128; -var MAX_NAME_LEN = 10; -var PROMPT_NAME = 'Enter a nickname to use (max ' + MAX_NAME_LEN + ' chars): '; +const RE_SPECIAL = +// eslint-disable-next-line no-control-regex + /[\x00-\x1F\x7F]+|(?:\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K])/g; +const MAX_MSG_LEN = 128; +const MAX_NAME_LEN = 10; +const PROMPT_NAME = `Enter a nickname to use (max ${MAX_NAME_LEN} chars): `; -var users = []; +const users = []; function formatMessage(msg, output) { - var output = output; output.parseTags = true; msg = output._parseTags(msg); output.parseTags = false; @@ -26,12 +28,11 @@ function formatMessage(msg, output) { } function userBroadcast(msg, source) { - var sourceMsg = '> ' + msg; - var name = '{cyan-fg}{bold}' + source.name + '{/}'; - msg = ': ' + msg; - for (var i = 0; i < users.length; ++i) { - var user = users[i]; - var output = user.output; + const sourceMsg = `> ${msg}`; + const name = `{cyan-fg}{bold}${source.name}{/}`; + msg = `: ${msg}`; + for (const user of users) { + const output = user.output; if (source === user) output.add(sourceMsg); else @@ -40,30 +41,31 @@ function userBroadcast(msg, source) { } function localMessage(msg, source) { - var output = source.output; + const output = source.output; output.add(formatMessage(msg, output)); } function noop(v) {} new Server({ - hostKeys: [fs.readFileSync('host.key')], -}, function(client) { - var stream; - var name; - - client.on('authentication', function(ctx) { - var nick = ctx.username; - var prompt = PROMPT_NAME; - var lowered; + hostKeys: [readFileSync('host.key')], +}, (client) => { + let stream; + let name; + + client.on('authentication', (ctx) => { + let nick = ctx.username; + let prompt = PROMPT_NAME; + let lowered; + // Try to use username as nickname if (nick.length > 0 && nick.length <= MAX_NAME_LEN) { lowered = nick.toLowerCase(); - var ok = true; - for (var i = 0; i < users.length; ++i) { - if (users[i].name.toLowerCase() === lowered) { + let ok = true; + for (const user of users) { + if (user.name.toLowerCase() === lowered) { ok = false; - prompt = 'That nickname is already in use.\n' + PROMPT_NAME; + prompt = `That nickname is already in use.\n${PROMPT_NAME}`; break; } } @@ -71,10 +73,11 @@ new Server({ name = nick; return ctx.accept(); } - } else if (nick.length === 0) + } else if (nick.length === 0) { prompt = 'A nickname is required.\n' + PROMPT_NAME; - else + } else { prompt = 'That nickname is too long.\n' + PROMPT_NAME; + } if (ctx.method !== 'keyboard-interactive') return ctx.reject(['keyboard-interactive']); @@ -84,33 +87,33 @@ new Server({ return ctx.reject(['keyboard-interactive']); nick = answers[0]; if (nick.length > MAX_NAME_LEN) { - return ctx.prompt('That nickname is too long.\n' + PROMPT_NAME, + return ctx.prompt(`That nickname is too long.\n${PROMPT_NAME}`, retryPrompt); } else if (nick.length === 0) { - return ctx.prompt('A nickname is required.\n' + PROMPT_NAME, + return ctx.prompt(`A nickname is required.\n${PROMPT_NAME}`, retryPrompt); } lowered = nick.toLowerCase(); - for (var i = 0; i < users.length; ++i) { - if (users[i].name.toLowerCase() === lowered) { - return ctx.prompt('That nickname is already in use.\n' + PROMPT_NAME, + for (const user of users) { + if (user.name.toLowerCase() === lowered) { + return ctx.prompt(`That nickname is already in use.\n${PROMPT_NAME}`, retryPrompt); } } name = nick; ctx.accept(); }); - }).on('ready', function() { - var rows; - var cols; - var term; - client.once('session', function(accept, reject) { - accept().once('pty', function(accept, reject, info) { + }).on('ready', () => { + let rows; + let cols; + let term; + client.once('session', (accept, reject) => { + accept().once('pty', (accept, reject, info) => { rows = info.rows; cols = info.cols; term = info.term; accept && accept(); - }).on('window-change', function(accept, reject, info) { + }).on('window-change', (accept, reject, info) => { rows = info.rows; cols = info.cols; if (stream) { @@ -119,7 +122,7 @@ new Server({ stream.emit('resize'); } accept && accept(); - }).once('shell', function(accept, reject) { + }).once('shell', (accept, reject) => { stream = accept(); users.push(stream); @@ -130,7 +133,7 @@ new Server({ stream.setRawMode = noop; stream.on('error', noop); - var screen = new blessed.screen({ + const screen = new blessed.screen({ autoPadding: true, smartCSR: true, program: new blessed.program({ @@ -144,14 +147,14 @@ new Server({ // Disable local echo screen.program.attr('invisible', true); - var output = stream.output = new blessed.log({ + const output = stream.output = new blessed.log({ screen: screen, top: 0, left: 0, width: '100%', bottom: 2, scrollOnInput: true - }) + }); screen.append(output); screen.append(new blessed.box({ @@ -164,7 +167,7 @@ new Server({ ch: '=' })); - var input = new blessed.textbox({ + const input = new blessed.textbox({ screen: screen, bottom: 0, height: 1, @@ -184,9 +187,8 @@ new Server({ stream); // Let everyone else know that this user just joined - for (var i = 0; i < users.length; ++i) { - var user = users[i]; - var output = user.output; + for (const user of users) { + const output = user.output; if (user === stream) continue; output.add(formatMessage('{green-fg}*** {bold}', output) @@ -200,7 +202,7 @@ new Server({ screen.program.emit('resize'); // Read a line of input from the user - input.on('submit', function(line) { + input.on('submit', (line) => { input.clearValue(); screen.render(); if (!input.focused) @@ -217,27 +219,20 @@ new Server({ }); }); }); - }).on('end', function() { + }).on('close', () => { if (stream !== undefined) { - spliceOne(users, users.indexOf(stream)); + users.splice(users.indexOf(stream), 1); // Let everyone else know that this user just left - for (var i = 0; i < users.length; ++i) { - var user = users[i]; - var output = user.output; + for (const user of users) { + const output = user.output; output.add(formatMessage('{magenta-fg}*** {bold}', output) + name + formatMessage('{/bold} has left the chat{/}', output)); } } - }).on('error', function(err) { + }).on('error', (err) => { // Ignore errors }); }).listen(0, function() { console.log('Listening on port ' + this.address().port); }); - -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) - list[i] = list[k]; - list.pop(); -} diff --git a/examples/sftp-server-download-only.js b/examples/sftp-server-download-only.js index 7f8d0660..d4ae4c58 100644 --- a/examples/sftp-server-download-only.js +++ b/examples/sftp-server-download-only.js @@ -1,113 +1,134 @@ -var crypto = require('crypto'); -var constants = require('constants'); -var fs = require('fs'); +'use strict'; -var ssh2 = require('ssh2'); -var OPEN_MODE = ssh2.SFTP_OPEN_MODE; -var STATUS_CODE = ssh2.SFTP_STATUS_CODE; +const { timingSafeEqual } = require('crypto'); +const { constants, readFileSync } = require('fs'); -var allowedUser = Buffer.from('foo'); -var allowedPassword = Buffer.from('bar'); +const { Server, sftp: { OPEN_MODE, STATUS_CODE } } = require('ssh2'); -new ssh2.Server({ - hostKeys: [fs.readFileSync('host.key')] -}, function(client) { +const allowedUser = Buffer.from('foo'); +const allowedPassword = Buffer.from('bar'); + +function checkValue(input, allowed) { + const autoReject = (input.length !== allowed.length); + if (autoReject) { + // Prevent leaking length information by always making a comparison with the + // same input when lengths don't match what we expect ... + allowed = input; + } + const isMatch = timingSafeEqual(input, allowed); + return (!autoReject && isMatch); +} + +new Server({ + hostKeys: [readFileSync('host.key')] +}, (client) => { console.log('Client connected!'); - client.on('authentication', function(ctx) { - var user = Buffer.from(ctx.username); - if (user.length !== allowedUser.length - || !crypto.timingSafeEqual(user, allowedUser)) { - return ctx.reject(['password']); - } + client.on('authentication', (ctx) => { + let allowed = true; + if (!checkValue(Buffer.from(ctx.username), allowedUser)) + allowed = false; switch (ctx.method) { case 'password': - var password = Buffer.from(ctx.password); - if (password.length !== allowedPassword.length - || !crypto.timingSafeEqual(password, allowedPassword)) { - return ctx.reject(['password']); - } + if (!checkValue(Buffer.from(ctx.password), allowedPassword)) + return ctx.reject(); break; default: - return ctx.reject(['password']); + return ctx.reject(); } - ctx.accept(); - }).on('ready', function() { + if (allowed) + ctx.accept(); + else + ctx.reject(); + }).on('ready', () => { console.log('Client authenticated!'); - client.on('session', function(accept, reject) { - var session = accept(); - session.on('sftp', function(accept, reject) { + client.on('session', (accept, reject) => { + const session = accept(); + session.on('sftp', (accept, reject) => { console.log('Client SFTP session'); - var openFiles = {}; - var handleCount = 0; - // `sftpStream` is an `SFTPStream` instance in server mode - // see: https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md - var sftpStream = accept(); - sftpStream.on('OPEN', function(reqid, filename, flags, attrs) { - console.log('OPEN', filename); - // only allow opening /tmp/foo.txt for writing + + const openFiles = new Map(); + let handleCount = 0; + const sftp = accept(); + sftp.on('OPEN', (reqid, filename, flags, attrs) => { + // Only allow opening /tmp/foo.txt for writing if (filename !== '/tmp/foo.txt' || !(flags & OPEN_MODE.READ)) - return sftpStream.status(reqid, STATUS_CODE.FAILURE); - // create a fake handle to return to the client, this could easily + return sftp.status(reqid, STATUS_CODE.FAILURE); + + // Create a fake handle to return to the client, this could easily // be a real file descriptor number for example if actually opening // the file on the disk - var handle = new Buffer(4); - openFiles[handleCount] = { read: false }; + const handle = Buffer.alloc(4); + openFiles.set(handleCount, { read: false }); handle.writeUInt32BE(handleCount++, 0, true); - sftpStream.handle(reqid, handle); - console.log('Opening file for read') - }).on('READ', function(reqid, handle, offset, length) { - if (handle.length !== 4 || !openFiles[handle.readUInt32BE(0, true)]) - return sftpStream.status(reqid, STATUS_CODE.FAILURE); - // fake the read - var state = openFiles[handle.readUInt32BE(0, true)]; - if (state.read) - sftpStream.status(reqid, STATUS_CODE.EOF); - else { + + console.log('Opening file for read'); + sftp.handle(reqid, handle); + }).on('READ', (reqid, handle, offset, length) => { + let fnum; + if (handle.length !== 4 + || !openFiles.has(fnum = handle.readUInt32BE(0, true))) { + return sftp.status(reqid, STATUS_CODE.FAILURE); + } + + // Fake the read + const state = openFiles.get(fnum); + if (state.read) { + sftp.status(reqid, STATUS_CODE.EOF); + } else { state.read = true; - sftpStream.data(reqid, 'bar'); - console.log('Read from file at offset %d, length %d', offset, length); + + console.log( + 'Read from file at offset %d, length %d', offset, length + ); + sftp.data(reqid, 'bar'); } - }).on('CLOSE', function(reqid, handle) { - var fnum; - if (handle.length !== 4 || !openFiles[(fnum = handle.readUInt32BE(0, true))]) - return sftpStream.status(reqid, STATUS_CODE.FAILURE); - delete openFiles[fnum]; - sftpStream.status(reqid, STATUS_CODE.OK); + }).on('CLOSE', (reqid, handle) => { + let fnum; + if (handle.length !== 4 + || !openFiles.has(fnum = handle.readUInt32BE(0))) { + return sftp.status(reqid, STATUS_CODE.FAILURE); + } + + openFiles.delete(fnum); + console.log('Closing file'); + sftp.status(reqid, STATUS_CODE.OK); }).on('REALPATH', function(reqid, path) { - var name = [{ + const name = [{ filename: '/tmp/foo.txt', longname: '-rwxrwxrwx 1 foo foo 3 Dec 8 2009 foo.txt', attrs: {} }]; - sftpStream.name(reqid, name); + sftp.name(reqid, name); }).on('STAT', onSTAT) .on('LSTAT', onSTAT); + function onSTAT(reqid, path) { if (path !== '/tmp/foo.txt') - return sftpStream.status(reqid, STATUS_CODE.FAILURE); - var mode = constants.S_IFREG; // Regular file - mode |= constants.S_IRWXU; // read, write, execute for user - mode |= constants.S_IRWXG; // read, write, execute for group - mode |= constants.S_IRWXO; // read, write, execute for other - sftpStream.attrs(reqid, { + return sftp.status(reqid, STATUS_CODE.FAILURE); + + let mode = constants.S_IFREG; // Regular file + mode |= constants.S_IRWXU; // Read, write, execute for user + mode |= constants.S_IRWXG; // Read, write, execute for group + mode |= constants.S_IRWXO; // Read, write, execute for other + sftp.attrs(reqid, { mode: mode, uid: 0, gid: 0, size: 3, atime: Date.now(), - mtime: Date.now() + mtime: Date.now(), }); } }); }); - }).on('end', function() { + }).on('close', () => { console.log('Client disconnected'); }); }).listen(0, '127.0.0.1', function() { - console.log('Listening on port ' + this.address().port); + console.log(`Listening on port ${this.address().port}`); }); diff --git a/install.js b/install.js new file mode 100644 index 00000000..a4376ec2 --- /dev/null +++ b/install.js @@ -0,0 +1,17 @@ +'use strict'; + +const { spawnSync } = require('child_process'); + +// Attempt to build the bundled optional binding +const result = spawnSync('node-gyp', ['rebuild'], { + cwd: 'lib/protocol/crypto', + encoding: 'utf8', + shell: true, + stdio: 'inherit', + windowsHide: true, +}); +if (result.error || result.status !== 0) + console.log('Failed to build optional crypto binding'); +else + console.log('Succeeded in building optional crypto binding'); +process.exit(0); diff --git a/lib/Channel.js b/lib/Channel.js index ffd6415c..2728fe1c 100644 --- a/lib/Channel.js +++ b/lib/Channel.js @@ -1,508 +1,292 @@ -var inherits = require('util').inherits; -var DuplexStream = require('stream').Duplex; -var ReadableStream = require('stream').Readable; -var WritableStream = require('stream').Writable; - -var STDERR = require('ssh2-streams').constants.CHANNEL_EXTENDED_DATATYPE.STDERR; - -var PACKET_SIZE = 32 * 1024; -var MAX_WINDOW = 2 * 1024 * 1024; -var WINDOW_THRESHOLD = MAX_WINDOW / 2; -var CUSTOM_EVENTS = [ - 'CHANNEL_EOF', - 'CHANNEL_CLOSE', - 'CHANNEL_DATA', - 'CHANNEL_EXTENDED_DATA', - 'CHANNEL_WINDOW_ADJUST', - 'CHANNEL_SUCCESS', - 'CHANNEL_FAILURE', - 'CHANNEL_REQUEST' -]; -var CUSTOM_EVENTS_LEN = CUSTOM_EVENTS.length; - -function Channel(info, client, opts) { - var streamOpts = { - highWaterMark: MAX_WINDOW, - allowHalfOpen: (!opts || (opts && opts.allowHalfOpen !== false)) - }; - - this.allowHalfOpen = streamOpts.allowHalfOpen; - - DuplexStream.call(this, streamOpts); - - var self = this; - var server = opts && opts.server; - - this.server = server; - this.type = info.type; - this.subtype = undefined; - /* - incoming and outgoing contain these properties: - { - id: undefined, - window: undefined, - packetSize: undefined, - state: 'closed' - } - */ - var incoming = this.incoming = info.incoming; - var incomingId = incoming.id; - var outgoing = this.outgoing = info.outgoing; - var callbacks = this._callbacks = []; - var exitCode; - var exitSignal; - var exitDump; - var exitDesc; - var exitLang; - - this._client = client; - this._hasX11 = false; - - var channels = client._channels; - var sshstream = client._sshstream; - - function ondrain() { - if (self._waitClientDrain) { - self._waitClientDrain = false; - if (!self._waitWindow) { - if (self._chunk) - self._write(self._chunk, null, self._chunkcb); - else if (self._chunkcb) - self._chunkcb(); - else if (self._chunkErr) - self.stderr._write(self._chunkErr, null, self._chunkcbErr); - else if (self._chunkcbErr) - self._chunkcbErr(); - } +'use strict'; + +const { + Duplex: DuplexStream, + Readable: ReadableStream, + Writable: WritableStream, +} = require('stream'); + +const { + CHANNEL_EXTENDED_DATATYPE: { STDERR }, +} = require('./protocol/constants.js'); +const { bufferSlice } = require('./protocol/utils.js'); + +const PACKET_SIZE = 32 * 1024; +const MAX_WINDOW = 2 * 1024 * 1024; +const WINDOW_THRESHOLD = MAX_WINDOW / 2; + +class ClientStderr extends ReadableStream { + constructor(channel, streamOpts) { + super(streamOpts); + + this._channel = channel; + } + _read(n) { + if (this._channel._waitChanDrain) { + this._channel._waitChanDrain = false; + if (this._channel.incoming.window <= WINDOW_THRESHOLD) + windowAdjust(this._channel); } } - client._sock.on('drain', ondrain); +} - sshstream.once('CHANNEL_EOF:' + incomingId, function() { - if (incoming.state !== 'open') - return; - incoming.state = 'eof'; - - if (self.readable) - self.push(null); - if (!server && self.stderr.readable) - self.stderr.push(null); - }).once('CHANNEL_CLOSE:' + incomingId, function() { - if (incoming.state === 'closed') - return; - incoming.state = 'closed'; - - if (self.readable) - self.push(null); - if (server && self.stderr.writable) - self.stderr.end(); - else if (!server && self.stderr.readable) - self.stderr.push(null); - - if (outgoing.state === 'open' || outgoing.state === 'eof') - self.close(); - if (outgoing.state === 'closing') - outgoing.state = 'closed'; - - delete channels[incomingId]; - - var state = self._writableState; - client._sock.removeListener('drain', ondrain); - if (!state.ending && !state.finished) - self.end(); - - // Take care of any outstanding channel requests - self._callbacks = []; - for (var i = 0; i < callbacks.length; ++i) - callbacks[i](true); - callbacks = self._callbacks; - - if (!server) { - // align more with node child processes, where the close event gets the - // same arguments as the exit event - if (!self.readable) { - if (exitCode === null) { - self.emit('close', exitCode, exitSignal, exitDump, exitDesc, - exitLang); - } else - self.emit('close', exitCode); - } else { - self.once('end', function() { - if (exitCode === null) { - self.emit('close', exitCode, exitSignal, exitDump, exitDesc, - exitLang); - } else - self.emit('close', exitCode); - }); - } +class ServerStderr extends WritableStream { + constructor(channel) { + super({ highWaterMark: MAX_WINDOW }); - if (!self.stderr.readable) - self.stderr.emit('close'); - else { - self.stderr.once('end', function() { - self.stderr.emit('close'); - }); - } - } else { // Server mode - if (!self.readable) - self.emit('close'); - else { - self.once('end', function() { - self.emit('close'); - }); - } - } + this._channel = channel; + } - for (var i = 0; i < CUSTOM_EVENTS_LEN; ++i) - sshstream.removeAllListeners(CUSTOM_EVENTS[i] + ':' + incomingId); - }).on('CHANNEL_DATA:' + incomingId, function(data) { - // the remote party should not be sending us data if there is no window - // space available ... - // TODO: raise error on data with not enough window - if (incoming.window === 0) + _write(data, encoding, cb) { + const channel = this._channel; + const protocol = channel._client._protocol; + const outgoing = channel.outgoing; + const packetSize = outgoing.packetSize; + const id = outgoing.id; + let window = outgoing.window; + const len = data.length; + let p = 0; + + if (outgoing.state !== 'open') return; - incoming.window -= data.length; + while (len - p > 0 && window > 0) { + let sliceLen = len - p; + if (sliceLen > window) + sliceLen = window; + if (sliceLen > packetSize) + sliceLen = packetSize; - if (!self.push(data)) { - self._waitChanDrain = true; - return; - } + if (p === 0 && sliceLen === len) + protocol.channelExtData(id, data, STDERR); + else + protocol.channelExtData(id, bufferSlice(data, p, p + sliceLen), STDERR); - if (incoming.window <= WINDOW_THRESHOLD) - windowAdjust(self); - }).on('CHANNEL_WINDOW_ADJUST:' + incomingId, function(amt) { - // the server is allowing us to send `amt` more bytes of data - outgoing.window += amt; - - if (self._waitWindow) { - self._waitWindow = false; - if (!self._waitClientDrain) { - if (self._chunk) - self._write(self._chunk, null, self._chunkcb); - else if (self._chunkcb) - self._chunkcb(); - else if (self._chunkErr) - self.stderr._write(self._chunkErr, null, self._chunkcbErr); - else if (self._chunkcbErr) - self._chunkcbErr(); - } - } - }).on('CHANNEL_SUCCESS:' + incomingId, function() { - if (server) { - sshstream._kalast = Date.now(); - sshstream._kacnt = 0; - } else - client._resetKA(); - if (callbacks.length) - callbacks.shift()(false); - }).on('CHANNEL_FAILURE:' + incomingId, function() { - if (server) { - sshstream._kalast = Date.now(); - sshstream._kacnt = 0; - } else - client._resetKA(); - if (callbacks.length) - callbacks.shift()(true); - }).on('CHANNEL_REQUEST:' + incomingId, function(info) { - if (!server) { - if (info.request === 'exit-status') { - self.emit('exit', exitCode = info.code); - return; - } else if (info.request === 'exit-signal') { - self.emit('exit', - exitCode = null, - exitSignal = 'SIG' + info.signal, - exitDump = info.coredump, - exitDesc = info.description, - exitLang = info.lang); - return; - } + p += sliceLen; + window -= sliceLen; } - // keepalive request? OpenSSH will send one as a channel request if there - // is a channel open + outgoing.window = window; - if (info.wantReply) - sshstream.channelFailure(outgoing.id); - }); + if (len - p > 0) { + if (window === 0) + channel._waitWindow = true; + if (p > 0) + channel._chunkErr = bufferSlice(data, p, len); + else + channel._chunkErr = data; + channel._chunkcbErr = cb; + return; + } - this.stdin = this.stdout = this; + cb(); + } +} - if (server) - this.stderr = new ServerStderr(this); - else { - this.stderr = new ReadableStream(streamOpts); - this.stderr._read = function(n) { - if (self._waitChanDrain) { - self._waitChanDrain = false; - if (incoming.window <= WINDOW_THRESHOLD) - windowAdjust(self); +class Channel extends DuplexStream { + constructor(client, info, opts) { + const streamOpts = { + highWaterMark: MAX_WINDOW, + allowHalfOpen: (!opts || (opts && opts.allowHalfOpen !== false)) + }; + super(streamOpts); + this.allowHalfOpen = streamOpts.allowHalfOpen; + + const server = !!(opts && opts.server); + + this.server = server; + this.type = info.type; + this.subtype = undefined; + + /* + incoming and outgoing contain these properties: + { + id: undefined, + window: undefined, + packetSize: undefined, + state: 'closed' } + */ + this.incoming = info.incoming; + this.outgoing = info.outgoing; + this._callbacks = []; + + this._client = client; + this._hasX11 = false; + this._exit = { + code: undefined, + signal: undefined, + dump: undefined, + desc: undefined, }; - sshstream.on('CHANNEL_EXTENDED_DATA:' + incomingId, - function(type, data) { - // the remote party should not be sending us data if there is no window - // space available ... - // TODO: raise error on data with not enough window - if (incoming.window === 0) - return; + this.stdin = this.stdout = this; - incoming.window -= data.length; + if (server) + this.stderr = new ServerStderr(this); + else + this.stderr = new ClientStderr(this, streamOpts); - if (!self.stderr.push(data)) { - self._waitChanDrain = true; - return; - } + // Outgoing data + this._waitWindow = false; // SSH-level backpressure - if (incoming.window <= WINDOW_THRESHOLD) - windowAdjust(self); - } - ); - } + // Incoming data + this._waitChanDrain = false; // Channel Readable side backpressure - // outgoing data - this._waitClientDrain = false; // Client stream-level backpressure - this._waitWindow = false; // SSH-level backpressure + this._chunk = undefined; + this._chunkcb = undefined; + this._chunkErr = undefined; + this._chunkcbErr = undefined; - // incoming data - this._waitChanDrain = false; // Channel Readable side backpressure + this.on('finish', onFinish) + .on('prefinish', onFinish); // For node v0.11+ - this._chunk = undefined; - this._chunkcb = undefined; - this._chunkErr = undefined; - this._chunkcbErr = undefined; - - function onFinish() { - self.eof(); - if (server || (!server && !self.allowHalfOpen)) - self.close(); - self.writable = false; - } - this.on('finish', onFinish) - .on('prefinish', onFinish); // for node v0.11+ - function onEnd() { - self.readable = false; + this.on('end', onEnd).on('close', onEnd); } - this.on('end', onEnd) - .on('close', onEnd); -} -inherits(Channel, DuplexStream); - -Channel.prototype.eof = function() { - var ret = true; - var outgoing = this.outgoing; - if (outgoing.state === 'open') { - outgoing.state = 'eof'; - ret = this._client._sshstream.channelEOF(outgoing.id); + _read(n) { + if (this._waitChanDrain) { + this._waitChanDrain = false; + if (this.incoming.window <= WINDOW_THRESHOLD) + windowAdjust(this); + } } - return ret; -}; + _write(data, encoding, cb) { + const protocol = this._client._protocol; + const outgoing = this.outgoing; + const packetSize = outgoing.packetSize; + const id = outgoing.id; + let window = outgoing.window; + const len = data.length; + let p = 0; -Channel.prototype.close = function() { - var ret = true; - var outgoing = this.outgoing; + if (outgoing.state !== 'open') + return; - if (outgoing.state === 'open' || outgoing.state === 'eof') { - outgoing.state = 'closing'; - ret = this._client._sshstream.channelClose(outgoing.id); - } + while (len - p > 0 && window > 0) { + let sliceLen = len - p; + if (sliceLen > window) + sliceLen = window; + if (sliceLen > packetSize) + sliceLen = packetSize; - return ret; -}; + if (p === 0 && sliceLen === len) + protocol.channelData(id, data); + else + protocol.channelData(id, bufferSlice(data, p, p + sliceLen)); -Channel.prototype._read = function(n) { - if (this._waitChanDrain) { - this._waitChanDrain = false; - if (this.incoming.window <= WINDOW_THRESHOLD) - windowAdjust(this); - } -}; + p += sliceLen; + window -= sliceLen; + } -Channel.prototype._write = function(data, encoding, cb) { - var sshstream = this._client._sshstream; - var outgoing = this.outgoing; - var packetSize = outgoing.packetSize; - var id = outgoing.id; - var window = outgoing.window; - var len = data.length; - var p = 0; - var ret; - var buf; - var sliceLen; - - if (outgoing.state !== 'open') - return; + outgoing.window = window; - while (len - p > 0 && window > 0) { - sliceLen = len - p; - if (sliceLen > window) - sliceLen = window; - if (sliceLen > packetSize) - sliceLen = packetSize; + if (len - p > 0) { + if (window === 0) + this._waitWindow = true; + if (p > 0) + this._chunk = bufferSlice(data, p, len); + else + this._chunk = data; + this._chunkcb = cb; + return; + } - ret = sshstream.channelData(id, data.slice(p, p + sliceLen)); + cb(); + } - p += sliceLen; - window -= sliceLen; + eof() { + if (this.outgoing.state === 'open') { + this.outgoing.state = 'eof'; + this._client._protocol.channelEOF(this.outgoing.id); + } + } - if (!ret) { - this._waitClientDrain = true; - this._chunk = undefined; - this._chunkcb = cb; - break; + close() { + if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') { + this.outgoing.state = 'closing'; + this._client._protocol.channelClose(this.outgoing.id); } } - outgoing.window = window; - - if (len - p > 0) { - if (window === 0) - this._waitWindow = true; - if (p > 0) { - // partial - buf = Buffer.allocUnsafe(len - p); - data.copy(buf, 0, p); - this._chunk = buf; - } else - this._chunk = data; - this._chunkcb = cb; - return; + destroy() { + this.end(); } - if (!this._waitClientDrain) - cb(); -}; + // Session type-specific methods ============================================= + setWindow(rows, cols, height, width) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + if (this.type === 'session' + && (this.subtype === 'shell' || this.subtype === 'exec') + && this.writable + && this.outgoing.state === 'open') { + this._client._protocol.windowChange(this.outgoing.id, + rows, + cols, + height, + width); + } + } -Channel.prototype.destroy = function() { - this.end(); -}; + signal(signalName) { + if (this.server) + throw new Error('Client-only method called in server mode'); -// session type-specific methods -Channel.prototype.setWindow = function(rows, cols, height, width) { - if (this.server) - throw new Error('Client-only method called in server mode'); - - if (this.type === 'session' - && (this.subtype === 'shell' || this.subtype === 'exec') - && this.writable - && this.outgoing.state === 'open') { - return this._client._sshstream.windowChange(this.outgoing.id, - rows, - cols, - height, - width); + if (this.type === 'session' + && this.writable + && this.outgoing.state === 'open') { + this._client._protocol.signal(this.outgoing.id, signalName); + } } - return true; -}; -Channel.prototype.signal = function(signalName) { - if (this.server) - throw new Error('Client-only method called in server mode'); + exit(statusOrSignal, coreDumped, msg) { + if (!this.server) + throw new Error('Server-only method called in client mode'); - if (this.type === 'session' - && this.writable - && this.outgoing.state === 'open') - return this._client._sshstream.signal(this.outgoing.id, signalName); - - return true; -}; -Channel.prototype.exit = function(name, coreDumped, msg) { - if (!this.server) - throw new Error('Server-only method called in client mode'); - - if (this.type === 'session' - && this.writable - && this.outgoing.state === 'open') { - if (typeof name === 'number') - return this._client._sshstream.exitStatus(this.outgoing.id, name); - else { - return this._client._sshstream.exitSignal(this.outgoing.id, - name, - coreDumped, - msg); + if (this.type === 'session' + && this.writable + && this.outgoing.state === 'open') { + if (typeof statusOrSignal === 'number') { + this._client._protocol.exitStatus(this.outgoing.id, statusOrSignal); + } else { + this._client._protocol.exitSignal(this.outgoing.id, + statusOrSignal, + coreDumped, + msg); + } } } - return true; -}; +} + +function onFinish() { + this.eof(); + if (this.server || !this.allowHalfOpen) + this.close(); + this.writable = false; +} -Channel.MAX_WINDOW = MAX_WINDOW; -Channel.PACKET_SIZE = PACKET_SIZE; +function onEnd() { + this.readable = false; +} function windowAdjust(self) { if (self.outgoing.state === 'closed') - return true; - var amt = MAX_WINDOW - self.incoming.window; + return; + const amt = MAX_WINDOW - self.incoming.window; if (amt <= 0) - return true; + return; self.incoming.window += amt; - return self._client._sshstream.channelWindowAdjust(self.outgoing.id, amt); + self._client._protocol.channelWindowAdjust(self.outgoing.id, amt); } -function ServerStderr(channel) { - WritableStream.call(this, { highWaterMark: MAX_WINDOW }); - this._channel = channel; -} -inherits(ServerStderr, WritableStream); - -ServerStderr.prototype._write = function(data, encoding, cb) { - var channel = this._channel; - var sshstream = channel._client._sshstream; - var outgoing = channel.outgoing; - var packetSize = outgoing.packetSize; - var id = outgoing.id; - var window = outgoing.window; - var len = data.length; - var p = 0; - var ret; - var buf; - var sliceLen; - - if (channel.outgoing.state !== 'open') - return; - - while (len - p > 0 && window > 0) { - sliceLen = len - p; - if (sliceLen > window) - sliceLen = window; - if (sliceLen > packetSize) - sliceLen = packetSize; - - ret = sshstream.channelExtData(id, data.slice(p, p + sliceLen), STDERR); - - p += sliceLen; - window -= sliceLen; - - if (!ret) { - channel._waitClientDrain = true; - channel._chunkErr = undefined; - channel._chunkcbErr = cb; - break; - } - } - - outgoing.window = window; - - if (len - p > 0) { - if (window === 0) - channel._waitWindow = true; - if (p > 0) { - // partial - buf = Buffer.allocUnsafe(len - p); - data.copy(buf, 0, p); - channel._chunkErr = buf; - } else - channel._chunkErr = data; - channel._chunkcbErr = cb; - return; - } - - if (!channel._waitClientDrain) - cb(); +module.exports = { + Channel, + MAX_WINDOW, + PACKET_SIZE, + windowAdjust, + WINDOW_THRESHOLD, }; - -module.exports = Channel; diff --git a/lib/SFTPWrapper.js b/lib/SFTPWrapper.js deleted file mode 100644 index 620dd38a..00000000 --- a/lib/SFTPWrapper.js +++ /dev/null @@ -1,145 +0,0 @@ -// This wrapper class is used to retain backwards compatibility with -// pre-v0.4 ssh2. If it weren't for `read()` and `write()` being used by the -// streams2/3 API, we could just pass the SFTPStream directly to the end user... - -var inherits = require('util').inherits; -var EventEmitter = require('events').EventEmitter; - -function SFTPWrapper(stream) { - var self = this; - - EventEmitter.call(this); - - this._stream = stream; - - stream.on('error', function(err) { - self.emit('error', err); - }).on('end', function() { - self.emit('end'); - }).on('close', function() { - self.emit('close'); - }).on('continue', function() { - self.emit('continue'); - }); -} -inherits(SFTPWrapper, EventEmitter); - -// stream-related methods to pass on -SFTPWrapper.prototype.end = function() { - return this._stream.end(); -}; -// SFTPStream client methods -SFTPWrapper.prototype.createReadStream = function(path, options) { - return this._stream.createReadStream(path, options); -}; -SFTPWrapper.prototype.createWriteStream = function(path, options) { - return this._stream.createWriteStream(path, options); -}; -SFTPWrapper.prototype.open = function(path, flags, attrs, cb) { - return this._stream.open(path, flags, attrs, cb); -}; -SFTPWrapper.prototype.close = function(handle, cb) { - return this._stream.close(handle, cb); -}; -SFTPWrapper.prototype.read = function(handle, buf, off, len, position, cb) { - return this._stream.readData(handle, buf, off, len, position, cb); -}; -SFTPWrapper.prototype.write = function(handle, buf, off, len, position, cb) { - return this._stream.writeData(handle, buf, off, len, position, cb); -}; -SFTPWrapper.prototype.fastGet = function(remotePath, localPath, opts, cb) { - return this._stream.fastGet(remotePath, localPath, opts, cb); -}; -SFTPWrapper.prototype.fastPut = function(localPath, remotePath, opts, cb) { - return this._stream.fastPut(localPath, remotePath, opts, cb); -}; -SFTPWrapper.prototype.readFile = function(path, options, callback_) { - return this._stream.readFile(path, options, callback_); -}; -SFTPWrapper.prototype.writeFile = function(path, data, options, callback_) { - return this._stream.writeFile(path, data, options, callback_); -}; -SFTPWrapper.prototype.appendFile = function(path, data, options, callback_) { - return this._stream.appendFile(path, data, options, callback_); -}; -SFTPWrapper.prototype.exists = function(path, cb) { - return this._stream.exists(path, cb); -}; -SFTPWrapper.prototype.unlink = function(filename, cb) { - return this._stream.unlink(filename, cb); -}; -SFTPWrapper.prototype.rename = function(oldPath, newPath, cb) { - return this._stream.rename(oldPath, newPath, cb); -}; -SFTPWrapper.prototype.mkdir = function(path, attrs, cb) { - return this._stream.mkdir(path, attrs, cb); -}; -SFTPWrapper.prototype.rmdir = function(path, cb) { - return this._stream.rmdir(path, cb); -}; -SFTPWrapper.prototype.readdir = function(where, opts, cb) { - return this._stream.readdir(where, opts, cb); -}; -SFTPWrapper.prototype.fstat = function(handle, cb) { - return this._stream.fstat(handle, cb); -}; -SFTPWrapper.prototype.stat = function(path, cb) { - return this._stream.stat(path, cb); -}; -SFTPWrapper.prototype.lstat = function(path, cb) { - return this._stream.lstat(path, cb); -}; -SFTPWrapper.prototype.opendir = function(path, cb) { - return this._stream.opendir(path, cb); -}; -SFTPWrapper.prototype.setstat = function(path, attrs, cb) { - return this._stream.setstat(path, attrs, cb); -}; -SFTPWrapper.prototype.fsetstat = function(handle, attrs, cb) { - return this._stream.fsetstat(handle, attrs, cb); -}; -SFTPWrapper.prototype.futimes = function(handle, atime, mtime, cb) { - return this._stream.futimes(handle, atime, mtime, cb); -}; -SFTPWrapper.prototype.utimes = function(path, atime, mtime, cb) { - return this._stream.utimes(path, atime, mtime, cb); -}; -SFTPWrapper.prototype.fchown = function(handle, uid, gid, cb) { - return this._stream.fchown(handle, uid, gid, cb); -}; -SFTPWrapper.prototype.chown = function(path, uid, gid, cb) { - return this._stream.chown(path, uid, gid, cb); -}; -SFTPWrapper.prototype.fchmod = function(handle, mode, cb) { - return this._stream.fchmod(handle, mode, cb); -}; -SFTPWrapper.prototype.chmod = function(path, mode, cb) { - return this._stream.chmod(path, mode, cb); -}; -SFTPWrapper.prototype.readlink = function(path, cb) { - return this._stream.readlink(path, cb); -}; -SFTPWrapper.prototype.symlink = function(targetPath, linkPath, cb) { - return this._stream.symlink(targetPath, linkPath, cb); -}; -SFTPWrapper.prototype.realpath = function(path, cb) { - return this._stream.realpath(path, cb); -}; -// extended requests -SFTPWrapper.prototype.ext_openssh_rename = function(oldPath, newPath, cb) { - return this._stream.ext_openssh_rename(oldPath, newPath, cb); -}; -SFTPWrapper.prototype.ext_openssh_statvfs = function(path, cb) { - return this._stream.ext_openssh_statvfs(path, cb); -}; -SFTPWrapper.prototype.ext_openssh_fstatvfs = function(handle, cb) { - return this._stream.ext_openssh_fstatvfs(handle, cb); -}; -SFTPWrapper.prototype.ext_openssh_hardlink = function(oldPath, newPath, cb) { - return this._stream.ext_openssh_hardlink(oldPath, newPath, cb); -}; -SFTPWrapper.prototype.ext_openssh_fsync = function(handle, cb) { - return this._stream.ext_openssh_fsync(handle, cb); -}; - -module.exports = SFTPWrapper; diff --git a/lib/agent.js b/lib/agent.js index 953c6ffd..4ad67580 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,44 +1,50 @@ -var Socket = require('net').Socket; -var EventEmitter = require('events').EventEmitter; -var inherits = require('util').inherits; -var path = require('path'); -var fs = require('fs'); -var cp = require('child_process'); - -var readUInt32BE = require('./buffer-helpers').readUInt32BE; -var writeUInt32BE = require('./buffer-helpers').writeUInt32BE; -var writeUInt32LE = require('./buffer-helpers').writeUInt32LE; - -var REQUEST_IDENTITIES = 11; -var IDENTITIES_ANSWER = 12; -var SIGN_REQUEST = 13; -var SIGN_RESPONSE = 14; -var FAILURE = 5; - -var RE_CYGWIN_SOCK = /^\!(\d+) s ([A-Z0-9]{8}\-[A-Z0-9]{8}\-[A-Z0-9]{8}\-[A-Z0-9]{8})/; - -// Format of `//./pipe/ANYTHING`, with forward slashes and backward slashes being interchangeable -var WINDOWS_PIPE_REGEX = /^[/\\][/\\]\.[/\\]pipe[/\\].+/; - -module.exports = function(sockPath, key, keyType, data, cb) { - var sock; - var error; - var sig; - var datalen; - var keylen = 0; - var isSigning = Buffer.isBuffer(key); - var type; - var count = 0; - var siglen = 0; - var nkeys = 0; - var keys; - var comlen = 0; - var comment = false; - var accept; - var reject; +'use strict'; + +const { Socket } = require('net'); +const EventEmitter = require('events'); +const { resolve } = require('path'); +const { readFile } = require('fs'); +const { execFile, spawn } = require('child_process'); + +const { + readUInt32BE, + writeUInt32BE, + writeUInt32LE, +} = require('./protocol/utils.js'); + +const REQUEST_IDENTITIES = 11; +const IDENTITIES_ANSWER = 12; +const SIGN_REQUEST = 13; +const SIGN_RESPONSE = 14; +const FAILURE = 5; + +let PageantSock; + +const RE_CYGWIN_SOCK = /^!(\d+) s ([A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8})/; + +// Format of `//./pipe/ANYTHING`, with forward slashes and backward slashes +// being interchangeable +const WINDOWS_PIPE_REGEX = /^[/\\][/\\]\.[/\\]pipe[/\\].+/; + +module.exports = (sockPath, key, keyType, data, cb) => { + let sock; + let error; + let sig; + let datalen; + let keylen = 0; + const isSigning = Buffer.isBuffer(key); + let type; + let count = 0; + let siglen = 0; + let nkeys = 0; + let keys; + let comlen = 0; + let comment = false; + let accept; + let reject; if (typeof key === 'function' && typeof keyType === 'function') { - // agent forwarding + // Agent forwarding accept = key; reject = keyType; } else if (isSigning) { @@ -50,7 +56,7 @@ module.exports = function(sockPath, key, keyType, data, cb) { } function onconnect() { - var buf; + let buf; if (isSigning) { /* byte SSH2_AGENTC_SIGN_REQUEST @@ -58,7 +64,7 @@ module.exports = function(sockPath, key, keyType, data, cb) { string data uint32 flags */ - var p = 9; + let p = 9; buf = Buffer.allocUnsafe(4 + 1 + 4 + keylen + 4 + datalen + 4); writeUInt32BE(buf, buf.length - 4, 0); buf[4] = SIGN_REQUEST; @@ -75,10 +81,11 @@ module.exports = function(sockPath, key, keyType, data, cb) { sock.write(Buffer.from([0, 0, 0, 1, REQUEST_IDENTITIES])); } } + function ondata(chunk) { - for (var i = 0, len = chunk.length; i < len; ++i) { + for (let i = 0; i < chunk.length; ++i) { if (type === undefined) { - // skip over packet length + // Skip over packet length if (++count === 5) { type = chunk[i]; count = 0; @@ -99,7 +106,8 @@ module.exports = function(sockPath, key, keyType, data, cb) { sig[count] = chunk[i]; if (++count === siglen) { sock.removeAllListeners('data'); - return sock.destroy(); + sock.destroy(); + return; } } } else if (type === IDENTITIES_ANSWER) { @@ -123,48 +131,43 @@ module.exports = function(sockPath, key, keyType, data, cb) { return sock.destroy(); } } - } else { - if (!key) { - keylen <<= 8; - keylen += chunk[i]; - if (++count === 4) { - key = Buffer.allocUnsafe(keylen); - count = 0; - } - } else if (comment === false) { - key[count] = chunk[i]; - if (++count === keylen) { - keys[nkeys - 1] = key; - keylen = 0; - count = 0; - comment = true; - if (--nkeys === 0) { - key = undefined; - sock.removeAllListeners('data'); - return sock.destroy(); - } - } - } else if (comment === true) { - comlen <<= 8; - comlen += chunk[i]; - if (++count === 4) { - count = 0; - if (comlen > 0) - comment = comlen; - else { - key = undefined; - comment = false; - } - comlen = 0; + } else if (!key) { + keylen <<= 8; + keylen += chunk[i]; + if (++count === 4) { + key = Buffer.allocUnsafe(keylen); + count = 0; + } + } else if (comment === false) { + key[count] = chunk[i]; + if (++count === keylen) { + keys[nkeys - 1] = key; + keylen = 0; + count = 0; + comment = true; + if (--nkeys === 0) { + key = undefined; + sock.removeAllListeners('data'); + return sock.destroy(); } - } else { - // skip comments - if (++count === comment) { - comment = false; - count = 0; + } + } else if (comment === true) { + comlen <<= 8; + comlen += chunk[i]; + if (++count === 4) { + count = 0; + if (comlen > 0) { + comment = comlen; + } else { key = undefined; + comment = false; } + comlen = 0; } + } else if (++count === comment) { // Skip comments + comment = false; + count = 0; + key = undefined; } } else if (type === FAILURE) { if (isSigning) @@ -176,9 +179,11 @@ module.exports = function(sockPath, key, keyType, data, cb) { } } } + function onerror(err) { error = err; } + function onclose() { if (error) cb(error); @@ -195,52 +200,45 @@ module.exports = function(sockPath, key, keyType, data, cb) { // Pageant (PuTTY authentication agent) sock = new PageantSock(); } else { - // cygwin ssh-agent instance - var triedCygpath = false; - fs.readFile(sockPath, function readCygsocket(err, data) { + // Cygwin ssh-agent instance + let triedCygpath = false; + readFile(sockPath, function readCygsocket(err, data) { if (err) { if (triedCygpath) return cb(new Error('Invalid cygwin unix socket path')); - // try using `cygpath` to convert a possible *nix-style path to the + // Try using `cygpath` to convert a possible *nix-style path to the // real Windows path before giving up ... - cp.exec('cygpath -w "' + sockPath + '"', - function(err, stdout, stderr) { + execFile('cygpath', ['-w', sockPath], (err, stdout, stderr) => { if (err || stdout.length === 0) return cb(new Error('Invalid cygwin unix socket path')); triedCygpath = true; sockPath = stdout.toString().replace(/[\r\n]/g, ''); - fs.readFile(sockPath, readCygsocket); + readFile(sockPath, readCygsocket); }); return; } - var m; + let m; if (m = RE_CYGWIN_SOCK.exec(data.toString('ascii'))) { - var port; - var secret; - var secretbuf; - var state; - var bc = 0; - var isRetrying = false; - var inbuf = []; - var credsbuf = Buffer.allocUnsafe(12); - var i; - var j; - - // use 0 for pid, uid, and gid to ensure we get an error and also + let state; + let bc = 0; + let isRetrying = false; + const inbuf = []; + + // Use 0 for pid, uid, and gid to ensure we get an error and also // a valid uid and gid from cygwin so that we don't have to figure it // out ourselves - credsbuf.fill(0); + let credsbuf = Buffer.alloc(12); - // parse cygwin unix socket file contents - port = parseInt(m[1], 10); - secret = m[2].replace(/\-/g, ''); - secretbuf = Buffer.allocUnsafe(16); - for (i = 0, j = 0; j < 32; ++i,j+=2) + // Parse cygwin unix socket file contents + const port = parseInt(m[1], 10); + const secret = m[2].replace(/-/g, ''); + const secretbuf = Buffer.allocUnsafe(16); + for (let i = 0, j = 0; j < 32; ++i, j += 2) secretbuf[i] = parseInt(secret.substring(j, j + 2), 16); - // convert to host order (always LE for Windows) - for (i = 0; i < 16; i += 4) + // Convert to host order (always LE for Windows) + for (let i = 0; i < 16; i += 4) writeUInt32LE(secretbuf, readUInt32BE(secretbuf, i), i); function _onconnect() { @@ -248,10 +246,11 @@ module.exports = function(sockPath, key, keyType, data, cb) { state = 'secret'; sock.write(secretbuf); } + function _ondata(data) { bc += data.length; if (state === 'secret') { - // the secret we sent is echoed back to us by cygwin, not sure of + // The secret we sent is echoed back to us by cygwin, not sure of // the reason for that, but we ignore it nonetheless ... if (bc === 16) { bc = 0; @@ -259,7 +258,7 @@ module.exports = function(sockPath, key, keyType, data, cb) { sock.write(credsbuf); } } else if (state === 'creds') { - // if this is the first attempt, make sure to gather the valid + // If this is the first attempt, make sure to gather the valid // uid and gid for our next attempt if (!isRetrying) inbuf.push(data); @@ -281,9 +280,11 @@ module.exports = function(sockPath, key, keyType, data, cb) { } } } + function _onclose() { cb(new Error('Problem negotiating cygwin unix socket security')); } + function tryConnect() { sock = new Socket(); sock.once('connect', _onconnect); @@ -291,14 +292,17 @@ module.exports = function(sockPath, key, keyType, data, cb) { sock.once('close', _onclose); sock.connect(port); } + tryConnect(); - } else + } else { cb(new Error('Malformed cygwin unix socket file')); + } }); return; } - } else + } else { sock = new Socket(); + } function addSockListeners() { if (!accept && !reject) { @@ -307,10 +311,10 @@ module.exports = function(sockPath, key, keyType, data, cb) { sock.once('error', onerror); sock.once('close', onclose); } else { - var chan; - sock.once('connect', function() { + let chan; + sock.once('connect', () => { chan = accept(); - var isDone = false; + let isDone = false; function onDone() { if (isDone) return; @@ -319,14 +323,10 @@ module.exports = function(sockPath, key, keyType, data, cb) { } chan.once('end', onDone) .once('close', onDone) - .on('data', function(data) { - sock.write(data); - }); - sock.on('data', function(data) { - chan.write(data); - }); + .on('data', (data) => sock.write(data)); + sock.on('data', (data) => chan.write(data)); }); - sock.once('close', function() { + sock.once('close', () => { if (!chan) reject(); }); @@ -336,84 +336,91 @@ module.exports = function(sockPath, key, keyType, data, cb) { sock.connect(sockPath); }; - -// win32 only ------------------------------------------------------------------ if (process.platform === 'win32') { - var RET_ERR_BADARGS = 10; - var RET_ERR_UNAVAILABLE = 11; - var RET_ERR_NOMAP = 12; - var RET_ERR_BINSTDIN = 13; - var RET_ERR_BINSTDOUT = 14; - var RET_ERR_BADLEN = 15; - - var ERROR = {}; - var EXEPATH = path.resolve(__dirname, '..', 'util/pagent.exe'); - ERROR[RET_ERR_BADARGS] = new Error('Invalid pagent.exe arguments'); - ERROR[RET_ERR_UNAVAILABLE] = new Error('Pageant is not running'); - ERROR[RET_ERR_NOMAP] = new Error('pagent.exe could not create an mmap'); - ERROR[RET_ERR_BINSTDIN] = new Error('pagent.exe could not set mode for stdin'); - ERROR[RET_ERR_BINSTDOUT] = new Error('pagent.exe could not set mode for stdout'); - ERROR[RET_ERR_BADLEN] = new Error('pagent.exe did not get expected input payload'); - - function PageantSock() { - this.proc = undefined; - this.buffer = null; - } - inherits(PageantSock, EventEmitter); - - PageantSock.prototype.write = function(buf) { - if (this.buffer === null) - this.buffer = buf; - else { - this.buffer = Buffer.concat([this.buffer, buf], - this.buffer.length + buf.length); - } - // Wait for at least all length bytes - if (this.buffer.length < 4) - return; + const RET_ERR_BADARGS = 10; + const RET_ERR_UNAVAILABLE = 11; + const RET_ERR_NOMAP = 12; + const RET_ERR_BINSTDIN = 13; + const RET_ERR_BINSTDOUT = 14; + const RET_ERR_BADLEN = 15; - var len = readUInt32BE(this.buffer, 0); - // Make sure we have a full message before querying pageant - if ((this.buffer.length - 4) < len) - return; + const EXEPATH = resolve(__dirname, '..', 'util/pagent.exe'); + const ERROR = { + [RET_ERR_BADARGS]: new Error('Invalid pagent.exe arguments'), + [RET_ERR_UNAVAILABLE]: new Error('Pageant is not running'), + [RET_ERR_NOMAP]: new Error('pagent.exe could not create an mmap'), + [RET_ERR_BINSTDIN]: new Error('pagent.exe could not set mode for stdin'), + [RET_ERR_BINSTDOUT]: new Error('pagent.exe could not set mode for stdout'), + [RET_ERR_BADLEN]: + new Error('pagent.exe did not get expected input payload'), + }; - buf = this.buffer.slice(0, 4 + len); - if (this.buffer.length > (4 + len)) - this.buffer = this.buffer.slice(4 + len); - else + PageantSock = class PageantSock extends EventEmitter { + constructor() { + super(); + this.proc = undefined; this.buffer = null; + } - var self = this; - var proc; - var hadError = false; - proc = this.proc = cp.spawn(EXEPATH, [ buf.length ]); - proc.stdout.on('data', function(data) { - self.emit('data', data); - }); - proc.once('error', function(err) { - if (!hadError) { - hadError = true; - self.emit('error', err); + write(buf) { + if (this.buffer === null) { + this.buffer = buf; + } else { + this.buffer = Buffer.concat([this.buffer, buf], + this.buffer.length + buf.length); } - }); - proc.once('close', function(code) { - self.proc = undefined; - if (ERROR[code] && !hadError) { - hadError = true; - self.emit('error', ERROR[code]); + // Wait for at least all length bytes + if (this.buffer.length < 4) + return; + + const len = readUInt32BE(this.buffer, 0); + // Make sure we have a full message before querying pageant + if ((this.buffer.length - 4) < len) + return; + + buf = this.buffer.slice(0, 4 + len); + if (this.buffer.length > (4 + len)) + this.buffer = this.buffer.slice(4 + len); + else + this.buffer = null; + + let hadError = false; + const proc = this.proc = spawn(EXEPATH, [ buf.length ]); + proc.stdout.on('data', (data) => { + this.emit('data', data); + }); + proc.once('error', (err) => { + if (!hadError) { + hadError = true; + this.emit('error', err); + } + }); + proc.once('close', (code) => { + this.proc = undefined; + let err; + if (!hadError && (err = ERROR[code])) { + hadError = true; + this.emit('error', err); + } + this.emit('close', hadError); + }); + proc.stdin.end(buf); + } + + end() { + this.buffer = null; + if (this.proc) { + this.proc.kill(); + this.proc = undefined; } - self.emit('close', hadError); - }); - proc.stdin.end(buf); - }; - PageantSock.prototype.end = PageantSock.prototype.destroy = function() { - this.buffer = null; - if (this.proc) { - this.proc.kill(); - this.proc = undefined; } - }; - PageantSock.prototype.connect = function() { - this.emit('connect'); + + destroy() { + this.end(); + } + + connect() { + this.emit('connect'); + } }; } diff --git a/lib/buffer-helpers.js b/lib/buffer-helpers.js deleted file mode 100644 index 4381995b..00000000 --- a/lib/buffer-helpers.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - readUInt32BE: function readUInt32BE(buf, offset) { - return buf[offset++] * 16777216 - + buf[offset++] * 65536 - + buf[offset++] * 256 - + buf[offset]; - }, - writeUInt32BE: function writeUInt32BE(buf, value, offset) { - buf[offset++] = (value >>> 24); - buf[offset++] = (value >>> 16); - buf[offset++] = (value >>> 8); - buf[offset++] = value; - return offset; - }, - writeUInt32LE: function writeUInt32LE(buf, value, offset) { - buf[offset++] = value; - buf[offset++] = (value >>> 8); - buf[offset++] = (value >>> 16); - buf[offset++] = (value >>> 24); - return offset; - } -}; diff --git a/lib/client.js b/lib/client.js index e3ed0781..a3026747 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,1237 +1,1368 @@ -var crypto = require('crypto'); -var Socket = require('net').Socket; -var dnsLookup = require('dns').lookup; -var EventEmitter = require('events').EventEmitter; -var inherits = require('util').inherits; -var HASHES = crypto.getHashes(); - -var ssh2_streams = require('ssh2-streams'); -var SSH2Stream = ssh2_streams.SSH2Stream; -var SFTPStream = ssh2_streams.SFTPStream; -var consts = ssh2_streams.constants; -var BUGS = consts.BUGS; -var ALGORITHMS = consts.ALGORITHMS; -var EDDSA_SUPPORTED = consts.EDDSA_SUPPORTED; -var parseKey = ssh2_streams.utils.parseKey; - -var HTTPAgents = require('./http-agents'); -var Channel = require('./Channel'); -var agentQuery = require('./agent'); -var SFTPWrapper = require('./SFTPWrapper'); -var readUInt32BE = require('./buffer-helpers').readUInt32BE; - -var MAX_CHANNEL = Math.pow(2, 32) - 1; -var RE_OPENSSH = /^OpenSSH_(?:(?![0-4])\d)|(?:\d{2,})/; -var DEBUG_NOOP = function(msg) {}; - -function Client() { - if (!(this instanceof Client)) - return new Client(); - - EventEmitter.call(this); - - this.config = { - host: undefined, - port: undefined, - localAddress: undefined, - localPort: undefined, - forceIPv4: undefined, - forceIPv6: undefined, - keepaliveCountMax: undefined, - keepaliveInterval: undefined, - readyTimeout: undefined, - - username: undefined, - password: undefined, - privateKey: undefined, - tryKeyboard: undefined, - agent: undefined, - allowAgentFwd: undefined, - authHandler: undefined, - - hostHashAlgo: undefined, - hostHashCb: undefined, - strictVendor: undefined, - debug: undefined - }; - - this._readyTimeout = undefined; - this._channels = undefined; - this._callbacks = undefined; - this._forwarding = undefined; - this._forwardingUnix = undefined; - this._acceptX11 = undefined; - this._agentFwdEnabled = undefined; - this._curChan = undefined; - this._remoteVer = undefined; - - this._sshstream = undefined; - this._sock = undefined; - this._resetKA = undefined; -} -inherits(Client, EventEmitter); - -Client.prototype.connect = function(cfg) { - var self = this; +// TODO: emit error when connection severed early (e.g. before handshake) +// TODO: add '.connected' or similar property to allow immediate connection +// status checking +'use strict'; + +const { + createHash, + getHashes, + randomFillSync, +} = require('crypto'); +const { Socket } = require('net'); +const { lookup: dnsLookup } = require('dns'); +const EventEmitter = require('events'); +const HASHES = getHashes(); + +const { + COMPAT, + CHANNEL_EXTENDED_DATATYPE: { STDERR }, + CHANNEL_OPEN_FAILURE, + DEFAULT_CIPHER, + DEFAULT_COMPRESSION, + DEFAULT_KEX, + DEFAULT_MAC, + DEFAULT_SERVER_HOST_KEY, + DISCONNECT_REASON, + DISCONNECT_REASON_BY_VALUE, + EDDSA_SUPPORTED, + SUPPORTED_CIPHER, + SUPPORTED_COMPRESSION, + SUPPORTED_KEX, + SUPPORTED_MAC, + SUPPORTED_SERVER_HOST_KEY, +} = require('./protocol/constants.js'); +const Protocol = require('./protocol/Protocol.js'); +const { parseKey } = require('./protocol/keyParser.js'); +const { SFTP } = require('./protocol/SFTP.js'); +const { readUInt32BE } = require('./protocol/utils.js'); + +const agentQuery = require('./agent.js'); +const { + Channel, + MAX_WINDOW, + PACKET_SIZE, + windowAdjust, + WINDOW_THRESHOLD, +} = require('./Channel.js'); +const { + ChannelManager, + generateAlgorithmList, + onChannelOpenFailure, + onCHANNEL_CLOSE, +} = require('./utils.js'); + +const RE_OPENSSH = /^OpenSSH_(?:(?![0-4])\d)|(?:\d{2,})/; +const noop = (err) => {}; + +class Client extends EventEmitter { + constructor() { + super(); + + this.config = { + host: undefined, + port: undefined, + localAddress: undefined, + localPort: undefined, + forceIPv4: undefined, + forceIPv6: undefined, + keepaliveCountMax: undefined, + keepaliveInterval: undefined, + readyTimeout: undefined, + + username: undefined, + password: undefined, + privateKey: undefined, + tryKeyboard: undefined, + agent: undefined, + allowAgentFwd: undefined, + authHandler: undefined, + + hostHashAlgo: undefined, + hostHashCb: undefined, + strictVendor: undefined, + debug: undefined + }; - if (this._sock && this._sock.writable) { - this.once('close', function() { - self.connect(cfg); - }); - this.end(); - return; + this._readyTimeout = undefined; + this._chanMgr = undefined; + this._callbacks = undefined; + this._forwarding = undefined; + this._forwardingUnix = undefined; + this._acceptX11 = undefined; + this._agentFwdEnabled = undefined; + this._remoteVer = undefined; + + this._protocol = undefined; + this._sock = undefined; + this._resetKA = undefined; } - this.config.host = cfg.hostname || cfg.host || 'localhost'; - this.config.port = cfg.port || 22; - this.config.localAddress = (typeof cfg.localAddress === 'string' - ? cfg.localAddress - : undefined); - this.config.localPort = (typeof cfg.localPort === 'string' - || typeof cfg.localPort === 'number' - ? cfg.localPort - : undefined); - this.config.forceIPv4 = cfg.forceIPv4 || false; - this.config.forceIPv6 = cfg.forceIPv6 || false; - this.config.keepaliveCountMax = (typeof cfg.keepaliveCountMax === 'number' - && cfg.keepaliveCountMax >= 0 - ? cfg.keepaliveCountMax - : 3); - this.config.keepaliveInterval = (typeof cfg.keepaliveInterval === 'number' - && cfg.keepaliveInterval > 0 - ? cfg.keepaliveInterval - : 0); - this.config.readyTimeout = (typeof cfg.readyTimeout === 'number' - && cfg.readyTimeout >= 0 - ? cfg.readyTimeout - : 20000); - - var algorithms = { - kex: undefined, - kexBuf: undefined, - cipher: undefined, - cipherBuf: undefined, - serverHostKey: undefined, - serverHostKeyBuf: undefined, - hmac: undefined, - hmacBuf: undefined, - compress: undefined, - compressBuf: undefined - }; - var i; - if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) { - var algosSupported; - var algoList; - - algoList = cfg.algorithms.kex; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_KEX; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) - throw new Error('Unsupported key exchange algorithm: ' + algoList[i]); - } - algorithms.kex = algoList; + connect(cfg) { + if (this._sock && this._sock.writable) { + this.once('close', () => { + this.connect(cfg); + }); + this.end(); + return this; } - algoList = cfg.algorithms.cipher; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_CIPHER; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) - throw new Error('Unsupported cipher algorithm: ' + algoList[i]); - } - algorithms.cipher = algoList; + this.config.host = cfg.hostname || cfg.host || 'localhost'; + this.config.port = cfg.port || 22; + this.config.localAddress = (typeof cfg.localAddress === 'string' + ? cfg.localAddress + : undefined); + this.config.localPort = (typeof cfg.localPort === 'string' + || typeof cfg.localPort === 'number' + ? cfg.localPort + : undefined); + this.config.forceIPv4 = cfg.forceIPv4 || false; + this.config.forceIPv6 = cfg.forceIPv6 || false; + this.config.keepaliveCountMax = (typeof cfg.keepaliveCountMax === 'number' + && cfg.keepaliveCountMax >= 0 + ? cfg.keepaliveCountMax + : 3); + this.config.keepaliveInterval = (typeof cfg.keepaliveInterval === 'number' + && cfg.keepaliveInterval > 0 + ? cfg.keepaliveInterval + : 0); + this.config.readyTimeout = (typeof cfg.readyTimeout === 'number' + && cfg.readyTimeout >= 0 + ? cfg.readyTimeout + : 20000); + + const algorithms = { + kex: undefined, + srvHostKey: undefined, + cs: { + cipher: undefined, + mac: undefined, + compress: undefined, + lang: [], + }, + sc: undefined, + }; + let allOfferDefaults = true; + if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) { + + algorithms.kex = generateAlgorithmList(cfg.algorithms.kex, + DEFAULT_KEX, + SUPPORTED_KEX); + if (algorithms.kex !== DEFAULT_KEX) + allOfferDefaults = false; + + algorithms.srvHostKey = + generateAlgorithmList(cfg.algorithms.serverHostKey, + DEFAULT_SERVER_HOST_KEY, + SUPPORTED_SERVER_HOST_KEY); + if (algorithms.srvHostKey !== DEFAULT_SERVER_HOST_KEY) + allOfferDefaults = false; + + algorithms.cs.cipher = generateAlgorithmList(cfg.algorithms.cipher, + DEFAULT_CIPHER, + SUPPORTED_CIPHER); + if (algorithms.cs.cipher !== DEFAULT_CIPHER) + allOfferDefaults = false; + + algorithms.cs.mac = generateAlgorithmList(cfg.algorithms.hmac, + DEFAULT_MAC, + SUPPORTED_MAC); + if (algorithms.cs.mac !== DEFAULT_MAC) + allOfferDefaults = false; + + algorithms.cs.compress = generateAlgorithmList(cfg.algorithms.compress, + DEFAULT_COMPRESSION, + SUPPORTED_COMPRESSION); + if (algorithms.cs.compress !== DEFAULT_COMPRESSION) + allOfferDefaults = false; + + if (!allOfferDefaults) + algorithms.sc = algorithms.cs; } - algoList = cfg.algorithms.serverHostKey; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_SERVER_HOST_KEY; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) { - throw new Error('Unsupported server host key algorithm: ' - + algoList[i]); - } - } - algorithms.serverHostKey = algoList; - } + if (typeof cfg.username === 'string') + this.config.username = cfg.username; + else if (typeof cfg.user === 'string') + this.config.username = cfg.user; + else + throw new Error('Invalid username'); - algoList = cfg.algorithms.hmac; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_HMAC; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) - throw new Error('Unsupported HMAC algorithm: ' + algoList[i]); - } - algorithms.hmac = algoList; - } + this.config.password = (typeof cfg.password === 'string' + ? cfg.password + : undefined); + this.config.privateKey = (typeof cfg.privateKey === 'string' + || Buffer.isBuffer(cfg.privateKey) + ? cfg.privateKey + : undefined); + this.config.localHostname = (typeof cfg.localHostname === 'string' + && cfg.localHostname.length + ? cfg.localHostname + : undefined); + this.config.localUsername = (typeof cfg.localUsername === 'string' + && cfg.localUsername.length + ? cfg.localUsername + : undefined); + this.config.tryKeyboard = (cfg.tryKeyboard === true); + this.config.agent = (typeof cfg.agent === 'string' && cfg.agent.length + ? cfg.agent + : undefined); + this.config.allowAgentFwd = (cfg.agentForward === true + && this.config.agent !== undefined); + let authHandler = this.config.authHandler = ( + typeof cfg.authHandler === 'function' ? cfg.authHandler : undefined + ); - algoList = cfg.algorithms.compress; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_COMPRESS; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) - throw new Error('Unsupported compression algorithm: ' + algoList[i]); - } - algorithms.compress = algoList; - } - } - if (algorithms.compress === undefined) { - if (cfg.compress) { - algorithms.compress = ['zlib@openssh.com', 'zlib']; - if (cfg.compress !== 'force') - algorithms.compress.push('none'); - } else if (cfg.compress === false) - algorithms.compress = ['none']; - } + this.config.strictVendor = (typeof cfg.strictVendor === 'boolean' + ? cfg.strictVendor + : true); - if (typeof cfg.username === 'string') - this.config.username = cfg.username; - else if (typeof cfg.user === 'string') - this.config.username = cfg.user; - else - throw new Error('Invalid username'); - - this.config.password = (typeof cfg.password === 'string' - ? cfg.password - : undefined); - this.config.privateKey = (typeof cfg.privateKey === 'string' - || Buffer.isBuffer(cfg.privateKey) - ? cfg.privateKey - : undefined); - this.config.localHostname = (typeof cfg.localHostname === 'string' - && cfg.localHostname.length - ? cfg.localHostname - : undefined); - this.config.localUsername = (typeof cfg.localUsername === 'string' - && cfg.localUsername.length - ? cfg.localUsername - : undefined); - this.config.tryKeyboard = (cfg.tryKeyboard === true); - this.config.agent = (typeof cfg.agent === 'string' && cfg.agent.length - ? cfg.agent - : undefined); - this.config.allowAgentFwd = (cfg.agentForward === true - && this.config.agent !== undefined); - var authHandler = this.config.authHandler = ( - typeof cfg.authHandler === 'function' ? cfg.authHandler : undefined - ); - - this.config.strictVendor = (typeof cfg.strictVendor === 'boolean' - ? cfg.strictVendor - : true); - - var debug = this.config.debug = (typeof cfg.debug === 'function' - ? cfg.debug - : DEBUG_NOOP); - - if (cfg.agentForward === true && !this.config.allowAgentFwd) - throw new Error('You must set a valid agent path to allow agent forwarding'); - - var callbacks = this._callbacks = []; - this._channels = {}; - this._forwarding = {}; - this._forwardingUnix = {}; - this._acceptX11 = 0; - this._agentFwdEnabled = false; - this._curChan = -1; - this._remoteVer = undefined; - var privateKey; - - if (this.config.privateKey) { - privateKey = parseKey(this.config.privateKey, cfg.passphrase); - if (privateKey instanceof Error) - throw new Error('Cannot parse privateKey: ' + privateKey.message); - if (Array.isArray(privateKey)) - privateKey = privateKey[0]; // OpenSSH's newer format only stores 1 key for now - if (privateKey.getPrivatePEM() === null) - throw new Error('privateKey value does not contain a (valid) private key'); - } + const debug = this.config.debug = (typeof cfg.debug === 'function' + ? cfg.debug + : undefined); - var stream = this._sshstream = new SSH2Stream({ - algorithms: algorithms, - debug: (debug === DEBUG_NOOP ? undefined : debug) - }); - var sock = this._sock = (cfg.sock || new Socket()); - - // drain stderr if we are connection hopping using an exec stream - if (this._sock.stderr && typeof this._sock.stderr.resume === 'function') - this._sock.stderr.resume(); - - // keepalive-related - var kainterval = this.config.keepaliveInterval; - var kacountmax = this.config.keepaliveCountMax; - var kacount = 0; - var katimer; - function sendKA() { - if (++kacount > kacountmax) { - clearInterval(katimer); - if (sock.readable) { - var err = new Error('Keepalive timeout'); - err.level = 'client-timeout'; - self.emit('error', err); - sock.destroy(); - } - return; + if (cfg.agentForward === true && !this.config.allowAgentFwd) { + throw new Error( + 'You must set a valid agent path to allow agent forwarding' + ); } - if (sock.writable) { - // append dummy callback to keep correct callback order - callbacks.push(resetKA); - stream.ping(); - } else - clearInterval(katimer); - } - function resetKA() { - if (kainterval > 0) { - kacount = 0; - clearInterval(katimer); - if (sock.writable) - katimer = setInterval(sendKA, kainterval); - } - } - this._resetKA = resetKA; - stream.on('USERAUTH_BANNER', function(msg) { - self.emit('banner', msg); - }); + let callbacks = this._callbacks = []; + this._chanMgr = new ChannelManager(this); + this._forwarding = {}; + this._forwardingUnix = {}; + this._acceptX11 = 0; + this._agentFwdEnabled = false; + this._remoteVer = undefined; + let privateKey; + + if (this.config.privateKey) { + privateKey = parseKey(this.config.privateKey, cfg.passphrase); + if (privateKey instanceof Error) + throw new Error(`Cannot parse privateKey: ${privateKey.message}`); + if (Array.isArray(privateKey)) { + // OpenSSH's newer format only stores 1 key for now + privateKey = privateKey[0]; + } + if (privateKey.getPrivatePEM() === null) { + throw new Error( + 'privateKey value does not contain a (valid) private key' + ); + } + } - sock.on('connect', function() { - debug('DEBUG: Client: Connected'); - self.emit('connect'); - if (!cfg.sock) - stream.pipe(sock).pipe(stream); - }).on('timeout', function() { - self.emit('timeout'); - }).on('error', function(err) { - clearTimeout(self._readyTimeout); - err.level = 'client-socket'; - self.emit('error', err); - }).on('end', function() { - stream.unpipe(sock); - clearTimeout(self._readyTimeout); - clearInterval(katimer); - self.emit('end'); - }).on('close', function() { - stream.unpipe(sock); - clearTimeout(self._readyTimeout); - clearInterval(katimer); - self.emit('close'); - - // notify outstanding channel requests of disconnection ... - var callbacks_ = callbacks; - var err = new Error('No response from server'); - callbacks = self._callbacks = []; - for (i = 0; i < callbacks_.length; ++i) - callbacks_[i](err); - - // simulate error for any channels waiting to be opened. this is safe - // against successfully opened channels because the success and failure - // event handlers are automatically removed when a success/failure response - // is received - var channels = self._channels; - var chanNos = Object.keys(channels); - self._channels = {}; - for (i = 0; i < chanNos.length; ++i) { - var ev1 = stream.emit('CHANNEL_OPEN_FAILURE:' + chanNos[i], err); - // emitting CHANNEL_CLOSE should be safe too and should help for any - // special channels which might otherwise keep the process alive, such - // as agent forwarding channels which have open unix sockets ... - var ev2 = stream.emit('CHANNEL_CLOSE:' + chanNos[i]); - var earlyCb; - if (!ev1 && !ev2 && (earlyCb = channels[chanNos[i]]) - && typeof earlyCb === 'function') { - earlyCb(err); + let hostVerifier; + if (typeof cfg.hostVerifier === 'function') { + const hashCb = cfg.hostVerifier; + let hasher; + if (HASHES.indexOf(cfg.hostHash) !== -1) { + // Default to old behavior of hashing on user's behalf + hasher = createHash(cfg.hostHash); } + hostVerifier = (key, verify) => { + if (hasher) { + hasher.update(key); + key = hasher.digest('hex'); + } + const ret = hashCb(key, verify); + if (ret !== undefined) + verify(ret); + }; } - }); - stream.on('drain', function() { - self.emit('drain'); - }).once('header', function(header) { - self._remoteVer = header.versions.software; - if (header.greeting) - self.emit('greeting', header.greeting); - }).on('continue', function() { - self.emit('continue'); - }).on('error', function(err) { - if (err.level === undefined) - err.level = 'protocol'; - else if (err.level === 'handshake') - clearTimeout(self._readyTimeout); - self.emit('error', err); - }).on('end', function() { - sock.resume(); - }); - if (typeof cfg.hostVerifier === 'function') { - if (HASHES.indexOf(cfg.hostHash) === -1) - throw new Error('Invalid host hash algorithm: ' + cfg.hostHash); - var hashCb = cfg.hostVerifier; - var hasher = crypto.createHash(cfg.hostHash); - stream.once('fingerprint', function(key, verify) { - hasher.update(key); - var ret = hashCb(hasher.digest('hex'), verify); - if (ret !== undefined) - verify(ret); + const sock = this._sock = (cfg.sock || new Socket()); + let ready = false; + if (this._protocol) + this._protocol.cleanup(); + const DEBUG_HANDLER = (!debug ? undefined : (p, display, msg) => { + debug(`Debug output from server: ${JSON.stringify(msg)}`); }); - } + const proto = this._protocol = new Protocol({ + offer: (allOfferDefaults ? undefined : algorithms), + onWrite: (data) => { + if (sock.writable) + sock.write(data); + }, + onError: (err) => { + if (err.level === 'handshake') + clearTimeout(this._readyTimeout); + if (!proto._destruct) + sock.removeAllListeners('data'); + this.emit('error', err); + try { + sock.end(); + } catch {} + }, + onHeader: (header) => { + this._remoteVer = header.versions.software; + if (header.greeting) + this.emit('greeting', header.greeting); + }, + onHandshakeComplete: (negotiated) => { + this.emit('handshake', negotiated); + if (!ready) { + ready = true; + proto.service('ssh-userauth'); + } + }, + debug, + hostVerifier, + messageHandlers: { + DEBUG: DEBUG_HANDLER, + DISCONNECT: (p, reason, desc) => { + if (reason !== DISCONNECT_REASON.BY_APPLICATION) { + if (!desc) { + desc = DISCONNECT_REASON_BY_VALUE[reason]; + if (desc === undefined) + desc = `Unexpected disconnection reason: ${reason}`; + } + const err = new Error(desc); + err.code = reason; + this.emit('error', err); + } + sock.end(); + }, + SERVICE_ACCEPT: (p, name) => { + if (name === 'ssh-userauth') + tryNextAuth(); + }, + USERAUTH_BANNER: (p, msg) => { + this.emit('banner', msg); + }, + USERAUTH_SUCCESS: (p) => { + // Start keepalive mechanism + resetKA(); + + clearTimeout(this._readyTimeout); + + this.emit('ready'); + }, + USERAUTH_FAILURE: (p, authMethods, partialSuccess) => { + if (curAuth === 'agent') { + debug && debug(`Client: Agent key #${agentKeyPos + 1} failed`); + return tryNextAgentKey(); + } - // begin authentication handling ============================================= - var curAuth; - var curPartial = null; - var curAuthsLeft = null; - var agentKeys; - var agentKeyPos = 0; - var authsAllowed = ['none']; - if (this.config.password !== undefined) - authsAllowed.push('password'); - if (privateKey !== undefined) - authsAllowed.push('publickey'); - if (this.config.agent !== undefined) - authsAllowed.push('agent'); - if (this.config.tryKeyboard) - authsAllowed.push('keyboard-interactive'); - if (privateKey !== undefined - && this.config.localHostname !== undefined - && this.config.localUsername !== undefined) { - authsAllowed.push('hostbased'); - } + debug && debug(`Client: ${curAuth} auth failed`); + + curPartial = partialSuccess; + curAuthsLeft = authMethods; + tryNextAuth(); + }, + USERAUTH_PK_OK: (p) => { + if (curAuth === 'agent') { + const agentKey = agentKeys[agentKeyPos]; + const keyLen = readUInt32BE(agentKey, 0); + const pubKeyFullType = agentKey.utf8Slice(4, 4 + keyLen); + const pubKeyType = pubKeyFullType.slice(4); + // Check that we support the key type first + // TODO: move key type checking logic to protocol implementation + switch (pubKeyFullType) { + case 'ssh-rsa': + case 'ssh-dss': + case 'ecdsa-sha2-nistp256': + case 'ecdsa-sha2-nistp384': + case 'ecdsa-sha2-nistp521': + break; + case 'ssh-ed25519': + if (EDDSA_SUPPORTED) + break; + // FALLTHROUGH + default: + debug && debug( + `Agent: Skipping unsupported key type: ${pubKeyFullType}` + ); + return tryNextAgentKey(); + } + proto.authPK(this.config.username, agentKey, (buf, cb) => { + agentQuery(this.config.agent, + agentKey, + pubKeyType, + buf, + (err, signed) => { + if (err) { + err.level = 'agent'; + this.emit('error', err); + } else { + const sigFullTypeLen = readUInt32BE(signed, 0); + if (4 + sigFullTypeLen + 4 < signed.length) { + const sigFullType = signed.utf8Slice(4, 4 + sigFullTypeLen); + if (sigFullType !== pubKeyFullType) { + err = new Error('Agent key/signature type mismatch'); + err.level = 'agent'; + this.emit('error', err); + } else { + // Skip algoLen + algo + sigLen + return cb(signed.slice(4 + sigFullTypeLen + 4)); + } + } + } + + tryNextAgentKey(); + }); + }); + } else if (curAuth === 'publickey') { + proto.authPK(this.config.username, privateKey, (buf, cb) => { + const signature = privateKey.sign(buf); + if (signature instanceof Error) { + signature.message = + `Error signing data with privateKey: ${signature.message}`; + signature.level = 'client-authentication'; + this.emit('error', signature); + return tryNextAuth(); + } + cb(signature); + }); + } + }, + USERAUTH_INFO_REQUEST: (p, name, instructions, prompts) => { + const nprompts = (Array.isArray(prompts) ? prompts.length : 0); + if (nprompts === 0) { + debug && debug('Client: Sending automatic USERAUTH_INFO_RESPONSE'); + proto.authInfoRes(); + return; + } + // We sent a keyboard-interactive user authentication request and now + // the server is sending us the prompts we need to present to the user + this.emit('keyboard-interactive', + name, + instructions, + '', + prompts, + (answers) => { + proto.authInfoRes(answers); + } + ); + }, + REQUEST_SUCCESS: (p, data) => { + if (callbacks.length) + callbacks.shift()(false, data); + }, + REQUEST_FAILURE: (p) => { + if (callbacks.length) + callbacks.shift()(true); + }, + GLOBAL_REQUEST: (name, wantReply, data) => { + // Auto-reject all global requests, this can be especially useful if + // the server is sending us dummy keepalive global requests + if (wantReply) + proto.requestFailure(); + }, + CHANNEL_OPEN: (p, info) => { + // Handle incoming requests from server, typically a forwarded TCP or + // X11 connection + onCHANNEL_OPEN(this, info); + }, + CHANNEL_OPEN_CONFIRMATION: (p, info) => { + const channel = this._chanMgr.get(info.recipient); + if (typeof channel !== 'function') + return; + + const isSFTP = (channel.type === 'sftp'); + const type = (isSFTP ? 'session' : channel.type); + const chanInfo = { + type, + incoming: { + id: info.recipient, + window: MAX_WINDOW, + packetSize: PACKET_SIZE, + state: 'open' + }, + outgoing: { + id: info.sender, + window: info.window, + packetSize: info.packetSize, + state: 'open' + } + }; + const instance = ( + isSFTP + ? new SFTP(this, chanInfo, { debug }) + : new Channel(this, chanInfo) + ); + this._chanMgr.update(info.recipient, instance); + channel(undefined, instance); + }, + CHANNEL_OPEN_FAILURE: (p, recipient, reason, description) => { + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'function') + return; + + const info = { reason, description }; + onChannelOpenFailure(this, recipient, info, channel); + }, + CHANNEL_DATA: (p, recipient, data) => { + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; + + // The remote party should not be sending us data if there is no + // window space available ... + // TODO: raise error on data with not enough window? + if (channel.incoming.window === 0) + return; + + channel.incoming.window -= data.length; + + if (channel.push(data) === false) { + channel._waitChanDrain = true; + return; + } - if (authHandler === undefined) { - var authPos = 0; - authHandler = function authHandler(authsLeft, partial, cb) { - if (authPos === authsAllowed.length) - return false; - return authsAllowed[authPos++]; - }; - } + if (channel.incoming.window <= WINDOW_THRESHOLD) + windowAdjust(channel); + }, + CHANNEL_EXTENDED_DATA: (p, recipient, data, type) => { + if (type !== STDERR) + return; - var hasSentAuth = false; - function doNextAuth(authName) { - hasSentAuth = true; - if (authName === false) { - stream.removeListener('USERAUTH_FAILURE', onUSERAUTH_FAILURE); - stream.removeListener('USERAUTH_PK_OK', onUSERAUTH_PK_OK); - var err = new Error('All configured authentication methods failed'); - err.level = 'client-authentication'; - self.emit('error', err); - if (stream.writable) - self.end(); - return; - } - if (authsAllowed.indexOf(authName) === -1) - throw new Error('Authentication method not allowed: ' + authName); - curAuth = authName; - switch (curAuth) { - case 'password': - stream.authPassword(self.config.username, self.config.password); - break; - case 'publickey': - stream.authPK(self.config.username, privateKey); - stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK); - break; - case 'hostbased': - function hostbasedCb(buf, cb) { - var signature = privateKey.sign(buf); - if (signature instanceof Error) { - signature.message = 'Error while signing data with privateKey: ' - + signature.message; - signature.level = 'client-authentication'; - self.emit('error', signature); - return tryNextAuth(); + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; + + // The remote party should not be sending us data if there is no + // window space available ... + // TODO: raise error on data with not enough window? + if (channel.incoming.window === 0) + return; + + channel.incoming.window -= data.length; + + if (!channel.stderr.push(data)) { + channel._waitChanDrain = true; + return; } - cb(signature); - } - stream.authHostbased(self.config.username, - privateKey, - self.config.localHostname, - self.config.localUsername, - hostbasedCb); - break; - case 'agent': - agentQuery(self.config.agent, function(err, keys) { - if (err) { - err.level = 'agent'; - self.emit('error', err); - agentKeys = undefined; - return tryNextAuth(); - } else if (keys.length === 0) { - debug('DEBUG: Agent: No keys stored in agent'); - agentKeys = undefined; - return tryNextAuth(); + if (channel.incoming.window <= WINDOW_THRESHOLD) + windowAdjust(channel); + }, + CHANNEL_WINDOW_ADJUST: (p, recipient, amount) => { + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; + + // The other side is allowing us to send `amount` more bytes of data + channel.outgoing.window += amount; + + if (channel._waitWindow) { + channel._waitWindow = false; + + if (channel._chunk) { + channel._write(channel._chunk, null, channel._chunkcb); + } else if (channel._chunkcb) { + channel._chunkcb(); + } else if (channel._chunkErr) { + channel.stderr._write(channel._chunkErr, + null, + channel._chunkcbErr); + } else if (channel._chunkcbErr) { + channel._chunkcbErr(); + } + } + }, + CHANNEL_SUCCESS: (p, recipient) => { + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; + + this._resetKA(); + + if (channel._callbacks.length) + channel._callbacks.shift()(false); + }, + CHANNEL_FAILURE: (p, recipient) => { + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; + + this._resetKA(); + + if (channel._callbacks.length) + channel._callbacks.shift()(true); + }, + CHANNEL_REQUEST: (p, recipient, type, wantReply, data) => { + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; + + const exit = channel._exit; + if (exit.code !== undefined) + return; + switch (type) { + case 'exit-status': + channel.emit('exit', exit.code = data); + return; + case 'exit-signal': + channel.emit('exit', + exit.code = null, + exit.signal = `SIG${data.signal}`, + exit.dump = data.coreDumped, + exit.desc = data.errorMessage); + return; } - agentKeys = keys; - agentKeyPos = 0; + // Keepalive request? OpenSSH will send one as a channel request if + // there is a channel open + + if (wantReply) + p.channelFailure(channel.outgoing.id); + }, + CHANNEL_EOF: (p, recipient) => { + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; + + if (channel.incoming.state !== 'open') + return; + channel.incoming.state = 'eof'; + + if (channel.readable) + channel.push(null); + if (channel.stderr.readable) + channel.stderr.push(null); + }, + CHANNEL_CLOSE: (p, recipient) => { + onCHANNEL_CLOSE(this, recipient, this._chanMgr.get(recipient)); + }, + }, + }); + + sock.on('data', (data) => { + // TODO: wrap in try-catch and emit caught error(s) + proto.parse(data, 0, data.length); + }); - stream.authPK(self.config.username, keys[0]); - stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK); - }); - break; - case 'keyboard-interactive': - stream.authKeyboard(self.config.username); - stream.on('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST); - break; - case 'none': - stream.authNone(self.config.username); - break; - } - } - function tryNextAuth() { - hasSentAuth = false; - var auth = authHandler(curAuthsLeft, curPartial, doNextAuth); - if (hasSentAuth || auth === undefined) - return; - doNextAuth(auth); - } - function tryNextAgentKey() { - if (curAuth === 'agent') { - if (agentKeyPos >= agentKeys.length) + // Drain stderr if we are connection hopping using an exec stream + if (sock.stderr && typeof sock.stderr.resume === 'function') + sock.stderr.resume(); + + // TODO: check keepalive implementation + // Keepalive-related + const kainterval = this.config.keepaliveInterval; + const kacountmax = this.config.keepaliveCountMax; + let kacount = 0; + let katimer; + const sendKA = () => { + if (++kacount > kacountmax) { + clearInterval(katimer); + if (sock.readable) { + const err = new Error('Keepalive timeout'); + err.level = 'client-timeout'; + this.emit('error', err); + sock.destroy(); + } return; - if (++agentKeyPos >= agentKeys.length) { - debug('DEBUG: Agent: No more keys left to try'); - debug('DEBUG: Client: agent auth failed'); - agentKeys = undefined; - tryNextAuth(); + } + if (sock.writable) { + // Append dummy callback to keep correct callback order + callbacks.push(resetKA); + proto.ping(); } else { - debug('DEBUG: Agent: Trying key #' + (agentKeyPos + 1)); - stream.authPK(self.config.username, agentKeys[agentKeyPos]); - stream.once('USERAUTH_PK_OK', onUSERAUTH_PK_OK); + clearInterval(katimer); } - } - } - function onUSERAUTH_INFO_REQUEST(name, instructions, lang, prompts) { - var nprompts = (Array.isArray(prompts) ? prompts.length : 0); - if (nprompts === 0) { - debug('DEBUG: Client: Sending automatic USERAUTH_INFO_RESPONSE'); - return stream.authInfoRes(); - } - // we sent a keyboard-interactive user authentication request and now the - // server is sending us the prompts we need to present to the user - self.emit('keyboard-interactive', - name, - instructions, - lang, - prompts, - function(answers) { - stream.authInfoRes(answers); - } - ); - } - function onUSERAUTH_PK_OK() { - if (curAuth === 'agent') { - var agentKey = agentKeys[agentKeyPos]; - var keyLen = readUInt32BE(agentKey, 0); - var pubKeyFullType = agentKey.toString('ascii', 4, 4 + keyLen); - var pubKeyType = pubKeyFullType.slice(4); - // Check that we support the key type first - // TODO: move key type checking logic to ssh2-streams - switch (pubKeyFullType) { - case 'ssh-rsa': - case 'ssh-dss': - case 'ecdsa-sha2-nistp256': - case 'ecdsa-sha2-nistp384': - case 'ecdsa-sha2-nistp521': - break; - default: - if (EDDSA_SUPPORTED && pubKeyFullType === 'ssh-ed25519') - break; - debug('DEBUG: Agent: Skipping unsupported key type: ' - + pubKeyFullType); - return tryNextAgentKey(); + }; + function resetKA() { + if (kainterval > 0) { + kacount = 0; + clearInterval(katimer); + if (sock.writable) + katimer = setInterval(sendKA, kainterval); } - stream.authPK(self.config.username, - agentKey, - function(buf, cb) { - agentQuery(self.config.agent, - agentKey, - pubKeyType, - buf, - function(err, signed) { - if (err) { - err.level = 'agent'; - self.emit('error', err); - } else { - var sigFullTypeLen = readUInt32BE(signed, 0); - if (4 + sigFullTypeLen + 4 < signed.length) { - var sigFullType = signed.toString('ascii', 4, 4 + sigFullTypeLen); - if (sigFullType !== pubKeyFullType) { - err = new Error('Agent key/signature type mismatch'); - err.level = 'agent'; - self.emit('error', err); - } else { - // skip algoLen + algo + sigLen - return cb(signed.slice(4 + sigFullTypeLen + 4)); - } - } - } - - tryNextAgentKey(); - }); - }); - } else if (curAuth === 'publickey') { - stream.authPK(self.config.username, privateKey, function(buf, cb) { - var signature = privateKey.sign(buf); - if (signature instanceof Error) { - signature.message = 'Error while signing data with privateKey: ' - + signature.message; - signature.level = 'client-authentication'; - self.emit('error', signature); - return tryNextAuth(); - } - cb(signature); - }); } - } - function onUSERAUTH_FAILURE(authsLeft, partial) { - stream.removeListener('USERAUTH_PK_OK', onUSERAUTH_PK_OK); - stream.removeListener('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST); - if (curAuth === 'agent') { - debug('DEBUG: Client: Agent key #' + (agentKeyPos + 1) + ' failed'); - return tryNextAgentKey(); - } else { - debug('DEBUG: Client: ' + curAuth + ' auth failed'); - } - - curPartial = partial; - curAuthsLeft = authsLeft; - tryNextAuth(); - } - stream.once('USERAUTH_SUCCESS', function() { - stream.removeListener('USERAUTH_FAILURE', onUSERAUTH_FAILURE); - stream.removeListener('USERAUTH_INFO_REQUEST', onUSERAUTH_INFO_REQUEST); - - // start keepalive mechanism - resetKA(); - - clearTimeout(self._readyTimeout); + this._resetKA = resetKA; + + sock.on('connect', () => { + debug && debug('Socket connected'); + this.emit('connect'); + }).on('timeout', () => { + this.emit('timeout'); + }).on('error', (err) => { + debug && debug(`Socket error: ${err.message}`); + clearTimeout(this._readyTimeout); + err.level = 'client-socket'; + this.emit('error', err); + }).on('end', () => { + debug && debug('Socket ended'); + proto.cleanup(); + clearTimeout(this._readyTimeout); + clearInterval(katimer); + this.emit('end'); + }).on('close', () => { + debug && debug('Socket closed'); + proto.cleanup(); + clearTimeout(this._readyTimeout); + clearInterval(katimer); + this.emit('close'); - self.emit('ready'); - }).on('USERAUTH_FAILURE', onUSERAUTH_FAILURE); - // end authentication handling =============================================== + // Notify outstanding channel requests of disconnection ... + const callbacks_ = callbacks; + callbacks = this._callbacks = []; + const err = new Error('No response from server'); + for (let i = 0; i < callbacks_.length; ++i) + callbacks_[i](err); - // handle initial handshake completion - stream.once('ready', function() { - stream.service('ssh-userauth'); - stream.once('SERVICE_ACCEPT', function(svcName) { - if (svcName === 'ssh-userauth') - tryNextAuth(); + // Simulate error for any channels waiting to be opened + this._chanMgr.cleanup(err); }); - }); - // handle incoming requests from server, typically a forwarded TCP or X11 - // connection - stream.on('CHANNEL_OPEN', function(info) { - onCHANNEL_OPEN(self, info); - }); + // Begin authentication handling =========================================== + let curAuth; + let curPartial = null; + let curAuthsLeft = null; + let agentKeys; + let agentKeyPos = 0; + const authsAllowed = ['none']; + if (this.config.password !== undefined) + authsAllowed.push('password'); + if (privateKey !== undefined) + authsAllowed.push('publickey'); + if (this.config.agent !== undefined) + authsAllowed.push('agent'); + if (this.config.tryKeyboard) + authsAllowed.push('keyboard-interactive'); + if (privateKey !== undefined + && this.config.localHostname !== undefined + && this.config.localUsername !== undefined) { + authsAllowed.push('hostbased'); + } - // handle responses for tcpip-forward and other global requests - stream.on('REQUEST_SUCCESS', function(data) { - if (callbacks.length) - callbacks.shift()(false, data); - }).on('REQUEST_FAILURE', function() { - if (callbacks.length) - callbacks.shift()(true); - }); + if (authHandler === undefined) { + let authPos = 0; + authHandler = (authsLeft, partial, cb) => { + if (authPos === authsAllowed.length) + return false; + return authsAllowed[authPos++]; + }; + } - stream.on('GLOBAL_REQUEST', function(name, wantReply, data) { - // auto-reject all global requests, this can be especially useful if the - // server is sending us dummy keepalive global requests - if (wantReply) - stream.requestFailure(); - }); + let hasSentAuth = false; + const doNextAuth = (authName) => { + hasSentAuth = true; + if (authName === false) { + const err = new Error('All configured authentication methods failed'); + err.level = 'client-authentication'; + this.emit('error', err); + this.end(); + return; + } + if (authsAllowed.indexOf(authName) === -1) + throw new Error(`Authentication method not allowed: ${authName}`); + curAuth = authName; + switch (curAuth) { + case 'password': + proto.authPassword(this.config.username, this.config.password); + break; + case 'publickey': + proto.authPK(this.config.username, privateKey); + break; + case 'hostbased': + function hostbasedCb(buf, cb) { + const signature = privateKey.sign(buf); + if (signature instanceof Error) { + signature.message = + `Error while signing with privateKey: ${signature.message}`; + signature.level = 'client-authentication'; + this.emit('error', signature); + return tryNextAuth(); + } - if (!cfg.sock) { - var host = this.config.host; - var forceIPv4 = this.config.forceIPv4; - var forceIPv6 = this.config.forceIPv6; + cb(signature); + } + proto.authHostbased(this.config.username, + privateKey, + this.config.localHostname, + this.config.localUsername, + hostbasedCb); + break; + case 'agent': + agentQuery(this.config.agent, (err, keys) => { + if (err) { + err.level = 'agent'; + this.emit('error', err); + agentKeys = undefined; + return tryNextAuth(); + } else if (keys.length === 0) { + debug && debug('Agent: No keys stored in agent'); + agentKeys = undefined; + return tryNextAuth(); + } - debug('DEBUG: Client: Trying ' - + host - + ' on port ' - + this.config.port - + ' ...'); + agentKeys = keys; + agentKeyPos = 0; - function doConnect() { - startTimeout(); - self._sock.connect({ - host: host, - port: self.config.port, - localAddress: self.config.localAddress, - localPort: self.config.localPort - }); - self._sock.setNoDelay(true); - self._sock.setMaxListeners(0); - self._sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0); + proto.authPK(this.config.username, keys[0]); + }); + break; + case 'keyboard-interactive': + proto.authKeyboard(this.config.username); + break; + case 'none': + proto.authNone(this.config.username); + break; + } + }; + function tryNextAuth() { + hasSentAuth = false; + const auth = authHandler(curAuthsLeft, curPartial, doNextAuth); + if (hasSentAuth || auth === undefined) + return; + doNextAuth(auth); } - - if ((!forceIPv4 && !forceIPv6) || (forceIPv4 && forceIPv6)) - doConnect(); - else { - dnsLookup(host, (forceIPv4 ? 4 : 6), function(err, address, family) { - if (err) { - var error = new Error('Error while looking up ' - + (forceIPv4 ? 'IPv4' : 'IPv6') - + ' address for host ' - + host - + ': ' + err); - clearTimeout(self._readyTimeout); - error.level = 'client-dns'; - self.emit('error', error); - self.emit('close'); + const tryNextAgentKey = () => { + if (curAuth === 'agent') { + if (agentKeyPos >= agentKeys.length) return; + if (++agentKeyPos >= agentKeys.length) { + debug && debug('Agent: No more keys left to try'); + debug && debug('Client: agent auth failed'); + agentKeys = undefined; + tryNextAuth(); + } else { + debug && debug(`Agent: Trying key #${agentKeyPos + 1}`); + proto.authPK(this.config.username, agentKeys[agentKeyPos]); } - host = address; + } + }; + + const startTimeout = () => { + if (this.config.readyTimeout > 0) { + this._readyTimeout = setTimeout(() => { + const err = new Error('Timed out while waiting for handshake'); + err.level = 'client-timeout'; + this.emit('error', err); + sock.destroy(); + }, this.config.readyTimeout); + } + }; + + if (!cfg.sock) { + let host = this.config.host; + const forceIPv4 = this.config.forceIPv4; + const forceIPv6 = this.config.forceIPv6; + + debug && debug(`Client: Trying ${host} on port ${this.config.port} ...`); + + const doConnect = () => { + startTimeout(); + sock.connect({ + host, + port: this.config.port, + localAddress: this.config.localAddress, + localPort: this.config.localPort + }); + sock.setNoDelay(true); + sock.setMaxListeners(0); + sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0); + }; + + if ((!forceIPv4 && !forceIPv6) || (forceIPv4 && forceIPv6)) { doConnect(); - }); + } else { + dnsLookup(host, (forceIPv4 ? 4 : 6), (err, address, family) => { + if (err) { + const type = (forceIPv4 ? 'IPv4' : 'IPv6'); + const error = new Error( + `Error while looking up ${type} address for '${host}': ${err}` + ); + clearTimeout(this._readyTimeout); + error.level = 'client-dns'; + this.emit('error', error); + this.emit('close'); + return; + } + host = address; + doConnect(); + }); + } + } else { + // Custom socket passed in + startTimeout(); } - } else { - startTimeout(); - stream.pipe(sock).pipe(stream); + + return this; } - function startTimeout() { - if (self.config.readyTimeout > 0) { - self._readyTimeout = setTimeout(function() { - var err = new Error('Timed out while waiting for handshake'); - err.level = 'client-timeout'; - self.emit('error', err); - sock.destroy(); - }, self.config.readyTimeout); + end() { + if (this._sock && this._sock.writable) { + this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION); + this._sock.end(); } + return this; } -}; - -Client.prototype.end = function() { - if (this._sock - && this._sock.writable - && this._sshstream - && this._sshstream.writable) - return this._sshstream.disconnect(); - return false; -}; - -Client.prototype.destroy = function() { - this._sock && this._sock.destroy(); -}; - -Client.prototype.exec = function(cmd, opts, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); - if (typeof opts === 'function') { - cb = opts; - opts = {}; + destroy() { + this._sock && this._sock.writable && this._sock.destroy(); + return this; } - var self = this; - var extraOpts = { allowHalfOpen: (opts.allowHalfOpen !== false) }; + exec(cmd, opts, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); - return openChannel(this, 'session', extraOpts, function(err, chan) { - if (err) - return cb(err); + if (typeof opts === 'function') { + cb = opts; + opts = {}; + } - var todo = []; + const extraOpts = { allowHalfOpen: (opts.allowHalfOpen !== false) }; - function reqCb(err) { + openChannel(this, 'session', extraOpts, (err, chan) => { if (err) { - chan.close(); - return cb(err); + cb(err); + return; } - if (todo.length) - todo.shift()(); - } - if (self.config.allowAgentFwd === true - || (opts - && opts.agentForward === true - && self.config.agent !== undefined)) { - todo.push(function() { - reqAgentFwd(chan, reqCb); - }); - } + const todo = []; - if (typeof opts === 'object' && opts !== null) { - if (typeof opts.env === 'object' && opts.env !== null) - reqEnv(chan, opts.env); - if ((typeof opts.pty === 'object' && opts.pty !== null) - || opts.pty === true) { - todo.push(function() { reqPty(chan, opts.pty, reqCb); }); + function reqCb(err) { + if (err) { + chan.close(); + cb(err); + return; + } + if (todo.length) + todo.shift()(); } - if ((typeof opts.x11 === 'object' && opts.x11 !== null) - || opts.x11 === 'number' - || opts.x11 === true) { - todo.push(function() { reqX11(chan, opts.x11, reqCb); }); + + if (this.config.allowAgentFwd === true + || (opts + && opts.agentForward === true + && this.config.agent !== undefined)) { + todo.push(() => reqAgentFwd(chan, reqCb)); } - } - todo.push(function() { reqExec(chan, cmd, opts, cb); }); - todo.shift()(); - }); -}; - -Client.prototype.shell = function(wndopts, opts, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); - - // start an interactive terminal/shell session - var self = this; - - if (typeof wndopts === 'function') { - cb = wndopts; - wndopts = opts = undefined; - } else if (typeof opts === 'function') { - cb = opts; - opts = undefined; - } - if (wndopts && (wndopts.x11 !== undefined || wndopts.env !== undefined)) { - opts = wndopts; - wndopts = undefined; - } + if (typeof opts === 'object' && opts !== null) { + if (typeof opts.env === 'object' && opts.env !== null) + reqEnv(chan, opts.env); + if ((typeof opts.pty === 'object' && opts.pty !== null) + || opts.pty === true) { + todo.push(() => reqPty(chan, opts.pty, reqCb)); + } + if ((typeof opts.x11 === 'object' && opts.x11 !== null) + || opts.x11 === 'number' + || opts.x11 === true) { + todo.push(() => reqX11(chan, opts.x11, reqCb)); + } + } - return openChannel(this, 'session', function(err, chan) { - if (err) - return cb(err); + todo.push(() => reqExec(chan, cmd, opts, cb)); + todo.shift()(); + }); - var todo = []; + return this; + } - function reqCb(err) { - if (err) { - chan.close(); - return cb(err); - } - if (todo.length) - todo.shift()(); - } + shell(wndopts, opts, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); - if (self.config.allowAgentFwd === true - || (opts - && opts.agentForward === true - && self.config.agent !== undefined)) { - todo.push(function() { reqAgentFwd(chan, reqCb); }); + if (typeof wndopts === 'function') { + cb = wndopts; + wndopts = opts = undefined; + } else if (typeof opts === 'function') { + cb = opts; + opts = undefined; } - - if (wndopts !== false) - todo.push(function() { reqPty(chan, wndopts, reqCb); }); - - if (typeof opts === 'object' && opts !== null) { - if (typeof opts.env === 'object' && opts.env !== null) - reqEnv(chan, opts.env); - if ((typeof opts.x11 === 'object' && opts.x11 !== null) - || opts.x11 === 'number' - || opts.x11 === true) { - todo.push(function() { reqX11(chan, opts.x11, reqCb); }); - } + if (wndopts && (wndopts.x11 !== undefined || wndopts.env !== undefined)) { + opts = wndopts; + wndopts = undefined; } - todo.push(function() { reqShell(chan, cb); }); - todo.shift()(); - }); -}; - -Client.prototype.subsys = function(name, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); - - return openChannel(this, 'session', function(err, chan) { - if (err) - return cb(err); - - reqSubsystem(chan, name, function(err, stream) { - if (err) - return cb(err); - - cb(undefined, stream); - }); - }); -}; - -Client.prototype.sftp = function(cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); - - var self = this; - - // start an SFTP session - return openChannel(this, 'session', function(err, chan) { - if (err) - return cb(err); - - reqSubsystem(chan, 'sftp', function(err, stream) { - if (err) - return cb(err); - - var serverIdentRaw = self._sshstream._state.incoming.identRaw; - var cfg = { debug: self.config.debug }; - var sftp = new SFTPStream(cfg, serverIdentRaw); - - function onError(err) { - sftp.removeListener('ready', onReady); - stream.removeListener('exit', onExit); + openChannel(this, 'session', (err, chan) => { + if (err) { cb(err); + return; } - function onReady() { - sftp.removeListener('error', onError); - stream.removeListener('exit', onExit); - cb(undefined, new SFTPWrapper(sftp)); - } + const todo = []; - function onExit(code, signal) { - sftp.removeListener('ready', onReady); - sftp.removeListener('error', onError); - var msg; - if (typeof code === 'number') { - msg = 'Received exit code ' - + code - + ' while establishing SFTP session'; - } else { - msg = 'Received signal ' - + signal - + ' while establishing SFTP session'; + function reqCb(err) { + if (err) { + chan.close(); + cb(err); + return; } - var err = new Error(msg); - err.code = code; - err.signal = signal; - cb(err); + if (todo.length) + todo.shift()(); } - sftp.once('error', onError) - .once('ready', onReady) - .once('close', function() { - stream.end(); - }); - - // OpenSSH server sends an exit-status if there was a problem spinning up - // an sftp server child process, so we listen for that here in order to - // properly raise an error. - stream.once('exit', onExit); + if (this.config.allowAgentFwd === true + || (opts + && opts.agentForward === true + && this.config.agent !== undefined)) { + todo.push(() => reqAgentFwd(chan, reqCb)); + } - sftp.pipe(stream).pipe(sftp); - }); - }); -}; + if (wndopts !== false) + todo.push(() => reqPty(chan, wndopts, reqCb)); -Client.prototype.forwardIn = function(bindAddr, bindPort, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); + if (typeof opts === 'object' && opts !== null) { + if (typeof opts.env === 'object' && opts.env !== null) + reqEnv(chan, opts.env); + if ((typeof opts.x11 === 'object' && opts.x11 !== null) + || opts.x11 === 'number' + || opts.x11 === true) { + todo.push(() => reqX11(chan, opts.x11, reqCb)); + } + } - // send a request for the server to start forwarding TCP connections to us - // on a particular address and port + todo.push(() => reqShell(chan, cb)); + todo.shift()(); + }); - var self = this; - var wantReply = (typeof cb === 'function'); + return this; + } - if (wantReply) { - this._callbacks.push(function(had_err, data) { - if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to bind to ' + bindAddr + ':' + bindPort)); - } + subsys(name, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); - var realPort = bindPort; - if (bindPort === 0 && data && data.length >= 4) { - realPort = readUInt32BE(data, 0); - if (!(self._sshstream.remoteBugs & BUGS.DYN_RPORT_BUG)) - bindPort = realPort; + openChannel(this, 'session', (err, chan) => { + if (err) { + cb(err); + return; } - self._forwarding[bindAddr + ':' + bindPort] = realPort; + reqSubsystem(chan, name, (err, stream) => { + if (err) { + cb(err); + return; + } - cb(undefined, realPort); + cb(undefined, stream); + }); }); - } - - return this._sshstream.tcpipForward(bindAddr, bindPort, wantReply); -}; -Client.prototype.unforwardIn = function(bindAddr, bindPort, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); + return this; + } - // send a request to stop forwarding us new connections for a particular - // address and port + forwardIn(bindAddr, bindPort, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); - var self = this; - var wantReply = (typeof cb === 'function'); + // Send a request for the server to start forwarding TCP connections to us + // on a particular address and port - if (wantReply) { - this._callbacks.push(function(had_err) { - if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to unbind from ' - + bindAddr + ':' + bindPort)); - } + const wantReply = (typeof cb === 'function'); - delete self._forwarding[bindAddr + ':' + bindPort]; + if (wantReply) { + this._callbacks.push((had_err, data) => { + if (had_err) { + cb(had_err !== true + ? had_err + : new Error(`Unable to bind to ${bindAddr}:${bindPort}`)); + return; + } - cb(); - }); - } + let realPort = bindPort; + if (bindPort === 0 && data && data.length >= 4) { + realPort = readUInt32BE(data, 0); + if (!(this._protocol._compatFlags & COMPAT.DYN_RPORT_BUG)) + bindPort = realPort; + } - return this._sshstream.cancelTcpipForward(bindAddr, bindPort, wantReply); -}; + this._forwarding[`${bindAddr}:${bindPort}`] = realPort; -Client.prototype.forwardOut = function(srcIP, srcPort, dstIP, dstPort, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); + cb(undefined, realPort); + }); + } - // send a request to forward a TCP connection to the server + this._protocol.tcpipForward(bindAddr, bindPort, wantReply); - var cfg = { - srcIP: srcIP, - srcPort: srcPort, - dstIP: dstIP, - dstPort: dstPort - }; + return this; + } - return openChannel(this, 'direct-tcpip', cfg, cb); -}; + unforwardIn(bindAddr, bindPort, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); -Client.prototype.openssh_noMoreSessions = function(cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); + // Send a request to stop forwarding us new connections for a particular + // address and port - var wantReply = (typeof cb === 'function'); + const wantReply = (typeof cb === 'function'); - if (!this.config.strictVendor - || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { if (wantReply) { - this._callbacks.push(function(had_err) { + this._callbacks.push((had_err) => { if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to disable future sessions')); + cb(had_err !== true + ? had_err + : new Error(`Unable to unbind from ${bindAddr}:${bindPort}`)); + return; } + delete this._forwarding[`${bindAddr}:${bindPort}`]; + cb(); }); } - return this._sshstream.openssh_noMoreSessions(wantReply); - } else if (wantReply) { - process.nextTick(function() { - cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version')); - }); + this._protocol.cancelTcpipForward(bindAddr, bindPort, wantReply); + + return this; } - return true; -}; + forwardOut(srcIP, srcPort, dstIP, dstPort, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); -Client.prototype.openssh_forwardInStreamLocal = function(socketPath, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); + // Send a request to forward a TCP connection to the server - var wantReply = (typeof cb === 'function'); - var self = this; + const cfg = { + srcIP: srcIP, + srcPort: srcPort, + dstIP: dstIP, + dstPort: dstPort + }; - if (!this.config.strictVendor - || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { - if (wantReply) { - this._callbacks.push(function(had_err) { - if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to bind to ' + socketPath)); - } - self._forwardingUnix[socketPath] = true; - cb(); - }); - } + if (typeof cb !== 'function') + cb = noop; - return this._sshstream.openssh_streamLocalForward(socketPath, wantReply); - } else if (wantReply) { - process.nextTick(function() { - cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version')); - }); - } + openChannel(this, 'direct-tcpip', cfg, cb); - return true; -}; + return this; + } -Client.prototype.openssh_unforwardInStreamLocal = function(socketPath, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); + openssh_noMoreSessions(cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); + + const wantReply = (typeof cb === 'function'); + + if (!this.config.strictVendor + || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { + if (wantReply) { + this._callbacks.push((had_err) => { + if (had_err) { + cb(had_err !== true + ? had_err + : new Error('Unable to disable future sessions')); + return; + } - var wantReply = (typeof cb === 'function'); - var self = this; + cb(); + }); + } - if (!this.config.strictVendor - || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { - if (wantReply) { - this._callbacks.push(function(had_err) { - if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to unbind on ' + socketPath)); - } - delete self._forwardingUnix[socketPath]; - cb(); - }); + this._protocol.openssh_noMoreSessions(wantReply); + return this; } - return this._sshstream.openssh_cancelStreamLocalForward(socketPath, - wantReply); - } else if (wantReply) { - process.nextTick(function() { - cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version')); - }); - } + if (!wantReply) + return this; - return true; -}; - -Client.prototype.openssh_forwardOutStreamLocal = function(socketPath, cb) { - if (!this._sock - || !this._sock.writable - || !this._sshstream - || !this._sshstream.writable) - throw new Error('Not connected'); - - if (!this.config.strictVendor - || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { - var cfg = { socketPath: socketPath }; - return openChannel(this, 'direct-streamlocal@openssh.com', cfg, cb); - } else { - process.nextTick(function() { - cb(new Error('strictVendor enabled and server is not OpenSSH or compatible version')); - }); + process.nextTick( + cb, + new Error( + 'strictVendor enabled and server is not OpenSSH or compatible version' + ) + ); + + return this; } - return true; -}; + openssh_forwardInStreamLocal(socketPath, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); + + const wantReply = (typeof cb === 'function'); + + if (!this.config.strictVendor + || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { + if (wantReply) { + this._callbacks.push((had_err) => { + if (had_err) { + cb(had_err !== true + ? had_err + : new Error(`Unable to bind to ${socketPath}`)); + return; + } + this._forwardingUnix[socketPath] = true; + cb(); + }); + } -function openChannel(self, type, opts, cb) { - // ask the server to open a channel for some purpose - // (e.g. session (sftp, exec, shell), or forwarding a TCP connection - var localChan = nextChannel(self); - var initWindow = Channel.MAX_WINDOW; - var maxPacket = Channel.PACKET_SIZE; - var ret = true; + this._protocol.openssh_streamLocalForward(socketPath, wantReply); + return this; + } - if (localChan === false) - return cb(new Error('No free channels available')); + if (!wantReply) + return this; - if (typeof opts === 'function') { - cb = opts; - opts = {}; - } + process.nextTick( + cb, + new Error( + 'strictVendor enabled and server is not OpenSSH or compatible version' + ) + ); - self._channels[localChan] = cb; - - var sshstream = self._sshstream; - sshstream.once('CHANNEL_OPEN_CONFIRMATION:' + localChan, onSuccess) - .once('CHANNEL_OPEN_FAILURE:' + localChan, onFailure) - .once('CHANNEL_CLOSE:' + localChan, onFailure); - - if (type === 'session') - ret = sshstream.session(localChan, initWindow, maxPacket); - else if (type === 'direct-tcpip') - ret = sshstream.directTcpip(localChan, initWindow, maxPacket, opts); - else if (type === 'direct-streamlocal@openssh.com') { - ret = sshstream.openssh_directStreamLocal(localChan, - initWindow, - maxPacket, - opts); + return this; } - return ret; + openssh_unforwardInStreamLocal(socketPath, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); + + const wantReply = (typeof cb === 'function'); + + if (!this.config.strictVendor + || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { + if (wantReply) { + this._callbacks.push((had_err) => { + if (had_err) { + cb(had_err !== true + ? had_err + : new Error(`Unable to unbind from ${socketPath}`)); + return; + } + delete this._forwardingUnix[socketPath]; + cb(); + }); + } - function onSuccess(info) { - sshstream.removeListener('CHANNEL_OPEN_FAILURE:' + localChan, onFailure); - sshstream.removeListener('CHANNEL_CLOSE:' + localChan, onFailure); + this._protocol.openssh_cancelStreamLocalForward(socketPath, wantReply); + return this; + } - var chaninfo = { - type: type, - incoming: { - id: localChan, - window: initWindow, - packetSize: maxPacket, - state: 'open' - }, - outgoing: { - id: info.sender, - window: info.window, - packetSize: info.packetSize, - state: 'open' - } - }; - cb(undefined, new Channel(chaninfo, self)); + if (!wantReply) + return this; + + process.nextTick( + cb, + new Error( + 'strictVendor enabled and server is not OpenSSH or compatible version' + ) + ); + + return this; } - function onFailure(info) { - sshstream.removeListener('CHANNEL_OPEN_CONFIRMATION:' + localChan, - onSuccess); - sshstream.removeListener('CHANNEL_OPEN_FAILURE:' + localChan, onFailure); - sshstream.removeListener('CHANNEL_CLOSE:' + localChan, onFailure); - - delete self._channels[localChan]; - - var err; - if (info instanceof Error) - err = info; - else if (typeof info === 'object' && info !== null) { - err = new Error('(SSH) Channel open failure: ' + info.description); - err.reason = info.reason; - err.lang = info.lang; - } else { - err = new Error('(SSH) Channel open failure: ' - + 'server closed channel unexpectedly'); - err.reason = err.lang = ''; + openssh_forwardOutStreamLocal(socketPath, cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); + + if (typeof cb !== 'function') + cb = noop; + + if (!this.config.strictVendor + || (this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))) { + openChannel(this, 'direct-streamlocal@openssh.com', { socketPath }, cb); + return this; } - cb(err); + process.nextTick( + cb, + new Error( + 'strictVendor enabled and server is not OpenSSH or compatible version' + ) + ); + + return this; + } + + sftp(cb) { + if (!this._sock || !this._sock.writable) + throw new Error('Not connected'); + + openChannel(this, 'sftp', (err, sftp) => { + if (err) { + cb(err); + return; + } + + reqSubsystem(sftp, 'sftp', (err, sftp_) => { + if (err) { + cb(err); + return; + } + + function removeListeners() { + sftp.removeListener('ready', onReady); + sftp.removeListener('error', onError); + sftp.removeListener('exit', onExit); + sftp.removeListener('close', onExit); + } + + function onReady() { + // TODO: do not remove exit/close in case remote end closes the + // channel abruptly and we need to notify outstanding callbacks + removeListeners(); + cb(undefined, sftp); + } + + function onError(err) { + removeListeners(); + cb(err); + } + + function onExit(code, signal) { + removeListeners(); + let msg; + if (typeof code === 'number') + msg = `Received exit code ${code} while establishing SFTP session`; + else if (signal !== undefined) + msg = `Received signal ${signal} while establishing SFTP session`; + else + msg = 'Received unexpected SFTP session termination'; + const err = new Error(msg); + err.code = code; + err.signal = signal; + cb(err); + } + + sftp.on('ready', onReady) + .on('error', onError) + .on('exit', onExit) + .on('close', onExit); + + sftp._init(); + }); + }); + + return this; } } -function nextChannel(self) { - // get the next available channel number +function openChannel(self, type, opts, cb) { + // Ask the server to open a channel for some purpose + // (e.g. session (sftp, exec, shell), or forwarding a TCP connection + const initWindow = MAX_WINDOW; + const maxPacket = PACKET_SIZE; + + if (typeof opts === 'function') { + cb = opts; + opts = {}; + } + + const wrapper = (err, stream) => { + cb(err, stream); + }; + wrapper.type = type; - // optimized path - if (self._curChan < MAX_CHANNEL) - return ++self._curChan; + const localChan = self._chanMgr.add(wrapper); - // slower lookup path - for (var i = 0, channels = self._channels; i < MAX_CHANNEL; ++i) - if (!channels[i]) - return i; + if (localChan === -1) { + cb(new Error('No free channels available')); + return; + } - return false; + switch (type) { + case 'session': + case 'sftp': + self._protocol.session(localChan, initWindow, maxPacket); + break; + case 'direct-tcpip': + self._protocol.directTcpip(localChan, initWindow, maxPacket, opts); + break; + case 'direct-streamlocal@openssh.com': + self._protocol.openssh_directStreamLocal( + localChan, initWindow, maxPacket, opts + ); + break; + default: + throw new Error(`Unsupported channel type: ${type}`); + } } function reqX11(chan, screen, cb) { - // asks server to start sending us X11 connections - var cfg = { + // Asks server to start sending us X11 connections + const cfg = { single: false, protocol: 'MIT-MAGIC-COOKIE-1', cookie: undefined, @@ -1250,29 +1381,29 @@ function reqX11(chan, screen, cb) { if (typeof screen.cookie === 'string') cfg.cookie = screen.cookie; else if (Buffer.isBuffer(screen.cookie)) - cfg.cookie = screen.cookie.toString('hex'); + cfg.cookie = screen.cookie.hexSlice(0, screen.cookie.length); } if (cfg.cookie === undefined) cfg.cookie = randomCookie(); - var wantReply = (typeof cb === 'function'); + const wantReply = (typeof cb === 'function'); if (chan.outgoing.state !== 'open') { - wantReply && cb(new Error('Channel is not open')); - return true; + if (wantReply) + cb(new Error('Channel is not open')); + return; } if (wantReply) { - chan._callbacks.push(function(had_err) { + chan._callbacks.push((had_err) => { if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to request X11')); + cb(had_err !== true ? had_err : new Error('Unable to request X11')); + return; } chan._hasX11 = true; ++chan._client._acceptX11; - chan.once('close', function() { + chan.once('close', () => { if (chan._client._acceptX11) --chan._client._acceptX11; }); @@ -1281,20 +1412,20 @@ function reqX11(chan, screen, cb) { }); } - return chan._client._sshstream.x11Forward(chan.outgoing.id, cfg, wantReply); + chan._client._protocol.x11Forward(chan.outgoing.id, cfg, wantReply); } function reqPty(chan, opts, cb) { - var rows = 24; - var cols = 80; - var width = 640; - var height = 480; - var term = 'vt100'; - var modes = null; - - if (typeof opts === 'function') + let rows = 24; + let cols = 80; + let width = 640; + let height = 480; + let term = 'vt100'; + let modes = null; + + if (typeof opts === 'function') { cb = opts; - else if (typeof opts === 'object' && opts !== null) { + } else if (typeof opts === 'object' && opts !== null) { if (typeof opts.rows === 'number') rows = opts.rows; if (typeof opts.cols === 'number') @@ -1309,149 +1440,154 @@ function reqPty(chan, opts, cb) { modes = opts.modes; } - var wantReply = (typeof cb === 'function'); + const wantReply = (typeof cb === 'function'); if (chan.outgoing.state !== 'open') { - wantReply && cb(new Error('Channel is not open')); - return true; + if (wantReply) + cb(new Error('Channel is not open')); + return; } if (wantReply) { - chan._callbacks.push(function(had_err) { + chan._callbacks.push((had_err) => { if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to request a pseudo-terminal')); + cb(had_err !== true + ? had_err + : new Error('Unable to request a pseudo-terminal')); + return; } cb(); }); } - return chan._client._sshstream.pty(chan.outgoing.id, - rows, - cols, - height, - width, - term, - modes, - wantReply); + chan._client._protocol.pty(chan.outgoing.id, + rows, + cols, + height, + width, + term, + modes, + wantReply); } function reqAgentFwd(chan, cb) { - var wantReply = (typeof cb === 'function'); + const wantReply = (typeof cb === 'function'); if (chan.outgoing.state !== 'open') { wantReply && cb(new Error('Channel is not open')); - return true; - } else if (chan._client._agentFwdEnabled) { + return; + } + if (chan._client._agentFwdEnabled) { wantReply && cb(false); - return true; + return; } chan._client._agentFwdEnabled = true; - chan._callbacks.push(function(had_err) { + chan._callbacks.push((had_err) => { if (had_err) { chan._client._agentFwdEnabled = false; - wantReply && cb(had_err !== true - ? had_err - : new Error('Unable to request agent forwarding')); + if (wantReply) { + cb(had_err !== true + ? had_err + : new Error('Unable to request agent forwarding')); + } return; } - wantReply && cb(); + if (wantReply) + cb(); }); - return chan._client._sshstream.openssh_agentForward(chan.outgoing.id, true); + chan._client._protocol.openssh_agentForward(chan.outgoing.id, true); } function reqShell(chan, cb) { if (chan.outgoing.state !== 'open') { cb(new Error('Channel is not open')); - return true; + return; } - chan._callbacks.push(function(had_err) { + + chan._callbacks.push((had_err) => { if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to open shell')); + cb(had_err !== true ? had_err : new Error('Unable to open shell')); + return; } chan.subtype = 'shell'; cb(undefined, chan); }); - return chan._client._sshstream.shell(chan.outgoing.id, true); + chan._client._protocol.shell(chan.outgoing.id, true); } function reqExec(chan, cmd, opts, cb) { if (chan.outgoing.state !== 'open') { cb(new Error('Channel is not open')); - return true; + return; } - chan._callbacks.push(function(had_err) { + + chan._callbacks.push((had_err) => { if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to exec')); + cb(had_err !== true ? had_err : new Error('Unable to exec')); + return; } chan.subtype = 'exec'; chan.allowHalfOpen = (opts.allowHalfOpen !== false); cb(undefined, chan); }); - return chan._client._sshstream.exec(chan.outgoing.id, cmd, true); + chan._client._protocol.exec(chan.outgoing.id, cmd, true); } function reqEnv(chan, env) { if (chan.outgoing.state !== 'open') - return true; - var ret = true; - var keys = Object.keys(env || {}); - var key; - var val; - - for (var i = 0, len = keys.length; i < len; ++i) { - key = keys[i]; - val = env[key]; - ret = chan._client._sshstream.env(chan.outgoing.id, key, val, false); - } + return; - return ret; + const keys = Object.keys(env || {}); + + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + const val = env[key]; + chan._client._protocol.env(chan.outgoing.id, key, val, false); + } } function reqSubsystem(chan, name, cb) { if (chan.outgoing.state !== 'open') { cb(new Error('Channel is not open')); - return true; + return; } - chan._callbacks.push(function(had_err) { + + chan._callbacks.push((had_err) => { if (had_err) { - return cb(had_err !== true - ? had_err - : new Error('Unable to start subsystem: ' + name)); + cb(had_err !== true + ? had_err + : new Error(`Unable to start subsystem: ${name}`)); + return; } chan.subtype = 'subsystem'; cb(undefined, chan); }); - return chan._client._sshstream.subsystem(chan.outgoing.id, name, true); + chan._client._protocol.subsystem(chan.outgoing.id, name, true); } +// TODO: inline implementation into single call site function onCHANNEL_OPEN(self, info) { - // the server is trying to open a channel with us, this is usually when + // The server is trying to open a channel with us, this is usually when // we asked the server to forward us connections on some port and now they // are asking us to accept/deny an incoming connection on their side - var localChan = false; - var reason; + let localChan = -1; + let reason; - function accept() { - var chaninfo = { + const accept = () => { + const chanInfo = { type: info.type, incoming: { id: localChan, - window: Channel.MAX_WINDOW, - packetSize: Channel.PACKET_SIZE, + window: MAX_WINDOW, + packetSize: PACKET_SIZE, state: 'open' }, outgoing: { @@ -1461,111 +1597,102 @@ function onCHANNEL_OPEN(self, info) { state: 'open' } }; - var stream = new Channel(chaninfo, self); + const stream = new Channel(self, chanInfo); + self._chanMgr.update(localChan, stream); - self._sshstream.channelOpenConfirm(info.sender, - localChan, - Channel.MAX_WINDOW, - Channel.PACKET_SIZE); + self._protocol.channelOpenConfirm(info.sender, + localChan, + MAX_WINDOW, + PACKET_SIZE); return stream; - } - function reject() { + }; + const reject = () => { if (reason === undefined) { - if (localChan === false) - reason = consts.CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE; + if (localChan === -1) + reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE; else - reason = consts.CHANNEL_OPEN_FAILURE.CONNECT_FAILED; + reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED; } - self._sshstream.channelOpenFail(info.sender, reason, '', ''); - } - - if (info.type === 'forwarded-tcpip' - || info.type === 'x11' - || info.type === 'auth-agent@openssh.com' - || info.type === 'forwarded-streamlocal@openssh.com') { - - // check for conditions for automatic rejection - var rejectConn = ( - (info.type === 'forwarded-tcpip' - && self._forwarding[info.data.destIP - + ':' - + info.data.destPort] === undefined) - || (info.type === 'forwarded-streamlocal@openssh.com' - && self._forwardingUnix[info.data.socketPath] === undefined) - || (info.type === 'x11' && self._acceptX11 === 0) - || (info.type === 'auth-agent@openssh.com' - && !self._agentFwdEnabled) - ); - - if (!rejectConn) { - localChan = nextChannel(self); - - if (localChan === false) { - self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: no channels available'); - rejectConn = true; - } else - self._channels[localChan] = true; - } else { - reason = consts.CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; - self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: unexpected channel open for: ' - + info.type); + self._protocol.channelOpenFail(info.sender, reason, ''); + }; + const reserveChannel = () => { + localChan = self._chanMgr.add(); + + if (localChan === -1) { + reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE; + if (self.config.debug) { + self.config.debug( + 'Client: Automatic rejection of incoming channel open: ' + + 'no channels available' + ); + } } - // TODO: automatic rejection after some timeout? - - if (rejectConn) - reject(); + return (localChan !== -1); + }; - if (localChan !== false) { - if (info.type === 'forwarded-tcpip') { - if (info.data.destPort === 0) { - info.data.destPort = self._forwarding[info.data.destIP - + ':' - + info.data.destPort]; - } - self.emit('tcp connection', info.data, accept, reject); - } else if (info.type === 'x11') { - self.emit('x11', info.data, accept, reject); - } else if (info.type === 'forwarded-streamlocal@openssh.com') { - self.emit('unix connection', info.data, accept, reject); - } else { + const data = info.data; + switch (info.type) { + case 'forwarded-tcpip': { + const val = self._forwarding[`${data.destIP}:${data.destPort}`]; + if (val !== undefined && reserveChannel()) { + if (data.destPort === 0) + data.destPort = val; + self.emit('tcp connection', data, accept, reject); + return; + } + break; + } + case 'forwarded-streamlocal@openssh.com': + if (self._forwardingUnix[data.socketPath] !== undefined + && reserveChannel()) { + self.emit('unix connection', data, accept, reject); + return; + } + break; + case 'auth-agent@openssh.com': + if (self._agentFwdEnabled && reserveChannel()) { agentQuery(self.config.agent, accept, reject); + return; } + break; + case 'x11': + if (self._acceptX11 !== 0 && reserveChannel()) { + self.emit('x11', data, accept, reject); + return; + } + break; + default: + // Automatically reject any unsupported channel open requests + reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE; + if (self.config.debug) { + self.config.debug( + 'Client: Automatic rejection of unsupported incoming channel open ' + + `type: ${info.type}` + ); + } + } + + if (reason === undefined) { + reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; + if (self.config.debug) { + self.config.debug( + 'Client: Automatic rejection of unexpected incoming channel open for: ' + + info.type + ); } - } else { - // automatically reject any unsupported channel open requests - self.config.debug('DEBUG: Client: Automatic rejection of incoming channel open: unsupported type: ' - + info.type); - reason = consts.CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE; - reject(); } + + reject(); } -var randomCookie = (function() { - if (typeof crypto.randomFillSync === 'function') { - var buffer = Buffer.alloc(16); - return function randomCookie() { - crypto.randomFillSync(buffer, 0, 16); - return buffer.toString('hex'); - }; - } else { - return function randomCookie() { - return crypto.randomBytes(16).toString('hex'); - }; - } +const randomCookie = (() => { + const buffer = Buffer.allocUnsafe(16); + return () => { + randomFillSync(buffer, 0, 16); + return buffer.hexSlice(0, 16); + }; })(); -Client.Client = Client; -Client.Server = require('./server'); -// pass some useful utilities on to end user (e.g. parseKey()) -Client.utils = ssh2_streams.utils; -// expose useful SFTPStream constants for sftp server usage -Client.SFTP_STATUS_CODE = SFTPStream.STATUS_CODE; -Client.SFTP_OPEN_MODE = SFTPStream.OPEN_MODE; -// expose http(s).Agent implementations to allow easy tunneling of HTTP(S) -// requests -Client.HTTPAgent = HTTPAgents.SSHTTPAgent; -Client.HTTPSAgent = HTTPAgents.SSHTTPSAgent; - -module.exports = Client; // backwards compatibility +module.exports = Client; diff --git a/lib/http-agents.js b/lib/http-agents.js index b49c108a..e2e676f9 100644 --- a/lib/http-agents.js +++ b/lib/http-agents.js @@ -1,63 +1,66 @@ -var HttpAgent = require('http').Agent; -var HttpsAgent = require('https').Agent; -var inherits = require('util').inherits; +'use strict'; -var Client; +const { Agent: HttpAgent } = require('http'); +const { Agent: HttpsAgent } = require('https'); +const { connect: tlsConnect } = require('tls'); -[HttpAgent, HttpsAgent].forEach((ctor) => { - function SSHAgent(connectCfg, agentOptions) { - if (!(this instanceof SSHAgent)) - return new SSHAgent(connectCfg, agentOptions); +let Client; - ctor.call(this, agentOptions); +for (const ctor of [HttpAgent, HttpsAgent]) { + class SSHAgent extends ctor { + constructor(connectCfg, agentOptions) { + super(agentOptions); - this._connectCfg = connectCfg; - this._defaultSrcIP = (agentOptions && agentOptions.srcIP) || 'localhost'; - } - inherits(SSHAgent, ctor); + this._connectCfg = connectCfg; + this._defaultSrcIP = (agentOptions && agentOptions.srcIP) || 'localhost'; + } + + createConnection(options, cb) { + const srcIP = (options && options.localAddress) || this._defaultSrcIP; + const srcPort = (options && options.localPort) || 0; + const dstIP = options.host; + const dstPort = options.port; + + if (Client === undefined) + ({ Client } = require('./client.js')); - SSHAgent.prototype.createConnection = createConnection; + const client = new Client(); + let triedForward = false; + client.on('ready', () => { + client.forwardOut(srcIP, srcPort, dstIP, dstPort, (err, stream) => { + triedForward = true; + if (err) { + client.end(); + return cb(err); + } + stream.once('close', () => client.end()); + cb(null, decorateStream(stream, ctor, options)); + }); + }).on('error', cb).on('close', () => { + if (!triedForward) + cb(new Error('Unexpected connection close')); + }).connect(this._connectCfg); + } + } exports[ctor === HttpAgent ? 'SSHTTPAgent' : 'SSHTTPSAgent'] = SSHAgent; -}); - -function createConnection(options, cb) { - var srcIP = (options && options.localAddress) || this._defaultSrcIP; - var srcPort = (options && options.localPort) || 0; - var dstIP = options.host; - var dstPort = options.port; - - if (Client === undefined) - Client = require('./client').Client; - - var client = new Client(); - var triedForward = false; - client.on('ready', () => { - client.forwardOut(srcIP, srcPort, dstIP, dstPort, (err, stream) => { - triedForward = true; - if (err) { - client.end(); - return cb(err); - } - stream.once('close', () => { - client.end(); - }); - cb(null, decorateStream(stream)); - }); - }).on('error', cb).on('close', () => { - if (!triedForward) - cb(new Error('Unexpected connection loss')); - }).connect(this._connectCfg); } function noop() {} -function decorateStream(stream) { - stream.setKeepAlive = noop; - stream.setNoDelay = noop; - stream.setTimeout = noop; - stream.ref = noop; - stream.unref = noop; - stream.destroySoon = stream.destroy; - return stream; +function decorateStream(stream, ctor, options) { + if (ctor === HttpAgent) { + // HTTP + stream.setKeepAlive = noop; + stream.setNoDelay = noop; + stream.setTimeout = noop; + stream.ref = noop; + stream.unref = noop; + stream.destroySoon = stream.destroy; + return stream; + } + + // HTTPS + options.socket = stream; + return tlsConnect(options); } diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..2c5bdcae --- /dev/null +++ b/lib/index.js @@ -0,0 +1,26 @@ +'use strict'; + +const HTTPAgents = require('./http-agents.js'); +const { parseKey } = require('./protocol/keyParser.js'); +const { + flagsToString, + OPEN_MODE, + STATUS_CODE, + stringToFlags, +} = require('./protocol/SFTP.js'); + +module.exports = { + Client: require('./client.js'), + HTTPAgent: HTTPAgents.SSHTTPAgent, + HTTPSAgent: HTTPAgents.SSHTTPSAgent, + Server: require('./server.js'), + utils: { + parseKey, + sftp: { + flagsToString, + OPEN_MODE, + STATUS_CODE, + stringToFlags, + }, + }, +}; diff --git a/lib/keepalivemgr.js b/lib/keepalivemgr.js deleted file mode 100644 index 35c3d012..00000000 --- a/lib/keepalivemgr.js +++ /dev/null @@ -1,80 +0,0 @@ -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) - list[i] = list[k]; - list.pop(); -} - -function Manager(interval, streamInterval, kaCountMax) { - var streams = this._streams = []; - this._timer = undefined; - this._timerInterval = interval; - this._timerfn = function() { - var now = Date.now(); - for (var i = 0, len = streams.length, s, last; i < len; ++i) { - s = streams[i]; - last = s._kalast; - if (last && (now - last) >= streamInterval) { - if (++s._kacnt > kaCountMax) { - var err = new Error('Keepalive timeout'); - err.level = 'client-timeout'; - s.emit('error', err); - s.disconnect(); - spliceOne(streams, i); - --i; - len = streams.length; - } else { - s._kalast = now; - // XXX: if the server ever starts sending real global requests to the - // client, we will need to add a dummy callback here to keep the - // correct reply order - s.ping(); - } - } - } - }; -} - -Manager.prototype.start = function() { - if (this._timer) - this.stop(); - this._timer = setInterval(this._timerfn, this._timerInterval); -}; - -Manager.prototype.stop = function() { - if (this._timer) { - clearInterval(this._timer); - this._timer = undefined; - } -}; - -Manager.prototype.add = function(stream) { - var streams = this._streams, - self = this; - - stream.once('end', function() { - self.remove(stream); - }).on('packet', resetKA); - - streams[streams.length] = stream; - - resetKA(); - - if (!this._timer) - this.start(); - - function resetKA() { - stream._kalast = Date.now(); - stream._kacnt = 0; - } -}; - -Manager.prototype.remove = function(stream) { - var streams = this._streams, - index = streams.indexOf(stream); - if (index > -1) - spliceOne(streams, index); - if (!streams.length) - this.stop(); -}; - -module.exports = Manager; diff --git a/lib/protocol/Protocol.js b/lib/protocol/Protocol.js new file mode 100644 index 00000000..5580250d --- /dev/null +++ b/lib/protocol/Protocol.js @@ -0,0 +1,1997 @@ +/* + TODO: + * Replace `throw` statements in crypto.js + * Replace `buffer._pos` usage in keyParser.js and elsewhere + * Utilize optional "writev" support when writing packets from + cipher.encrypt() + * Fix no error emitted when remote closes connection before handshake + * How to handle failure to load optional binding when it exists on disk? + * Do nothing special (currently) + * Use node's built-in "warning" mechanism when the binding failed to load + (e.g. wrong ABI version or wrong arch) -- this might(?) be helpful for + end users when they copy modules around. We would still fallback like + normal, so there would be no crashing but it might be nice to let the + end user know they are using a less optimal setup? + * Built-in support for automatic re-keying, on by default + * Revisit receiving unexpected/unknown packets + * Error (fatal or otherwise) or ignore or pass on to user (in some or all + cases)? + * Including server/client check for single directional packet types? + * Check packets for validity or bail as early as possible? + * Automatic re-key every 2**31 packets after the last key exchange (sent or + received), as suggested by RFC4344. OpenSSH currently does this. + * Automatic re-key every so many blocks depending on cipher. RFC4344: + Because of a birthday property of block ciphers and some modes of + operation, implementations must be careful not to encrypt too many + blocks with the same encryption key. + + Let L be the block length (in bits) of an SSH encryption method's + block cipher (e.g., 128 for AES). If L is at least 128, then, after + rekeying, an SSH implementation SHOULD NOT encrypt more than 2**(L/4) + blocks before rekeying again. If L is at least 128, then SSH + implementations should also attempt to force a rekey before receiving + more than 2**(L/4) blocks. If L is less than 128 (which is the case + for older ciphers such as 3DES, Blowfish, CAST-128, and IDEA), then, + although it may be too expensive to rekey every 2**(L/4) blocks, it + is still advisable for SSH implementations to follow the original + recommendation in [RFC4253]: rekey at least once for every gigabyte + of transmitted data. + + Note that if L is less than or equal to 128, then the recommendation + in this subsection supersedes the recommendation in Section 3.1. If + an SSH implementation uses a block cipher with a larger block size + (e.g., Rijndael with 256-bit blocks), then the recommendations in + Section 3.1 may supersede the recommendations in this subsection + (depending on the lengths of the packets). +*/ + +'use strict'; + +const { inspect } = require('util'); + +const { bindingAvailable, NullCipher, NullDecipher } = require('./crypto.js'); +const { + COMPAT_CHECKS, + DISCONNECT_REASON, + MESSAGE, + SIGNALS, + TERMINAL_MODE, +} = require('./constants.js'); +const { + DEFAULT_KEXINIT, + KexInit, + kexinit, + onKEXPayload, +} = require('./kex.js'); +const MESSAGE_HANDLERS = require('./handlers.js'); +const { + bufferCopy, + bufferFill, + bufferSlice, + convertSignature, + readUInt32BE, + sendPacket, + writeUInt32BE, +} = require('./utils.js'); +const { + PacketReader, + PacketWriter, + ZlibPacketReader, + ZlibPacketWriter, +} = require('./zlib.js'); + +const MODULE_VER = require('../../package.json').version; + +const VALID_DISCONNECT_REASONS = new Map( + Object.values(DISCONNECT_REASON).map((n) => [n, 1]) +); +const IDENT = Buffer.from(`SSH-2.0-ssh2js${MODULE_VER}`); +const CRLF = Buffer.from('\r\n'); +const MAX_HEADER_LEN = 8192; +const PING_PAYLOAD = Buffer.from([ + MESSAGE.GLOBAL_REQUEST, + // "keepalive@openssh.com" + 0, 0, 0, 21, + 107, 101, 101, 112, 97, 108, 105, 118, 101, 64, 111, 112, 101, 110, 115, + 115, 104, 46, 99, 111, 109, + // Request a reply + 1, +]); +const NO_TERMINAL_MODES_BUFFER = Buffer.from([ TERMINAL_MODE.TTY_OP_END ]); + +function noop() {} + +/* + Inbound: + * kexinit payload (needed only until exchange hash is generated) + * raw ident + * rekey packet queue + * expected packet (implemented as separate _parse() function?) + Outbound: + * kexinit payload (needed only until exchange hash is generated) + * rekey packet queue + * kex secret (needed only until NEWKEYS) + * exchange hash (needed only until NEWKEYS) + * session ID (set to exchange hash from initial handshake) +*/ +class Protocol { + constructor(config) { + const onWrite = config.onWrite; + if (typeof onWrite !== 'function') + throw new Error('Missing onWrite function'); + this._onWrite = (data) => { onWrite(data); }; + + const onError = config.onError; + if (typeof onError !== 'function') + throw new Error('Missing onError function'); + this._onError = (err) => { onError(err); }; + + const debug = config.debug; + this._debug = (typeof debug === 'function' + ? (msg) => { debug(msg); } + : undefined); + + const onHeader = config.onHeader; + this._onHeader = (typeof onHeader === 'function' + ? (...args) => { onHeader(...args); } + : noop); + + const onPacket = config.onPacket; + this._onPacket = (typeof onPacket === 'function' + ? () => { onPacket(); } + : noop); + + let onHandshakeComplete = config.onHandshakeComplete; + if (typeof onHandshakeComplete !== 'function') + onHandshakeComplete = noop; + this._onHandshakeComplete = (...args) => { + this._debug && this._debug('Handshake completed'); + + // Process packets queued during a rekey where necessary + const oldQueue = this._queue; + if (oldQueue) { + this._queue = undefined; + this._debug && this._debug( + `Draining outbound queue (${oldQueue.length}) ...` + ); + for (let i = 0; i < oldQueue.length; ++i) { + const data = oldQueue[i]; + // data === payload only + + // XXX: hacky + let finalized = this._packetRW.write.finalize(data); + if (finalized === data) { + const packet = this._cipher.allocPacket(data.length); + packet.set(data, 5); + finalized = packet; + } + + sendPacket(this, finalized); + } + this._debug && this._debug('... finished draining outbound queue'); + } + + onHandshakeComplete(...args); + }; + this._queue = undefined; + + const messageHandlers = config.messageHandlers; + if (typeof messageHandlers === 'object' && messageHandlers !== null) + this._handlers = messageHandlers; + else + this._handlers = {}; + + this._onPayload = onPayload.bind(this); + + this._server = !!config.server; + this._banner = undefined; + let greeting; + if (this._server) { + if (typeof config.hostKeys !== 'object' || config.hostKeys === null) + throw new Error('Missing server host key(s)'); + this._hostKeys = config.hostKeys; + + // Greeting displayed before the ssh identification string is sent, this + // is usually ignored by most clients + if (typeof config.greeting === 'string' && config.greeting.length) { + greeting = (config.greeting.slice(-2) === '\r\n' + ? config.greeting + : `${config.greeting}\r\n`); + } + + // Banner shown after the handshake completes, but before user + // authentication begins + if (typeof config.banner === 'string' && config.banner.length) { + this._banner = (config.banner.slice(-2) === '\r\n' + ? config.banner + : `${config.banner}\r\n`); + } + } else { + this._hostKeys = undefined; + } + + let offer = config.offer; + if (typeof offer !== 'object' || offer === null) + offer = DEFAULT_KEXINIT; + else if (offer.constructor !== KexInit) + offer = new KexInit(offer); + this._kex = undefined; + this._kexinit = undefined; + this._offer = offer; + this._cipher = undefined; + this._decipher = undefined; + this._skipNextInboundPacket = false; + this._packetRW = { + read: new PacketReader(), + write: new PacketWriter(this), + }; + this._hostVerifier = (!this._server + && typeof config.hostVerifier === 'function' + ? config.hostVerifier + : undefined); + + this._parse = parseHeader; + this._buffer = undefined; + this._authsQueue = []; + this._authenticated = false; + this._remoteIdentRaw = undefined; + if (typeof config.ident === 'string') { + this._identRaw = Buffer.from(`SSH-2.0-${config.ident}`); + } else if (Buffer.isBuffer(config.ident)) { + const fullIdent = Buffer.allocUnsafe(8 + config.ident.length); + fullIdent.latin1Write('SSH-2.0-', 0, 8); + fullIdent.set(config.ident, 8); + this._identRaw = fullIdent; + } else { + this._identRaw = IDENT; + } + this._compatFlags = 0; + + if (this._debug) { + if (bindingAvailable) + this._debug('Custom crypto binding available'); + else + this._debug('Custom crypto binding not available'); + } + + process.nextTick(() => { + this._debug && this._debug( + `Local ident: ${inspect(this._identRaw.toString())}` + ); + if (greeting) + this._onWrite(greeting); + this._onWrite(this._identRaw); + this._onWrite(CRLF); + }); + } + _destruct(reason) { + this._packetRW.read.cleanup(); + this._packetRW.write.cleanup(); + this._cipher && this._cipher.free(); + this._decipher && this._decipher.free(); + if (typeof reason !== 'string' || reason.length === 0) + reason = 'fatal error'; + this.parse = () => { + throw new Error(`Instance unusable after ${reason}`); + }; + this._onWrite = () => { + throw new Error(`Instance unusable after ${reason}`); + }; + this._destruct = undefined; + } + cleanup() { + this._destruct && this._destruct(); + } + parse(chunk, i, len) { + while (i < len) + i = this._parse(chunk, i, len); + } + + // Protocol message API + + // =========================================================================== + // Common/Shared ============================================================= + // =========================================================================== + + // Global + // ------ + disconnect(reason) { + const pktLen = 1 + 4 + 4 + 4; + // We don't use _packetRW.write.* here because we need to make sure that + // we always get a full packet allocated because this message can be sent + // at any time -- even during a key exchange + let p = this._packetRW.write.allocStartKEX; + const packet = this._packetRW.write.alloc(pktLen, true); + const end = p + pktLen; + + if (!VALID_DISCONNECT_REASONS.has(reason)) + reason = DISCONNECT_REASON.PROTOCOL_ERROR; + + packet[p] = MESSAGE.DISCONNECT; + writeUInt32BE(packet, reason, ++p); + packet.fill(0, p += 4, end); + + this._debug && this._debug(`Outbound: Sending DISCONNECT (${reason})`); + sendPacket(this, this._packetRW.write.finalize(packet, true), true); + } + ping() { + const p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(PING_PAYLOAD.length); + + packet.set(PING_PAYLOAD, p); + + this._debug && this._debug( + 'Outbound: Sending ping (GLOBAL_REQUEST: keepalive@openssh.com)' + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + rekey() { + if (this._kexinit === undefined) { + this._debug && this._debug('Outbound: Initiated explicit rekey'); + this._queue = []; + kexinit(this); + } else { + this._debug && this._debug('Outbound: Ignoring rekey during handshake'); + } + } + + // 'ssh-connection' service-specific + // --------------------------------- + requestSuccess(data) { + let p = this._packetRW.write.allocStart; + let packet; + if (Buffer.isBuffer(data)) { + packet = this._packetRW.write.alloc(1 + data.length); + + packet[p] = MESSAGE.REQUEST_SUCCESS; + + packet.set(data, ++p); + } else { + packet = this._packetRW.write.alloc(1); + + packet[p] = MESSAGE.REQUEST_SUCCESS; + } + + this._debug && this._debug('Outbound: Sending REQUEST_SUCCESS'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + requestFailure() { + const p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1); + + packet[p] = MESSAGE.REQUEST_FAILURE; + + this._debug && this._debug('Outbound: Sending REQUEST_FAILURE'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelSuccess(chan) { + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4); + + packet[p] = MESSAGE.CHANNEL_SUCCESS; + + writeUInt32BE(packet, chan, ++p); + + this._debug && this._debug(`Outbound: Sending CHANNEL_SUCCESS (r:${chan})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelFailure(chan) { + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4); + + packet[p] = MESSAGE.CHANNEL_FAILURE; + + writeUInt32BE(packet, chan, ++p); + + this._debug && this._debug(`Outbound: Sending CHANNEL_FAILURE (r:${chan})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelEOF(chan) { + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4); + + packet[p] = MESSAGE.CHANNEL_EOF; + + writeUInt32BE(packet, chan, ++p); + + this._debug && this._debug(`Outbound: Sending CHANNEL_EOF (r:${chan})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelClose(chan) { + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4); + + packet[p] = MESSAGE.CHANNEL_CLOSE; + + writeUInt32BE(packet, chan, ++p); + + this._debug && this._debug(`Outbound: Sending CHANNEL_CLOSE (r:${chan})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelWindowAdjust(chan, amount) { + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4); + + packet[p] = MESSAGE.CHANNEL_WINDOW_ADJUST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, amount, p += 4); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_WINDOW_ADJUST (r:${chan}, ${amount})` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelData(chan, data) { + const isBuffer = Buffer.isBuffer(data); + const dataLen = (isBuffer ? data.length : Buffer.byteLength(data)); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + dataLen); + + packet[p] = MESSAGE.CHANNEL_DATA; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, dataLen, p += 4); + + if (isBuffer) + packet.set(data, p += 4); + else + packet.utf8Write(data, p += 4, dataLen); + + this._debug && this._debug(`Outbound: Sending CHANNEL_DATA (r:${chan})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelExtData(chan, data, type) { + const isBuffer = Buffer.isBuffer(data); + const dataLen = (isBuffer ? data.length : Buffer.byteLength(data)); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + 4 + dataLen); + + packet[p] = MESSAGE.CHANNEL_EXTENDED_DATA; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, type, p += 4); + + writeUInt32BE(packet, dataLen, p += 4); + + if (isBuffer) + packet.set(data, p += 4); + else + packet.utf8Write(data, p += 4, dataLen); + + this._debug + && this._debug(`Outbound: Sending CHANNEL_EXTENDED_DATA (r:${chan})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelOpenConfirm(remote, local, initWindow, maxPacket) { + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + 4 + 4); + + packet[p] = MESSAGE.CHANNEL_OPEN_CONFIRMATION; + + writeUInt32BE(packet, remote, ++p); + + writeUInt32BE(packet, local, p += 4); + + writeUInt32BE(packet, initWindow, p += 4); + + writeUInt32BE(packet, maxPacket, p += 4); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_OPEN_CONFIRMATION (r:${remote}, l:${local})` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + channelOpenFail(remote, reason, desc) { + if (typeof desc !== 'string') + desc = ''; + + const descLen = Buffer.byteLength(desc); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + 4 + descLen + 4); + + packet[p] = MESSAGE.CHANNEL_OPEN_FAILURE; + + writeUInt32BE(packet, remote, ++p); + + writeUInt32BE(packet, reason, p += 4); + + writeUInt32BE(packet, descLen, p += 4); + + p += 4; + if (descLen) { + packet.utf8Write(desc, p, descLen); + p += descLen; + } + + writeUInt32BE(packet, 0, p); // Empty language tag + + this._debug + && this._debug(`Outbound: Sending CHANNEL_OPEN_FAILURE (r:${remote})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + + // =========================================================================== + // Client-specific =========================================================== + // =========================================================================== + + // Global + // ------ + service(name) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const nameLen = Buffer.byteLength(name); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + nameLen); + + packet[p] = MESSAGE.SERVICE_REQUEST; + + writeUInt32BE(packet, nameLen, ++p); + packet.utf8Write(name, p += 4, nameLen); + + this._debug && this._debug(`Outbound: Sending SERVICE_REQUEST (${name})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + + // 'ssh-userauth' service-specific + // ------------------------------- + authPassword(username, password) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const userLen = Buffer.byteLength(username); + const passLen = Buffer.byteLength(password); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + userLen + 4 + 14 + 4 + 8 + 1 + 4 + passLen + ); + + packet[p] = MESSAGE.USERAUTH_REQUEST; + + writeUInt32BE(packet, userLen, ++p); + packet.utf8Write(username, p += 4, userLen); + + writeUInt32BE(packet, 14, p += userLen); + packet.utf8Write('ssh-connection', p += 4, 14); + + writeUInt32BE(packet, 8, p += 14); + packet.utf8Write('password', p += 4, 8); + + packet[p += 8] = 0; + + writeUInt32BE(packet, passLen, ++p); + packet.utf8Write(password, p += 4, passLen); + + this._authsQueue.push('password'); + + this._debug && this._debug('Outbound: Sending USERAUTH_REQUEST (password)'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + authPK(username, pubKey, cbSign) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + let keyType; + + if (typeof pubKey.getPublicSSH === 'function') { + keyType = pubKey.type; + pubKey = pubKey.getPublicSSH(); + } else { + const len = readUInt32BE(pubKey, 0); + keyType = pubKey.utf8Slice(4, 4 + len); + } + + const userLen = Buffer.byteLength(username); + const algoLen = Buffer.byteLength(keyType); + const pubKeyLen = pubKey.length; + const sessionID = this._kex.sessionID; + const sesLen = sessionID.length; + const payloadLen = + (cbSign ? 4 + sesLen : 0) + + 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen; + let packet; + let p; + if (cbSign) { + packet = Buffer.allocUnsafe(payloadLen); + p = 0; + writeUInt32BE(packet, sesLen, p); + packet.set(sessionID, p += 4); + p += sesLen; + } else { + packet = this._packetRW.write.alloc(payloadLen); + p = this._packetRW.write.allocStart; + } + + packet[p] = MESSAGE.USERAUTH_REQUEST; + + writeUInt32BE(packet, userLen, ++p); + packet.utf8Write(username, p += 4, userLen); + + writeUInt32BE(packet, 14, p += userLen); + packet.utf8Write('ssh-connection', p += 4, 14); + + writeUInt32BE(packet, 9, p += 14); + packet.utf8Write('publickey', p += 4, 9); + + packet[p += 9] = (cbSign ? 1 : 0); + + writeUInt32BE(packet, algoLen, ++p); + packet.utf8Write(keyType, p += 4, algoLen); + + writeUInt32BE(packet, pubKeyLen, p += algoLen); + packet.set(pubKey, p += 4); + + if (!cbSign) { + this._authsQueue.push('publickey'); + + this._debug && this._debug( + 'Outbound: Sending USERAUTH_REQUEST (publickey -- check)' + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + return; + } + + cbSign(packet, (signature) => { + signature = convertSignature(signature, keyType); + if (signature === false) + throw new Error('Error while converting handshake signature'); + + const sigLen = signature.length; + p = this._packetRW.write.allocStart; + packet = this._packetRW.write.alloc( + 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen + 4 + + 4 + algoLen + 4 + sigLen + ); + + // TODO: simply copy from original "packet" to new `packet` to avoid + // having to write each individual field a second time? + packet[p] = MESSAGE.USERAUTH_REQUEST; + + writeUInt32BE(packet, userLen, ++p); + packet.utf8Write(username, p += 4, userLen); + + writeUInt32BE(packet, 14, p += userLen); + packet.utf8Write('ssh-connection', p += 4, 14); + + writeUInt32BE(packet, 9, p += 14); + packet.utf8Write('publickey', p += 4, 9); + + packet[p += 9] = 1; + + writeUInt32BE(packet, algoLen, ++p); + packet.utf8Write(keyType, p += 4, algoLen); + + writeUInt32BE(packet, pubKeyLen, p += algoLen); + packet.set(pubKey, p += 4); + + writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen); + + writeUInt32BE(packet, algoLen, p += 4); + packet.utf8Write(keyType, p += 4, algoLen); + + writeUInt32BE(packet, sigLen, p += algoLen); + packet.set(signature, p += 4); + + // Servers shouldn't send packet type 60 in response to signed publickey + // attempts, but if they do, interpret as type 60. + this._authsQueue.push('publickey'); + + this._debug && this._debug( + 'Outbound: Sending USERAUTH_REQUEST (publickey)' + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + }); + } + authHostbased(username, pubKey, hostname, userlocal, cbSign) { + // TODO: Make DRY by sharing similar code with authPK() + if (this._server) + throw new Error('Client-only method called in server mode'); + + let keyType; + + if (typeof pubKey.getPublicSSH === 'function') { + keyType = pubKey.type; + pubKey = pubKey.getPublicSSH(); + } else { + const len = readUInt32BE(pubKey, 0); + keyType = pubKey.utf8Slice(4, 4 + len); + } + + const userLen = Buffer.byteLength(username); + const algoLen = Buffer.byteLength(keyType); + const pubKeyLen = pubKey.length; + const sessionID = this._kex.sessionID; + const sesLen = sessionID.length; + const hostnameLen = Buffer.byteLength(hostname); + const userlocalLen = Buffer.byteLength(userlocal); + const data = Buffer.allocUnsafe( + 4 + sesLen + 1 + 4 + userLen + 4 + 14 + 4 + 9 + 4 + algoLen + + 4 + pubKeyLen + 4 + hostnameLen + 4 + userlocalLen + ); + let p = 0; + + writeUInt32BE(data, sesLen, p); + data.set(sessionID, p += 4); + + data[p += sesLen] = MESSAGE.USERAUTH_REQUEST; + + writeUInt32BE(data, userLen, ++p); + data.utf8Write(username, p += 4, userLen); + + writeUInt32BE(data, 14, p += userLen); + data.utf8Write('ssh-connection', p += 4, 14); + + writeUInt32BE(data, 9, p += 14); + data.utf8Write('hostbased', p += 4, 9); + + writeUInt32BE(data, algoLen, p += 9); + data.utf8Write(keyType, p += 4, algoLen); + + writeUInt32BE(data, pubKeyLen, p += algoLen); + data.set(pubKey, p += 4); + + writeUInt32BE(data, hostnameLen, p += pubKeyLen); + data.utf8Write(hostname, p += 4, hostnameLen); + + writeUInt32BE(data, userlocalLen, p += hostnameLen); + data.utf8Write(userlocal, p += 4, userlocalLen); + + cbSign(data, (signature) => { + signature = convertSignature(signature, keyType); + if (!signature) + throw new Error('Error while converting handshake signature'); + + const sigLen = signature.length; + const reqDataLen = (data.length - sesLen - 4); + p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + reqDataLen + 4 + 4 + algoLen + 4 + sigLen + ); + + bufferCopy(data, packet, 4 + sesLen, data.length, p); + + writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += reqDataLen); + writeUInt32BE(packet, algoLen, p += 4); + packet.utf8Write(keyType, p += 4, algoLen); + writeUInt32BE(packet, sigLen, p += algoLen); + packet.set(signature, p += 4); + + this._authsQueue.push('hostbased'); + + this._debug && this._debug( + 'Outbound: Sending USERAUTH_REQUEST (hostbased)' + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + }); + } + authKeyboard(username) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const userLen = Buffer.byteLength(username); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + userLen + 4 + 14 + 4 + 20 + 4 + 4 + ); + + packet[p] = MESSAGE.USERAUTH_REQUEST; + + writeUInt32BE(packet, userLen, ++p); + packet.utf8Write(username, p += 4, userLen); + + writeUInt32BE(packet, 14, p += userLen); + packet.utf8Write('ssh-connection', p += 4, 14); + + writeUInt32BE(packet, 20, p += 14); + packet.utf8Write('keyboard-interactive', p += 4, 20); + + writeUInt32BE(packet, 0, p += 20); + + writeUInt32BE(packet, 0, p += 4); + + this._authsQueue.push('keyboard-interactive'); + + this._debug && this._debug( + 'Outbound: Sending USERAUTH_REQUEST (keyboard-interactive)' + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + authNone(username) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const userLen = Buffer.byteLength(username); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + userLen + 4 + 14 + 4 + 4); + + packet[p] = MESSAGE.USERAUTH_REQUEST; + + writeUInt32BE(packet, userLen, ++p); + packet.utf8Write(username, p += 4, userLen); + + writeUInt32BE(packet, 14, p += userLen); + packet.utf8Write('ssh-connection', p += 4, 14); + + writeUInt32BE(packet, 4, p += 14); + packet.utf8Write('none', p += 4, 4); + + this._authsQueue.push('none'); + + this._debug && this._debug('Outbound: Sending USERAUTH_REQUEST (none)'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + authInfoRes(responses) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + let responsesTotalLen = 0; + let responseLens; + + if (responses) { + responseLens = new Array(responses.length); + for (let i = 0; i < responses.length; ++i) { + const len = Buffer.byteLength(responses[i]); + responseLens[i] = len; + responsesTotalLen += 4 + len; + } + } + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + responsesTotalLen); + + packet[p] = MESSAGE.USERAUTH_INFO_RESPONSE; + + if (responses) { + writeUInt32BE(packet, responses.length, ++p); + p += 4; + for (let i = 0; i < responses.length; ++i) { + const len = responseLens[i]; + writeUInt32BE(packet, len, p); + p += 4; + if (len) { + packet.utf8Write(responses[i], p, len); + p += len; + } + } + } else { + writeUInt32BE(packet, 0, ++p); + } + + this._debug && this._debug('Outbound: Sending USERAUTH_INFO_RESPONSE'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + + // 'ssh-connection' service-specific + // --------------------------------- + tcpipForward(bindAddr, bindPort, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const addrLen = Buffer.byteLength(bindAddr); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 13 + 1 + 4 + addrLen + 4); + + packet[p] = MESSAGE.GLOBAL_REQUEST; + + writeUInt32BE(packet, 13, ++p); + packet.utf8Write('tcpip-forward', p += 4, 13); + + packet[p += 13] = (wantReply === undefined || wantReply === true ? 1 : 0); + + writeUInt32BE(packet, addrLen, ++p); + packet.utf8Write(bindAddr, p += 4, addrLen); + + writeUInt32BE(packet, bindPort, p += addrLen); + + this._debug + && this._debug('Outbound: Sending GLOBAL_REQUEST (tcpip-forward)'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + cancelTcpipForward(bindAddr, bindPort, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const addrLen = Buffer.byteLength(bindAddr); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 20 + 1 + 4 + addrLen + 4); + + packet[p] = MESSAGE.GLOBAL_REQUEST; + + writeUInt32BE(packet, 20, ++p); + packet.utf8Write('cancel-tcpip-forward', p += 4, 20); + + packet[p += 20] = (wantReply === undefined || wantReply === true ? 1 : 0); + + writeUInt32BE(packet, addrLen, ++p); + packet.utf8Write(bindAddr, p += 4, addrLen); + + writeUInt32BE(packet, bindPort, p += addrLen); + + this._debug + && this._debug('Outbound: Sending GLOBAL_REQUEST (cancel-tcpip-forward)'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + openssh_streamLocalForward(socketPath, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const socketPathLen = Buffer.byteLength(socketPath); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 31 + 1 + 4 + socketPathLen + ); + + packet[p] = MESSAGE.GLOBAL_REQUEST; + + writeUInt32BE(packet, 31, ++p); + packet.utf8Write('streamlocal-forward@openssh.com', p += 4, 31); + + packet[p += 31] = (wantReply === undefined || wantReply === true ? 1 : 0); + + writeUInt32BE(packet, socketPathLen, ++p); + packet.utf8Write(socketPath, p += 4, socketPathLen); + + this._debug && this._debug( + 'Outbound: Sending GLOBAL_REQUEST (streamlocal-forward@openssh.com)' + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + openssh_cancelStreamLocalForward(socketPath, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const socketPathLen = Buffer.byteLength(socketPath); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 38 + 1 + 4 + socketPathLen + ); + + packet[p] = MESSAGE.GLOBAL_REQUEST; + + writeUInt32BE(packet, 38, ++p); + packet.utf8Write('cancel-streamlocal-forward@openssh.com', p += 4, 38); + + packet[p += 38] = (wantReply === undefined || wantReply === true ? 1 : 0); + + writeUInt32BE(packet, socketPathLen, ++p); + packet.utf8Write(socketPath, p += 4, socketPathLen); + + if (this._debug) { + this._debug( + 'Outbound: Sending GLOBAL_REQUEST ' + + '(cancel-streamlocal-forward@openssh.com)' + ); + } + sendPacket(this, this._packetRW.write.finalize(packet)); + } + directTcpip(chan, initWindow, maxPacket, cfg) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const srcLen = Buffer.byteLength(cfg.srcIP); + const dstLen = Buffer.byteLength(cfg.dstIP); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 12 + 4 + 4 + 4 + 4 + srcLen + 4 + 4 + dstLen + 4 + ); + + packet[p] = MESSAGE.CHANNEL_OPEN; + + writeUInt32BE(packet, 12, ++p); + packet.utf8Write('direct-tcpip', p += 4, 12); + + writeUInt32BE(packet, chan, p += 12); + + writeUInt32BE(packet, initWindow, p += 4); + + writeUInt32BE(packet, maxPacket, p += 4); + + writeUInt32BE(packet, dstLen, p += 4); + packet.utf8Write(cfg.dstIP, p += 4, dstLen); + + writeUInt32BE(packet, cfg.dstPort, p += dstLen); + + writeUInt32BE(packet, srcLen, p += 4); + packet.utf8Write(cfg.srcIP, p += 4, srcLen); + + writeUInt32BE(packet, cfg.srcPort, p += srcLen); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_OPEN (r:${chan}, direct-tcpip)` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + openssh_directStreamLocal(chan, initWindow, maxPacket, cfg) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + const pathLen = Buffer.byteLength(cfg.socketPath); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 30 + 4 + 4 + 4 + 4 + pathLen + 4 + 4 + ); + + packet[p] = MESSAGE.CHANNEL_OPEN; + + writeUInt32BE(packet, 30, ++p); + packet.utf8Write('direct-streamlocal@openssh.com', p += 4, 30); + + writeUInt32BE(packet, chan, p += 30); + + writeUInt32BE(packet, initWindow, p += 4); + + writeUInt32BE(packet, maxPacket, p += 4); + + writeUInt32BE(packet, pathLen, p += 4); + packet.utf8Write(cfg.socketPath, p += 4, pathLen); + + // zero-fill reserved fields (string and uint32) + bufferFill(packet, 0, p += pathLen, p + 8); + + if (this._debug) { + this._debug( + 'Outbound: Sending CHANNEL_OPEN ' + + `(r:${chan}, direct-streamlocal@openssh.com)` + ); + } + sendPacket(this, this._packetRW.write.finalize(packet)); + } + openssh_noMoreSessions(wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 28 + 1); + + packet[p] = MESSAGE.GLOBAL_REQUEST; + + writeUInt32BE(packet, 28, ++p); + packet.utf8Write('no-more-sessions@openssh.com', p += 4, 28); + + packet[p += 28] = (wantReply === undefined || wantReply === true ? 1 : 0); + + this._debug && this._debug( + 'Outbound: Sending GLOBAL_REQUEST (no-more-sessions@openssh.com)' + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + session(chan, initWindow, maxPacket) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 7 + 4 + 4 + 4); + + packet[p] = MESSAGE.CHANNEL_OPEN; + + writeUInt32BE(packet, 7, ++p); + packet.utf8Write('session', p += 4, 7); + + writeUInt32BE(packet, chan, p += 7); + + writeUInt32BE(packet, initWindow, p += 4); + + writeUInt32BE(packet, maxPacket, p += 4); + + this._debug + && this._debug(`Outbound: Sending CHANNEL_OPEN (r:${chan}, session)`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + windowChange(chan, rows, cols, height, width) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 4 + 13 + 1 + 4 + 4 + 4 + 4 + ); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 13, p += 4); + packet.utf8Write('window-change', p += 4, 13); + + packet[p += 13] = 0; + + writeUInt32BE(packet, cols, ++p); + + writeUInt32BE(packet, rows, p += 4); + + writeUInt32BE(packet, width, p += 4); + + writeUInt32BE(packet, height, p += 4); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_REQUEST (r:${chan}, window-change)` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + pty(chan, rows, cols, height, width, term, modes, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + if (!term || !term.length) + term = 'vt100'; + if (modes + && !Buffer.isBuffer(modes) + && !Array.isArray(modes) + && typeof modes === 'object' + && modes !== null) { + modes = modesToBytes(modes); + } + if (!modes || !modes.length) + modes = NO_TERMINAL_MODES_BUFFER; + + const termLen = term.length; + const modesLen = modes.length; + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 4 + 7 + 1 + 4 + termLen + 4 + 4 + 4 + 4 + 4 + modesLen + ); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 7, p += 4); + packet.utf8Write('pty-req', p += 4, 7); + + packet[p += 7] = (wantReply === undefined || wantReply === true ? 1 : 0); + + writeUInt32BE(packet, termLen, ++p); + packet.utf8Write(term, p += 4, termLen); + + writeUInt32BE(packet, cols, p += termLen); + + writeUInt32BE(packet, rows, p += 4); + + writeUInt32BE(packet, width, p += 4); + + writeUInt32BE(packet, height, p += 4); + + writeUInt32BE(packet, modesLen, p += 4); + p += 4; + if (Array.isArray(modes)) { + for (let i = 0; i < modesLen; ++i) + packet[p++] = modes[i]; + } else if (Buffer.isBuffer(modes)) { + packet.set(modes, p); + } + + this._debug + && this._debug(`Outbound: Sending CHANNEL_REQUEST (r:${chan}, pty-req)`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + shell(chan, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + 5 + 1); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 5, p += 4); + packet.utf8Write('shell', p += 4, 5); + + packet[p += 5] = (wantReply === undefined || wantReply === true ? 1 : 0); + + this._debug + && this._debug(`Outbound: Sending CHANNEL_REQUEST (r:${chan}, shell)`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + exec(chan, cmd, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + const isBuf = Buffer.isBuffer(cmd); + const cmdLen = (isBuf ? cmd.length : Buffer.byteLength(cmd)); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + 4 + 1 + 4 + cmdLen); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 4, p += 4); + packet.utf8Write('exec', p += 4, 4); + + packet[p += 4] = (wantReply === undefined || wantReply === true ? 1 : 0); + + writeUInt32BE(packet, cmdLen, ++p); + if (isBuf) + packet.set(cmd, p += 4); + else + packet.utf8Write(cmd, p += 4, cmdLen); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_REQUEST (r:${chan}, exec: ${cmd})` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + signal(chan, signal) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + const origSignal = signal; + + signal = signal.toUpperCase(); + if (signal.slice(0, 3) === 'SIG') + signal = signal.slice(3); + + if (SIGNALS[signal] !== 1) + throw new Error(`Invalid signal: ${origSignal}`); + + const signalLen = signal.length; + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 4 + 6 + 1 + 4 + signalLen + ); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 6, p += 4); + packet.utf8Write('signal', p += 4, 6); + + packet[p += 6] = 0; + + writeUInt32BE(packet, signalLen, ++p); + packet.utf8Write(signal, p += 4, signalLen); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_REQUEST (r:${chan}, signal: ${signal})` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + env(chan, key, val, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + const keyLen = Buffer.byteLength(key); + const isBuf = Buffer.isBuffer(val); + const valLen = (isBuf ? val.length : Buffer.byteLength(val)); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 4 + 3 + 1 + 4 + keyLen + 4 + valLen + ); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 3, p += 4); + packet.utf8Write('env', p += 4, 3); + + packet[p += 3] = (wantReply === undefined || wantReply === true ? 1 : 0); + + writeUInt32BE(packet, keyLen, ++p); + packet.utf8Write(key, p += 4, keyLen); + + writeUInt32BE(packet, valLen, p += keyLen); + if (isBuf) + packet.set(val, p += 4); + else + packet.utf8Write(val, p += 4, valLen); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_REQUEST (r:${chan}, env: ${key}=${val})` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + x11Forward(chan, cfg, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + const protocol = cfg.protocol; + const cookie = cfg.cookie; + const isBufProto = Buffer.isBuffer(protocol); + const protoLen = (isBufProto + ? protocol.length + : Buffer.byteLength(protocol)); + const isBufCookie = Buffer.isBuffer(cookie); + const cookieLen = (isBufCookie + ? cookie.length + : Buffer.byteLength(cookie)); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 4 + 7 + 1 + 1 + 4 + protoLen + 4 + cookieLen + 4 + ); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 7, p += 4); + packet.utf8Write('x11-req', p += 4, 7); + + packet[p += 7] = (wantReply === undefined || wantReply === true ? 1 : 0); + + packet[++p] = (cfg.single ? 1 : 0); + + writeUInt32BE(packet, protoLen, ++p); + if (isBufProto) + packet.set(protocol, p += 4); + else + packet.utf8Write(protocol, p += 4, protoLen); + + writeUInt32BE(packet, cookieLen, p += protoLen); + if (isBufCookie) + packet.set(cookie, p += 4); + else + packet.latin1Write(cookie, p += 4, cookieLen); + + writeUInt32BE(packet, (cfg.screen || 0), p += cookieLen); + + this._debug + && this._debug(`Outbound: Sending CHANNEL_REQUEST (r:${chan}, x11-req)`); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + subsystem(chan, name, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + const nameLen = Buffer.byteLength(name); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + 9 + 1 + 4 + nameLen); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 9, p += 4); + packet.utf8Write('subsystem', p += 4, 9); + + packet[p += 9] = (wantReply === undefined || wantReply === true ? 1 : 0); + + writeUInt32BE(packet, nameLen, ++p); + packet.utf8Write(name, p += 4, nameLen); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_REQUEST (r:${chan}, subsystem: ${name})` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + openssh_agentForward(chan, wantReply) { + if (this._server) + throw new Error('Client-only method called in server mode'); + + // Does not consume window space + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + 26 + 1); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 26, p += 4); + packet.utf8Write('auth-agent-req@openssh.com', p += 4, 26); + + packet[p += 26] = (wantReply === undefined || wantReply === true ? 1 : 0); + + if (this._debug) { + this._debug( + 'Outbound: Sending CHANNEL_REQUEST ' + + `(r:${chan}, auth-agent-req@openssh.com)` + ); + } + sendPacket(this, this._packetRW.write.finalize(packet)); + } + + // =========================================================================== + // Server-specific =========================================================== + // =========================================================================== + + // Global + // ------ + serviceAccept(svcName) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + const svcNameLen = Buffer.byteLength(svcName); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + svcNameLen); + + packet[p] = MESSAGE.SERVICE_ACCEPT; + + writeUInt32BE(packet, svcNameLen, ++p); + packet.utf8Write(svcName, p += 4, svcNameLen); + + this._debug && this._debug(`Outbound: Sending SERVICE_ACCEPT (${svcName})`); + sendPacket(this, this._packetRW.write.finalize(packet)); + + if (this._server && this._banner && svcName === 'ssh-userauth') { + const banner = this._banner; + this._banner = undefined; // Prevent banner from being displayed again + const bannerLen = Buffer.byteLength(banner); + p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + bannerLen + 4); + + packet[p] = MESSAGE.USERAUTH_BANNER; + + writeUInt32BE(packet, bannerLen, ++p); + packet.utf8Write(banner, p += 4, bannerLen); + + writeUInt32BE(packet, 0, p += bannerLen); // Empty language tag + + this._debug && this._debug('Outbound: Sending USERAUTH_BANNER'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + } + // 'ssh-connection' service-specific + forwardedTcpip(chan, initWindow, maxPacket, cfg) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + const boundAddrLen = Buffer.byteLength(cfg.boundAddr); + const remoteAddrLen = Buffer.byteLength(cfg.remoteAddr); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 15 + 4 + 4 + 4 + 4 + boundAddrLen + 4 + 4 + remoteAddrLen + 4 + ); + + packet[p] = MESSAGE.CHANNEL_OPEN; + + writeUInt32BE(packet, 15, ++p); + packet.utf8Write('forwarded-tcpip', p += 4, 15); + + writeUInt32BE(packet, chan, p += 15); + + writeUInt32BE(packet, initWindow, p += 4); + + writeUInt32BE(packet, maxPacket, p += 4); + + writeUInt32BE(packet, boundAddrLen, p += 4); + packet.utf8Write(cfg.boundAddr, p += 4, boundAddrLen); + + writeUInt32BE(packet, cfg.boundPort, p += boundAddrLen); + + writeUInt32BE(packet, remoteAddrLen, p += 4); + packet.utf8Write(cfg.remoteAddr, p += 4, remoteAddrLen); + + writeUInt32BE(packet, cfg.remotePort, p += remoteAddrLen); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_OPEN (r:${chan}, forwarded-tcpip)` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + x11(chan, initWindow, maxPacket, cfg) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + const addrLen = Buffer.byteLength(cfg.originAddr); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 3 + 4 + 4 + 4 + 4 + addrLen + 4 + ); + + packet[p] = MESSAGE.CHANNEL_OPEN; + + writeUInt32BE(packet, 3, ++p); + packet.utf8Write('x11', p += 4, 3); + + writeUInt32BE(packet, chan, p += 3); + + writeUInt32BE(packet, initWindow, p += 4); + + writeUInt32BE(packet, maxPacket, p += 4); + + writeUInt32BE(packet, addrLen, p += 4); + packet.utf8Write(cfg.originAddr, p += 4, addrLen); + + writeUInt32BE(packet, cfg.originPort, p += addrLen); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_OPEN (r:${chan}, x11)` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + openssh_authAgent(chan, initWindow, maxPacket) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 22 + 4 + 4 + 4); + + packet[p] = MESSAGE.CHANNEL_OPEN; + + writeUInt32BE(packet, 22, ++p); + packet.utf8Write('auth-agent@openssh.com', p += 4, 22); + + writeUInt32BE(packet, chan, p += 22); + + writeUInt32BE(packet, initWindow, p += 4); + + writeUInt32BE(packet, maxPacket, p += 4); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_OPEN (r:${chan}, auth-agent@openssh.com)` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + openssh_forwardedStreamLocal(chan, initWindow, maxPacket, cfg) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + const pathLen = Buffer.byteLength(cfg.socketPath); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 33 + 4 + 4 + 4 + 4 + pathLen + 4 + ); + + packet[p] = MESSAGE.CHANNEL_OPEN; + + writeUInt32BE(packet, 33, ++p); + packet.utf8Write('forwarded-streamlocal@openssh.com', p += 4, 33); + + writeUInt32BE(packet, chan, p += 33); + + writeUInt32BE(packet, initWindow, p += 4); + + writeUInt32BE(packet, maxPacket, p += 4); + + writeUInt32BE(packet, pathLen, p += 4); + packet.utf8Write(cfg.socketPath, p += 4, pathLen); + + writeUInt32BE(packet, 0, p += pathLen); + + if (this._debug) { + this._debug( + 'Outbound: Sending CHANNEL_OPEN ' + + `(r:${chan}, forwarded-streamlocal@openssh.com)` + ); + } + sendPacket(this, this._packetRW.write.finalize(packet)); + } + exitStatus(chan, status) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + // Does not consume window space + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + 4 + 11 + 1 + 4); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 11, p += 4); + packet.utf8Write('exit-status', p += 4, 11); + + packet[p += 11] = 0; + + writeUInt32BE(packet, status, ++p); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_REQUEST (r:${chan}, exit-status: ${status})` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + exitSignal(chan, name, coreDumped, msg) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + // Does not consume window space + const nameLen = Buffer.byteLength(name); + const msgLen = (msg ? Buffer.byteLength(msg) : 0); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + 4 + 11 + 1 + 4 + nameLen + 1 + 4 + msgLen + 4 + ); + + packet[p] = MESSAGE.CHANNEL_REQUEST; + + writeUInt32BE(packet, chan, ++p); + + writeUInt32BE(packet, 11, p += 4); + packet.utf8Write('exit-signal', p += 4, 11); + + packet[p += 11] = 0; + + writeUInt32BE(packet, nameLen, ++p); + packet.utf8Write(name, p += 4, nameLen); + + packet[p += nameLen] = (coreDumped ? 1 : 0); + + writeUInt32BE(packet, msgLen, ++p); + + p += 4; + if (msgLen) { + packet.utf8Write(msg, p, msgLen); + p += msgLen; + } + + writeUInt32BE(packet, 0, p); + + this._debug && this._debug( + `Outbound: Sending CHANNEL_REQUEST (r:${chan}, exit-signal: ${name})` + ); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + // 'ssh-userauth' service-specific + authFailure(authMethods, isPartial) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + if (this._authsQueue.length === 0) + throw new Error('No auth in progress'); + + let methods; + + if (typeof authMethods === 'boolean') { + isPartial = authMethods; + authMethods = undefined; + } + + if (authMethods) { + methods = []; + for (let i = 0; i < authMethods.length; ++i) { + if (authMethods[i].toLowerCase() === 'none') + continue; + methods.push(authMethods[i]); + } + methods = methods.join(','); + } else { + methods = ''; + } + + const methodsLen = methods.length; + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + methodsLen + 1); + + packet[p] = MESSAGE.USERAUTH_FAILURE; + + writeUInt32BE(packet, methodsLen, ++p); + packet.utf8Write(methods, p += 4, methodsLen); + + packet[p += methodsLen] = (isPartial === true ? 1 : 0); + + this._authsQueue.shift(); + + this._debug && this._debug('Outbound: Sending USERAUTH_FAILURE'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + authSuccess() { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + if (this._authsQueue.length === 0) + throw new Error('No auth in progress'); + + const p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1); + + packet[p] = MESSAGE.USERAUTH_SUCCESS; + + this._authsQueue.shift(); + this._authenticated = true; + + this._debug && this._debug('Outbound: Sending USERAUTH_SUCCESS'); + sendPacket(this, this._packetRW.write.finalize(packet)); + + if (this._kex.negotiated.cs.compress === 'zlib@openssh.com') + this._packetRW.read = new ZlibPacketReader(); + if (this._kex.negotiated.sc.compress === 'zlib@openssh.com') + this._packetRW.write = new ZlibPacketWriter(this); + } + authPKOK(keyAlgo, key) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + if (this._authsQueue.length === 0 || this._authsQueue[0] !== 'publickey') + throw new Error('"publickey" auth not in progress'); + + const keyAlgoLen = Buffer.byteLength(keyAlgo); + const keyLen = key.length; + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + keyAlgoLen + 4 + keyLen); + + packet[p] = MESSAGE.USERAUTH_PK_OK; + + writeUInt32BE(packet, keyAlgoLen, ++p); + packet.utf8Write(keyAlgo, p += 4, keyAlgoLen); + + writeUInt32BE(packet, keyLen, p += keyAlgoLen); + packet.set(key, p += 4); + + this._authsQueue.shift(); + + this._debug && this._debug('Outbound: Sending USERAUTH_PK_OK'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + authPasswdChg(prompt) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + const promptLen = Buffer.byteLength(prompt); + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc(1 + 4 + promptLen + 4); + + packet[p] = MESSAGE.USERAUTH_PASSWD_CHANGEREQ; + + writeUInt32BE(packet, promptLen, ++p); + packet.utf8Write(prompt, p += 4, promptLen); + + writeUInt32BE(packet, 0, p += promptLen); // Empty language tag + + this._debug && this._debug('Outbound: Sending USERAUTH_PASSWD_CHANGEREQ'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } + authInfoReq(name, instructions, prompts) { + if (!this._server) + throw new Error('Server-only method called in client mode'); + + let promptsLen = 0; + const nameLen = name ? Buffer.byteLength(name) : 0; + const instrLen = instructions ? Buffer.byteLength(instructions) : 0; + + for (let i = 0; i < prompts.length; ++i) + promptsLen += 4 + Buffer.byteLength(prompts[i].prompt) + 1; + + let p = this._packetRW.write.allocStart; + const packet = this._packetRW.write.alloc( + 1 + 4 + nameLen + 4 + instrLen + 4 + 4 + promptsLen + ); + + packet[p] = MESSAGE.USERAUTH_INFO_REQUEST; + + writeUInt32BE(packet, nameLen, ++p); + p += 4; + if (name) { + packet.utf8Write(name, p, nameLen); + p += nameLen; + } + + writeUInt32BE(packet, instrLen, p); + p += 4; + if (instructions) { + packet.utf8Write(instructions, p, instrLen); + p += instrLen; + } + + writeUInt32BE(packet, 0, p); + + writeUInt32BE(packet, prompts.length, p += 4); + p += 4; + for (let i = 0; i < prompts.length; ++i) { + const prompt = prompts[i]; + const promptLen = Buffer.byteLength(prompt.prompt); + + writeUInt32BE(packet, promptLen, p); + p += 4; + if (promptLen) { + packet.utf8Write(prompt.prompt, p, promptLen); + p += promptLen; + } + packet[p++] = (prompt.echo ? 1 : 0); + } + + this._debug && this._debug('Outbound: Sending USERAUTH_INFO_REQUEST'); + sendPacket(this, this._packetRW.write.finalize(packet)); + } +} + +// SSH-protoversion-softwareversion (SP comments) CR LF +const RE_IDENT = /^SSH-(2\.0|1\.99)-([^ ]+)(?: (.*))?$/; + +// TODO: optimize this by starting n bytes from the end of this._buffer instead +// of the beginning +function parseHeader(chunk, p, len) { + let data; + let chunkOffset; + if (this._buffer) { + data = Buffer.allocUnsafe(this._buffer.length + (len - p)); + data.set(this._buffer, 0); + if (p === 0) { + data.set(chunk, this._buffer.length); + } else { + data.set(new Uint8Array(chunk.buffer, + chunk.byteOffset + p, + (len - p)), + this._buffer.length); + } + chunkOffset = this._buffer.length; + p = 0; + } else { + data = chunk; + chunkOffset = 0; + } + const op = p; + let start = p; + let end = p; + let nl = false; + let count = 0; + for (; p < data.length; ++p) { + const ch = data[p]; + if (ch === 13 /* '\r' */) { + nl = true; + } else if (ch === 10 /* '\n' */) { + if (end > start + && end - start > 4 + && data[start] === 83 /* 'S' */ + && data[start + 1] === 83 /* 'S' */ + && data[start + 2] === 72 /* 'H' */ + && data[start + 3] === 45 /* '-' */) { + + // Disallow client greetings + if (this._server && start !== op) + throw new Error('Client greeting not permitted'); + + const full = data.latin1Slice(op, end); + const identRaw = (start === op ? full : full.slice(start - op)); + const m = RE_IDENT.exec(identRaw); + if (!m) + throw new Error('Invalid identification string'); + + const header = { + greeting: (start === op ? '' : full.slice(0, start - op)), + identRaw, + versions: { + protocol: m[1], + software: m[2], + }, + comments: m[3] + }; + + // Needed during handshake + this._remoteIdentRaw = Buffer.from(identRaw); + + this._debug && this._debug(`Remote ident: ${inspect(identRaw)}`); + this._compatFlags = getCompatFlags(header); + + this._buffer = undefined; + this._cipher = new NullCipher(0, this._onWrite); + this._decipher = + new NullDecipher(0, onKEXPayload.bind(this, { firstPacket: true })); + this._parse = parsePacket; + + this._onHeader(header); + if (!this._destruct) { + // We disconnected inside _onHeader + return len; + } + + kexinit(this); + + return p + 1 - chunkOffset; + } + nl = false; + start = p + 1; + } else if (ch === 0 /* '\0' */ || nl) { + throw new Error( + `Invalid header character: ${JSON.stringify(String.fromCharCode(ch))}` + ); + } else if (++count >= MAX_HEADER_LEN) { + throw new Error('Header too long'); + } + end = p; + } + if (!this._buffer) + this._buffer = bufferSlice(data, op); + + return p - chunkOffset; +} + +function parsePacket(chunk, p, len) { + return this._decipher.decrypt(chunk, p, len); +} + +function onPayload(payload) { + // XXX: move this to the Decipher implementations? + + this._onPacket(); + + if (payload.length === 0) { + this._debug && this._debug('Inbound: Skipping empty packet payload'); + return; + } + + payload = this._packetRW.read.read(payload); + + const type = payload[0]; + if (type === MESSAGE.USERAUTH_SUCCESS + && !this._server + && !this._authenticated) { + this._authenticated = true; + if (this._kex.negotiated.cs.compress === 'zlib@openssh.com') + this._packetRW.write = new ZlibPacketWriter(this); + if (this._kex.negotiated.sc.compress === 'zlib@openssh.com') + this._packetRW.read = new ZlibPacketReader(); + } + const handler = MESSAGE_HANDLERS[type]; + if (handler === undefined) { + this._debug && this._debug(`Inbound: Unsupported message type: ${type}`); + return; + } + + return handler(this, payload); +} + +function getCompatFlags(header) { + const software = header.versions.software; + + let flags = 0; + + for (const rule of COMPAT_CHECKS) { + if (typeof rule[0] === 'string') { + if (software === rule[0]) + flags |= rule[1]; + } else if (rule[0].test(software)) { + flags |= rule[1]; + } + } + + return flags; +} + +function modesToBytes(modes) { + const keys = Object.keys(modes); + const bytes = Buffer.allocUnsafe((5 * keys.length) + 1); + let b = 0; + + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (key === 'TTY_OP_END') + continue; + + const opcode = TERMINAL_MODE[key]; + if (opcode === undefined) + continue; + + const val = modes[key]; + if (typeof val === 'number' && isFinite(val)) { + bytes[b++] = opcode; + bytes[b++] = val >>> 24; + bytes[b++] = val >>> 16; + bytes[b++] = val >>> 8; + bytes[b++] = val; + } + } + + bytes[b++] = TERMINAL_MODE.TTY_OP_END; + + if (b < bytes.length) + return bufferSlice(bytes, 0, b); + + return bytes; +} + +module.exports = Protocol; diff --git a/lib/protocol/SFTP.js b/lib/protocol/SFTP.js new file mode 100644 index 00000000..93ede0cf --- /dev/null +++ b/lib/protocol/SFTP.js @@ -0,0 +1,3531 @@ +'use strict'; + +const EventEmitter = require('events'); +const fs = require('fs'); +const { constants } = fs.constants; +const { + Readable: ReadableStream, + Writable: WritableStream +} = require('stream'); +const { inherits, isDate } = require('util'); + +const FastBuffer = Buffer[Symbol.species]; + +const { + bufferCopy, + bufferSlice, + makeBufferParser, + writeUInt32BE, +} = require('./utils.js'); + +const ATTR = { + SIZE: 0x00000001, + UIDGID: 0x00000002, + PERMISSIONS: 0x00000004, + ACMODTIME: 0x00000008, + EXTENDED: 0x80000000, +}; + +// Large enough to store all possible attributes +const ATTRS_BUF = Buffer.alloc(28); + +const STATUS_CODE = { + OK: 0, + EOF: 1, + NO_SUCH_FILE: 2, + PERMISSION_DENIED: 3, + FAILURE: 4, + BAD_MESSAGE: 5, + NO_CONNECTION: 6, + CONNECTION_LOST: 7, + OP_UNSUPPORTED: 8 +}; + +const VALID_STATUS_CODES = new Map( + Object.values(STATUS_CODE).map((n) => [n, 1]) +); + +const STATUS_CODE_STR = { + [STATUS_CODE.OK]: 'No error', + [STATUS_CODE.EOF]: 'End of file', + [STATUS_CODE.NO_SUCH_FILE]: 'No such file or directory', + [STATUS_CODE.PERMISSION_DENIED]: 'Permission denied', + [STATUS_CODE.FAILURE]: 'Failure', + [STATUS_CODE.BAD_MESSAGE]: 'Bad message', + [STATUS_CODE.NO_CONNECTION]: 'No connection', + [STATUS_CODE.CONNECTION_LOST]: 'Connection lost', + [STATUS_CODE.OP_UNSUPPORTED]: 'Operation unsupported', +}; + +const REQUEST = { + INIT: 1, + OPEN: 3, + CLOSE: 4, + READ: 5, + WRITE: 6, + LSTAT: 7, + FSTAT: 8, + SETSTAT: 9, + FSETSTAT: 10, + OPENDIR: 11, + READDIR: 12, + REMOVE: 13, + MKDIR: 14, + RMDIR: 15, + REALPATH: 16, + STAT: 17, + RENAME: 18, + READLINK: 19, + SYMLINK: 20, + EXTENDED: 200 +}; + +const RESPONSE = { + VERSION: 2, + STATUS: 101, + HANDLE: 102, + DATA: 103, + NAME: 104, + ATTRS: 105, + EXTENDED: 201 +}; + +const OPEN_MODE = { + READ: 0x00000001, + WRITE: 0x00000002, + APPEND: 0x00000004, + CREAT: 0x00000008, + TRUNC: 0x00000010, + EXCL: 0x00000020 +}; + +const MAX_PKT_LEN = 34000; +const MAX_REQID = 2 ** 32 - 1; +const CLIENT_VERSION_BUFFER = Buffer.from([ + 0, 0, 0, 5 /* length */, + REQUEST.INIT, + 0, 0, 0, 3 /* version */ +]); +const SERVER_VERSION_BUFFER = Buffer.from([ + 0, 0, 0, 5 /* length */, + RESPONSE.VERSION, + 0, 0, 0, 3 /* version */ +]); + +const RE_OPENSSH = /^SSH-2.0-(?:OpenSSH|dropbear)/; +const OPENSSH_MAX_DATA_LEN = + (256 * 1024) - (2 * 1024)/* account for header data */; + +const bufferParser = makeBufferParser(); + +const fakeStderr = { + readable: false, + writable: false, + push: (data) => {}, + once: () => {}, + on: () => {}, + emit: () => {}, + end: () => {}, +}; + +function noop() {} + +// Emulates enough of `Channel` to be able to be used as a drop-in replacement +// in order to process incoming data with as little overhead as possible +class SFTP extends EventEmitter { + constructor(client, chanInfo, cfg) { + super(); + + if (typeof cfg !== 'object' || !cfg) + cfg = {}; + + const remoteIdentRaw = client._protocol._remoteIdentRaw; + + this.server = !!cfg.server; + this._debug = (typeof cfg.debug === 'function' ? cfg.debug : undefined); + this._isOpenSSH = (remoteIdentRaw && RE_OPENSSH.test(remoteIdentRaw)); + + this._version = -1; + this._extensions = {}; + this._biOpt = cfg.biOpt; + this._pktLenBytes = 0; + this._pktLen = 0; + this._pktPos = 0; + this._pktType = 0; + this._pktData = undefined; + this._writeReqid = -1; + this._requests = {}; + this._maxDataLen = (this._isOpenSSH ? OPENSSH_MAX_DATA_LEN : 32768); + + // Channel compatibility + this._client = client; + this._protocol = client._protocol; + this._callbacks = []; + this._hasX11 = false; + this._exit = { + code: undefined, + signal: undefined, + dump: undefined, + desc: undefined, + }; + this._waitWindow = false; // SSH-level backpressure + this._chunkcb = undefined; + this._buffer = []; + this.type = chanInfo.type; + this.subtype = undefined; + this.incoming = chanInfo.incoming; + this.outgoing = chanInfo.outgoing; + this.stderr = fakeStderr; + this.readable = true; + } + + // This handles incoming data to parse + push(data) { + if (data === null) { + if (!this.readable) + return; + // No more incoming data from the remote side + this.readable = false; + this.emit('end'); + return; + } + /* + uint32 length + byte type + byte[length - 1] data payload + */ + let p = 0; + + while (p < data.length) { + if (this._pktLenBytes < 4) { + let nb = Math.min(4 - this._pktLenBytes, data.length - p); + this._pktLenBytes += nb; + + while (nb--) + this._pktLen = (this._pktLen << 8) + data[p++]; + + if (this._pktLenBytes < 4) + return; + if (this._pktLen === 0) + return doFatalSFTPError(this, 'Invalid packet length'); + if (this._pktLen > MAX_PKT_LEN) { + return doFatalSFTPError( + this, + `Packet length ${this._pktLen} exceeds max length of ${MAX_PKT_LEN}` + ); + } + if (p >= data.length) + return; + } + if (this._pktPos < this._pktLen) { + const nb = Math.min(this._pktLen - this._pktPos, data.length - p); + if (p !== 0 || nb !== data.length) { + if (nb === this._pktLen) { + this._pkt = new FastBuffer(data.buffer, data.byteOffset + p, nb); + } else { + if (!this._pkt) + this._pkt = Buffer.allocUnsafe(this._pktLen); + this._pkt.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._pktPos + ); + } + } else if (nb === this._pktLen) { + this._pkt = data; + } else { + this._pkt.set(data, this._pktPos); + } + p += nb; + this._pktPos += nb; + if (this._pktPos < this._pktLen) + return; + } + + const type = this._pkt[0]; + const payload = this._pkt; + + // Prepare for next packet + this._pktLen = 0; + this._pktLenBytes = 0; + this._pkt = undefined; + this._pktPos = 0; + + const handler = (this.server + ? SERVER_HANDLERS[type] + : CLIENT_HANDLERS[type]); + if (!handler) + return doFatalSFTPError(this, `Unknown packet type ${type}`); + + if (this._version === -1) { + if (this.server) { + if (type !== REQUEST.INIT) + return doFatalSFTPError(this, `Expected INIT packet, got ${type}`); + } else if (type !== RESPONSE.VERSION) { + return doFatalSFTPError(this, `Expected VERSION packet, got ${type}`); + } + } + + if (handler(this, payload) === false) + return; + } + } + + end() { + this.destroy(); + } + destroy() { + if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') { + this.outgoing.state = 'closing'; + this._protocol.channelClose(this.outgoing.id); + } + } + _init() { + this._init = noop; + if (!this.server) + sendOrBuffer(this, CLIENT_VERSION_BUFFER); + } + + // =========================================================================== + // Client-specific =========================================================== + // =========================================================================== + createReadStream(path, options) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + return new ReadStream(this, path, options); + } + createWriteStream(path, options) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + return new WriteStream(this, path, options); + } + open(path, flags_, attrs, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + if (typeof attrs === 'function') { + cb = attrs; + attrs = undefined; + } + + const flags = (typeof flags_ === 'number' ? flags_ : stringToFlags(flags_)); + if (flags === null) + throw new Error(`Unknown flags string: ${flags_}`); + + let attrsFlags = 0; + let attrsLen = 0; + if (typeof attrs === 'string' || typeof attrs === 'number') + attrs = { mode: attrs }; + if (typeof attrs === 'object' && attrs !== null) { + attrs = attrsToBytes(attrs); + attrsFlags = attrs.flags; + attrsLen = attrs.nb; + } + + /* + uint32 id + string filename + uint32 pflags + ATTRS attrs + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + 4 + attrsLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.OPEN; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + writeUInt32BE(buf, flags, p += pathLen); + writeUInt32BE(buf, attrsFlags, p += 4); + if (attrsLen) { + p += 4; + + if (attrsLen === ATTRS_BUF.length) + buf.set(ATTRS_BUF, p); + else + bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); + + p += attrsLen; + } + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPEN` + ); + } + close(handle, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + if (!Buffer.isBuffer(handle)) + throw new Error('handle is not a Buffer'); + + /* + uint32 id + string handle + */ + const handleLen = handle.length; + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.CLOSE; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, handleLen, p); + buf.set(handle, p += 4); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} CLOSE` + ); + } + read(handle, buf, off, len, position, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + if (!Buffer.isBuffer(handle)) + throw new Error('handle is not a Buffer'); + if (!Buffer.isBuffer(buf)) + throw new Error('buffer is not a Buffer'); + if (off >= buf.length) + throw new Error('offset is out of bounds'); + if (off + len > buf.length) + throw new Error('length extends beyond buffer'); + if (position === null) + throw new Error('null position currently unsupported'); + + /* + uint32 id + string handle + uint64 offset + uint32 len + */ + const handleLen = handle.length; + let p = 9; + let pos = position; + const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4); + + writeUInt32BE(out, out.length - 4, 0); + out[4] = REQUEST.READ; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(out, reqid, 5); + + writeUInt32BE(out, handleLen, p); + out.set(handle, p += 4); + p += handleLen; + for (let i = 7; i >= 0; --i) { + out[p + i] = pos & 0xFF; + pos /= 256; + } + writeUInt32BE(out, len, p += 8); + + this._requests[reqid] = { + cb: (err, data, nb) => { + if (err) { + if (cb._wantEOFError || err.code !== STATUS_CODE.EOF) + return cb(err); + } else if (nb > len) { + return cb(new Error('Received more data than requested')); + } + cb(undefined, nb || 0, data, position); + }, + buffer: bufferSlice(buf, off, off + len), + }; + + const isBuffered = sendOrBuffer(this, out); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READ` + ); + } + readData(handle, buf, off, len, position, cb) { + // Backwards compatibility + this.read(handle, buf, off, len, position, cb); + } + write(handle, buf, off, len, position, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + if (!Buffer.isBuffer(handle)) + throw new Error('handle is not a Buffer'); + if (!Buffer.isBuffer(buf)) + throw new Error('buffer is not a Buffer'); + if (off > buf.length) + throw new Error('offset is out of bounds'); + if (off + len > buf.length) + throw new Error('length extends beyond buffer'); + if (position === null) + throw new Error('null position currently unsupported'); + + if (!len) { + cb && process.nextTick(cb, undefined, 0); + return; + } + + const overflow = Math.max(len - this._maxDataLen, 0); + const origPosition = position; + + if (overflow) + len = this._maxDataLen; + + /* + uint32 id + string handle + uint64 offset + string data + */ + const handleLen = handle.length; + let p = 9; + const out = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 8 + 4 + len); + + writeUInt32BE(out, out.length - 4, 0); + out[4] = REQUEST.WRITE; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(out, reqid, 5); + + writeUInt32BE(out, handleLen, p); + out.set(handle, p += 4); + p += handleLen; + for (let i = 7; i >= 0; --i) { + out[p + i] = position & 0xFF; + position /= 256; + } + writeUInt32BE(out, len, p += 8); + bufferCopy(buf, out, off, off + len, p += 4); + + this._requests[reqid] = { + cb: (err) => { + if (err) { + cb && cb(err); + } else if (overflow) { + this.write(handle, + buf, + off + len, + overflow, + origPosition + len, + cb); + } else { + cb && cb(undefined, off + len); + } + } + }; + + const isBuffered = sendOrBuffer(this, out); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} WRITE` + ); + } + writeData(handle, buf, off, len, position, cb) { + // Backwards compatibility + this.write(handle, buf, off, len, position, cb); + } + fastGet(remotePath, localPath, opts, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + fastXfer(this, fs, remotePath, localPath, opts, cb); + } + fastPut(localPath, remotePath, opts, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + fastXfer(fs, this, localPath, remotePath, opts, cb); + } + // TODO: revisit `readFile()` from node + readFile(path, options, callback_) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + let callback; + if (typeof callback_ === 'function') { + callback = callback_; + } else if (typeof options === 'function') { + callback = options; + options = undefined; + } + + if (typeof options === 'string') + options = { encoding: options, flag: 'r' }; + else if (!options) + options = { encoding: null, flag: 'r' }; + else if (typeof options !== 'object') + throw new TypeError('Bad arguments'); + + const encoding = options.encoding; + if (encoding && !Buffer.isEncoding(encoding)) + throw new Error(`Unknown encoding: ${encoding}`); + + // First stat the file, so we know the size. + let size; + let buffer; // Single buffer with file data + let buffers; // List for when size is unknown + let pos = 0; + let handle; + + // SFTPv3 does not support using -1 for read position, so we have to track + // read position manually + let bytesRead = 0; + + const flag = options.flag || 'r'; + + const read = () => { + if (size === 0) { + buffer = Buffer.allocUnsafe(8192); + this.read(handle, buffer, 0, 8192, bytesRead, afterRead); + } else { + this.read(handle, buffer, pos, size - pos, bytesRead, afterRead); + } + }; + + const afterRead = (er, nbytes) => { + let eof; + if (er) { + eof = (er.code === STATUS_CODE.EOF); + if (!eof) { + return this.close(handle, () => { + return callback && callback(er); + }); + } + } else { + eof = false; + } + + if (eof || (size === 0 && nbytes === 0)) + return close(); + + bytesRead += nbytes; + pos += nbytes; + if (size !== 0) { + if (pos === size) + close(); + else + read(); + } else { + // Unknown size, just read until we don't get bytes. + buffers.push(bufferSlice(buffer, 0, nbytes)); + read(); + } + }; + afterRead._wantEOFError = true; + + const close = () => { + this.close(handle, (er) => { + if (size === 0) { + // Collect the data into the buffers list. + // TODO: replace `Buffer.concat()` + buffer = Buffer.concat(buffers, pos); + } else if (pos < size) { + buffer = bufferSlice(buffer, 0, pos); + } + + if (encoding) + buffer = buffer.toString(encoding); + return callback && callback(er, buffer); + }); + }; + + this.open(path, flag, 0o666, (er, handle_) => { + if (er) + return callback && callback(er); + handle = handle_; + + const tryStat = (er, st) => { + if (er) { + // Try stat() for sftp servers that may not support fstat() for + // whatever reason + this.stat(path, (er_, st_) => { + if (er_) { + return this.close(handle, () => { + callback && callback(er); + }); + } + tryStat(null, st_); + }); + return; + } + + size = st.size || 0; + if (size === 0) { + // The kernel lies about many files. + // Go ahead and try to read some bytes. + buffers = []; + return read(); + } + + buffer = Buffer.allocUnsafe(size); + read(); + }; + this.fstat(handle, tryStat); + }); + } + writeFile(path, data, options, callback_) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + let callback; + if (typeof callback_ === 'function') { + callback = callback_; + } else if (typeof options === 'function') { + callback = options; + options = undefined; + } + + if (typeof options === 'string') + options = { encoding: options, mode: 0o666, flag: 'w' }; + else if (!options) + options = { encoding: 'utf8', mode: 0o666, flag: 'w' }; + else if (typeof options !== 'object') + throw new TypeError('Bad arguments'); + + if (options.encoding && !Buffer.isEncoding(options.encoding)) + throw new Error(`Unknown encoding: ${options.encoding}`); + + const flag = options.flag || 'w'; + this.open(path, flag, options.mode, (openErr, handle) => { + if (openErr) { + callback && callback(openErr); + } else { + const buffer = (Buffer.isBuffer(data) + ? data + : Buffer.from('' + data, options.encoding || 'utf8')); + const position = (/a/.test(flag) ? null : 0); + + // SFTPv3 does not support the notion of 'current position' + // (null position), so we just attempt to append to the end of the file + // instead + if (position === null) { + const tryStat = (er, st) => { + if (er) { + // Try stat() for sftp servers that may not support fstat() for + // whatever reason + this.stat(path, (er_, st_) => { + if (er_) { + return this.close(handle, () => { + callback && callback(er); + }); + } + tryStat(null, st_); + }); + return; + } + writeAll(this, handle, buffer, 0, buffer.length, st.size, callback); + }; + this.fstat(handle, tryStat); + return; + } + writeAll(this, handle, buffer, 0, buffer.length, position, callback); + } + }); + } + appendFile(path, data, options, callback_) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + let callback; + if (typeof callback_ === 'function') { + callback = callback_; + } else if (typeof options === 'function') { + callback = options; + options = undefined; + } + + if (typeof options === 'string') + options = { encoding: options, mode: 0o666, flag: 'a' }; + else if (!options) + options = { encoding: 'utf8', mode: 0o666, flag: 'a' }; + else if (typeof options !== 'object') + throw new TypeError('Bad arguments'); + + if (!options.flag) + options = Object.assign({ flag: 'a' }, options); + this.writeFile(path, data, options, callback); + } + exists(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + this.stat(path, (err) => { + cb && cb(err ? false : true); + }); + } + unlink(filename, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string filename + */ + const fnameLen = Buffer.byteLength(filename); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + fnameLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.REMOVE; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, fnameLen, p); + buf.utf8Write(filename, p += 4, fnameLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REMOVE` + ); + } + rename(oldPath, newPath, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string oldpath + string newpath + */ + const oldLen = Buffer.byteLength(oldPath); + const newLen = Buffer.byteLength(newPath); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + oldLen + 4 + newLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.RENAME; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, oldLen, p); + buf.utf8Write(oldPath, p += 4, oldLen); + writeUInt32BE(buf, newLen, p += oldLen); + buf.utf8Write(newPath, p += 4, newLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RENAME` + ); + } + mkdir(path, attrs, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + let flags = 0; + let attrsLen = 0; + + if (typeof attrs === 'function') { + cb = attrs; + attrs = undefined; + } + if (typeof attrs === 'object' && attrs !== null) { + attrs = attrsToBytes(attrs); + flags = attrs.flags; + attrsLen = attrs.nb; + } + + /* + uint32 id + string path + ATTRS attrs + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.MKDIR; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + writeUInt32BE(buf, flags, p += pathLen); + if (attrsLen) { + p += 4; + + if (attrsLen === ATTRS_BUF.length) + buf.set(ATTRS_BUF, p); + else + bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); + + p += attrsLen; + } + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} MKDIR` + ); + } + rmdir(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string path + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.RMDIR; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} RMDIR` + ); + } + readdir(where, opts, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + if (typeof opts === 'function') { + cb = opts; + opts = {}; + } + if (typeof opts !== 'object' || opts === null) + opts = {}; + + const doFilter = (opts && opts.full ? false : true); + + if (!Buffer.isBuffer(where) && typeof where !== 'string') + throw new Error('missing directory handle or path'); + + if (typeof where === 'string') { + const entries = []; + let e = 0; + + const reread = (err, handle) => { + if (err) + return cb(err); + + this.readdir(handle, opts, (err, list) => { + const eof = (err && err.code === STATUS_CODE.EOF); + + if (err && !eof) + return this.close(handle, () => cb(err)); + + if (eof) { + return this.close(handle, (err) => { + if (err) + return cb(err); + cb(undefined, entries); + }); + } + + for (let i = 0; i < list.length; ++i, ++e) + entries[e] = list[i]; + + reread(undefined, handle); + }); + }; + return this.opendir(where, reread); + } + + /* + uint32 id + string handle + */ + const handleLen = where.length; + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.READDIR; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, handleLen, p); + buf.set(where, p += 4); + + this._requests[reqid] = { + cb: (doFilter + ? (err, list) => { + if (err) + return cb(err); + + for (let i = list.length - 1; i >= 0; --i) { + if (list[i].filename === '.' || list[i].filename === '..') + list.splice(i, 1); + } + + cb(undefined, list); + } + : cb) + }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READDIR` + ); + } + fstat(handle, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + if (!Buffer.isBuffer(handle)) + throw new Error('handle is not a Buffer'); + + /* + uint32 id + string handle + */ + const handleLen = handle.length; + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.FSTAT; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, handleLen, p); + buf.set(handle, p += 4); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSTAT` + ); + } + stat(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string path + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.STAT; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STAT` + ); + } + lstat(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string path + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.LSTAT; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} LSTAT` + ); + } + opendir(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string path + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.OPENDIR; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} OPENDIR` + ); + } + setstat(path, attrs, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + let flags = 0; + let attrsLen = 0; + + if (typeof attrs === 'object' && attrs !== null) { + attrs = attrsToBytes(attrs); + flags = attrs.flags; + attrsLen = attrs.nb; + } else if (typeof attrs === 'function') { + cb = attrs; + } + + /* + uint32 id + string path + ATTRS attrs + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen + 4 + attrsLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.SETSTAT; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + writeUInt32BE(buf, flags, p += pathLen); + if (attrsLen) { + p += 4; + + if (attrsLen === ATTRS_BUF.length) + buf.set(ATTRS_BUF, p); + else + bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); + + p += attrsLen; + } + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SETSTAT` + ); + } + fsetstat(handle, attrs, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + if (!Buffer.isBuffer(handle)) + throw new Error('handle is not a Buffer'); + + let flags = 0; + let attrsLen = 0; + + if (typeof attrs === 'object' && attrs !== null) { + attrs = attrsToBytes(attrs); + flags = attrs.flags; + attrsLen = attrs.nb; + } else if (typeof attrs === 'function') { + cb = attrs; + } + + /* + uint32 id + string handle + ATTRS attrs + */ + const handleLen = handle.length; + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen + 4 + attrsLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.FSETSTAT; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, handleLen, p); + buf.set(handle, p += 4); + writeUInt32BE(buf, flags, p += handleLen); + if (attrsLen) { + p += 4; + + if (attrsLen === ATTRS_BUF.length) + buf.set(ATTRS_BUF, p); + else + bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); + + p += attrsLen; + } + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} FSETSTAT` + ); + } + futimes(handle, atime, mtime, cb) { + return this.fsetstat(handle, { + atime: toUnixTimestamp(atime), + mtime: toUnixTimestamp(mtime) + }, cb); + } + utimes(path, atime, mtime, cb) { + return this.setstat(path, { + atime: toUnixTimestamp(atime), + mtime: toUnixTimestamp(mtime) + }, cb); + } + fchown(handle, uid, gid, cb) { + return this.fsetstat(handle, { + uid: uid, + gid: gid + }, cb); + } + chown(path, uid, gid, cb) { + return this.setstat(path, { + uid: uid, + gid: gid + }, cb); + } + fchmod(handle, mode, cb) { + return this.fsetstat(handle, { + mode: mode + }, cb); + } + chmod(path, mode, cb) { + return this.setstat(path, { + mode: mode + }, cb); + } + readlink(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string path + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.READLINK; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + + this._requests[reqid] = { + cb: (err, names) => { + if (err) + return cb(err); + if (!names || !names.length) + return cb(new Error('Response missing link info')); + cb(undefined, names[0].filename); + } + }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} READLINK` + ); + } + symlink(targetPath, linkPath, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string linkpath + string targetpath + */ + const linkLen = Buffer.byteLength(linkPath); + const targetLen = Buffer.byteLength(targetPath); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + linkLen + 4 + targetLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.SYMLINK; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + if (this._isOpenSSH) { + // OpenSSH has linkpath and targetpath positions switched + writeUInt32BE(buf, targetLen, p); + buf.utf8Write(targetPath, p += 4, targetLen); + writeUInt32BE(buf, linkLen, p += targetLen); + buf.utf8Write(linkPath, p += 4, linkLen); + } else { + writeUInt32BE(buf, linkLen, p); + buf.utf8Write(linkPath, p += 4, linkLen); + writeUInt32BE(buf, targetLen, p += linkLen); + buf.utf8Write(targetPath, p += 4, targetLen); + } + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} SYMLINK` + ); + } + realpath(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + /* + uint32 id + string path + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + pathLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.REALPATH; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, pathLen, p); + buf.utf8Write(path, p += 4, pathLen); + + this._requests[reqid] = { + cb: (err, names) => { + if (err) + return cb(err); + if (!names || !names.length) + return cb(new Error('Response missing path info')); + cb(undefined, names[0].filename); + } + }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} REALPATH` + ); + } + // extended requests + ext_openssh_rename(oldPath, newPath, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + const ext = this._extensions['posix-rename@openssh.com']; + if (!ext || ext !== '1') + throw new Error('Server does not support this extended request'); + + /* + uint32 id + string "posix-rename@openssh.com" + string oldpath + string newpath + */ + const oldLen = Buffer.byteLength(oldPath); + const newLen = Buffer.byteLength(newPath); + let p = 9; + const buf = + Buffer.allocUnsafe(4 + 1 + 4 + 4 + 24 + 4 + oldLen + 4 + newLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.EXTENDED; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, 24, p); + buf.utf8Write('posix-rename@openssh.com', p += 4, 24); + writeUInt32BE(buf, oldLen, p += 24); + buf.utf8Write(oldPath, p += 4, oldLen); + writeUInt32BE(buf, newLen, p += oldLen); + buf.utf8Write(newPath, p += 4, newLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + if (this._debug) { + const which = (isBuffered ? 'Buffered' : 'Sending'); + this._debug(`SFTP: Outbound: ${which} posix-rename@openssh.com`); + } + } + ext_openssh_statvfs(path, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + const ext = this._extensions['statvfs@openssh.com']; + if (!ext || ext !== '2') + throw new Error('Server does not support this extended request'); + + /* + uint32 id + string "statvfs@openssh.com" + string path + */ + const pathLen = Buffer.byteLength(path); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 19 + 4 + pathLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.EXTENDED; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, 19, p); + buf.utf8Write('statvfs@openssh.com', p += 4, 19); + writeUInt32BE(buf, pathLen, p += 19); + buf.utf8Write(path, p += 4, pathLen); + + this._requests[reqid] = { extended: 'statvfs@openssh.com', cb }; + + const isBuffered = sendOrBuffer(this, buf); + if (this._debug) { + const which = (isBuffered ? 'Buffered' : 'Sending'); + this._debug(`SFTP: Outbound: ${which} statvfs@openssh.com`); + } + } + ext_openssh_fstatvfs(handle, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + const ext = this._extensions['fstatvfs@openssh.com']; + if (!ext || ext !== '2') + throw new Error('Server does not support this extended request'); + if (!Buffer.isBuffer(handle)) + throw new Error('handle is not a Buffer'); + + /* + uint32 id + string "fstatvfs@openssh.com" + string handle + */ + const handleLen = handle.length; + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + handleLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.EXTENDED; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, 20, p); + buf.utf8Write('fstatvfs@openssh.com', p += 4, 20); + writeUInt32BE(buf, handleLen, p += 20); + buf.set(handle, p += 4); + + this._requests[reqid] = { extended: 'fstatvfs@openssh.com', cb }; + + const isBuffered = sendOrBuffer(this, buf); + if (this._debug) { + const which = (isBuffered ? 'Buffered' : 'Sending'); + this._debug(`SFTP: Outbound: ${which} fstatvfs@openssh.com`); + } + } + ext_openssh_hardlink(oldPath, newPath, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + const ext = this._extensions['hardlink@openssh.com']; + if (!ext || ext.indexOf('1') === -1) + throw new Error('Server does not support this extended request'); + + /* + uint32 id + string "hardlink@openssh.com" + string oldpath + string newpath + */ + const oldLen = Buffer.byteLength(oldPath); + const newLen = Buffer.byteLength(newPath); + let p = 9; + const buf = + Buffer.allocUnsafe(4 + 1 + 4 + 4 + 20 + 4 + oldLen + 4 + newLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.EXTENDED; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, 20, p); + buf.utf8Write('hardlink@openssh.com', p += 4, 20); + writeUInt32BE(buf, oldLen, p += 20); + buf.utf8Write(oldPath, p += 4, oldLen); + writeUInt32BE(buf, newLen, p += oldLen); + buf.utf8Write(newPath, p += 4, newLen); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + if (this._debug) { + const which = (isBuffered ? 'Buffered' : 'Sending'); + this._debug(`SFTP: Outbound: ${which} hardlink@openssh.com`); + } + } + ext_openssh_fsync(handle, cb) { + if (this.server) + throw new Error('Client-only method called in server mode'); + + const ext = this._extensions['fsync@openssh.com']; + if (!ext || ext.indexOf('1') === -1) + throw new Error('Server does not support this extended request'); + if (!Buffer.isBuffer(handle)) + throw new Error('handle is not a Buffer'); + + /* + uint32 id + string "fsync@openssh.com" + string handle + */ + const handleLen = handle.length; + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 17 + 4 + handleLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = REQUEST.EXTENDED; + const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, 17, p); + buf.utf8Write('fsync@openssh.com', p += 4, 17); + writeUInt32BE(buf, handleLen, p += 17); + buf.set(handle, p += 4); + + this._requests[reqid] = { cb }; + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} fsync@openssh.com` + ); + } + // =========================================================================== + // Client-specific =========================================================== + // =========================================================================== + handle(reqid, handle) { + if (!this.server) + throw new Error('Server-only method called in client mode'); + + if (!Buffer.isBuffer(handle)) + throw new Error('handle is not a Buffer'); + + const handleLen = handle.length; + + if (handleLen > 256) + throw new Error('handle too large (> 256 bytes)'); + + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + handleLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = RESPONSE.HANDLE; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, handleLen, p); + if (handleLen) + buf.set(handle, p += 4); + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} HANDLE` + ); + } + status(reqid, code, message) { + if (!this.server) + throw new Error('Server-only method called in client mode'); + + if (!VALID_STATUS_CODES.has(code)) + throw new Error(`Bad status code: ${code}`); + + message || (message = ''); + + const msgLen = Buffer.byteLength(message); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + 4 + msgLen + 4); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = RESPONSE.STATUS; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, code, p); + + writeUInt32BE(buf, msgLen, p += 4); + p += 4; + if (msgLen) { + buf.utf8Write(message, p, msgLen); + p += msgLen; + } + + writeUInt32BE(buf, 0, p); // Empty language tag + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} STATUS` + ); + } + data(reqid, data, encoding) { + if (!this.server) + throw new Error('Server-only method called in client mode'); + + const isBuffer = Buffer.isBuffer(data); + + if (!isBuffer && typeof data !== 'string') + throw new Error('data is not a Buffer or string'); + + let isUTF8; + if (!isBuffer && !encoding) { + encoding = undefined; + isUTF8 = true; + } + + const dataLen = ( + isBuffer + ? data.length + : Buffer.byteLength(data, encoding) + ); + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + dataLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = RESPONSE.DATA; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, dataLen, p); + if (dataLen) { + if (isBuffer) + buf.set(data, p += 4); + else if (isUTF8) + buf.utf8Write(data, p += 4, dataLen); + else + buf.write(data, p += 4, dataLen, encoding); + } + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} DATA` + ); + } + name(reqid, names) { + if (!this.server) + throw new Error('Server-only method called in client mode'); + + if (!Array.isArray(names)) { + if (typeof names !== 'object' || names === null) + throw new Error('names is not an object or array'); + names = [ names ]; + } + + const count = names.length; + let namesLen = 0; + let nameAttrs; + const attrs = []; + + for (let i = 0; i < count; ++i) { + const name = names[i]; + const filename = ( + !name || !name.filename || typeof name.filename !== 'string' + ? '' + : name.filename + ); + namesLen += 4 + Buffer.byteLength(filename); + const longname = ( + !name || !name.longname || typeof name.longname !== 'string' + ? '' + : name.longname + ); + namesLen += 4 + Buffer.byteLength(longname); + + if (typeof name.attrs === 'object' && name.attrs !== null) { + nameAttrs = attrsToBytes(name.attrs); + namesLen += 4 + nameAttrs.nb; + + if (nameAttrs.nb) { + let bytes; + + if (nameAttrs.nb === ATTRS_BUF.length) { + bytes = new Uint8Array(ATTRS_BUF); + } else { + bytes = new Uint8Array(nameAttrs.nb); + bufferCopy(ATTRS_BUF, bytes, 0, nameAttrs.nb, 0); + } + + nameAttrs.bytes = bytes; + } + + attrs.push(nameAttrs); + } else { + namesLen += 4; + attrs.push(null); + } + } + + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + namesLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = RESPONSE.NAME; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, count, p); + + p += 4; + + for (let i = 0; i < count; ++i) { + const name = names[i]; + + { + const filename = ( + !name || !name.filename || typeof name.filename !== 'string' + ? '' + : name.filename + ); + const len = Buffer.byteLength(filename); + writeUInt32BE(buf, len, p); + p += 4; + if (len) { + buf.utf8Write(filename, p, len); + p += len; + } + } + + { + const longname = ( + !name || !name.longname || typeof name.longname !== 'string' + ? '' + : name.longname + ); + const len = Buffer.byteLength(longname); + writeUInt32BE(buf, len, p); + p += 4; + if (len) { + buf.utf8Write(longname, p, len); + p += len; + } + } + + const attr = attrs[i]; + if (attr) { + writeUInt32BE(buf, attr.flags, p); + p += 4; + if (attr.flags && attr.bytes) { + buf.set(attr.bytes, p); + p += attr.nb; + } + } else { + writeUInt32BE(buf, 0, p); + p += 4; + } + } + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} NAME` + ); + } + attrs(reqid, attrs) { + if (!this.server) + throw new Error('Server-only method called in client mode'); + + if (typeof attrs !== 'object' || attrs === null) + throw new Error('attrs is not an object'); + + attrs = attrsToBytes(attrs); + const flags = attrs.flags; + const attrsLen = attrs.nb; + let p = 9; + const buf = Buffer.allocUnsafe(4 + 1 + 4 + 4 + attrsLen); + + writeUInt32BE(buf, buf.length - 4, 0); + buf[4] = RESPONSE.ATTRS; + writeUInt32BE(buf, reqid, 5); + + writeUInt32BE(buf, flags, p); + if (attrsLen) { + p += 4; + + if (attrsLen === ATTRS_BUF.length) + buf.set(ATTRS_BUF, p); + else + bufferCopy(ATTRS_BUF, buf, 0, attrsLen, p); + + p += attrsLen; + } + + const isBuffered = sendOrBuffer(this, buf); + this._debug && this._debug( + `SFTP: Outbound: ${isBuffered ? 'Buffered' : 'Sending'} ATTRS` + ); + } +} + +function tryCreateBuffer(size) { + try { + return Buffer.allocUnsafe(size); + } catch (ex) { + return ex; + } +} + +function fastXfer(src, dst, srcPath, dstPath, opts, cb) { + let concurrency = 64; + let chunkSize = 32768; + let onstep; + let mode; + let fileSize; + + if (typeof opts === 'function') { + cb = opts; + } else if (typeof opts === 'object' && opts !== null) { + if (typeof opts.concurrency === 'number' + && opts.concurrency > 0 + && !isNaN(opts.concurrency)) { + concurrency = opts.concurrency; + } + if (typeof opts.chunkSize === 'number' + && opts.chunkSize > 0 + && !isNaN(opts.chunkSize)) { + chunkSize = opts.chunkSize; + } + if (typeof opts.fileSize === 'number' + && opts.fileSize > 0 + && !isNaN(opts.fileSize)) { + fileSize = opts.fileSize; + } + if (typeof opts.step === 'function') + onstep = opts.step; + + if (typeof opts.mode === 'string' || typeof opts.mode === 'number') + mode = modeNum(opts.mode); + } + + // Internal state variables + let fsize; + let pdst = 0; + let total = 0; + let hadError = false; + let srcHandle; + let dstHandle; + let readbuf; + let bufsize = chunkSize * concurrency; + + function onerror(err) { + if (hadError) + return; + + hadError = true; + + let left = 0; + let cbfinal; + + if (srcHandle || dstHandle) { + cbfinal = () => { + if (--left === 0) + cb(err); + }; + if (srcHandle && (src === fs || src.writable)) + ++left; + if (dstHandle && (dst === fs || dst.writable)) + ++left; + if (srcHandle && (src === fs || src.writable)) + src.close(srcHandle, cbfinal); + if (dstHandle && (dst === fs || dst.writable)) + dst.close(dstHandle, cbfinal); + } else { + cb(err); + } + } + + src.open(srcPath, 'r', (err, sourceHandle) => { + if (err) + return onerror(err); + + srcHandle = sourceHandle; + + if (fileSize === undefined) + src.fstat(srcHandle, tryStat); + else + tryStat(null, { size: fileSize }); + + function tryStat(err, attrs) { + if (err) { + if (src !== fs) { + // Try stat() for sftp servers that may not support fstat() for + // whatever reason + src.stat(srcPath, (err_, attrs_) => { + if (err_) + return onerror(err); + tryStat(null, attrs_); + }); + return; + } + return onerror(err); + } + fsize = attrs.size; + + dst.open(dstPath, 'w', (err, destHandle) => { + if (err) + return onerror(err); + + dstHandle = destHandle; + + if (fsize <= 0) + return onerror(); + + // Use less memory where possible + while (bufsize > fsize) { + if (concurrency === 1) { + bufsize = fsize; + break; + } + bufsize -= chunkSize; + --concurrency; + } + + readbuf = tryCreateBuffer(bufsize); + if (readbuf instanceof Error) + return onerror(readbuf); + + if (mode !== undefined) { + dst.fchmod(dstHandle, mode, function tryAgain(err) { + if (err) { + // Try chmod() for sftp servers that may not support fchmod() + // for whatever reason + dst.chmod(dstPath, mode, (err_) => tryAgain()); + return; + } + startReads(); + }); + } else { + startReads(); + } + + function onread(err, nb, data, dstpos, datapos, origChunkLen) { + if (err) + return onerror(err); + + datapos = datapos || 0; + + dst.write(dstHandle, readbuf, datapos, nb, dstpos, writeCb); + + function writeCb(err) { + if (err) + return onerror(err); + + total += nb; + onstep && onstep(total, nb, fsize); + + if (nb < origChunkLen) + return singleRead(datapos, dstpos + nb, origChunkLen - nb); + + if (total === fsize) { + dst.close(dstHandle, (err) => { + dstHandle = undefined; + if (err) + return onerror(err); + src.close(srcHandle, (err) => { + srcHandle = undefined; + if (err) + return onerror(err); + cb(); + }); + }); + return; + } + + if (pdst >= fsize) + return; + + const chunk = + (pdst + chunkSize > fsize ? fsize - pdst : chunkSize); + singleRead(datapos, pdst, chunk); + pdst += chunk; + } + } + + function makeCb(psrc, pdst, chunk) { + return (err, nb, data) => { + onread(err, nb, data, pdst, psrc, chunk); + }; + } + + function singleRead(psrc, pdst, chunk) { + src.read(srcHandle, + readbuf, + psrc, + chunk, + pdst, + makeCb(psrc, pdst, chunk)); + } + + function startReads() { + let reads = 0; + let psrc = 0; + while (pdst < fsize && reads < concurrency) { + const chunk = + (pdst + chunkSize > fsize ? fsize - pdst : chunkSize); + singleRead(psrc, pdst, chunk); + psrc += chunk; + pdst += chunk; + ++reads; + } + } + }); + } + }); +} + +function writeAll(sftp, handle, buffer, offset, length, position, callback_) { + const callback = (typeof callback_ === 'function' ? callback_ : undefined); + + sftp.write(handle, + buffer, + offset, + length, + position, + (writeErr, written) => { + if (writeErr) { + return sftp.close(handle, () => { + callback && callback(writeErr); + }); + } + if (written === length) { + sftp.close(handle, callback); + } else { + offset += written; + length -= written; + position += written; + writeAll(sftp, handle, buffer, offset, length, position, callback); + } + }); +} + +class Stats { + constructor(initial) { + this.mode = (initial && initial.mode); + this.uid = (initial && initial.uid); + this.gid = (initial && initial.gid); + this.size = (initial && initial.size); + this.atime = (initial && initial.atime); + this.mtime = (initial && initial.mtime); + this.extended = (initial && initial.extended); + } + isDirectory() { + return ((this.mode & constants.S_IFMT) === constants.S_IFDIR); + } + isFile() { + return ((this.mode & constants.S_IFMT) === constants.S_IFREG); + } + isBlockDevice() { + return ((this.mode & constants.S_IFMT) === constants.S_IFBLK); + } + isCharacterDevice() { + return ((this.mode & constants.S_IFMT) === constants.S_IFCHR); + } + isSymbolicLink() { + return ((this.mode & constants.S_IFMT) === constants.S_IFLNK); + } + isFIFO() { + return ((this.mode & constants.S_IFMT) === constants.S_IFIFO); + } + isSocket() { + return ((this.mode & constants.S_IFMT) === constants.S_IFSOCK); + } +} + +function attrsToBytes(attrs) { + let flags = 0; + let nb = 0; + + if (typeof attrs === 'object' && attrs !== null) { + if (typeof attrs.size === 'number') { + flags |= ATTR.SIZE; + const val = attrs.size; + // Big Endian + ATTRS_BUF[nb++] = val / 72057594037927940; // 2**56 + ATTRS_BUF[nb++] = val / 281474976710656; // 2**48 + ATTRS_BUF[nb++] = val / 1099511627776; // 2**40 + ATTRS_BUF[nb++] = val / 4294967296; // 2**32 + ATTRS_BUF[nb++] = val / 16777216; // 2**24 + ATTRS_BUF[nb++] = val / 65536; // 2**16 + ATTRS_BUF[nb++] = val / 256; // 2**8 + ATTRS_BUF[nb++] = val; + } + if (typeof attrs.uid === 'number' && typeof attrs.gid === 'number') { + flags |= ATTR.UIDGID; + const uid = attrs.uid; + const gid = attrs.gid; + // Big Endian + ATTRS_BUF[nb++] = uid >>> 24; + ATTRS_BUF[nb++] = uid >>> 16; + ATTRS_BUF[nb++] = uid >>> 8; + ATTRS_BUF[nb++] = uid; + ATTRS_BUF[nb++] = gid >>> 24; + ATTRS_BUF[nb++] = gid >>> 16; + ATTRS_BUF[nb++] = gid >>> 8; + ATTRS_BUF[nb++] = gid; + } + if (typeof attrs.mode === 'number' || typeof attrs.mode === 'string') { + const mode = modeNum(attrs.mode); + flags |= ATTR.PERMISSIONS; + // Big Endian + ATTRS_BUF[nb++] = mode >>> 24; + ATTRS_BUF[nb++] = mode >>> 16; + ATTRS_BUF[nb++] = mode >>> 8; + ATTRS_BUF[nb++] = mode; + } + if ((typeof attrs.atime === 'number' || isDate(attrs.atime)) + && (typeof attrs.mtime === 'number' || isDate(attrs.mtime))) { + const atime = toUnixTimestamp(attrs.atime); + const mtime = toUnixTimestamp(attrs.mtime); + + flags |= ATTR.ACMODTIME; + // Big Endian + ATTRS_BUF[nb++] = atime >>> 24; + ATTRS_BUF[nb++] = atime >>> 16; + ATTRS_BUF[nb++] = atime >>> 8; + ATTRS_BUF[nb++] = atime; + ATTRS_BUF[nb++] = mtime >>> 24; + ATTRS_BUF[nb++] = mtime >>> 16; + ATTRS_BUF[nb++] = mtime >>> 8; + ATTRS_BUF[nb++] = mtime; + } + // TODO: extended attributes + } + + return { flags, nb }; +} + +function toUnixTimestamp(time) { + // eslint-disable-next-line no-self-compare + if (typeof time === 'number' && time === time) // Valid, non-NaN number + return time; + if (isDate(time)) + return parseInt(time.getTime() / 1000, 10); + throw new Error(`Cannot parse time: ${time}`); +} + +function modeNum(mode) { + // eslint-disable-next-line no-self-compare + if (typeof mode === 'number' && mode === mode) // Valid, non-NaN number + return mode; + if (typeof mode === 'string') + return modeNum(parseInt(mode, 8)); + throw new Error(`Cannot parse mode: ${mode}`); +} + +const stringFlagMap = { + 'r': OPEN_MODE.READ, + 'r+': OPEN_MODE.READ | OPEN_MODE.WRITE, + 'w': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE, + 'wx': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL, + 'xw': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL, + 'w+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE, + 'wx+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE + | OPEN_MODE.EXCL, + 'xw+': OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE + | OPEN_MODE.EXCL, + 'a': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE, + 'ax': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL, + 'xa': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL, + 'a+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE, + 'ax+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE + | OPEN_MODE.EXCL, + 'xa+': OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE + | OPEN_MODE.EXCL +}; + +function stringToFlags(str) { + const flags = stringFlagMap[str]; + return (flags !== undefined ? flags : null); +} + +const flagsToString = (() => { + const stringFlagMapKeys = Object.keys(stringFlagMap); + // TODO: optimize + return (flags) => { + for (let i = 0; i < stringFlagMapKeys.length; ++i) { + const key = stringFlagMapKeys[i]; + if (stringFlagMap[key] === flags) + return key; + } + return null; + }; +})(); + +function readAttrs(biOpt) { + /* + uint32 flags + uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE + uint32 uid present only if flag SSH_FILEXFER_ATTR_UIDGID + uint32 gid present only if flag SSH_FILEXFER_ATTR_UIDGID + uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS + uint32 atime present only if flag SSH_FILEXFER_ACMODTIME + uint32 mtime present only if flag SSH_FILEXFER_ACMODTIME + uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED + string extended_type + string extended_data + ... more extended data (extended_type - extended_data pairs), + so that number of pairs equals extended_count + */ + const flags = bufferParser.readUInt32BE(); + if (flags === undefined) + return; + + const attrs = new Stats(); + if (flags & ATTR.SIZE) { + const size = bufferParser.readUInt64BE(biOpt); + if (size === undefined) + return; + attrs.size = size; + } + + if (flags & ATTR.UIDGID) { + const uid = bufferParser.readUInt32BE(); + const gid = bufferParser.readUInt32BE(); + if (gid === undefined) + return; + attrs.uid = uid; + attrs.gid = gid; + } + + if (flags & ATTR.PERMISSIONS) { + const mode = bufferParser.readUInt32BE(); + if (mode === undefined) + return; + attrs.mode = mode; + } + + if (flags & ATTR.ACMODTIME) { + const atime = bufferParser.readUInt32BE(); + const mtime = bufferParser.readUInt32BE(); + if (mtime === undefined) + return; + attrs.atime = atime; + attrs.mtime = mtime; + } + + if (flags & ATTR.EXTENDED) { + const count = bufferParser.readUInt32BE(); + if (count === undefined) + return; + const extended = {}; + for (let i = 0; i < count; ++i) { + const type = bufferParser.readString(true); + const data = bufferParser.readString(); + if (data === undefined) + return; + extended[type] = data; + } + attrs.extended = extended; + } + + return attrs; +} + +function sendOrBuffer(sftp, payload) { + const ret = tryWritePayload(sftp, payload); + if (ret !== undefined) { + sftp._buffer.push(payload); + return false; + } + return true; +} + +function tryWritePayload(sftp, payload) { + const outgoing = sftp.outgoing; + if (outgoing.state !== 'open') + return; + + if (outgoing.window === 0) { + sftp._waitWindow = true; // XXX: Unnecessary? + return payload; + } + + let ret; + const len = payload.length; + let p = 0; + + while (len - p > 0 && outgoing.window > 0) { + const actualLen = Math.min(len - p, outgoing.window, outgoing.packetSize); + outgoing.window -= actualLen; + if (outgoing.window === 0) { + sftp._waitWindow = true; + sftp._chunkcb = drainBuffer; + } + + if (p === 0 && actualLen === len) { + sftp._protocol.channelData(sftp.outgoing.id, payload); + } else { + sftp._protocol.channelData(sftp.outgoing.id, + bufferSlice(payload, p, p + actualLen)); + } + + p += actualLen; + } + + if (len - p > 0) { + if (p > 0) + ret = bufferSlice(payload, p, len); + else + ret = payload; // XXX: should never get here? + } + + return ret; +} + +function drainBuffer() { + this._chunkcb = undefined; + const buffer = this._buffer; + let i = 0; + while (i < buffer.length) { + const payload = buffer[i]; + const ret = tryWritePayload(this, payload); + if (ret !== undefined) { + if (ret !== payload) + buffer[i] = ret; + if (i > 0) + this._buffer = buffer.slice(i); + return; + } + ++i; + } + if (i > 0) + this._buffer = []; +} + +function doFatalSFTPError(sftp, msg, noDebug) { + const err = new Error(msg); + err.level = 'sftp-protocol'; + if (!noDebug && sftp._debug) + sftp._debug(`SFTP: Inbound: ${msg}`); + sftp.emit('error', err); + sftp.destroy(); + return false; +} + +const CLIENT_HANDLERS = { + [RESPONSE.VERSION]: (sftp, payload) => { + if (sftp._version !== -1) + return doFatalSFTPError(sftp, 'Duplicate VERSION packet'); + + const extensions = {}; + + /* + uint32 version + + */ + bufferParser.init(payload, 1); + let version = bufferParser.readUInt32BE(); + while (bufferParser.avail()) { + const extName = bufferParser.readString(true); + const extData = bufferParser.readString(true); + if (extData === undefined) { + version = undefined; + break; + } + extensions[extName] = extData; + } + bufferParser.clear(); + + if (version === undefined) + return doFatalSFTPError(sftp, 'Malformed VERSION packet'); + + if (sftp._debug) { + const names = Object.keys(extensions); + if (names.length) { + sftp._debug( + `SFTP: Inbound: Received VERSION (v${version}, exts:${names})` + ); + } else { + sftp._debug(`SFTP: Inbound: Received VERSION (v${version})`); + } + } + + sftp._version = version; + sftp._extensions = extensions; + sftp.emit('ready'); + }, + [RESPONSE.STATUS]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + uint32 error/status code + string error message (ISO-10646 UTF-8) + string language tag + */ + const errorCode = bufferParser.readUInt32BE(); + const errorMsg = bufferParser.readString(true); + const lang = bufferParser.skipString(); + bufferParser.clear(); + + if (lang === undefined) { + if (reqID !== undefined) + delete sftp._requests[reqID]; + return doFatalSFTPError(sftp, 'Malformed STATUS packet'); + } + + if (sftp._debug) { + const jsonMsg = JSON.stringify(errorMsg); + sftp._debug( + `SFTP: Inbound: Received STATUS (id:${reqID}, ${errorCode}, ${jsonMsg})` + ); + } + const req = sftp._requests[reqID]; + delete sftp._requests[reqID]; + if (req) { + if (errorCode === STATUS_CODE.OK) { + req.cb(); + return; + } + const err = new Error(errorMsg + || STATUS_CODE_STR[errorCode] + || 'Unknown status'); + err.code = errorCode; + req.cb(err); + } + }, + [RESPONSE.HANDLE]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string handle + */ + const handle = bufferParser.readString(); + bufferParser.clear(); + + if (handle === undefined) { + if (reqID !== undefined) + delete sftp._requests[reqID]; + return doFatalSFTPError(sftp, 'Malformed HANDLE packet'); + } + + sftp._debug && sftp._debug(`SFTP: Inbound: Received HANDLE (id:${reqID})`); + + const req = sftp._requests[reqID]; + delete sftp._requests[reqID]; + req && req.cb(undefined, handle); + }, + [RESPONSE.DATA]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + let req; + if (reqID !== undefined) { + req = sftp._requests[reqID]; + delete sftp._requests[reqID]; + } + /* + string data + */ + if (req) { + if (req.buffer) { + // We have already pre-allocated space to store the data + + const nb = bufferParser.readString(req.buffer); + bufferParser.clear(); + + if (nb !== undefined) { + sftp._debug && sftp._debug( + `SFTP: Inbound: Received DATA (id:${reqID}, ${nb})` + ); + req.cb(undefined, req.buffer, nb); + return; + } + } else { + const data = bufferParser.readString(); + bufferParser.clear(); + + if (data !== undefined) { + sftp._debug && sftp._debug( + `SFTP: Inbound: Received DATA (id:${reqID}, ${data.length})` + ); + req.cb(undefined, data); + return; + } + } + } else { + const nb = bufferParser.skipString(); + bufferParser.clear(); + if (nb !== undefined) { + sftp._debug && sftp._debug( + `SFTP: Inbound: Received DATA (id:${reqID}, ${nb})` + ); + return; + } + } + + return doFatalSFTPError(sftp, 'Malformed DATA packet'); + }, + [RESPONSE.NAME]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + let req; + if (reqID !== undefined) { + req = sftp._requests[reqID]; + delete sftp._requests[reqID]; + } + /* + uint32 count + repeats count times: + string filename + string longname + ATTRS attrs + */ + const count = bufferParser.readUInt32BE(); + if (count !== undefined) { + let names = []; + for (let i = 0; i < count; ++i) { + // We are going to assume UTF-8 for filenames despite the SFTPv3 + // spec not specifying an encoding because the specs for newer + // versions of the protocol all explicitly specify UTF-8 for + // filenames + const filename = bufferParser.readString(true); + + // `longname` only exists in SFTPv3 and since it typically will + // contain the filename, we assume it is also UTF-8 + const longname = bufferParser.readString(true); + + const attrs = readAttrs(sftp._biOpt); + if (attrs === undefined) { + names = undefined; + break; + } + names.push({ filename, longname, attrs }); + } + if (names !== undefined) { + sftp._debug && sftp._debug( + `SFTP: Inbound: Received NAME (id:${reqID}, ${names.length})` + ); + bufferParser.clear(); + req && req.cb(undefined, names); + return; + } + } + + bufferParser.clear(); + return doFatalSFTPError(sftp, 'Malformed NAME packet'); + }, + [RESPONSE.ATTRS]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + let req; + if (reqID !== undefined) { + req = sftp._requests[reqID]; + delete sftp._requests[reqID]; + } + /* + ATTRS attrs + */ + const attrs = readAttrs(sftp._biOpt); + bufferParser.clear(); + if (attrs !== undefined) { + sftp._debug && sftp._debug(`SFTP: Inbound: Received ATTRS (id:${reqID})`); + req && req.cb(undefined, attrs); + return; + } + + return doFatalSFTPError(sftp, 'Malformed ATTRS packet'); + }, + [RESPONSE.EXTENDED]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + if (reqID !== undefined) { + const req = sftp._requests[reqID]; + if (req) { + delete sftp._requests[reqID]; + switch (req.extended) { + case 'statvfs@openssh.com': + case 'fstatvfs@openssh.com': { + /* + uint64 f_bsize // file system block size + uint64 f_frsize // fundamental fs block size + uint64 f_blocks // number of blocks (unit f_frsize) + uint64 f_bfree // free blocks in file system + uint64 f_bavail // free blocks for non-root + uint64 f_files // total file inodes + uint64 f_ffree // free file inodes + uint64 f_favail // free file inodes for to non-root + uint64 f_fsid // file system id + uint64 f_flag // bit mask of f_flag values + uint64 f_namemax // maximum filename length + */ + const biOpt = sftp._biOpt; + const stats = { + f_bsize: bufferParser.readUInt64BE(biOpt), + f_frsize: bufferParser.readUInt64BE(biOpt), + f_blocks: bufferParser.readUInt64BE(biOpt), + f_bfree: bufferParser.readUInt64BE(biOpt), + f_bavail: bufferParser.readUInt64BE(biOpt), + f_files: bufferParser.readUInt64BE(biOpt), + f_ffree: bufferParser.readUInt64BE(biOpt), + f_favail: bufferParser.readUInt64BE(biOpt), + f_sid: bufferParser.readUInt64BE(biOpt), + f_flag: bufferParser.readUInt64BE(biOpt), + f_namemax: bufferParser.readUInt64BE(biOpt), + }; + if (stats.f_namemax === undefined) + break; + if (sftp._debug) { + sftp._debug( + 'SFTP: Inbound: Received EXTENDED_REPLY ' + + `(id:${reqID}, ${req.extended})` + ); + } + bufferParser.clear(); + req.cb(undefined, stats); + return; + } + default: + // Unknown extended request + sftp._debug && sftp._debug( + `SFTP: Inbound: Received EXTENDED_REPLY (id:${reqID}, ???)` + ); + bufferParser.clear(); + req.cb(); + return; + } + } else { + sftp._debug && sftp._debug( + `SFTP: Inbound: Received EXTENDED_REPLY (id:${reqID}, ???)` + ); + bufferParser.clear(); + return; + } + } + + bufferParser.clear(); + return doFatalSFTPError(sftp, 'Malformed EXTENDED_REPLY packet'); + }, +}; +const SERVER_HANDLERS = { + [REQUEST.INIT]: (sftp, payload) => { + if (sftp._version !== -1) + return doFatalSFTPError(sftp, 'Duplicate INIT packet'); + + const extensions = {}; + + /* + uint32 version + + */ + bufferParser.init(payload, 1); + let version = bufferParser.readUInt32BE(); + while (bufferParser.avail()) { + const extName = bufferParser.readString(true); + const extData = bufferParser.readString(true); + if (extData === undefined) { + version = undefined; + break; + } + extensions[extName] = extData; + } + bufferParser.clear(); + + if (version === undefined) + return doFatalSFTPError(sftp, 'Malformed INIT packet'); + + if (sftp._debug) { + const names = Object.keys(extensions); + if (names.length) { + sftp._debug( + `SFTP: Inbound: Received INIT (v${version}, exts:${names})` + ); + } else { + sftp._debug(`SFTP: Inbound: Received INIT (v${version})`); + } + } + + sendOrBuffer(sftp, SERVER_VERSION_BUFFER); + + sftp._version = version; + sftp._extensions = extensions; + sftp.emit('ready'); + }, + [REQUEST.OPEN]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string filename + uint32 pflags + ATTRS attrs + */ + const filename = bufferParser.readString(true); + const pflags = bufferParser.readUInt32BE(); + const attrs = readAttrs(sftp._biOpt); + bufferParser.clear(); + + if (attrs === undefined) + return doFatalSFTPError(sftp, 'Malformed OPEN packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received OPEN (id:${reqID})`); + + if (!sftp.emit('OPEN', reqID, filename, pflags, attrs)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.CLOSE]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string handle + */ + const handle = bufferParser.readString(); + bufferParser.clear(); + + if (handle === undefined || handle.length > 256) + return doFatalSFTPError(sftp, 'Malformed CLOSE packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received CLOSE (id:${reqID})`); + + if (!sftp.emit('CLOSE', reqID, handle)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.READ]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string handle + uint64 offset + uint32 len + */ + const handle = bufferParser.readString(); + const offset = bufferParser.readUInt64BE(sftp._biOpt); + const len = bufferParser.readUInt32BE(); + bufferParser.clear(); + + if (len === undefined || handle.length > 256) + return doFatalSFTPError(sftp, 'Malformed READ packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received READ (id:${reqID})`); + + if (!sftp.emit('READ', reqID, handle, offset, len)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.WRITE]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string handle + uint64 offset + string data + */ + const handle = bufferParser.readString(); + const offset = bufferParser.readUInt64BE(sftp._biOpt); + const data = bufferParser.readString(); + bufferParser.clear(); + + if (data === undefined || handle.length > 256) + return doFatalSFTPError(sftp, 'Malformed WRITE packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received WRITE (id:${reqID})`); + + if (!sftp.emit('WRITE', reqID, handle, offset, data)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.LSTAT]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + */ + const path = bufferParser.readString(true); + bufferParser.clear(); + + if (path === undefined) + return doFatalSFTPError(sftp, 'Malformed LSTAT packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received LSTAT (id:${reqID})`); + + if (!sftp.emit('LSTAT', reqID, path)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.FSTAT]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string handle + */ + const handle = bufferParser.readString(); + bufferParser.clear(); + + if (handle === undefined || handle.length > 256) + return doFatalSFTPError(sftp, 'Malformed FSTAT packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received FSTAT (id:${reqID})`); + + if (!sftp.emit('FSTAT', reqID, handle)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.SETSTAT]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + ATTRS attrs + */ + const path = bufferParser.readString(true); + const attrs = readAttrs(sftp._biOpt); + bufferParser.clear(); + + if (attrs === undefined) + return doFatalSFTPError(sftp, 'Malformed SETSTAT packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received SETSTAT (id:${reqID})`); + + if (!sftp.emit('SETSTAT', reqID, path, attrs)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.FSETSTAT]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string handle + ATTRS attrs + */ + const handle = bufferParser.readString(); + const attrs = readAttrs(sftp._biOpt); + bufferParser.clear(); + + if (attrs === undefined || handle.length > 256) + return doFatalSFTPError(sftp, 'Malformed FSETSTAT packet'); + + sftp._debug && sftp._debug( + `SFTP: Inbound: Received FSETSTAT (id:${reqID})` + ); + + if (!sftp.emit('FSETSTAT', reqID, handle, attrs)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.OPENDIR]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + */ + const path = bufferParser.readString(true); + bufferParser.clear(); + + if (path === undefined) + return doFatalSFTPError(sftp, 'Malformed OPENDIR packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received OPENDIR (id:${reqID})`); + + if (!sftp.emit('OPENDIR', reqID, path)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.READDIR]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string handle + */ + const handle = bufferParser.readString(); + bufferParser.clear(); + + if (handle === undefined || handle.length > 256) + return doFatalSFTPError(sftp, 'Malformed READDIR packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received READDIR (id:${reqID})`); + + if (!sftp.emit('READDIR', reqID, handle)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.REMOVE]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + */ + const path = bufferParser.readString(true); + bufferParser.clear(); + + if (path === undefined) + return doFatalSFTPError(sftp, 'Malformed REMOVE packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received REMOVE (id:${reqID})`); + + if (!sftp.emit('REMOVE', reqID, path)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.MKDIR]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + ATTRS attrs + */ + const path = bufferParser.readString(true); + const attrs = readAttrs(sftp._biOpt); + bufferParser.clear(); + + if (attrs === undefined) + return doFatalSFTPError(sftp, 'Malformed MKDIR packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received MKDIR (id:${reqID})`); + + if (!sftp.emit('MKDIR', reqID, path, attrs)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.RMDIR]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + */ + const path = bufferParser.readString(true); + bufferParser.clear(); + + if (path === undefined) + return doFatalSFTPError(sftp, 'Malformed RMDIR packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received RMDIR (id:${reqID})`); + + if (!sftp.emit('RMDIR', reqID, path)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.REALPATH]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + */ + const path = bufferParser.readString(true); + bufferParser.clear(); + + if (path === undefined) + return doFatalSFTPError(sftp, 'Malformed REALPATH packet'); + + sftp._debug && sftp._debug( + `SFTP: Inbound: Received REALPATH (id:${reqID})` + ); + + if (!sftp.emit('REALPATH', reqID, path)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.STAT]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + */ + const path = bufferParser.readString(true); + bufferParser.clear(); + + if (path === undefined) + return doFatalSFTPError(sftp, 'Malformed STAT packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received STAT (id:${reqID})`); + + if (!sftp.emit('STAT', reqID, path)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.RENAME]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string oldpath + string newpath + */ + const oldPath = bufferParser.readString(true); + const newPath = bufferParser.readString(true); + bufferParser.clear(); + + if (newPath === undefined) + return doFatalSFTPError(sftp, 'Malformed RENAME packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received RENAME (id:${reqID})`); + + if (!sftp.emit('RENAME', reqID, oldPath, newPath)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.READLINK]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string path + */ + const path = bufferParser.readString(true); + bufferParser.clear(); + + if (path === undefined) + return doFatalSFTPError(sftp, 'Malformed READLINK packet'); + + sftp._debug && sftp._debug( + `SFTP: Inbound: Received READLINK (id:${reqID})` + ); + + if (!sftp.emit('READLINK', reqID, path)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.SYMLINK]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string linkpath + string targetpath + */ + const linkPath = bufferParser.readString(true); + const targetPath = bufferParser.readString(true); + bufferParser.clear(); + + if (targetPath === undefined) + return doFatalSFTPError(sftp, 'Malformed SYMLINK packet'); + + sftp._debug && sftp._debug(`SFTP: Inbound: Received SYMLINK (id:${reqID})`); + + let handled; + if (sftp._isOpenSSH) { + // OpenSSH has linkpath and targetpath positions switched + handled = sftp.emit('SYMLINK', reqID, targetPath, linkPath); + } else { + handled = sftp.emit('SYMLINK', reqID, linkPath, targetPath); + } + if (!handled) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, + [REQUEST.EXTENDED]: (sftp, payload) => { + bufferParser.init(payload, 1); + const reqID = bufferParser.readUInt32BE(); + /* + string extended-request + ... any request-specific data ... + */ + const extName = bufferParser.readString(true); + if (extName === undefined) { + bufferParser.clear(); + return doFatalSFTPError(sftp, 'Malformed EXTENDED packet'); + } + + let extData; + if (bufferParser.avail()) + extData = bufferParser.readRaw(); + bufferParser.clear(); + + sftp._debug && sftp._debug( + `SFTP: Inbound: Received EXTENDED (id:${reqID})` + ); + + if (!sftp.emit('EXTENDED', reqID, extName, extData)) { + // Automatically reject request if no handler for request type + sftp.status(reqID, STATUS_CODE.OP_UNSUPPORTED); + } + }, +}; + +// ============================================================================= +// ReadStream/WriteStream-related ============================================== +// ============================================================================= +const { + ERR_INVALID_ARG_TYPE, + ERR_OUT_OF_RANGE, + validateNumber +} = require('./node-fs-compat'); + +const kMinPoolSpace = 128; + +let pool; +// It can happen that we expect to read a large chunk of data, and reserve +// a large chunk of the pool accordingly, but the read() call only filled +// a portion of it. If a concurrently executing read() then uses the same pool, +// the "reserved" portion cannot be used, so we allow it to be re-used as a +// new pool later. +const poolFragments = []; + +function allocNewPool(poolSize) { + if (poolFragments.length > 0) + pool = poolFragments.pop(); + else + pool = Buffer.allocUnsafe(poolSize); + pool.used = 0; +} + +// Check the `this.start` and `this.end` of stream. +function checkPosition(pos, name) { + if (!Number.isSafeInteger(pos)) { + validateNumber(pos, name); + if (!Number.isInteger(pos)) + throw new ERR_OUT_OF_RANGE(name, 'an integer', pos); + throw new ERR_OUT_OF_RANGE(name, '>= 0 and <= 2 ** 53 - 1', pos); + } + if (pos < 0) + throw new ERR_OUT_OF_RANGE(name, '>= 0 and <= 2 ** 53 - 1', pos); +} + +function roundUpToMultipleOf8(n) { + return (n + 7) & ~7; // Align to 8 byte boundary. +} + +function ReadStream(sftp, path, options) { + if (options === undefined) + options = {}; + else if (typeof options === 'string') + options = { encoding: options }; + else if (options === null || typeof options !== 'object') + throw new TypeError('"options" argument must be a string or an object'); + else + options = Object.create(options); + + // A little bit bigger buffer and water marks by default + if (options.highWaterMark === undefined) + options.highWaterMark = 64 * 1024; + + // For backwards compat do not emit close on destroy. + options.emitClose = false; + + ReadableStream.call(this, options); + + this.path = path; + this.flags = options.flags === undefined ? 'r' : options.flags; + this.mode = options.mode === undefined ? 0o666 : options.mode; + + this.start = options.start; + this.end = options.end; + this.autoClose = options.autoClose === undefined ? true : options.autoClose; + this.pos = 0; + this.bytesRead = 0; + this.closed = false; + + this.handle = options.handle === undefined ? null : options.handle; + this.sftp = sftp; + this._opening = false; + + if (this.start !== undefined) { + checkPosition(this.start, 'start'); + + this.pos = this.start; + } + + if (this.end === undefined) { + this.end = Infinity; + } else if (this.end !== Infinity) { + checkPosition(this.end, 'end'); + + if (this.start !== undefined && this.start > this.end) { + throw new ERR_OUT_OF_RANGE( + 'start', + `<= "end" (here: ${this.end})`, + this.start + ); + } + } + + this.on('end', function() { + if (this.autoClose) + this.destroy(); + }); + + if (!Buffer.isBuffer(this.handle)) + this.open(); +} +inherits(ReadStream, ReadableStream); + +ReadStream.prototype.open = function() { + if (this._opening) + return; + + this._opening = true; + + this.sftp.open(this.path, this.flags, this.mode, (er, handle) => { + this._opening = false; + + if (er) { + this.emit('error', er); + if (this.autoClose) + this.destroy(); + return; + } + + this.handle = handle; + this.emit('open', handle); + this.emit('ready'); + // Start the flow of data. + this.read(); + }); +}; + +ReadStream.prototype._read = function(n) { + if (!Buffer.isBuffer(this.handle)) + return this.once('open', () => this._read(n)); + + // XXX: safe to remove this? + if (this.destroyed) + return; + + if (!pool || pool.length - pool.used < kMinPoolSpace) { + // Discard the old pool. + allocNewPool(this.readableHighWaterMark + || this._readableState.highWaterMark); + } + + // Grab another reference to the pool in the case that while we're + // in the thread pool another read() finishes up the pool, and + // allocates a new one. + const thisPool = pool; + let toRead = Math.min(pool.length - pool.used, n); + const start = pool.used; + + if (this.end !== undefined) + toRead = Math.min(this.end - this.pos + 1, toRead); + + // Already read everything we were supposed to read! + // treat as EOF. + if (toRead <= 0) + return this.push(null); + + // the actual read. + this.sftp.read(this.handle, + pool, + pool.used, + toRead, + this.pos, + (er, bytesRead) => { + if (er) { + this.emit('error', er); + if (this.autoClose) + this.destroy(); + return; + } + let b = null; + + // Now that we know how much data we have actually read, re-wind the + // 'used' field if we can, and otherwise allow the remainder of our + // reservation to be used as a new pool later. + if (start + toRead === thisPool.used && thisPool === pool) { + thisPool.used = roundUpToMultipleOf8(thisPool.used + bytesRead - toRead); + } else { + // Round down to the next lowest multiple of 8 to ensure the new pool + // fragment start and end positions are aligned to an 8 byte boundary. + const alignedEnd = (start + toRead) & ~7; + const alignedStart = roundUpToMultipleOf8(start + bytesRead); + if (alignedEnd - alignedStart >= kMinPoolSpace) + poolFragments.push(thisPool.slice(alignedStart, alignedEnd)); + } + + if (bytesRead > 0) { + this.bytesRead += bytesRead; + b = thisPool.slice(start, start + bytesRead); + } + + // Move the pool positions, and internal position for reading. + this.pos += bytesRead; + + this.push(b); + }); + + pool.used = roundUpToMultipleOf8(pool.used + toRead); +}; + +ReadStream.prototype._destroy = function(err, cb) { + if (this._opening && !Buffer.isBuffer(this.handle)) { + this.once('open', closeStream.bind(null, this, cb, err)); + return; + } + + closeStream(this, cb, err); + this.handle = null; + this._opening = false; +}; + +function closeStream(stream, cb, err) { + if (!stream.handle) + return onclose(); + + stream.sftp.close(stream.handle, onclose); + + function onclose(er) { + er = er || err; + cb(er); + stream.closed = true; + if (!er) + stream.emit('close'); + } +} + +ReadStream.prototype.close = function(cb) { + this.destroy(null, cb); +}; + +Object.defineProperty(ReadStream.prototype, 'pending', { + get() { + return this.handle === null; + }, + configurable: true +}); + +// TODO: add `concurrency` setting to allow more than one in-flight WRITE +// request to server to improve throughput +function WriteStream(sftp, path, options) { + if (options === undefined) + options = {}; + else if (typeof options === 'string') + options = { encoding: options }; + else if (options === null || typeof options !== 'object') + throw new TypeError('"options" argument must be a string or an object'); + else + options = Object.create(options); + + // For backwards compat do not emit close on destroy. + options.emitClose = false; + + WritableStream.call(this, options); + + this.path = path; + this.flags = options.flags === undefined ? 'w' : options.flags; + this.mode = options.mode === undefined ? 0o666 : options.mode; + + this.start = options.start; + this.autoClose = options.autoClose === undefined ? true : options.autoClose; + this.pos = 0; + this.bytesWritten = 0; + this.closed = false; + + this.handle = options.handle === undefined ? null : options.handle; + this.sftp = sftp; + this._opening = false; + + if (this.start !== undefined) { + checkPosition(this.start, 'start'); + + this.pos = this.start; + } + + if (options.encoding) + this.setDefaultEncoding(options.encoding); + + // Node v6.x only + this.on('finish', function() { + if (this._writableState.finalCalled) + return; + if (this.autoClose) + this.destroy(); + }); + + if (!Buffer.isBuffer(this.handle)) + this.open(); +} +inherits(WriteStream, WritableStream); + +WriteStream.prototype._final = function(cb) { + if (this.autoClose) + this.destroy(); + cb(); +}; + +WriteStream.prototype.open = function() { + if (this._opening) + return; + + this._opening = true; + + this.sftp.open(this.path, this.flags, this.mode, (er, handle) => { + this._opening = false; + + if (er) { + this.emit('error', er); + if (this.autoClose) + this.destroy(); + return; + } + + this.handle = handle; + + const tryAgain = (err) => { + if (err) { + // Try chmod() for sftp servers that may not support fchmod() for + // whatever reason + this.sftp.chmod(this.path, this.mode, (err_) => tryAgain()); + return; + } + + // SFTPv3 requires absolute offsets, no matter the open flag used + if (this.flags[0] === 'a') { + const tryStat = (err, st) => { + if (err) { + // Try stat() for sftp servers that may not support fstat() for + // whatever reason + this.sftp.stat(this.path, (err_, st_) => { + if (err_) { + this.destroy(); + this.emit('error', err); + return; + } + tryStat(null, st_); + }); + return; + } + + this.pos = st.size; + this.emit('open', handle); + this.emit('ready'); + }; + + this.sftp.fstat(handle, tryStat); + return; + } + + this.emit('open', handle); + this.emit('ready'); + }; + + this.sftp.fchmod(handle, this.mode, tryAgain); + }); +}; + +WriteStream.prototype._write = function(data, encoding, cb) { + if (!Buffer.isBuffer(data)) { + const err = new ERR_INVALID_ARG_TYPE('data', 'Buffer', data); + return this.emit('error', err); + } + + if (!Buffer.isBuffer(this.handle)) { + return this.once('open', function() { + this._write(data, encoding, cb); + }); + } + + this.sftp.write(this.handle, + data, + 0, + data.length, + this.pos, + (er, bytes) => { + if (er) { + if (this.autoClose) + this.destroy(); + return cb(er); + } + this.bytesWritten += bytes; + cb(); + }); + + this.pos += data.length; +}; + +WriteStream.prototype._writev = function(data, cb) { + if (!Buffer.isBuffer(this.handle)) { + return this.once('open', function() { + this._writev(data, cb); + }); + } + + const sftp = this.sftp; + const handle = this.handle; + let writesLeft = data.length; + + const onwrite = (er, bytes) => { + if (er) { + this.destroy(); + return cb(er); + } + this.bytesWritten += bytes; + if (--writesLeft === 0) + cb(); + }; + + // TODO: try to combine chunks to reduce number of requests to the server? + for (let i = 0; i < data.length; ++i) { + const chunk = data[i].chunk; + + sftp.write(handle, chunk, 0, chunk.length, this.pos, onwrite); + this.pos += chunk.length; + } +}; + +if (typeof WritableStream.prototype.destroy !== 'function') + WriteStream.prototype.destroy = ReadStream.prototype.destroy; + +WriteStream.prototype._destroy = ReadStream.prototype._destroy; +WriteStream.prototype.close = function(cb) { + if (cb) { + if (this.closed) { + process.nextTick(cb); + return; + } + this.on('close', cb); + } + + // If we are not autoClosing, we should call + // destroy on 'finish'. + if (!this.autoClose) + this.on('finish', this.destroy.bind(this)); + + this.end(); +}; + +// There is no shutdown() for files. +WriteStream.prototype.destroySoon = WriteStream.prototype.end; + +Object.defineProperty(WriteStream.prototype, 'pending', { + get() { + return this.handle === null; + }, + configurable: true +}); +// ============================================================================= + +module.exports = { + flagsToString, + OPEN_MODE, + SFTP, + Stats, + STATUS_CODE, + stringToFlags, +}; diff --git a/lib/protocol/constants.js b/lib/protocol/constants.js new file mode 100644 index 00000000..24a552d1 --- /dev/null +++ b/lib/protocol/constants.js @@ -0,0 +1,345 @@ +// TODO: support server host key format: rsa-sha2-256/512 +'use strict'; + +const crypto = require('crypto'); + +let cpuInfo; +try { + cpuInfo = require('cpu-features')(); +} catch {} + +const { bindingAvailable } = require('./crypto.js'); + +const eddsaSupported = (() => { + if (typeof crypto.sign === 'function' + && typeof crypto.verify === 'function') { + const key = + '-----BEGIN PRIVATE KEY-----\r\nMC4CAQAwBQYDK2VwBCIEIHKj+sVa9WcD' + + '/q2DJUJaf43Kptc8xYuUQA4bOFj9vC8T\r\n-----END PRIVATE KEY-----'; + const data = Buffer.from('a'); + let sig; + let verified; + try { + sig = crypto.sign(null, data, key); + verified = crypto.verify(null, data, key, sig); + } catch {} + return (Buffer.isBuffer(sig) && sig.length === 64 && verified === true); + } + + return false; +})(); + +const curve25519Supported = (typeof crypto.diffieHellman === 'function' + && typeof crypto.generateKeyPairSync === 'function' + && typeof crypto.createPublicKey === 'function'); + +const DEFAULT_KEX = [ + // https://tools.ietf.org/html/rfc5656#section-10.1 + 'ecdh-sha2-nistp256', + 'ecdh-sha2-nistp384', + 'ecdh-sha2-nistp521', + + // https://tools.ietf.org/html/rfc4419#section-4 + 'diffie-hellman-group-exchange-sha256', + + // https://tools.ietf.org/html/rfc8268 + 'diffie-hellman-group14-sha256', + 'diffie-hellman-group15-sha512', + 'diffie-hellman-group16-sha512', + 'diffie-hellman-group17-sha512', + 'diffie-hellman-group18-sha512', +]; +if (curve25519Supported) { + DEFAULT_KEX.unshift('curve25519-sha256'); + DEFAULT_KEX.unshift('curve25519-sha256@libssh.org'); +} +const SUPPORTED_KEX = DEFAULT_KEX.concat([ + // https://tools.ietf.org/html/rfc4419#section-4 + 'diffie-hellman-group-exchange-sha1', + + 'diffie-hellman-group14-sha1', // REQUIRED + 'diffie-hellman-group1-sha1', // REQUIRED +]); + + +const DEFAULT_SERVER_HOST_KEY = [ + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + 'rsa-sha2-512', // RFC 8332 + 'rsa-sha2-256', // RFC 8332 + 'ssh-rsa', +]; +if (eddsaSupported) + DEFAULT_SERVER_HOST_KEY.unshift('ssh-ed25519'); +const SUPPORTED_SERVER_HOST_KEY = DEFAULT_SERVER_HOST_KEY.concat([ + 'ssh-dss', +]); + + +const DEFAULT_CIPHER = [ + // http://tools.ietf.org/html/rfc5647 + 'aes128-gcm', + 'aes128-gcm@openssh.com', + 'aes256-gcm', + 'aes256-gcm@openssh.com', + + // http://tools.ietf.org/html/rfc4344#section-4 + 'aes128-ctr', + 'aes192-ctr', + 'aes256-ctr', +]; +if (cpuInfo && cpuInfo.flags && !cpuInfo.flags.aes) { + // We know for sure the CPU does not support AES acceleration + if (bindingAvailable) + DEFAULT_CIPHER.unshift('chacha20-poly1305@openssh.com'); + else + DEFAULT_CIPHER.push('chacha20-poly1305@openssh.com'); +} else if (bindingAvailable && cpuInfo && cpuInfo.arch === 'x86') { + // Places chacha20-poly1305 immediately after GCM ciphers since GCM ciphers + // seem to outperform it on x86, but it seems to be faster than CTR ciphers + DEFAULT_CIPHER.splice(4, 0, 'chacha20-poly1305@openssh.com'); +} else { + DEFAULT_CIPHER.push('chacha20-poly1305@openssh.com'); +} +const SUPPORTED_CIPHER = DEFAULT_CIPHER.concat([ + 'aes256-cbc', + 'aes192-cbc', + 'aes128-cbc', + 'blowfish-cbc', + '3des-cbc', + + // http://tools.ietf.org/html/rfc4345#section-4: + 'arcfour256', + 'arcfour128', + + 'cast128-cbc', + 'arcfour', +]); + + +const DEFAULT_MAC = [ + 'hmac-sha2-256-etm@openssh.com', + 'hmac-sha2-512-etm@openssh.com', + 'hmac-sha1-etm@openssh.com', + 'hmac-sha2-256', + 'hmac-sha2-512', + 'hmac-sha1', +]; +const SUPPORTED_MAC = DEFAULT_MAC.concat([ + 'hmac-md5', + 'hmac-sha2-256-96', // first 96 bits of HMAC-SHA256 + 'hmac-sha2-512-96', // first 96 bits of HMAC-SHA512 + 'hmac-ripemd160', + 'hmac-sha1-96', // first 96 bits of HMAC-SHA1 + 'hmac-md5-96', // first 96 bits of HMAC-MD5 +]); + +const DEFAULT_COMPRESSION = [ + 'none', + 'zlib@openssh.com', // ZLIB (LZ77) compression, except + // compression/decompression does not start until after + // successful user authentication + 'zlib', // ZLIB (LZ77) compression +]; +const SUPPORTED_COMPRESSION = DEFAULT_COMPRESSION.concat([ +]); + + +const COMPAT = { + BAD_DHGEX: 1 << 0, + OLD_EXIT: 1 << 1, + DYN_RPORT_BUG: 1 << 2, + BUG_DHGEX_LARGE: 1 << 3, +}; + +module.exports = { + MESSAGE: { + // Transport layer protocol -- generic (1-19) + DISCONNECT: 1, + IGNORE: 2, + UNIMPLEMENTED: 3, + DEBUG: 4, + SERVICE_REQUEST: 5, + SERVICE_ACCEPT: 6, + + // Transport layer protocol -- algorithm negotiation (20-29) + KEXINIT: 20, + NEWKEYS: 21, + + // Transport layer protocol -- key exchange method-specific (30-49) + KEXDH_INIT: 30, + KEXDH_REPLY: 31, + + KEXDH_GEX_GROUP: 31, + KEXDH_GEX_INIT: 32, + KEXDH_GEX_REPLY: 33, + KEXDH_GEX_REQUEST: 34, + + KEXECDH_INIT: 30, + KEXECDH_REPLY: 31, + + // User auth protocol -- generic (50-59) + USERAUTH_REQUEST: 50, + USERAUTH_FAILURE: 51, + USERAUTH_SUCCESS: 52, + USERAUTH_BANNER: 53, + + // User auth protocol -- user auth method-specific (60-79) + USERAUTH_PASSWD_CHANGEREQ: 60, + + USERAUTH_PK_OK: 60, + + USERAUTH_INFO_REQUEST: 60, + USERAUTH_INFO_RESPONSE: 61, + + // Connection protocol -- generic (80-89) + GLOBAL_REQUEST: 80, + REQUEST_SUCCESS: 81, + REQUEST_FAILURE: 82, + + // Connection protocol -- channel-related (90-127) + CHANNEL_OPEN: 90, + CHANNEL_OPEN_CONFIRMATION: 91, + CHANNEL_OPEN_FAILURE: 92, + CHANNEL_WINDOW_ADJUST: 93, + CHANNEL_DATA: 94, + CHANNEL_EXTENDED_DATA: 95, + CHANNEL_EOF: 96, + CHANNEL_CLOSE: 97, + CHANNEL_REQUEST: 98, + CHANNEL_SUCCESS: 99, + CHANNEL_FAILURE: 100 + + // Reserved for client protocols (128-191) + + // Local extensions (192-155) + }, + DISCONNECT_REASON: { + HOST_NOT_ALLOWED_TO_CONNECT: 1, + PROTOCOL_ERROR: 2, + KEY_EXCHANGE_FAILED: 3, + RESERVED: 4, + MAC_ERROR: 5, + COMPRESSION_ERROR: 6, + SERVICE_NOT_AVAILABLE: 7, + PROTOCOL_VERSION_NOT_SUPPORTED: 8, + HOST_KEY_NOT_VERIFIABLE: 9, + CONNECTION_LOST: 10, + BY_APPLICATION: 11, + TOO_MANY_CONNECTIONS: 12, + AUTH_CANCELED_BY_USER: 13, + NO_MORE_AUTH_METHODS_AVAILABLE: 14, + ILLEGAL_USER_NAME: 15, + }, + DISCONNECT_REASON_STR: undefined, + CHANNEL_OPEN_FAILURE: { + ADMINISTRATIVELY_PROHIBITED: 1, + CONNECT_FAILED: 2, + UNKNOWN_CHANNEL_TYPE: 3, + RESOURCE_SHORTAGE: 4 + }, + TERMINAL_MODE: { + TTY_OP_END: 0, // Indicates end of options. + VINTR: 1, // Interrupt character; 255 if none. Similarly for the + // other characters. Not all of these characters are + // supported on all systems. + VQUIT: 2, // The quit character (sends SIGQUIT signal on POSIX + // systems). + VERASE: 3, // Erase the character to left of the cursor. + VKILL: 4, // Kill the current input line. + VEOF: 5, // End-of-file character (sends EOF from the + // terminal). + VEOL: 6, // End-of-line character in addition to carriage + // return and/or linefeed. + VEOL2: 7, // Additional end-of-line character. + VSTART: 8, // Continues paused output (normally control-Q). + VSTOP: 9, // Pauses output (normally control-S). + VSUSP: 10, // Suspends the current program. + VDSUSP: 11, // Another suspend character. + VREPRINT: 12, // Reprints the current input line. + VWERASE: 13, // Erases a word left of cursor. + VLNEXT: 14, // Enter the next character typed literally, even if + // it is a special character + VFLUSH: 15, // Character to flush output. + VSWTCH: 16, // Switch to a different shell layer. + VSTATUS: 17, // Prints system status line (load, command, pid, + // etc). + VDISCARD: 18, // Toggles the flushing of terminal output. + IGNPAR: 30, // The ignore parity flag. The parameter SHOULD be 0 + // if this flag is FALSE, and 1 if it is TRUE. + PARMRK: 31, // Mark parity and framing errors. + INPCK: 32, // Enable checking of parity errors. + ISTRIP: 33, // Strip 8th bit off characters. + INLCR: 34, // Map NL into CR on input. + IGNCR: 35, // Ignore CR on input. + ICRNL: 36, // Map CR to NL on input. + IUCLC: 37, // Translate uppercase characters to lowercase. + IXON: 38, // Enable output flow control. + IXANY: 39, // Any char will restart after stop. + IXOFF: 40, // Enable input flow control. + IMAXBEL: 41, // Ring bell on input queue full. + ISIG: 50, // Enable signals INTR, QUIT, [D]SUSP. + ICANON: 51, // Canonicalize input lines. + XCASE: 52, // Enable input and output of uppercase characters by + // preceding their lowercase equivalents with "\". + ECHO: 53, // Enable echoing. + ECHOE: 54, // Visually erase chars. + ECHOK: 55, // Kill character discards current line. + ECHONL: 56, // Echo NL even if ECHO is off. + NOFLSH: 57, // Don't flush after interrupt. + TOSTOP: 58, // Stop background jobs from output. + IEXTEN: 59, // Enable extensions. + ECHOCTL: 60, // Echo control characters as ^(Char). + ECHOKE: 61, // Visual erase for line kill. + PENDIN: 62, // Retype pending input. + OPOST: 70, // Enable output processing. + OLCUC: 71, // Convert lowercase to uppercase. + ONLCR: 72, // Map NL to CR-NL. + OCRNL: 73, // Translate carriage return to newline (output). + ONOCR: 74, // Translate newline to carriage return-newline + // (output). + ONLRET: 75, // Newline performs a carriage return (output). + CS7: 90, // 7 bit mode. + CS8: 91, // 8 bit mode. + PARENB: 92, // Parity enable. + PARODD: 93, // Odd parity, else even. + TTY_OP_ISPEED: 128, // Specifies the input baud rate in bits per second. + TTY_OP_OSPEED: 129, // Specifies the output baud rate in bits per second. + }, + CHANNEL_EXTENDED_DATATYPE: { + STDERR: 1, + }, + + SIGNALS: [ + 'ABRT', 'ALRM', 'FPE', 'HUP', 'ILL', 'INT', 'QUIT', 'SEGV', 'TERM', 'USR1', + 'USR2', 'KILL', 'PIPE' + ].reduce((cur, val) => ({ ...cur, [val]: 1 }), {}), + + COMPAT, + COMPAT_CHECKS: [ + [ 'Cisco-1.25', COMPAT.BAD_DHGEX ], + [ /^Cisco-1\./, COMPAT.BUG_DHGEX_LARGE ], + [ /^[0-9.]+$/, COMPAT.OLD_EXIT ], // old SSH.com implementations + [ /^OpenSSH_5\.\d+/, COMPAT.DYN_RPORT_BUG ], + ], + + // KEX proposal-related + DEFAULT_KEX, + SUPPORTED_KEX, + DEFAULT_SERVER_HOST_KEY, + SUPPORTED_SERVER_HOST_KEY, + DEFAULT_CIPHER, + SUPPORTED_CIPHER, + DEFAULT_MAC, + SUPPORTED_MAC, + DEFAULT_COMPRESSION, + SUPPORTED_COMPRESSION, + + curve25519Supported, + eddsaSupported, +}; + +module.exports.DISCONNECT_REASON_BY_VALUE = + Array.from(Object.entries(module.exports.DISCONNECT_REASON)) + .reduce((obj, [key, value]) => ({ ...obj, [value]: key }), {}); diff --git a/lib/protocol/crypto.js b/lib/protocol/crypto.js new file mode 100644 index 00000000..0580a794 --- /dev/null +++ b/lib/protocol/crypto.js @@ -0,0 +1,1607 @@ +// TODO: +// * make max packet size configurable +// * if decompression is enabled, use `._packet` in decipher instances as +// input to (sync) zlib inflater with appropriate offset and length to +// avoid an additional copy of payload data before inflation +// * factor decompression status into packet length checks +'use strict'; + +const { + createCipheriv, createDecipheriv, createHmac, randomFillSync, timingSafeEqual +} = require('crypto'); + +const { readUInt32BE, writeUInt32BE } = require('./utils.js'); + +const FastBuffer = Buffer[Symbol.species]; +const MAX_SEQNO = 2 ** 32 - 1; +const EMPTY_BUFFER = Buffer.alloc(0); +const BUF_INT = Buffer.alloc(4); +const DISCARD_CACHE = new Map(); +const MAX_PACKET_SIZE = 35000; + +let binding; +let AESGCMCipher; +let ChaChaPolyCipher; +let GenericCipher; +let AESGCMDecipher; +let ChaChaPolyDecipher; +let GenericDecipher; +try { + binding = require('./crypto/build/Release/sshcrypto.node'); + ({ AESGCMCipher, ChaChaPolyCipher, GenericCipher, + AESGCMDecipher, ChaChaPolyDecipher, GenericDecipher } = binding); +} catch {} + +const CIPHER_STREAM = 1 << 0; +const CIPHER_INFO = (() => { + function info(sslName, blockLen, keyLen, ivLen, authLen, discardLen, flags) { + return { + sslName, + blockLen, + keyLen, + ivLen: (ivLen !== 0 || (flags & CIPHER_STREAM) + ? ivLen + : blockLen), + authLen, + discardLen, + stream: !!(flags & CIPHER_STREAM), + }; + } + + return { + 'chacha20-poly1305@openssh.com': + info('chacha20', 8, 64, 0, 16, 0, CIPHER_STREAM), + + 'aes128-gcm': info('aes-128-gcm', 16, 16, 12, 16, 0, CIPHER_STREAM), + 'aes256-gcm': info('aes-256-gcm', 16, 32, 12, 16, 0, CIPHER_STREAM), + 'aes128-gcm@openssh.com': + info('aes-128-gcm', 16, 16, 12, 16, 0, CIPHER_STREAM), + 'aes256-gcm@openssh.com': + info('aes-256-gcm', 16, 32, 12, 16, 0, CIPHER_STREAM), + + 'aes128-cbc': info('aes-128-cbc', 16, 16, 0, 0, 0, 0), + 'aes192-cbc': info('aes-192-cbc', 16, 24, 0, 0, 0, 0), + 'aes256-cbc': info('aes-256-cbc', 16, 32, 0, 0, 0, 0), + 'rijndael-cbc@lysator.liu.se': info('aes-256-cbc', 16, 32, 0, 0, 0, 0), + '3des-cbc': info('des-ede3-cbc', 8, 24, 0, 0, 0, 0), + 'blowfish-cbc': info('bf-cbc', 8, 16, 0, 0, 0, 0), + 'idea-cbc': info('idea-cbc', 8, 16, 0, 0, 0, 0), + 'cast128-cbc': info('cast-cbc', 8, 16, 0, 0, 0, 0), + + 'aes128-ctr': info('aes-128-ctr', 16, 16, 16, 0, 0, CIPHER_STREAM), + 'aes192-ctr': info('aes-192-ctr', 16, 24, 16, 0, 0, CIPHER_STREAM), + 'aes256-ctr': info('aes-256-ctr', 16, 32, 16, 0, 0, CIPHER_STREAM), + '3des-ctr': info('des-ede3', 8, 24, 8, 0, 0, CIPHER_STREAM), + 'blowfish-ctr': info('bf-ecb', 8, 16, 8, 0, 0, CIPHER_STREAM), + 'cast128-ctr': info('cast5-ecb', 8, 16, 8, 0, 0, CIPHER_STREAM), + + /* The "arcfour128" algorithm is the RC4 cipher, as described in + [SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream + generated by the cipher MUST be discarded, and the first byte of the + first encrypted packet MUST be encrypted using the 1537th byte of + keystream. + + -- http://tools.ietf.org/html/rfc4345#section-4 */ + 'arcfour': info('rc4', 8, 16, 0, 0, 1536, CIPHER_STREAM), + 'arcfour128': info('rc4', 8, 16, 0, 0, 1536, CIPHER_STREAM), + 'arcfour256': info('rc4', 8, 32, 0, 0, 1536, CIPHER_STREAM), + 'arcfour512': info('rc4', 8, 64, 0, 0, 1536, CIPHER_STREAM), + }; +})(); + +const MAC_INFO = (() => { + function info(sslName, len, actualLen, isETM) { + return { + sslName, + len, + actualLen, + isETM, + }; + } + + return { + 'hmac-md5': info('md5', 16, 16, false), + 'hmac-md5-96': info('md5', 16, 12, false), + 'hmac-ripemd160': info('ripemd160', 20, 20, false), + 'hmac-sha1': info('sha1', 20, 20, false), + 'hmac-sha1-etm@openssh.com': info('sha1', 20, 20, true), + 'hmac-sha1-96': info('sha1', 20, 12, false), + 'hmac-sha2-256': info('sha256', 32, 32, false), + 'hmac-sha2-256-etm@openssh.com': info('sha256', 32, 32, true), + 'hmac-sha2-256-96': info('sha256', 32, 12, false), + 'hmac-sha2-512': info('sha512', 64, 64, false), + 'hmac-sha2-512-etm@openssh.com': info('sha512', 64, 64, true), + 'hmac-sha2-512-96': info('sha512', 64, 12, false), + }; +})(); + + +// Should only_be used during the initial handshake +class NullCipher { + constructor(seqno, onWrite) { + this.outSeqno = seqno; + this._onWrite = onWrite; + this._dead = false; + } + free() { + this._dead = true; + } + allocPacket(payloadLen) { + let pktLen = 4 + 1 + payloadLen; + let padLen = 8 - (pktLen & (8 - 1)); + if (padLen < 4) + padLen += 8; + pktLen += padLen; + + const packet = Buffer.allocUnsafe(pktLen); + + writeUInt32BE(packet, pktLen - 4, 0); + packet[4] = padLen; + + randomFillSync(packet, 5 + payloadLen, padLen); + + return packet; + } + encrypt(packet) { + // `packet` === unencrypted packet + + if (this._dead) + return; + + this._onWrite(packet); + + this.outSeqno = (this.outSeqno + 1) >>> 0; + } +} + + +const CCP_ZEROS = Buffer.alloc(32); +const CCP_OUT_COMPUTE = Buffer.alloc(16); +const CCP_WASM_MODULE = require('./crypto/poly1305.js'); +const CCP_RESULT_MALLOC = CCP_WASM_MODULE._malloc(16); +const poly1305_auth = CCP_WASM_MODULE.cwrap( + 'poly1305_auth', + null, + ['number', 'array', 'number', 'array', 'number', 'array'] +); +class ChaChaPolyCipherNative { + constructor(config) { + const enc = config.outbound; + this.outSeqno = enc.seqno; + this._onWrite = enc.onWrite; + this._encKeyMain = enc.cipherKey.slice(0, 32); + this._encKeyPktLen = enc.cipherKey.slice(32); + this._dead = false; + } + free() { + this._dead = true; + } + allocPacket(payloadLen) { + let pktLen = 4 + 1 + payloadLen; + let padLen = 8 - ((pktLen - 4) & (8 - 1)); + if (padLen < 4) + padLen += 8; + pktLen += padLen; + + const packet = Buffer.allocUnsafe(pktLen); + + writeUInt32BE(packet, pktLen - 4, 0); + packet[4] = padLen; + + randomFillSync(packet, 5 + payloadLen, padLen); + + return packet; + } + encrypt(packet) { + // `packet` === unencrypted packet + + if (this._dead) + return; + + // Generate Poly1305 key + CCP_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian) + writeUInt32BE(CCP_OUT_COMPUTE, this.outSeqno, 12); + const polyKey = + createCipheriv('chacha20', this._encKeyMain, CCP_OUT_COMPUTE) + .update(CCP_ZEROS); + + // Encrypt packet length + const pktLenEnc = + createCipheriv('chacha20', this._encKeyPktLen, CCP_OUT_COMPUTE) + .update(packet.slice(0, 4)); + this._onWrite(pktLenEnc); + + // Encrypt rest of packet + CCP_OUT_COMPUTE[0] = 1; // Set counter to 1 (little endian) + const payloadEnc = + createCipheriv('chacha20', this._encKeyMain, CCP_OUT_COMPUTE) + .update(packet.slice(4)); + this._onWrite(payloadEnc); + + // Calculate Poly1305 MAC + poly1305_auth(CCP_RESULT_MALLOC, + pktLenEnc, + pktLenEnc.length, + payloadEnc, + payloadEnc.length, + polyKey); + const mac = Buffer.allocUnsafe(16); + mac.set( + new Uint8Array(CCP_WASM_MODULE.HEAPU8.buffer, CCP_RESULT_MALLOC, 16), + 0 + ); + this._onWrite(mac); + + this.outSeqno = (this.outSeqno + 1) >>> 0; + } +} + +class ChaChaPolyCipherBinding { + constructor(config) { + const enc = config.outbound; + this.outSeqno = enc.seqno; + this._onWrite = enc.onWrite; + this._instance = new ChaChaPolyCipher(enc.cipherKey); + this._dead = false; + } + free() { + this._dead = true; + this._instance.free(); + } + allocPacket(payloadLen) { + let pktLen = 4 + 1 + payloadLen; + let padLen = 8 - ((pktLen - 4) & (8 - 1)); + if (padLen < 4) + padLen += 8; + pktLen += padLen; + + const packet = Buffer.allocUnsafe(pktLen + 16/* MAC */); + + writeUInt32BE(packet, pktLen - 4, 0); + packet[4] = padLen; + + randomFillSync(packet, 5 + payloadLen, padLen); + + return packet; + } + encrypt(packet) { + // `packet` === unencrypted packet + + if (this._dead) + return; + + // Encrypts in-place + this._instance.encrypt(packet, this.outSeqno); + + this._onWrite(packet); + + this.outSeqno = (this.outSeqno + 1) >>> 0; + } +} + + +class AESGCMCipherNative { + constructor(config) { + const enc = config.outbound; + this.outSeqno = enc.seqno; + this._onWrite = enc.onWrite; + this._encSSLName = enc.cipherInfo.sslName; + this._encKey = enc.cipherKey; + this._encIV = enc.cipherIV; + this._dead = false; + } + free() { + this._dead = true; + } + allocPacket(payloadLen) { + let pktLen = 4 + 1 + payloadLen; + let padLen = 16 - ((pktLen - 4) & (16 - 1)); + if (padLen < 4) + padLen += 16; + pktLen += padLen; + + const packet = Buffer.allocUnsafe(pktLen); + + writeUInt32BE(packet, pktLen - 4, 0); + packet[4] = padLen; + + randomFillSync(packet, 5 + payloadLen, padLen); + + return packet; + } + encrypt(packet) { + // `packet` === unencrypted packet + + if (this._dead) + return; + + const cipher = createCipheriv(this._encSSLName, this._encKey, this._encIV); + cipher.setAutoPadding(false); + + const lenData = packet.slice(0, 4); + cipher.setAAD(lenData); + this._onWrite(lenData); + + // Encrypt pad length, payload, and padding + const encrypted = cipher.update(packet.slice(4)); + this._onWrite(encrypted); + const final = cipher.final(); + // XXX: final.length === 0 always? + if (final.length) + this._onWrite(final); + + // Generate MAC + const tag = cipher.getAuthTag(); + this._onWrite(tag); + + // Increment counter in IV by 1 for next packet + ivIncrement(this._encIV); + + this.outSeqno = (this.outSeqno + 1) >>> 0; + } +} + +class AESGCMCipherBinding { + constructor(config) { + const enc = config.outbound; + this.outSeqno = enc.seqno; + this._onWrite = enc.onWrite; + this._instance = new AESGCMCipher(enc.cipherInfo.sslName, + enc.cipherKey, + enc.cipherIV); + this._dead = false; + } + free() { + this._dead = true; + this._instance.free(); + } + allocPacket(payloadLen) { + let pktLen = 4 + 1 + payloadLen; + let padLen = 16 - ((pktLen - 4) & (16 - 1)); + if (padLen < 4) + padLen += 16; + pktLen += padLen; + + const packet = Buffer.allocUnsafe(pktLen + 16/* authTag */); + + writeUInt32BE(packet, pktLen - 4, 0); + packet[4] = padLen; + + randomFillSync(packet, 5 + payloadLen, padLen); + + return packet; + } + encrypt(packet) { + // `packet` === unencrypted packet + + if (this._dead) + return; + + // Encrypts in-place + this._instance.encrypt(packet); + + this._onWrite(packet); + + this.outSeqno = (this.outSeqno + 1) >>> 0; + } +} + + +class GenericCipherNative { + constructor(config) { + const enc = config.outbound; + this.outSeqno = enc.seqno; + this._onWrite = enc.onWrite; + this._encBlockLen = enc.cipherInfo.blockLen; + this._cipherInstance = createCipheriv(enc.cipherInfo.sslName, + enc.cipherKey, + enc.cipherIV); + this._macSSLName = enc.macInfo.sslName; + this._macKey = enc.macKey; + this._macActualLen = enc.macInfo.actualLen; + this._macETM = enc.macInfo.isETM; + this._aadLen = (this._macETM ? 4 : 0); + this._dead = false; + + const discardLen = enc.cipherInfo.discardLen; + if (discardLen) { + let discard = DISCARD_CACHE.get(discardLen); + if (discard === undefined) { + discard = Buffer.alloc(discardLen); + DISCARD_CACHE.set(discardLen, discard); + } + this._cipherInstance.update(discard); + } + } + free() { + this._dead = true; + } + allocPacket(payloadLen) { + const blockLen = this._encBlockLen; + + let pktLen = 4 + 1 + payloadLen; + let padLen = blockLen - ((pktLen - this._aadLen) & (blockLen - 1)); + if (padLen < 4) + padLen += blockLen; + pktLen += padLen; + + const packet = Buffer.allocUnsafe(pktLen); + + writeUInt32BE(packet, pktLen - 4, 0); + packet[4] = padLen; + + randomFillSync(packet, 5 + payloadLen, padLen); + + return packet; + } + encrypt(packet) { + // `packet` === unencrypted packet + + if (this._dead) + return; + + let mac; + if (this._macETM) { + // Encrypt pad length, payload, and padding + const lenBytes = new Uint8Array(packet.buffer, packet.byteOffset, 4); + const encrypted = this._cipherInstance.update( + new Uint8Array(packet.buffer, + packet.byteOffset + 4, + packet.length - 4) + ); + + this._onWrite(lenBytes); + this._onWrite(encrypted); + + // TODO: look into storing seqno as 4-byte buffer and incrementing like we + // do for AES-GCM IVs to avoid having to (re)write all 4 bytes every time + mac = createHmac(this._macSSLName, this._macKey); + writeUInt32BE(BUF_INT, this.outSeqno, 0); + mac.update(BUF_INT); + mac.update(lenBytes); + mac.update(encrypted); + } else { + // Encrypt length field, pad length, payload, and padding + const encrypted = this._cipherInstance.update(packet); + this._onWrite(encrypted); + + // TODO: look into storing seqno as 4-byte buffer and incrementing like we + // do for AES-GCM IVs to avoid having to (re)write all 4 bytes every time + mac = createHmac(this._macSSLName, this._macKey); + writeUInt32BE(BUF_INT, this.outSeqno, 0); + mac.update(BUF_INT); + mac.update(packet); + } + + let digest = mac.digest(); + if (digest.length > this._macActualLen) + digest = digest.slice(0, this._macActualLen); + this._onWrite(digest); + + this.outSeqno = (this.outSeqno + 1) >>> 0; + } +} + +class GenericCipherBinding { + constructor(config) { + const enc = config.outbound; + this.outSeqno = enc.seqno; + this._onWrite = enc.onWrite; + this._encBlockLen = enc.cipherInfo.blockLen; + this._macLen = enc.macInfo.len; + this._macActualLen = enc.macInfo.actualLen; + this._aadLen = (enc.macInfo.isETM ? 4 : 0); + this._instance = new GenericCipher(enc.cipherInfo.sslName, + enc.cipherKey, + enc.cipherIV, + enc.macInfo.sslName, + enc.macKey, + enc.macInfo.isETM); + this._dead = false; + } + free() { + this._dead = true; + this._instance.free(); + } + allocPacket(payloadLen) { + const blockLen = this._encBlockLen; + + let pktLen = 4 + 1 + payloadLen; + let padLen = blockLen - ((pktLen - this._aadLen) & (blockLen - 1)); + if (padLen < 4) + padLen += blockLen; + pktLen += padLen; + + const packet = Buffer.allocUnsafe(pktLen + this._macLen); + + writeUInt32BE(packet, pktLen - 4, 0); + packet[4] = padLen; + + randomFillSync(packet, 5 + payloadLen, padLen); + + return packet; + } + encrypt(packet) { + // `packet` === unencrypted packet + + if (this._dead) + return; + + // Encrypts in-place + this._instance.encrypt(packet, this.outSeqno); + + if (this._macActualLen < this._macLen) { + packet = new FastBuffer(packet.buffer, + packet.byteOffset, + (packet.length + - (this._macLen - this._macActualLen))); + } + this._onWrite(packet); + + this.outSeqno = (this.outSeqno + 1) >>> 0; + } +} + + +class NullDecipher { + constructor(seqno, onPayload) { + this.inSeqno = seqno; + this._onPayload = onPayload; + this._len = 0; + this._lenBytes = 0; + this._packet = null; + this._packetPos = 0; + } + free() {} + decrypt(data, p, dataLen) { + while (p < dataLen) { + // Read packet length + if (this._lenBytes < 4) { + let nb = Math.min(4 - this._lenBytes, dataLen - p); + + this._lenBytes += nb; + while (nb--) + this._len = (this._len << 8) + data[p++]; + + if (this._lenBytes < 4) + return; + + if (this._len > MAX_PACKET_SIZE + || this._len < 8 + || (4 + this._len & 7) !== 0) { + throw new Error('Bad packet length'); + } + if (p >= dataLen) + return; + } + + // Read padding length, payload, and padding + if (this._packetPos < this._len) { + const nb = Math.min(this._len - this._packetPos, dataLen - p); + if (p !== 0 || nb !== dataLen) { + if (nb === this._len) { + this._packet = new FastBuffer(data.buffer, data.byteOffset + p, nb); + } else { + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._packetPos + ); + } + } else if (nb === this._len) { + this._packet = data; + } else { + this._packet.set(data, this._packetPos); + } + p += nb; + this._packetPos += nb; + if (this._packetPos < this._len) + return; + } + + const payload = (!this._packet + ? EMPTY_BUFFER + : new FastBuffer(this._packet.buffer, + this._packet.byteOffset + 1, + this._packet.length + - this._packet[0] - 1)); + + // Prepare for next packet + this.inSeqno = (this.inSeqno + 1) >>> 0; + this._len = 0; + this._lenBytes = 0; + this._packet = null; + this._packetPos = 0; + + { + const ret = this._onPayload(payload); + if (ret !== undefined) + return (ret === false ? p : ret); + } + } + } +} + +class ChaChaPolyDecipherNative { + constructor(config) { + const dec = config.inbound; + this.inSeqno = dec.seqno; + this._onPayload = dec.onPayload; + this._decKeyMain = dec.decipherKey.slice(0, 32); + this._decKeyPktLen = dec.decipherKey.slice(32); + this._len = 0; + this._lenBuf = Buffer.alloc(4); + this._lenPos = 0; + this._packet = null; + this._pktLen = 0; + this._mac = Buffer.allocUnsafe(16); + this._calcMac = Buffer.allocUnsafe(16); + this._macPos = 0; + } + free() {} + decrypt(data, p, dataLen) { + // `data` === encrypted data + + while (p < dataLen) { + // Read packet length + if (this._lenPos < 4) { + let nb = Math.min(4 - this._lenPos, dataLen - p); + while (nb--) + this._lenBuf[this._lenPos++] = data[p++]; + if (this._lenPos < 4) + return; + + CCP_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian) + writeUInt32BE(CCP_OUT_COMPUTE, this.inSeqno, 12); + + const decLenBytes = + createDecipheriv('chacha20', this._decKeyPktLen, CCP_OUT_COMPUTE) + .update(this._lenBuf); + this._len = readUInt32BE(decLenBytes, 0); + + if (this._len > MAX_PACKET_SIZE + || this._len < 8 + || (this._len & 7) !== 0) { + throw new Error('Bad packet length'); + } + } + + // Read padding length, payload, and padding + if (this._pktLen < this._len) { + if (p >= dataLen) + return; + const nb = Math.min(this._len - this._pktLen, dataLen - p); + let encrypted; + if (p !== 0 || nb !== dataLen) + encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb); + else + encrypted = data; + if (nb === this._len) { + this._packet = encrypted; + } else { + if (!this._packet) + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set(encrypted, this._pktLen); + } + p += nb; + this._pktLen += nb; + if (this._pktLen < this._len || p >= dataLen) + return; + } + + // Read Poly1305 MAC + { + const nb = Math.min(16 - this._macPos, dataLen - p); + // TODO: avoid copying if entire MAC is in current chunk + if (p !== 0 || nb !== dataLen) { + this._mac.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._macPos + ); + } else { + this._mac.set(data, this._macPos); + } + p += nb; + this._macPos += nb; + if (this._macPos < 16) + return; + } + + // Generate Poly1305 key + CCP_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian) + writeUInt32BE(CCP_OUT_COMPUTE, this.inSeqno, 12); + const polyKey = + createCipheriv('chacha20', this._decKeyMain, CCP_OUT_COMPUTE) + .update(CCP_ZEROS); + + // Calculate and compare Poly1305 MACs + poly1305_auth(CCP_RESULT_MALLOC, + this._lenBuf, + 4, + this._packet, + this._packet.length, + polyKey); + + this._calcMac.set( + new Uint8Array(CCP_WASM_MODULE.HEAPU8.buffer, CCP_RESULT_MALLOC, 16), + 0 + ); + if (!timingSafeEqual(this._calcMac, this._mac)) + throw new Error('Invalid MAC'); + + // Decrypt packet + CCP_OUT_COMPUTE[0] = 1; // Set counter to 1 (little endian) + const packet = + createDecipheriv('chacha20', this._decKeyMain, CCP_OUT_COMPUTE) + .update(this._packet); + + const payload = new FastBuffer(packet.buffer, + packet.byteOffset + 1, + packet.length - packet[0] - 1); + + // Prepare for next packet + this.inSeqno = (this.inSeqno + 1) >>> 0; + this._len = 0; + this._lenPos = 0; + this._packet = null; + this._pktLen = 0; + this._macPos = 0; + + { + const ret = this._onPayload(payload); + if (ret !== undefined) + return (ret === false ? p : ret); + } + } + } +} + +class ChaChaPolyDecipherBinding { + constructor(config) { + const dec = config.inbound; + this.inSeqno = dec.seqno; + this._onPayload = dec.onPayload; + this._instance = new ChaChaPolyDecipher(dec.decipherKey); + this._len = 0; + this._lenBuf = Buffer.alloc(4); + this._lenPos = 0; + this._packet = null; + this._pktLen = 0; + this._mac = Buffer.allocUnsafe(16); + this._macPos = 0; + } + free() { + this._instance.free(); + } + decrypt(data, p, dataLen) { + // `data` === encrypted data + + while (p < dataLen) { + // Read packet length + if (this._lenPos < 4) { + let nb = Math.min(4 - this._lenPos, dataLen - p); + while (nb--) + this._lenBuf[this._lenPos++] = data[p++]; + if (this._lenPos < 4) + return; + + this._len = this._instance.decryptLen(this._lenBuf, this.inSeqno); + + if (this._len > MAX_PACKET_SIZE + || this._len < 8 + || (this._len & 7) !== 0) { + throw new Error('Bad packet length'); + } + + if (p >= dataLen) + return; + } + + // Read padding length, payload, and padding + if (this._pktLen < this._len) { + const nb = Math.min(this._len - this._pktLen, dataLen - p); + let encrypted; + if (p !== 0 || nb !== dataLen) + encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb); + else + encrypted = data; + if (nb === this._len) { + this._packet = encrypted; + } else { + if (!this._packet) + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set(encrypted, this._pktLen); + } + p += nb; + this._pktLen += nb; + if (this._pktLen < this._len || p >= dataLen) + return; + } + + // Read Poly1305 MAC + { + const nb = Math.min(16 - this._macPos, dataLen - p); + // TODO: avoid copying if entire MAC is in current chunk + if (p !== 0 || nb !== dataLen) { + this._mac.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._macPos + ); + } else { + this._mac.set(data, this._macPos); + } + p += nb; + this._macPos += nb; + if (this._macPos < 16) + return; + } + + this._instance.decrypt(this._packet, this._mac, this.inSeqno); + + const payload = new FastBuffer(this._packet.buffer, + this._packet.byteOffset + 1, + this._packet.length - this._packet[0] - 1); + + // Prepare for next packet + this.inSeqno = (this.inSeqno + 1) >>> 0; + this._len = 0; + this._lenPos = 0; + this._packet = null; + this._pktLen = 0; + this._macPos = 0; + + { + const ret = this._onPayload(payload); + if (ret !== undefined) + return (ret === false ? p : ret); + } + } + } +} + +class AESGCMDecipherNative { + constructor(config) { + const dec = config.inbound; + this.inSeqno = dec.seqno; + this._onPayload = dec.onPayload; + this._decipherInstance = null; + this._decipherSSLName = dec.decipherInfo.sslName; + this._decipherKey = dec.decipherKey; + this._decipherIV = dec.decipherIV; + this._len = 0; + this._lenBytes = 0; + this._packet = null; + this._packetPos = 0; + this._pktLen = 0; + this._tag = Buffer.allocUnsafe(16); + this._tagPos = 0; + } + free() {} + decrypt(data, p, dataLen) { + // `data` === encrypted data + + while (p < dataLen) { + // Read packet length (unencrypted, but AAD) + if (this._lenBytes < 4) { + let nb = Math.min(4 - this._lenBytes, dataLen - p); + this._lenBytes += nb; + while (nb--) + this._len = (this._len << 8) + data[p++]; + if (this._lenBytes < 4) + return; + + if ((this._len + 20) > MAX_PACKET_SIZE + || this._len < 16 + || (this._len & 15) !== 0) { + throw new Error('Bad packet length'); + } + + this._decipherInstance = createDecipheriv( + this._decipherSSLName, + this._decipherKey, + this._decipherIV + ); + this._decipherInstance.setAutoPadding(false); + this._decipherInstance.setAAD(intToBytes(this._len)); + } + + // Read padding length, payload, and padding + if (this._pktLen < this._len) { + if (p >= dataLen) + return; + const nb = Math.min(this._len - this._pktLen, dataLen - p); + let decrypted; + if (p !== 0 || nb !== dataLen) { + decrypted = this._decipherInstance.update( + new Uint8Array(data.buffer, data.byteOffset + p, nb) + ); + } else { + decrypted = this._decipherInstance.update(data); + } + if (decrypted.length) { + if (nb === this._len) { + this._packet = decrypted; + } else { + if (!this._packet) + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set(decrypted, this._packetPos); + } + this._packetPos += decrypted.length; + } + p += nb; + this._pktLen += nb; + if (this._pktLen < this._len || p >= dataLen) + return; + } + + // Read authentication tag + { + const nb = Math.min(16 - this._tagPos, dataLen - p); + if (p !== 0 || nb !== dataLen) { + this._tag.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._tagPos + ); + } else { + this._tag.set(data, this._tagPos); + } + p += nb; + this._tagPos += nb; + if (this._tagPos < 16) + return; + } + + { + // Verify authentication tag + this._decipherInstance.setAuthTag(this._tag); + + const decrypted = this._decipherInstance.final(); + + // XXX: this should never output any data since stream ciphers always + // return data from .update() and block ciphers must end on a multiple + // of the block length, which would have caused an exception to be + // thrown if the total input was not... + if (decrypted.length) { + if (this._packet) + this._packet.set(decrypted, this._packetPos); + else + this._packet = decrypted; + } + } + + const payload = (!this._packet + ? EMPTY_BUFFER + : new FastBuffer(this._packet.buffer, + this._packet.byteOffset + 1, + this._packet.length + - this._packet[0] - 1)); + + // Prepare for next packet + this.inSeqno = (this.inSeqno + 1) >>> 0; + ivIncrement(this._decipherIV); + this._len = 0; + this._lenBytes = 0; + this._packet = null; + this._packetPos = 0; + this._pktLen = 0; + this._tagPos = 0; + + { + const ret = this._onPayload(payload); + if (ret !== undefined) + return (ret === false ? p : ret); + } + } + } +} + +class AESGCMDecipherBinding { + constructor(config) { + const dec = config.inbound; + this.inSeqno = dec.seqno; + this._onPayload = dec.onPayload; + this._instance = new AESGCMDecipher(dec.decipherInfo.sslName, + dec.decipherKey, + dec.decipherIV); + this._len = 0; + this._lenBytes = 0; + this._packet = null; + this._pktLen = 0; + this._tag = Buffer.allocUnsafe(16); + this._tagPos = 0; + } + free() {} + decrypt(data, p, dataLen) { + // `data` === encrypted data + + while (p < dataLen) { + // Read packet length (unencrypted, but AAD) + if (this._lenBytes < 4) { + let nb = Math.min(4 - this._lenBytes, dataLen - p); + this._lenBytes += nb; + while (nb--) + this._len = (this._len << 8) + data[p++]; + if (this._lenBytes < 4) + return; + + if ((this._len + 20) > MAX_PACKET_SIZE + || this._len < 16 + || (this._len & 15) !== 0) { + throw new Error(`Bad packet length: ${this._len}`); + } + } + + // Read padding length, payload, and padding + if (this._pktLen < this._len) { + if (p >= dataLen) + return; + const nb = Math.min(this._len - this._pktLen, dataLen - p); + let encrypted; + if (p !== 0 || nb !== dataLen) + encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb); + else + encrypted = data; + if (nb === this._len) { + this._packet = encrypted; + } else { + if (!this._packet) + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set(encrypted, this._pktLen); + } + p += nb; + this._pktLen += nb; + if (this._pktLen < this._len || p >= dataLen) + return; + } + + // Read authentication tag + { + const nb = Math.min(16 - this._tagPos, dataLen - p); + if (p !== 0 || nb !== dataLen) { + this._tag.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._tagPos + ); + } else { + this._tag.set(data, this._tagPos); + } + p += nb; + this._tagPos += nb; + if (this._tagPos < 16) + return; + } + + this._instance.decrypt(this._packet, this._len, this._tag); + + const payload = new FastBuffer(this._packet.buffer, + this._packet.byteOffset + 1, + this._packet.length - this._packet[0] - 1); + + // Prepare for next packet + this.inSeqno = (this.inSeqno + 1) >>> 0; + this._len = 0; + this._lenBytes = 0; + this._packet = null; + this._pktLen = 0; + this._tagPos = 0; + + { + const ret = this._onPayload(payload); + if (ret !== undefined) + return (ret === false ? p : ret); + } + } + } +} + +// TODO: test incremental .update()s vs. copying to _packet and doing a single +// .update() after entire packet read -- a single .update() would allow +// verifying MAC before decrypting for ETM MACs +class GenericDecipherNative { + constructor(config) { + const dec = config.inbound; + this.inSeqno = dec.seqno; + this._onPayload = dec.onPayload; + this._decipherInstance = createDecipheriv(dec.decipherInfo.sslName, + dec.decipherKey, + dec.decipherIV); + this._decipherInstance.setAutoPadding(false); + this._block = Buffer.allocUnsafe( + dec.macInfo.isETM ? 4 : dec.decipherInfo.blockLen + ); + this._blockSize = dec.decipherInfo.blockLen; + this._blockPos = 0; + this._len = 0; + this._packet = null; + this._packetPos = 0; + this._pktLen = 0; + this._mac = Buffer.allocUnsafe(dec.macInfo.actualLen); + this._macPos = 0; + this._macSSLName = dec.macInfo.sslName; + this._macKey = dec.macKey; + this._macActualLen = dec.macInfo.actualLen; + this._macETM = dec.macInfo.isETM; + this._macInstance = null; + + const discardLen = dec.decipherInfo.discardLen; + if (discardLen) { + let discard = DISCARD_CACHE.get(discardLen); + if (discard === undefined) { + discard = Buffer.alloc(discardLen); + DISCARD_CACHE.set(discardLen, discard); + } + this._decipherInstance.update(discard); + } + } + free() {} + decrypt(data, p, dataLen) { + // `data` === encrypted data + + while (p < dataLen) { + // Read first encrypted block + if (this._blockPos < this._block.length) { + const nb = Math.min(this._block.length - this._blockPos, dataLen - p); + if (p !== 0 || nb !== dataLen || nb < data.length) { + this._block.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._blockPos + ); + } else { + this._block.set(data, this._blockPos); + } + + p += nb; + this._blockPos += nb; + if (this._blockPos < this._block.length) + return; + + let decrypted; + let need; + if (this._macETM) { + this._len = need = readUInt32BE(this._block, 0); + } else { + // Decrypt first block to get packet length + decrypted = this._decipherInstance.update(this._block); + this._len = readUInt32BE(decrypted, 0); + need = 4 + this._len - this._blockSize; + } + + if (this._len > MAX_PACKET_SIZE + || this._len < 5 + || (need & (this._blockSize - 1)) !== 0) { + throw new Error('Bad packet length'); + } + + // Create MAC up front to calculate in parallel with decryption + this._macInstance = createHmac(this._macSSLName, this._macKey); + + writeUInt32BE(BUF_INT, this.inSeqno, 0); + this._macInstance.update(BUF_INT); + if (this._macETM) { + this._macInstance.update(this._block); + } else { + this._macInstance.update(new Uint8Array(decrypted.buffer, + decrypted.byteOffset, + 4)); + this._pktLen = decrypted.length - 4; + this._packetPos = this._pktLen; + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set( + new Uint8Array(decrypted.buffer, + decrypted.byteOffset + 4, + this._packetPos), + 0 + ); + } + + if (p >= dataLen) + return; + } + + // Read padding length, payload, and padding + if (this._pktLen < this._len) { + const nb = Math.min(this._len - this._pktLen, dataLen - p); + let encrypted; + if (p !== 0 || nb !== dataLen) + encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb); + else + encrypted = data; + if (this._macETM) + this._macInstance.update(encrypted); + const decrypted = this._decipherInstance.update(encrypted); + if (decrypted.length) { + if (nb === this._len) { + this._packet = decrypted; + } else { + if (!this._packet) + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set(decrypted, this._packetPos); + } + this._packetPos += decrypted.length; + } + p += nb; + this._pktLen += nb; + if (this._pktLen < this._len || p >= dataLen) + return; + } + + // Read MAC + { + const nb = Math.min(this._macActualLen - this._macPos, dataLen - p); + if (p !== 0 || nb !== dataLen) { + this._mac.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._macPos + ); + } else { + this._mac.set(data, this._macPos); + } + p += nb; + this._macPos += nb; + if (this._macPos < this._macActualLen) + return; + } + + // Verify MAC + if (!this._macETM) + this._macInstance.update(this._packet); + let calculated = this._macInstance.digest(); + if (this._macActualLen < calculated.length) { + calculated = new Uint8Array(calculated.buffer, + calculated.byteOffset, + this._macActualLen); + } + if (!timingSafeEquals(calculated, this._mac)) + throw new Error('Invalid MAC'); + + const payload = new FastBuffer(this._packet.buffer, + this._packet.byteOffset + 1, + this._packet.length - this._packet[0] - 1); + + // Prepare for next packet + this.inSeqno = (this.inSeqno + 1) >>> 0; + this._blockPos = 0; + this._len = 0; + this._packet = null; + this._packetPos = 0; + this._pktLen = 0; + this._macPos = 0; + this._macInstance = null; + + { + const ret = this._onPayload(payload); + if (ret !== undefined) + return (ret === false ? p : ret); + } + } + } +} + +class GenericDecipherBinding { + constructor(config) { + const dec = config.inbound; + this.inSeqno = dec.seqno; + this._onPayload = dec.onPayload; + this._instance = new GenericDecipher(dec.decipherInfo.sslName, + dec.decipherKey, + dec.decipherIV, + dec.macInfo.sslName, + dec.macKey, + dec.macInfo.isETM, + dec.macInfo.actualLen); + this._block = Buffer.allocUnsafe( + dec.macInfo.isETM || dec.decipherInfo.stream + ? 4 + : dec.decipherInfo.blockLen + ); + this._blockPos = 0; + this._len = 0; + this._packet = null; + this._pktLen = 0; + this._mac = Buffer.allocUnsafe(dec.macInfo.actualLen); + this._macPos = 0; + this._macActualLen = dec.macInfo.actualLen; + this._macETM = dec.macInfo.isETM; + } + free() { + this._instance.free(); + } + decrypt(data, p, dataLen) { + // `data` === encrypted data + + while (p < dataLen) { + // Read first encrypted block + if (this._blockPos < this._block.length) { + const nb = Math.min(this._block.length - this._blockPos, dataLen - p); + if (p !== 0 || nb !== dataLen || nb < data.length) { + this._block.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._blockPos + ); + } else { + this._block.set(data, this._blockPos); + } + + p += nb; + this._blockPos += nb; + if (this._blockPos < this._block.length) + return; + + let need; + if (this._macETM) { + this._len = need = readUInt32BE(this._block, 0); + } else { + // Decrypt first block to get packet length + this._instance.decryptBlock(this._block, this.inSeqno); + this._len = readUInt32BE(this._block, 0); + need = 4 + this._len - this._block.length; + } + + if (this._len > MAX_PACKET_SIZE + || this._len < 5 + || (need & (this._block.length - 1)) !== 0) { + throw new Error('Bad packet length'); + } + + if (!this._macETM) { + const pktStart = (this._block.length - 4); + const startP = p - pktStart; + let endP; + if (p >= pktStart && (endP = startP + this._len) <= dataLen) { + // The entire packet exists within the current chunk, with the + // first block already decrypted + if (startP === 0 && endP === dataLen) { + this._packet = data; + this._pktLen = this._len; + } else { + this._packet = new FastBuffer( + data.buffer, + data.byteOffset + startP, + this._len + ); + this._pktLen = this._len; + } + p = endP; + } else { + this._pktLen = pktStart; + if (this._pktLen) { + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set( + new Uint8Array(this._block.buffer, + this._block.byteOffset + 4, + this._pktLen), + 0 + ); + } + } + } + + if (p >= dataLen) + return; + } + + // Read padding length, payload, and padding + if (this._pktLen < this._len) { + const nb = Math.min(this._len - this._pktLen, dataLen - p); + let encrypted; + if (p !== 0 || nb !== dataLen) + encrypted = new Uint8Array(data.buffer, data.byteOffset + p, nb); + else + encrypted = data; + if (nb === this._len) { + this._packet = encrypted; + } else { + if (!this._packet) + this._packet = Buffer.allocUnsafe(this._len); + this._packet.set(encrypted, this._pktLen); + } + p += nb; + this._pktLen += nb; + if (this._pktLen < this._len || p >= dataLen) + return; + } + + // Read MAC + { + const nb = Math.min(this._macActualLen - this._macPos, dataLen - p); + if (p !== 0 || nb !== dataLen) { + this._mac.set( + new Uint8Array(data.buffer, data.byteOffset + p, nb), + this._macPos + ); + } else { + this._mac.set(data, this._macPos); + } + p += nb; + this._macPos += nb; + if (this._macPos < this._macActualLen) + return; + } + + // Decrypt and verify MAC + this._instance.decrypt(this._packet, + this.inSeqno, + this._block, + this._mac); + + const payload = new FastBuffer(this._packet.buffer, + this._packet.byteOffset + 1, + this._packet.length - this._packet[0] - 1); + + // Prepare for next packet + this.inSeqno = (this.inSeqno + 1) >>> 0; + this._blockPos = 0; + this._len = 0; + this._packet = null; + this._pktLen = 0; + this._macPos = 0; + this._macInstance = null; + + { + const ret = this._onPayload(payload); + if (ret !== undefined) + return (ret === false ? p : ret); + } + } + } +} + +// Increments unsigned, big endian counter (last 8 bytes) of AES-GCM IV +function ivIncrement(iv) { + ++iv[11] >>> 8 + && ++iv[10] >>> 8 + && ++iv[9] >>> 8 + && ++iv[8] >>> 8 + && ++iv[7] >>> 8 + && ++iv[6] >>> 8 + && ++iv[5] >>> 8 + && ++iv[4] >>> 8; +} + +const intToBytes = (() => { + const ret = Buffer.alloc(4); + return (n) => { + ret[0] = (n >>> 24); + ret[1] = (n >>> 16); + ret[2] = (n >>> 8); + ret[3] = n; + return ret; + }; +})(); + +function timingSafeEquals(a, b) { + if (a.length !== b.length) { + timingSafeEqual(a, a); + return false; + } + return timingSafeEqual(a, b); +} + +function createCipher(config) { + if (typeof config !== 'object' || config === null) + throw new Error('Invalid config'); + + if (typeof config.outbound !== 'object' || config.outbound === null) + throw new Error('Invalid outbound'); + + const outbound = config.outbound; + + if (typeof outbound.onWrite !== 'function') + throw new Error('Invalid outbound.onWrite'); + + if (typeof outbound.cipherInfo !== 'object' || outbound.cipherInfo === null) + throw new Error('Invalid outbound.cipherInfo'); + + if (!Buffer.isBuffer(outbound.cipherKey) + || outbound.cipherKey.length !== outbound.cipherInfo.keyLen) { + throw new Error('Invalid outbound.cipherKey'); + } + + if (outbound.cipherInfo.ivLen + && (!Buffer.isBuffer(outbound.cipherIV) + || outbound.cipherIV.length !== outbound.cipherInfo.ivLen)) { + throw new Error('Invalid outbound.cipherIV'); + } + + if (typeof outbound.seqno !== 'number' + || outbound.seqno < 0 + || outbound.seqno > MAX_SEQNO) { + throw new Error('Invalid outbound.seqno'); + } + + const forceNative = !!outbound.forceNative; + + switch (outbound.cipherInfo.sslName) { + case 'aes-128-gcm': + case 'aes-256-gcm': + return (AESGCMCipher && !forceNative + ? new AESGCMCipherBinding(config) + : new AESGCMCipherNative(config)); + case 'chacha20': + return (ChaChaPolyCipher && !forceNative + ? new ChaChaPolyCipherBinding(config) + : new ChaChaPolyCipherNative(config)); + default: { + if (typeof outbound.macInfo !== 'object' || outbound.macInfo === null) + throw new Error('Invalid outbound.macInfo'); + if (!Buffer.isBuffer(outbound.macKey) + || outbound.macKey.length !== outbound.macInfo.len) { + throw new Error('Invalid outbound.macKey'); + } + return (GenericCipher && !forceNative + ? new GenericCipherBinding(config) + : new GenericCipherNative(config)); + } + } +} + +function createDecipher(config) { + if (typeof config !== 'object' || config === null) + throw new Error('Invalid config'); + + if (typeof config.inbound !== 'object' || config.inbound === null) + throw new Error('Invalid inbound'); + + const inbound = config.inbound; + + if (typeof inbound.onPayload !== 'function') + throw new Error('Invalid inbound.onPayload'); + + if (typeof inbound.decipherInfo !== 'object' + || inbound.decipherInfo === null) { + throw new Error('Invalid inbound.decipherInfo'); + } + + if (!Buffer.isBuffer(inbound.decipherKey) + || inbound.decipherKey.length !== inbound.decipherInfo.keyLen) { + throw new Error('Invalid inbound.decipherKey'); + } + + if (inbound.decipherInfo.ivLen + && (!Buffer.isBuffer(inbound.decipherIV) + || inbound.decipherIV.length !== inbound.decipherInfo.ivLen)) { + throw new Error('Invalid inbound.decipherIV'); + } + + if (typeof inbound.seqno !== 'number' + || inbound.seqno < 0 + || inbound.seqno > MAX_SEQNO) { + throw new Error('Invalid inbound.seqno'); + } + + const forceNative = !!inbound.forceNative; + + switch (inbound.decipherInfo.sslName) { + case 'aes-128-gcm': + case 'aes-256-gcm': + return (AESGCMDecipher && !forceNative + ? new AESGCMDecipherBinding(config) + : new AESGCMDecipherNative(config)); + case 'chacha20': + return (ChaChaPolyDecipher && !forceNative + ? new ChaChaPolyDecipherBinding(config) + : new ChaChaPolyDecipherNative(config)); + default: { + if (typeof inbound.macInfo !== 'object' || inbound.macInfo === null) + throw new Error('Invalid inbound.macInfo'); + if (!Buffer.isBuffer(inbound.macKey) + || inbound.macKey.length !== inbound.macInfo.len) { + throw new Error('Invalid inbound.macKey'); + } + return (GenericDecipher && !forceNative + ? new GenericDecipherBinding(config) + : new GenericDecipherNative(config)); + } + } +} + +module.exports = { + CIPHER_INFO, + MAC_INFO, + bindingAvailable: !!binding, + + NullCipher, + createCipher, + NullDecipher, + createDecipher, +}; diff --git a/lib/protocol/crypto/binding.gyp b/lib/protocol/crypto/binding.gyp new file mode 100644 index 00000000..29c32827 --- /dev/null +++ b/lib/protocol/crypto/binding.gyp @@ -0,0 +1,14 @@ +{ + 'targets': [ + { + 'target_name': 'sshcrypto', + 'include_dirs': [ + "=l){var na=f.charCodeAt(++u);l=65536+((l&1023)<<10)|na&1023}if(127>=l){if(h>=m)break;g[h++]=l}else{if(2047>=l){if(h+1>=m)break;g[h++]=192|l>>6}else{if(65535>=l){if(h+2>=m)break;g[h++]=224|l>>12}else{if(h+3>=m)break;g[h++]=240|l>>18;g[h++]=128|l>>12&63}g[h++]=128|l>>6&63}g[h++]=128|l&63}}g[h]= +0}}return n},array:function(f){var n=N(f.length);P.set(f,n);return n}},p=M(a),A=[];a=0;if(e)for(var r=0;r=m);)++h;if(16g?m+=String.fromCharCode(g):(g-=65536,m+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else m+=String.fromCharCode(g)}f=m}}else f="";else f="boolean"===c?!!f:f;return f}(d);0!==a&&ha(a);return d}var fa="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0;"undefined"!==typeof TextDecoder&&new TextDecoder("utf-16le");var Q,P,O,ia; +function ja(a){Q=a;b.HEAP8=P=new Int8Array(a);b.HEAP16=new Int16Array(a);b.HEAP32=ia=new Int32Array(a);b.HEAPU8=O=new Uint8Array(a);b.HEAPU16=new Uint16Array(a);b.HEAPU32=new Uint32Array(a);b.HEAPF32=new Float32Array(a);b.HEAPF64=new Float64Array(a)}var ka=b.INITIAL_MEMORY||16777216;b.wasmMemory?K=b.wasmMemory:K=new WebAssembly.Memory({initial:ka/65536,maximum:32768});K&&(Q=K.buffer);ka=Q.byteLength;ja(Q);ia[384]=5244576; +function R(a){for(;0>4; +k=(k&15)<<4|p>>2;var r=(p&3)<<6|A;c+=String.fromCharCode(e);64!==p&&(c+=String.fromCharCode(k));64!==A&&(c+=String.fromCharCode(r))}while(d=d;d*=2){var e=c*(1+.2/d);e=Math.min(e,a+100663296);e=Math.max(16777216,a,e);0>>16);ja(K.buffer);var k=1;break a}catch(p){}k=void 0}if(k)return!0}return!1},memory:K,table:ca},X=function(){function a(d){b.asm=d.exports;S--;b.monitorRunDependencies&&b.monitorRunDependencies(S);0==S&&(null!==T&&(clearInterval(T),T=null),U&&(d=U,U=null,d()))} +var c={a:wa};S++;b.monitorRunDependencies&&b.monitorRunDependencies(S);if(b.instantiateWasm)try{return b.instantiateWasm(c,a)}catch(d){return I("Module.instantiateWasm callback failed with error: "+d),!1}(function(){try{var d=sa();var e=new WebAssembly.Module(d);var k=new WebAssembly.Instance(e,c)}catch(p){throw k=p.toString(),I("failed to compile wasm module: "+k),(0<=k.indexOf("imported Memory")||0<=k.indexOf("memory import"))&&I("Memory size incompatibility issues may be due to changing INITIAL_MEMORY at runtime to something too large. Use ALLOW_MEMORY_GROWTH to allow any size memory (and also make sure not to set INITIAL_MEMORY at runtime to something smaller than it was at compile time)."), +p;}a(k,e)})();return b.asm}(),ta=b.___wasm_call_ctors=X.b;b._poly1305_auth=X.c;b._malloc=X.d;b._free=X.e;var ea=b.stackSave=X.f,N=b.stackAlloc=X.g,ha=b.stackRestore=X.h;b.asm=X;b.cwrap=function(a,c,d,e){d=d||[];var k=d.every(function(p){return"number"===p});return"string"!==c&&k&&!e?M(a):function(){return da(a,c,d,arguments)}};var Y;U=function xa(){Y||Z();Y||(U=xa)}; +function Z(){function a(){if(!Y&&(Y=!0,b.calledRun=!0,!L)){R(ma);R(oa);if(b.onRuntimeInitialized)b.onRuntimeInitialized();if(b.postRun)for("function"==typeof b.postRun&&(b.postRun=[b.postRun]);b.postRun.length;){var c=b.postRun.shift();pa.unshift(c)}R(pa)}}if(!(0 +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace node; +using namespace v8; +using namespace std; + +//~ int hexdump(FILE *fd, void const *data, size_t length, int linelen, int split) +//~ { + //~ char buffer[512]; + //~ char *ptr; + //~ const void *inptr; + //~ int pos; + //~ int remaining = length; + + //~ inptr = data; + + //~ /* + //~ * Assert that the buffer is large enough. This should pretty much + //~ * always be the case... + //~ * + //~ * hex/ascii gap (2 chars) + closing \0 (1 char) + //~ * split = 4 chars (2 each for hex/ascii) * number of splits + //~ * + //~ * (hex = 3 chars, ascii = 1 char) * linelen number of chars + //~ */ + //~ assert(sizeof(buffer) >= (3 + (4 * (linelen / split)) + (linelen * 4))); + + //~ /* + //~ * Loop through each line remaining + //~ */ + //~ while (remaining > 0) { + //~ int lrem; + //~ int splitcount; + //~ ptr = buffer; + + //~ /* + //~ * Loop through the hex chars of this line + //~ */ + //~ lrem = remaining; + //~ splitcount = 0; + //~ for (pos = 0; pos < linelen; pos++) { + + //~ /* Split hex section if required */ + //~ if (split == splitcount++) { + //~ sprintf(ptr, " "); + //~ ptr += 2; + //~ splitcount = 1; + //~ } + + //~ /* If still remaining chars, output, else leave a space */ + //~ if (lrem) { + //~ sprintf(ptr, "%0.2x ", *((unsigned char *) inptr + pos)); + //~ lrem--; + //~ } else { + //~ sprintf(ptr, " "); + //~ } + //~ ptr += 3; + //~ } + + //~ *ptr++ = ' '; + //~ *ptr++ = ' '; + + //~ /* + //~ * Loop through the ASCII chars of this line + //~ */ + //~ lrem = remaining; + //~ splitcount = 0; + //~ for (pos = 0; pos < linelen; pos++) { + //~ unsigned char c; + + //~ /* Split ASCII section if required */ + //~ if (split == splitcount++) { + //~ sprintf(ptr, " "); + //~ ptr += 2; + //~ splitcount = 1; + //~ } + + //~ if (lrem) { + //~ c = *((unsigned char *) inptr + pos); + //~ if (c > 31 && c < 127) { + //~ sprintf(ptr, "%c", c); + //~ } else { + //~ sprintf(ptr, "."); + //~ } + //~ lrem--; + //~ /* + //~ * These two lines would pad out the last line with spaces + //~ * which seems a bit pointless generally. + //~ */ + //~ /* + //~ } else { + //~ sprintf(ptr, " "); + //~ */ + + //~ } + //~ ptr++; + //~ } + + //~ *ptr = '\0'; + //~ fprintf(fd, "%s\n", buffer); + + //~ inptr += linelen; + //~ remaining -= linelen; + //~ } + + //~ return 0; +//~ } + +struct MarkPopErrorOnReturn { + MarkPopErrorOnReturn() { ERR_set_mark(); } + ~MarkPopErrorOnReturn() { ERR_pop_to_mark(); } +}; + +enum ErrorType { + kErrNone, + kErrOpenSSL, + kErrBadIVLen, + kErrBadKeyLen, + kErrAADFailure, + kErrTagFailure, + kErrPartialEncrypt, + kErrBadCipherName, + kErrBadHMACName, + kErrBadHMACLen, + kErrBadInit, + kErrPartialDecrypt, + kErrInvalidMAC, + kErrBadBlockLen +}; + +#define POLY1305_KEYLEN 32 +#define POLY1305_TAGLEN 16 +class ChaChaPolyCipher : public ObjectWrap { + public: + static NAN_MODULE_INIT(Init) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("ChaChaPolyCipher").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + SetPrototypeMethod(tpl, "encrypt", Encrypt); + SetPrototypeMethod(tpl, "free", Free); + + constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked()); + + Nan::Set(target, + Nan::New("ChaChaPolyCipher").ToLocalChecked(), + Nan::GetFunction(tpl).ToLocalChecked()); + } + + private: + explicit ChaChaPolyCipher() + : ctx_main_(nullptr), + ctx_pktlen_(nullptr), + md_ctx_(nullptr), + polykey_(nullptr) {} + + ~ChaChaPolyCipher() { + clear(); + } + + void clear() { + if (ctx_pktlen_) { + EVP_CIPHER_CTX_cleanup(ctx_pktlen_); + EVP_CIPHER_CTX_free(ctx_pktlen_); + ctx_pktlen_ = nullptr; + } + if (ctx_main_) { + EVP_CIPHER_CTX_cleanup(ctx_main_); + EVP_CIPHER_CTX_free(ctx_main_); + ctx_main_ = nullptr; + } + if (polykey_) { + EVP_PKEY_free(polykey_); + polykey_ = nullptr; + } + if (md_ctx_) { + EVP_MD_CTX_free(md_ctx_); + md_ctx_ = nullptr; + } + // `polykey_ctx_` is not explicitly freed as it is freed implicitly when + // `md_ctx_` is freed + } + + ErrorType init(unsigned char* keys, size_t keys_len) { + ErrorType r = kErrNone; + + if (keys_len != 64) { + r = kErrBadKeyLen; + goto out; + } + + if ((ctx_pktlen_ = EVP_CIPHER_CTX_new()) == nullptr + || (ctx_main_ = EVP_CIPHER_CTX_new()) == nullptr + || (md_ctx_ = EVP_MD_CTX_new()) == nullptr + || EVP_EncryptInit_ex(ctx_pktlen_, + EVP_chacha20(), + nullptr, + keys + 32, + nullptr) != 1 + || EVP_EncryptInit_ex(ctx_main_, + EVP_chacha20(), + nullptr, + keys, + nullptr) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_CIPHER_CTX_iv_length(ctx_pktlen_) != 16) { + r = kErrBadIVLen; + goto out; + } + +out: + if (r != kErrNone) + clear(); + return r; + } + + ErrorType encrypt(unsigned char* packet, + uint32_t packet_len, + uint32_t seqno) { + ErrorType r = kErrNone; + size_t sig_len = 16; + int outlen = 0; + + // `packet` layout: + // + uint32_t data_len = packet_len - POLY1305_TAGLEN; + + unsigned char polykey[POLY1305_KEYLEN] = {0}; + + uint8_t seqbuf[16] = {0}; + ((uint8_t*)(seqbuf))[12] = (seqno >> 24) & 0xff; + ((uint8_t*)(seqbuf))[13] = (seqno >> 16) & 0xff; + ((uint8_t*)(seqbuf))[14] = (seqno >> 8) & 0xff; + ((uint8_t*)(seqbuf))[15] = seqno & 0xff; + + // Generate Poly1305 key + if (EVP_EncryptInit_ex(ctx_main_, nullptr, nullptr, nullptr, seqbuf) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_EncryptUpdate(ctx_main_, + polykey, + &outlen, + polykey, + sizeof(polykey)) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != sizeof(polykey)) { + r = kErrPartialEncrypt; + goto out; + } + + // Encrypt packet length + if (EVP_EncryptInit_ex(ctx_pktlen_, + nullptr, + nullptr, + nullptr, + seqbuf) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_EncryptUpdate(ctx_pktlen_, packet, &outlen, packet, 4) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != 4) { + r = kErrPartialEncrypt; + goto out; + } + + // Encrypt rest of packet + seqbuf[0] = 1; + if (EVP_EncryptInit_ex(ctx_main_, nullptr, nullptr, nullptr, seqbuf) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_EncryptUpdate(ctx_main_, + packet + 4, + &outlen, + packet + 4, + data_len - 4) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != data_len - 4) { + r = kErrPartialEncrypt; + goto out; + } + + // Poly1305 over ciphertext + if (polykey_) { + if (EVP_PKEY_CTX_ctrl(polykey_ctx_, + -1, + EVP_PKEY_OP_SIGNCTX, + EVP_PKEY_CTRL_SET_MAC_KEY, + sizeof(polykey), + (void*)polykey) <= 0) { + r = kErrOpenSSL; + goto out; + } + } else { + polykey_ = EVP_PKEY_new_raw_private_key(EVP_PKEY_POLY1305, + nullptr, + polykey, + sizeof(polykey)); + if (polykey_ == nullptr) { + r = kErrOpenSSL; + goto out; + } + + if (!EVP_DigestSignInit(md_ctx_, + &polykey_ctx_, + nullptr, + nullptr, + polykey_)) { + r = kErrOpenSSL; + goto out; + } + } + + // Generate and write Poly1305 tag + if (EVP_DigestSign(md_ctx_, + packet + data_len, + &sig_len, + packet, + data_len) != 1) { + r = kErrOpenSSL; + goto out; + } + + out: + return r; + } + + static NAN_METHOD(New) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid keys"); + + ChaChaPolyCipher* obj = new ChaChaPolyCipher(); + ErrorType r = obj->init( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]) + ); + if (r != kErrNone) { + delete obj; + switch (r) { + case kErrBadKeyLen: + return Nan::ThrowError("Invalid keys length"); + case kErrBadIVLen: + return Nan::ThrowError("Invalid IV length"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown init failure"); + } + } + + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } + + static NAN_METHOD(Encrypt) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + ChaChaPolyCipher* obj = ObjectWrap::Unwrap(info.Holder()); + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid packet"); + + if (!info[1]->IsUint32()) + return Nan::ThrowTypeError("Missing/Invalid sequence number"); + + ErrorType r = obj->encrypt( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]), + Nan::To(info[1]).FromJust() + ); + switch (r) { + case kErrNone: + return; + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown encrypt failure"); + } + } + + static NAN_METHOD(Free) { + ChaChaPolyCipher* obj = ObjectWrap::Unwrap(info.Holder()); + obj->clear(); + } + + static inline Nan::Persistent & constructor() { + static Nan::Persistent my_constructor; + return my_constructor; + } + + EVP_CIPHER_CTX* ctx_main_; + EVP_CIPHER_CTX* ctx_pktlen_; + EVP_MD_CTX* md_ctx_; + EVP_PKEY* polykey_; + EVP_PKEY_CTX* polykey_ctx_; +}; + +class AESGCMCipher : public ObjectWrap { + public: + static NAN_MODULE_INIT(Init) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("AESGCMCipher").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + SetPrototypeMethod(tpl, "encrypt", Encrypt); + SetPrototypeMethod(tpl, "free", Free); + + constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked()); + + Nan::Set(target, + Nan::New("AESGCMCipher").ToLocalChecked(), + Nan::GetFunction(tpl).ToLocalChecked()); + } + + private: + explicit AESGCMCipher() : ctx_(nullptr) {} + + ~AESGCMCipher() { + clear(); + } + + void clear() { + if (ctx_) { + EVP_CIPHER_CTX_cleanup(ctx_); + EVP_CIPHER_CTX_free(ctx_); + ctx_ = nullptr; + } + } + + ErrorType init(const char* name, + unsigned char* key, + size_t key_len, + unsigned char* iv, + size_t iv_len) { + ErrorType r = kErrNone; + + const EVP_CIPHER* const cipher = EVP_get_cipherbyname(name); + if (cipher == nullptr) { + r = kErrOpenSSL; + goto out; + } + + if (cipher != EVP_aes_128_gcm() && cipher != EVP_aes_256_gcm()) { + r = kErrBadCipherName; + goto out; + } + + if ((ctx_ = EVP_CIPHER_CTX_new()) == nullptr + || EVP_EncryptInit_ex(ctx_, cipher, nullptr, nullptr, nullptr) != 1) { + r = kErrOpenSSL; + goto out; + } + + if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_AEAD_SET_IVLEN, iv_len, nullptr)) { + r = kErrOpenSSL; + goto out; + } + + //~ if (iv_len != static_cast(EVP_CIPHER_CTX_iv_length(ctx_))) { + //~ r = kErrBadIVLen; + //~ goto out; + //~ } + + if (key_len != static_cast(EVP_CIPHER_CTX_key_length(ctx_))) { + if (!EVP_CIPHER_CTX_set_key_length(ctx_, key_len)) { + r = kErrBadKeyLen; + goto out; + } + } + + // Set key and IV + if (EVP_EncryptInit_ex(ctx_, nullptr, nullptr, key, iv) != 1) { + r = kErrOpenSSL; + goto out; + } + if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_SET_IV_FIXED, -1, iv)) { + r = kErrOpenSSL; + goto out; + } + + // Disable padding + EVP_CIPHER_CTX_set_padding(ctx_, 0); + +out: + if (r != kErrNone) + clear(); + return r; + } + + ErrorType encrypt(unsigned char* packet, uint32_t packet_len) { + ErrorType r = kErrNone; + + // `packet` layout: + // + uint32_t data_len = packet_len - 16; + + int outlen = 0; + + // Increment IV + unsigned char lastiv[1]; + if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_IV_GEN, 1, lastiv)) { + r = kErrOpenSSL; + goto out; + } + + // Set AAD (the packet length) + if (!EVP_EncryptUpdate(ctx_, nullptr, &outlen, packet, 4)) { + r = kErrOpenSSL; + goto out; + } + if (outlen != 4) { + r = kErrAADFailure; + goto out; + } + + // Encrypt everything but the packet length + if (EVP_EncryptUpdate(ctx_, + packet + 4, + &outlen, + packet + 4, + data_len - 4) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != data_len - 4) { + r = kErrPartialEncrypt; + goto out; + } + + // Generate authentication tag + if (!EVP_EncryptFinal_ex(ctx_, nullptr, &outlen)) { + r = kErrOpenSSL; + goto out; + } + + // Write authentication tag + if (EVP_CIPHER_CTX_ctrl(ctx_, + EVP_CTRL_AEAD_GET_TAG, + 16, + packet + data_len) != 1) { + r = kErrOpenSSL; + goto out; + } + +out: + return r; + } + + static NAN_METHOD(New) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (!info[0]->IsString()) + return Nan::ThrowTypeError("Missing/Invalid OpenSSL cipher name"); + + if (!Buffer::HasInstance(info[1])) + return Nan::ThrowTypeError("Missing/Invalid key"); + + if (!Buffer::HasInstance(info[2])) + return Nan::ThrowTypeError("Missing/Invalid iv"); + + const Nan::Utf8String cipher_name(info[0]); + + AESGCMCipher* obj = new AESGCMCipher(); + ErrorType r = obj->init( + *cipher_name, + reinterpret_cast(Buffer::Data(info[1])), + Buffer::Length(info[1]), + reinterpret_cast(Buffer::Data(info[2])), + Buffer::Length(info[2]) + ); + if (r != kErrNone) { + delete obj; + switch (r) { + case kErrBadKeyLen: + return Nan::ThrowError("Invalid keys length"); + case kErrBadIVLen: + return Nan::ThrowError("Invalid IV length"); + case kErrBadCipherName: + return Nan::ThrowError("Invalid AES GCM cipher name"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown init failure"); + } + } + + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } + + static NAN_METHOD(Encrypt) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + AESGCMCipher* obj = ObjectWrap::Unwrap(info.Holder()); + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid packet"); + + ErrorType r = obj->encrypt( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]) + ); + switch (r) { + case kErrNone: + return; + case kErrAADFailure: + return Nan::ThrowError("Error setting AAD"); + case kErrPartialEncrypt: + return Nan::ThrowError("Failed to completely encrypt packet"); + case kErrTagFailure: + return Nan::ThrowError("Error generating authentication tag"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown encrypt failure"); + } + } + + static NAN_METHOD(Free) { + AESGCMCipher* obj = ObjectWrap::Unwrap(info.Holder()); + obj->clear(); + } + + static inline Nan::Persistent & constructor() { + static Nan::Persistent my_constructor; + return my_constructor; + } + + EVP_CIPHER_CTX* ctx_; +}; + +class GenericCipher : public ObjectWrap { + public: + static NAN_MODULE_INIT(Init) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("GenericCipher").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + SetPrototypeMethod(tpl, "encrypt", Encrypt); + SetPrototypeMethod(tpl, "free", Free); + + constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked()); + + Nan::Set(target, + Nan::New("GenericCipher").ToLocalChecked(), + Nan::GetFunction(tpl).ToLocalChecked()); + } + + private: + explicit GenericCipher() + : ctx_(nullptr), + ctx_hmac_(nullptr), + hmac_len_(0), + is_etm_(0) {} + + ~GenericCipher() { + clear(); + } + + void clear() { + if (ctx_) { + EVP_CIPHER_CTX_cleanup(ctx_); + EVP_CIPHER_CTX_free(ctx_); + ctx_ = nullptr; + } + if (ctx_hmac_) { + HMAC_CTX_free(ctx_hmac_); + ctx_hmac_ = nullptr; + } + } + + ErrorType init(const char* name, + unsigned char* key, + size_t key_len, + unsigned char* iv, + size_t iv_len, + const char* hmac_name, + unsigned char* hmac_key, + size_t hmac_key_len, + int is_etm) { + ErrorType r = kErrNone; + + const EVP_MD* md; + const EVP_CIPHER* const cipher = EVP_get_cipherbyname(name); + if (cipher == nullptr) { + r = kErrOpenSSL; + goto out; + } + + if ((ctx_ = EVP_CIPHER_CTX_new()) == nullptr + || EVP_EncryptInit_ex(ctx_, cipher, nullptr, nullptr, nullptr) != 1) { + r = kErrOpenSSL; + goto out; + } + + if (iv_len != static_cast(EVP_CIPHER_CTX_iv_length(ctx_))) { + r = kErrBadIVLen; + goto out; + } + + if (key_len != static_cast(EVP_CIPHER_CTX_key_length(ctx_))) { + if (!EVP_CIPHER_CTX_set_key_length(ctx_, key_len)) { + r = kErrBadKeyLen; + goto out; + } + } + + // Set key and IV + if (EVP_EncryptInit_ex(ctx_, nullptr, nullptr, key, iv) != 1) { + r = kErrOpenSSL; + goto out; + } + + // Disable padding + EVP_CIPHER_CTX_set_padding(ctx_, 0); + + if (cipher == EVP_rc4()) { + /* The "arcfour128" algorithm is the RC4 cipher, as described in + [SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream + generated by the cipher MUST be discarded, and the first byte of the + first encrypted packet MUST be encrypted using the 1537th byte of + keystream. + + -- http://tools.ietf.org/html/rfc4345#section-4 */ + unsigned char zeros[1536] = {0}; + int outlen = sizeof(zeros); + if (EVP_EncryptUpdate(ctx_, + zeros, + &outlen, + zeros, + sizeof(zeros)) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != sizeof(zeros)) { + r = kErrBadInit; + goto out; + } + } + + md = EVP_get_digestbyname(hmac_name); + if (md == nullptr) { + r = kErrBadHMACName; + goto out; + } + + if ((ctx_hmac_ = HMAC_CTX_new()) == nullptr + || HMAC_Init_ex(ctx_hmac_, hmac_key, hmac_key_len, md, nullptr) != 1) { + r = kErrOpenSSL; + goto out; + } + + hmac_len_ = HMAC_size(ctx_hmac_); + is_etm_ = is_etm; + +out: + if (r != kErrNone) + clear(); + return r; + } + + ErrorType encrypt(unsigned char* packet, + uint32_t packet_len, + uint32_t seqno) { + ErrorType r = kErrNone; + + // `packet` layout: + // + uint32_t data_len = packet_len - hmac_len_; + + int outlen; + + uint8_t seqbuf[4] = {0}; + ((uint8_t*)(seqbuf))[0] = (seqno >> 24) & 0xff; + ((uint8_t*)(seqbuf))[1] = (seqno >> 16) & 0xff; + ((uint8_t*)(seqbuf))[2] = (seqno >> 8) & 0xff; + ((uint8_t*)(seqbuf))[3] = seqno & 0xff; + + if (is_etm_) { + // Encrypt everything but packet length + if (EVP_EncryptUpdate(ctx_, + packet + 4, + &outlen, + packet + 4, + data_len - 4) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != data_len - 4) { + r = kErrPartialEncrypt; + goto out; + } + + // HMAC over unencrypted packet length and ciphertext + { + unsigned int outlen = hmac_len_; + if (HMAC_Init_ex(ctx_hmac_, nullptr, 0, nullptr, nullptr) != 1 + || HMAC_Update(ctx_hmac_, seqbuf, sizeof(seqbuf)) != 1 + || HMAC_Update(ctx_hmac_, packet, data_len) != 1 + || HMAC_Final(ctx_hmac_, packet + data_len, &outlen) != 1) { + r = kErrOpenSSL; + goto out; + } + if (outlen != hmac_len_) { + r = kErrBadHMACLen; + goto out; + } + } + } else { + // HMAC over plaintext + { + unsigned int outlen = hmac_len_; + if (HMAC_Init_ex(ctx_hmac_, nullptr, 0, nullptr, nullptr) != 1 + || HMAC_Update(ctx_hmac_, seqbuf, sizeof(seqbuf)) != 1 + || HMAC_Update(ctx_hmac_, packet, data_len) != 1 + || HMAC_Final(ctx_hmac_, packet + data_len, &outlen) != 1) { + r = kErrOpenSSL; + goto out; + } + if (outlen != hmac_len_) { + r = kErrBadHMACLen; + goto out; + } + } + + // Encrypt packet + if (EVP_EncryptUpdate(ctx_, + packet, + &outlen, + packet, + data_len) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != data_len) { + + r = kErrPartialEncrypt; + goto out; + } + } + +out: + return r; + } + + static NAN_METHOD(New) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (!info[0]->IsString()) + return Nan::ThrowTypeError("Missing/Invalid cipher name"); + + if (!Buffer::HasInstance(info[1])) + return Nan::ThrowTypeError("Missing/Invalid cipher key"); + + if (!Buffer::HasInstance(info[2])) + return Nan::ThrowTypeError("Missing/Invalid cipher IV"); + + if (!info[3]->IsString()) + return Nan::ThrowTypeError("Missing/Invalid HMAC name"); + + if (!Buffer::HasInstance(info[4])) + return Nan::ThrowTypeError("Missing/Invalid HMAC key"); + + if (!info[5]->IsBoolean()) + return Nan::ThrowTypeError("Missing/Invalid HMAC ETM flag"); + + const Nan::Utf8String cipher_name(info[0]); + const Nan::Utf8String mac_name(info[3]); + int is_etm = (Nan::To(info[5]).FromJust() ? 1 : 0); + + GenericCipher* obj = new GenericCipher(); + ErrorType r = obj->init( + *cipher_name, + reinterpret_cast(Buffer::Data(info[1])), + Buffer::Length(info[1]), + reinterpret_cast(Buffer::Data(info[2])), + Buffer::Length(info[2]), + *mac_name, + reinterpret_cast(Buffer::Data(info[4])), + Buffer::Length(info[4]), + is_etm + ); + if (r != kErrNone) { + delete obj; + switch (r) { + case kErrBadKeyLen: + return Nan::ThrowError("Invalid keys length"); + case kErrBadIVLen: + return Nan::ThrowError("Invalid IV length"); + case kErrBadCipherName: + return Nan::ThrowError("Invalid cipher name"); + case kErrBadHMACName: + return Nan::ThrowError("Invalid MAC name"); + case kErrBadInit: + return Nan::ThrowError("Failed to properly initialize cipher"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown init failure"); + } + } + + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } + + static NAN_METHOD(Encrypt) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + GenericCipher* obj = ObjectWrap::Unwrap(info.Holder()); + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid packet"); + + if (!info[1]->IsUint32()) + return Nan::ThrowTypeError("Missing/Invalid sequence number"); + + ErrorType r = obj->encrypt( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]), + Nan::To(info[1]).FromJust() + ); + switch (r) { + case kErrNone: + return; + case kErrPartialEncrypt: + return Nan::ThrowError("Failed to completely encrypt packet"); + case kErrBadHMACLen: + return Nan::ThrowError("Unexpected HMAC length"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown encrypt failure"); + } + } + + static NAN_METHOD(Free) { + GenericCipher* obj = ObjectWrap::Unwrap(info.Holder()); + obj->clear(); + } + + static inline Nan::Persistent & constructor() { + static Nan::Persistent my_constructor; + return my_constructor; + } + + EVP_CIPHER_CTX* ctx_; + HMAC_CTX* ctx_hmac_; + unsigned int hmac_len_; + int is_etm_; +}; + +// ============================================================================= + +class ChaChaPolyDecipher : public ObjectWrap { + public: + static NAN_MODULE_INIT(Init) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("ChaChaPolyDecipher").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + SetPrototypeMethod(tpl, "decrypt", Decrypt); + SetPrototypeMethod(tpl, "decryptLen", DecryptLen); + SetPrototypeMethod(tpl, "free", Free); + + constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked()); + + Nan::Set(target, + Nan::New("ChaChaPolyDecipher").ToLocalChecked(), + Nan::GetFunction(tpl).ToLocalChecked()); + } + + private: + explicit ChaChaPolyDecipher() + : ctx_main_(nullptr), + ctx_pktlen_(nullptr), + md_ctx_(nullptr), + polykey_(nullptr) {} + + ~ChaChaPolyDecipher() { + clear(); + } + + void clear() { + if (ctx_pktlen_) { + EVP_CIPHER_CTX_cleanup(ctx_pktlen_); + EVP_CIPHER_CTX_free(ctx_pktlen_); + ctx_pktlen_ = nullptr; + } + if (ctx_main_) { + EVP_CIPHER_CTX_cleanup(ctx_main_); + EVP_CIPHER_CTX_free(ctx_main_); + ctx_main_ = nullptr; + } + if (polykey_) { + EVP_PKEY_free(polykey_); + polykey_ = nullptr; + } + if (md_ctx_) { + EVP_MD_CTX_free(md_ctx_); + md_ctx_ = nullptr; + } + // `polykey_ctx_` is not explicitly freed as it is freed implicitly when + // `md_ctx_` is freed + } + + ErrorType init(unsigned char* keys, size_t keys_len) { + ErrorType r = kErrNone; + + if (keys_len != 64) { + r = kErrBadKeyLen; + goto out; + } + + if ((ctx_pktlen_ = EVP_CIPHER_CTX_new()) == nullptr + || (ctx_main_ = EVP_CIPHER_CTX_new()) == nullptr + || (md_ctx_ = EVP_MD_CTX_new()) == nullptr + || EVP_DecryptInit_ex(ctx_pktlen_, + EVP_chacha20(), + nullptr, + keys + 32, + nullptr) != 1 + || EVP_DecryptInit_ex(ctx_main_, + EVP_chacha20(), + nullptr, + keys, + nullptr) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_CIPHER_CTX_iv_length(ctx_pktlen_) != 16) { + r = kErrBadIVLen; + goto out; + } + +out: + if (r != kErrNone) + clear(); + return r; + } + + ErrorType decrypt_length(unsigned char* data, + size_t data_len, + uint32_t seqno, + uint32_t* packet_length) { + ErrorType r = kErrNone; + int outlen; + + unsigned char dec_length_bytes[4]; + + uint8_t seqbuf[16] = {0}; + ((uint8_t*)(seqbuf))[12] = (seqno >> 24) & 0xff; + ((uint8_t*)(seqbuf))[13] = (seqno >> 16) & 0xff; + ((uint8_t*)(seqbuf))[14] = (seqno >> 8) & 0xff; + ((uint8_t*)(seqbuf))[15] = seqno & 0xff; + + if (EVP_DecryptInit_ex(ctx_pktlen_, + nullptr, + nullptr, + nullptr, + seqbuf) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_DecryptUpdate(ctx_pktlen_, + dec_length_bytes, + &outlen, + data, + data_len) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != 4) { + r = kErrPartialDecrypt; + goto out; + } + + *packet_length = (uint32_t)dec_length_bytes[0] << 24 + | (uint32_t)dec_length_bytes[1] << 16 + | (uint32_t)dec_length_bytes[2] << 8 + | (uint32_t)dec_length_bytes[3]; + memcpy(length_bytes, data, data_len); +out: + return r; + } + + ErrorType decrypt(unsigned char* packet, + uint32_t packet_len, + unsigned char* mac, + uint32_t seqno) { + ErrorType r = kErrNone; + size_t sig_len = 16; + int outlen = 0; + + // `packet` layout: + // + + unsigned char polykey[POLY1305_KEYLEN] = {0}; + unsigned char calc_mac[POLY1305_TAGLEN] = {0}; + + uint8_t seqbuf[16] = {0}; + ((uint8_t*)(seqbuf))[12] = (seqno >> 24) & 0xff; + ((uint8_t*)(seqbuf))[13] = (seqno >> 16) & 0xff; + ((uint8_t*)(seqbuf))[14] = (seqno >> 8) & 0xff; + ((uint8_t*)(seqbuf))[15] = seqno & 0xff; + + // Generate Poly1305 key + if (EVP_EncryptInit_ex(ctx_main_, nullptr, nullptr, nullptr, seqbuf) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_EncryptUpdate(ctx_main_, + polykey, + &outlen, + polykey, + sizeof(polykey)) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != sizeof(polykey)) { + r = kErrPartialEncrypt; + goto out; + } + + // Poly1305 over ciphertext + if (polykey_) { + if (EVP_PKEY_CTX_ctrl(polykey_ctx_, + -1, + EVP_PKEY_OP_SIGNCTX, + EVP_PKEY_CTRL_SET_MAC_KEY, + sizeof(polykey), + (void*)polykey) <= 0) { + r = kErrOpenSSL; + goto out; + } + } else { + polykey_ = EVP_PKEY_new_raw_private_key(EVP_PKEY_POLY1305, + nullptr, + polykey, + sizeof(polykey)); + if (polykey_ == nullptr) { + r = kErrOpenSSL; + goto out; + } + + if (!EVP_DigestSignInit(md_ctx_, + &polykey_ctx_, + nullptr, + nullptr, + polykey_)) { + r = kErrOpenSSL; + goto out; + } + } + if (EVP_DigestSignUpdate(md_ctx_, + length_bytes, + sizeof(length_bytes)) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_DigestSignUpdate(md_ctx_, packet, packet_len) != 1) { + r = kErrOpenSSL; + goto out; + } + + // Generate Poly1305 MAC + if (EVP_DigestSignFinal(md_ctx_, calc_mac, &sig_len) != 1) { + r = kErrOpenSSL; + goto out; + } + + // Compare MACs + if (CRYPTO_memcmp(mac, calc_mac, sizeof(calc_mac))) { + r = kErrInvalidMAC; + goto out; + } + + // Decrypt packet + seqbuf[0] = 1; + if (EVP_DecryptInit_ex(ctx_main_, nullptr, nullptr, nullptr, seqbuf) != 1) { + r = kErrOpenSSL; + goto out; + } + if (EVP_DecryptUpdate(ctx_main_, + packet, + &outlen, + packet, + packet_len) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != packet_len) { + r = kErrPartialDecrypt; + goto out; + } + + out: + return r; + } + + static NAN_METHOD(New) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid keys"); + + ChaChaPolyDecipher* obj = new ChaChaPolyDecipher(); + ErrorType r = obj->init( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]) + ); + if (r != kErrNone) { + delete obj; + switch (r) { + case kErrBadKeyLen: + return Nan::ThrowError("Invalid keys length"); + case kErrBadIVLen: + return Nan::ThrowError("Invalid IV length"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown init failure"); + } + } + + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } + + static NAN_METHOD(DecryptLen) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + ChaChaPolyDecipher* obj = + ObjectWrap::Unwrap(info.Holder()); + + if (!Buffer::HasInstance(info[0]) || Buffer::Length(info[0]) != 4) + return Nan::ThrowTypeError("Missing/Invalid length bytes"); + + if (!info[1]->IsUint32()) + return Nan::ThrowTypeError("Missing/Invalid sequence number"); + + unsigned char* length_bytes = + reinterpret_cast(Buffer::Data(info[0])); + + uint32_t dec_packet_length; + ErrorType r = obj->decrypt_length( + length_bytes, + Buffer::Length(info[0]), + Nan::To(info[1]).FromJust(), + &dec_packet_length + ); + + switch (r) { + case kErrNone: + return info.GetReturnValue().Set(dec_packet_length); + case kErrPartialDecrypt: + return Nan::ThrowError("Failed to completely decrypt packet length"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown decrypt failure"); + } + } + + static NAN_METHOD(Decrypt) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + ChaChaPolyDecipher* obj = + ObjectWrap::Unwrap(info.Holder()); + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid packet"); + + if (!Buffer::HasInstance(info[1]) + || Buffer::Length(info[1]) != POLY1305_TAGLEN) { + return Nan::ThrowTypeError("Missing/Invalid mac"); + } + + if (!info[2]->IsUint32()) + return Nan::ThrowTypeError("Missing/Invalid sequence number"); + + ErrorType r = obj->decrypt( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]), + reinterpret_cast(Buffer::Data(info[1])), + Nan::To(info[2]).FromJust() + ); + + switch (r) { + case kErrNone: + return; + case kErrInvalidMAC: + return Nan::ThrowError("Invalid MAC"); + case kErrPartialDecrypt: + return Nan::ThrowError("Failed to completely decrypt packet length"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown decrypt failure"); + } + } + + static NAN_METHOD(Free) { + ChaChaPolyDecipher* obj = + ObjectWrap::Unwrap(info.Holder()); + obj->clear(); + } + + static inline Nan::Persistent & constructor() { + static Nan::Persistent my_constructor; + return my_constructor; + } + + unsigned char length_bytes[4]; + EVP_CIPHER_CTX* ctx_main_; + EVP_CIPHER_CTX* ctx_pktlen_; + EVP_MD_CTX* md_ctx_; + EVP_PKEY* polykey_; + EVP_PKEY_CTX* polykey_ctx_; +}; + +class AESGCMDecipher : public ObjectWrap { + public: + static NAN_MODULE_INIT(Init) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("AESGCMDecipher").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + SetPrototypeMethod(tpl, "decrypt", Decrypt); + SetPrototypeMethod(tpl, "free", Free); + + constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked()); + + Nan::Set(target, + Nan::New("AESGCMDecipher").ToLocalChecked(), + Nan::GetFunction(tpl).ToLocalChecked()); + } + + private: + explicit AESGCMDecipher() : ctx_(nullptr) {} + + ~AESGCMDecipher() { + clear(); + } + + void clear() { + if (ctx_) { + EVP_CIPHER_CTX_cleanup(ctx_); + EVP_CIPHER_CTX_free(ctx_); + ctx_ = nullptr; + } + } + + ErrorType init(const char* name, + unsigned char* key, + size_t key_len, + unsigned char* iv, + size_t iv_len) { + ErrorType r = kErrNone; + + const EVP_CIPHER* const cipher = EVP_get_cipherbyname(name); + if (cipher == nullptr) { + r = kErrOpenSSL; + goto out; + } + + if (cipher != EVP_aes_128_gcm() && cipher != EVP_aes_256_gcm()) { + r = kErrBadCipherName; + goto out; + } + + if ((ctx_ = EVP_CIPHER_CTX_new()) == nullptr + || EVP_DecryptInit_ex(ctx_, cipher, nullptr, nullptr, nullptr) != 1) { + r = kErrOpenSSL; + goto out; + } + + if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_AEAD_SET_IVLEN, iv_len, nullptr)) { + r = kErrOpenSSL; + goto out; + } + + //~ if (iv_len != static_cast(EVP_CIPHER_CTX_iv_length(ctx_))) { + //~ r = kErrBadIVLen; + //~ goto out; + //~ } + + if (key_len != static_cast(EVP_CIPHER_CTX_key_length(ctx_))) { + if (!EVP_CIPHER_CTX_set_key_length(ctx_, key_len)) { + r = kErrBadKeyLen; + goto out; + } + } + + // Set key and IV + if (EVP_DecryptInit_ex(ctx_, nullptr, nullptr, key, iv) != 1) { + r = kErrOpenSSL; + goto out; + } + if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_SET_IV_FIXED, -1, iv)) { + r = kErrOpenSSL; + goto out; + } + + // Disable padding + EVP_CIPHER_CTX_set_padding(ctx_, 0); + +out: + if (r != kErrNone) + clear(); + return r; + } + + ErrorType decrypt(unsigned char* packet, + uint32_t packet_len, + unsigned char* length_bytes, + unsigned char* tag) { + ErrorType r = kErrNone; + + // `packet` layout: + // + + int outlen; + + // Increment IV + unsigned char lastiv[1]; + if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_IV_GEN, 1, lastiv)) { + r = kErrOpenSSL; + goto out; + } + + // Set AAD (the packet length) + if (!EVP_DecryptUpdate(ctx_, nullptr, &outlen, length_bytes, 4)) { + r = kErrOpenSSL; + goto out; + } + if (outlen != 4) { + r = kErrAADFailure; + goto out; + } + + // Decrypt everything but the packet length + if (EVP_DecryptUpdate(ctx_, packet, &outlen, packet, packet_len) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != packet_len) { + r = kErrPartialDecrypt; + goto out; + } + + // Set authentication tag + if (EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_AEAD_SET_TAG, 16, tag) != 1) { + r = kErrOpenSSL; + goto out; + } + + // Verify authentication tag + if (!EVP_DecryptFinal_ex(ctx_, nullptr, &outlen)) { + r = kErrOpenSSL; + goto out; + } + +out: + return r; + } + + static NAN_METHOD(New) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (!info[0]->IsString()) + return Nan::ThrowTypeError("Missing/Invalid OpenSSL cipher name"); + + if (!Buffer::HasInstance(info[1])) + return Nan::ThrowTypeError("Missing/Invalid key"); + + if (!Buffer::HasInstance(info[2])) + return Nan::ThrowTypeError("Missing/Invalid iv"); + + const Nan::Utf8String cipher_name(info[0]); + + AESGCMDecipher* obj = new AESGCMDecipher(); + ErrorType r = obj->init( + *cipher_name, + reinterpret_cast(Buffer::Data(info[1])), + Buffer::Length(info[1]), + reinterpret_cast(Buffer::Data(info[2])), + Buffer::Length(info[2]) + ); + if (r != kErrNone) { + delete obj; + switch (r) { + case kErrBadKeyLen: + return Nan::ThrowError("Invalid keys length"); + case kErrBadIVLen: + return Nan::ThrowError("Invalid IV length"); + case kErrBadCipherName: + return Nan::ThrowError("Invalid AES GCM cipher name"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown init failure"); + } + } + + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } + + static NAN_METHOD(Decrypt) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + AESGCMDecipher* obj = ObjectWrap::Unwrap(info.Holder()); + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid packet"); + + if (!info[1]->IsUint32()) + return Nan::ThrowTypeError("Missing/Invalid length"); + + if (!Buffer::HasInstance(info[2]) || Buffer::Length(info[2]) != 16) + return Nan::ThrowTypeError("Missing/Invalid tag"); + + uint32_t length = Nan::To(info[1]).FromJust(); + unsigned char length_bytes[4]; + length_bytes[0] = (length >> 24) & 0xFF; + length_bytes[1] = (length >> 16) & 0xFF; + length_bytes[2] = (length >> 8) & 0xFF; + length_bytes[3] = length & 0xFF; + + ErrorType r = obj->decrypt( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]), + length_bytes, + reinterpret_cast(Buffer::Data(info[2])) + ); + switch (r) { + case kErrNone: + return; + case kErrAADFailure: + return Nan::ThrowError("Error setting AAD"); + case kErrPartialDecrypt: + return Nan::ThrowError("Failed to completely decrypt packet"); + case kErrTagFailure: + return Nan::ThrowError("Error generating authentication tag"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown decrypt failure"); + } + } + + static NAN_METHOD(Free) { + AESGCMDecipher* obj = ObjectWrap::Unwrap(info.Holder()); + obj->clear(); + } + + static inline Nan::Persistent & constructor() { + static Nan::Persistent my_constructor; + return my_constructor; + } + + EVP_CIPHER_CTX* ctx_; +}; + +class GenericDecipher : public ObjectWrap { + public: + static NAN_MODULE_INIT(Init) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("GenericDecipher").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + SetPrototypeMethod(tpl, "decryptBlock", DecryptBlock); + SetPrototypeMethod(tpl, "decrypt", Decrypt); + SetPrototypeMethod(tpl, "free", Free); + + constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked()); + + Nan::Set(target, + Nan::New("GenericDecipher").ToLocalChecked(), + Nan::GetFunction(tpl).ToLocalChecked()); + } + + private: + explicit GenericDecipher() + : ctx_(nullptr), + ctx_hmac_(nullptr), + hmac_len_(0), + is_etm_(0) {} + + ~GenericDecipher() { + clear(); + } + + void clear() { + if (ctx_) { + EVP_CIPHER_CTX_cleanup(ctx_); + EVP_CIPHER_CTX_free(ctx_); + ctx_ = nullptr; + } + if (ctx_hmac_) { + HMAC_CTX_free(ctx_hmac_); + ctx_hmac_ = nullptr; + } + } + + ErrorType init(const char* name, + unsigned char* key, + size_t key_len, + unsigned char* iv, + size_t iv_len, + const char* hmac_name, + unsigned char* hmac_key, + size_t hmac_key_len, + int is_etm, + size_t hmac_actual_len) { + ErrorType r = kErrNone; + + const EVP_MD* md; + const EVP_CIPHER* const cipher = EVP_get_cipherbyname(name); + if (cipher == nullptr) { + r = kErrOpenSSL; + goto out; + } + + if ((ctx_ = EVP_CIPHER_CTX_new()) == nullptr + || EVP_DecryptInit_ex(ctx_, cipher, nullptr, nullptr, nullptr) != 1) { + r = kErrOpenSSL; + goto out; + } + + if (iv_len != static_cast(EVP_CIPHER_CTX_iv_length(ctx_))) { + r = kErrBadIVLen; + goto out; + } + + if (key_len != static_cast(EVP_CIPHER_CTX_key_length(ctx_))) { + if (!EVP_CIPHER_CTX_set_key_length(ctx_, key_len)) { + r = kErrBadKeyLen; + goto out; + } + } + + // Set key and IV + if (EVP_DecryptInit_ex(ctx_, nullptr, nullptr, key, iv) != 1) { + r = kErrOpenSSL; + goto out; + } + + // Disable padding + EVP_CIPHER_CTX_set_padding(ctx_, 0); + + if (cipher == EVP_rc4()) { + /* The "arcfour128" algorithm is the RC4 cipher, as described in + [SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream + generated by the cipher MUST be discarded, and the first byte of the + first encrypted packet MUST be encrypted using the 1537th byte of + keystream. + + -- http://tools.ietf.org/html/rfc4345#section-4 */ + unsigned char zeros[1536] = {0}; + int outlen = sizeof(zeros); + if (EVP_DecryptUpdate(ctx_, + zeros, + &outlen, + zeros, + sizeof(zeros)) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != sizeof(zeros)) { + r = kErrBadInit; + goto out; + } + } + + md = EVP_get_digestbyname(hmac_name); + if (md == nullptr) { + r = kErrBadHMACName; + goto out; + } + + if ((ctx_hmac_ = HMAC_CTX_new()) == nullptr + || HMAC_Init_ex(ctx_hmac_, hmac_key, hmac_key_len, md, nullptr) != 1) { + r = kErrOpenSSL; + goto out; + } + + hmac_len_ = HMAC_size(ctx_hmac_); + hmac_actual_len_ = hmac_actual_len; + is_etm_ = is_etm; + switch (EVP_CIPHER_CTX_mode(ctx_)) { + case EVP_CIPH_STREAM_CIPHER: + case EVP_CIPH_CTR_MODE: + is_stream_ = 1; + break; + default: + is_stream_ = 0; + } + block_size_ = EVP_CIPHER_CTX_block_size(ctx_); + +out: + if (r != kErrNone) + clear(); + return r; + } + + ErrorType decrypt_block(unsigned char* data, + uint32_t data_len, + uint32_t seqno) { + ErrorType r = kErrNone; + + int outlen; + + uint8_t seqbuf[4] = {0}; + ((uint8_t*)(seqbuf))[0] = (seqno >> 24) & 0xff; + ((uint8_t*)(seqbuf))[1] = (seqno >> 16) & 0xff; + ((uint8_t*)(seqbuf))[2] = (seqno >> 8) & 0xff; + ((uint8_t*)(seqbuf))[3] = seqno & 0xff; + + if (!is_stream_ && data_len != block_size_) { + r = kErrBadBlockLen; + goto out; + } + + // Decrypt block + if (EVP_DecryptUpdate(ctx_, data, &outlen, data, data_len) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != data_len) { + r = kErrPartialDecrypt; + goto out; + } + +out: + return r; + } + + ErrorType decrypt(unsigned char* packet, + uint32_t packet_len, + uint32_t seqno, + unsigned char* first_block, + uint32_t first_block_len, + unsigned char* mac, + uint32_t mac_len) { + ErrorType r = kErrNone; + + int outlen; + unsigned char calc_mac[hmac_len_] = {0}; + + uint8_t seqbuf[4] = {0}; + ((uint8_t*)(seqbuf))[0] = (seqno >> 24) & 0xff; + ((uint8_t*)(seqbuf))[1] = (seqno >> 16) & 0xff; + ((uint8_t*)(seqbuf))[2] = (seqno >> 8) & 0xff; + ((uint8_t*)(seqbuf))[3] = seqno & 0xff; + + if (is_etm_) { + // `first_block` for ETM should just be the unencrypted packet length + if (first_block_len != 4) { + r = kErrBadBlockLen; + goto out; + } + + // HMAC over unencrypted packet length and ciphertext + { + unsigned int outlen = hmac_len_; + if (HMAC_Init_ex(ctx_hmac_, nullptr, 0, nullptr, nullptr) != 1 + || HMAC_Update(ctx_hmac_, seqbuf, sizeof(seqbuf)) != 1 + || HMAC_Update(ctx_hmac_, first_block, first_block_len) != 1 + || HMAC_Update(ctx_hmac_, packet, packet_len) != 1 + || HMAC_Final(ctx_hmac_, calc_mac, &outlen) != 1) { + r = kErrOpenSSL; + goto out; + } + + if (outlen != hmac_len_ || mac_len != hmac_len_) { + r = kErrBadHMACLen; + goto out; + } + + // Compare MACs + if (CRYPTO_memcmp(mac, calc_mac, sizeof(calc_mac))) { + r = kErrInvalidMAC; + goto out; + } + } + + // Decrypt packet + if (EVP_DecryptUpdate(ctx_, packet, &outlen, packet, packet_len) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != packet_len) { + r = kErrPartialDecrypt; + goto out; + } + } else { + // `first_block` for non-ETM should be a completely decrypted first block + if (!is_stream_ && first_block_len != block_size_) { + r = kErrBadBlockLen; + goto out; + } + + const int offset = (is_stream_ ? 0 : block_size_ - 4); + // Decrypt the rest of the packet + if (EVP_DecryptUpdate(ctx_, + packet + offset, + &outlen, + packet + offset, + packet_len - offset) != 1) { + r = kErrOpenSSL; + goto out; + } + if (static_cast(outlen) != packet_len - offset) { + r = kErrPartialDecrypt; + goto out; + } + + // HMAC over plaintext + { + unsigned int outlen = hmac_len_; + if (HMAC_Init_ex(ctx_hmac_, nullptr, 0, nullptr, nullptr) != 1 + || HMAC_Update(ctx_hmac_, seqbuf, sizeof(seqbuf)) != 1 + || HMAC_Update(ctx_hmac_, first_block, first_block_len) != 1 + || HMAC_Update(ctx_hmac_, packet, packet_len) != 1 + || HMAC_Final(ctx_hmac_, calc_mac, &outlen) != 1) { + r = kErrOpenSSL; + goto out; + } + + if (outlen != hmac_len_ || mac_len != hmac_actual_len_) { + r = kErrBadHMACLen; + goto out; + } + + // Compare MACs + if (CRYPTO_memcmp(mac, calc_mac, hmac_actual_len_)) { + r = kErrInvalidMAC; + goto out; + } + } + } + +out: + return r; + } + + static NAN_METHOD(New) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + if (!info[0]->IsString()) + return Nan::ThrowTypeError("Missing/Invalid decipher name"); + + if (!Buffer::HasInstance(info[1])) + return Nan::ThrowTypeError("Missing/Invalid decipher key"); + + if (!Buffer::HasInstance(info[2])) + return Nan::ThrowTypeError("Missing/Invalid decipher IV"); + + if (!info[3]->IsString()) + return Nan::ThrowTypeError("Missing/Invalid HMAC name"); + + if (!Buffer::HasInstance(info[4])) + return Nan::ThrowTypeError("Missing/Invalid HMAC key"); + + if (!info[5]->IsBoolean()) + return Nan::ThrowTypeError("Missing/Invalid HMAC ETM flag"); + + if (!info[6]->IsUint32()) + return Nan::ThrowTypeError("Missing/Invalid HMAC ETM flag"); + + const Nan::Utf8String cipher_name(info[0]); + const Nan::Utf8String mac_name(info[3]); + int is_etm = (Nan::To(info[5]).FromJust() ? 1 : 0); + + GenericDecipher* obj = new GenericDecipher(); + ErrorType r = obj->init( + *cipher_name, + reinterpret_cast(Buffer::Data(info[1])), + Buffer::Length(info[1]), + reinterpret_cast(Buffer::Data(info[2])), + Buffer::Length(info[2]), + *mac_name, + reinterpret_cast(Buffer::Data(info[4])), + Buffer::Length(info[4]), + is_etm, + Nan::To(info[6]).FromJust() + ); + if (r != kErrNone) { + delete obj; + switch (r) { + case kErrBadKeyLen: + return Nan::ThrowError("Invalid decipher key length"); + case kErrBadIVLen: + return Nan::ThrowError("Invalid decipher IV length"); + case kErrBadCipherName: + return Nan::ThrowError("Invalid decipher name"); + case kErrBadHMACName: + return Nan::ThrowError("Invalid MAC name"); + case kErrBadInit: + return Nan::ThrowError("Failed to properly initialize decipher"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown init failure"); + } + } + + obj->Wrap(info.This()); + info.GetReturnValue().Set(info.This()); + } + + static NAN_METHOD(DecryptBlock) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + GenericDecipher* obj = ObjectWrap::Unwrap(info.Holder()); + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid block"); + + if (!info[1]->IsUint32()) + return Nan::ThrowTypeError("Missing/Invalid sequence number"); + + ErrorType r = obj->decrypt_block( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]), + Nan::To(info[1]).FromJust() + ); + switch (r) { + case kErrNone: + return; + case kErrBadBlockLen: + return Nan::ThrowError("Invalid block length"); + case kErrPartialDecrypt: + return Nan::ThrowError("Failed to completely decrypt packet"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown decrypt failure"); + } + } + + static NAN_METHOD(Decrypt) { + MarkPopErrorOnReturn mark_pop_error_on_return; + + GenericDecipher* obj = ObjectWrap::Unwrap(info.Holder()); + + if (!Buffer::HasInstance(info[0])) + return Nan::ThrowTypeError("Missing/Invalid packet"); + + if (!info[1]->IsUint32()) + return Nan::ThrowTypeError("Missing/Invalid sequence number"); + + if (!Buffer::HasInstance(info[2])) + return Nan::ThrowTypeError("Missing/Invalid first block"); + + if (!Buffer::HasInstance(info[3])) + return Nan::ThrowTypeError("Missing/Invalid MAC"); + + ErrorType r = obj->decrypt( + reinterpret_cast(Buffer::Data(info[0])), + Buffer::Length(info[0]), + Nan::To(info[1]).FromJust(), + reinterpret_cast(Buffer::Data(info[2])), + Buffer::Length(info[2]), + reinterpret_cast(Buffer::Data(info[3])), + Buffer::Length(info[3]) + ); + switch (r) { + case kErrNone: + return; + case kErrBadBlockLen: + return Nan::ThrowError("Invalid block length"); + case kErrPartialDecrypt: + return Nan::ThrowError("Failed to completely decrypt packet"); + case kErrBadHMACLen: + return Nan::ThrowError("Unexpected HMAC length"); + case kErrOpenSSL: { + char msg_buf[128] = {0}; + ERR_error_string_n(ERR_get_error(), msg_buf, sizeof(msg_buf)); + return Nan::ThrowError(msg_buf); + } + default: + return Nan::ThrowError("Unknown decrypt failure"); + } + } + + static NAN_METHOD(Free) { + GenericDecipher* obj = ObjectWrap::Unwrap(info.Holder()); + obj->clear(); + } + + static inline Nan::Persistent & constructor() { + static Nan::Persistent my_constructor; + return my_constructor; + } + + EVP_CIPHER_CTX* ctx_; + HMAC_CTX* ctx_hmac_; + unsigned int hmac_len_; + unsigned int hmac_actual_len_; + uint8_t is_etm_; + uint8_t is_stream_; + uint32_t block_size_; +}; + + +NAN_MODULE_INIT(init) { + ChaChaPolyCipher::Init(target); + AESGCMCipher::Init(target); + GenericCipher::Init(target); + + ChaChaPolyDecipher::Init(target); + AESGCMDecipher::Init(target); + GenericDecipher::Init(target); +} + +NODE_MODULE(sshcrypto, init) diff --git a/lib/protocol/handlers.js b/lib/protocol/handlers.js new file mode 100644 index 00000000..35fb9f28 --- /dev/null +++ b/lib/protocol/handlers.js @@ -0,0 +1,16 @@ +'use strict'; + +const MESSAGE_HANDLERS = new Array(256); +[ + require('./kex.js').HANDLERS, + require('./handlers.misc.js'), +].forEach((handlers) => { + // eslint-disable-next-line prefer-const + for (let [type, handler] of Object.entries(handlers)) { + type = +type; + if (isFinite(type) && type >= 0 && type < MESSAGE_HANDLERS.length) + MESSAGE_HANDLERS[type] = handler; + } +}); + +module.exports = MESSAGE_HANDLERS; diff --git a/lib/protocol/handlers.misc.js b/lib/protocol/handlers.misc.js new file mode 100644 index 00000000..e970412f --- /dev/null +++ b/lib/protocol/handlers.misc.js @@ -0,0 +1,1194 @@ +'use strict'; + +const { + bufferSlice, + bufferParser, + doFatalError, + sigSSHToASN1, + writeUInt32BE, +} = require('./utils.js'); + +const { + CHANNEL_OPEN_FAILURE, + COMPAT, + MESSAGE, + TERMINAL_MODE, +} = require('./constants.js'); + +const TERMINAL_MODE_BY_VALUE = + Array.from(Object.entries(TERMINAL_MODE)) + .reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {}); + +module.exports = { + // Transport layer protocol ================================================== + [MESSAGE.DISCONNECT]: (self, payload) => { + /* + byte SSH_MSG_DISCONNECT + uint32 reason code + string description in ISO-10646 UTF-8 encoding + string language tag + */ + bufferParser.init(payload, 1); + const reason = bufferParser.readUInt32BE(); + const desc = bufferParser.readString(true); + const lang = bufferParser.readString(); + bufferParser.clear(); + + if (lang === undefined) { + return doFatalError( + self, + 'Inbound: Malformed DISCONNECT packet' + ); + } + + self._debug && self._debug( + `Inbound: Received DISCONNECT (${reason}, "${desc}")` + ); + + const handler = self._handlers.DISCONNECT; + handler && handler(self, reason, desc); + }, + [MESSAGE.IGNORE]: (self, payload) => { + /* + byte SSH_MSG_IGNORE + string data + */ + self._debug && self._debug('Inbound: Received IGNORE'); + }, + [MESSAGE.UNIMPLEMENTED]: (self, payload) => { + /* + byte SSH_MSG_UNIMPLEMENTED + uint32 packet sequence number of rejected message + */ + bufferParser.init(payload, 1); + const seqno = bufferParser.readUInt32BE(); + bufferParser.clear(); + + if (seqno === undefined) { + return doFatalError( + self, + 'Inbound: Malformed UNIMPLEMENTED packet' + ); + } + + self._debug + && self._debug(`Inbound: Received UNIMPLEMENTED (seqno ${seqno})`); + }, + [MESSAGE.DEBUG]: (self, payload) => { + /* + byte SSH_MSG_DEBUG + boolean always_display + string message in ISO-10646 UTF-8 encoding [RFC3629] + string language tag [RFC3066] + */ + bufferParser.init(payload, 1); + const display = bufferParser.readBool(); + const msg = bufferParser.readString(true); + const lang = bufferParser.readString(); + bufferParser.clear(); + + if (lang === undefined) { + return doFatalError( + self, + 'Inbound: Malformed DEBUG packet' + ); + } + + self._debug && self._debug('Inbound: Received DEBUG'); + + const handler = self._handlers.DEBUG; + handler && handler(self, display, msg); + }, + [MESSAGE.SERVICE_REQUEST]: (self, payload) => { + /* + byte SSH_MSG_SERVICE_REQUEST + string service name + */ + bufferParser.init(payload, 1); + const name = bufferParser.readString(true); + bufferParser.clear(); + + if (name === undefined) { + return doFatalError( + self, + 'Inbound: Malformed SERVICE_REQUEST packet' + ); + } + + self._debug && self._debug(`Inbound: Received SERVICE_REQUEST (${name})`); + + const handler = self._handlers.SERVICE_REQUEST; + handler && handler(self, name); + }, + [MESSAGE.SERVICE_ACCEPT]: (self, payload) => { + // S->C + /* + byte SSH_MSG_SERVICE_ACCEPT + string service name + */ + bufferParser.init(payload, 1); + const name = bufferParser.readString(true); + bufferParser.clear(); + + if (name === undefined) { + return doFatalError( + self, + 'Inbound: Malformed SERVICE_ACCEPT packet' + ); + } + + self._debug && self._debug(`Inbound: Received SERVICE_ACCEPT (${name})`); + + const handler = self._handlers.SERVICE_ACCEPT; + handler && handler(self, name); + }, + + // User auth protocol -- generic ============================================= + [MESSAGE.USERAUTH_REQUEST]: (self, payload) => { + /* + byte SSH_MSG_USERAUTH_REQUEST + string user name in ISO-10646 UTF-8 encoding [RFC3629] + string service name in US-ASCII + string method name in US-ASCII + .... method specific fields + */ + bufferParser.init(payload, 1); + const user = bufferParser.readString(true); + const service = bufferParser.readString(true); + const method = bufferParser.readString(true); + let methodData; + let methodDesc; + switch (method) { + case 'none': + methodData = null; + break; + case 'password': { + /* + boolean + string plaintext password in ISO-10646 UTF-8 encoding [RFC3629] + [string new password] + */ + const isChange = bufferParser.readBool(); + if (isChange !== undefined) { + methodData = bufferParser.readString(true); + if (methodData !== undefined && isChange) { + const newPassword = bufferParser.readString(true); + if (newPassword !== undefined) + methodData = { oldPassword: methodData, newPassword }; + } + } + break; + } + case 'publickey': { + /* + boolean + string public key algorithm name + string public key blob + [string signature] + */ + const hasSig = bufferParser.readBool(); + if (hasSig !== undefined) { + const keyAlgo = bufferParser.readString(true); + const key = bufferParser.readString(); + if (hasSig) { + const blobEnd = bufferParser.pos(); + let signature = bufferParser.readString(); + if (signature !== undefined) { + if (signature.length > (4 + keyAlgo.length + 4) + && signature.utf8Slice(4, 4 + keyAlgo.length) === keyAlgo) { + // Skip algoLen + algo + sigLen + signature = bufferSlice(signature, 4 + keyAlgo.length + 4); + } + + signature = sigSSHToASN1(signature, keyAlgo); + if (signature) { + const sessionID = self._kex.sessionID; + const blob = Buffer.allocUnsafe(4 + sessionID.length + blobEnd); + writeUInt32BE(blob, sessionID.length, 0); + blob.set(sessionID, 4); + blob.set( + new Uint8Array(payload.buffer, payload.byteOffset, blobEnd), + 4 + sessionID.length + ); + methodData = { + keyAlgo, + key, + signature, + blob, + }; + } + } + } else { + methodData = { keyAlgo, key }; + methodDesc = 'publickey -- check'; + } + } + break; + } + case 'hostbased': { + /* + string public key algorithm for host key + string public host key and certificates for client host + string client host name expressed as the FQDN in US-ASCII + string user name on the client host in ISO-10646 UTF-8 encoding + [RFC3629] + string signature + */ + const keyAlgo = bufferParser.readString(true); + const key = bufferParser.readString(); + const localHostname = bufferParser.readString(true); + const localUsername = bufferParser.readString(true); + + const blobEnd = bufferParser.pos(); + let signature = bufferParser.readString(); + if (signature !== undefined) { + if (signature.length > (4 + keyAlgo.length + 4) + && signature.utf8Slice(4, 4 + keyAlgo.length) === keyAlgo) { + // Skip algoLen + algo + sigLen + signature = bufferSlice(signature, 4 + keyAlgo.length + 4); + } + + signature = sigSSHToASN1(signature, keyAlgo); + if (signature !== undefined) { + const sessionID = self._kex.sessionID; + const blob = Buffer.allocUnsafe(4 + sessionID.length + blobEnd); + writeUInt32BE(blob, sessionID.length, 0); + blob.set(sessionID, 4); + blob.set( + new Uint8Array(payload.buffer, payload.byteOffset, blobEnd), + 4 + sessionID.length + ); + methodData = { + keyAlgo, + key, + signature, + blob, + localHostname, + localUsername, + }; + } + } + break; + } + case 'keyboard-interactive': + /* + string language tag (as defined in [RFC-3066]) + string submethods (ISO-10646 UTF-8) + */ + // Skip/ignore language field -- it's deprecated in RFC 4256 + bufferParser.skipString(); + + methodData = bufferParser.readList(); + break; + default: + if (method !== undefined) + methodData = bufferParser.readRaw(); + } + bufferParser.clear(); + + if (methodData === undefined) { + return doFatalError( + self, + 'Inbound: Malformed USERAUTH_REQUEST packet' + ); + } + + if (methodDesc === undefined) + methodDesc = method; + + self._authsQueue.push(method); + + self._debug + && self._debug(`Inbound: Received USERAUTH_REQUEST (${methodDesc})`); + + const handler = self._handlers.USERAUTH_REQUEST; + handler && handler(self, user, service, method, methodData); + }, + [MESSAGE.USERAUTH_FAILURE]: (self, payload) => { + // S->C + /* + byte SSH_MSG_USERAUTH_FAILURE + name-list authentications that can continue + boolean partial success + */ + bufferParser.init(payload, 1); + const authMethods = bufferParser.readList(); + const partialSuccess = bufferParser.readBool(); + bufferParser.clear(); + + if (partialSuccess === undefined) { + return doFatalError( + self, + 'Inbound: Malformed USERAUTH_FAILURE packet' + ); + } + + self._debug + && self._debug(`Inbound: Received USERAUTH_FAILURE (${authMethods})`); + + self._authsQueue.shift(); + const handler = self._handlers.USERAUTH_FAILURE; + handler && handler(self, authMethods, partialSuccess); + }, + [MESSAGE.USERAUTH_SUCCESS]: (self, payload) => { + // S->C + /* + byte SSH_MSG_USERAUTH_SUCCESS + */ + self._debug && self._debug('Inbound: Received USERAUTH_SUCCESS'); + + self._authsQueue.shift(); + const handler = self._handlers.USERAUTH_SUCCESS; + handler && handler(self); + }, + [MESSAGE.USERAUTH_BANNER]: (self, payload) => { + // S->C + /* + byte SSH_MSG_USERAUTH_BANNER + string message in ISO-10646 UTF-8 encoding [RFC3629] + string language tag [RFC3066] + */ + bufferParser.init(payload, 1); + const msg = bufferParser.readString(true); + const lang = bufferParser.readString(); + bufferParser.clear(); + + if (lang === undefined) { + return doFatalError( + self, + 'Inbound: Malformed USERAUTH_BANNER packet' + ); + } + + self._debug && self._debug('Inbound: Received USERAUTH_BANNER'); + + const handler = self._handlers.USERAUTH_BANNER; + handler && handler(self, msg); + }, + + // User auth protocol -- method-specific ===================================== + 60: (self, payload) => { + if (!self._authsQueue.length) { + self._debug + && self._debug('Inbound: Received payload type 60 without auth'); + return; + } + + switch (self._authsQueue[0]) { + case 'password': { + // S->C + /* + byte SSH_MSG_USERAUTH_PASSWD_CHANGEREQ + string prompt in ISO-10646 UTF-8 encoding [RFC3629] + string language tag [RFC3066] + */ + bufferParser.init(payload, 1); + const prompt = bufferParser.readString(true); + const lang = bufferParser.readString(); + bufferParser.clear(); + + if (lang === undefined) { + return doFatalError( + self, + 'Inbound: Malformed USERAUTH_PASSWD_CHANGEREQ packet' + ); + } + + self._debug + && self._debug('Inbound: Received USERAUTH_PASSWD_CHANGEREQ'); + + const handler = self._handlers.USERAUTH_PASSWD_CHANGEREQ; + handler && handler(self, prompt); + break; + } + case 'publickey': { + // S->C + /* + byte SSH_MSG_USERAUTH_PK_OK + string public key algorithm name from the request + string public key blob from the request + */ + bufferParser.init(payload, 1); + const keyAlgo = bufferParser.readString(true); + const key = bufferParser.readString(); + bufferParser.clear(); + + if (key === undefined) { + return doFatalError( + self, + 'Inbound: Malformed USERAUTH_PK_OK packet' + ); + } + + self._debug && self._debug('Inbound: Received USERAUTH_PK_OK'); + + self._authsQueue.shift(); + const handler = self._handlers.USERAUTH_PK_OK; + handler && handler(self, keyAlgo, key); + break; + } + case 'keyboard-interactive': { + // S->C + /* + byte SSH_MSG_USERAUTH_INFO_REQUEST + string name (ISO-10646 UTF-8) + string instruction (ISO-10646 UTF-8) + string language tag (as defined in [RFC-3066]) + int num-prompts + string prompt[1] (ISO-10646 UTF-8) + boolean echo[1] + ... + string prompt[num-prompts] (ISO-10646 UTF-8) + boolean echo[num-prompts] + */ + bufferParser.init(payload, 1); + const name = bufferParser.readString(true); + const instructions = bufferParser.readString(true); + bufferParser.readString(); // skip lang + const numPrompts = bufferParser.readUInt32BE(); + let prompts; + if (numPrompts !== undefined) { + prompts = new Array(numPrompts); + let i; + for (i = 0; i < numPrompts; ++i) { + const prompt = bufferParser.readString(true); + const echo = bufferParser.readBool(); + if (echo === undefined) + break; + prompts[i] = { prompt, echo }; + } + if (i !== numPrompts) + prompts = undefined; + } + bufferParser.clear(); + + if (prompts === undefined) { + return doFatalError( + self, + 'Inbound: Malformed USERAUTH_INFO_REQUEST packet' + ); + } + + self._debug && self._debug('Inbound: Received USERAUTH_INFO_REQUEST'); + + const handler = self._handlers.USERAUTH_INFO_REQUEST; + handler && handler(self, name, instructions, prompts); + break; + } + default: + self._debug + && self._debug('Inbound: Received unexpected payload type 60'); + } + }, + 61: (self, payload) => { + if (!self._authsQueue.length) { + self._debug + && self._debug('Inbound: Received payload type 61 without auth'); + return; + } + /* + byte SSH_MSG_USERAUTH_INFO_RESPONSE + int num-responses + string response[1] (ISO-10646 UTF-8) + ... + string response[num-responses] (ISO-10646 UTF-8) + */ + if (self._authsQueue[0] !== 'keyboard-interactive') { + return doFatalError( + self, + 'Inbound: Received unexpected payload type 61' + ); + } + bufferParser.init(payload, 1); + const numResponses = bufferParser.readUInt32BE(); + let responses; + if (numResponses !== undefined) { + responses = new Array(numResponses); + let i; + for (i = 0; i < numResponses; ++i) { + const response = bufferParser.readString(true); + if (response === undefined) + break; + responses[i] = response; + } + if (i !== numResponses) + responses = undefined; + } + bufferParser.clear(); + + if (responses === undefined) { + return doFatalError( + self, + 'Inbound: Malformed USERAUTH_INFO_RESPONSE packet' + ); + } + + self._debug && self._debug('Inbound: Received USERAUTH_INFO_RESPONSE'); + + const handler = self._handlers.USERAUTH_INFO_RESPONSE; + handler && handler(self, responses); + }, + + // Connection protocol -- generic ============================================ + [MESSAGE.GLOBAL_REQUEST]: (self, payload) => { + /* + byte SSH_MSG_GLOBAL_REQUEST + string request name in US-ASCII only + boolean want reply + .... request-specific data follows + */ + bufferParser.init(payload, 1); + const name = bufferParser.readString(true); + const wantReply = bufferParser.readBool(); + let data; + if (wantReply !== undefined) { + switch (name) { + case 'tcpip-forward': + case 'cancel-tcpip-forward': { + /* + string address to bind (e.g., "0.0.0.0") + uint32 port number to bind + */ + const bindAddr = bufferParser.readString(true); + const bindPort = bufferParser.readUInt32BE(); + if (bindPort !== undefined) + data = { bindAddr, bindPort }; + break; + } + case 'streamlocal-forward@openssh.com': + case 'cancel-streamlocal-forward@openssh.com': { + /* + string socket path + */ + const socketPath = bufferParser.readString(true); + if (socketPath !== undefined) + data = { socketPath }; + break; + } + case 'no-more-sessions@openssh.com': + data = null; + break; + default: + data = bufferParser.readRaw(); + } + } + bufferParser.clear(); + + if (data === undefined) { + return doFatalError( + self, + 'Inbound: Malformed GLOBAL_REQUEST packet' + ); + } + + self._debug && self._debug(`Inbound: GLOBAL_REQUEST (${name})`); + + const handler = self._handlers.GLOBAL_REQUEST; + if (handler) + handler(self, name, wantReply, data); + else + self.requestFailure(); // Auto reject + }, + [MESSAGE.REQUEST_SUCCESS]: (self, payload) => { + /* + byte SSH_MSG_REQUEST_SUCCESS + .... response specific data + */ + const data = (payload.length > 1 ? bufferSlice(payload, 1) : null); + + self._debug && self._debug('Inbound: REQUEST_SUCCESS'); + + const handler = self._handlers.REQUEST_SUCCESS; + handler && handler(self, data); + }, + [MESSAGE.REQUEST_FAILURE]: (self, payload) => { + /* + byte SSH_MSG_REQUEST_FAILURE + */ + self._debug && self._debug('Inbound: Received REQUEST_FAILURE'); + + const handler = self._handlers.REQUEST_FAILURE; + handler && handler(self); + }, + + // Connection protocol -- channel-related ==================================== + [MESSAGE.CHANNEL_OPEN]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_OPEN + string channel type in US-ASCII only + uint32 sender channel + uint32 initial window size + uint32 maximum packet size + .... channel type specific data follows + */ + bufferParser.init(payload, 1); + const type = bufferParser.readString(true); + const sender = bufferParser.readUInt32BE(); + const window = bufferParser.readUInt32BE(); + const packetSize = bufferParser.readUInt32BE(); + let channelInfo; + + switch (type) { + case 'forwarded-tcpip': // S->C + case 'direct-tcpip': { // C->S + /* + string address that was connected / host to connect + uint32 port that was connected / port to connect + string originator IP address + uint32 originator port + */ + const destIP = bufferParser.readString(true); + const destPort = bufferParser.readUInt32BE(); + const srcIP = bufferParser.readString(true); + const srcPort = bufferParser.readUInt32BE(); + if (srcPort !== undefined) { + channelInfo = { + type, + sender, + window, + packetSize, + data: { destIP, destPort, srcIP, srcPort } + }; + } + break; + } + case 'forwarded-streamlocal@openssh.com': // S->C + case 'direct-streamlocal@openssh.com': { // C->S + /* + string socket path + string reserved for future use + + (direct-streamlocal@openssh.com additionally has:) + uint32 reserved + */ + const socketPath = bufferParser.readString(true); + if (socketPath !== undefined) { + channelInfo = { + type, + sender, + window, + packetSize, + data: { socketPath } + }; + } + break; + } + case 'x11': { // S->C + /* + string originator address (e.g., "192.168.7.38") + uint32 originator port + */ + const srcIP = bufferParser.readString(true); + const srcPort = bufferParser.readUInt32BE(); + if (srcPort !== undefined) { + channelInfo = { + type, + sender, + window, + packetSize, + data: { srcIP, srcPort } + }; + } + break; + } + default: + // Includes: + // 'session' (C->S) + // 'auth-agent@openssh.com' (S->C) + channelInfo = { + type, + sender, + window, + packetSize, + data: {} + }; + } + bufferParser.clear(); + + if (channelInfo === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_OPEN packet' + ); + } + + self._debug && self._debug(`Inbound: CHANNEL_OPEN (s:${sender}, ${type})`); + + const handler = self._handlers.CHANNEL_OPEN; + if (handler) { + handler(self, channelInfo); + } else { + self.channelOpenFail( + channelInfo.sender, + CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED, + '', + '' + ); + } + }, + [MESSAGE.CHANNEL_OPEN_CONFIRMATION]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION + uint32 recipient channel + uint32 sender channel + uint32 initial window size + uint32 maximum packet size + .... channel type specific data follows + */ + // "The 'recipient channel' is the channel number given in the + // original open request, and 'sender channel' is the channel number + // allocated by the other side." + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + const sender = bufferParser.readUInt32BE(); + const window = bufferParser.readUInt32BE(); + const packetSize = bufferParser.readUInt32BE(); + const data = (bufferParser.avail() ? bufferParser.readRaw() : undefined); + bufferParser.clear(); + + if (packetSize === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_OPEN_CONFIRMATION packet' + ); + } + + self._debug && self._debug( + `Inbound: CHANNEL_OPEN_CONFIRMATION (r:${recipient}, s:${sender})` + ); + + const handler = self._handlers.CHANNEL_OPEN_CONFIRMATION; + if (handler) + handler(self, { recipient, sender, window, packetSize, data }); + }, + [MESSAGE.CHANNEL_OPEN_FAILURE]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_OPEN_FAILURE + uint32 recipient channel + uint32 reason code + string description in ISO-10646 UTF-8 encoding [RFC3629] + string language tag [RFC3066] + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + const reason = bufferParser.readUInt32BE(); + const description = bufferParser.readString(true); + const lang = bufferParser.readString(); + bufferParser.clear(); + + if (lang === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_OPEN_FAILURE packet' + ); + } + + self._debug + && self._debug(`Inbound: CHANNEL_OPEN_FAILURE (r:${recipient})`); + + const handler = self._handlers.CHANNEL_OPEN_FAILURE; + handler && handler(self, recipient, reason, description); + }, + [MESSAGE.CHANNEL_WINDOW_ADJUST]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_WINDOW_ADJUST + uint32 recipient channel + uint32 bytes to add + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + const bytesToAdd = bufferParser.readUInt32BE(); + bufferParser.clear(); + + if (bytesToAdd === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_WINDOW_ADJUST packet' + ); + } + + self._debug && self._debug( + `Inbound: CHANNEL_WINDOW_ADJUST (r:${recipient}, ${bytesToAdd})` + ); + + const handler = self._handlers.CHANNEL_WINDOW_ADJUST; + handler && handler(self, recipient, bytesToAdd); + }, + [MESSAGE.CHANNEL_DATA]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_DATA + uint32 recipient channel + string data + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + const data = bufferParser.readString(); + bufferParser.clear(); + + if (data === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_DATA packet' + ); + } + + self._debug + && self._debug(`Inbound: CHANNEL_DATA (r:${recipient}, ${data.length})`); + + const handler = self._handlers.CHANNEL_DATA; + handler && handler(self, recipient, data); + }, + [MESSAGE.CHANNEL_EXTENDED_DATA]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_EXTENDED_DATA + uint32 recipient channel + uint32 data_type_code + string data + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + const type = bufferParser.readUInt32BE(); + const data = bufferParser.readString(); + bufferParser.clear(); + + if (data === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_EXTENDED_DATA packet' + ); + } + + self._debug && self._debug( + `Inbound: CHANNEL_EXTENDED_DATA (r:${recipient}, ${data.length})` + ); + + const handler = self._handlers.CHANNEL_EXTENDED_DATA; + handler && handler(self, recipient, data, type); + }, + [MESSAGE.CHANNEL_EOF]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_EOF + uint32 recipient channel + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + bufferParser.clear(); + + if (recipient === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_EOF packet' + ); + } + + self._debug && self._debug(`Inbound: CHANNEL_EOF (r:${recipient})`); + + const handler = self._handlers.CHANNEL_EOF; + handler && handler(self, recipient); + }, + [MESSAGE.CHANNEL_CLOSE]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_CLOSE + uint32 recipient channel + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + bufferParser.clear(); + + if (recipient === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_CLOSE packet' + ); + } + + self._debug && self._debug(`Inbound: CHANNEL_CLOSE (r:${recipient})`); + + const handler = self._handlers.CHANNEL_CLOSE; + handler && handler(self, recipient); + }, + [MESSAGE.CHANNEL_REQUEST]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_REQUEST + uint32 recipient channel + string request type in US-ASCII characters only + boolean want reply + .... type-specific data follows + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + const type = bufferParser.readString(true); + const wantReply = bufferParser.readBool(); + let data; + if (wantReply !== undefined) { + switch (type) { + case 'exit-status': // S->C + /* + uint32 exit_status + */ + data = bufferParser.readUInt32BE(); + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})` + ); + break; + case 'exit-signal': { // S->C + /* + string signal name (without the "SIG" prefix) + boolean core dumped + string error message in ISO-10646 UTF-8 encoding + string language tag + */ + let signal; + let coreDumped; + if (self._compatFlags & COMPAT.OLD_EXIT) { + /* + Instead of `signal name` and `core dumped`, we have just: + uint32 signal number + */ + const num = bufferParser.readUInt32BE(); + switch (num) { + case 1: + signal = 'HUP'; + break; + case 2: + signal = 'INT'; + break; + case 3: + signal = 'QUIT'; + break; + case 6: + signal = 'ABRT'; + break; + case 9: + signal = 'KILL'; + break; + case 14: + signal = 'ALRM'; + break; + case 15: + signal = 'TERM'; + break; + default: + if (num !== undefined) { + // Unknown or OS-specific + signal = `UNKNOWN (${num})`; + } + } + coreDumped = false; + } else { + signal = bufferParser.readString(true); + coreDumped = bufferParser.readBool(); + if (coreDumped === undefined) + signal = undefined; + } + const errorMessage = bufferParser.readString(true); + if (bufferParser.skipString() !== undefined) + data = { signal, coreDumped, errorMessage }; + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${signal})` + ); + break; + } + case 'pty-req': { // C->S + /* + string TERM environment variable value (e.g., vt100) + uint32 terminal width, characters (e.g., 80) + uint32 terminal height, rows (e.g., 24) + uint32 terminal width, pixels (e.g., 640) + uint32 terminal height, pixels (e.g., 480) + string encoded terminal modes + */ + const term = bufferParser.readString(true); + const cols = bufferParser.readUInt32BE(); + const rows = bufferParser.readUInt32BE(); + const width = bufferParser.readUInt32BE(); + const height = bufferParser.readUInt32BE(); + const modesBinary = bufferParser.readString(); + if (modesBinary !== undefined) { + bufferParser.init(modesBinary, 1); + let modes = {}; + while (bufferParser.avail()) { + const opcode = bufferParser.readByte(); + if (opcode === TERMINAL_MODE.TTY_OP_END) + break; + const name = TERMINAL_MODE_BY_VALUE[opcode]; + const value = bufferParser.readUInt32BE(); + if (opcode === undefined + || name === undefined + || value === undefined) { + modes = undefined; + break; + } + modes[name] = value; + } + if (modes !== undefined) + data = { term, cols, rows, width, height, modes }; + } + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})` + ); + break; + } + case 'window-change': { // C->S + /* + uint32 terminal width, columns + uint32 terminal height, rows + uint32 terminal width, pixels + uint32 terminal height, pixels + */ + const cols = bufferParser.readUInt32BE(); + const rows = bufferParser.readUInt32BE(); + const width = bufferParser.readUInt32BE(); + const height = bufferParser.readUInt32BE(); + if (height !== undefined) + data = { cols, rows, width, height }; + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})` + ); + break; + } + case 'x11-req': { // C->S + /* + boolean single connection + string x11 authentication protocol + string x11 authentication cookie + uint32 x11 screen number + */ + const single = bufferParser.readBool(); + const protocol = bufferParser.readString(true); + const cookie = bufferParser.readString(); + const screen = bufferParser.readUInt32BE(); + if (screen !== undefined) + data = { single, protocol, cookie, screen }; + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})` + ); + break; + } + case 'env': { // C->S + /* + string variable name + string variable value + */ + const name = bufferParser.readString(true); + const value = bufferParser.readString(true); + if (value !== undefined) + data = { name, value }; + if (self._debug) { + self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ` + + `${name}=${value})` + ); + } + break; + } + case 'shell': // C->S + data = null; // No extra data + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})` + ); + break; + case 'exec': // C->S + /* + string command + */ + data = bufferParser.readString(true); + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})` + ); + break; + case 'subsystem': // C->S + /* + string subsystem name + */ + data = bufferParser.readString(true); + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})` + ); + break; + case 'signal': // C->S + /* + string signal name (without the "SIG" prefix) + */ + data = bufferParser.readString(true); + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})` + ); + break; + case 'xon-xoff': // C->S + /* + boolean client can do + */ + data = bufferParser.readBool(); + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type}: ${data})` + ); + break; + case 'auth-agent-req@openssh.com': // C-S + data = null; // No extra data + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})` + ); + break; + default: + data = (bufferParser.avail() ? bufferParser.readRaw() : null); + self._debug && self._debug( + `Inbound: CHANNEL_REQUEST (r:${recipient}, ${type})` + ); + } + } + bufferParser.clear(); + + if (data === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_REQUEST packet' + ); + } + + const handler = self._handlers.CHANNEL_REQUEST; + handler && handler(self, recipient, type, wantReply, data); + }, + [MESSAGE.CHANNEL_SUCCESS]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_SUCCESS + uint32 recipient channel + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + bufferParser.clear(); + + if (recipient === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_SUCCESS packet' + ); + } + + self._debug && self._debug(`Inbound: CHANNEL_SUCCESS (r:${recipient})`); + + const handler = self._handlers.CHANNEL_SUCCESS; + handler && handler(self, recipient); + }, + [MESSAGE.CHANNEL_FAILURE]: (self, payload) => { + /* + byte SSH_MSG_CHANNEL_FAILURE + uint32 recipient channel + */ + bufferParser.init(payload, 1); + const recipient = bufferParser.readUInt32BE(); + bufferParser.clear(); + + if (recipient === undefined) { + return doFatalError( + self, + 'Inbound: Malformed CHANNEL_FAILURE packet' + ); + } + + self._debug && self._debug(`Inbound: CHANNEL_FAILURE (r:${recipient})`); + + const handler = self._handlers.CHANNEL_FAILURE; + handler && handler(self, recipient); + }, +}; diff --git a/lib/protocol/kex.js b/lib/protocol/kex.js new file mode 100644 index 00000000..49dd04f7 --- /dev/null +++ b/lib/protocol/kex.js @@ -0,0 +1,1792 @@ +'use strict'; + +const { + createDiffieHellman, + createDiffieHellmanGroup, + createECDH, + createHash, + createPublicKey, + diffieHellman, + generateKeyPairSync, + randomFillSync, +} = require('crypto'); + +const { Ber } = require('asn1'); + +const { + COMPAT, + curve25519Supported, + DEFAULT_KEX, + DEFAULT_SERVER_HOST_KEY, + DEFAULT_CIPHER, + DEFAULT_MAC, + DEFAULT_COMPRESSION, + DISCONNECT_REASON, + MESSAGE, +} = require('./constants.js'); +const { + CIPHER_INFO, + createCipher, + createDecipher, + MAC_INFO, +} = require('./crypto.js'); +const { parseDERKey } = require('./keyParser.js'); +const { + bufferFill, + bufferParser, + convertSignature, + doFatalError, + FastBuffer, + sigSSHToASN1, + writeUInt32BE, +} = require('./utils.js'); +const { + PacketReader, + PacketWriter, + ZlibPacketReader, + ZlibPacketWriter, +} = require('./zlib.js'); + +let MESSAGE_HANDLERS; + +const GEX_MIN_BITS = 2048; // RFC 8270 +const GEX_MAX_BITS = 8192; // RFC 8270 + +const EMPTY_BUFFER = Buffer.alloc(0); + +// Client/Server +function kexinit(self) { + /* + byte SSH_MSG_KEXINIT + byte[16] cookie (random bytes) + name-list kex_algorithms + name-list server_host_key_algorithms + name-list encryption_algorithms_client_to_server + name-list encryption_algorithms_server_to_client + name-list mac_algorithms_client_to_server + name-list mac_algorithms_server_to_client + name-list compression_algorithms_client_to_server + name-list compression_algorithms_server_to_client + name-list languages_client_to_server + name-list languages_server_to_client + boolean first_kex_packet_follows + uint32 0 (reserved for future extension) + */ + + let payload; + if (self._compatFlags & COMPAT.BAD_DHGEX) { + const entry = self._offer.lists.kex; + let kex = entry.array; + let found = false; + for (let i = 0; i < kex.length; ++i) { + if (kex[i].indexOf('group-exchange') !== -1) { + if (!found) { + found = true; + // Copy array lazily + kex = kex.slice(); + } + kex.splice(i--, 1); + } + } + if (found) { + let len = 1 + 16 + self._offer.totalSize + 1 + 4; + const newKexBuf = Buffer.from(kex.join(',')); + len -= (entry.buffer.length - newKexBuf.length); + + const all = self._offer.lists.all; + const rest = new Uint8Array( + all.buffer, + all.byteOffset + 4 + entry.buffer.length, + all.length - (4 + entry.buffer.length) + ); + + payload = Buffer.allocUnsafe(len); + writeUInt32BE(payload, newKexBuf.length, 0); + payload.set(newKexBuf, 4); + payload.set(rest, 4 + newKexBuf.length); + } + } + + if (payload === undefined) { + payload = Buffer.allocUnsafe(1 + 16 + self._offer.totalSize + 1 + 4); + self._offer.copyAllTo(payload, 17); + } + + self._debug && self._debug('Outbound: Sending KEXINIT'); + + payload[0] = MESSAGE.KEXINIT; + randomFillSync(payload, 1, 16); + + // Zero-fill first_kex_packet_follows and reserved bytes + bufferFill(payload, 0, payload.length - 5); + + self._kexinit = payload; + + // Needed to correct the starting position in allocated "packets" when packets + // will be buffered due to active key exchange + self._packetRW.write.allocStart = 0; + + // TODO: only create single buffer and set _kexinit as slice of packet instead + { + const p = self._packetRW.write.allocStartKEX; + const packet = self._packetRW.write.alloc(payload.length, true); + packet.set(payload, p); + self._cipher.encrypt(self._packetRW.write.finalize(packet, true)); + } +} + +function handleKexInit(self, payload) { + /* + byte SSH_MSG_KEXINIT + byte[16] cookie (random bytes) + name-list kex_algorithms + name-list server_host_key_algorithms + name-list encryption_algorithms_client_to_server + name-list encryption_algorithms_server_to_client + name-list mac_algorithms_client_to_server + name-list mac_algorithms_server_to_client + name-list compression_algorithms_client_to_server + name-list compression_algorithms_server_to_client + name-list languages_client_to_server + name-list languages_server_to_client + boolean first_kex_packet_follows + uint32 0 (reserved for future extension) + */ + const init = { + kex: undefined, + srvHostKey: undefined, + cs: { + cipher: undefined, + mac: undefined, + compress: undefined, + lang: undefined, + }, + sc: { + cipher: undefined, + mac: undefined, + compress: undefined, + lang: undefined, + }, + }; + + bufferParser.init(payload, 17); + + if ((init.kex = bufferParser.readList()) === undefined + || (init.srvHostKey = bufferParser.readList()) === undefined + || (init.cs.cipher = bufferParser.readList()) === undefined + || (init.sc.cipher = bufferParser.readList()) === undefined + || (init.cs.mac = bufferParser.readList()) === undefined + || (init.sc.mac = bufferParser.readList()) === undefined + || (init.cs.compress = bufferParser.readList()) === undefined + || (init.sc.compress = bufferParser.readList()) === undefined + || (init.cs.lang = bufferParser.readList()) === undefined + || (init.sc.lang = bufferParser.readList()) === undefined) { + bufferParser.clear(); + return doFatalError( + self, + 'Received malformed KEXINIT', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + const pos = bufferParser.pos(); + const firstFollows = (pos < payload.length && payload[pos] === 1); + bufferParser.clear(); + + const local = self._offer; + const remote = init; + + let localKex = local.lists.kex.array; + if (self._compatFlags & COMPAT.BAD_DHGEX) { + let found = false; + for (let i = 0; i < localKex.length; ++i) { + if (localKex[i].indexOf('group-exchange') !== -1) { + if (!found) { + found = true; + // Copy array lazily + localKex = localKex.slice(); + } + localKex.splice(i--, 1); + } + } + } + + let clientList; + let serverList; + let i; + const debug = self._debug; + + debug && debug('Inbound: Handshake in progress'); + + // Key exchange method ======================================================= + debug && debug(`Handshake: (local) KEX method: ${localKex}`); + debug && debug(`Handshake: (remote) KEX method: ${remote.kex}`); + if (self._server) { + serverList = localKex; + clientList = remote.kex; + } else { + serverList = remote.kex; + clientList = localKex; + } + // Check for agreeable key exchange algorithm + for (i = 0; + i < clientList.length && serverList.indexOf(clientList[i]) === -1; + ++i); + if (i === clientList.length) { + // No suitable match found! + debug && debug('Handshake: No matching key exchange algorithm'); + return doFatalError( + self, + 'Handshake failed: no matching key exchange algorithm', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + init.kex = clientList[i]; + debug && debug(`Handshake: KEX algorithm: ${clientList[i]}`); + if (firstFollows && (!remote.kex.length || clientList[i] !== remote.kex[0])) { + // Ignore next inbound packet, it was a wrong first guess at KEX algorithm + self._skipNextInboundPacket = true; + } + + + // Server host key format ==================================================== + const localSrvHostKey = local.lists.srvHostKey.array; + debug && debug(`Handshake: (local) Host key format: ${localSrvHostKey}`); + debug && debug(`Handshake: (remote) Host key format: ${remote.srvHostKey}`); + if (self._server) { + serverList = localSrvHostKey; + clientList = remote.srvHostKey; + } else { + serverList = remote.srvHostKey; + clientList = localSrvHostKey; + } + // Check for agreeable server host key format + for (i = 0; + i < clientList.length && serverList.indexOf(clientList[i]) === -1; + ++i); + if (i === clientList.length) { + // No suitable match found! + debug && debug('Handshake: No matching host key format'); + return doFatalError( + self, + 'Handshake failed: no matching host key format', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + init.srvHostKey = clientList[i]; + debug && debug(`Handshake: Host key format: ${clientList[i]}`); + + + // Client->Server cipher ===================================================== + const localCSCipher = local.lists.cs.cipher.array; + debug && debug(`Handshake: (local) C->S cipher: ${localCSCipher}`); + debug && debug(`Handshake: (remote) C->S cipher: ${remote.cs.cipher}`); + if (self._server) { + serverList = localCSCipher; + clientList = remote.cs.cipher; + } else { + serverList = remote.cs.cipher; + clientList = localCSCipher; + } + // Check for agreeable client->server cipher + for (i = 0; + i < clientList.length && serverList.indexOf(clientList[i]) === -1; + ++i); + if (i === clientList.length) { + // No suitable match found! + debug && debug('Handshake: No matching C->S cipher'); + return doFatalError( + self, + 'Handshake failed: no matching C->S cipher', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + init.cs.cipher = clientList[i]; + debug && debug(`Handshake: C->S Cipher: ${clientList[i]}`); + + + // Server->Client cipher ===================================================== + const localSCCipher = local.lists.sc.cipher.array; + debug && debug(`Handshake: (local) S->C cipher: ${localSCCipher}`); + debug && debug(`Handshake: (remote) S->C cipher: ${remote.sc.cipher}`); + if (self._server) { + serverList = localSCCipher; + clientList = remote.sc.cipher; + } else { + serverList = remote.sc.cipher; + clientList = localSCCipher; + } + // Check for agreeable server->client cipher + for (i = 0; + i < clientList.length && serverList.indexOf(clientList[i]) === -1; + ++i); + if (i === clientList.length) { + // No suitable match found! + debug && debug('Handshake: No matching S->C cipher'); + return doFatalError( + self, + 'Handshake failed: no matching S->C cipher', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + init.sc.cipher = clientList[i]; + debug && debug(`Handshake: S->C cipher: ${clientList[i]}`); + + + // Client->Server MAC ======================================================== + const localCSMAC = local.lists.cs.mac.array; + debug && debug(`Handshake: (local) C->S MAC: ${localCSMAC}`); + debug && debug(`Handshake: (remote) C->S MAC: ${remote.cs.mac}`); + if (CIPHER_INFO[init.cs.cipher].authLen > 0) { + init.cs.mac = ''; + debug && debug('Handshake: C->S MAC: '); + } else { + if (self._server) { + serverList = localCSMAC; + clientList = remote.cs.mac; + } else { + serverList = remote.cs.mac; + clientList = localCSMAC; + } + // Check for agreeable client->server hmac algorithm + for (i = 0; + i < clientList.length && serverList.indexOf(clientList[i]) === -1; + ++i); + if (i === clientList.length) { + // No suitable match found! + debug && debug('Handshake: No matching C->S MAC'); + return doFatalError( + self, + 'Handshake failed: no matching C->S MAC', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + init.cs.mac = clientList[i]; + debug && debug(`Handshake: C->S MAC: ${clientList[i]}`); + } + + + // Server->Client MAC ======================================================== + const localSCMAC = local.lists.sc.mac.array; + debug && debug(`Handshake: (local) S->C MAC: ${localSCMAC}`); + debug && debug(`Handshake: (remote) S->C MAC: ${remote.sc.mac}`); + if (CIPHER_INFO[init.sc.cipher].authLen > 0) { + init.sc.mac = ''; + debug && debug('Handshake: S->C MAC: '); + } else { + if (self._server) { + serverList = localSCMAC; + clientList = remote.sc.mac; + } else { + serverList = remote.sc.mac; + clientList = localSCMAC; + } + // Check for agreeable server->client hmac algorithm + for (i = 0; + i < clientList.length && serverList.indexOf(clientList[i]) === -1; + ++i); + if (i === clientList.length) { + // No suitable match found! + debug && debug('Handshake: No matching S->C MAC'); + return doFatalError( + self, + 'Handshake failed: no matching S->C MAC', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + init.sc.mac = clientList[i]; + debug && debug(`Handshake: S->C MAC: ${clientList[i]}`); + } + + + // Client->Server compression ================================================ + const localCSCompress = local.lists.cs.compress.array; + debug && debug(`Handshake: (local) C->S compression: ${localCSCompress}`); + debug && debug(`Handshake: (remote) C->S compression: ${remote.cs.compress}`); + if (self._server) { + serverList = localCSCompress; + clientList = remote.cs.compress; + } else { + serverList = remote.cs.compress; + clientList = localCSCompress; + } + // Check for agreeable client->server compression algorithm + for (i = 0; + i < clientList.length && serverList.indexOf(clientList[i]) === -1; + ++i); + if (i === clientList.length) { + // No suitable match found! + debug && debug('Handshake: No matching C->S compression'); + return doFatalError( + self, + 'Handshake failed: no matching C->S compression', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + init.cs.compress = clientList[i]; + debug && debug(`Handshake: C->S compression: ${clientList[i]}`); + + + // Server->Client compression ================================================ + const localSCCompress = local.lists.sc.compress.array; + debug && debug(`Handshake: (local) S->C compression: ${localSCCompress}`); + debug && debug(`Handshake: (remote) S->C compression: ${remote.sc.compress}`); + if (self._server) { + serverList = localSCCompress; + clientList = remote.sc.compress; + } else { + serverList = remote.sc.compress; + clientList = localSCCompress; + } + // Check for agreeable server->client compression algorithm + for (i = 0; + i < clientList.length && serverList.indexOf(clientList[i]) === -1; + ++i); + if (i === clientList.length) { + // No suitable match found! + debug && debug('Handshake: No matching S->C compression'); + return doFatalError( + self, + 'Handshake failed: no matching S->C compression', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + init.sc.compress = clientList[i]; + debug && debug(`Handshake: S->C compression: ${clientList[i]}`); + + init.cs.lang = ''; + init.sc.lang = ''; + + // XXX: hack -- find a better way to do this + if (self._kex) { + if (!self._kexinit) { + // We received a rekey request, but we haven't sent a KEXINIT in response + // yet + kexinit(self); + } + self._decipher._onPayload = onKEXPayload.bind(self, { firstPacket: false }); + } + + self._kex = createKeyExchange(init, self, payload); + self._kex.start(); +} + +const createKeyExchange = (() => { + function convertToMpint(buf) { + let idx = 0; + let length = buf.length; + while (buf[idx] === 0x00) { + ++idx; + --length; + } + let newBuf; + if (buf[idx] & 0x80) { + newBuf = Buffer.allocUnsafe(1 + length); + newBuf[0] = 0; + buf.copy(newBuf, 1, idx); + buf = newBuf; + } else if (length !== buf.length) { + newBuf = Buffer.allocUnsafe(length); + buf.copy(newBuf, 0, idx); + buf = newBuf; + } + return buf; + } + + class KeyExchange { + constructor(negotiated, protocol, remoteKexinit) { + this._protocol = protocol; + + this.sessionID = (protocol._kex ? protocol._kex.sessionID : undefined); + this.negotiated = negotiated; + this._step = 1; + this._public = null; + this._dh = null; + this._sentNEWKEYS = false; + this._receivedNEWKEYS = false; + this._finished = false; + this._hostVerified = false; + + // Data needed for initializing cipher/decipher/etc. + this._kexinit = protocol._kexinit; + this._remoteKexinit = remoteKexinit; + this._identRaw = protocol._identRaw; + this._remoteIdentRaw = protocol._remoteIdentRaw; + this._hostKey = undefined; + this._dhData = undefined; + this._sig = undefined; + } + finish() { + if (this._finished) + return false; + this._finished = true; + + const isServer = this._protocol._server; + const negotiated = this.negotiated; + + const pubKey = this.convertPublicKey(this._dhData); + let secret = this.computeSecret(this._dhData); + if (secret instanceof Error) { + secret.message = + `Error while computing DH secret (${this.type}): ${secret.message}`; + secret.level = 'handshake'; + return doFatalError( + this._protocol, + secret, + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + const hash = createHash(this.hashName); + // V_C + hashString(hash, (isServer ? this._remoteIdentRaw : this._identRaw)); + // "V_S" + hashString(hash, (isServer ? this._identRaw : this._remoteIdentRaw)); + // "I_C" + hashString(hash, (isServer ? this._remoteKexinit : this._kexinit)); + // "I_S" + hashString(hash, (isServer ? this._kexinit : this._remoteKexinit)); + // "K_S" + const serverPublicHostKey = (isServer + ? this._hostKey.getPublicSSH() + : this._hostKey); + hashString(hash, serverPublicHostKey); + + if (this.type === 'groupex') { + // Group exchange-specific + const params = this.getDHParams(); + const num = Buffer.allocUnsafe(4); + // min (uint32) + writeUInt32BE(num, GEX_MIN_BITS, 0); + hash.update(num); + // preferred (uint32) + let nbits = dhEstimate(this.negotiated); + if (this._protocol._compatFlags & COMPAT.BUG_DHGEX_LARGE) + nbits = Math.min(nbits, 4096); + writeUInt32BE(num, nbits, 0); + hash.update(num); + // max (uint32) + writeUInt32BE(num, GEX_MAX_BITS, 0); + hash.update(num); + // prime + hashString(hash, params.prime); + // generator + hashString(hash, params.generator); + } + + // method-specific data sent by client + hashString(hash, (isServer ? pubKey : this.getPublicKey())); + // method-specific data sent by server + const serverPublicKey = (isServer ? this.getPublicKey() : pubKey); + hashString(hash, serverPublicKey); + // shared secret ("K") + hashString(hash, secret); + + // "H" + const exchangeHash = hash.digest(); + + if (!isServer) { + bufferParser.init(this._sig, 0); + const sigType = bufferParser.readString(true); + + if (!sigType) { + return doFatalError( + this._protocol, + 'Malformed packet while reading signature', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + if (sigType !== negotiated.srvHostKey) { + return doFatalError( + this._protocol, + `Wrong signature type: ${sigType}, ` + + `expected: ${negotiated.srvHostKey}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + // "s" + let sigValue = bufferParser.readString(); + + bufferParser.clear(); + + if (sigValue === undefined) { + return doFatalError( + this._protocol, + 'Malformed packet while reading signature', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + if (!(sigValue = sigSSHToASN1(sigValue, sigType))) { + return doFatalError( + this._protocol, + 'Malformed signature', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + let parsedHostKey; + { + bufferParser.init(this._hostKey, 0); + const name = bufferParser.readString(true); + const hostKey = this._hostKey.slice(bufferParser.pos()); + bufferParser.clear(); + parsedHostKey = parseDERKey(hostKey, name); + if (parsedHostKey instanceof Error) { + parsedHostKey.level = 'handshake'; + return doFatalError( + this._protocol, + parsedHostKey, + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + } + + let hashAlgo; + // Check if we need to override the default hash algorithm + switch (this.negotiated.srvHostKey) { + case 'rsa-sha2-256': hashAlgo = 'sha256'; break; + case 'rsa-sha2-512': hashAlgo = 'sha512'; break; + } + + this._protocol._debug + && this._protocol._debug('Verifying signature ...'); + + const verified = parsedHostKey.verify(exchangeHash, sigValue, hashAlgo); + if (verified !== true) { + if (verified instanceof Error) { + this._protocol._debug && this._protocol._debug( + `Signature verification failed: ${verified.stack}` + ); + } else { + this._protocol._debug && this._protocol._debug( + 'Signature verification failed' + ); + } + return doFatalError( + this._protocol, + 'Handshake failed: signature verification failed', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + this._protocol._debug && this._protocol._debug('Verified signature'); + } else { + // Server + + let hashAlgo; + // Check if we need to override the default hash algorithm + switch (this.negotiated.srvHostKey) { + case 'rsa-sha2-256': hashAlgo = 'sha256'; break; + case 'rsa-sha2-512': hashAlgo = 'sha512'; break; + } + + this._protocol._debug && this._protocol._debug( + 'Generating signature ...' + ); + + let signature = this._hostKey.sign(exchangeHash, hashAlgo); + if (signature instanceof Error) { + return doFatalError( + this._protocol, + 'Handshake failed: signature generation failed for ' + + `${this._hostKey.type} host key: ${signature.message}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + signature = convertSignature(signature, this._hostKey.type); + if (signature === false) { + return doFatalError( + this._protocol, + 'Handshake failed: signature conversion failed for ' + + `${this._hostKey.type} host key`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + // Send KEX reply + /* + byte SSH_MSG_KEXDH_REPLY + / SSH_MSG_KEX_DH_GEX_REPLY + / SSH_MSG_KEX_ECDH_REPLY + string server public host key and certificates (K_S) + string + string signature of H + */ + const sigType = this.negotiated.srvHostKey; + const sigTypeLen = Buffer.byteLength(sigType); + const sigLen = 4 + sigTypeLen + 4 + signature.length; + let p = this._protocol._packetRW.write.allocStartKEX; + const packet = this._protocol._packetRW.write.alloc( + 1 + + 4 + serverPublicHostKey.length + + 4 + serverPublicKey.length + + 4 + sigLen, + true + ); + + packet[p] = MESSAGE.KEXDH_REPLY; + + writeUInt32BE(packet, serverPublicHostKey.length, ++p); + packet.set(serverPublicHostKey, p += 4); + + writeUInt32BE(packet, + serverPublicKey.length, + p += serverPublicHostKey.length); + packet.set(serverPublicKey, p += 4); + + writeUInt32BE(packet, sigLen, p += serverPublicKey.length); + + writeUInt32BE(packet, sigTypeLen, p += 4); + packet.utf8Write(sigType, p += 4, sigTypeLen); + + writeUInt32BE(packet, signature.length, p += sigTypeLen); + packet.set(signature, p += 4); + + if (this._protocol._debug) { + let type; + switch (this.type) { + case 'group': + type = 'KEXDH_REPLY'; + break; + case 'groupex': + type = 'KEXDH_GEX_REPLY'; + break; + default: + type = 'KEXECDH_REPLY'; + } + this._protocol._debug(`Outbound: Sending ${type}`); + } + this._protocol._cipher.encrypt( + this._protocol._packetRW.write.finalize(packet, true) + ); + } + if (!this._sentNEWKEYS) { + this._protocol._debug && this._protocol._debug( + 'Outbound: Sending NEWKEYS' + ); + const p = this._protocol._packetRW.write.allocStartKEX; + const packet = this._protocol._packetRW.write.alloc(1, true); + packet[p] = MESSAGE.NEWKEYS; + this._protocol._cipher.encrypt( + this._protocol._packetRW.write.finalize(packet, true) + ); + this._sentNEWKEYS = true; + } + + const completeHandshake = () => { + if (!this.sessionID) + this.sessionID = exchangeHash; + + { + const newSecret = Buffer.allocUnsafe(4 + secret.length); + writeUInt32BE(newSecret, secret.length, 0); + newSecret.set(secret, 4); + secret = newSecret; + } + + // Initialize new ciphers, deciphers, etc. + + const csCipherInfo = CIPHER_INFO[negotiated.cs.cipher]; + const scCipherInfo = CIPHER_INFO[negotiated.sc.cipher]; + + const csIV = generateKEXVal(csCipherInfo.ivLen, + this.hashName, + secret, + exchangeHash, + this.sessionID, + 'A'); + const scIV = generateKEXVal(scCipherInfo.ivLen, + this.hashName, + secret, + exchangeHash, + this.sessionID, + 'B'); + const csKey = generateKEXVal(csCipherInfo.keyLen, + this.hashName, + secret, + exchangeHash, + this.sessionID, + 'C'); + const scKey = generateKEXVal(scCipherInfo.keyLen, + this.hashName, + secret, + exchangeHash, + this.sessionID, + 'D'); + let csMacInfo; + let csMacKey; + if (!csCipherInfo.authLen) { + csMacInfo = MAC_INFO[negotiated.cs.mac]; + csMacKey = generateKEXVal(csMacInfo.len, + this.hashName, + secret, + exchangeHash, + this.sessionID, + 'E'); + } + let scMacInfo; + let scMacKey; + if (!scCipherInfo.authLen) { + scMacInfo = MAC_INFO[negotiated.sc.mac]; + scMacKey = generateKEXVal(scMacInfo.len, + this.hashName, + secret, + exchangeHash, + this.sessionID, + 'F'); + } + + const config = { + inbound: { + onPayload: this._protocol._onPayload, + seqno: this._protocol._decipher.inSeqno, + decipherInfo: (!isServer ? scCipherInfo : csCipherInfo), + decipherIV: (!isServer ? scIV : csIV), + decipherKey: (!isServer ? scKey : csKey), + macInfo: (!isServer ? scMacInfo : csMacInfo), + macKey: (!isServer ? scMacKey : csMacKey), + }, + outbound: { + onWrite: this._protocol._onWrite, + seqno: this._protocol._cipher.outSeqno, + cipherInfo: (isServer ? scCipherInfo : csCipherInfo), + cipherIV: (isServer ? scIV : csIV), + cipherKey: (isServer ? scKey : csKey), + macInfo: (isServer ? scMacInfo : csMacInfo), + macKey: (isServer ? scMacKey : csMacKey), + }, + }; + this._protocol._cipher && this._protocol._cipher.free(); + this._protocol._decipher && this._protocol._decipher.free(); + this._protocol._cipher = createCipher(config); + this._protocol._decipher = createDecipher(config); + + const rw = { + read: undefined, + write: undefined, + }; + switch (negotiated.cs.compress) { + case 'zlib': // starts immediately + if (isServer) + rw.read = new ZlibPacketReader(); + else + rw.write = new ZlibPacketWriter(this._protocol); + break; + case 'zlib@openssh.com': + // Starts after successful user authentication + + if (this._protocol._authenticated) { + // If a rekey happens and this compression method is selected and + // we already authenticated successfully, we need to start + // immediately instead + if (isServer) + rw.read = new ZlibPacketReader(); + else + rw.write = new ZlibPacketWriter(this._protocol); + break; + } + // FALLTHROUGH + default: + // none -- never any compression/decompression + + if (isServer) + rw.read = new PacketReader(); + else + rw.write = new PacketWriter(this._protocol); + } + switch (negotiated.sc.compress) { + case 'zlib': // starts immediately + if (isServer) + rw.write = new ZlibPacketWriter(this._protocol); + else + rw.read = new ZlibPacketReader(); + break; + case 'zlib@openssh.com': + // Starts after successful user authentication + + if (this._protocol._authenticated) { + // If a rekey happens and this compression method is selected and + // we already authenticated successfully, we need to start + // immediately instead + if (isServer) + rw.write = new ZlibPacketWriter(this._protocol); + else + rw.read = new ZlibPacketReader(); + break; + } + // FALLTHROUGH + default: + // none -- never any compression/decompression + + if (isServer) + rw.write = new PacketWriter(this._protocol); + else + rw.read = new PacketReader(); + } + this._protocol._packetRW.read.cleanup(); + this._protocol._packetRW.write.cleanup(); + this._protocol._packetRW = rw; + + // Cleanup/reset various state + this._public = null; + this._dh = null; + this._kexinit = this._protocol._kexinit = undefined; + this._remoteKexinit = undefined; + this._identRaw = undefined; + this._remoteIdentRaw = undefined; + this._hostKey = undefined; + this._dhData = undefined; + this._sig = undefined; + + this._protocol._onHandshakeComplete(negotiated); + + return false; + }; + if (!isServer) + return completeHandshake(); + this.finish = completeHandshake; + } + + start() { + if (!this._protocol._server) { + if (this._protocol._debug) { + let type; + switch (this.type) { + case 'group': + type = 'KEXDH_INIT'; + break; + case 'groupex': + type = 'KEXDH_GEX_INIT'; + break; + default: + type = 'KEXECDH_INIT'; + } + this._protocol._debug(`Outbound: Sending ${type}`); + } + + const pubKey = this.getPublicKey(); + + let p = this._protocol._packetRW.write.allocStartKEX; + const packet = this._protocol._packetRW.write.alloc( + 1 + 4 + pubKey.length, + true + ); + packet[p] = MESSAGE.KEXDH_INIT; + writeUInt32BE(packet, pubKey.length, ++p); + packet.set(pubKey, p += 4); + this._protocol._cipher.encrypt( + this._protocol._packetRW.write.finalize(packet, true) + ); + } + } + getPublicKey() { + this.generateKeys(); + + const key = this._public; + + if (key) + return this.convertPublicKey(key); + } + convertPublicKey(key) { + let newKey; + let idx = 0; + let len = key.length; + while (key[idx] === 0x00) { + ++idx; + --len; + } + + if (key[idx] & 0x80) { + newKey = Buffer.allocUnsafe(1 + len); + newKey[0] = 0; + key.copy(newKey, 1, idx); + return newKey; + } + + if (len !== key.length) { + newKey = Buffer.allocUnsafe(len); + key.copy(newKey, 0, idx); + key = newKey; + } + return key; + } + computeSecret(otherPublicKey) { + this.generateKeys(); + + try { + return convertToMpint(this._dh.computeSecret(otherPublicKey)); + } catch (ex) { + return ex; + } + } + parse(payload) { + const type = payload[0]; + switch (this._step) { + case 1: + if (this._protocol._server) { + // Server + if (type !== MESSAGE.KEXDH_INIT) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ${MESSAGE.KEXDH_INIT}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + // TODO: debug message + /* + byte SSH_MSG_KEXDH_INIT + / SSH_MSG_KEX_ECDH_INIT + string + */ + bufferParser.init(payload, 1); + const dhData = bufferParser.readString(); + bufferParser.clear(); + if (dhData === undefined) { + return doFatalError( + this._protocol, + 'Received malformed KEX*_INIT', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + // Client public key + this._dhData = dhData; + + let hostKey = this._protocol._hostKeys[this.negotiated.srvHostKey]; + if (Array.isArray(hostKey)) + hostKey = hostKey[0]; + this._hostKey = hostKey; + + this.finish(); + } else { + // Client + if (type !== MESSAGE.KEXDH_REPLY) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ${MESSAGE.KEXDH_REPLY}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + // TODO: debug message + /* + byte SSH_MSG_KEXDH_REPLY + / SSH_MSG_KEX_DH_GEX_REPLY + / SSH_MSG_KEX_ECDH_REPLY + string server public host key and certificates (K_S) + string + string signature of H + */ + bufferParser.init(payload, 1); + let hostPubKey; + let dhData; + let sig; + if ((hostPubKey = bufferParser.readString()) === undefined + || (dhData = bufferParser.readString()) === undefined + || (sig = bufferParser.readString()) === undefined) { + bufferParser.clear(); + return doFatalError( + this._protocol, + 'Received malformed KEX*_REPLY', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + bufferParser.clear(); + + // Check that the host public key type matches what was negotiated + // during KEXINIT swap + bufferParser.init(hostPubKey, 0); + const hostPubKeyType = bufferParser.readString(true); + bufferParser.clear(); + if (hostPubKeyType === undefined) { + return doFatalError( + this._protocol, + 'Received malformed host public key', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + if (hostPubKeyType !== this.negotiated.srvHostKey) { + // Check if we need to make an exception + switch (this.negotiated.srvHostKey) { + case 'rsa-sha2-256': + case 'rsa-sha2-512': + if (hostPubKeyType === 'ssh-rsa') + break; + // FALLTHROUGH + default: + return doFatalError( + this._protocol, + 'Host key does not match negotiated type', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + } + + this._hostKey = hostPubKey; + this._dhData = dhData; + this._sig = sig; + + let checked = false; + let ret; + if (this._protocol._hostVerifier === undefined) { + ret = true; + this._protocol._debug && this._protocol._debug( + 'Host accepted by default (no verification)' + ); + } else { + ret = this._protocol._hostVerifier(hostPubKey, (permitted) => { + if (checked) + return; + checked = true; + if (permitted === false) { + this._protocol._debug && this._protocol._debug( + 'Host denied (verification failed)' + ); + return doFatalError( + this._protocol, + 'Host denied (verification failed)', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + this._protocol._debug && this._protocol._debug( + 'Host accepted (verified)' + ); + this._hostVerified = true; + if (this._receivedNEWKEYS) + this.finish(); + }); + } + if (ret === undefined) { + // Async host verification + ++this._step; + return; + } + checked = true; + if (ret === false) { + this._protocol._debug && this._protocol._debug( + 'Host denied (verification failed)' + ); + return doFatalError( + this._protocol, + 'Host denied (verification failed)', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + this._protocol._debug && this._protocol._debug( + 'Host accepted (verified)' + ); + this._hostVerified = true; + } + ++this._step; + break; + case 2: + if (type !== MESSAGE.NEWKEYS) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ${MESSAGE.NEWKEYS}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + this._protocol._debug && this._protocol._debug( + 'Inbound: NEWKEYS' + ); + this._receivedNEWKEYS = true; + ++this._step; + if (this._protocol._server || this._hostVerified) + return this.finish(); + + // Signal to current decipher that we need to change to a new decipher + // for the next packet + return false; + default: + return doFatalError( + this._protocol, + `Received unexpected packet ${type} after NEWKEYS`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + } + } + + class Curve25519Exchange extends KeyExchange { + constructor(hashName, ...args) { + super(...args); + + this.type = '25519'; + this.hashName = hashName; + this._keys = null; + } + // TODO: start() + generateKeys() { + if (!this._keys) + this._keys = generateKeyPairSync('x25519'); + } + getPublicKey() { + this.generateKeys(); + + const key = this._keys.publicKey.export({ type: 'spki', format: 'der' }); + return key.slice(-32); // HACK: avoids parsing DER/BER header + } + convertPublicKey(key) { + let newKey; + let idx = 0; + let len = key.length; + while (key[idx] === 0x00) { + ++idx; + --len; + } + + if (key.length === 32) + return key; + + if (len !== key.length) { + newKey = Buffer.allocUnsafe(len); + key.copy(newKey, 0, idx); + key = newKey; + } + return key; + } + computeSecret(otherPublicKey) { + this.generateKeys(); + + try { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + // algorithm + asnWriter.startSequence(); + asnWriter.writeOID('1.3.101.110'); // id-X25519 + asnWriter.endSequence(); + + // PublicKey + asnWriter.startSequence(Ber.BitString); + asnWriter.writeByte(0x00); + // XXX: hack to write a raw buffer without a tag -- yuck + asnWriter._ensure(otherPublicKey.length); + otherPublicKey.copy(asnWriter._buf, + asnWriter._offset, + 0, + otherPublicKey.length); + asnWriter._offset += otherPublicKey.length; + asnWriter.endSequence(); + asnWriter.endSequence(); + + return convertToMpint(diffieHellman({ + privateKey: this._keys.privateKey, + publicKey: createPublicKey({ + key: asnWriter.buffer, + type: 'spki', + format: 'der', + }), + })); + } catch (ex) { + return ex; + } + } + } + + class ECDHExchange extends KeyExchange { + constructor(curveName, hashName, ...args) { + super(...args); + + this.type = 'ecdh'; + this.curveName = curveName; + this.hashName = hashName; + } + generateKeys() { + if (!this._dh) { + this._dh = createECDH(this.curveName); + this._public = this._dh.generateKeys(); + } + } + } + + class DHGroupExchange extends KeyExchange { + constructor(hashName, ...args) { + super(...args); + + this.type = 'groupex'; + this.hashName = hashName; + this._prime = null; + this._generator = null; + } + // TODO: start() + generateKeys() { + if (!this._dh && this._prime && this._generator) { + this._dh = createDiffieHellman(this._prime, this._generator); + this._public = this._dh.generateKeys(); + } + } + setDHParams(prime, generator) { + if (!Buffer.isBuffer(prime)) + throw new Error('Invalid prime value'); + if (!Buffer.isBuffer(generator)) + throw new Error('Invalid generator value'); + this._prime = prime; + this._generator = generator; + } + getDHParams() { + if (this._dh) { + return { + prime: convertToMpint(this._dh.getPrime()), + generator: convertToMpint(this._dh.getGenerator()), + }; + } + } + parse(payload) { + const type = payload[0]; + switch (this._step) { + case 1: + if (this._protocol._server) { + if (type !== MESSAGE.KEXDH_GEX_REQUEST) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ` + + MESSAGE.KEXDH_GEX_REQUEST, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + // TODO: debug message + // TODO: allow user implementation to provide safe prime and + // generator on demand to support group exchange on server side + return doFatalError( + this._protocol, + 'Group exchange not implemented for server', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + + if (type !== MESSAGE.KEXDH_GEX_GROUP) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_GROUP}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + // TODO: debug message + /* + byte SSH_MSG_KEX_DH_GEX_GROUP + mpint p, safe prime + mpint g, generator for subgroup in GF(p) + */ + bufferParser.init(payload, 1); + let prime; + let gen; + if ((prime = bufferParser.readString()) === undefined + || (gen = bufferParser.readString()) === undefined) { + bufferParser.clear(); + return doFatalError( + this._protocol, + 'Received malformed KEXDH_GEX_GROUP', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + bufferParser.clear(); + + // TODO: validate prime + this.setDHParams(prime, gen); + this.generateKeys(); + const pubkey = this.getPublicKey(); + + this._protocol._debug && this._protocol._debug( + 'Outbound: Sending KEXDH_GEX_INIT' + ); + + let p = this._protocol._packetRW.write.allocStartKEX; + const packet = + this._protocol._packetRW.write.alloc(1 + 4 + pubkey.length, true); + packet[p] = MESSAGE.KEXDH_GEX_INIT; + writeUInt32BE(packet, pubkey.length, ++p); + packet.set(pubkey, p += 4); + this._protocol._cipher.encrypt( + this._protocol._packetRW.write.finalize(packet, true) + ); + + ++this._step; + break; + case 2: + if (this._protocol._server) { + if (type !== MESSAGE.KEXDH_GEX_INIT) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_INIT}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + // TODO: debug message + return doFatalError( + this._protocol, + 'Group exchange not implemented for server', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } else if (type !== MESSAGE.KEXDH_GEX_REPLY) { + return doFatalError( + this._protocol, + `Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_REPLY}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + // TODO: debug message + this._step = 1; + payload[0] = MESSAGE.KEXDH_REPLY; + this.parse = KeyExchange.prototype.parse; + this.parse(payload); + } + } + } + + class DHExchange extends KeyExchange { + constructor(groupName, hashName, ...args) { + super(...args); + + this.type = 'group'; + this.groupName = groupName; + this.hashName = hashName; + } + start() { + if (!this._protocol._server) { + this._protocol._debug && this._protocol._debug( + 'Outbound: Sending KEXDH_INIT' + ); + const pubKey = this.getPublicKey(); + let p = this._protocol._packetRW.write.allocStartKEX; + const packet = + this._protocol._packetRW.write.alloc(1 + 4 + pubKey.length, true); + packet[p] = MESSAGE.KEXDH_INIT; + writeUInt32BE(packet, pubKey.length, ++p); + packet.set(pubKey, p += 4); + this._protocol._cipher.encrypt( + this._protocol._packetRW.write.finalize(packet, true) + ); + } + } + generateKeys() { + if (!this._dh) { + this._dh = createDiffieHellmanGroup(this.groupName); + this._public = this._dh.generateKeys(); + } + } + getDHParams() { + if (this._dh) { + return { + prime: convertToMpint(this._dh.getPrime()), + generator: convertToMpint(this._dh.getGenerator()), + }; + } + } + } + + return (negotiated, ...args) => { + if (typeof negotiated !== 'object' || negotiated === null) + throw new Error('Invalid negotiated argument'); + const kexType = negotiated.kex; + if (typeof kexType === 'string') { + args = [negotiated, ...args]; + switch (kexType) { + case 'curve25519-sha256': + case 'curve25519-sha256@libssh.org': + if (!curve25519Supported) + break; + return new Curve25519Exchange('sha256', ...args); + + case 'ecdh-sha2-nistp256': + return new ECDHExchange('prime256v1', 'sha256', ...args); + case 'ecdh-sha2-nistp384': + return new ECDHExchange('secp384r1', 'sha384', ...args); + case 'ecdh-sha2-nistp521': + return new ECDHExchange('secp521r1', 'sha512', ...args); + + case 'diffie-hellman-group1-sha1': + return new DHExchange('modp2', 'sha1', ...args); + case 'diffie-hellman-group14-sha1': + return new DHExchange('modp14', 'sha1', ...args); + case 'diffie-hellman-group14-sha256': + return new DHExchange('modp14', 'sha256', ...args); + case 'diffie-hellman-group15-sha512': + return new DHExchange('modp15', 'sha512', ...args); + case 'diffie-hellman-group16-sha512': + return new DHExchange('modp16', 'sha512', ...args); + case 'diffie-hellman-group17-sha512': + return new DHExchange('modp17', 'sha512', ...args); + case 'diffie-hellman-group18-sha512': + return new DHExchange('modp18', 'sha512', ...args); + + case 'diffie-hellman-group-exchange-sha1': + return new DHGroupExchange('sha1', ...args); + case 'diffie-hellman-group-exchange-sha256': + return new DHGroupExchange('sha256', ...args); + } + throw new Error(`Unsupported key exchange algorithm: ${kexType}`); + } + throw new Error(`Invalid key exchange type: ${kexType}`); + }; +})(); + +const KexInit = (() => { + const KEX_PROPERTY_NAMES = [ + 'kex', + 'srvHostKey', + ['cs', 'cipher' ], + ['sc', 'cipher' ], + ['cs', 'mac' ], + ['sc', 'mac' ], + ['cs', 'compress' ], + ['sc', 'compress' ], + ['cs', 'lang' ], + ['sc', 'lang' ], + ]; + return class KexInit { + constructor(obj) { + if (typeof obj !== 'object' || obj === null) + throw new TypeError('Argument must be an object'); + + const lists = { + kex: undefined, + srvHostKey: undefined, + cs: { + cipher: undefined, + mac: undefined, + compress: undefined, + lang: undefined, + }, + sc: { + cipher: undefined, + mac: undefined, + compress: undefined, + lang: undefined, + }, + + all: undefined, + }; + let totalSize = 0; + for (const prop of KEX_PROPERTY_NAMES) { + let base; + let val; + let desc; + let key; + if (typeof prop === 'string') { + base = lists; + val = obj[prop]; + desc = key = prop; + } else { + const parent = prop[0]; + base = lists[parent]; + key = prop[1]; + val = obj[parent][key]; + desc = `${parent}.${key}`; + } + const entry = { array: undefined, buffer: undefined }; + if (Buffer.isBuffer(val)) { + entry.array = ('' + val).split(','); + entry.buffer = val; + totalSize += 4 + val.length; + } else { + if (typeof val === 'string') + val = val.split(','); + if (Array.isArray(val)) { + entry.array = val; + entry.buffer = Buffer.from(val.join(',')); + } else { + throw new TypeError(`Invalid \`${desc}\` type: ${typeof val}`); + } + totalSize += 4 + entry.buffer.length; + } + base[key] = entry; + } + + const all = Buffer.allocUnsafe(totalSize); + lists.all = all; + + let allPos = 0; + for (const prop of KEX_PROPERTY_NAMES) { + let data; + if (typeof prop === 'string') + data = lists[prop].buffer; + else + data = lists[prop[0]][prop[1]].buffer; + allPos = writeUInt32BE(all, data.length, allPos); + all.set(data, allPos); + allPos += data.length; + } + + this.totalSize = totalSize; + this.lists = lists; + } + copyAllTo(buf, offset) { + const src = this.lists.all; + if (typeof offset !== 'number') + throw new TypeError(`Invalid offset value: ${typeof offset}`); + if (buf.length - offset < src.length) + throw new Error('Insufficient space to copy list'); + buf.set(src, offset); + return src.length; + } + }; +})(); + +const hashString = (() => { + const LEN = Buffer.allocUnsafe(4); + return (hash, buf) => { + writeUInt32BE(LEN, buf.length, 0); + hash.update(LEN); + hash.update(buf); + }; +})(); + +function generateKEXVal(len, hashName, secret, exchangeHash, sessionID, char) { + let ret; + if (len) { + let digest = createHash(hashName) + .update(secret) + .update(exchangeHash) + .update(char) + .update(sessionID) + .digest(); + while (digest.length < len) { + const chunk = createHash(hashName) + .update(secret) + .update(exchangeHash) + .update(digest) + .digest(); + const extended = Buffer.allocUnsafe(digest.length + chunk.length); + extended.set(digest, 0); + extended.set(chunk, digest.length); + digest = extended; + } + if (digest.length === len) + ret = digest; + else + ret = new FastBuffer(digest.buffer, digest.byteOffset, len); + } else { + ret = EMPTY_BUFFER; + } + return ret; +} + +function onKEXPayload(state, payload) { + // XXX: move this to the Decipher implementations? + if (payload.length === 0) { + this._debug && this._debug('Inbound: Skipping empty packet payload'); + return; + } + + if (this._skipNextInboundPacket) { + this._skipNextInboundPacket = false; + return; + } + + payload = this._packetRW.read.read(payload); + + const type = payload[0]; + switch (type) { + case MESSAGE.DISCONNECT: + case MESSAGE.IGNORE: + case MESSAGE.UNIMPLEMENTED: + case MESSAGE.DEBUG: + if (!MESSAGE_HANDLERS) + MESSAGE_HANDLERS = require('./handlers.js'); + return MESSAGE_HANDLERS[type](this, payload); + case MESSAGE.KEXINIT: + if (!state.firstPacket) { + return doFatalError( + this, + 'Received extra KEXINIT during handshake', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + state.firstPacket = false; + return handleKexInit(this, payload); + default: + if (type < 20 || type > 49) { + return doFatalError( + this, + `Received unexpected packet type ${type}`, + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + } + + return this._kex.parse(payload); +} + +function dhEstimate(neg) { + const bits = Math.max( + 0, + (neg.cs.cipher.sslName === 'des-ede3-cbc' ? 14 : neg.cs.cipher.keyLen), + neg.cs.cipher.blockLen, + neg.cs.cipher.ivLen, + neg.cs.mac.actualLen, + (neg.sc.cipher.sslName === 'des-ede3-cbc' ? 14 : neg.sc.cipher.keyLen), + neg.sc.cipher.blockLen, + neg.sc.cipher.ivLen, + neg.sc.mac.actualLen + ) * 8; + if (bits <= 112) + return 2048; + if (bits <= 128) + return 3072; + if (bits <= 192) + return 7680; + return 8192; +} + +module.exports = { + KexInit, + kexinit, + onKEXPayload, + DEFAULT_KEXINIT: new KexInit({ + kex: DEFAULT_KEX, + srvHostKey: DEFAULT_SERVER_HOST_KEY, + cs: { + cipher: DEFAULT_CIPHER, + mac: DEFAULT_MAC, + compress: DEFAULT_COMPRESSION, + lang: [], + }, + sc: { + cipher: DEFAULT_CIPHER, + mac: DEFAULT_MAC, + compress: DEFAULT_COMPRESSION, + lang: [], + }, + }), + HANDLERS: { + [MESSAGE.KEXINIT]: handleKexInit, + }, +}; diff --git a/lib/protocol/keyParser.js b/lib/protocol/keyParser.js new file mode 100644 index 00000000..17d4e277 --- /dev/null +++ b/lib/protocol/keyParser.js @@ -0,0 +1,1389 @@ +// TODO: +// * utilize `crypto.create(Private|Public)Key()` and `keyObject.export()` +// * handle multi-line header values (OpenSSH)? +// * more thorough validation? +'use strict'; + +const { + createDecipheriv, + createECDH, + createHash, + createHmac, + createSign, + createVerify, + getCiphers, + sign: sign_, + verify: verify_, +} = require('crypto'); +const supportedOpenSSLCiphers = getCiphers(); + +const { Ber } = require('asn1'); +const bcrypt_pbkdf = require('bcrypt-pbkdf').pbkdf; + +const { CIPHER_INFO } = require('./crypto.js'); +const { eddsaSupported, SUPPORTED_CIPHER } = require('./constants.js'); +const { + bufferSlice, + readString, + readUInt32BE, + writeUInt32BE, +} = require('./utils.js'); + +const SYM_HASH_ALGO = Symbol('Hash Algorithm'); +const SYM_PRIV_PEM = Symbol('Private key PEM'); +const SYM_PUB_PEM = Symbol('Public key PEM'); +const SYM_PUB_SSH = Symbol('Public key SSH'); +const SYM_DECRYPTED = Symbol('Decrypted Key'); + +// Create OpenSSL cipher name -> SSH cipher name conversion table +const CIPHER_INFO_OPENSSL = Object.create(null); +{ + const keys = Object.keys(CIPHER_INFO); + for (let i = 0; i < keys.length; ++i) { + const cipherName = CIPHER_INFO[keys[i]].sslName; + if (!cipherName || CIPHER_INFO_OPENSSL[cipherName]) + continue; + CIPHER_INFO_OPENSSL[cipherName] = CIPHER_INFO[keys[i]]; + } +} + +function makePEM(type, data) { + data = data.base64Slice(0, data.length); + let formatted = data.replace(/.{64}/g, '$&\n'); + if (data.length & 63) + formatted += '\n'; + return `-----BEGIN ${type} KEY-----\n${formatted}-----END ${type} KEY-----`; +} + +function combineBuffers(buf1, buf2) { + const result = Buffer.allocUnsafe(buf1.length + buf2.length); + result.set(buf1, 0); + result.set(buf2, buf1.length); + return result; +} + +function skipFields(buf, nfields) { + const bufLen = buf.length; + let pos = (buf._pos || 0); + for (let i = 0; i < nfields; ++i) { + const left = (bufLen - pos); + if (pos >= bufLen || left < 4) + return false; + const len = readUInt32BE(buf, pos); + if (left < 4 + len) + return false; + pos += 4 + len; + } + buf._pos = pos; + return true; +} + +function genOpenSSLRSAPub(n, e) { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + // algorithm + asnWriter.startSequence(); + asnWriter.writeOID('1.2.840.113549.1.1.1'); // rsaEncryption + // algorithm parameters (RSA has none) + asnWriter.writeNull(); + asnWriter.endSequence(); + + // subjectPublicKey + asnWriter.startSequence(Ber.BitString); + asnWriter.writeByte(0x00); + asnWriter.startSequence(); + asnWriter.writeBuffer(n, Ber.Integer); + asnWriter.writeBuffer(e, Ber.Integer); + asnWriter.endSequence(); + asnWriter.endSequence(); + asnWriter.endSequence(); + return makePEM('PUBLIC', asnWriter.buffer); +} + +function genOpenSSHRSAPub(n, e) { + const publicKey = Buffer.allocUnsafe(4 + 7 + 4 + e.length + 4 + n.length); + + writeUInt32BE(publicKey, 7, 0); + publicKey.utf8Write('ssh-rsa', 4, 7); + + let i = 4 + 7; + writeUInt32BE(publicKey, e.length, i); + publicKey.set(e, i += 4); + + writeUInt32BE(publicKey, n.length, i += e.length); + publicKey.set(n, i + 4); + + return publicKey; +} + +const genOpenSSLRSAPriv = (() => { + function genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp) { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + asnWriter.writeInt(0x00, Ber.Integer); + asnWriter.writeBuffer(n, Ber.Integer); + asnWriter.writeBuffer(e, Ber.Integer); + asnWriter.writeBuffer(d, Ber.Integer); + asnWriter.writeBuffer(p, Ber.Integer); + asnWriter.writeBuffer(q, Ber.Integer); + asnWriter.writeBuffer(dmp1, Ber.Integer); + asnWriter.writeBuffer(dmq1, Ber.Integer); + asnWriter.writeBuffer(iqmp, Ber.Integer); + asnWriter.endSequence(); + return asnWriter.buffer; + } + + function bigIntFromBuffer(buf) { + return BigInt(`0x${buf.hexSlice(0, buf.length)}`); + } + + function bigIntToBuffer(bn) { + let hex = bn.toString(16); + if ((hex.length & 1) !== 0) { + hex = `0${hex}`; + } else { + const sigbit = hex.charCodeAt(0); + // BER/DER integers require leading zero byte to denote a positive value + // when first byte >= 0x80 + if (sigbit === 56 || (sigbit >= 97 && sigbit <= 102)) + hex = `00${hex}`; + } + return Buffer.from(hex, 'hex'); + } + + return function genOpenSSLRSAPriv(n, e, d, iqmp, p, q) { + const bn_d = bigIntFromBuffer(d); + const dmp1 = bigIntToBuffer(bn_d % (bigIntFromBuffer(p) - 1n)); + const dmq1 = bigIntToBuffer(bn_d % (bigIntFromBuffer(q) - 1n)); + return makePEM('RSA PRIVATE', + genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp)); + }; +})(); + +function genOpenSSLDSAPub(p, q, g, y) { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + // algorithm + asnWriter.startSequence(); + asnWriter.writeOID('1.2.840.10040.4.1'); // id-dsa + // algorithm parameters + asnWriter.startSequence(); + asnWriter.writeBuffer(p, Ber.Integer); + asnWriter.writeBuffer(q, Ber.Integer); + asnWriter.writeBuffer(g, Ber.Integer); + asnWriter.endSequence(); + asnWriter.endSequence(); + + // subjectPublicKey + asnWriter.startSequence(Ber.BitString); + asnWriter.writeByte(0x00); + asnWriter.writeBuffer(y, Ber.Integer); + asnWriter.endSequence(); + asnWriter.endSequence(); + return makePEM('PUBLIC', asnWriter.buffer); +} + +function genOpenSSHDSAPub(p, q, g, y) { + const publicKey = Buffer.allocUnsafe( + 4 + 7 + 4 + p.length + 4 + q.length + 4 + g.length + 4 + y.length + ); + + writeUInt32BE(publicKey, 7, 0); + publicKey.utf8Write('ssh-dss', 4, 7); + + let i = 4 + 7; + writeUInt32BE(publicKey, p.length, i); + publicKey.set(p, i += 4); + + writeUInt32BE(publicKey, q.length, i += p.length); + publicKey.set(q, i += 4); + + writeUInt32BE(publicKey, g.length, i += q.length); + publicKey.set(g, i += 4); + + writeUInt32BE(publicKey, y.length, i += g.length); + publicKey.set(y, i + 4); + + return publicKey; +} + +function genOpenSSLDSAPriv(p, q, g, y, x) { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + asnWriter.writeInt(0x00, Ber.Integer); + asnWriter.writeBuffer(p, Ber.Integer); + asnWriter.writeBuffer(q, Ber.Integer); + asnWriter.writeBuffer(g, Ber.Integer); + asnWriter.writeBuffer(y, Ber.Integer); + asnWriter.writeBuffer(x, Ber.Integer); + asnWriter.endSequence(); + return makePEM('DSA PRIVATE', asnWriter.buffer); +} + +function genOpenSSLEdPub(pub) { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + // algorithm + asnWriter.startSequence(); + asnWriter.writeOID('1.3.101.112'); // id-Ed25519 + asnWriter.endSequence(); + + // PublicKey + asnWriter.startSequence(Ber.BitString); + asnWriter.writeByte(0x00); + // XXX: hack to write a raw buffer without a tag -- yuck + asnWriter._ensure(pub.length); + asnWriter._buf.set(pub, asnWriter._offset); + asnWriter._offset += pub.length; + asnWriter.endSequence(); + asnWriter.endSequence(); + return makePEM('PUBLIC', asnWriter.buffer); +} + +function genOpenSSHEdPub(pub) { + const publicKey = Buffer.allocUnsafe(4 + 11 + 4 + pub.length); + + writeUInt32BE(publicKey, 11, 0); + publicKey.utf8Write('ssh-ed25519', 4, 11); + + writeUInt32BE(publicKey, pub.length, 15); + publicKey.set(pub, 19); + + return publicKey; +} + +function genOpenSSLEdPriv(priv) { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + // version + asnWriter.writeInt(0x00, Ber.Integer); + + // algorithm + asnWriter.startSequence(); + asnWriter.writeOID('1.3.101.112'); // id-Ed25519 + asnWriter.endSequence(); + + // PrivateKey + asnWriter.startSequence(Ber.OctetString); + asnWriter.writeBuffer(priv, Ber.OctetString); + asnWriter.endSequence(); + asnWriter.endSequence(); + return makePEM('PRIVATE', asnWriter.buffer); +} + +function genOpenSSLECDSAPub(oid, Q) { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + // algorithm + asnWriter.startSequence(); + asnWriter.writeOID('1.2.840.10045.2.1'); // id-ecPublicKey + // algorithm parameters (namedCurve) + asnWriter.writeOID(oid); + asnWriter.endSequence(); + + // subjectPublicKey + asnWriter.startSequence(Ber.BitString); + asnWriter.writeByte(0x00); + // XXX: hack to write a raw buffer without a tag -- yuck + asnWriter._ensure(Q.length); + asnWriter._buf.set(Q, asnWriter._offset); + asnWriter._offset += Q.length; + // end hack + asnWriter.endSequence(); + asnWriter.endSequence(); + return makePEM('PUBLIC', asnWriter.buffer); +} + +function genOpenSSHECDSAPub(oid, Q) { + let curveName; + switch (oid) { + case '1.2.840.10045.3.1.7': + // prime256v1/secp256r1 + curveName = 'nistp256'; + break; + case '1.3.132.0.34': + // secp384r1 + curveName = 'nistp384'; + break; + case '1.3.132.0.35': + // secp521r1 + curveName = 'nistp521'; + break; + default: + return; + } + + const publicKey = Buffer.allocUnsafe(4 + 19 + 4 + 8 + 4 + Q.length); + + writeUInt32BE(publicKey, 19, 0); + publicKey.utf8Write(`ecdsa-sha2-${curveName}`, 4, 19); + + writeUInt32BE(publicKey, 8, 23); + publicKey.utf8Write(curveName, 27, 8); + + writeUInt32BE(publicKey, Q.length, 35); + publicKey.set(Q, 39); + + return publicKey; +} + +function genOpenSSLECDSAPriv(oid, pub, priv) { + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + // version + asnWriter.writeInt(0x01, Ber.Integer); + // privateKey + asnWriter.writeBuffer(priv, Ber.OctetString); + // parameters (optional) + asnWriter.startSequence(0xA0); + asnWriter.writeOID(oid); + asnWriter.endSequence(); + // publicKey (optional) + asnWriter.startSequence(0xA1); + asnWriter.startSequence(Ber.BitString); + asnWriter.writeByte(0x00); + // XXX: hack to write a raw buffer without a tag -- yuck + asnWriter._ensure(pub.length); + asnWriter._buf.set(pub, asnWriter._offset); + asnWriter._offset += pub.length; + // end hack + asnWriter.endSequence(); + asnWriter.endSequence(); + asnWriter.endSequence(); + return makePEM('EC PRIVATE', asnWriter.buffer); +} + +function genOpenSSLECDSAPubFromPriv(curveName, priv) { + const tempECDH = createECDH(curveName); + tempECDH.setPrivateKey(priv); + return tempECDH.getPublicKey(); +} + +const BaseKey = { + sign: (() => { + if (typeof sign_ === 'function') { + return function sign(data, algo) { + const pem = this[SYM_PRIV_PEM]; + if (pem === null) + return new Error('No private key available'); + if (!algo || typeof algo !== 'string') + algo = this[SYM_HASH_ALGO]; + try { + return sign_(algo, data, pem); + } catch (ex) { + return ex; + } + }; + } + return function sign(data, algo) { + const pem = this[SYM_PRIV_PEM]; + if (pem === null) + return new Error('No private key available'); + if (!algo || typeof algo !== 'string') + algo = this[SYM_HASH_ALGO]; + const signature = createSign(algo); + signature.update(data); + try { + return signature.sign(pem); + } catch (ex) { + return ex; + } + }; + })(), + verify: (() => { + if (typeof verify_ === 'function') { + return function verify(data, signature, algo) { + const pem = this[SYM_PUB_PEM]; + if (pem === null) + return new Error('No public key available'); + if (!algo || typeof algo !== 'string') + algo = this[SYM_HASH_ALGO]; + try { + return verify_(algo, data, pem, signature); + } catch (ex) { + return ex; + } + }; + } + return function verify(data, signature, algo) { + const pem = this[SYM_PUB_PEM]; + if (pem === null) + return new Error('No public key available'); + if (!algo || typeof algo !== 'string') + algo = this[SYM_HASH_ALGO]; + const verifier = createVerify(algo); + verifier.update(data); + try { + return verifier.verify(pem, signature); + } catch (ex) { + return ex; + } + }; + })(), + getPrivatePEM: function getPrivatePEM() { + return this[SYM_PRIV_PEM]; + }, + getPublicPEM: function getPublicPEM() { + return this[SYM_PUB_PEM]; + }, + getPublicSSH: function getPublicSSH() { + return this[SYM_PUB_SSH]; + }, +}; + + +function OpenSSH_Private(type, comment, privPEM, pubPEM, pubSSH, algo, + decrypted) { + this.type = type; + this.comment = comment; + this[SYM_PRIV_PEM] = privPEM; + this[SYM_PUB_PEM] = pubPEM; + this[SYM_PUB_SSH] = pubSSH; + this[SYM_HASH_ALGO] = algo; + this[SYM_DECRYPTED] = decrypted; +} +OpenSSH_Private.prototype = BaseKey; +{ + const regexp = /^-----BEGIN OPENSSH PRIVATE KEY-----(?:\r\n|\n)([\s\S]+)(?:\r\n|\n)-----END OPENSSH PRIVATE KEY-----$/; + OpenSSH_Private.parse = (str, passphrase) => { + const m = regexp.exec(str); + if (m === null) + return null; + let ret; + const data = Buffer.from(m[1], 'base64'); + if (data.length < 31) // magic (+ magic null term.) + minimum field lengths + return new Error('Malformed OpenSSH private key'); + const magic = data.utf8Slice(0, 15); + if (magic !== 'openssh-key-v1\0') + return new Error(`Unsupported OpenSSH key magic: ${magic}`); + + const cipherName = readString(data, 15, true); + if (cipherName === undefined) + return new Error('Malformed OpenSSH private key'); + if (cipherName !== 'none' && SUPPORTED_CIPHER.indexOf(cipherName) === -1) + return new Error(`Unsupported cipher for OpenSSH key: ${cipherName}`); + + const kdfName = readString(data, data._pos, true); + if (kdfName === undefined) + return new Error('Malformed OpenSSH private key'); + if (kdfName !== 'none') { + if (cipherName === 'none') + return new Error('Malformed OpenSSH private key'); + if (kdfName !== 'bcrypt') + return new Error(`Unsupported kdf name for OpenSSH key: ${kdfName}`); + if (!passphrase) { + return new Error( + 'Encrypted private OpenSSH key detected, but no passphrase given' + ); + } + } else if (cipherName !== 'none') { + return new Error('Malformed OpenSSH private key'); + } + + let encInfo; + let cipherKey; + let cipherIV; + if (cipherName !== 'none') + encInfo = CIPHER_INFO[cipherName]; + const kdfOptions = readString(data, data._pos); + if (kdfOptions === undefined) + return new Error('Malformed OpenSSH private key'); + if (kdfOptions.length) { + switch (kdfName) { + case 'none': + return new Error('Malformed OpenSSH private key'); + case 'bcrypt': + /* + string salt + uint32 rounds + */ + const salt = readString(kdfOptions, 0); + if (salt === undefined || kdfOptions._pos + 4 > kdfOptions.length) + return new Error('Malformed OpenSSH private key'); + const rounds = readUInt32BE(kdfOptions, kdfOptions._pos); + const gen = Buffer.allocUnsafe(encInfo.keyLen + encInfo.ivLen); + const r = bcrypt_pbkdf(passphrase, + passphrase.length, + salt, + salt.length, + gen, + gen.length, + rounds); + if (r !== 0) + return new Error('Failed to generate information to decrypt key'); + cipherKey = bufferSlice(gen, 0, encInfo.keyLen); + cipherIV = bufferSlice(gen, encInfo.keyLen, gen.length); + break; + } + } else if (kdfName !== 'none') { + return new Error('Malformed OpenSSH private key'); + } + + if (data._pos + 3 >= data.length) + return new Error('Malformed OpenSSH private key'); + const keyCount = readUInt32BE(data, data._pos); + data._pos += 4; + + if (keyCount > 0) { + // TODO: place sensible limit on max `keyCount` + + // Read public keys first + for (let i = 0; i < keyCount; ++i) { + const pubData = readString(data, data._pos); + if (pubData === undefined) + return new Error('Malformed OpenSSH private key'); + const type = readString(pubData, 0, true); + if (type === undefined) + return new Error('Malformed OpenSSH private key'); + } + + let privBlob = readString(data, data._pos); + if (privBlob === undefined) + return new Error('Malformed OpenSSH private key'); + + if (cipherKey !== undefined) { + // Encrypted private key(s) + if (privBlob.length < encInfo.blockLen + || (privBlob.length % encInfo.blockLen) !== 0) { + return new Error('Malformed OpenSSH private key'); + } + try { + const options = { authTagLength: encInfo.authLen }; + const decipher = createDecipheriv(encInfo.sslName, + cipherKey, + cipherIV, + options); + if (encInfo.authLen > 0) { + if (data.length - data._pos < encInfo.authLen) + return new Error('Malformed OpenSSH private key'); + decipher.setAuthTag( + bufferSlice(data, data._pos, data._pos += encInfo.authLen) + ); + } + privBlob = combineBuffers(decipher.update(privBlob), + decipher.final()); + } catch (ex) { + return ex; + } + } + // Nothing should we follow the private key(s), except a possible + // authentication tag for relevant ciphers + if (data._pos !== data.length) + return new Error('Malformed OpenSSH private key'); + + ret = parseOpenSSHPrivKeys(privBlob, keyCount, cipherKey !== undefined); + } else { + ret = []; + } + return ret; + }; + + function parseOpenSSHPrivKeys(data, nkeys, decrypted) { + const keys = []; + /* + uint32 checkint + uint32 checkint + string privatekey1 + string comment1 + string privatekey2 + string comment2 + ... + string privatekeyN + string commentN + char 1 + char 2 + char 3 + ... + char padlen % 255 + */ + if (data.length < 8) + return new Error('Malformed OpenSSH private key'); + const check1 = readUInt32BE(data, 0); + const check2 = readUInt32BE(data, 4); + if (check1 !== check2) { + if (decrypted) { + return new Error( + 'OpenSSH key integrity check failed -- bad passphrase?' + ); + } + return new Error('OpenSSH key integrity check failed'); + } + data._pos = 8; + let i; + let oid; + for (i = 0; i < nkeys; ++i) { + let algo; + let privPEM; + let pubPEM; + let pubSSH; + // The OpenSSH documentation for the key format actually lies, the + // entirety of the private key content is not contained with a string + // field, it's actually the literal contents of the private key, so to be + // able to find the end of the key data you need to know the layout/format + // of each key type ... + const type = readString(data, data._pos, true); + if (type === undefined) + return new Error('Malformed OpenSSH private key'); + + switch (type) { + case 'ssh-rsa': { + /* + string n -- public + string e -- public + string d -- private + string iqmp -- private + string p -- private + string q -- private + */ + const n = readString(data, data._pos); + if (n === undefined) + return new Error('Malformed OpenSSH private key'); + const e = readString(data, data._pos); + if (e === undefined) + return new Error('Malformed OpenSSH private key'); + const d = readString(data, data._pos); + if (d === undefined) + return new Error('Malformed OpenSSH private key'); + const iqmp = readString(data, data._pos); + if (iqmp === undefined) + return new Error('Malformed OpenSSH private key'); + const p = readString(data, data._pos); + if (p === undefined) + return new Error('Malformed OpenSSH private key'); + const q = readString(data, data._pos); + if (q === undefined) + return new Error('Malformed OpenSSH private key'); + + pubPEM = genOpenSSLRSAPub(n, e); + pubSSH = genOpenSSHRSAPub(n, e); + privPEM = genOpenSSLRSAPriv(n, e, d, iqmp, p, q); + algo = 'sha1'; + break; + } + case 'ssh-dss': { + /* + string p -- public + string q -- public + string g -- public + string y -- public + string x -- private + */ + const p = readString(data, data._pos); + if (p === undefined) + return new Error('Malformed OpenSSH private key'); + const q = readString(data, data._pos); + if (q === undefined) + return new Error('Malformed OpenSSH private key'); + const g = readString(data, data._pos); + if (g === undefined) + return new Error('Malformed OpenSSH private key'); + const y = readString(data, data._pos); + if (y === undefined) + return new Error('Malformed OpenSSH private key'); + const x = readString(data, data._pos); + if (x === undefined) + return new Error('Malformed OpenSSH private key'); + + pubPEM = genOpenSSLDSAPub(p, q, g, y); + pubSSH = genOpenSSHDSAPub(p, q, g, y); + privPEM = genOpenSSLDSAPriv(p, q, g, y, x); + algo = 'sha1'; + break; + } + case 'ssh-ed25519': { + if (!eddsaSupported) + return new Error(`Unsupported OpenSSH private key type: ${type}`); + /* + * string public key + * string private key + public key + */ + const edpub = readString(data, data._pos); + if (edpub === undefined || edpub.length !== 32) + return new Error('Malformed OpenSSH private key'); + const edpriv = readString(data, data._pos); + if (edpriv === undefined || edpriv.length !== 64) + return new Error('Malformed OpenSSH private key'); + + pubPEM = genOpenSSLEdPub(edpub); + pubSSH = genOpenSSHEdPub(edpub); + privPEM = genOpenSSLEdPriv(bufferSlice(edpriv, 0, 32)); + algo = null; + break; + } + case 'ecdsa-sha2-nistp256': + algo = 'sha256'; + oid = '1.2.840.10045.3.1.7'; + // FALLTHROUGH + case 'ecdsa-sha2-nistp384': + if (algo === undefined) { + algo = 'sha384'; + oid = '1.3.132.0.34'; + } + // FALLTHROUGH + case 'ecdsa-sha2-nistp521': { + if (algo === undefined) { + algo = 'sha512'; + oid = '1.3.132.0.35'; + } + /* + string curve name + string Q -- public + string d -- private + */ + // TODO: validate curve name against type + if (!skipFields(data, 1)) // Skip curve name + return new Error('Malformed OpenSSH private key'); + const ecpub = readString(data, data._pos); + if (ecpub === undefined) + return new Error('Malformed OpenSSH private key'); + const ecpriv = readString(data, data._pos); + if (ecpriv === undefined) + return new Error('Malformed OpenSSH private key'); + + pubPEM = genOpenSSLECDSAPub(oid, ecpub); + pubSSH = genOpenSSHECDSAPub(oid, ecpub); + privPEM = genOpenSSLECDSAPriv(oid, ecpub, ecpriv); + break; + } + default: + return new Error(`Unsupported OpenSSH private key type: ${type}`); + } + + const privComment = readString(data, data._pos, true); + if (privComment === undefined) + return new Error('Malformed OpenSSH private key'); + + keys.push( + new OpenSSH_Private(type, privComment, privPEM, pubPEM, pubSSH, algo, + decrypted) + ); + } + let cnt = 0; + for (i = data._pos; i < data.length; ++i) { + if (data[i] !== (++cnt % 255)) + return new Error('Malformed OpenSSH private key'); + } + + return keys; + } +} + + +function OpenSSH_Old_Private(type, comment, privPEM, pubPEM, pubSSH, algo, + decrypted) { + this.type = type; + this.comment = comment; + this[SYM_PRIV_PEM] = privPEM; + this[SYM_PUB_PEM] = pubPEM; + this[SYM_PUB_SSH] = pubSSH; + this[SYM_HASH_ALGO] = algo; + this[SYM_DECRYPTED] = decrypted; +} +OpenSSH_Old_Private.prototype = BaseKey; +{ + const regexp = /^-----BEGIN (RSA|DSA|EC) PRIVATE KEY-----(?:\r\n|\n)((?:[^:]+:\s*[\S].*(?:\r\n|\n))*)([\s\S]+)(?:\r\n|\n)-----END (RSA|DSA|EC) PRIVATE KEY-----$/; + OpenSSH_Old_Private.parse = (str, passphrase) => { + const m = regexp.exec(str); + if (m === null) + return null; + let privBlob = Buffer.from(m[3], 'base64'); + let headers = m[2]; + let decrypted = false; + if (headers !== undefined) { + // encrypted key + headers = headers.split(/\r\n|\n/g); + for (let i = 0; i < headers.length; ++i) { + const header = headers[i]; + let sepIdx = header.indexOf(':'); + if (header.slice(0, sepIdx) === 'DEK-Info') { + const val = header.slice(sepIdx + 2); + sepIdx = val.indexOf(','); + if (sepIdx === -1) + continue; + const cipherName = val.slice(0, sepIdx).toLowerCase(); + if (supportedOpenSSLCiphers.indexOf(cipherName) === -1) { + return new Error( + `Cipher (${cipherName}) not supported ` + + 'for encrypted OpenSSH private key' + ); + } + const encInfo = CIPHER_INFO_OPENSSL[cipherName]; + if (!encInfo) { + return new Error( + `Cipher (${cipherName}) not supported ` + + 'for encrypted OpenSSH private key' + ); + } + const cipherIV = Buffer.from(val.slice(sepIdx + 1), 'hex'); + if (cipherIV.length !== encInfo.ivLen) + return new Error('Malformed encrypted OpenSSH private key'); + if (!passphrase) { + return new Error( + 'Encrypted OpenSSH private key detected, but no passphrase given' + ); + } + const ivSlice = bufferSlice(cipherIV, 0, 8); + let cipherKey = createHash('md5') + .update(passphrase) + .update(ivSlice) + .digest(); + while (cipherKey.length < encInfo.keyLen) { + cipherKey = combineBuffers( + cipherKey, + createHash('md5') + .update(cipherKey) + .update(passphrase) + .update(ivSlice) + .digest() + ); + } + if (cipherKey.length > encInfo.keyLen) + cipherKey = bufferSlice(cipherKey, 0, encInfo.keyLen); + try { + const decipher = createDecipheriv(cipherName, cipherKey, cipherIV); + decipher.setAutoPadding(false); + privBlob = combineBuffers(decipher.update(privBlob), + decipher.final()); + decrypted = true; + } catch (ex) { + return ex; + } + } + } + } + + let type; + let privPEM; + let pubPEM; + let pubSSH; + let algo; + let reader; + let errMsg = 'Malformed OpenSSH private key'; + if (decrypted) + errMsg += '. Bad passphrase?'; + switch (m[1]) { + case 'RSA': + type = 'ssh-rsa'; + privPEM = makePEM('RSA PRIVATE', privBlob); + try { + reader = new Ber.Reader(privBlob); + reader.readSequence(); + reader.readInt(); // skip version + const n = reader.readString(Ber.Integer, true); + if (n === null) + return new Error(errMsg); + const e = reader.readString(Ber.Integer, true); + if (e === null) + return new Error(errMsg); + pubPEM = genOpenSSLRSAPub(n, e); + pubSSH = genOpenSSHRSAPub(n, e); + } catch { + return new Error(errMsg); + } + algo = 'sha1'; + break; + case 'DSA': + type = 'ssh-dss'; + privPEM = makePEM('DSA PRIVATE', privBlob); + try { + reader = new Ber.Reader(privBlob); + reader.readSequence(); + reader.readInt(); // skip version + const p = reader.readString(Ber.Integer, true); + if (p === null) + return new Error(errMsg); + const q = reader.readString(Ber.Integer, true); + if (q === null) + return new Error(errMsg); + const g = reader.readString(Ber.Integer, true); + if (g === null) + return new Error(errMsg); + const y = reader.readString(Ber.Integer, true); + if (y === null) + return new Error(errMsg); + pubPEM = genOpenSSLDSAPub(p, q, g, y); + pubSSH = genOpenSSHDSAPub(p, q, g, y); + } catch { + return new Error(errMsg); + } + algo = 'sha1'; + break; + case 'EC': + let ecSSLName; + let ecPriv; + let ecOID; + try { + reader = new Ber.Reader(privBlob); + reader.readSequence(); + reader.readInt(); // skip version + ecPriv = reader.readString(Ber.OctetString, true); + reader.readByte(); // Skip "complex" context type byte + const offset = reader.readLength(); // Skip context length + if (offset !== null) { + reader._offset = offset; + ecOID = reader.readOID(); + if (ecOID === null) + return new Error(errMsg); + switch (ecOID) { + case '1.2.840.10045.3.1.7': + // prime256v1/secp256r1 + ecSSLName = 'prime256v1'; + type = 'ecdsa-sha2-nistp256'; + algo = 'sha256'; + break; + case '1.3.132.0.34': + // secp384r1 + ecSSLName = 'secp384r1'; + type = 'ecdsa-sha2-nistp384'; + algo = 'sha384'; + break; + case '1.3.132.0.35': + // secp521r1 + ecSSLName = 'secp521r1'; + type = 'ecdsa-sha2-nistp521'; + algo = 'sha512'; + break; + default: + return new Error(`Unsupported private key EC OID: ${ecOID}`); + } + } else { + return new Error(errMsg); + } + } catch { + return new Error(errMsg); + } + privPEM = makePEM('EC PRIVATE', privBlob); + const pubBlob = genOpenSSLECDSAPubFromPriv(ecSSLName, ecPriv); + pubPEM = genOpenSSLECDSAPub(ecOID, pubBlob); + pubSSH = genOpenSSHECDSAPub(ecOID, pubBlob); + break; + } + + return new OpenSSH_Old_Private(type, '', privPEM, pubPEM, pubSSH, algo, + decrypted); + }; +} + + +function PPK_Private(type, comment, privPEM, pubPEM, pubSSH, algo, decrypted) { + this.type = type; + this.comment = comment; + this[SYM_PRIV_PEM] = privPEM; + this[SYM_PUB_PEM] = pubPEM; + this[SYM_PUB_SSH] = pubSSH; + this[SYM_HASH_ALGO] = algo; + this[SYM_DECRYPTED] = decrypted; +} +PPK_Private.prototype = BaseKey; +{ + const EMPTY_PASSPHRASE = Buffer.alloc(0); + const PPK_IV = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + const PPK_PP1 = Buffer.from([0, 0, 0, 0]); + const PPK_PP2 = Buffer.from([0, 0, 0, 1]); + const regexp = /^PuTTY-User-Key-File-2: (ssh-(?:rsa|dss))\r?\nEncryption: (aes256-cbc|none)\r?\nComment: ([^\r\n]*)\r?\nPublic-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-MAC: ([^\r\n]+)/; + PPK_Private.parse = (str, passphrase) => { + const m = regexp.exec(str); + if (m === null) + return null; + // m[1] = key type + // m[2] = encryption type + // m[3] = comment + // m[4] = base64-encoded public key data: + // for "ssh-rsa": + // string "ssh-rsa" + // mpint e (public exponent) + // mpint n (modulus) + // for "ssh-dss": + // string "ssh-dss" + // mpint p (modulus) + // mpint q (prime) + // mpint g (base number) + // mpint y (public key parameter: g^x mod p) + // m[5] = base64-encoded private key data: + // for "ssh-rsa": + // mpint d (private exponent) + // mpint p (prime 1) + // mpint q (prime 2) + // mpint iqmp ([inverse of q] mod p) + // for "ssh-dss": + // mpint x (private key parameter) + // m[6] = SHA1 HMAC over: + // string name of algorithm ("ssh-dss", "ssh-rsa") + // string encryption type + // string comment + // string public key data + // string private-plaintext (including the final padding) + const cipherName = m[2]; + const encrypted = (cipherName !== 'none'); + if (encrypted && !passphrase) { + return new Error( + 'Encrypted PPK private key detected, but no passphrase given' + ); + } + + let privBlob = Buffer.from(m[5], 'base64'); + + if (encrypted) { + const encInfo = CIPHER_INFO[cipherName]; + let cipherKey = combineBuffers( + createHash('sha1').update(PPK_PP1).update(passphrase).digest(), + createHash('sha1').update(PPK_PP2).update(passphrase).digest() + ); + if (cipherKey.length > encInfo.keyLen) + cipherKey = bufferSlice(cipherKey, 0, encInfo.keyLen); + try { + const decipher = createDecipheriv(encInfo.sslName, + cipherKey, + PPK_IV); + decipher.setAutoPadding(false); + privBlob = combineBuffers(decipher.update(privBlob), + decipher.final()); + } catch (ex) { + return ex; + } + } + + const type = m[1]; + const comment = m[3]; + const pubBlob = Buffer.from(m[4], 'base64'); + + const mac = m[6]; + const typeLen = type.length; + const cipherNameLen = cipherName.length; + const commentLen = Buffer.byteLength(comment); + const pubLen = pubBlob.length; + const privLen = privBlob.length; + const macData = Buffer.allocUnsafe(4 + typeLen + + 4 + cipherNameLen + + 4 + commentLen + + 4 + pubLen + + 4 + privLen); + let p = 0; + + writeUInt32BE(macData, typeLen, p); + macData.utf8Write(type, p += 4, typeLen); + writeUInt32BE(macData, cipherNameLen, p += typeLen); + macData.utf8Write(cipherName, p += 4, cipherNameLen); + writeUInt32BE(macData, commentLen, p += cipherNameLen); + macData.utf8Write(comment, p += 4, commentLen); + writeUInt32BE(macData, pubLen, p += commentLen); + macData.set(pubBlob, p += 4); + writeUInt32BE(macData, privLen, p += pubLen); + macData.set(privBlob, p + 4); + + if (!passphrase) + passphrase = EMPTY_PASSPHRASE; + + const calcMAC = createHmac( + 'sha1', + createHash('sha1') + .update('putty-private-key-file-mac-key') + .update(passphrase) + .digest() + ).update(macData).digest('hex'); + + if (calcMAC !== mac) { + if (encrypted) { + return new Error( + 'PPK private key integrity check failed -- bad passphrase?' + ); + } + return new Error('PPK private key integrity check failed'); + } + + let pubPEM; + let pubSSH; + let privPEM; + pubBlob._pos = 0; + skipFields(pubBlob, 1); // skip (duplicate) key type + switch (type) { + case 'ssh-rsa': { + const e = readString(pubBlob, pubBlob._pos); + if (e === undefined) + return new Error('Malformed PPK public key'); + const n = readString(pubBlob, pubBlob._pos); + if (n === undefined) + return new Error('Malformed PPK public key'); + const d = readString(privBlob, 0); + if (d === undefined) + return new Error('Malformed PPK private key'); + const p = readString(privBlob, privBlob._pos); + if (p === undefined) + return new Error('Malformed PPK private key'); + const q = readString(privBlob, privBlob._pos); + if (q === undefined) + return new Error('Malformed PPK private key'); + const iqmp = readString(privBlob, privBlob._pos); + if (iqmp === undefined) + return new Error('Malformed PPK private key'); + pubPEM = genOpenSSLRSAPub(n, e); + pubSSH = genOpenSSHRSAPub(n, e); + privPEM = genOpenSSLRSAPriv(n, e, d, iqmp, p, q); + break; + } + case 'ssh-dss': { + const p = readString(pubBlob, pubBlob._pos); + if (p === undefined) + return new Error('Malformed PPK public key'); + const q = readString(pubBlob, pubBlob._pos); + if (q === undefined) + return new Error('Malformed PPK public key'); + const g = readString(pubBlob, pubBlob._pos); + if (g === undefined) + return new Error('Malformed PPK public key'); + const y = readString(pubBlob, pubBlob._pos); + if (y === undefined) + return new Error('Malformed PPK public key'); + const x = readString(privBlob, 0); + if (x === undefined) + return new Error('Malformed PPK private key'); + + pubPEM = genOpenSSLDSAPub(p, q, g, y); + pubSSH = genOpenSSHDSAPub(p, q, g, y); + privPEM = genOpenSSLDSAPriv(p, q, g, y, x); + break; + } + } + + return new PPK_Private(type, comment, privPEM, pubPEM, pubSSH, 'sha1', + encrypted); + }; +} + + +function OpenSSH_Public(type, comment, pubPEM, pubSSH, algo) { + this.type = type; + this.comment = comment; + this[SYM_PRIV_PEM] = null; + this[SYM_PUB_PEM] = pubPEM; + this[SYM_PUB_SSH] = pubSSH; + this[SYM_HASH_ALGO] = algo; + this[SYM_DECRYPTED] = false; +} +OpenSSH_Public.prototype = BaseKey; +{ + let regexp; + if (eddsaSupported) + regexp = /^(((?:ssh-(?:rsa|dss|ed25519))|ecdsa-sha2-nistp(?:256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z/+=]+)(?:$|\s+([\S].*)?)$/; + else + regexp = /^(((?:ssh-(?:rsa|dss))|ecdsa-sha2-nistp(?:256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z/+=]+)(?:$|\s+([\S].*)?)$/; + OpenSSH_Public.parse = (str) => { + const m = regexp.exec(str); + if (m === null) + return null; + // m[1] = full type + // m[2] = base type + // m[3] = base64-encoded public key + // m[4] = comment + + const fullType = m[1]; + const baseType = m[2]; + const data = Buffer.from(m[3], 'base64'); + const comment = (m[4] || ''); + + const type = readString(data, data._pos, true); + if (type === undefined || type.indexOf(baseType) !== 0) + return new Error('Malformed OpenSSH public key'); + + return parseDER(data, baseType, comment, fullType); + }; +} + + +function RFC4716_Public(type, comment, pubPEM, pubSSH, algo) { + this.type = type; + this.comment = comment; + this[SYM_PRIV_PEM] = null; + this[SYM_PUB_PEM] = pubPEM; + this[SYM_PUB_SSH] = pubSSH; + this[SYM_HASH_ALGO] = algo; + this[SYM_DECRYPTED] = false; +} +RFC4716_Public.prototype = BaseKey; +{ + const regexp = /^---- BEGIN SSH2 PUBLIC KEY ----(?:\r\n|\n)((?:(?:[\x21-\x7E]+?):(?:(?:.*?\\\r?\n)*.*)(?:\r\n|\n))*)((?:[A-Z0-9a-z/+=]+(?:\r\n|\n))+)---- END SSH2 PUBLIC KEY ----$/; + const RE_HEADER = /^([\x21-\x7E]+?):((?:.*?\\\r?\n)*.*)$/gm; + const RE_HEADER_ENDS = /\\\r?\n/g; + RFC4716_Public.parse = (str) => { + let m = regexp.exec(str); + if (m === null) + return null; + // m[1] = header(s) + // m[2] = base64-encoded public key + + const headers = m[1]; + const data = Buffer.from(m[2], 'base64'); + let comment = ''; + + if (headers !== undefined) { + while (m = RE_HEADER.exec(headers)) { + if (m[1].toLowerCase() === 'comment') { + comment = m[2].replace(RE_HEADER_ENDS, '').trimStart(); + if (comment.length > 1 + && comment.charCodeAt(0) === 34/* '"' */ + && comment.charCodeAt(comment.length - 1) === 34/* '"' */) { + comment = comment.slice(1, -1); + } + } + } + } + + const type = readString(data, 0, true); + if (type === undefined) + return new Error('Malformed RFC4716 public key'); + + let pubPEM = null; + let pubSSH = null; + switch (type) { + case 'ssh-rsa': { + const e = readString(data, data._pos); + if (e === undefined) + return new Error('Malformed RFC4716 public key'); + const n = readString(data, data._pos); + if (n === undefined) + return new Error('Malformed RFC4716 public key'); + pubPEM = genOpenSSLRSAPub(n, e); + pubSSH = genOpenSSHRSAPub(n, e); + break; + } + case 'ssh-dss': { + const p = readString(data, data._pos); + if (p === undefined) + return new Error('Malformed RFC4716 public key'); + const q = readString(data, data._pos); + if (q === undefined) + return new Error('Malformed RFC4716 public key'); + const g = readString(data, data._pos); + if (g === undefined) + return new Error('Malformed RFC4716 public key'); + const y = readString(data, data._pos); + if (y === undefined) + return new Error('Malformed RFC4716 public key'); + pubPEM = genOpenSSLDSAPub(p, q, g, y); + pubSSH = genOpenSSHDSAPub(p, q, g, y); + break; + } + default: + return new Error('Malformed RFC4716 public key'); + } + + return new RFC4716_Public(type, comment, pubPEM, pubSSH, 'sha1'); + }; +} + + +function parseDER(data, baseType, comment, fullType) { + let algo; + let oid; + let pubPEM = null; + let pubSSH = null; + switch (baseType) { + case 'ssh-rsa': { + const e = readString(data, data._pos || 0); + if (e === undefined) + return new Error('Malformed OpenSSH public key'); + const n = readString(data, data._pos); + if (n === undefined) + return new Error('Malformed OpenSSH public key'); + pubPEM = genOpenSSLRSAPub(n, e); + pubSSH = genOpenSSHRSAPub(n, e); + algo = 'sha1'; + break; + } + case 'ssh-dss': { + const p = readString(data, data._pos || 0); + if (p === undefined) + return new Error('Malformed OpenSSH public key'); + const q = readString(data, data._pos); + if (q === undefined) + return new Error('Malformed OpenSSH public key'); + const g = readString(data, data._pos); + if (g === undefined) + return new Error('Malformed OpenSSH public key'); + const y = readString(data, data._pos); + if (y === undefined) + return new Error('Malformed OpenSSH public key'); + pubPEM = genOpenSSLDSAPub(p, q, g, y); + pubSSH = genOpenSSHDSAPub(p, q, g, y); + algo = 'sha1'; + break; + } + case 'ssh-ed25519': { + const edpub = readString(data, data._pos || 0); + if (edpub === undefined || edpub.length !== 32) + return new Error('Malformed OpenSSH public key'); + pubPEM = genOpenSSLEdPub(edpub); + pubSSH = genOpenSSHEdPub(edpub); + algo = null; + break; + } + case 'ecdsa-sha2-nistp256': + algo = 'sha256'; + oid = '1.2.840.10045.3.1.7'; + // FALLTHROUGH + case 'ecdsa-sha2-nistp384': + if (algo === undefined) { + algo = 'sha384'; + oid = '1.3.132.0.34'; + } + // FALLTHROUGH + case 'ecdsa-sha2-nistp521': { + if (algo === undefined) { + algo = 'sha512'; + oid = '1.3.132.0.35'; + } + // TODO: validate curve name against type + if (!skipFields(data, 1)) // Skip curve name + return new Error('Malformed OpenSSH public key'); + const ecpub = readString(data, data._pos || 0); + if (ecpub === undefined) + return new Error('Malformed OpenSSH public key'); + pubPEM = genOpenSSLECDSAPub(oid, ecpub); + pubSSH = genOpenSSHECDSAPub(oid, ecpub); + break; + } + default: + return new Error(`Unsupported OpenSSH public key type: ${baseType}`); + } + + return new OpenSSH_Public(fullType, comment, pubPEM, pubSSH, algo); +} + + +module.exports = { + parseDERKey: (data, type) => parseDER(data, type, '', type), + parseKey: (data, passphrase) => { + if (Buffer.isBuffer(data)) + data = data.utf8Slice(0, data.length).trim(); + else if (typeof data !== 'string') + return new Error('Key data must be a Buffer or string'); + else + data = data.trim(); + + // eslint-disable-next-line eqeqeq + if (passphrase != undefined) { + if (typeof passphrase === 'string') + passphrase = Buffer.from(passphrase); + else if (!Buffer.isBuffer(passphrase)) + return new Error('Passphrase must be a string or Buffer when supplied'); + } + + let ret; + + // Private keys + if ((ret = OpenSSH_Private.parse(data, passphrase)) !== null) + return ret; + if ((ret = OpenSSH_Old_Private.parse(data, passphrase)) !== null) + return ret; + if ((ret = PPK_Private.parse(data, passphrase)) !== null) + return ret; + + // Public keys + if ((ret = OpenSSH_Public.parse(data)) !== null) + return ret; + if ((ret = RFC4716_Public.parse(data)) !== null) + return ret; + + return new Error('Unsupported key format'); + } +}; diff --git a/lib/protocol/node-fs-compat.js b/lib/protocol/node-fs-compat.js new file mode 100644 index 00000000..80ed71fc --- /dev/null +++ b/lib/protocol/node-fs-compat.js @@ -0,0 +1,115 @@ +'use strict'; + +const assert = require('assert'); +const { inspect } = require('util'); + +// Only use this for integers! Decimal numbers do not work with this function. +function addNumericalSeparator(val) { + let res = ''; + let i = val.length; + const start = val[0] === '-' ? 1 : 0; + for (; i >= start + 4; i -= 3) + res = `_${val.slice(i - 3, i)}${res}`; + return `${val.slice(0, i)}${res}`; +} + +function oneOf(expected, thing) { + assert(typeof thing === 'string', '`thing` has to be of type string'); + if (Array.isArray(expected)) { + const len = expected.length; + assert(len > 0, 'At least one expected value needs to be specified'); + expected = expected.map((i) => String(i)); + if (len > 2) { + return `one of ${thing} ${expected.slice(0, len - 1).join(', ')}, or ` + + expected[len - 1]; + } else if (len === 2) { + return `one of ${thing} ${expected[0]} or ${expected[1]}`; + } + return `of ${thing} ${expected[0]}`; + } + return `of ${thing} ${String(expected)}`; +} + + +exports.ERR_INTERNAL_ASSERTION = class ERR_INTERNAL_ASSERTION extends Error { + constructor(message) { + super(); + Error.captureStackTrace(this, ERR_INTERNAL_ASSERTION); + + const suffix = 'This is caused by either a bug in ssh2 ' + + 'or incorrect usage of ssh2 internals.\n' + + 'Please open an issue with this stack trace at ' + + 'https://github.com/mscdex/ssh2/issues\n'; + + this.message = (message === undefined ? suffix : `${message}\n${suffix}`); + } +}; + +const MAX_32BIT_INT = 2 ** 32; +const MAX_32BIT_BIGINT = (() => { + try { + return new Function('return 2n ** 32n')(); + } catch {} +})(); +exports.ERR_OUT_OF_RANGE = class ERR_OUT_OF_RANGE extends RangeError { + constructor(str, range, input, replaceDefaultBoolean) { + super(); + Error.captureStackTrace(this, ERR_OUT_OF_RANGE); + + assert(range, 'Missing "range" argument'); + let msg = (replaceDefaultBoolean + ? str + : `The value of "${str}" is out of range.`); + let received; + if (Number.isInteger(input) && Math.abs(input) > MAX_32BIT_INT) { + received = addNumericalSeparator(String(input)); + } else if (typeof input === 'bigint') { + received = String(input); + if (input > MAX_32BIT_BIGINT || input < -MAX_32BIT_BIGINT) + received = addNumericalSeparator(received); + received += 'n'; + } else { + received = inspect(input); + } + msg += ` It must be ${range}. Received ${received}`; + + this.message = msg; + } +}; + +class ERR_INVALID_ARG_TYPE extends TypeError { + constructor(name, expected, actual) { + super(); + Error.captureStackTrace(this, ERR_INVALID_ARG_TYPE); + + assert(typeof name === 'string', `'name' must be a string`); + + // determiner: 'must be' or 'must not be' + let determiner; + if (typeof expected === 'string' && expected.startsWith('not ')) { + determiner = 'must not be'; + expected = expected.replace(/^not /, ''); + } else { + determiner = 'must be'; + } + + let msg; + if (name.endsWith(' argument')) { + // For cases like 'first argument' + msg = `The ${name} ${determiner} ${oneOf(expected, 'type')}`; + } else { + const type = (name.includes('.') ? 'property' : 'argument'); + msg = `The "${name}" ${type} ${determiner} ${oneOf(expected, 'type')}`; + } + + msg += `. Received type ${typeof actual}`; + + this.message = msg; + } +} +exports.ERR_INVALID_ARG_TYPE = ERR_INVALID_ARG_TYPE; + +exports.validateNumber = function validateNumber(value, name) { + if (typeof value !== 'number') + throw new ERR_INVALID_ARG_TYPE(name, 'number', value); +}; diff --git a/lib/protocol/utils.js b/lib/protocol/utils.js new file mode 100644 index 00000000..13d002c4 --- /dev/null +++ b/lib/protocol/utils.js @@ -0,0 +1,376 @@ +'use strict'; + +const { timingSafeEqual: timingSafeEqual_ } = require('crypto'); + +const Ber = require('asn1').Ber; + +let DISCONNECT_REASON; + +const FastBuffer = Buffer[Symbol.species]; +const TypedArrayFill = Object.getPrototypeOf(Uint8Array.prototype).fill; + +const EMPTY_BUFFER = Buffer.alloc(0); + +function readUInt32BE(buf, offset) { + return (buf[offset++] * 16777216) + + (buf[offset++] * 65536) + + (buf[offset++] * 256) + + buf[offset]; +} + +function bufferCopy(src, dest, srcStart, srcEnd, destStart) { + if (!destStart) + destStart = 0; + if (srcEnd > src.length) + srcEnd = src.length; + let nb = srcEnd - srcStart; + const destLeft = (dest.length - destStart); + if (nb > destLeft) + nb = destLeft; + dest.set(new Uint8Array(src.buffer, src.byteOffset + srcStart, nb), + destStart); + return nb; +} + +function bufferSlice(buf, start, end) { + if (end === undefined) + end = buf.length; + return new FastBuffer(buf.buffer, buf.byteOffset + start, end - start); +} + +function makeBufferParser() { + let pos = 0; + let buffer; + + const self = { + init: (buf, start) => { + buffer = buf; + pos = (typeof start === 'number' ? start : 0); + }, + pos: () => pos, + length: () => (buffer ? buffer.length : 0), + avail: () => (buffer && pos < buffer.length ? buffer.length - pos : 0), + clear: () => { + buffer = undefined; + }, + readUInt32BE: () => { + if (!buffer || pos + 3 >= buffer.length) + return; + return (buffer[pos++] * 16777216) + + (buffer[pos++] * 65536) + + (buffer[pos++] * 256) + + buffer[pos++]; + }, + readUInt64BE: (behavior) => { + if (!buffer || pos + 7 >= buffer.length) + return; + switch (behavior) { + case 'always': + return BigInt(`0x${buffer.hexSlice(pos, pos += 8)}`); + case 'maybe': + if (buffer[pos] > 0x1F) + return BigInt(`0x${buffer.hexSlice(pos, pos += 8)}`); + // FALLTHROUGH + default: + return (buffer[pos++] * 72057594037927940) + + (buffer[pos++] * 281474976710656) + + (buffer[pos++] * 1099511627776) + + (buffer[pos++] * 4294967296) + + (buffer[pos++] * 16777216) + + (buffer[pos++] * 65536) + + (buffer[pos++] * 256) + + buffer[pos++]; + } + }, + skip: (n) => { + if (buffer && n > 0) + pos += n; + }, + skipString: () => { + const len = self.readUInt32BE(); + if (len === undefined) + return; + pos += len; + return (pos <= buffer.length ? len : undefined); + }, + readByte: () => { + if (buffer && pos < buffer.length) + return buffer[pos++]; + }, + readBool: () => { + if (buffer && pos < buffer.length) + return !!buffer[pos++]; + }, + readList: () => { + const list = self.readString(true); + if (list === undefined) + return; + return (list ? list.split(',') : []); + }, + readString: (dest, maxLen) => { + if (typeof dest === 'number') { + maxLen = dest; + dest = undefined; + } + + const len = self.readUInt32BE(); + if (len === undefined) + return; + + if ((buffer.length - pos) < len + || (typeof maxLen === 'number' && len > maxLen)) { + return; + } + + if (dest) { + if (Buffer.isBuffer(dest)) + return bufferCopy(buffer, dest, pos, pos += len); + return buffer.utf8Slice(pos, pos += len); + } + return bufferSlice(buffer, pos, pos += len); + }, + readRaw: (len) => { + if (!buffer) + return; + if (typeof len !== 'number') + return bufferSlice(buffer, pos, pos += (buffer.length - pos)); + if ((buffer.length - pos) >= len) + return bufferSlice(buffer, pos, pos += len); + }, + }; + + return self; +} + +function makeError(msg, level, fatal) { + const err = new Error(msg); + if (typeof level === 'boolean') { + fatal = level; + err.level = 'protocol'; + } else { + err.level = level || 'protocol'; + } + err.fatal = !!fatal; + return err; +} + +function writeUInt32BE(buf, value, offset) { + buf[offset++] = (value >>> 24); + buf[offset++] = (value >>> 16); + buf[offset++] = (value >>> 8); + buf[offset++] = value; + return offset; +} + +const utilBufferParser = makeBufferParser(); + +module.exports = { + bufferCopy, + bufferSlice, + FastBuffer, + bufferFill: (buf, value, start, end) => { + return TypedArrayFill.call(buf, value, start, end); + }, + makeError, + doFatalError: (protocol, msg, level, reason) => { + let err; + if (DISCONNECT_REASON === undefined) + ({ DISCONNECT_REASON } = require('./utils.js')); + if (msg instanceof Error) { + // doFatalError(protocol, err[, reason]) + err = msg; + if (typeof level !== 'number') + reason = DISCONNECT_REASON.PROTOCOL_ERROR; + else + reason = level; + } else { + // doFatalError(protocol, msg[, level[, reason]]) + err = makeError(msg, level, true); + } + if (typeof reason !== 'number') + reason = DISCONNECT_REASON.PROTOCOL_ERROR; + protocol.disconnect(reason); + protocol._destruct(); + protocol._onError(err); + return Infinity; + }, + getBinaryList: (list) => { + if (Buffer.isBuffer(list)) + return list; + if (typeof list === 'string') + return (list.length === 0 ? EMPTY_BUFFER : Buffer.from(list)); + if (Array.isArray(list)) + return (list.length === 0 ? EMPTY_BUFFER : Buffer.from(list.join(','))); + throw new Error(`Invalid list type: ${typeof list}`); + }, + timingSafeEquals: (a, b) => { + if (a.length !== b.length) { + timingSafeEqual_(a, a); + return false; + } + return timingSafeEqual_(a, b); + }, + readUInt32BE, + writeUInt32BE, + writeUInt32LE: (buf, value, offset) => { + buf[offset++] = value; + buf[offset++] = (value >>> 8); + buf[offset++] = (value >>> 16); + buf[offset++] = (value >>> 24); + return offset; + }, + makeBufferParser, + bufferParser: makeBufferParser(), + readString: (buffer, start, dest, maxLen) => { + if (typeof dest === 'number') { + maxLen = dest; + dest = undefined; + } + + if (start === undefined) + start = 0; + + const left = (buffer.length - start); + if (start < 0 || start >= buffer.length || left < 4) + return; + + const len = readUInt32BE(buffer, start); + if (left < (4 + len) || (typeof maxLen === 'number' && len > maxLen)) + return; + + start += 4; + const end = start + len; + buffer._pos = end; + + if (dest) { + if (Buffer.isBuffer(dest)) + return bufferCopy(buffer, dest, start, end); + return buffer.utf8Slice(start, end); + } + return bufferSlice(buffer, start, end); + }, + sigSSHToASN1: (sig, type) => { + switch (type) { + case 'ssh-dss': { + if (sig.length > 40) + return sig; + // Change bare signature r and s values to ASN.1 BER values for OpenSSL + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + let r = sig.slice(0, 20); + let s = sig.slice(20); + if (r[0] & 0x80) { + const rNew = Buffer.allocUnsafe(21); + rNew[0] = 0x00; + r.copy(rNew, 1); + r = rNew; + } else if (r[0] === 0x00 && !(r[1] & 0x80)) { + r = r.slice(1); + } + if (s[0] & 0x80) { + const sNew = Buffer.allocUnsafe(21); + sNew[0] = 0x00; + s.copy(sNew, 1); + s = sNew; + } else if (s[0] === 0x00 && !(s[1] & 0x80)) { + s = s.slice(1); + } + asnWriter.writeBuffer(r, Ber.Integer); + asnWriter.writeBuffer(s, Ber.Integer); + asnWriter.endSequence(); + return asnWriter.buffer; + } + case 'ecdsa-sha2-nistp256': + case 'ecdsa-sha2-nistp384': + case 'ecdsa-sha2-nistp521': { + utilBufferParser.init(sig, 0); + const r = utilBufferParser.readString(); + const s = utilBufferParser.readString(); + utilBufferParser.clear(); + if (r === undefined || s === undefined) + return; + + const asnWriter = new Ber.Writer(); + asnWriter.startSequence(); + asnWriter.writeBuffer(r, Ber.Integer); + asnWriter.writeBuffer(s, Ber.Integer); + asnWriter.endSequence(); + return asnWriter.buffer; + } + default: + return sig; + } + }, + convertSignature: (signature, keyType) => { + switch (keyType) { + case 'ssh-dss': { + if (signature.length <= 40) + return signature; + // This is a quick and dirty way to get from BER encoded r and s that + // OpenSSL gives us, to just the bare values back to back (40 bytes + // total) like OpenSSH (and possibly others) are expecting + const asnReader = new Ber.Reader(signature); + asnReader.readSequence(); + let r = asnReader.readString(Ber.Integer, true); + let s = asnReader.readString(Ber.Integer, true); + let rOffset = 0; + let sOffset = 0; + if (r.length < 20) { + const rNew = Buffer.allocUnsafe(20); + rNew.set(r, 1); + r = rNew; + r[0] = 0; + } + if (s.length < 20) { + const sNew = Buffer.allocUnsafe(20); + sNew.set(s, 1); + s = sNew; + s[0] = 0; + } + if (r.length > 20 && r[0] === 0) + rOffset = 1; + if (s.length > 20 && s[0] === 0) + sOffset = 1; + const newSig = + Buffer.allocUnsafe((r.length - rOffset) + (s.length - sOffset)); + bufferCopy(r, newSig, rOffset, r.length, 0); + bufferCopy(s, newSig, sOffset, s.length, r.length - rOffset); + return newSig; + } + case 'ecdsa-sha2-nistp256': + case 'ecdsa-sha2-nistp384': + case 'ecdsa-sha2-nistp521': { + if (signature[0] === 0) + return signature; + // Convert SSH signature parameters to ASN.1 BER values for OpenSSL + const asnReader = new Ber.Reader(signature); + asnReader.readSequence(); + const r = asnReader.readString(Ber.Integer, true); + const s = asnReader.readString(Ber.Integer, true); + if (r === null || s === null) + return; + const newSig = Buffer.allocUnsafe(4 + r.length + 4 + s.length); + writeUInt32BE(newSig, r.length, 0); + newSig.set(r, 4); + writeUInt32BE(newSig, s.length, 4 + r.length); + newSig.set(s, 4 + 4 + r.length); + return newSig; + } + } + + return signature; + }, + sendPacket: (proto, packet, bypass) => { + if (!bypass && proto._kexinit !== undefined) { + // We're currently in the middle of a handshake + + if (proto._queue === undefined) + proto._queue = []; + proto._queue.push(packet); + proto._debug && proto._debug('Outbound: ... packet queued'); + return false; + } + proto._cipher.encrypt(packet); + return true; + }, +}; diff --git a/lib/protocol/zlib.js b/lib/protocol/zlib.js new file mode 100644 index 00000000..f68319a1 --- /dev/null +++ b/lib/protocol/zlib.js @@ -0,0 +1,255 @@ +'use strict'; + +const { kMaxLength } = require('buffer'); +const { + createInflate, + constants: { + DEFLATE, + INFLATE, + Z_DEFAULT_CHUNK, + Z_DEFAULT_COMPRESSION, + Z_DEFAULT_MEMLEVEL, + Z_DEFAULT_STRATEGY, + Z_DEFAULT_WINDOWBITS, + Z_PARTIAL_FLUSH, + } +} = require('zlib'); +const ZlibHandle = createInflate()._handle.constructor; + +function processCallback() { + throw new Error('Should not get here'); +} + +function zlibOnError(message, errno, code) { + const self = this._owner; + // There is no way to cleanly recover. + // Continuing only obscures problems. + + const error = new Error(message); + error.errno = errno; + error.code = code; + self._err = error; +} + +function _close(engine) { + // Caller may invoke .close after a zlib error (which will null _handle). + if (!engine._handle) + return; + + engine._handle.close(); + engine._handle = null; +} + +class Zlib { + constructor(mode) { + const windowBits = Z_DEFAULT_WINDOWBITS; + const level = Z_DEFAULT_COMPRESSION; + const memLevel = Z_DEFAULT_MEMLEVEL; + const strategy = Z_DEFAULT_STRATEGY; + const dictionary = undefined; + + this._err = undefined; + this._writeState = new Uint32Array(2); + this._chunkSize = Z_DEFAULT_CHUNK; + this._maxOutputLength = kMaxLength; + this._outBuffer = Buffer.allocUnsafe(this._chunkSize); + this._outOffset = 0; + + this._handle = new ZlibHandle(mode); + this._handle._owner = this; + this._handle.onerror = zlibOnError; + this._handle.init(windowBits, + level, + memLevel, + strategy, + this._writeState, + processCallback, + dictionary); + } + + writeSync(chunk, retChunks) { + const handle = this._handle; + if (!handle) + throw new Error('Invalid Zlib instance'); + + let availInBefore = chunk.length; + let availOutBefore = this._chunkSize - this._outOffset; + let inOff = 0; + let availOutAfter; + let availInAfter; + + let buffers; + let nread = 0; + const state = this._writeState; + let buffer = this._outBuffer; + let offset = this._outOffset; + const chunkSize = this._chunkSize; + + while (true) { + handle.writeSync(Z_PARTIAL_FLUSH, + chunk, // in + inOff, // in_off + availInBefore, // in_len + buffer, // out + offset, // out_off + availOutBefore); // out_len + if (this._err) + throw this._err; + + availOutAfter = state[0]; + availInAfter = state[1]; + + const inDelta = availInBefore - availInAfter; + const have = availOutBefore - availOutAfter; + + if (have > 0) { + const out = (offset === 0 && have === buffer.length + ? buffer + : buffer.slice(offset, offset + have)); + offset += have; + if (!buffers) + buffers = out; + else if (buffers.push === undefined) + buffers = [buffers, out]; + else + buffers.push(out); + nread += out.byteLength; + + if (nread > this._maxOutputLength) { + _close(this); + throw new Error( + `Output length exceeded maximum of ${this._maxOutputLength}` + ); + } + } else if (have !== 0) { + throw new Error('have should not go down'); + } + + // Exhausted the output buffer, or used all the input create a new one. + if (availOutAfter === 0 || offset >= chunkSize) { + availOutBefore = chunkSize; + offset = 0; + buffer = Buffer.allocUnsafe(chunkSize); + } + + if (availOutAfter === 0) { + // Not actually done. Need to reprocess. + // Also, update the availInBefore to the availInAfter value, + // so that if we have to hit it a third (fourth, etc.) time, + // it'll have the correct byte counts. + inOff += inDelta; + availInBefore = availInAfter; + } else { + break; + } + } + + this._outBuffer = buffer; + this._outOffset = offset; + + if (nread === 0) + buffers = Buffer.alloc(0); + + if (retChunks) { + buffers.totalLen = nread; + return buffers; + } + + if (buffers.push === undefined) + return buffers; + + const output = Buffer.allocUnsafe(nread); + for (let i = 0, p = 0; i < buffers.length; ++i) { + const buf = buffers[i]; + output.set(buf, p); + p += buf.length; + } + return output; + } +} + +class ZlibPacketWriter { + constructor(protocol) { + this.allocStart = 0; + this.allocStartKEX = 0; + this._protocol = protocol; + this._zlib = new Zlib(DEFLATE); + } + + cleanup() { + if (this._zlib) + _close(this._zlib); + } + + alloc(payloadSize, force) { + return Buffer.allocUnsafe(payloadSize); + } + + finalize(payload, force) { + if (this._protocol._kexinit === undefined || force) { + const output = this._zlib.writeSync(payload, true); + const packet = this._protocol._cipher.allocPacket(output.totalLen); + if (output.push === undefined) { + packet.set(output, 5); + } else { + for (let i = 0, p = 5; i < output.length; ++i) { + const chunk = output[i]; + packet.set(chunk, p); + p += chunk.length; + } + } + return packet; + } + return payload; + } +} + +class PacketWriter { + constructor(protocol) { + this.allocStart = 5; + this.allocStartKEX = 5; + this._protocol = protocol; + } + + cleanup() {} + + alloc(payloadSize, force) { + if (this._protocol._kexinit === undefined || force) + return this._protocol._cipher.allocPacket(payloadSize); + return Buffer.allocUnsafe(payloadSize); + } + + finalize(packet, force) { + return packet; + } +} + +class ZlibPacketReader { + constructor() { + this._zlib = new Zlib(INFLATE); + } + + cleanup() { + if (this._zlib) + _close(this._zlib); + } + + read(data) { + return this._zlib.writeSync(data, false); + } +} + +class PacketReader { + cleanup() {} + + read(data) { + return data; + } +} + +module.exports = { + PacketReader, + PacketWriter, + ZlibPacketReader, + ZlibPacketWriter, +}; diff --git a/lib/server.js b/lib/server.js index 424ad272..ddfec516 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,1160 +1,1363 @@ -var net = require('net'); -var EventEmitter = require('events').EventEmitter; -var listenerCount = EventEmitter.listenerCount; -var inherits = require('util').inherits; - -var ssh2_streams = require('ssh2-streams'); -var parseKey = ssh2_streams.utils.parseKey; -var SSH2Stream = ssh2_streams.SSH2Stream; -var SFTPStream = ssh2_streams.SFTPStream; -var consts = ssh2_streams.constants; -var DISCONNECT_REASON = consts.DISCONNECT_REASON; -var CHANNEL_OPEN_FAILURE = consts.CHANNEL_OPEN_FAILURE; -var ALGORITHMS = consts.ALGORITHMS; - -var Channel = require('./Channel'); -var KeepaliveManager = require('./keepalivemgr'); -var writeUInt32BE = require('./buffer-helpers').writeUInt32BE; - -var MAX_CHANNEL = Math.pow(2, 32) - 1; -var MAX_PENDING_AUTHS = 10; - -var kaMgr; - -function Server(cfg, listener) { - if (!(this instanceof Server)) - return new Server(cfg, listener); - - var hostKeys = { - 'ssh-rsa': null, - 'ssh-dss': null, - 'ssh-ed25519': null, - 'ecdsa-sha2-nistp256': null, - 'ecdsa-sha2-nistp384': null, - 'ecdsa-sha2-nistp521': null - }; +// TODO: +// * convert listenerCount() usage to emit() return value checking? +// * deal with outstanding channel callbacks for S->C channel requests, +// similar to how Client already does +// * emit error when connection severed early (e.g. before handshake) +// * add '.connected' or similar property to connection objects to allow +// immediate connection status checking +'use strict'; + +const { Server: netServer } = require('net'); +const EventEmitter = require('events'); +const { listenerCount } = EventEmitter; + +const { + CHANNEL_OPEN_FAILURE, + DEFAULT_CIPHER, + DEFAULT_COMPRESSION, + DEFAULT_KEX, + DEFAULT_MAC, + DEFAULT_SERVER_HOST_KEY, + DISCONNECT_REASON, + DISCONNECT_REASON_BY_VALUE, + SUPPORTED_CIPHER, + SUPPORTED_COMPRESSION, + SUPPORTED_KEX, + SUPPORTED_MAC, + SUPPORTED_SERVER_HOST_KEY, +} = require('./protocol/constants.js'); +const { KexInit } = require('./protocol/kex.js'); +const { parseKey } = require('./protocol/keyParser.js'); +const Protocol = require('./protocol/Protocol.js'); +const { SFTP } = require('./protocol/SFTP.js'); +const { writeUInt32BE } = require('./protocol/utils.js'); + +const { + Channel, + MAX_WINDOW, + PACKET_SIZE, + windowAdjust, + WINDOW_THRESHOLD, +} = require('./Channel.js'); + +const { + ChannelManager, + generateAlgorithmList, + onChannelOpenFailure, + onCHANNEL_CLOSE, +} = require('./utils.js'); + +const MAX_PENDING_AUTHS = 10; + +class AuthContext extends EventEmitter { + constructor(protocol, username, service, method, cb) { + super(); + + this.username = this.user = username; + this.service = service; + this.method = method; + this._initialResponse = false; + this._finalResponse = false; + this._multistep = false; + this._cbfinal = (allowed, methodsLeft, isPartial) => { + if (!this._finalResponse) { + this._finalResponse = true; + cb(this, allowed, methodsLeft, isPartial); + } + }; + this._protocol = protocol; + } - var hostKeys_ = cfg.hostKeys; - if (!Array.isArray(hostKeys_)) - throw new Error('hostKeys must be an array'); - - var i; - for (i = 0; i < hostKeys_.length; ++i) { - var privateKey; - if (Buffer.isBuffer(hostKeys_[i]) || typeof hostKeys_[i] === 'string') - privateKey = parseKey(hostKeys_[i]); - else - privateKey = parseKey(hostKeys_[i].key, hostKeys_[i].passphrase); - if (privateKey instanceof Error) - throw new Error('Cannot parse privateKey: ' + privateKey.message); - if (Array.isArray(privateKey)) - privateKey = privateKey[0]; // OpenSSH's newer format only stores 1 key for now - if (privateKey.getPrivatePEM() === null) - throw new Error('privateKey value contains an invalid private key'); - if (hostKeys[privateKey.type]) - continue; - hostKeys[privateKey.type] = privateKey; + accept() { + this._cleanup && this._cleanup(); + this._initialResponse = true; + this._cbfinal(true); } + reject(methodsLeft, isPartial) { + this._cleanup && this._cleanup(); + this._initialResponse = true; + this._cbfinal(false, methodsLeft, isPartial); + } +} - var algorithms = { - kex: undefined, - kexBuf: undefined, - cipher: undefined, - cipherBuf: undefined, - serverHostKey: undefined, - serverHostKeyBuf: undefined, - hmac: undefined, - hmacBuf: undefined, - compress: undefined, - compressBuf: undefined - }; - if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) { - var algosSupported; - var algoList; - - algoList = cfg.algorithms.kex; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_KEX; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) - throw new Error('Unsupported key exchange algorithm: ' + algoList[i]); - } - algorithms.kex = algoList; - } - algoList = cfg.algorithms.cipher; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_CIPHER; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) - throw new Error('Unsupported cipher algorithm: ' + algoList[i]); - } - algorithms.cipher = algoList; - } +const RE_KBINT_SUBMETHODS = /[ \t\r\n]*,[ \t\r\n]*/g; +class KeyboardAuthContext extends AuthContext { + constructor(protocol, username, service, method, submethods, cb) { + super(protocol, username, service, method, cb); - algoList = cfg.algorithms.serverHostKey; - var copied = false; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_SERVER_HOST_KEY; - for (i = algoList.length - 1; i >= 0; --i) { - if (algosSupported.indexOf(algoList[i]) === -1) { - throw new Error('Unsupported server host key algorithm: ' - + algoList[i]); - } - if (!hostKeys[algoList[i]]) { - // Silently discard for now - if (!copied) { - algoList = algoList.slice(); - copied = true; - } - algoList.splice(i, 1); - } - } - if (algoList.length > 0) - algorithms.serverHostKey = algoList; - } + this._multistep = true; - algoList = cfg.algorithms.hmac; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_HMAC; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) - throw new Error('Unsupported HMAC algorithm: ' + algoList[i]); + this._cb = undefined; + this._onInfoResponse = (responses) => { + const callback = this._cb; + if (callback) { + this._cb = undefined; + callback(responses); } - algorithms.hmac = algoList; + }; + this.submethods = submethods.split(RE_KBINT_SUBMETHODS); + this.on('abort', () => { + this._cb && this._cb(new Error('Authentication request aborted')); + }); + } + + prompt(prompts, title, instructions, cb) { + if (!Array.isArray(prompts)) + prompts = [ prompts ]; + + if (typeof title === 'function') { + cb = title; + title = instructions = undefined; + } else if (typeof instructions === 'function') { + cb = instructions; + instructions = undefined; + } else if (typeof cb !== 'function') { + cb = undefined; } - algoList = cfg.algorithms.compress; - if (Array.isArray(algoList) && algoList.length > 0) { - algosSupported = ALGORITHMS.SUPPORTED_COMPRESS; - for (i = 0; i < algoList.length; ++i) { - if (algosSupported.indexOf(algoList[i]) === -1) - throw new Error('Unsupported compression algorithm: ' + algoList[i]); + for (let i = 0; i < prompts.length; ++i) { + if (typeof prompts[i] === 'string') { + prompts[i] = { + prompt: prompts[i], + echo: true + }; } - algorithms.compress = algoList; } + + this._cb = cb; + this._initialResponse = true; + + this._protocol.authInfoReq(title, instructions, prompts); } +} - // Make sure we at least have some kind of valid list of support key - // formats - if (algorithms.serverHostKey === undefined) { - var hostKeyAlgos = Object.keys(hostKeys); - for (i = hostKeyAlgos.length - 1; i >= 0; --i) { - if (!hostKeys[hostKeyAlgos[i]]) - hostKeyAlgos.splice(i, 1); +class PKAuthContext extends AuthContext { + constructor(protocol, username, service, method, pkInfo, cb) { + super(protocol, username, service, method, cb); + + this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key }; + this.signature = pkInfo.signature; + let sigAlgo; + if (this.signature) { + // TODO: move key type checking logic to ssh2-streams + switch (pkInfo.keyAlgo) { + case 'ssh-rsa': + case 'ssh-dss': + sigAlgo = 'sha1'; + break; + case 'ssh-ed25519': + sigAlgo = null; + break; + case 'ecdsa-sha2-nistp256': + sigAlgo = 'sha256'; + break; + case 'ecdsa-sha2-nistp384': + sigAlgo = 'sha384'; + break; + case 'ecdsa-sha2-nistp521': + sigAlgo = 'sha512'; + break; + } } - algorithms.serverHostKey = hostKeyAlgos; + this.sigAlgo = sigAlgo; + this.blob = pkInfo.blob; } - if (!kaMgr - && Server.KEEPALIVE_INTERVAL > 0 - && Server.KEEPALIVE_CLIENT_INTERVAL > 0 - && Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0) { - kaMgr = new KeepaliveManager(Server.KEEPALIVE_INTERVAL, - Server.KEEPALIVE_CLIENT_INTERVAL, - Server.KEEPALIVE_CLIENT_COUNT_MAX); + accept() { + if (!this.signature) { + this._initialResponse = true; + this._protocol.authPKOK(this.key.algo, this.key.data); + } else { + AuthContext.prototype.accept.call(this); + } } +} - var self = this; - - EventEmitter.call(this); - - if (typeof listener === 'function') - self.on('connection', listener); - - var streamcfg = { - algorithms: algorithms, - hostKeys: hostKeys, - server: true - }; - var keys; - var len; - for (i = 0, keys = Object.keys(cfg), len = keys.length; i < len; ++i) { - var key = keys[i]; - if (key === 'privateKey' - || key === 'publicKey' - || key === 'passphrase' - || key === 'algorithms' - || key === 'hostKeys' - || key === 'server') { - continue; +class HostbasedAuthContext extends AuthContext { + constructor(protocol, username, service, method, pkInfo, cb) { + super(protocol, username, service, method, cb); + + this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key }; + this.signature = pkInfo.signature; + let sigAlgo; + if (this.signature) { + // TODO: move key type checking logic to ssh2-streams + switch (pkInfo.keyAlgo) { + case 'ssh-rsa': + case 'ssh-dss': + sigAlgo = 'sha1'; + break; + case 'ssh-ed25519': + sigAlgo = null; + break; + case 'ecdsa-sha2-nistp256': + sigAlgo = 'sha256'; + break; + case 'ecdsa-sha2-nistp384': + sigAlgo = 'sha384'; + break; + case 'ecdsa-sha2-nistp521': + sigAlgo = 'sha512'; + break; + } } - streamcfg[key] = cfg[key]; + this.sigAlgo = sigAlgo; + this.blob = pkInfo.blob; + this.localHostname = pkInfo.localHostname; + this.localUsername = pkInfo.localUsername; } +} + +class PwdAuthContext extends AuthContext { + constructor(protocol, username, service, method, password, cb) { + super(protocol, username, service, method, cb); - if (typeof streamcfg.debug === 'function') { - var oldDebug = streamcfg.debug; - var cfgKeys = Object.keys(streamcfg); + this.password = password; } +} - this._srv = new net.Server(function(socket) { - if (self._connections >= self.maxConnections) { - socket.destroy(); - return; - } - ++self._connections; - socket.once('close', function(had_err) { - --self._connections; - - // since joyent/node#993bb93e0a, we have to "read past EOF" in order to - // get an `end` event on streams. thankfully adding this does not - // negatively affect node versions pre-joyent/node#993bb93e0a. - sshstream.read(); - }).on('error', function(err) { - sshstream.reset(); - sshstream.emit('error', err); - }); - var conncfg = streamcfg; +class Session extends EventEmitter { + constructor(client, info, localChan) { + super(); - // prepend debug output with a unique identifier in case there are multiple - // clients connected at the same time - if (oldDebug) { - conncfg = {}; - for (var i = 0, key; i < cfgKeys.length; ++i) { - key = cfgKeys[i]; - conncfg[key] = streamcfg[key]; + this.type = 'session'; + this.subtype = undefined; + this._ending = false; + this._channel = undefined; + this._chanInfo = { + type: 'session', + incoming: { + id: localChan, + window: MAX_WINDOW, + packetSize: PACKET_SIZE, + state: 'open' + }, + outgoing: { + id: info.sender, + window: info.window, + packetSize: info.packetSize, + state: 'open' } - var debugPrefix = '[' + process.hrtime().join('.') + '] '; - conncfg.debug = function(msg) { - oldDebug(debugPrefix + msg); - }; - } - - var sshstream = new SSH2Stream(conncfg); - var client = new Client(sshstream, socket); + }; + } +} - socket.pipe(sshstream).pipe(socket); - // silence pre-header errors - function onClientPreHeaderError(err) {} - client.on('error', onClientPreHeaderError); - - sshstream.once('header', function(header) { - if (sshstream._readableState.ended) { - // already disconnected internally in SSH2Stream due to incompatible - // protocol version - return; - } else if (!listenerCount(self, 'connection')) { - // auto reject - return sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION); +class Server extends EventEmitter { + constructor(cfg, listener) { + super(); + + if (typeof cfg !== 'object' || cfg === null) + throw new Error('Missing configuration object'); + + const hostKeys = Object.create(null); + const hostKeyAlgoOrder = []; + + const hostKeys_ = cfg.hostKeys; + if (!Array.isArray(hostKeys_)) + throw new Error('hostKeys must be an array'); + + const cfgAlgos = ( + typeof cfg.algorithms === 'object' && cfg.algorithms !== null + ? cfg.algorithms + : {} + ); + + const hostKeyAlgos = generateAlgorithmList( + cfgAlgos.serverHostKey, + DEFAULT_SERVER_HOST_KEY, + SUPPORTED_SERVER_HOST_KEY + ); + for (let i = 0; i < hostKeys_.length; ++i) { + let privateKey; + if (Buffer.isBuffer(hostKeys_[i]) || typeof hostKeys_[i] === 'string') + privateKey = parseKey(hostKeys_[i]); + else + privateKey = parseKey(hostKeys_[i].key, hostKeys_[i].passphrase); + + if (privateKey instanceof Error) + throw new Error(`Cannot parse privateKey: ${privateKey.message}`); + + if (Array.isArray(privateKey)) { + // OpenSSH's newer format only stores 1 key for now + privateKey = privateKey[0]; } - client.removeListener('error', onClientPreHeaderError); + if (privateKey.getPrivatePEM() === null) + throw new Error('privateKey value contains an invalid private key'); + + // Discard key if we already found a key of the same type + if (hostKeyAlgoOrder.includes(privateKey.type)) + continue; + + if (privateKey.type === 'ssh-rsa') { + // SSH supports multiple signature hashing algorithms for RSA, so we add + // the algorithms in the desired order + let sha1Pos = hostKeyAlgos.indexOf('ssh-rsa'); + const sha256Pos = hostKeyAlgos.indexOf('rsa-sha2-256'); + const sha512Pos = hostKeyAlgos.indexOf('rsa-sha2-512'); + if (sha1Pos === -1) { + // Fall back to giving SHA1 the lowest priority + sha1Pos = Infinity; + } + [sha1Pos, sha256Pos, sha512Pos].sort(compareNumbers).forEach((pos) => { + if (pos === -1) + return; - self.emit('connection', - client, - { ip: socket.remoteAddress, - family: socket.remoteFamily, - port: socket.remotePort, - header: header }); - }); - }).on('error', function(err) { - self.emit('error', err); - }).on('listening', function() { - self.emit('listening'); - }).on('close', function() { - self.emit('close'); - }); - this._connections = 0; - this.maxConnections = Infinity; -} -inherits(Server, EventEmitter); - -Server.prototype.listen = function() { - this._srv.listen.apply(this._srv, arguments); - return this; -}; - -Server.prototype.address = function() { - return this._srv.address(); -}; - -Server.prototype.getConnections = function(cb) { - this._srv.getConnections(cb); -}; - -Server.prototype.close = function(cb) { - this._srv.close(cb); - return this; -}; - -Server.prototype.ref = function() { - this._srv.ref(); -}; - -Server.prototype.unref = function() { - this._srv.unref(); -}; - - -function Client(stream, socket) { - EventEmitter.call(this); - - var self = this; - - this._sshstream = stream; - var channels = this._channels = {}; - this._curChan = -1; - this._sock = socket; - this.noMoreSessions = false; - this.authenticated = false; - - stream.on('end', function() { - socket.resume(); - self.emit('end'); - }).on('close', function(hasErr) { - self.emit('close', hasErr); - }).on('error', function(err) { - self.emit('error', err); - }).on('drain', function() { - self.emit('drain'); - }).on('continue', function() { - self.emit('continue'); - }); - - var exchanges = 0; - var acceptedAuthSvc = false; - var pendingAuths = []; - var authCtx; - - // begin service/auth-related ================================================ - stream.on('SERVICE_REQUEST', function(service) { - if (exchanges === 0 - || acceptedAuthSvc - || self.authenticated - || service !== 'ssh-userauth') - return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE); - - acceptedAuthSvc = true; - stream.serviceAccept(service); - }).on('USERAUTH_REQUEST', onUSERAUTH_REQUEST); - function onUSERAUTH_REQUEST(username, service, method, methodData) { - if (exchanges === 0 - || (authCtx - && (authCtx.username !== username || authCtx.service !== service)) - // TODO: support hostbased auth - || (method !== 'password' - && method !== 'publickey' - && method !== 'hostbased' - && method !== 'keyboard-interactive' - && method !== 'none') - || pendingAuths.length === MAX_PENDING_AUTHS) - return stream.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR); - else if (service !== 'ssh-connection') - return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE); - - // XXX: this really shouldn't be reaching into private state ... - stream._state.authMethod = method; - - var ctx; - if (method === 'keyboard-interactive') { - ctx = new KeyboardAuthContext(stream, username, service, method, - methodData, onAuthDecide); - } else if (method === 'publickey') { - ctx = new PKAuthContext(stream, username, service, method, methodData, - onAuthDecide); - } else if (method === 'hostbased') { - ctx = new HostbasedAuthContext(stream, username, service, method, - methodData, onAuthDecide); - } else if (method === 'password') { - ctx = new PwdAuthContext(stream, username, service, method, methodData, - onAuthDecide); - } else if (method === 'none') - ctx = new AuthContext(stream, username, service, method, onAuthDecide); - - if (authCtx) { - if (!authCtx._initialResponse) - return pendingAuths.push(ctx); - else if (authCtx._multistep && !this._finalResponse) { - // RFC 4252 says to silently abort the current auth request if a new - // auth request comes in before the final response from an auth method - // that requires additional request/response exchanges -- this means - // keyboard-interactive for now ... - authCtx._cleanup && authCtx._cleanup(); - authCtx.emit('abort'); - } - } + let type; + switch (pos) { + case sha1Pos: type = 'ssh-rsa'; break; + case sha256Pos: type = 'rsa-sha2-256'; break; + case sha512Pos: type = 'rsa-sha2-512'; break; + default: return; + } - authCtx = ctx; + // Store same RSA key under each hash algorithm name for convenience + hostKeys[type] = privateKey; - if (listenerCount(self, 'authentication')) - self.emit('authentication', authCtx); - else - authCtx.reject(); - } - function onAuthDecide(ctx, allowed, methodsLeft, isPartial) { - if (authCtx === ctx && !self.authenticated) { - if (allowed) { - stream.removeListener('USERAUTH_REQUEST', onUSERAUTH_REQUEST); - authCtx = undefined; - self.authenticated = true; - stream.authSuccess(); - pendingAuths = []; - self.emit('ready'); + hostKeyAlgoOrder.push(type); + }); } else { - stream.authFailure(methodsLeft, isPartial); - if (pendingAuths.length) { - authCtx = pendingAuths.pop(); - if (listenerCount(self, 'authentication')) - self.emit('authentication', authCtx); - else - authCtx.reject(); - } + hostKeys[privateKey.type] = privateKey; + hostKeyAlgoOrder.push(privateKey.type); } } - } - // end service/auth-related ================================================== - - var unsentGlobalRequestsReplies = []; - - function sendReplies() { - var reply; - while (unsentGlobalRequestsReplies.length > 0 - && unsentGlobalRequestsReplies[0].type) { - reply = unsentGlobalRequestsReplies.shift(); - if (reply.type === 'SUCCESS') - stream.requestSuccess(reply.buf); - if (reply.type === 'FAILURE') - stream.requestFailure(); - } - } - stream.on('GLOBAL_REQUEST', function(name, wantReply, data) { - var reply = { - type: null, - buf: null + const algorithms = { + kex: generateAlgorithmList(cfgAlgos.kex, DEFAULT_KEX, SUPPORTED_KEX), + srvHostKey: hostKeyAlgoOrder, + cs: { + cipher: generateAlgorithmList( + cfgAlgos.cipher, + DEFAULT_CIPHER, + SUPPORTED_CIPHER + ), + mac: generateAlgorithmList(cfgAlgos.hmac, DEFAULT_MAC, SUPPORTED_MAC), + compress: generateAlgorithmList( + cfgAlgos.compress, + DEFAULT_COMPRESSION, + SUPPORTED_COMPRESSION + ), + lang: [], + }, + sc: undefined, }; + algorithms.sc = algorithms.cs; - function setReply(type, buf) { - reply.type = type; - reply.buf = buf; - sendReplies(); - } + if (typeof listener === 'function') + this.on('connection', listener); - if (wantReply) - unsentGlobalRequestsReplies.push(reply); - - if ((name === 'tcpip-forward' - || name === 'cancel-tcpip-forward' - || name === 'no-more-sessions@openssh.com' - || name === 'streamlocal-forward@openssh.com' - || name === 'cancel-streamlocal-forward@openssh.com') - && listenerCount(self, 'request') - && self.authenticated) { - var accept; - var reject; - - if (wantReply) { - var replied = false; - accept = function(chosenPort) { - if (replied) - return; - replied = true; - var bufPort; - if (name === 'tcpip-forward' - && data.bindPort === 0 - && typeof chosenPort === 'number') { - bufPort = Buffer.allocUnsafe(4); - writeUInt32BE(bufPort, chosenPort, 0); - } - setReply('SUCCESS', bufPort); - }; - reject = function() { - if (replied) - return; - replied = true; - setReply('FAILURE'); - }; - } + const origDebug = (typeof cfg.debug === 'function' ? cfg.debug : undefined); + const ident = (cfg.ident ? Buffer.from(cfg.ident) : undefined); + const offer = new KexInit(algorithms); - if (name === 'no-more-sessions@openssh.com') { - self.noMoreSessions = true; - accept && accept(); + this._srv = new netServer((socket) => { + if (this._connections >= this.maxConnections) { + socket.destroy(); return; } + ++this._connections; + socket.once('close', () => { + --this._connections; + }); - self.emit('request', accept, reject, name, data); - } else if (wantReply) - setReply('FAILURE'); - }); - - stream.on('CHANNEL_OPEN', function(info) { - // do early reject in some cases to prevent wasteful channel allocation - if ((info.type === 'session' && self.noMoreSessions) - || !self.authenticated) { - var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; - return stream.channelOpenFail(info.sender, reasonCode); - } + let debug; + if (origDebug) { + // Prepend debug output with a unique identifier in case there are + // multiple clients connected at the same time + const debugPrefix = `[${process.hrtime().join('.')}] `; + debug = (msg) => { + origDebug(`${debugPrefix}${msg}`); + }; + } - var localChan = nextChannel(self); - var accept; - var reject; - var replied = false; - if (localChan === false) { - // auto-reject due to no channels available - return stream.channelOpenFail(info.sender, - CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE); - } + // eslint-disable-next-line no-use-before-define + new Client(socket, hostKeys, ident, offer, debug, this, cfg); + }).on('error', (err) => { + this.emit('error', err); + }).on('listening', () => { + this.emit('listening'); + }).on('close', () => { + this.emit('close'); + }); + this._connections = 0; + this.maxConnections = Infinity; + } - // be optimistic, reserve channel to prevent another request from trying to - // take the same channel - channels[localChan] = true; + listen(...args) { + this._srv.listen(...args); + return this; + } - reject = function() { - if (replied) - return; + address() { + return this._srv.address(); + } - replied = true; + getConnections(cb) { + this._srv.getConnections(cb); + return this; + } - delete channels[localChan]; + close(cb) { + this._srv.close(cb); + return this; + } - var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; - return stream.channelOpenFail(info.sender, reasonCode); - }; + ref() { + this._srv.ref(); + return this; + } - switch (info.type) { - case 'session': - if (listenerCount(self, 'session')) { - accept = function() { - if (replied) - return; + unref() { + this._srv.unref(); + return this; + } +} +Server.KEEPALIVE_CLIENT_INTERVAL = 15000; +Server.KEEPALIVE_CLIENT_COUNT_MAX = 3; - replied = true; - stream.channelOpenConfirm(info.sender, - localChan, - Channel.MAX_WINDOW, - Channel.PACKET_SIZE); +class Client extends EventEmitter { + constructor(socket, hostKeys, ident, offer, debug, server, srvCfg) { + super(); + + let exchanges = 0; + let acceptedAuthSvc = false; + let pendingAuths = []; + let authCtx; + let kaTimer; + let onPacket; + const unsentGlobalRequestsReplies = []; + this._sock = socket; + this._chanMgr = new ChannelManager(this); + this._debug = debug; + this.noMoreSessions = false; + this.authenticated = false; + + socket.on('error', (err) => { + err.level = 'socket'; + this.emit('error', err); + }).once('close', () => { + debug && debug('Socket closed'); + this.emit('close'); + }).once('end', () => { + debug && debug('Socket ended'); + this.emit('end'); + }); - return new Session(self, info, localChan); - }; + // Silence pre-header errors + function onClientPreHeaderError(err) {} + this.on('error', onClientPreHeaderError); - self.emit('session', accept, reject); - } else - reject(); - break; - case 'direct-tcpip': - if (listenerCount(self, 'tcpip')) { - accept = function() { - if (replied) - return; + const DEBUG_HANDLER = (!debug ? undefined : (p, display, msg) => { + debug(`Debug output from client: ${JSON.stringify(msg)}`); + }); - replied = true; + const kaIntvl = ( + typeof srvCfg.keepaliveInterval === 'number' + && isFinite(srvCfg.keepaliveInterval) + && srvCfg.keepaliveInterval > 0 + ? srvCfg.keepaliveInterval + : ( + typeof Server.KEEPALIVE_CLIENT_INTERVAL === 'number' + && isFinite(Server.KEEPALIVE_CLIENT_INTERVAL) + && Server.KEEPALIVE_CLIENT_INTERVAL > 0 + ? Server.KEEPALIVE_CLIENT_INTERVAL + : -1 + ) + ); + const kaCountMax = ( + typeof srvCfg.keepaliveCountMax === 'number' + && isFinite(srvCfg.keepaliveCountMax) + && srvCfg.keepaliveCountMax >= 0 + ? srvCfg.keepaliveCountMax + : ( + typeof Server.KEEPALIVE_CLIENT_COUNT_MAX === 'number' + && isFinite(Server.KEEPALIVE_CLIENT_COUNT_MAX) + && Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0 + ? Server.KEEPALIVE_CLIENT_COUNT_MAX + : -1 + ) + ); + let kaCurCount = 0; + if (kaIntvl !== -1 && kaCountMax !== -1) { + this.once('ready', () => { + const onClose = () => { + clearInterval(kaTimer); + }; + this.on('close', onClose).on('end', onClose); + kaTimer = setInterval(() => { + if (++kaCurCount > kaCountMax) { + clearInterval(kaTimer); + const err = new Error('Keepalive timeout'); + err.level = 'client-timeout'; + this.emit('error', err); + this.end(); + } else { + // XXX: if the server ever starts sending real global requests to + // the client, we will need to add a dummy callback here to + // keep the correct reply order + proto.ping(); + } + }, kaIntvl); + }); + // TODO: re-verify keepalive behavior with OpenSSH + onPacket = () => { + kaTimer && kaTimer.refresh(); + kaCurCount = 0; + }; + } - stream.channelOpenConfirm(info.sender, - localChan, - Channel.MAX_WINDOW, - Channel.PACKET_SIZE); - - var chaninfo = { - type: undefined, - incoming: { - id: localChan, - window: Channel.MAX_WINDOW, - packetSize: Channel.PACKET_SIZE, - state: 'open' - }, - outgoing: { - id: info.sender, - window: info.window, - packetSize: info.packetSize, - state: 'open' - } - }; + const proto = this._protocol = new Protocol({ + server: true, + hostKeys, + ident, + offer, + onPacket, + greeting: srvCfg.greeting, + banner: srvCfg.banner, + onWrite: (data) => { + if (socket.writable) + socket.write(data); + }, + onError: (err) => { + if (!proto._destruct) + socket.removeAllListeners('data'); + this.emit('error', err); + try { + socket.end(); + } catch {} + }, + onHeader: (header) => { + this.removeListener('error', onClientPreHeaderError); + + const info = { + ip: socket.remoteAddress, + family: socket.remoteFamily, + port: socket.remotePort, + header, + }; + if (!server.emit('connection', this, info)) { + // auto reject + proto.disconnect(DISCONNECT_REASON.BY_APPLICATION); + socket.end(); + return; + } - return new Channel(chaninfo, self); - }; + if (header.greeting) + this.emit('greeting', header.greeting); + }, + onHandshakeComplete: (negotiated) => { + if (++exchanges > 1) + this.emit('rekey'); + this.emit('handshake', negotiated); + }, + debug, + messageHandlers: { + DEBUG: DEBUG_HANDLER, + DISCONNECT: (p, reason, desc) => { + if (reason !== DISCONNECT_REASON.BY_APPLICATION) { + if (!desc) { + desc = DISCONNECT_REASON_BY_VALUE[reason]; + if (desc === undefined) + desc = `Unexpected disconnection reason: ${reason}`; + } + const err = new Error(desc); + err.code = reason; + this.emit('error', err); + } + socket.end(); + }, + CHANNEL_OPEN: (p, info) => { + // Handle incoming requests from client + + // Do early reject in some cases to prevent wasteful channel + // allocation + if ((info.type === 'session' && this.noMoreSessions) + || !this.authenticated) { + const reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; + return proto.channelOpenFail(info.sender, reasonCode); + } - self.emit('tcpip', accept, reject, info.data); - } else - reject(); - break; - case 'direct-streamlocal@openssh.com': - if (listenerCount(self, 'openssh.streamlocal')) { - accept = function() { + let localChan = -1; + let reason; + let replied = false; + + let accept; + const reject = () => { if (replied) return; - replied = true; - stream.channelOpenConfirm(info.sender, - localChan, - Channel.MAX_WINDOW, - Channel.PACKET_SIZE); - - var chaninfo = { - type: undefined, - incoming: { - id: localChan, - window: Channel.MAX_WINDOW, - packetSize: Channel.PACKET_SIZE, - state: 'open' - }, - outgoing: { - id: info.sender, - window: info.window, - packetSize: info.packetSize, - state: 'open' + if (reason === undefined) { + if (localChan === -1) + reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE; + else + reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED; + } + + proto.channelOpenFail(info.sender, reason, ''); + }; + const reserveChannel = () => { + localChan = this._chanMgr.add(); + + if (localChan === -1) { + reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE; + if (debug) { + debug('Automatic rejection of incoming channel open: ' + + 'no channels available'); } - }; + } - return new Channel(chaninfo, self); + return (localChan !== -1); }; - self.emit('openssh.streamlocal', accept, reject, info.data); - } else - reject(); - break; - default: - // auto-reject unsupported channel types - reject(); - } - }); + const data = info.data; + switch (info.type) { + case 'session': + if (listenerCount(this, 'session') && reserveChannel()) { + accept = () => { + if (replied) + return; + replied = true; - stream.on('NEWKEYS', function() { - if (++exchanges > 1) - self.emit('rekey'); - }); + const instance = new Session(this, info, localChan); + this._chanMgr.update(localChan, instance); - if (kaMgr) { - this.once('ready', function() { - kaMgr.add(stream); - }); - } -} -inherits(Client, EventEmitter); + proto.channelOpenConfirm(info.sender, + localChan, + MAX_WINDOW, + PACKET_SIZE); -Client.prototype.end = function() { - return this._sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION); -}; + return instance; + }; -Client.prototype.x11 = function(originAddr, originPort, cb) { - var opts = { - originAddr: originAddr, - originPort: originPort - }; - return openChannel(this, 'x11', opts, cb); -}; - -Client.prototype.forwardOut = function(boundAddr, boundPort, remoteAddr, - remotePort, cb) { - var opts = { - boundAddr: boundAddr, - boundPort: boundPort, - remoteAddr: remoteAddr, - remotePort: remotePort - }; - return openChannel(this, 'forwarded-tcpip', opts, cb); -}; + this.emit('session', accept, reject); + return; + } + break; + case 'direct-tcpip': + if (listenerCount(this, 'tcpip') && reserveChannel()) { + accept = () => { + if (replied) + return; + replied = true; + + const chanInfo = { + type: undefined, + incoming: { + id: localChan, + window: MAX_WINDOW, + packetSize: PACKET_SIZE, + state: 'open' + }, + outgoing: { + id: info.sender, + window: info.window, + packetSize: info.packetSize, + state: 'open' + } + }; + + const stream = new Channel(this, chanInfo); + this._chanMgr.update(localChan, stream); + + proto.channelOpenConfirm(info.sender, + localChan, + MAX_WINDOW, + PACKET_SIZE); + + return stream; + }; + + this.emit('tcpip', accept, reject, data); + return; + } + break; + case 'direct-streamlocal@openssh.com': + if (listenerCount(this, 'openssh.streamlocal') + && reserveChannel()) { + accept = () => { + if (replied) + return; + replied = true; + + const chanInfo = { + type: undefined, + incoming: { + id: localChan, + window: MAX_WINDOW, + packetSize: PACKET_SIZE, + state: 'open' + }, + outgoing: { + id: info.sender, + window: info.window, + packetSize: info.packetSize, + state: 'open' + } + }; + + const stream = new Channel(this, chanInfo); + this._chanMgr.update(localChan, stream); + + proto.channelOpenConfirm(info.sender, + localChan, + MAX_WINDOW, + PACKET_SIZE); + + return stream; + }; + + this.emit('openssh.streamlocal', accept, reject, data); + return; + } + break; + default: + // Automatically reject any unsupported channel open requests + reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE; + if (debug) { + debug('Automatic rejection of unsupported incoming channel open' + + ` type: ${info.type}`); + } + } -Client.prototype.openssh_forwardOutStreamLocal = function(socketPath, cb) { - var opts = { - socketPath: socketPath - }; - return openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb); -}; - -Client.prototype.rekey = function(cb) { - var stream = this._sshstream; - var ret = true; - var error; - - try { - ret = stream.rekey(); - } catch (ex) { - error = ex; - } + if (reason === undefined) { + reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED; + if (debug) { + debug('Automatic rejection of unexpected incoming channel open' + + ` for: ${info.type}`); + } + } - // TODO: re-throw error if no callback? + reject(); + }, + CHANNEL_OPEN_CONFIRMATION: (p, info) => { + const channel = this._chanMgr.get(info.recipient); + if (typeof channel !== 'function') + return; - if (typeof cb === 'function') { - if (error) { - process.nextTick(function() { - cb(error); - }); - } else - this.once('rekey', cb); - } + const chanInfo = { + type: channel.type, + incoming: { + id: info.recipient, + window: MAX_WINDOW, + packetSize: PACKET_SIZE, + state: 'open' + }, + outgoing: { + id: info.sender, + window: info.window, + packetSize: info.packetSize, + state: 'open' + } + }; - return ret; -}; - -function Session(client, info, localChan) { - this.subtype = undefined; - - var ending = false; - var self = this; - var outgoingId = info.sender; - var channel; - - var chaninfo = { - type: 'session', - incoming: { - id: localChan, - window: Channel.MAX_WINDOW, - packetSize: Channel.PACKET_SIZE, - state: 'open' - }, - outgoing: { - id: info.sender, - window: info.window, - packetSize: info.packetSize, - state: 'open' - } - }; + const instance = new Channel(this, chanInfo, { server: true }); + this._chanMgr.update(info.recipient, instance); + channel(undefined, instance); + }, + CHANNEL_OPEN_FAILURE: (p, recipient, reason, description) => { + const channel = this._chanMgr.get(recipient); + if (typeof channel !== 'function') + return; - function onREQUEST(info) { - var replied = false; - var accept; - var reject; - - if (info.wantReply) { - // "real session" requests will have custom accept behaviors - if (info.request !== 'shell' - && info.request !== 'exec' - && info.request !== 'subsystem') { - accept = function() { - if (replied || ending || channel) + const info = { reason, description }; + onChannelOpenFailure(this, recipient, info, channel); + }, + CHANNEL_DATA: (p, recipient, data) => { + let channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) return; - replied = true; + if (channel.constructor === Session) { + channel = channel._channel; + if (!channel) + return; + } - return client._sshstream.channelSuccess(outgoingId); - }; - } + // The remote party should not be sending us data if there is no + // window space available ... + // TODO: raise error on data with not enough window? + if (channel.incoming.window === 0) + return; - reject = function() { - if (replied || ending || channel) - return; + channel.incoming.window -= data.length; - replied = true; + if (channel.push(data) === false) { + channel._waitChanDrain = true; + return; + } - return client._sshstream.channelFailure(outgoingId); - }; - } + if (channel.incoming.window <= WINDOW_THRESHOLD) + windowAdjust(channel); + }, + CHANNEL_EXTENDED_DATA: (p, recipient, data, type) => { + // NOOP -- should not be sent by client + }, + CHANNEL_WINDOW_ADJUST: (p, recipient, amount) => { + let channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; - if (ending) { - reject && reject(); - return; - } + if (channel.constructor === Session) { + channel = channel._channel; + if (!channel) + return; + } - switch (info.request) { - // "pre-real session start" requests - case 'env': - if (listenerCount(self, 'env')) { - self.emit('env', accept, reject, { - key: info.key, - val: info.val - }); - } else - reject && reject(); - break; - case 'pty-req': - if (listenerCount(self, 'pty')) { - self.emit('pty', accept, reject, { - cols: info.cols, - rows: info.rows, - width: info.width, - height: info.height, - term: info.term, - modes: info.modes, - }); - } else - reject && reject(); - break; - case 'window-change': - if (listenerCount(self, 'window-change')) { - self.emit('window-change', accept, reject, { - cols: info.cols, - rows: info.rows, - width: info.width, - height: info.height - }); - } else - reject && reject(); - break; - case 'x11-req': - if (listenerCount(self, 'x11')) { - self.emit('x11', accept, reject, { - single: info.single, - protocol: info.protocol, - cookie: info.cookie, - screen: info.screen - }); - } else - reject && reject(); - break; - // "post-real session start" requests - case 'signal': - if (listenerCount(self, 'signal')) { - self.emit('signal', accept, reject, { - name: info.signal - }); - } else - reject && reject(); - break; - // XXX: is `auth-agent-req@openssh.com` really "post-real session start"? - case 'auth-agent-req@openssh.com': - if (listenerCount(self, 'auth-agent')) - self.emit('auth-agent', accept, reject); - else - reject && reject(); - break; - // "real session start" requests - case 'shell': - if (listenerCount(self, 'shell')) { - accept = function() { - if (replied || ending || channel) + // The other side is allowing us to send `amount` more bytes of data + channel.outgoing.window += amount; + + if (channel._waitWindow) { + channel._waitWindow = false; + + if (channel._chunk) { + channel._write(channel._chunk, null, channel._chunkcb); + } else if (channel._chunkcb) { + channel._chunkcb(); + } else if (channel._chunkErr) { + channel.stderr._write(channel._chunkErr, + null, + channel._chunkcbErr); + } else if (channel._chunkcbErr) { + channel._chunkcbErr(); + } + } + }, + CHANNEL_SUCCESS: (p, recipient) => { + let channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; + + if (channel.constructor === Session) { + channel = channel._channel; + if (!channel) return; + } - replied = true; + if (channel._callbacks.length) + channel._callbacks.shift()(false); + }, + CHANNEL_FAILURE: (p, recipient) => { + let channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; - if (info.wantReply) - client._sshstream.channelSuccess(outgoingId); + if (channel.constructor === Session) { + channel = channel._channel; + if (!channel) + return; + } - channel = new Channel(chaninfo, client, { server: true }); + if (channel._callbacks.length) + channel._callbacks.shift()(true); + }, + CHANNEL_REQUEST: (p, recipient, type, wantReply, data) => { + const session = this._chanMgr.get(recipient); + if (typeof session !== 'object' || session === null) + return; - channel.subtype = self.subtype = info.request; + let replied = false; + let accept; + let reject; - return channel; - }; + if (session.constructor !== Session) { + // normal Channel instance + if (wantReply) + proto.channelFailure(session.outgoing.id); + return; + } - self.emit('shell', accept, reject); - } else - reject && reject(); - break; - case 'exec': - if (listenerCount(self, 'exec')) { - accept = function() { - if (replied || ending || channel) - return; + if (wantReply) { + // "real session" requests will have custom accept behaviors + if (type !== 'shell' + && type !== 'exec' + && type !== 'subsystem') { + accept = () => { + if (replied || session._ending || session._channel) + return; + replied = true; + + proto.channelSuccess(session._chanInfo.outgoing.id); + }; + } + + reject = () => { + if (replied || session._ending || session._channel) + return; + replied = true; + + proto.channelFailure(session._chanInfo.outgoing.id); + }; + } - replied = true; + if (session._ending) { + reject && reject(); + return; + } - if (info.wantReply) - client._sshstream.channelSuccess(outgoingId); + switch (type) { + // "pre-real session start" requests + case 'env': + if (listenerCount(session, 'env')) { + session.emit('env', accept, reject, { + key: data.name, + val: data.value + }); + return; + } + break; + case 'pty-req': + if (listenerCount(session, 'pty')) { + session.emit('pty', accept, reject, data); + return; + } + break; + case 'window-change': + if (listenerCount(session, 'window-change')) + session.emit('window-change', accept, reject, data); + else + reject && reject(); + break; + case 'x11-req': + if (listenerCount(session, 'x11')) { + session.emit('x11', accept, reject, data); + return; + } + break; + // "post-real session start" requests + case 'signal': + if (listenerCount(session, 'signal')) { + session.emit('signal', accept, reject, { + name: data + }); + return; + } + break; + // XXX: is `auth-agent-req@openssh.com` really "post-real session + // start"? + case 'auth-agent-req@openssh.com': + if (listenerCount(session, 'auth-agent')) { + session.emit('auth-agent', accept, reject); + return; + } + break; + // "real session start" requests + case 'shell': + if (listenerCount(session, 'shell')) { + accept = () => { + if (replied || session._ending || session._channel) + return; + replied = true; + + if (wantReply) + proto.channelSuccess(session._chanInfo.outgoing.id); + + const channel = new Channel( + this, session._chanInfo, { server: true } + ); + + channel.subtype = session.subtype = type; + session._channel = channel; + + return channel; + }; + + session.emit('shell', accept, reject); + return; + } + break; + case 'exec': + if (listenerCount(session, 'exec')) { + accept = () => { + if (replied || session._ending || session._channel) + return; + replied = true; + + if (wantReply) + proto.channelSuccess(session._chanInfo.outgoing.id); + + const channel = new Channel( + this, session._chanInfo, { server: true } + ); + + channel.subtype = session.subtype = type; + session._channel = channel; + + return channel; + }; + + session.emit('exec', accept, reject, { + command: data + }); + return; + } + break; + case 'subsystem': { + let useSFTP = (data === 'sftp'); + accept = () => { + if (replied || session._ending || session._channel) + return; + replied = true; + + if (wantReply) + proto.channelSuccess(session._chanInfo.outgoing.id); + + let instance; + if (useSFTP) { + instance = new SFTP(this, session._chanInfo, { + server: true, + debug, + }); + } else { + instance = new Channel( + this, session._chanInfo, { server: true } + ); + instance.subtype = + session.subtype = `${type}:${data}`; + } + session._channel = instance; + + return instance; + }; + + if (data === 'sftp') { + if (listenerCount(session, 'sftp')) { + session.emit('sftp', accept, reject); + return; + } + useSFTP = false; + } + if (listenerCount(session, 'subsystem')) { + session.emit('subsystem', accept, reject, { + name: data + }); + return; + } + break; + } + } + debug && debug( + `Automatic rejection of incoming channel request: ${type}` + ); + reject && reject(); + }, + CHANNEL_EOF: (p, recipient) => { + let channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; - channel = new Channel(chaninfo, client, { server: true }); + if (channel.constructor === Session) { + if (!channel._ending) { + channel._ending = true; + channel.emit('eof'); + channel.emit('end'); + } + channel = channel._channel; + if (!channel) + return; + } - channel.subtype = self.subtype = info.request; + if (channel.incoming.state !== 'open') + return; + channel.incoming.state = 'eof'; + + if (channel.readable) + channel.push(null); + }, + CHANNEL_CLOSE: (p, recipient) => { + let channel = this._chanMgr.get(recipient); + if (typeof channel !== 'object' || channel === null) + return; - return channel; - }; + if (channel.constructor === Session) { + channel._ending = true; + channel.emit('close'); + channel = channel._channel; + if (!channel) + return; + } - self.emit('exec', accept, reject, { - command: info.command - }); - } else - reject && reject(); - break; - case 'subsystem': - accept = function() { - if (replied || ending || channel) + onCHANNEL_CLOSE(this, recipient, channel); + }, + // Begin service/auth-related ========================================== + SERVICE_REQUEST: (p, service) => { + if (exchanges === 0 + || acceptedAuthSvc + || this.authenticated + || service !== 'ssh-userauth') { + proto.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE); + socket.end(); return; + } - replied = true; + acceptedAuthSvc = true; + proto.serviceAccept(service); + }, + USERAUTH_REQUEST: (p, username, service, method, methodData) => { + if (exchanges === 0 + || this.authenticated + || (authCtx + && (authCtx.username !== username + || authCtx.service !== service)) + // TODO: support hostbased auth + || (method !== 'password' + && method !== 'publickey' + && method !== 'hostbased' + && method !== 'keyboard-interactive' + && method !== 'none') + || pendingAuths.length === MAX_PENDING_AUTHS) { + proto.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR); + socket.end(); + return; + } else if (service !== 'ssh-connection') { + proto.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE); + socket.end(); + return; + } - if (info.wantReply) - client._sshstream.channelSuccess(outgoingId); + let ctx; + switch (method) { + case 'keyboard-interactive': + ctx = new KeyboardAuthContext(proto, username, service, method, + methodData, onAuthDecide); + break; + case 'publickey': + ctx = new PKAuthContext(proto, username, service, method, + methodData, onAuthDecide); + break; + case 'hostbased': + ctx = new HostbasedAuthContext(proto, username, service, method, + methodData, onAuthDecide); + break; + case 'password': + ctx = new PwdAuthContext(proto, username, service, method, + methodData, onAuthDecide); + break; + case 'none': + ctx = new AuthContext(proto, username, service, method, + onAuthDecide); + break; + } - channel = new Channel(chaninfo, client, { server: true }); + if (authCtx) { + if (!authCtx._initialResponse) { + return pendingAuths.push(ctx); + } else if (authCtx._multistep && !authCtx._finalResponse) { + // RFC 4252 says to silently abort the current auth request if a + // new auth request comes in before the final response from an + // auth method that requires additional request/response exchanges + // -- this means keyboard-interactive for now ... + authCtx._cleanup && authCtx._cleanup(); + authCtx.emit('abort'); + } + } - channel.subtype = self.subtype = (info.request + ':' + info.subsystem); + authCtx = ctx; - if (info.subsystem === 'sftp') { - var sftp = new SFTPStream({ - server: true, - debug: client._sshstream.debug - }); - channel.pipe(sftp).pipe(channel); + if (listenerCount(this, 'authentication')) + this.emit('authentication', authCtx); + else + authCtx.reject(); + }, + USERAUTH_INFO_RESPONSE: (p, responses) => { + if (authCtx && authCtx instanceof KeyboardAuthContext) + authCtx._onInfoResponse(responses); + }, + // End service/auth-related ============================================ + GLOBAL_REQUEST: (p, name, wantReply, data) => { + const reply = { + type: null, + buf: null + }; - return sftp; - } else - return channel; - }; + function setReply(type, buf) { + reply.type = type; + reply.buf = buf; + sendReplies(); + } - if (info.subsystem === 'sftp' && listenerCount(self, 'sftp')) - self.emit('sftp', accept, reject); - else if (info.subsystem !== 'sftp' && listenerCount(self, 'subsystem')) { - self.emit('subsystem', accept, reject, { - name: info.subsystem - }); - } else - reject && reject(); - break; - default: - reject && reject(); - } - } - function onEOF() { - ending = true; - self.emit('eof'); - self.emit('end'); - } - function onCLOSE() { - ending = true; - self.emit('close'); - } - client._sshstream - .on('CHANNEL_REQUEST:' + localChan, onREQUEST) - .once('CHANNEL_EOF:' + localChan, onEOF) - .once('CHANNEL_CLOSE:' + localChan, onCLOSE); -} -inherits(Session, EventEmitter); + if (wantReply) + unsentGlobalRequestsReplies.push(reply); + + if ((name === 'tcpip-forward' + || name === 'cancel-tcpip-forward' + || name === 'no-more-sessions@openssh.com' + || name === 'streamlocal-forward@openssh.com' + || name === 'cancel-streamlocal-forward@openssh.com') + && listenerCount(this, 'request') + && this.authenticated) { + let accept; + let reject; + + if (wantReply) { + let replied = false; + accept = (chosenPort) => { + if (replied) + return; + replied = true; + let bufPort; + if (name === 'tcpip-forward' + && data.bindPort === 0 + && typeof chosenPort === 'number') { + bufPort = Buffer.allocUnsafe(4); + writeUInt32BE(bufPort, chosenPort, 0); + } + setReply('SUCCESS', bufPort); + }; + reject = () => { + if (replied) + return; + replied = true; + setReply('FAILURE'); + }; + } + + if (name === 'no-more-sessions@openssh.com') { + this.noMoreSessions = true; + accept && accept(); + return; + } + this.emit('request', accept, reject, name, data); + } else if (wantReply) { + setReply('FAILURE'); + } + }, + }, + }); -function AuthContext(stream, username, service, method, cb) { - EventEmitter.call(this); + socket.on('data', (data) => { + // TODO: wrap in try-catch and emit caught error(s) + proto.parse(data, 0, data.length); + }); - var self = this; + const onAuthDecide = (ctx, allowed, methodsLeft, isPartial) => { + if (authCtx === ctx && !this.authenticated) { + if (allowed) { + authCtx = undefined; + this.authenticated = true; + proto.authSuccess(); + pendingAuths = []; + this.emit('ready'); + } else { + proto.authFailure(methodsLeft, isPartial); + if (pendingAuths.length) { + authCtx = pendingAuths.pop(); + if (listenerCount(this, 'authentication')) + this.emit('authentication', authCtx); + else + authCtx.reject(); + } + } + } + }; - this.username = this.user = username; - this.service = service; - this.method = method; - this._initialResponse = false; - this._finalResponse = false; - this._multistep = false; - this._cbfinal = function(allowed, methodsLeft, isPartial) { - if (!self._finalResponse) { - self._finalResponse = true; - cb(self, allowed, methodsLeft, isPartial); - } - }; - this._stream = stream; -} -inherits(AuthContext, EventEmitter); -AuthContext.prototype.accept = function() { - this._cleanup && this._cleanup(); - this._initialResponse = true; - this._cbfinal(true); -}; -AuthContext.prototype.reject = function(methodsLeft, isPartial) { - this._cleanup && this._cleanup(); - this._initialResponse = true; - this._cbfinal(false, methodsLeft, isPartial); -}; - -var RE_KBINT_SUBMETHODS = /[ \t\r\n]*,[ \t\r\n]*/g; -function KeyboardAuthContext(stream, username, service, method, submethods, cb) { - AuthContext.call(this, stream, username, service, method, cb); - this._multistep = true; - - var self = this; - - this._cb = undefined; - this._onInfoResponse = function(responses) { - if (self._cb) { - var callback = self._cb; - self._cb = undefined; - callback(responses); + function sendReplies() { + while (unsentGlobalRequestsReplies.length > 0 + && unsentGlobalRequestsReplies[0].type) { + const reply = unsentGlobalRequestsReplies.shift(); + if (reply.type === 'SUCCESS') + proto.requestSuccess(reply.buf); + if (reply.type === 'FAILURE') + proto.requestFailure(); + } } - }; - this.submethods = submethods.split(RE_KBINT_SUBMETHODS); - this.on('abort', function() { - self._cb && self._cb(new Error('Authentication request aborted')); - }); -} -inherits(KeyboardAuthContext, AuthContext); -KeyboardAuthContext.prototype._cleanup = function() { - this._stream.removeListener('USERAUTH_INFO_RESPONSE', this._onInfoResponse); -}; -KeyboardAuthContext.prototype.prompt = function(prompts, title, instructions, - cb) { - if (!Array.isArray(prompts)) - prompts = [ prompts ]; - - if (typeof title === 'function') { - cb = title; - title = instructions = undefined; - } else if (typeof instructions === 'function') { - cb = instructions; - instructions = undefined; } - for (var i = 0; i < prompts.length; ++i) { - if (typeof prompts[i] === 'string') { - prompts[i] = { - prompt: prompts[i], - echo: true - }; + end() { + if (this._sock && this._sock.writable) { + this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION); + this._sock.end(); } + return this; } - this._cb = cb; - this._initialResponse = true; - this._stream.once('USERAUTH_INFO_RESPONSE', this._onInfoResponse); - - return this._stream.authInfoReq(title, instructions, prompts); -}; - -function PKAuthContext(stream, username, service, method, pkInfo, cb) { - AuthContext.call(this, stream, username, service, method, cb); - - this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key }; - this.signature = pkInfo.signature; - var sigAlgo; - if (this.signature) { - // TODO: move key type checking logic to ssh2-streams - switch (pkInfo.keyAlgo) { - case 'ssh-rsa': - case 'ssh-dss': - sigAlgo = 'sha1'; - break; - case 'ssh-ed25519': - sigAlgo = null; - break; - case 'ecdsa-sha2-nistp256': - sigAlgo = 'sha256'; - break; - case 'ecdsa-sha2-nistp384': - sigAlgo = 'sha384'; - break; - case 'ecdsa-sha2-nistp521': - sigAlgo = 'sha512'; - break; - } + x11(originAddr, originPort, cb) { + const opts = { originAddr, originPort }; + openChannel(this, 'x11', opts, cb); + return this; } - this.sigAlgo = sigAlgo; - this.blob = pkInfo.blob; -} -inherits(PKAuthContext, AuthContext); -PKAuthContext.prototype.accept = function() { - if (!this.signature) { - this._initialResponse = true; - this._stream.authPKOK(this.key.algo, this.key.data); - } else { - AuthContext.prototype.accept.call(this); + + forwardOut(boundAddr, boundPort, remoteAddr, remotePort, cb) { + const opts = { boundAddr, boundPort, remoteAddr, remotePort }; + openChannel(this, 'forwarded-tcpip', opts, cb); + return this; } -}; - -function HostbasedAuthContext(stream, username, service, method, pkInfo, cb) { - AuthContext.call(this, stream, username, service, method, cb); - - this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key }; - this.signature = pkInfo.signature; - var sigAlgo; - if (this.signature) { - // TODO: move key type checking logic to ssh2-streams - switch (pkInfo.keyAlgo) { - case 'ssh-rsa': - case 'ssh-dss': - sigAlgo = 'sha1'; - break; - case 'ssh-ed25519': - sigAlgo = null; - break; - case 'ecdsa-sha2-nistp256': - sigAlgo = 'sha256'; - break; - case 'ecdsa-sha2-nistp384': - sigAlgo = 'sha384'; - break; - case 'ecdsa-sha2-nistp521': - sigAlgo = 'sha512'; - break; - } + + openssh_forwardOutStreamLocal(socketPath, cb) { + const opts = { socketPath }; + openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb); + return this; } - this.sigAlgo = sigAlgo; - this.blob = pkInfo.blob; - this.localHostname = pkInfo.localHostname; - this.localUsername = pkInfo.localUsername; -} -inherits(HostbasedAuthContext, AuthContext); -function PwdAuthContext(stream, username, service, method, password, cb) { - AuthContext.call(this, stream, username, service, method, cb); + rekey(cb) { + let error; + + try { + this._protocol.rekey(); + } catch (ex) { + error = ex; + } + + // TODO: re-throw error if no callback? - this.password = password; + if (typeof cb === 'function') { + if (error) + process.nextTick(cb, error); + else + this.once('rekey', cb); + } + } } -inherits(PwdAuthContext, AuthContext); function openChannel(self, type, opts, cb) { - // ask the client to open a channel for some purpose - // (e.g. a forwarded TCP connection) - var localChan = nextChannel(self); - var initWindow = Channel.MAX_WINDOW; - var maxPacket = Channel.PACKET_SIZE; - var ret = true; - - if (localChan === false) - return cb(new Error('No free channels available')); + // Ask the client to open a channel for some purpose (e.g. a forwarded TCP + // connection) + const initWindow = MAX_WINDOW; + const maxPacket = PACKET_SIZE; if (typeof opts === 'function') { cb = opts; opts = {}; } - self._channels[localChan] = true; + const wrapper = (err, stream) => { + cb(err, stream); + }; + wrapper.type = type; - var sshstream = self._sshstream; - sshstream.once('CHANNEL_OPEN_CONFIRMATION:' + localChan, function(info) { - sshstream.removeAllListeners('CHANNEL_OPEN_FAILURE:' + localChan); + const localChan = self._chanMgr.add(wrapper); - var chaninfo = { - type: type, - incoming: { - id: localChan, - window: initWindow, - packetSize: maxPacket, - state: 'open' - }, - outgoing: { - id: info.sender, - window: info.window, - packetSize: info.packetSize, - state: 'open' - } - }; - cb(undefined, new Channel(chaninfo, self, { server: true })); - }).once('CHANNEL_OPEN_FAILURE:' + localChan, function(info) { - sshstream.removeAllListeners('CHANNEL_OPEN_CONFIRMATION:' + localChan); - - delete self._channels[localChan]; - - var err = new Error('(SSH) Channel open failure: ' + info.description); - err.reason = info.reason; - err.lang = info.lang; - cb(err); - }); - - if (type === 'forwarded-tcpip') - ret = sshstream.forwardedTcpip(localChan, initWindow, maxPacket, opts); - else if (type === 'x11') - ret = sshstream.x11(localChan, initWindow, maxPacket, opts); - else if (type === 'forwarded-streamlocal@openssh.com') { - ret = sshstream.openssh_forwardedStreamLocal(localChan, - initWindow, - maxPacket, - opts); + if (localChan === -1) { + cb(new Error('No free channels available')); + return; } - return ret; + switch (type) { + case 'forwarded-tcpip': + self._protocol.forwardedTcpip(localChan, initWindow, maxPacket, opts); + break; + case 'x11': + self._protocol.x11(localChan, initWindow, maxPacket, opts); + break; + case 'forwarded-streamlocal@openssh.com': + self._protocol.openssh_forwardedStreamLocal( + localChan, initWindow, maxPacket, opts + ); + break; + default: + throw new Error(`Unsupported channel type: ${type}`); + } } -function nextChannel(self) { - // get the next available channel number - - // fast path - if (self._curChan < MAX_CHANNEL) - return ++self._curChan; - - // slower lookup path - for (var i = 0, channels = self._channels; i < MAX_CHANNEL; ++i) - if (!channels[i]) - return i; - - return false; +function compareNumbers(a, b) { + return a - b; } - -Server.createServer = function(cfg, listener) { - return new Server(cfg, listener); -}; -Server.KEEPALIVE_INTERVAL = 1000; -Server.KEEPALIVE_CLIENT_INTERVAL = 15000; -Server.KEEPALIVE_CLIENT_COUNT_MAX = 3; - module.exports = Server; module.exports.IncomingClient = Client; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..f83eebf7 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,316 @@ +'use strict'; + +const { SFTP } = require('./protocol/SFTP.js'); + +const MAX_CHANNEL = 2 ** 32 - 1; + +function onChannelOpenFailure(self, recipient, info, cb) { + self._chanMgr.remove(recipient); + if (typeof cb !== 'function') + return; + + let err; + if (info instanceof Error) { + err = info; + } else if (typeof info === 'object' && info !== null) { + err = new Error(`(SSH) Channel open failure: ${info.description}`); + err.reason = info.reason; + } else { + err = new Error( + '(SSH) Channel open failure: server closed channel unexpectedly' + ); + err.reason = ''; + } + + cb(err); +} + +function onCHANNEL_CLOSE(self, recipient, channel, err, dead) { + if (typeof channel === 'function') { + // We got CHANNEL_CLOSE instead of CHANNEL_OPEN_FAILURE when + // requesting to open a channel + onChannelOpenFailure(self, recipient, err, channel); + return; + } + if (typeof channel !== 'object' + || channel === null + || channel.incoming.state === 'closed') { + return; + } + + channel.incoming.state = 'closed'; + + if (channel.readable) + channel.push(null); + if (channel.server) { + if (channel.stderr.writable) + channel.stderr.end(); + } else if (channel.stderr.readable) { + channel.stderr.push(null); + } + + if (channel.constructor !== SFTP + && (channel.outgoing.state === 'open' + || channel.outgoing.state === 'eof') + && !dead) { + channel.close(); + } + if (channel.outgoing.state === 'closing') + channel.outgoing.state = 'closed'; + + self._chanMgr.remove(recipient); + + const state = channel._writableState; + if (state && !state.ending && !state.finished && !dead) + channel.end(); + + // Take care of any outstanding channel requests + const chanCallbacks = channel._callbacks; + channel._callbacks = []; + for (let i = 0; i < chanCallbacks.length; ++i) + chanCallbacks[i](true); + + if (channel.server) { + if (!channel.readable) { + channel.emit('close'); + } else { + channel.once('end', () => { + channel.emit('close'); + }); + } + } else { + const exit = channel._exit; + // Align more with node child processes, where the close event gets + // the same arguments as the exit event + if (!channel.readable) { + if (exit.code === null) { + channel.emit('close', exit.code, exit.signal, exit.dump, + exit.desc, exit.lang); + } else { + channel.emit('close', exit.code); + } + } else { + channel.once('end', () => { + if (exit.code === null) { + channel.emit('close', exit.code, exit.signal, exit.dump, + exit.desc, exit.lang); + } else { + channel.emit('close', exit.code); + } + }); + } + + if (!channel.stderr.readable) { + channel.stderr.emit('close'); + } else { + channel.stderr.once('end', () => { + channel.stderr.emit('close'); + }); + } + } +} + +class ChannelManager { + constructor(client) { + this._client = client; + this._channels = {}; + this._cur = -1; + this._count = 0; + } + add(val) { + // Attempt to reserve an id + + let id; + // Optimized paths + if (this._cur < MAX_CHANNEL) { + id = ++this._cur; + } else if (this._count === 0) { + // Revert and reset back to fast path once we no longer have any channels + // open + this._cur = 0; + id = 0; + } else { + // Slower lookup path + + // This path is triggered we have opened at least MAX_CHANNEL channels + // while having at least one channel open at any given time, so we have + // to search for a free id. + const channels = this._channels; + for (let i = 0; i < MAX_CHANNEL; ++i) { + if (channels[i] === undefined) { + id = i; + break; + } + } + } + + if (id === undefined) + return -1; + + this._channels[id] = (val || true); + ++this._count; + + return id; + } + update(id, val) { + if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id)) + throw new Error(`Invalid channel id: ${id}`); + + if (val && this._channels[id]) + this._channels[id] = val; + } + get(id) { + if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id)) + throw new Error(`Invalid channel id: ${id}`); + + return this._channels[id]; + } + remove(id) { + if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id)) + throw new Error(`Invalid channel id: ${id}`); + + if (this._channels[id]) { + delete this._channels[id]; + if (this._count) + --this._count; + } + } + cleanup(err) { + const channels = this._channels; + this._channels = {}; + this._cur = -1; + this._count = 0; + + const chanIDs = Object.keys(channels); + const client = this._client; + for (let i = 0; i < chanIDs.length; ++i) { + const id = +chanIDs[i]; + onCHANNEL_CLOSE(client, id, channels[id], err, true); + } + } +} + +const isRegExp = (() => { + const toString = Object.prototype.toString; + return (val) => toString.call(val) === '[object RegExp]'; +})(); + +function generateAlgorithmList(algoList, defaultList, supportedList) { + if (Array.isArray(algoList) && algoList.length > 0) { + // Exact list + for (let i = 0; i < algoList.length; ++i) { + if (supportedList.indexOf(algoList[i]) === -1) + throw new Error(`Unsupported algorithm: ${algoList[i]}`); + } + return algoList; + } + + if (typeof algoList === 'object' && algoList !== null) { + // Operations based on the default list + const keys = Object.keys(algoList); + let list = defaultList; + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + let val = algoList[key]; + switch (key) { + case 'append': + if (!Array.isArray(val)) + val = [val]; + if (Array.isArray(val)) { + for (let j = 0; j < val.length; ++j) { + const append = val[j]; + if (typeof append === 'string') { + if (!append || list.indexOf(append) !== -1) + continue; + if (supportedList.indexOf(append) === -1) + throw new Error(`Unsupported algorithm: ${append}`); + if (list === defaultList) + list = list.slice(); + list.push(append); + } else if (isRegExp(append)) { + for (let k = 0; k < supportedList.length; ++k) { + const algo = list[k]; + if (append.test(algo)) { + if (list.indexOf(algo) !== -1) + continue; + if (list === defaultList) + list = list.slice(); + list.push(algo); + } + } + } + } + } + break; + case 'prepend': + if (!Array.isArray(val)) + val = [val]; + if (Array.isArray(val)) { + for (let j = val.length; j >= 0; --j) { + const prepend = val[j]; + if (typeof prepend === 'string') { + if (!prepend || list.indexOf(prepend) !== -1) + continue; + if (supportedList.indexOf(prepend) === -1) + throw new Error(`Unsupported algorithm: ${prepend}`); + if (list === defaultList) + list = list.slice(); + list.unshift(prepend); + } else if (isRegExp(prepend)) { + for (let k = supportedList.length; k >= 0; --k) { + const algo = list[k]; + if (prepend.test(algo)) { + if (list.indexOf(algo) !== -1) + continue; + if (list === defaultList) + list = list.slice(); + list.unshift(algo); + } + } + } + } + } + break; + case 'remove': + if (!Array.isArray(val)) + val = [val]; + if (Array.isArray(val)) { + for (let j = 0; j < val.length; ++j) { + const search = val[j]; + if (typeof search === 'string') { + if (!search) + continue; + const idx = list.indexOf(search); + if (idx === -1) + continue; + if (list === defaultList) + list = list.slice(); + list.splice(idx, 1); + } else if (isRegExp(search)) { + for (let k = 0; k < list.length; ++k) { + if (search.test(list[k])) { + if (list === defaultList) + list = list.slice(); + list.splice(k, 1); + --k; + } + } + } + } + } + break; + } + } + + return list; + } + + return defaultList; +} + +module.exports = { + ChannelManager, + generateAlgorithmList, + onChannelOpenFailure, + onCHANNEL_CLOSE, +}; diff --git a/package.json b/package.json index ebe3298b..1ed53c34 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,43 @@ -{ "name": "ssh2", - "version": "0.8.9", +{ + "name": "ssh2", + "version": "1.0.0-beta.0", "author": "Brian White ", "description": "SSH2 client and server modules written in pure JavaScript for node.js", "main": "./lib/client", - "engines": { "node": ">=5.2.0" }, + "engines": { + "node": ">=10.16.0" + }, "dependencies": { - "ssh2-streams": "~0.4.10" + "asn1": "^0.2.4", + "bcrypt-pbkdf": "^1.0.2" + }, + "optionalDependencies": { + "cpu-features": "0.0.2", + "nan": "^2.14.1" }, "scripts": { + "install": "node install.js", + "rebuild": "node install.js", "test": "node test/test.js" }, - "keywords": [ "ssh", "ssh2", "sftp", "secure", "shell", "exec", "remote", "client" ], - "licenses": [ { "type": "MIT", "url": "http://github.com/mscdex/ssh2/raw/master/LICENSE" } ], - "repository" : { "type": "git", "url": "http://github.com/mscdex/ssh2.git" } + "keywords": [ + "ssh", + "ssh2", + "sftp", + "secure", + "shell", + "exec", + "remote", + "client" + ], + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/mscdex/ssh2/raw/master/LICENSE" + } + ], + "repository": { + "type": "git", + "url": "http://github.com/mscdex/ssh2.git" + } } diff --git a/test/common.js b/test/common.js new file mode 100644 index 00000000..dd7f71d6 --- /dev/null +++ b/test/common.js @@ -0,0 +1,107 @@ +'use strict'; + +const assert = require('assert'); +const { inspect } = require('util'); + +const mustCallChecks = []; + +function noop() {} + +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + + const failed = mustCallChecks.filter((context) => { + if ('minimum' in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + failed.forEach((context) => { + console.error('Mismatched %s function calls. Expected %s, actual %d.', + context.name, + context.messageSegment, + context.actual); + console.error(context.stack.split('\n').slice(2).join('\n')); + }); + + if (failed.length) + process.exit(1); +} + +function mustCall(fn, exact) { + return _mustCallInner(fn, exact, 'exact'); +} + +function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, 'minimum'); +} + +function _mustCallInner(fn, criteria = 1, field) { + if (process._exiting) + throw new Error('Cannot use common.mustCall*() in process exit handler'); + + if (typeof fn === 'number') { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + if (typeof criteria !== 'number') + throw new TypeError(`Invalid ${field} value: ${criteria}`); + + const context = { + [field]: criteria, + actual: 0, + stack: inspect(new Error()), + name: fn.name || '' + }; + + // Add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) + process.on('exit', runCallChecks); + + mustCallChecks.push(context); + + function wrapped(...args) { + ++context.actual; + return fn.call(this, ...args); + } + wrapped.origFn = fn; + + return wrapped; +} + +function getCallSite(top) { + const originalStackFormatter = Error.prepareStackTrace; + Error.prepareStackTrace = (err, stack) => + `${stack[0].getFileName()}:${stack[0].getLineNumber()}`; + const err = new Error(); + Error.captureStackTrace(err, top); + // With the V8 Error API, the stack is not formatted until it is accessed + err.stack; + Error.prepareStackTrace = originalStackFormatter; + return err.stack; +} + +function mustNotCall(msg) { + const callSite = getCallSite(mustNotCall); + return function mustNotCall(...args) { + args = args.map(inspect).join(', '); + const argsInfo = (args.length > 0 + ? `\ncalled with arguments: ${args}` + : ''); + assert.fail( + `${msg || 'function should not have been called'} at ${callSite}` + + argsInfo); + }; +} + +module.exports = { + mustCall, + mustCallAtLeast, + mustNotCall, +}; diff --git a/test/fixtures/keyParser/openssh_new_dsa b/test/fixtures/keyParser/openssh_new_dsa new file mode 100644 index 00000000..a8722abc --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa @@ -0,0 +1,21 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsgAAAAdzc2gtZH +NzAAAAgQDg+DsMAituSW/NJpWVy2w7xN6Uu/IfCqpy38CFBW+mBnOX7OzPulI+1uZxXRLy +UKiQDAegXCqSHMCo5ACZhw2BRwq74J4VA5fOFGdwcacTQo1zKDF64wvyVSgQE/E2PSFLKu +NHHtRFnjvq6WrgTQsL9aif2FBWS5q0MGahzXhNkQAAABUAn1ASRSRcIVsWqrrZubFQq4pU +OlMAAACBALcKIRLTtYG5+N/vzEULdsXSGToDRth6X5Yjb7c0UotAmy9VGrnmN5IO+//1em +2USHeSoO+5shRq92zdggdQwNaXXzU301huIETztfRwGHOfUGZbzJmIqdzLhdziFhneAzaN +zVeUFyIqvWL1Q89WgC2Uh3DY/lK/gIhRK7WD0cDAAAAAgC882WUEEig48DVyjbNi1xf8rG +svyypMHSs2rj6pja2Upfm+C5AKKU387x8Vj/Kz291ROIl7h/AhmKOlwdxwPZOG5ffDygaW +Tlo4/JagwP9HmTsK1Tyd1chuyMk9cNLdgWFsCGGHY2RcEwccq9panvvtKp57HqDaT1W7AS +g2spT9AAAB8G4oDW5uKA1uAAAAB3NzaC1kc3MAAACBAOD4OwwCK25Jb80mlZXLbDvE3pS7 +8h8KqnLfwIUFb6YGc5fs7M+6Uj7W5nFdEvJQqJAMB6BcKpIcwKjkAJmHDYFHCrvgnhUDl8 +4UZ3BxpxNCjXMoMXrjC/JVKBAT8TY9IUsq40ce1EWeO+rpauBNCwv1qJ/YUFZLmrQwZqHN +eE2RAAAAFQCfUBJFJFwhWxaqutm5sVCrilQ6UwAAAIEAtwohEtO1gbn43+/MRQt2xdIZOg +NG2HpfliNvtzRSi0CbL1UaueY3kg77//V6bZRId5Kg77myFGr3bN2CB1DA1pdfNTfTWG4g +RPO19HAYc59QZlvMmYip3MuF3OIWGd4DNo3NV5QXIiq9YvVDz1aALZSHcNj+Ur+AiFErtY +PRwMAAAACALzzZZQQSKDjwNXKNs2LXF/ysay/LKkwdKzauPqmNrZSl+b4LkAopTfzvHxWP +8rPb3VE4iXuH8CGYo6XB3HA9k4bl98PKBpZOWjj8lqDA/0eZOwrVPJ3VyG7IyT1w0t2BYW +wIYYdjZFwTBxyr2lqe++0qnnseoNpPVbsBKDaylP0AAAAVAIoWASGAfFqckLwvtPRNCzow +TTl1AAAAEm5ldyBvcGVuc3NoIGZvcm1hdAECAwQFBgc= +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_dsa.pub b/test/fixtures/keyParser/openssh_new_dsa.pub new file mode 100644 index 00000000..d5b662dd --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAOD4OwwCK25Jb80mlZXLbDvE3pS78h8KqnLfwIUFb6YGc5fs7M+6Uj7W5nFdEvJQqJAMB6BcKpIcwKjkAJmHDYFHCrvgnhUDl84UZ3BxpxNCjXMoMXrjC/JVKBAT8TY9IUsq40ce1EWeO+rpauBNCwv1qJ/YUFZLmrQwZqHNeE2RAAAAFQCfUBJFJFwhWxaqutm5sVCrilQ6UwAAAIEAtwohEtO1gbn43+/MRQt2xdIZOgNG2HpfliNvtzRSi0CbL1UaueY3kg77//V6bZRId5Kg77myFGr3bN2CB1DA1pdfNTfTWG4gRPO19HAYc59QZlvMmYip3MuF3OIWGd4DNo3NV5QXIiq9YvVDz1aALZSHcNj+Ur+AiFErtYPRwMAAAACALzzZZQQSKDjwNXKNs2LXF/ysay/LKkwdKzauPqmNrZSl+b4LkAopTfzvHxWP8rPb3VE4iXuH8CGYo6XB3HA9k4bl98PKBpZOWjj8lqDA/0eZOwrVPJ3VyG7IyT1w0t2BYWwIYYdjZFwTBxyr2lqe++0qnnseoNpPVbsBKDaylP0= new openssh format diff --git a/test/fixtures/keyParser/openssh_new_dsa.pub.result b/test/fixtures/keyParser/openssh_new_dsa.pub.result new file mode 100644 index 00000000..7b8d66ea --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-dss", + "comment": "new openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtzCCASwGByqGSM44BAEwggEfAoGBAOD4OwwCK25Jb80mlZXLbDvE3pS78h8K\nqnLfwIUFb6YGc5fs7M+6Uj7W5nFdEvJQqJAMB6BcKpIcwKjkAJmHDYFHCrvgnhUD\nl84UZ3BxpxNCjXMoMXrjC/JVKBAT8TY9IUsq40ce1EWeO+rpauBNCwv1qJ/YUFZL\nmrQwZqHNeE2RAhUAn1ASRSRcIVsWqrrZubFQq4pUOlMCgYEAtwohEtO1gbn43+/M\nRQt2xdIZOgNG2HpfliNvtzRSi0CbL1UaueY3kg77//V6bZRId5Kg77myFGr3bN2C\nB1DA1pdfNTfTWG4gRPO19HAYc59QZlvMmYip3MuF3OIWGd4DNo3NV5QXIiq9YvVD\nz1aALZSHcNj+Ur+AiFErtYPRwMADgYQAAoGALzzZZQQSKDjwNXKNs2LXF/ysay/L\nKkwdKzauPqmNrZSl+b4LkAopTfzvHxWP8rPb3VE4iXuH8CGYo6XB3HA9k4bl98PK\nBpZOWjj8lqDA/0eZOwrVPJ3VyG7IyT1w0t2BYWwIYYdjZFwTBxyr2lqe++0qnnse\noNpPVbsBKDaylP0=\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAOD4OwwCK25Jb80mlZXLbDvE3pS78h8KqnLfwIUFb6YGc5fs7M+6Uj7W5nFdEvJQqJAMB6BcKpIcwKjkAJmHDYFHCrvgnhUDl84UZ3BxpxNCjXMoMXrjC/JVKBAT8TY9IUsq40ce1EWeO+rpauBNCwv1qJ/YUFZLmrQwZqHNeE2RAAAAFQCfUBJFJFwhWxaqutm5sVCrilQ6UwAAAIEAtwohEtO1gbn43+/MRQt2xdIZOgNG2HpfliNvtzRSi0CbL1UaueY3kg77//V6bZRId5Kg77myFGr3bN2CB1DA1pdfNTfTWG4gRPO19HAYc59QZlvMmYip3MuF3OIWGd4DNo3NV5QXIiq9YvVDz1aALZSHcNj+Ur+AiFErtYPRwMAAAACALzzZZQQSKDjwNXKNs2LXF/ysay/LKkwdKzauPqmNrZSl+b4LkAopTfzvHxWP8rPb3VE4iXuH8CGYo6XB3HA9k4bl98PKBpZOWjj8lqDA/0eZOwrVPJ3VyG7IyT1w0t2BYWwIYYdjZFwTBxyr2lqe++0qnnseoNpPVbsBKDaylP0=", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_new_dsa.result b/test/fixtures/keyParser/openssh_new_dsa.result new file mode 100644 index 00000000..9ab799f0 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa.result @@ -0,0 +1,7 @@ +[{ + "type": "ssh-dss", + "comment": "new openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtzCCASwGByqGSM44BAEwggEfAoGBAOD4OwwCK25Jb80mlZXLbDvE3pS78h8K\nqnLfwIUFb6YGc5fs7M+6Uj7W5nFdEvJQqJAMB6BcKpIcwKjkAJmHDYFHCrvgnhUD\nl84UZ3BxpxNCjXMoMXrjC/JVKBAT8TY9IUsq40ce1EWeO+rpauBNCwv1qJ/YUFZL\nmrQwZqHNeE2RAhUAn1ASRSRcIVsWqrrZubFQq4pUOlMCgYEAtwohEtO1gbn43+/M\nRQt2xdIZOgNG2HpfliNvtzRSi0CbL1UaueY3kg77//V6bZRId5Kg77myFGr3bN2C\nB1DA1pdfNTfTWG4gRPO19HAYc59QZlvMmYip3MuF3OIWGd4DNo3NV5QXIiq9YvVD\nz1aALZSHcNj+Ur+AiFErtYPRwMADgYQAAoGALzzZZQQSKDjwNXKNs2LXF/ysay/L\nKkwdKzauPqmNrZSl+b4LkAopTfzvHxWP8rPb3VE4iXuH8CGYo6XB3HA9k4bl98PK\nBpZOWjj8lqDA/0eZOwrVPJ3VyG7IyT1w0t2BYWwIYYdjZFwTBxyr2lqe++0qnnse\noNpPVbsBKDaylP0=\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAOD4OwwCK25Jb80mlZXLbDvE3pS78h8KqnLfwIUFb6YGc5fs7M+6Uj7W5nFdEvJQqJAMB6BcKpIcwKjkAJmHDYFHCrvgnhUDl84UZ3BxpxNCjXMoMXrjC/JVKBAT8TY9IUsq40ce1EWeO+rpauBNCwv1qJ/YUFZLmrQwZqHNeE2RAAAAFQCfUBJFJFwhWxaqutm5sVCrilQ6UwAAAIEAtwohEtO1gbn43+/MRQt2xdIZOgNG2HpfliNvtzRSi0CbL1UaueY3kg77//V6bZRId5Kg77myFGr3bN2CB1DA1pdfNTfTWG4gRPO19HAYc59QZlvMmYip3MuF3OIWGd4DNo3NV5QXIiq9YvVDz1aALZSHcNj+Ur+AiFErtYPRwMAAAACALzzZZQQSKDjwNXKNs2LXF/ysay/LKkwdKzauPqmNrZSl+b4LkAopTfzvHxWP8rPb3VE4iXuH8CGYo6XB3HA9k4bl98PKBpZOWjj8lqDA/0eZOwrVPJ3VyG7IyT1w0t2BYWwIYYdjZFwTBxyr2lqe++0qnnseoNpPVbsBKDaylP0=", + "private": "-----BEGIN DSA PRIVATE KEY-----\nMIIBvAIBAAKBgQDg+DsMAituSW/NJpWVy2w7xN6Uu/IfCqpy38CFBW+mBnOX7OzP\nulI+1uZxXRLyUKiQDAegXCqSHMCo5ACZhw2BRwq74J4VA5fOFGdwcacTQo1zKDF6\n4wvyVSgQE/E2PSFLKuNHHtRFnjvq6WrgTQsL9aif2FBWS5q0MGahzXhNkQIVAJ9Q\nEkUkXCFbFqq62bmxUKuKVDpTAoGBALcKIRLTtYG5+N/vzEULdsXSGToDRth6X5Yj\nb7c0UotAmy9VGrnmN5IO+//1em2USHeSoO+5shRq92zdggdQwNaXXzU301huIETz\ntfRwGHOfUGZbzJmIqdzLhdziFhneAzaNzVeUFyIqvWL1Q89WgC2Uh3DY/lK/gIhR\nK7WD0cDAAoGALzzZZQQSKDjwNXKNs2LXF/ysay/LKkwdKzauPqmNrZSl+b4LkAop\nTfzvHxWP8rPb3VE4iXuH8CGYo6XB3HA9k4bl98PKBpZOWjj8lqDA/0eZOwrVPJ3V\nyG7IyT1w0t2BYWwIYYdjZFwTBxyr2lqe++0qnnseoNpPVbsBKDaylP0CFQCKFgEh\ngHxanJC8L7T0TQs6ME05dQ==\n-----END DSA PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_dsa_enc b/test/fixtures/keyParser/openssh_new_dsa_enc new file mode 100644 index 00000000..392f2143 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa_enc @@ -0,0 +1,22 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBgJ5gXYn +/2IFE2+CrAxYR8AAAAEAAAAAEAAAGxAAAAB3NzaC1kc3MAAACBAPKhVnFGWb0KLibdYnJz +0RwFy/mt98KMIdByHKQWRm9UjoVJk1ypuQpnj+bqFnxCzCFSU9OUj0/Xe0Wuk+kF2BtMO0 +w+ZYfVHCqEaaIJ1D/iLqi8aBbYs552l9+P0DsFUlTE0D/AvKTQ2PsztFq7wHUTQVmnj4vy +k1bw7ske+ImLAAAAFQDnXsk6hdenasLyE8ylLHSE+0XR3QAAAIBsMerhmMT0/416hJV/pr +s7crOX0e0gF8C7kar/ILj5WULX7k143+4lgluoogrPXbd5fXgOnqdQawow8a/IjU62Sz6n +/qfHLJtQ2sJOK2Vkj5NF2UCcRHrewqJw9nDCS7yYh3c+gUfIBcIRkEJK6eRJfrZuaq0Yue +nUa9AuFwnjPAAAAIBwjDUjp9jaJu46eobNK8CWJL/Noi2fXTtFZFgUFRwkr/FXLLsOckQT +mYxaWcxP4NwuvMyI25tOueM0RvAIR7J3Afc5pbuCx6dIgiOf2gRClQU5OlqhrnMW2BQXlR +hBKBNMp5LjM5t46KTBkjh/30//s4Kimrp/C2XBGgEuRdgyqQAAAgDIGP0oYyi7sTk0HdU9 +uWZLaDhHpW4Z8xTzfgUDbxoTYQ2igO90O32vSqW/cC2QKWTFuPCFnsCerHAIGzX/eyxlCQ +VyNa7VrhbNjIKAHBF3XMcRVRbW2SdYq8tHSkeZHr5EuO5dRfJ7wsR8flkPb4O4viNlIbvF +Ake8dsZEOhcnVNiv+NMR9mTq8l91wR60tr3XiWzCMkEYrJiWOfQuZSvzYi7dUmFxQuEZfQ +vIPkZD3L6XdaAz/r6YAONFAbtUMAOaUxOGV9puSsunSosAvmi+NcJ9iUM2FpAu561gp+Tv +RRcgXHxLGuzTNASiMaTN3M+HenqUh3RWmWauL5wSR7DbrH7Vq47YTnVjtg8xcZnMCfOx2D +Wz775hD6uyLwbkxKMaNMf8p4sOcXsSpHNqKmfkUxQBpNRp6Vg5W+AVaAkyXQng2LRt6txJ +Xv5zBiSFdsobkrWko/ONfGKfG+zVP+LIVcghLpp71GZQX6Ci02vB55pvk8k0G91H3INn/c +t6Q5zY5pK9VZwxjZ29psm7V+FdeD1g8VQ1Rp9muq6zDXHKKyqkBK/oGCM9UhBHFjki0gBR +v6LY/iXsz/eG14svhLjM5zYFSX7jUOI9b/PnhhL7Mos4wguHN2EjfGWuC07PkkqDPoqSwn +cC91OKhub6yqZsqvBz9BcV+2FxVNPNKzRdzA== +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_dsa_enc.pub b/test/fixtures/keyParser/openssh_new_dsa_enc.pub new file mode 100644 index 00000000..c2b1190f --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa_enc.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAPKhVnFGWb0KLibdYnJz0RwFy/mt98KMIdByHKQWRm9UjoVJk1ypuQpnj+bqFnxCzCFSU9OUj0/Xe0Wuk+kF2BtMO0w+ZYfVHCqEaaIJ1D/iLqi8aBbYs552l9+P0DsFUlTE0D/AvKTQ2PsztFq7wHUTQVmnj4vyk1bw7ske+ImLAAAAFQDnXsk6hdenasLyE8ylLHSE+0XR3QAAAIBsMerhmMT0/416hJV/prs7crOX0e0gF8C7kar/ILj5WULX7k143+4lgluoogrPXbd5fXgOnqdQawow8a/IjU62Sz6n/qfHLJtQ2sJOK2Vkj5NF2UCcRHrewqJw9nDCS7yYh3c+gUfIBcIRkEJK6eRJfrZuaq0YuenUa9AuFwnjPAAAAIBwjDUjp9jaJu46eobNK8CWJL/Noi2fXTtFZFgUFRwkr/FXLLsOckQTmYxaWcxP4NwuvMyI25tOueM0RvAIR7J3Afc5pbuCx6dIgiOf2gRClQU5OlqhrnMW2BQXlRhBKBNMp5LjM5t46KTBkjh/30//s4Kimrp/C2XBGgEuRdgyqQ== diff --git a/test/fixtures/keyParser/openssh_new_dsa_enc.pub.result b/test/fixtures/keyParser/openssh_new_dsa_enc.pub.result new file mode 100644 index 00000000..d15133a0 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa_enc.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-dss", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtjCCASsGByqGSM44BAEwggEeAoGBAPKhVnFGWb0KLibdYnJz0RwFy/mt98KM\nIdByHKQWRm9UjoVJk1ypuQpnj+bqFnxCzCFSU9OUj0/Xe0Wuk+kF2BtMO0w+ZYfV\nHCqEaaIJ1D/iLqi8aBbYs552l9+P0DsFUlTE0D/AvKTQ2PsztFq7wHUTQVmnj4vy\nk1bw7ske+ImLAhUA517JOoXXp2rC8hPMpSx0hPtF0d0CgYBsMerhmMT0/416hJV/\nprs7crOX0e0gF8C7kar/ILj5WULX7k143+4lgluoogrPXbd5fXgOnqdQawow8a/I\njU62Sz6n/qfHLJtQ2sJOK2Vkj5NF2UCcRHrewqJw9nDCS7yYh3c+gUfIBcIRkEJK\n6eRJfrZuaq0YuenUa9AuFwnjPAOBhAACgYBwjDUjp9jaJu46eobNK8CWJL/Noi2f\nXTtFZFgUFRwkr/FXLLsOckQTmYxaWcxP4NwuvMyI25tOueM0RvAIR7J3Afc5pbuC\nx6dIgiOf2gRClQU5OlqhrnMW2BQXlRhBKBNMp5LjM5t46KTBkjh/30//s4Kimrp/\nC2XBGgEuRdgyqQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAPKhVnFGWb0KLibdYnJz0RwFy/mt98KMIdByHKQWRm9UjoVJk1ypuQpnj+bqFnxCzCFSU9OUj0/Xe0Wuk+kF2BtMO0w+ZYfVHCqEaaIJ1D/iLqi8aBbYs552l9+P0DsFUlTE0D/AvKTQ2PsztFq7wHUTQVmnj4vyk1bw7ske+ImLAAAAFQDnXsk6hdenasLyE8ylLHSE+0XR3QAAAIBsMerhmMT0/416hJV/prs7crOX0e0gF8C7kar/ILj5WULX7k143+4lgluoogrPXbd5fXgOnqdQawow8a/IjU62Sz6n/qfHLJtQ2sJOK2Vkj5NF2UCcRHrewqJw9nDCS7yYh3c+gUfIBcIRkEJK6eRJfrZuaq0YuenUa9AuFwnjPAAAAIBwjDUjp9jaJu46eobNK8CWJL/Noi2fXTtFZFgUFRwkr/FXLLsOckQTmYxaWcxP4NwuvMyI25tOueM0RvAIR7J3Afc5pbuCx6dIgiOf2gRClQU5OlqhrnMW2BQXlRhBKBNMp5LjM5t46KTBkjh/30//s4Kimrp/C2XBGgEuRdgyqQ==", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_new_dsa_enc.result b/test/fixtures/keyParser/openssh_new_dsa_enc.result new file mode 100644 index 00000000..b0c428e2 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa_enc.result @@ -0,0 +1,7 @@ +[{ + "type": "ssh-dss", + "comment": "new openssh format encrypted", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtjCCASsGByqGSM44BAEwggEeAoGBAPKhVnFGWb0KLibdYnJz0RwFy/mt98KM\nIdByHKQWRm9UjoVJk1ypuQpnj+bqFnxCzCFSU9OUj0/Xe0Wuk+kF2BtMO0w+ZYfV\nHCqEaaIJ1D/iLqi8aBbYs552l9+P0DsFUlTE0D/AvKTQ2PsztFq7wHUTQVmnj4vy\nk1bw7ske+ImLAhUA517JOoXXp2rC8hPMpSx0hPtF0d0CgYBsMerhmMT0/416hJV/\nprs7crOX0e0gF8C7kar/ILj5WULX7k143+4lgluoogrPXbd5fXgOnqdQawow8a/I\njU62Sz6n/qfHLJtQ2sJOK2Vkj5NF2UCcRHrewqJw9nDCS7yYh3c+gUfIBcIRkEJK\n6eRJfrZuaq0YuenUa9AuFwnjPAOBhAACgYBwjDUjp9jaJu46eobNK8CWJL/Noi2f\nXTtFZFgUFRwkr/FXLLsOckQTmYxaWcxP4NwuvMyI25tOueM0RvAIR7J3Afc5pbuC\nx6dIgiOf2gRClQU5OlqhrnMW2BQXlRhBKBNMp5LjM5t46KTBkjh/30//s4Kimrp/\nC2XBGgEuRdgyqQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAPKhVnFGWb0KLibdYnJz0RwFy/mt98KMIdByHKQWRm9UjoVJk1ypuQpnj+bqFnxCzCFSU9OUj0/Xe0Wuk+kF2BtMO0w+ZYfVHCqEaaIJ1D/iLqi8aBbYs552l9+P0DsFUlTE0D/AvKTQ2PsztFq7wHUTQVmnj4vyk1bw7ske+ImLAAAAFQDnXsk6hdenasLyE8ylLHSE+0XR3QAAAIBsMerhmMT0/416hJV/prs7crOX0e0gF8C7kar/ILj5WULX7k143+4lgluoogrPXbd5fXgOnqdQawow8a/IjU62Sz6n/qfHLJtQ2sJOK2Vkj5NF2UCcRHrewqJw9nDCS7yYh3c+gUfIBcIRkEJK6eRJfrZuaq0YuenUa9AuFwnjPAAAAIBwjDUjp9jaJu46eobNK8CWJL/Noi2fXTtFZFgUFRwkr/FXLLsOckQTmYxaWcxP4NwuvMyI25tOueM0RvAIR7J3Afc5pbuCx6dIgiOf2gRClQU5OlqhrnMW2BQXlRhBKBNMp5LjM5t46KTBkjh/30//s4Kimrp/C2XBGgEuRdgyqQ==", + "private": "-----BEGIN DSA PRIVATE KEY-----\nMIIBugIBAAKBgQDyoVZxRlm9Ci4m3WJyc9EcBcv5rffCjCHQchykFkZvVI6FSZNc\nqbkKZ4/m6hZ8QswhUlPTlI9P13tFrpPpBdgbTDtMPmWH1RwqhGmiCdQ/4i6ovGgW\n2LOedpffj9A7BVJUxNA/wLyk0Nj7M7Rau8B1E0FZp4+L8pNW8O7JHviJiwIVAOde\nyTqF16dqwvITzKUsdIT7RdHdAoGAbDHq4ZjE9P+NeoSVf6a7O3Kzl9HtIBfAu5Gq\n/yC4+VlC1+5NeN/uJYJbqKIKz123eX14Dp6nUGsKMPGvyI1Otks+p/6nxyybUNrC\nTitlZI+TRdlAnER63sKicPZwwku8mId3PoFHyAXCEZBCSunkSX62bmqtGLnp1GvQ\nLhcJ4zwCgYBwjDUjp9jaJu46eobNK8CWJL/Noi2fXTtFZFgUFRwkr/FXLLsOckQT\nmYxaWcxP4NwuvMyI25tOueM0RvAIR7J3Afc5pbuCx6dIgiOf2gRClQU5OlqhrnMW\n2BQXlRhBKBNMp5LjM5t46KTBkjh/30//s4Kimrp/C2XBGgEuRdgyqQIUSNLlRVPv\nMC3Q3P3ajY1DdZvi9z8=\n-----END DSA PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_dsa_enc_gcm b/test/fixtures/keyParser/openssh_new_dsa_enc_gcm new file mode 100644 index 00000000..38566b47 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa_enc_gcm @@ -0,0 +1,23 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAAFmFlczEyOC1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA +AAGAAAABD01pNY1+DTCAHuI6mcjB0YAAAAEAAAAAEAAAGyAAAAB3NzaC1kc3MAAACBAPLA +N0jFExSJiCvw7p2W2v5tqvXIG4YwCglrl2wnGOMBGmfaeIcxZErzW00hOxq+NvDIlK43kJ +iP98Vz0XTHIW6DpkE9DcC5GGA6nDZn9L+BSrBL8NhuBlz2ekgWOTCqnDC7Il/iyUCMi79s +ZPOEg/bMExWJlB5AosJr7v5twVftAAAAFQC5AGsioHKAc2Cd2QwKLUZSmDZAVwAAAIBxYf +EThMIXPQkSer3snKJfDz0uvc1y/6htsjXLk93TAAi3LSD2dGqYs5s0WfzO4RnFso0EovrL +OnIbqU1XApr6CPKAVX2REsXFWWF3VixEHIEF1Q9gIvHdYgAxSxtwYvOPpAwDmaPxWeV5/q +MsMu2RSKkK6f08J0vsESnKU4nmnwAAAIEAxH8NZyntzihIAHnx1Lbo7h1sPi4RhcpKK5pS +UiaKoWxkjseqUsyWENt6DTByIdGhBNrOp9/vw2R5CSUkxuI0TlI8bj3qhq/B3bspx1GWjL +qLfKbeVi4un8CrooRRq2g8+nYLu2EWbF/56pEEzws6DptlDJQi7GdZG8Q0tuyfXxsAAAIA +PDupGK4wMtROtFZqo7vduzkHJuDrE/tAwGqiD2pKMova7WaKM0EUznwcl3gtmhHvFeY+NJ +3Uc9sQcX/9n3y6NAYsC+eZeqe7Sy2GWVyqxOUJHpZqfsKYJidG61TBgKgx+JXAeidYdz4L +4cEapwwocOptbY3ZRFmszekq5xPomnkP9DeSQG6l4eYSv7OpeAHlFj2KCmJMVEZDOl6RyJ +KCqOpfEJIIVoCmna/hQdd9ptLVFmbX/VShgLjvUwfBggJtZNPb5jx+PMy+I0ylywaCIG5K +JQAqust6dzFBx3mBoO4kZPBHlb8XwQ4HYLYph0Ur/lINsHrpLxgmtEw7zzs73Nshl6go2V +uvBtcZ5ywAMk+8CLP5ZgpiGBxlMtFGowp/5zuJxRpc9FgdfxnnVWDyzcQ/YvX9lwzb6cNz +bXeLPsKjOSLPV7G/RFIiuCAOa97ZCM8Ho4FhdNYOGilmjuxV7FJiTc7KP2r+Wh3oxsV7AB +Q6Thj06b2mX3iE4hqLaMKIVE1zs22nMlUtFJv8YY1ZWBihUVlnR9vWgIH7ODoZOwNWBlLd +Qfyfi8w3KgJWj5oVNAM7WniNFQjfNxEbrPklfYg93deVE/LhPghs9I7fsIeHY/p8GtsO/S +amTcjkYi6pUuT8m7IeFYQ8cWvGnbaYz6/9+ni+0aoUL93GKHQw1+mBUVuswVZXBF1WVCf+ +LMgZ +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub b/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub new file mode 100644 index 00000000..d9eb1a5a --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAPLAN0jFExSJiCvw7p2W2v5tqvXIG4YwCglrl2wnGOMBGmfaeIcxZErzW00hOxq+NvDIlK43kJiP98Vz0XTHIW6DpkE9DcC5GGA6nDZn9L+BSrBL8NhuBlz2ekgWOTCqnDC7Il/iyUCMi79sZPOEg/bMExWJlB5AosJr7v5twVftAAAAFQC5AGsioHKAc2Cd2QwKLUZSmDZAVwAAAIBxYfEThMIXPQkSer3snKJfDz0uvc1y/6htsjXLk93TAAi3LSD2dGqYs5s0WfzO4RnFso0EovrLOnIbqU1XApr6CPKAVX2REsXFWWF3VixEHIEF1Q9gIvHdYgAxSxtwYvOPpAwDmaPxWeV5/qMsMu2RSKkK6f08J0vsESnKU4nmnwAAAIEAxH8NZyntzihIAHnx1Lbo7h1sPi4RhcpKK5pSUiaKoWxkjseqUsyWENt6DTByIdGhBNrOp9/vw2R5CSUkxuI0TlI8bj3qhq/B3bspx1GWjLqLfKbeVi4un8CrooRRq2g8+nYLu2EWbF/56pEEzws6DptlDJQi7GdZG8Q0tuyfXxs= diff --git a/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub.result b/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub.result new file mode 100644 index 00000000..6a918a80 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-dss", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtzCCASsGByqGSM44BAEwggEeAoGBAPLAN0jFExSJiCvw7p2W2v5tqvXIG4Yw\nCglrl2wnGOMBGmfaeIcxZErzW00hOxq+NvDIlK43kJiP98Vz0XTHIW6DpkE9DcC5\nGGA6nDZn9L+BSrBL8NhuBlz2ekgWOTCqnDC7Il/iyUCMi79sZPOEg/bMExWJlB5A\nosJr7v5twVftAhUAuQBrIqBygHNgndkMCi1GUpg2QFcCgYBxYfEThMIXPQkSer3s\nnKJfDz0uvc1y/6htsjXLk93TAAi3LSD2dGqYs5s0WfzO4RnFso0EovrLOnIbqU1X\nApr6CPKAVX2REsXFWWF3VixEHIEF1Q9gIvHdYgAxSxtwYvOPpAwDmaPxWeV5/qMs\nMu2RSKkK6f08J0vsESnKU4nmnwOBhQACgYEAxH8NZyntzihIAHnx1Lbo7h1sPi4R\nhcpKK5pSUiaKoWxkjseqUsyWENt6DTByIdGhBNrOp9/vw2R5CSUkxuI0TlI8bj3q\nhq/B3bspx1GWjLqLfKbeVi4un8CrooRRq2g8+nYLu2EWbF/56pEEzws6DptlDJQi\n7GdZG8Q0tuyfXxs=\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAPLAN0jFExSJiCvw7p2W2v5tqvXIG4YwCglrl2wnGOMBGmfaeIcxZErzW00hOxq+NvDIlK43kJiP98Vz0XTHIW6DpkE9DcC5GGA6nDZn9L+BSrBL8NhuBlz2ekgWOTCqnDC7Il/iyUCMi79sZPOEg/bMExWJlB5AosJr7v5twVftAAAAFQC5AGsioHKAc2Cd2QwKLUZSmDZAVwAAAIBxYfEThMIXPQkSer3snKJfDz0uvc1y/6htsjXLk93TAAi3LSD2dGqYs5s0WfzO4RnFso0EovrLOnIbqU1XApr6CPKAVX2REsXFWWF3VixEHIEF1Q9gIvHdYgAxSxtwYvOPpAwDmaPxWeV5/qMsMu2RSKkK6f08J0vsESnKU4nmnwAAAIEAxH8NZyntzihIAHnx1Lbo7h1sPi4RhcpKK5pSUiaKoWxkjseqUsyWENt6DTByIdGhBNrOp9/vw2R5CSUkxuI0TlI8bj3qhq/B3bspx1GWjLqLfKbeVi4un8CrooRRq2g8+nYLu2EWbF/56pEEzws6DptlDJQi7GdZG8Q0tuyfXxs=", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.result b/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.result new file mode 100644 index 00000000..17a93639 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.result @@ -0,0 +1,7 @@ +[{ + "type": "ssh-dss", + "comment": "new openssh format encrypted gcm", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtzCCASsGByqGSM44BAEwggEeAoGBAPLAN0jFExSJiCvw7p2W2v5tqvXIG4Yw\nCglrl2wnGOMBGmfaeIcxZErzW00hOxq+NvDIlK43kJiP98Vz0XTHIW6DpkE9DcC5\nGGA6nDZn9L+BSrBL8NhuBlz2ekgWOTCqnDC7Il/iyUCMi79sZPOEg/bMExWJlB5A\nosJr7v5twVftAhUAuQBrIqBygHNgndkMCi1GUpg2QFcCgYBxYfEThMIXPQkSer3s\nnKJfDz0uvc1y/6htsjXLk93TAAi3LSD2dGqYs5s0WfzO4RnFso0EovrLOnIbqU1X\nApr6CPKAVX2REsXFWWF3VixEHIEF1Q9gIvHdYgAxSxtwYvOPpAwDmaPxWeV5/qMs\nMu2RSKkK6f08J0vsESnKU4nmnwOBhQACgYEAxH8NZyntzihIAHnx1Lbo7h1sPi4R\nhcpKK5pSUiaKoWxkjseqUsyWENt6DTByIdGhBNrOp9/vw2R5CSUkxuI0TlI8bj3q\nhq/B3bspx1GWjLqLfKbeVi4un8CrooRRq2g8+nYLu2EWbF/56pEEzws6DptlDJQi\n7GdZG8Q0tuyfXxs=\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAPLAN0jFExSJiCvw7p2W2v5tqvXIG4YwCglrl2wnGOMBGmfaeIcxZErzW00hOxq+NvDIlK43kJiP98Vz0XTHIW6DpkE9DcC5GGA6nDZn9L+BSrBL8NhuBlz2ekgWOTCqnDC7Il/iyUCMi79sZPOEg/bMExWJlB5AosJr7v5twVftAAAAFQC5AGsioHKAc2Cd2QwKLUZSmDZAVwAAAIBxYfEThMIXPQkSer3snKJfDz0uvc1y/6htsjXLk93TAAi3LSD2dGqYs5s0WfzO4RnFso0EovrLOnIbqU1XApr6CPKAVX2REsXFWWF3VixEHIEF1Q9gIvHdYgAxSxtwYvOPpAwDmaPxWeV5/qMsMu2RSKkK6f08J0vsESnKU4nmnwAAAIEAxH8NZyntzihIAHnx1Lbo7h1sPi4RhcpKK5pSUiaKoWxkjseqUsyWENt6DTByIdGhBNrOp9/vw2R5CSUkxuI0TlI8bj3qhq/B3bspx1GWjLqLfKbeVi4un8CrooRRq2g8+nYLu2EWbF/56pEEzws6DptlDJQi7GdZG8Q0tuyfXxs=", + "private": "-----BEGIN DSA PRIVATE KEY-----\nMIIBuwIBAAKBgQDywDdIxRMUiYgr8O6dltr+bar1yBuGMAoJa5dsJxjjARpn2niH\nMWRK81tNITsavjbwyJSuN5CYj/fFc9F0xyFug6ZBPQ3AuRhgOpw2Z/S/gUqwS/DY\nbgZc9npIFjkwqpwwuyJf4slAjIu/bGTzhIP2zBMViZQeQKLCa+7+bcFX7QIVALkA\nayKgcoBzYJ3ZDAotRlKYNkBXAoGAcWHxE4TCFz0JEnq97JyiXw89Lr3Ncv+obbI1\ny5Pd0wAIty0g9nRqmLObNFn8zuEZxbKNBKL6yzpyG6lNVwKa+gjygFV9kRLFxVlh\nd1YsRByBBdUPYCLx3WIAMUsbcGLzj6QMA5mj8Vnlef6jLDLtkUipCun9PCdL7BEp\nylOJ5p8CgYEAxH8NZyntzihIAHnx1Lbo7h1sPi4RhcpKK5pSUiaKoWxkjseqUsyW\nENt6DTByIdGhBNrOp9/vw2R5CSUkxuI0TlI8bj3qhq/B3bspx1GWjLqLfKbeVi4u\nn8CrooRRq2g8+nYLu2EWbF/56pEEzws6DptlDJQi7GdZG8Q0tuyfXxsCFG8ERflm\nOIBFUymTHP8ZeVOgNm/1\n-----END DSA PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_ecdsa b/test/fixtures/keyParser/openssh_new_ecdsa new file mode 100644 index 00000000..114e0787 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTjIb0On/AzYDLFRi+g3fGdAIF72KFG +iZBpP8oKZ8bsncH9ULtVV9517cNcRNuDETQtvLqoCdIn7TipYo8Jv/lKAAAAsA6ULqEOlC +6hAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOMhvQ6f8DNgMsVG +L6Dd8Z0AgXvYoUaJkGk/ygpnxuydwf1Qu1VX3nXtw1xE24MRNC28uqgJ0iftOKlijwm/+U +oAAAAfVd3jjve28r7FhY6Uo//cKIM1rBeWZG16b8bjyVyFswAAABJuZXcgb3BlbnNzaCBm +b3JtYXQBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_ecdsa.pub b/test/fixtures/keyParser/openssh_new_ecdsa.pub new file mode 100644 index 00000000..8ebee0fe --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOMhvQ6f8DNgMsVGL6Dd8Z0AgXvYoUaJkGk/ygpnxuydwf1Qu1VX3nXtw1xE24MRNC28uqgJ0iftOKlijwm/+Uo= new openssh format diff --git a/test/fixtures/keyParser/openssh_new_ecdsa.pub.result b/test/fixtures/keyParser/openssh_new_ecdsa.pub.result new file mode 100644 index 00000000..b430d737 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ecdsa-sha2-nistp256", + "comment": "new openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4yG9Dp/wM2AyxUYvoN3xnQCBe9ih\nRomQaT/KCmfG7J3B/VC7VVfede3DXETbgxE0Lby6qAnSJ+04qWKPCb/5Sg==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOMhvQ6f8DNgMsVGL6Dd8Z0AgXvYoUaJkGk/ygpnxuydwf1Qu1VX3nXtw1xE24MRNC28uqgJ0iftOKlijwm/+Uo=", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_new_ecdsa.result b/test/fixtures/keyParser/openssh_new_ecdsa.result new file mode 100644 index 00000000..74707dd3 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa.result @@ -0,0 +1,7 @@ +[{ + "type": "ecdsa-sha2-nistp256", + "comment": "new openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4yG9Dp/wM2AyxUYvoN3xnQCBe9ih\nRomQaT/KCmfG7J3B/VC7VVfede3DXETbgxE0Lby6qAnSJ+04qWKPCb/5Sg==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOMhvQ6f8DNgMsVGL6Dd8Z0AgXvYoUaJkGk/ygpnxuydwf1Qu1VX3nXtw1xE24MRNC28uqgJ0iftOKlijwm/+Uo=", + "private": "-----BEGIN EC PRIVATE KEY-----\nMHYCAQEEH1Xd4473tvK+xYWOlKP/3CiDNawXlmRtem/G48lchbOgCgYIKoZIzj0D\nAQehRANCAATjIb0On/AzYDLFRi+g3fGdAIF72KFGiZBpP8oKZ8bsncH9ULtVV951\n7cNcRNuDETQtvLqoCdIn7TipYo8Jv/lK\n-----END EC PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_ecdsa_enc b/test/fixtures/keyParser/openssh_new_ecdsa_enc new file mode 100644 index 00000000..08fe2d12 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa_enc @@ -0,0 +1,10 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBqNbb13W +CKfO7B1vpwJDwbAAAAEAAAAAEAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlz +dHAyNTYAAABBBJibjz7zvP/EhMZrW/JDdKvYgiEATNUKMfg2NOVxKlf++eTRypLFc1doTp +r+04Ebm1fkyp8RgpFsmvLXLt/dKU0AAADA86k3lHnP6pfD977mwEtKxHOJm44wx8NsdBwN +mNLqxlxUE520nsXjDgpgNU0MF9JDnc1kdhSy8PcdTAAH5+k6bpf3gotPrltPUBMFQdPqst +5kVS7zOgaxv1qZnlyhOqEdNR3Hee09gJByRrAojtcs+sPI7Nba879NPMb5c5K+gKhONHsa +wLAnz66eFQH5iLjd2MwrV4gJe0x6NGCSI2kyzNlxFsoIl7IcHlJHyyuaSlEOFWQJB8cbB4 +BVZB+/8yAx +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub b/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub new file mode 100644 index 00000000..3d87cb2c --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJibjz7zvP/EhMZrW/JDdKvYgiEATNUKMfg2NOVxKlf++eTRypLFc1doTpr+04Ebm1fkyp8RgpFsmvLXLt/dKU0= diff --git a/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub.result b/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub.result new file mode 100644 index 00000000..dcca403f --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub.result @@ -0,0 +1,8 @@ +{ + "type": "ecdsa-sha2-nistp256", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmJuPPvO8/8SExmtb8kN0q9iCIQBM\n1Qox+DY05XEqV/755NHKksVzV2hOmv7TgRubV+TKnxGCkWya8tcu390pTQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJibjz7zvP/EhMZrW/JDdKvYgiEATNUKMfg2NOVxKlf++eTRypLFc1doTpr+04Ebm1fkyp8RgpFsmvLXLt/dKU0=", + "private": null +} + diff --git a/test/fixtures/keyParser/openssh_new_ecdsa_enc.result b/test/fixtures/keyParser/openssh_new_ecdsa_enc.result new file mode 100644 index 00000000..52d1d4a6 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa_enc.result @@ -0,0 +1,7 @@ +[{ + "type": "ecdsa-sha2-nistp256", + "comment": "new openssh format encrypted", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmJuPPvO8/8SExmtb8kN0q9iCIQBM\n1Qox+DY05XEqV/755NHKksVzV2hOmv7TgRubV+TKnxGCkWya8tcu390pTQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJibjz7zvP/EhMZrW/JDdKvYgiEATNUKMfg2NOVxKlf++eTRypLFc1doTpr+04Ebm1fkyp8RgpFsmvLXLt/dKU0=", + "private": "-----BEGIN EC PRIVATE KEY-----\nMHgCAQEEIQDG2nALLBBmkBnw1QvdW4ClRfF3Zl3CcRHujsYz9CLvf6AKBggqhkjO\nPQMBB6FEA0IABJibjz7zvP/EhMZrW/JDdKvYgiEATNUKMfg2NOVxKlf++eTRypLF\nc1doTpr+04Ebm1fkyp8RgpFsmvLXLt/dKU0=\n-----END EC PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm b/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm new file mode 100644 index 00000000..84178baf --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm @@ -0,0 +1,10 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAAFmFlczEyOC1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA +AAGAAAABAHURyWtYwqVbjholNpL6opAAAAEAAAAAEAAABoAAAAE2VjZHNhLXNoYTItbmlz +dHAyNTYAAAAIbmlzdHAyNTYAAABBBM+ppawNxvkdHbOaB3ygsRueTdIKiT+OQkAH/5LpDx +XcD6i5AR8T/vrCsZ9/y+8GxU8gmvg4Uszr6LDfaQBZnsUAAADAFqKM/ylVkJ/ZA40ZROrW +LNgrttf2+lpVkADwXWzhuESFPPzERKlbHVsVtbiiYmPkLnY1s5VM4zXIj7xyO9YNA9KcM5 +GHOKUL2/NmDaTyGgc9s3BGu/ibpjSeOd1rtGAB4cw1s9ifbXBQd3qDbqzaEmovs3MGaGHD +c3VagdxhsppjrPjZ+B40Pzs9QkSGutsSJDpH9wVIu4OLr89TquTU3PVACDRU03lPPENVbt +rh2IMJeEQyNINQHtfVwordj8LMOEsBjyQ1aqHNva/iKyTBiw== +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub b/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub new file mode 100644 index 00000000..61b0b990 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM+ppawNxvkdHbOaB3ygsRueTdIKiT+OQkAH/5LpDxXcD6i5AR8T/vrCsZ9/y+8GxU8gmvg4Uszr6LDfaQBZnsU= diff --git a/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub.result b/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub.result new file mode 100644 index 00000000..10786485 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub.result @@ -0,0 +1,8 @@ +{ + "type": "ecdsa-sha2-nistp256", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz6mlrA3G+R0ds5oHfKCxG55N0gqJ\nP45CQAf/kukPFdwPqLkBHxP++sKxn3/L7wbFTyCa+DhSzOvosN9pAFmexQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM+ppawNxvkdHbOaB3ygsRueTdIKiT+OQkAH/5LpDxXcD6i5AR8T/vrCsZ9/y+8GxU8gmvg4Uszr6LDfaQBZnsU=", + "private": null +} + diff --git a/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.result b/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.result new file mode 100644 index 00000000..82b79b4a --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.result @@ -0,0 +1,7 @@ +[{ + "type": "ecdsa-sha2-nistp256", + "comment": "new openssh format encrypted gcm", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz6mlrA3G+R0ds5oHfKCxG55N0gqJ\nP45CQAf/kukPFdwPqLkBHxP++sKxn3/L7wbFTyCa+DhSzOvosN9pAFmexQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM+ppawNxvkdHbOaB3ygsRueTdIKiT+OQkAH/5LpDxXcD6i5AR8T/vrCsZ9/y+8GxU8gmvg4Uszr6LDfaQBZnsU=", + "private": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIHQfJ+4ZNcwSBaCR5kwrR6HjUsTF//R1F983RSTR8vbJoAoGCCqGSM49\nAwEHoUQDQgAEz6mlrA3G+R0ds5oHfKCxG55N0gqJP45CQAf/kukPFdwPqLkBHxP+\n+sKxn3/L7wbFTyCa+DhSzOvosN9pAFmexQ==\n-----END EC PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_ed25519 b/test/fixtures/keyParser/openssh_new_ed25519 new file mode 100644 index 00000000..7ae3165b --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCyOMGts0WaAdug9NeXbGn2Jrt4wwiO64dumxV2a1IgKQAAAJBOfs+eTn7P +ngAAAAtzc2gtZWQyNTUxOQAAACCyOMGts0WaAdug9NeXbGn2Jrt4wwiO64dumxV2a1IgKQ +AAAEBgQKxJoToGE/Xi4UkYR+FXfin4jG8NTcZ13rJ4CDnCfLI4wa2zRZoB26D015dsafYm +u3jDCI7rh26bFXZrUiApAAAAB3Rlc3RpbmcBAgMEBQY= +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_ed25519.pub b/test/fixtures/keyParser/openssh_new_ed25519.pub new file mode 100644 index 00000000..c85c7d1f --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILI4wa2zRZoB26D015dsafYmu3jDCI7rh26bFXZrUiAp testing diff --git a/test/fixtures/keyParser/openssh_new_ed25519.pub.result b/test/fixtures/keyParser/openssh_new_ed25519.pub.result new file mode 100644 index 00000000..3c9ca299 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ed25519.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-ed25519", + "comment": "testing", + "public": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAsjjBrbNFmgHboPTXl2xp9ia7eMMIjuuHbpsVdmtSICk=\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAC3NzaC1lZDI1NTE5AAAAILI4wa2zRZoB26D015dsafYmu3jDCI7rh26bFXZrUiAp", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_new_ed25519.result b/test/fixtures/keyParser/openssh_new_ed25519.result new file mode 100644 index 00000000..1f7a0e9b --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_ed25519.result @@ -0,0 +1,7 @@ +[{ + "type": "ssh-ed25519", + "comment": "testing", + "public": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAsjjBrbNFmgHboPTXl2xp9ia7eMMIjuuHbpsVdmtSICk=\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAC3NzaC1lZDI1NTE5AAAAILI4wa2zRZoB26D015dsafYmu3jDCI7rh26bFXZrUiAp", + "private": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIGBArEmhOgYT9eLhSRhH4Vd+KfiMbw1NxnXesngIOcJ8\n-----END PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_rsa b/test/fixtures/keyParser/openssh_new_rsa new file mode 100644 index 00000000..ccded2a2 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEA4q6eZdx7LYh46PcZNcS3CnO7GuYsEJZeTj5LQSgp21IyTelaBPpr +ijnMwKa+pLQt5TEobpKFFNecPdT6oPoOKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHM +BNkoTFeGrursPkqYRJ0HL4CqYqRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKb +zibJc64JFM7tUoK6Vl64YiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs +8zjxsf6c6N2tKXkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38 +KvTx3wjNQwAAA8hLhVBxS4VQcQAAAAdzc2gtcnNhAAABAQDirp5l3HstiHjo9xk1xLcKc7 +sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+l +fQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg6 +4MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u +2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28 +uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1DAAAAAwEAAQAAAQAmShSbZBiyYkD6KPLr +MCUy8MWED6kVzDB1yvPvN5eKYmH44xe/i4UqvgSl7gR50a2G7zzDIKC2Go1brGQBWPuXRa +ZtOjQygeD4rMHBiH/b7zfy4pQyKDfITTHOFXWE8ERiyL00bAZt09icCy92rQaq8IY/+U56 +sPPJH9UAYG9nEev8opFjAWToFDu0U2+dC+lbqLlXDqDRo75NlnDFmgUoja3y2eFr9A0Cc+ +hjecrdxyJFsCJfEfaLWtBnZb886gqzzvfbHImSQtBAKERcSxuki7uxMoP67g3iQOXa65uz +8kFWRNmbQTGQttakoUaybh1t9eLpBqvVON/4Kg0THShRAAAAgFBTz2ajBK/R/crOSL9VK1 +f7oQv2iJTRVfnUs0r+qPGgf/a/5UwkGRj0KfEWBp3qYD+keShnPr6PDPFrm8UmIdUX8AY7 +3tWT2K/JQVlzJNuINsw+DNjn4M17Z25q0LPmReRWL0nRc2w6W/hmQ/Jmqz6w8Qc4+xpeqS +/HG5feliVnAAAAgQD90a+5Ky3o/2YtueqRf/3dKoiMgGB7JAOzye4dDKGABSlWuQ4N4xEI +CW5MSTp7i/uobTF/tyFO3tTSyb5b2Xwbn/kLO0vgvFCdUGR2BQfN3mcT92T0Gn3JDF3Wym +i2mgU6qnPf+eu+RKZQ9IiyNGny61ROUQa0R0z0pgiAfA89xwAAAIEA5KE9i6hHmigJwfD7 +/AGI4ujyWIVpNyrTdXG3HAPhsdoFuG5ggHggrPuuBF9wNcosrhL20VNOQGHg15gWZIVudu +0qxky4ivQs67Sk9XUjuvTnf+VubM51rIsmh4atKJFSSZo78DEcTRt8aXLrSNvGQ4WPRweM +2Z0YGfMMDM9KJKUAAAASbmV3IG9wZW5zc2ggZm9ybWF0AQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_rsa.pub b/test/fixtures/keyParser/openssh_new_rsa.pub new file mode 100644 index 00000000..133afc9a --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D new openssh format diff --git a/test/fixtures/keyParser/openssh_new_rsa.pub.result b/test/fixtures/keyParser/openssh_new_rsa.pub.result new file mode 100644 index 00000000..dd8a8b4b --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "new openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4q6eZdx7LYh46PcZNcS3\nCnO7GuYsEJZeTj5LQSgp21IyTelaBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoO\nKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYq\nRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64\nYiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKX\nkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjN\nQwIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_new_rsa.result b/test/fixtures/keyParser/openssh_new_rsa.result new file mode 100644 index 00000000..aca28616 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa.result @@ -0,0 +1,7 @@ +[{ + "type": "ssh-rsa", + "comment": "new openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4q6eZdx7LYh46PcZNcS3\nCnO7GuYsEJZeTj5LQSgp21IyTelaBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoO\nKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYq\nRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64\nYiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKX\nkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjN\nQwIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D", + "private": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA4q6eZdx7LYh46PcZNcS3CnO7GuYsEJZeTj5LQSgp21IyTela\nBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoOKKMe6oH/pX0BNyAEB9KFZfZgh0v4\nJ4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYqRdINy1sgDU6jUIOuDD5XZzlpDXb1\nftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64YiPgxsNXOJYMTrelVJYebtsNrJFm\nh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKXkk9G4EDKKip4g0bzDmD/fREPQ9vL\ni59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjNQwIDAQABAoIBACZKFJtkGLJiQPoo\n8uswJTLwxYQPqRXMMHXK8+83l4piYfjjF7+LhSq+BKXuBHnRrYbvPMMgoLYajVus\nZAFY+5dFpm06NDKB4PiswcGIf9vvN/LilDIoN8hNMc4VdYTwRGLIvTRsBm3T2JwL\nL3atBqrwhj/5Tnqw88kf1QBgb2cR6/yikWMBZOgUO7RTb50L6VuouVcOoNGjvk2W\ncMWaBSiNrfLZ4Wv0DQJz6GN5yt3HIkWwIl8R9ota0GdlvzzqCrPO99sciZJC0EAo\nRFxLG6SLu7Eyg/ruDeJA5drrm7PyQVZE2ZtBMZC21qShRrJuHW314ukGq9U43/gq\nDRMdKFECgYEA/dGvuSst6P9mLbnqkX/93SqIjIBgeyQDs8nuHQyhgAUpVrkODeMR\nCAluTEk6e4v7qG0xf7chTt7U0sm+W9l8G5/5CztL4LxQnVBkdgUHzd5nE/dk9Bp9\nyQxd1spotpoFOqpz3/nrvkSmUPSIsjRp8utUTlEGtEdM9KYIgHwPPccCgYEA5KE9\ni6hHmigJwfD7/AGI4ujyWIVpNyrTdXG3HAPhsdoFuG5ggHggrPuuBF9wNcosrhL2\n0VNOQGHg15gWZIVudu0qxky4ivQs67Sk9XUjuvTnf+VubM51rIsmh4atKJFSSZo7\n8DEcTRt8aXLrSNvGQ4WPRweM2Z0YGfMMDM9KJKUCgYB7Yh0b1EOjCdQv0jqWtDNB\n+dUbB6Te92jdUwHvGR7AzsGDqL2OPp0e3QbDCq3lNO0GuN3hCbKlVmj6dpuUpqpP\n+3ni3dZKzwAZGOVdAaEDkGNnL1Hh36bZvqs3KHmymjiEhiuB60mP2mtG2zg/+H6w\nWXlIANdTd32PR87GNohqLQKBgA36ic/LJy2Wuxn/iPicg2kUQxUEey1jUfCBVmfB\nGQCNywG+xem07pKFBNvBlhPD27187VhZFpS7J0snQl89BUcCMzZSpIniagizT86u\nLdQVez4HohvG98zn6SAqLNYpJHXZl0aVShywzIeJ/jbDMTkZpmv6WzNG9p1HjfoO\nhoL9AoGAUFPPZqMEr9H9ys5Iv1UrV/uhC/aIlNFV+dSzSv6o8aB/9r/lTCQZGPQp\n8RYGnepgP6R5KGc+vo8M8WubxSYh1RfwBjve1ZPYr8lBWXMk24g2zD4M2OfgzXtn\nbmrQs+ZF5FYvSdFzbDpb+GZD8marPrDxBzj7Gl6pL8cbl96WJWc=\n-----END RSA PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_rsa_enc b/test/fixtures/keyParser/openssh_new_rsa_enc new file mode 100644 index 00000000..09aa65d9 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa_enc @@ -0,0 +1,28 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAS8H9Cyk +rueA/Ue6tOb1MOAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQC8hCiCPnRs +0ucZeyn3pNYKN63dVoxbMB4Yzjs7gvo7XKDby/6GXoU/CFQ/Q9zXRxRZmFglMYh2pOD8iW +dwpLBdd+GmHb4a6xxKtoPpz1+yCPYvi6nXzKPO3B9Wbg8dtTpV23l8MZDxSRUQ9HIkYHQO +oOjJx/AaMdZyHZP+eYK7UqmX1+dtCzr5vvLyEABxrsoFxH/oW/iKO6cDmTxoMyFl9DfUhD +TS7cL1OVBulSBav3aJPxjsCEIs6OE94wLJfFtZAPe4GqWWcC7uG1uUL5Muy2N+SfXHOHLa +I5n1vozt7lIO5TqvykcqTxipKblMW4Y7Iwlhh0YKJxzH3KJ+Qkn7AAAD4GeinUMcN5H0RP +KnXzIsYGq4rG+pEYNL0WyXCOFnyHzr6cASFYa/ViRVRN5H2dDoc0i2tcQStvDt2AfBxP97 +xbTEmRhLkKW7Sxif+bRRpNt2sO1y7ThufOZ8ZSJdbUYf9nc++k5GMZZUTtkFGhFIyhdyl+ +ZReuQFrc1Fv0/JV0K72uLSMSSMvunFjnGchch98Z1t0jEuiym8AIAwFtlvRpbOOySJhHun +fClEOahNvgzkgpqvviged7Gl9Kh3Fpp57ke1087WUF4hdgG2wuLqRq3Jq2kNvTKVi6+PMv +Kz5cLl6beqAJpbkJCpujzrmffo5NHh94R/v8DbAWCyrkjB6NHjOPIVnKaDmXixkcJ489W3 +PQF0kZ9kLrNU2yP1hBLjikr1zollw6xXC5eEpUsIrNcAHrofTMCMsGKuZhlEgTNe0cEATp +ycxi4gHdA6kNSDnMPwOv9rLDZDkgqCqIzxjZCWabqRHwiyoN3CrdDsJNrk8jSqF5epuzXA +EjrPUvu+sgFHIWDJOij+HQCvCgmdO/W7NkL/xCEx6QagjoJhapGICnq6CXPO5vBQeK7AMV +KWUPB1jdxxlHdrSUYU9v11j0SPUM51AMpWA89GZmuQbe/tK14W35VjtL9aGKsz9Ubio029 +O23HJXMxM9Dd6EYXAR9xMLFDTcLT03kjRlL/4XFS4fJqbTGDtuQNqRO3QK/myVAYjgnXwz +X1s77WeIK3sOMwTIXaHReUiQ1Cw+WmkXOhefePT+HrkyDlJk3ikgPUy2s5QW5/d6Lmolwb +mcS9JUfaai0ysP3v1bew8go/IHiUD/X9AkjkKM2kfS1NcPSi18r2721e6RqZiIHxSoyKvq +yUmwiS1kUklSuhlTORBvbclbv4HTwp1iJfu/6zsMqVJc2E8H6WUw3kTeh9fhDMpTY5NArF +KD2aRIYHFvOKav+0vSbQ/KqmKeiTvyZaV7q6giRxVLxBddl4+ucD+FybPJZSebRQ+0QT1j +aUDSpp541zW0rX7sCiZ6sFUybCPVDM1uA5gTAP015OD/FS342gi+Y04K0jBSjlApuy6BQx +sMEQbR3weMmnodbhCtbcgDZDagSFNPlDud0GJl9IWV4hO/K1f9a+Ox3G27Jq4YC2PFgTDb +aYib4xAXPUHJpoWsstSjpMnfgKcS3AGRdJ/jxlKRWV/NXFf4DYIwpzITqFMF+4VqXCa2AS +JWOcSxOK92UqCcZEs8RED3x9dF9E2yBBwHeuwDvH3c9x/nsM/cjDY+EE9VcEUOxF6qMOhO +CiRtEihEAYM46XeFzcSOQrwWPcKu3WTv3IpnzTaofBxV065CUn +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_rsa_enc.pub b/test/fixtures/keyParser/openssh_new_rsa_enc.pub new file mode 100644 index 00000000..0e80f0fd --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa_enc.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8hCiCPnRs0ucZeyn3pNYKN63dVoxbMB4Yzjs7gvo7XKDby/6GXoU/CFQ/Q9zXRxRZmFglMYh2pOD8iWdwpLBdd+GmHb4a6xxKtoPpz1+yCPYvi6nXzKPO3B9Wbg8dtTpV23l8MZDxSRUQ9HIkYHQOoOjJx/AaMdZyHZP+eYK7UqmX1+dtCzr5vvLyEABxrsoFxH/oW/iKO6cDmTxoMyFl9DfUhDTS7cL1OVBulSBav3aJPxjsCEIs6OE94wLJfFtZAPe4GqWWcC7uG1uUL5Muy2N+SfXHOHLaI5n1vozt7lIO5TqvykcqTxipKblMW4Y7Iwlhh0YKJxzH3KJ+Qkn7 diff --git a/test/fixtures/keyParser/openssh_new_rsa_enc.pub.result b/test/fixtures/keyParser/openssh_new_rsa_enc.pub.result new file mode 100644 index 00000000..ee0fd943 --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa_enc.pub.result @@ -0,0 +1,8 @@ +{ + "type": "ssh-rsa", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvIQogj50bNLnGXsp96TW\nCjet3VaMWzAeGM47O4L6O1yg28v+hl6FPwhUP0Pc10cUWZhYJTGIdqTg/IlncKSw\nXXfhph2+GuscSraD6c9fsgj2L4up18yjztwfVm4PHbU6Vdt5fDGQ8UkVEPRyJGB0\nDqDoycfwGjHWch2T/nmCu1Kpl9fnbQs6+b7y8hAAca7KBcR/6Fv4ijunA5k8aDMh\nZfQ31IQ00u3C9TlQbpUgWr92iT8Y7AhCLOjhPeMCyXxbWQD3uBqllnAu7htblC+T\nLstjfkn1xzhy2iOZ9b6M7e5SDuU6r8pHKk8YqSm5TFuGOyMJYYdGCiccx9yifkJJ\n+wIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQC8hCiCPnRs0ucZeyn3pNYKN63dVoxbMB4Yzjs7gvo7XKDby/6GXoU/CFQ/Q9zXRxRZmFglMYh2pOD8iWdwpLBdd+GmHb4a6xxKtoPpz1+yCPYvi6nXzKPO3B9Wbg8dtTpV23l8MZDxSRUQ9HIkYHQOoOjJx/AaMdZyHZP+eYK7UqmX1+dtCzr5vvLyEABxrsoFxH/oW/iKO6cDmTxoMyFl9DfUhDTS7cL1OVBulSBav3aJPxjsCEIs6OE94wLJfFtZAPe4GqWWcC7uG1uUL5Muy2N+SfXHOHLaI5n1vozt7lIO5TqvykcqTxipKblMW4Y7Iwlhh0YKJxzH3KJ+Qkn7", + "private": null +} + diff --git a/test/fixtures/keyParser/openssh_new_rsa_enc.result b/test/fixtures/keyParser/openssh_new_rsa_enc.result new file mode 100644 index 00000000..8160b16c --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa_enc.result @@ -0,0 +1,7 @@ +[{ + "type": "ssh-rsa", + "comment": "new openssh format encrypted", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvIQogj50bNLnGXsp96TW\nCjet3VaMWzAeGM47O4L6O1yg28v+hl6FPwhUP0Pc10cUWZhYJTGIdqTg/IlncKSw\nXXfhph2+GuscSraD6c9fsgj2L4up18yjztwfVm4PHbU6Vdt5fDGQ8UkVEPRyJGB0\nDqDoycfwGjHWch2T/nmCu1Kpl9fnbQs6+b7y8hAAca7KBcR/6Fv4ijunA5k8aDMh\nZfQ31IQ00u3C9TlQbpUgWr92iT8Y7AhCLOjhPeMCyXxbWQD3uBqllnAu7htblC+T\nLstjfkn1xzhy2iOZ9b6M7e5SDuU6r8pHKk8YqSm5TFuGOyMJYYdGCiccx9yifkJJ\n+wIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQC8hCiCPnRs0ucZeyn3pNYKN63dVoxbMB4Yzjs7gvo7XKDby/6GXoU/CFQ/Q9zXRxRZmFglMYh2pOD8iWdwpLBdd+GmHb4a6xxKtoPpz1+yCPYvi6nXzKPO3B9Wbg8dtTpV23l8MZDxSRUQ9HIkYHQOoOjJx/AaMdZyHZP+eYK7UqmX1+dtCzr5vvLyEABxrsoFxH/oW/iKO6cDmTxoMyFl9DfUhDTS7cL1OVBulSBav3aJPxjsCEIs6OE94wLJfFtZAPe4GqWWcC7uG1uUL5Muy2N+SfXHOHLaI5n1vozt7lIO5TqvykcqTxipKblMW4Y7Iwlhh0YKJxzH3KJ+Qkn7", + "private": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAvIQogj50bNLnGXsp96TWCjet3VaMWzAeGM47O4L6O1yg28v+\nhl6FPwhUP0Pc10cUWZhYJTGIdqTg/IlncKSwXXfhph2+GuscSraD6c9fsgj2L4up\n18yjztwfVm4PHbU6Vdt5fDGQ8UkVEPRyJGB0DqDoycfwGjHWch2T/nmCu1Kpl9fn\nbQs6+b7y8hAAca7KBcR/6Fv4ijunA5k8aDMhZfQ31IQ00u3C9TlQbpUgWr92iT8Y\n7AhCLOjhPeMCyXxbWQD3uBqllnAu7htblC+TLstjfkn1xzhy2iOZ9b6M7e5SDuU6\nr8pHKk8YqSm5TFuGOyMJYYdGCiccx9yifkJJ+wIDAQABAoIBAD1UXX1p5iSVRHvk\nttWLOdsfHCA7DPSJpfD5/wkwZkozq112czqxu3WzNv1SDaG3zSYMyvhmsfevUka2\nSQG7gmkWHEIXwQYu4Qhpcmb5gS+BfN4g+MNtHwmoUUWkDqTilbTi7xX5ZicpWIIo\nlI3DF16++JzUwAc1mYeMmd4bF+3quh93xW7hhrcQ31+D9kzqt6nLG1d9+IVpMbhD\nnNB9zapkZHwnz6YYhb5waMOHr6U902TyGgKyjq3Z/PkMJ0zKg01roUtQs9oQOIZF\nvueF2hwyzHqeIgpqhWJl9HMpfdym6Lh2lwguK3KYwNIMFQg+gNBWruYlH6SGfylq\n0wB5xIECgYEA8FdyEDd4TbVBKIXzzmY6zYmN/Q9uiz0IjbeYYzuRxZ4a7stE/t8n\nM5UxxkqeD8rtRAQJyFDGPAhFeeOpIfzEVPG+5s72pI69+9aE/gCGA91+sOSnLoiJ\nPW1I7SouZfCeaaRQxSSIMjsCea2s6yraujGZJyPEWSkG5TijY8+vzDsCgYEAyMxX\nCYvqlRTaT5lAkRTFLqf0/NSpRoCnG7qSPUyJjxJsVfYFLv1FZCyyrA+SaIyufjoT\nKutKE31r7wre5bkjRRenIcTkR/tdNRdkWsB/ysZ9Cp43FIPTXS5gxTQxOaJyRGvJ\n9MW0m8N1pMvPIsagzoxxvzgU9ZOejs2NQ69qXUECgYBq7DxOgp7+0zhdsto4ZLqc\nXinQ/2CKiWiYw6kD3KiJZkFNIxla2iQyiplOQjv3gqvzqmg/uc+3PWbLR0EjYbRm\npfXr8P9BTk+vDky0Q79bUNrgD5lg1lVYApqDCFUD/Pw8u2FDk3EUB7SeNWnMZZBR\nbWdZRkw/7kSnDX+DFA59qQKBgG9v0AHxT4/LEdlJEOczYrcg6TqDfyosbhFaepxg\nZJstO0h9j6TjVGZi1AnfXn59TL2q10ZjbCni2krAerF9DNDkbpG0Joi4PKMhR0WC\nPam4fF6vLZxKCLxW58epzoPQ3p+QPnWEX1ZupFR/84W2PDpFAT+BDUi40y8nbnWY\n3WvBAoGADjh0hEkq3sy6oWt0m1NjGU1yxKV+geg48BFnu2LVSFv1rw1V7X8XFEYl\nP1B3sEpOOpPGuoz+r2E9PrsdMuYNOmVlRFRpe7pm7zyhzdFYBvLE2btJqv1PmxFu\ncEkrXJS/ETxkKdMaoUbYHcKiTIMi2pDrdJtg6oHcipm0yTBZkKs=\n-----END RSA PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_new_rsa_enc_gcm b/test/fixtures/keyParser/openssh_new_rsa_enc_gcm new file mode 100644 index 00000000..442a4f5f --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa_enc_gcm @@ -0,0 +1,29 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAAFmFlczEyOC1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA +AAGAAAABBJL2YVn88iqv/H9bFiyW2PAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQAB +AAABAQDMemjkha1c+2s58qzx4968svvvpbxt6EiLlyRHuqXCouTdBZeXGtVRlxpkqnnOE0 +ETMSQSqm1d5k1EMa7VVcTeXFQaBIc2XF0S1uIoEvNV0JXpDjiIdPmjUFuUf9oGGLKKQQMf +zpymqoiHYQNhuarYd1mSb0+a+UwKxAxGeCPd95o/JfWjKO0JTr3nnEj1eTjtu0pofmchab +9HC9YbJ3JsvbdRq7Z2ZHp8uu16SflPpP2A9l+F4HN+gPOLcGxbVkVZHsLI07OpkWdxMPBU +rzPF9OnCntRWoBhQ4LFHYHllTtd+/E90QXXhe1pxj8FktJiaitiz09GU5h4IWi3isNr/AA +AD4Ktd9gUs9KHBmWTVFnDofcB6P1dZJsYHAapQgNXZtx5SjwgfBpP5aBLtSjN1iHFE+3XC +Cofc9UJ8fbytwT7LCEQIzo3KJaOhVzgJN+lrjtFouWsw0Y1q2JONHvvNJ5A9nGjIGbp3du +4TAMSgVAvxZBEYez4ajhb2NL7TE56AjOxW4n/M2ZDJLCo11F3ON3Eq6MirHZMgGKo/lbOc +SaBld7tzqknye+1fKVlnCLyu+v0KCbATBypRsMeX1+E/D8L5cMIgRSe97swqiWeG9yBhQi +xahbWDpmU34nz1cxc9H7KnL1rbbOxrr4OEdMOHNBQjbLlpJpnSJ3XvEGP74zjfd5zMocgx +rnqreMmY+eDEObkw33+XD5ROYJT+SW/zI+r3SeIjS3UPh0ucU5nipBvXfkUezek9i/FN1X +CY7xJnAZGGKU0JSqiVW3JWXp18v8lmo3ACvXeotJfUGkwvJOeO2N4Qb7RTIzivLV5Q5Plf +zHWqHE57UqDL/Ya7SrX1FaqqhOHOlS1mqPQ+/VdsOSP5fJcXN+oKoL7jPr2WlmtFjo8PKc +rpgKC3DhUzvRXnNYotG7trbPOGJbBRgoxTQ06rlChoaBp7kUKqNNBxXhFQCeN0sCb90fHV +c+X3Yy8oUsAIxxmCymuVV8gRzLD6OdqQRBthEUQktNJLhv4mSufwSfsLDluEc7YEOrsJhx +jk57TmkFFyLj++IAKi80FnSkRfSBQF3dTSrBZ4BIHWnek8V6goxhy6lRMaFoTow2foknvr +VHgiNGvimOM3ESYVcOwt3YQqbUG/7b4jRlY3nNBJcsbxGe54B8zaoLt5pQNRxUuHc3fR4R +haWHR6IWsfey7jAlRzrJAVVEEj4d6yvJ4bLqWGmoim5QlrePRuRFyV4FNb8N6hJ9gvWY9f +HUT9TwxArDIMzu4T1khwRoFU45XN0U6xHEPcT/pZ2C5jJSSQ5W/SyBudexjMMPRKf2EIeD +gjv8vIhdtkmxHv7bapaaYeYX5gtKYl+McRollDxVC8Kr48RmOVJnK4aFBQ99Wu7SXDbwas +vcvVHI+zUiRGjU01/CU/Tf4GTodAlmZIuqKmBTX/KvVj6ZiK0BsZuEl9qom+l4rlazaahY +FdL5M4u0qt7rVirWJWgWzmPXZ+MCK0Fs70ORvqRGxVMilhQcWsng3ZXnHaYiBRhk31KqF+ +BEPEh79OknD0okKed2YYfg8vdUR+noENybrsIleP1aKBBmQCNbKU04N/9Su+wxX8YfGhYU +kPST35Wg45zER9gZGsREnON4sQTng9LHB5CrJCo/MowcZG/ycqL1mxemApZ9nYUrjA8HJi +zDwRHHUtkkLNG8Cmyg== +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub b/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub new file mode 100644 index 00000000..d5c7685f --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMemjkha1c+2s58qzx4968svvvpbxt6EiLlyRHuqXCouTdBZeXGtVRlxpkqnnOE0ETMSQSqm1d5k1EMa7VVcTeXFQaBIc2XF0S1uIoEvNV0JXpDjiIdPmjUFuUf9oGGLKKQQMfzpymqoiHYQNhuarYd1mSb0+a+UwKxAxGeCPd95o/JfWjKO0JTr3nnEj1eTjtu0pofmchab9HC9YbJ3JsvbdRq7Z2ZHp8uu16SflPpP2A9l+F4HN+gPOLcGxbVkVZHsLI07OpkWdxMPBUrzPF9OnCntRWoBhQ4LFHYHllTtd+/E90QXXhe1pxj8FktJiaitiz09GU5h4IWi3isNr/ diff --git a/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub.result b/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub.result new file mode 100644 index 00000000..0f00545e --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub.result @@ -0,0 +1,8 @@ +{ + "type": "ssh-rsa", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzHpo5IWtXPtrOfKs8ePe\nvLL776W8behIi5ckR7qlwqLk3QWXlxrVUZcaZKp5zhNBEzEkEqptXeZNRDGu1VXE\n3lxUGgSHNlxdEtbiKBLzVdCV6Q44iHT5o1BblH/aBhiyikEDH86cpqqIh2EDYbmq\n2HdZkm9PmvlMCsQMRngj3feaPyX1oyjtCU6955xI9Xk47btKaH5nIWm/RwvWGydy\nbL23Uau2dmR6fLrtekn5T6T9gPZfheBzfoDzi3BsW1ZFWR7CyNOzqZFncTDwVK8z\nxfTpwp7UVqAYUOCxR2B5ZU7XfvxPdEF14XtacY/BZLSYmorYs9PRlOYeCFot4rDa\n/wIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDMemjkha1c+2s58qzx4968svvvpbxt6EiLlyRHuqXCouTdBZeXGtVRlxpkqnnOE0ETMSQSqm1d5k1EMa7VVcTeXFQaBIc2XF0S1uIoEvNV0JXpDjiIdPmjUFuUf9oGGLKKQQMfzpymqoiHYQNhuarYd1mSb0+a+UwKxAxGeCPd95o/JfWjKO0JTr3nnEj1eTjtu0pofmchab9HC9YbJ3JsvbdRq7Z2ZHp8uu16SflPpP2A9l+F4HN+gPOLcGxbVkVZHsLI07OpkWdxMPBUrzPF9OnCntRWoBhQ4LFHYHllTtd+/E90QXXhe1pxj8FktJiaitiz09GU5h4IWi3isNr/", + "private": null +} + diff --git a/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.result b/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.result new file mode 100644 index 00000000..104beeac --- /dev/null +++ b/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.result @@ -0,0 +1,7 @@ +[{ + "type": "ssh-rsa", + "comment": "new openssh format encrypted gcm", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzHpo5IWtXPtrOfKs8ePe\nvLL776W8behIi5ckR7qlwqLk3QWXlxrVUZcaZKp5zhNBEzEkEqptXeZNRDGu1VXE\n3lxUGgSHNlxdEtbiKBLzVdCV6Q44iHT5o1BblH/aBhiyikEDH86cpqqIh2EDYbmq\n2HdZkm9PmvlMCsQMRngj3feaPyX1oyjtCU6955xI9Xk47btKaH5nIWm/RwvWGydy\nbL23Uau2dmR6fLrtekn5T6T9gPZfheBzfoDzi3BsW1ZFWR7CyNOzqZFncTDwVK8z\nxfTpwp7UVqAYUOCxR2B5ZU7XfvxPdEF14XtacY/BZLSYmorYs9PRlOYeCFot4rDa\n/wIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDMemjkha1c+2s58qzx4968svvvpbxt6EiLlyRHuqXCouTdBZeXGtVRlxpkqnnOE0ETMSQSqm1d5k1EMa7VVcTeXFQaBIc2XF0S1uIoEvNV0JXpDjiIdPmjUFuUf9oGGLKKQQMfzpymqoiHYQNhuarYd1mSb0+a+UwKxAxGeCPd95o/JfWjKO0JTr3nnEj1eTjtu0pofmchab9HC9YbJ3JsvbdRq7Z2ZHp8uu16SflPpP2A9l+F4HN+gPOLcGxbVkVZHsLI07OpkWdxMPBUrzPF9OnCntRWoBhQ4LFHYHllTtd+/E90QXXhe1pxj8FktJiaitiz09GU5h4IWi3isNr/", + "private": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAzHpo5IWtXPtrOfKs8ePevLL776W8behIi5ckR7qlwqLk3QWX\nlxrVUZcaZKp5zhNBEzEkEqptXeZNRDGu1VXE3lxUGgSHNlxdEtbiKBLzVdCV6Q44\niHT5o1BblH/aBhiyikEDH86cpqqIh2EDYbmq2HdZkm9PmvlMCsQMRngj3feaPyX1\noyjtCU6955xI9Xk47btKaH5nIWm/RwvWGydybL23Uau2dmR6fLrtekn5T6T9gPZf\nheBzfoDzi3BsW1ZFWR7CyNOzqZFncTDwVK8zxfTpwp7UVqAYUOCxR2B5ZU7XfvxP\ndEF14XtacY/BZLSYmorYs9PRlOYeCFot4rDa/wIDAQABAoIBAQCCb7uluxhh7gfy\niTmFfETDvrEzqFfRDJHqadm83/WJeXvg+gY/X+CgEXHGsXDN4j5qzbgjKBBoC9dS\nHxdWA0Z4ShFkH2tZZAYDVIwj4CLVpR9b8bRiZ6wvX71rtzsPFIYf52Tkz1nif3pk\nUaBkoJm5SDkdTmBLjafSXkkuUskeeAV7gx+fzWqSpcKmhTqjnQfdlmD8OSIq4jjD\nagiHmmfBhZ4NOvF/E9UBydqFV8GNyfSFC6kC2LYmiQD1hvqNhMdYVjh99V1L3ZPq\nHMSQVAOv5WgpLTLKY8MFNBbqqp0eKhatRNA8q9O23jADDp3fubKV0aUQSrRZz0y9\nPmmEJnTRAoGBAPZoL+p+AbI5yTg01LdsaQL2f3Ieb3CGudesmjAVnI3QEoC6gxGX\n4cbmBSCY+vBzh2RJNJcS+Rq6VmJZA930Tb0npHiQYOohB7BFOCbBJ2L18g/JdNpi\nVb3wqFs9NG1GFOOV6iGtV/6t4CRTKtAbd695YZAJ5S6DDvMrH9pTnAKrAoGBANRw\nVuLfBTFhSKvFz+0W0yy6Sn0koXjpp1ifC0BWLwHiA/IZjAY7qmsNQZxWdleWLP28\nRNaac3vMJO/HFD4IyL59Zli+kREGKazvZM1dvOs0mgdVMTPMsT57wcJr5OSxqCvJ\nD3NkcgFuA1e3jVC5p/wUJCi/lhyFPx3z1C5vRqj9AoGBANeyYmd5wFBcp1ktXhvm\nqZIvZ2blX5X4ScyTSjHXaUD2qIvJORz4gGqVRl2/rMM5zoYqUwAAWtFb1mynEWyF\nBFwVzLLBaCTrnwhdv4alRK4rL6dEKadVt0ra1PVxgWg6leSXgenTDRli6bfCmdKs\niLuxnIbzMozhqv+Qe4Sp9gKbAoGBALWBThsEpXEtR2PL3P0atU7P0/jcJUIjkCF9\nsaVEfWFEdE6TWTmyHMbeSqKClRX8b3BTPRWGXQj2wNBE7Zya8LkgdyN3noZHF7Bz\n0VJNtq3XAYsmVKWHTCCwqDmu6aAj0iWm4ZabyXRDRIPbhdfk6AvOQZ63IlA34Fd9\nDlqmJF8ZAoGAIJzfMDT2LvlMOHqpKgelS4ZTHEmqqJZM5rXdsZwYqcyekjz25COE\nTJwme3xIt3kSZEcOauGHCgUVeBcE6GwZbQ1WoNIvazhnUXeErOeoxQ+ZqdfC8iyT\nUn/P27yx/FcwDdubQhbgxZ5M+pu+0OQ1WPu02LQZQrX7x4a6isYtTDo=\n-----END RSA PRIVATE KEY-----" +}] diff --git a/test/fixtures/keyParser/openssh_old_dsa b/test/fixtures/keyParser/openssh_old_dsa new file mode 100644 index 00000000..f2ae4d42 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_dsa @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBvQIBAAKBgQDs+n9ZKhwYNr1V2uGn0C/2MSTM4KB4puy4jR5ubRTT1yq5SbzK +RQlCjfplDN//Eqa6aiFmvGKA3RKUtPtBmD96EHW1mvr7O+Pc8z8L/4zg9tkVQR6V +WBKgBhVwZHDzzs5+Ag2j54BZfcaGMcNGhTE9DcZYeI/t6FhOxgpID3EA/QIVAMyI +czBU74xB48IMoamlEhc5Lh+3AoGBAMuy2h9K9+oQIPcTcsD/mtmhOYlw2ZPCJV2b +WFeZ3QxAujenBzEp0oqht8tdj+BE7Er+CWT2Ab/A92MrjYUaGaPjdF5+K6CSPMUX +rK8nBabSBJ+ELqTo/8vHJ2eVWIUJBwCzbw3ryitH7LD3gyEr2NuQQJE++wyWPBHK +M3SFOft6AoGBAOdrYUJ38yjc9tnrvLWsB1KlkYhc+UbTMSRKfA8Yo/Xs5QldFycz +bUtsFGdLvqPol0pww2LqeKUQ8zVIF56Aw3SxmPMnOzRVQXpUI7z2W3/Ie4/i2Lu/ +xXos8ZHnIu+e7SLJRHe+RGNvISbsQhk+vnpNQP5ciuO0ltu90L9+2YvWAhUAr/vy +ahuEz4UFGhB8IIeLWQUO5FA= +-----END DSA PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_old_dsa.pub b/test/fixtures/keyParser/openssh_old_dsa.pub new file mode 100644 index 00000000..a7fd375a --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_dsa.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAOz6f1kqHBg2vVXa4afQL/YxJMzgoHim7LiNHm5tFNPXKrlJvMpFCUKN+mUM3/8SprpqIWa8YoDdEpS0+0GYP3oQdbWa+vs749zzPwv/jOD22RVBHpVYEqAGFXBkcPPOzn4CDaPngFl9xoYxw0aFMT0Nxlh4j+3oWE7GCkgPcQD9AAAAFQDMiHMwVO+MQePCDKGppRIXOS4ftwAAAIEAy7LaH0r36hAg9xNywP+a2aE5iXDZk8IlXZtYV5ndDEC6N6cHMSnSiqG3y12P4ETsSv4JZPYBv8D3YyuNhRoZo+N0Xn4roJI8xResrycFptIEn4QupOj/y8cnZ5VYhQkHALNvDevKK0fssPeDISvY25BAkT77DJY8EcozdIU5+3oAAACBAOdrYUJ38yjc9tnrvLWsB1KlkYhc+UbTMSRKfA8Yo/Xs5QldFyczbUtsFGdLvqPol0pww2LqeKUQ8zVIF56Aw3SxmPMnOzRVQXpUI7z2W3/Ie4/i2Lu/xXos8ZHnIu+e7SLJRHe+RGNvISbsQhk+vnpNQP5ciuO0ltu90L9+2YvW old openssh format diff --git a/test/fixtures/keyParser/openssh_old_dsa.pub.result b/test/fixtures/keyParser/openssh_old_dsa.pub.result new file mode 100644 index 00000000..05f81407 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_dsa.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-dss", + "comment": "old openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBuDCCASwGByqGSM44BAEwggEfAoGBAOz6f1kqHBg2vVXa4afQL/YxJMzgoHim\n7LiNHm5tFNPXKrlJvMpFCUKN+mUM3/8SprpqIWa8YoDdEpS0+0GYP3oQdbWa+vs7\n49zzPwv/jOD22RVBHpVYEqAGFXBkcPPOzn4CDaPngFl9xoYxw0aFMT0Nxlh4j+3o\nWE7GCkgPcQD9AhUAzIhzMFTvjEHjwgyhqaUSFzkuH7cCgYEAy7LaH0r36hAg9xNy\nwP+a2aE5iXDZk8IlXZtYV5ndDEC6N6cHMSnSiqG3y12P4ETsSv4JZPYBv8D3YyuN\nhRoZo+N0Xn4roJI8xResrycFptIEn4QupOj/y8cnZ5VYhQkHALNvDevKK0fssPeD\nISvY25BAkT77DJY8EcozdIU5+3oDgYUAAoGBAOdrYUJ38yjc9tnrvLWsB1KlkYhc\n+UbTMSRKfA8Yo/Xs5QldFyczbUtsFGdLvqPol0pww2LqeKUQ8zVIF56Aw3SxmPMn\nOzRVQXpUI7z2W3/Ie4/i2Lu/xXos8ZHnIu+e7SLJRHe+RGNvISbsQhk+vnpNQP5c\niuO0ltu90L9+2YvW\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAOz6f1kqHBg2vVXa4afQL/YxJMzgoHim7LiNHm5tFNPXKrlJvMpFCUKN+mUM3/8SprpqIWa8YoDdEpS0+0GYP3oQdbWa+vs749zzPwv/jOD22RVBHpVYEqAGFXBkcPPOzn4CDaPngFl9xoYxw0aFMT0Nxlh4j+3oWE7GCkgPcQD9AAAAFQDMiHMwVO+MQePCDKGppRIXOS4ftwAAAIEAy7LaH0r36hAg9xNywP+a2aE5iXDZk8IlXZtYV5ndDEC6N6cHMSnSiqG3y12P4ETsSv4JZPYBv8D3YyuNhRoZo+N0Xn4roJI8xResrycFptIEn4QupOj/y8cnZ5VYhQkHALNvDevKK0fssPeDISvY25BAkT77DJY8EcozdIU5+3oAAACBAOdrYUJ38yjc9tnrvLWsB1KlkYhc+UbTMSRKfA8Yo/Xs5QldFyczbUtsFGdLvqPol0pww2LqeKUQ8zVIF56Aw3SxmPMnOzRVQXpUI7z2W3/Ie4/i2Lu/xXos8ZHnIu+e7SLJRHe+RGNvISbsQhk+vnpNQP5ciuO0ltu90L9+2YvW", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_old_dsa.result b/test/fixtures/keyParser/openssh_old_dsa.result new file mode 100644 index 00000000..d21d1cbd --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_dsa.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-dss", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBuDCCASwGByqGSM44BAEwggEfAoGBAOz6f1kqHBg2vVXa4afQL/YxJMzgoHim\n7LiNHm5tFNPXKrlJvMpFCUKN+mUM3/8SprpqIWa8YoDdEpS0+0GYP3oQdbWa+vs7\n49zzPwv/jOD22RVBHpVYEqAGFXBkcPPOzn4CDaPngFl9xoYxw0aFMT0Nxlh4j+3o\nWE7GCkgPcQD9AhUAzIhzMFTvjEHjwgyhqaUSFzkuH7cCgYEAy7LaH0r36hAg9xNy\nwP+a2aE5iXDZk8IlXZtYV5ndDEC6N6cHMSnSiqG3y12P4ETsSv4JZPYBv8D3YyuN\nhRoZo+N0Xn4roJI8xResrycFptIEn4QupOj/y8cnZ5VYhQkHALNvDevKK0fssPeD\nISvY25BAkT77DJY8EcozdIU5+3oDgYUAAoGBAOdrYUJ38yjc9tnrvLWsB1KlkYhc\n+UbTMSRKfA8Yo/Xs5QldFyczbUtsFGdLvqPol0pww2LqeKUQ8zVIF56Aw3SxmPMn\nOzRVQXpUI7z2W3/Ie4/i2Lu/xXos8ZHnIu+e7SLJRHe+RGNvISbsQhk+vnpNQP5c\niuO0ltu90L9+2YvW\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAOz6f1kqHBg2vVXa4afQL/YxJMzgoHim7LiNHm5tFNPXKrlJvMpFCUKN+mUM3/8SprpqIWa8YoDdEpS0+0GYP3oQdbWa+vs749zzPwv/jOD22RVBHpVYEqAGFXBkcPPOzn4CDaPngFl9xoYxw0aFMT0Nxlh4j+3oWE7GCkgPcQD9AAAAFQDMiHMwVO+MQePCDKGppRIXOS4ftwAAAIEAy7LaH0r36hAg9xNywP+a2aE5iXDZk8IlXZtYV5ndDEC6N6cHMSnSiqG3y12P4ETsSv4JZPYBv8D3YyuNhRoZo+N0Xn4roJI8xResrycFptIEn4QupOj/y8cnZ5VYhQkHALNvDevKK0fssPeDISvY25BAkT77DJY8EcozdIU5+3oAAACBAOdrYUJ38yjc9tnrvLWsB1KlkYhc+UbTMSRKfA8Yo/Xs5QldFyczbUtsFGdLvqPol0pww2LqeKUQ8zVIF56Aw3SxmPMnOzRVQXpUI7z2W3/Ie4/i2Lu/xXos8ZHnIu+e7SLJRHe+RGNvISbsQhk+vnpNQP5ciuO0ltu90L9+2YvW", + "private": "-----BEGIN DSA PRIVATE KEY-----\nMIIBvQIBAAKBgQDs+n9ZKhwYNr1V2uGn0C/2MSTM4KB4puy4jR5ubRTT1yq5SbzK\nRQlCjfplDN//Eqa6aiFmvGKA3RKUtPtBmD96EHW1mvr7O+Pc8z8L/4zg9tkVQR6V\nWBKgBhVwZHDzzs5+Ag2j54BZfcaGMcNGhTE9DcZYeI/t6FhOxgpID3EA/QIVAMyI\nczBU74xB48IMoamlEhc5Lh+3AoGBAMuy2h9K9+oQIPcTcsD/mtmhOYlw2ZPCJV2b\nWFeZ3QxAujenBzEp0oqht8tdj+BE7Er+CWT2Ab/A92MrjYUaGaPjdF5+K6CSPMUX\nrK8nBabSBJ+ELqTo/8vHJ2eVWIUJBwCzbw3ryitH7LD3gyEr2NuQQJE++wyWPBHK\nM3SFOft6AoGBAOdrYUJ38yjc9tnrvLWsB1KlkYhc+UbTMSRKfA8Yo/Xs5QldFycz\nbUtsFGdLvqPol0pww2LqeKUQ8zVIF56Aw3SxmPMnOzRVQXpUI7z2W3/Ie4/i2Lu/\nxXos8ZHnIu+e7SLJRHe+RGNvISbsQhk+vnpNQP5ciuO0ltu90L9+2YvWAhUAr/vy\nahuEz4UFGhB8IIeLWQUO5FA=\n-----END DSA PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/openssh_old_dsa_enc b/test/fixtures/keyParser/openssh_old_dsa_enc new file mode 100644 index 00000000..57064cdf --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_dsa_enc @@ -0,0 +1,15 @@ +-----BEGIN DSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,3239878D1E2D496289CE9CD2CB639BE8 + +k8/4Ax6UcnImNvEuybHwa9OHZHeCpKmq3Cu/q29a9AkTnktAWVmU9rQFch5CweDH +TEuRN+ZHecHrrMPR0fTpjXzZTxmU3549BQ2DfMSAdikPNKtBvhJwpT2se0rJ9M98 +p2xJQNhpxXT6f4Hy8m6QvjP5iTmlnQrrVBjV05ih9TLLQb4Y4NlydC08OyEcEoJV +w43G69sv2ws/tUVr7XSUtv8l+51ywSm42Pw6YOVlMZ7y+XB/uWmFNMz5gLN17tkc +wikhgvNnMWGLqb/AruuKPp5FrGRIC19DKRzDSPF5WlzLBdd2TQKDltknDj08AQMJ +bDsImbePteqhU+D7GiN2pVAD2b5kCZlFzYG43/Q8R3+O2l0Lvq5VBIqNB7LyJfTy +DL8XX0gzHk7FgG5MfLYin/qp7upnDXeSnIm8A2tlBYh9YzG3q/a53c5V2NomWjX0 +zvS+C7+w5NDwDRT5t+kecMhmHWNBuE/Pbvy0DaZQ/nnsC6TlkcaROJ0fiY3Da8E6 +EYvM4uKaZudsOOapwx0ZXHu2GZgLnly0p2Cd0Yf9t2UX9uySfwdL2TNw8nLVNVkh +aBE/x9LkKPWqOBV8tg/9ITGys/qgZh0A1r+RGmj/tII= +-----END DSA PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_old_dsa_enc.pub b/test/fixtures/keyParser/openssh_old_dsa_enc.pub new file mode 100644 index 00000000..18f58045 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_dsa_enc.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAP25RC69mW4t09jpaine5ZRHmOtqNJa2nbsRrSsZkvGXxbJ7ojxsybWf4kAAI4GpsGMzlrFrlMEpHQfebJAn+zJwGS+loR7T+gNz8JoVIgPF9dabXVymcygl4FB/sNAmV4XK3OjvSW1NCKdSkwZZr/gz5JBo1qAiQDKMD/ikWqq/AAAAFQC/rPmzFozpCeLbFQykOaDGFZaqaQAAAIEAw1hJAYQzn/ZboF/xXDHzP49uRpIIoyaSfUz5W3+Lpi/CBkOIGaGOuitwcpTfzBSZIDZ9ORs9fq5oBh29JJcAdBNgVXfzThSiGvBgU4UIj41MlG4PG6St88VXCy0niEXWmjSkdcW3hZ0ai0SOlVxxEkYneg7RH9Seh+U3rRacrh4AAACAOX41OCxx8mTuxpON/uZn6GwvK/m0K9fr/UmIX8D4Mp8PgnPLC71AOwLy1HrCVi3ohCqeSY2C1uf1VWUVlSqMH85Pxc7pLtuULoQdCgiYt1agVrioFSP6bEyFdV8vGxA4YGh6cUSkeFZBJBrdNM4VmYBeT+3n/IO5uUbWoPK5iAo= diff --git a/test/fixtures/keyParser/openssh_old_dsa_enc.pub.result b/test/fixtures/keyParser/openssh_old_dsa_enc.pub.result new file mode 100644 index 00000000..ad142600 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_dsa_enc.pub.result @@ -0,0 +1,8 @@ +{ + "type": "ssh-dss", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtzCCASwGByqGSM44BAEwggEfAoGBAP25RC69mW4t09jpaine5ZRHmOtqNJa2\nnbsRrSsZkvGXxbJ7ojxsybWf4kAAI4GpsGMzlrFrlMEpHQfebJAn+zJwGS+loR7T\n+gNz8JoVIgPF9dabXVymcygl4FB/sNAmV4XK3OjvSW1NCKdSkwZZr/gz5JBo1qAi\nQDKMD/ikWqq/AhUAv6z5sxaM6Qni2xUMpDmgxhWWqmkCgYEAw1hJAYQzn/ZboF/x\nXDHzP49uRpIIoyaSfUz5W3+Lpi/CBkOIGaGOuitwcpTfzBSZIDZ9ORs9fq5oBh29\nJJcAdBNgVXfzThSiGvBgU4UIj41MlG4PG6St88VXCy0niEXWmjSkdcW3hZ0ai0SO\nlVxxEkYneg7RH9Seh+U3rRacrh4DgYQAAoGAOX41OCxx8mTuxpON/uZn6GwvK/m0\nK9fr/UmIX8D4Mp8PgnPLC71AOwLy1HrCVi3ohCqeSY2C1uf1VWUVlSqMH85Pxc7p\nLtuULoQdCgiYt1agVrioFSP6bEyFdV8vGxA4YGh6cUSkeFZBJBrdNM4VmYBeT+3n\n/IO5uUbWoPK5iAo=\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAP25RC69mW4t09jpaine5ZRHmOtqNJa2nbsRrSsZkvGXxbJ7ojxsybWf4kAAI4GpsGMzlrFrlMEpHQfebJAn+zJwGS+loR7T+gNz8JoVIgPF9dabXVymcygl4FB/sNAmV4XK3OjvSW1NCKdSkwZZr/gz5JBo1qAiQDKMD/ikWqq/AAAAFQC/rPmzFozpCeLbFQykOaDGFZaqaQAAAIEAw1hJAYQzn/ZboF/xXDHzP49uRpIIoyaSfUz5W3+Lpi/CBkOIGaGOuitwcpTfzBSZIDZ9ORs9fq5oBh29JJcAdBNgVXfzThSiGvBgU4UIj41MlG4PG6St88VXCy0niEXWmjSkdcW3hZ0ai0SOlVxxEkYneg7RH9Seh+U3rRacrh4AAACAOX41OCxx8mTuxpON/uZn6GwvK/m0K9fr/UmIX8D4Mp8PgnPLC71AOwLy1HrCVi3ohCqeSY2C1uf1VWUVlSqMH85Pxc7pLtuULoQdCgiYt1agVrioFSP6bEyFdV8vGxA4YGh6cUSkeFZBJBrdNM4VmYBeT+3n/IO5uUbWoPK5iAo=", + "private": null +} + diff --git a/test/fixtures/keyParser/openssh_old_dsa_enc.result b/test/fixtures/keyParser/openssh_old_dsa_enc.result new file mode 100644 index 00000000..18c52716 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_dsa_enc.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-dss", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtzCCASwGByqGSM44BAEwggEfAoGBAP25RC69mW4t09jpaine5ZRHmOtqNJa2\nnbsRrSsZkvGXxbJ7ojxsybWf4kAAI4GpsGMzlrFrlMEpHQfebJAn+zJwGS+loR7T\n+gNz8JoVIgPF9dabXVymcygl4FB/sNAmV4XK3OjvSW1NCKdSkwZZr/gz5JBo1qAi\nQDKMD/ikWqq/AhUAv6z5sxaM6Qni2xUMpDmgxhWWqmkCgYEAw1hJAYQzn/ZboF/x\nXDHzP49uRpIIoyaSfUz5W3+Lpi/CBkOIGaGOuitwcpTfzBSZIDZ9ORs9fq5oBh29\nJJcAdBNgVXfzThSiGvBgU4UIj41MlG4PG6St88VXCy0niEXWmjSkdcW3hZ0ai0SO\nlVxxEkYneg7RH9Seh+U3rRacrh4DgYQAAoGAOX41OCxx8mTuxpON/uZn6GwvK/m0\nK9fr/UmIX8D4Mp8PgnPLC71AOwLy1HrCVi3ohCqeSY2C1uf1VWUVlSqMH85Pxc7p\nLtuULoQdCgiYt1agVrioFSP6bEyFdV8vGxA4YGh6cUSkeFZBJBrdNM4VmYBeT+3n\n/IO5uUbWoPK5iAo=\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAP25RC69mW4t09jpaine5ZRHmOtqNJa2nbsRrSsZkvGXxbJ7ojxsybWf4kAAI4GpsGMzlrFrlMEpHQfebJAn+zJwGS+loR7T+gNz8JoVIgPF9dabXVymcygl4FB/sNAmV4XK3OjvSW1NCKdSkwZZr/gz5JBo1qAiQDKMD/ikWqq/AAAAFQC/rPmzFozpCeLbFQykOaDGFZaqaQAAAIEAw1hJAYQzn/ZboF/xXDHzP49uRpIIoyaSfUz5W3+Lpi/CBkOIGaGOuitwcpTfzBSZIDZ9ORs9fq5oBh29JJcAdBNgVXfzThSiGvBgU4UIj41MlG4PG6St88VXCy0niEXWmjSkdcW3hZ0ai0SOlVxxEkYneg7RH9Seh+U3rRacrh4AAACAOX41OCxx8mTuxpON/uZn6GwvK/m0K9fr/UmIX8D4Mp8PgnPLC71AOwLy1HrCVi3ohCqeSY2C1uf1VWUVlSqMH85Pxc7pLtuULoQdCgiYt1agVrioFSP6bEyFdV8vGxA4YGh6cUSkeFZBJBrdNM4VmYBeT+3n/IO5uUbWoPK5iAo=", + "private": "-----BEGIN DSA PRIVATE KEY-----\nMIIBvAIBAAKBgQD9uUQuvZluLdPY6Wop3uWUR5jrajSWtp27Ea0rGZLxl8Wye6I8\nbMm1n+JAACOBqbBjM5axa5TBKR0H3myQJ/sycBkvpaEe0/oDc/CaFSIDxfXWm11c\npnMoJeBQf7DQJleFytzo70ltTQinUpMGWa/4M+SQaNagIkAyjA/4pFqqvwIVAL+s\n+bMWjOkJ4tsVDKQ5oMYVlqppAoGBAMNYSQGEM5/2W6Bf8Vwx8z+PbkaSCKMmkn1M\n+Vt/i6YvwgZDiBmhjrorcHKU38wUmSA2fTkbPX6uaAYdvSSXAHQTYFV3804Uohrw\nYFOFCI+NTJRuDxukrfPFVwstJ4hF1po0pHXFt4WdGotEjpVccRJGJ3oO0R/Unofl\nN60WnK4eAoGAOX41OCxx8mTuxpON/uZn6GwvK/m0K9fr/UmIX8D4Mp8PgnPLC71A\nOwLy1HrCVi3ohCqeSY2C1uf1VWUVlSqMH85Pxc7pLtuULoQdCgiYt1agVrioFSP6\nbEyFdV8vGxA4YGh6cUSkeFZBJBrdNM4VmYBeT+3n/IO5uUbWoPK5iAoCFQCdYU1l\nO1pCZ3Jhf/YDAAnfQHAtMxAQEBAQEBAQEBAQEBAQEBA=\n-----END DSA PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/openssh_old_ecdsa b/test/fixtures/keyParser/openssh_old_ecdsa new file mode 100644 index 00000000..f4170ac1 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_ecdsa @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJx7zPcbJg1zUAsBhKbmN0eOjbr+/W2qGSZTCP/c0mz4oAoGCCqGSM49 +AwEHoUQDQgAELN85t86lbEONGsyPNDxD/P2f9D9/ePBT3ZpAeVYUdyrVO00jO4JE +FPfKlVc4htC9oZbDaNeW1ssAIbn4uzigMQ== +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_old_ecdsa.pub b/test/fixtures/keyParser/openssh_old_ecdsa.pub new file mode 100644 index 00000000..8f39dd68 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_ecdsa.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCzfObfOpWxDjRrMjzQ8Q/z9n/Q/f3jwU92aQHlWFHcq1TtNIzuCRBT3ypVXOIbQvaGWw2jXltbLACG5+Ls4oDE= old openssh format diff --git a/test/fixtures/keyParser/openssh_old_ecdsa.pub.result b/test/fixtures/keyParser/openssh_old_ecdsa.pub.result new file mode 100644 index 00000000..68f3c147 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_ecdsa.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ecdsa-sha2-nistp256", + "comment": "old openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELN85t86lbEONGsyPNDxD/P2f9D9/\nePBT3ZpAeVYUdyrVO00jO4JEFPfKlVc4htC9oZbDaNeW1ssAIbn4uzigMQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCzfObfOpWxDjRrMjzQ8Q/z9n/Q/f3jwU92aQHlWFHcq1TtNIzuCRBT3ypVXOIbQvaGWw2jXltbLACG5+Ls4oDE=", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_old_ecdsa.result b/test/fixtures/keyParser/openssh_old_ecdsa.result new file mode 100644 index 00000000..cfabd13a --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_ecdsa.result @@ -0,0 +1,7 @@ +{ + "type": "ecdsa-sha2-nistp256", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELN85t86lbEONGsyPNDxD/P2f9D9/\nePBT3ZpAeVYUdyrVO00jO4JEFPfKlVc4htC9oZbDaNeW1ssAIbn4uzigMQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCzfObfOpWxDjRrMjzQ8Q/z9n/Q/f3jwU92aQHlWFHcq1TtNIzuCRBT3ypVXOIbQvaGWw2jXltbLACG5+Ls4oDE=", + "private": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIJx7zPcbJg1zUAsBhKbmN0eOjbr+/W2qGSZTCP/c0mz4oAoGCCqGSM49\nAwEHoUQDQgAELN85t86lbEONGsyPNDxD/P2f9D9/ePBT3ZpAeVYUdyrVO00jO4JE\nFPfKlVc4htC9oZbDaNeW1ssAIbn4uzigMQ==\n-----END EC PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/openssh_old_ecdsa_enc b/test/fixtures/keyParser/openssh_old_ecdsa_enc new file mode 100644 index 00000000..7e118d8d --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_ecdsa_enc @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,4BE217089AE8B7311672C159E0690AB4 + +AkqjOP53cDHrdkJFRVLHYS7fSPVcIa4BgKegLwqRUqJOvEOnn5j6RYCh2CMdPjwN +rdw26Gc0V++xeMISAbrX4TGAQPWyDyiuoCffTIAfbkNq8YQR/sNJjNmZEgtCs6+O +4iBQ8TMXO+7oWRC221FDbTIhB6k4lXXph/HzdW0/Y2A= +-----END EC PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub b/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub new file mode 100644 index 00000000..8efc1fe3 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA4KgjqWJj9PR55PeF7t7PTXdx7cvMDqNkq4UTMjoXA5WtQYdoC2sxJnI5Psqvtrfa13C31gY8TlFAZ1cClnoBk= diff --git a/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub.result b/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub.result new file mode 100644 index 00000000..3b3064ff --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub.result @@ -0,0 +1,8 @@ +{ + "type": "ecdsa-sha2-nistp256", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDgqCOpYmP09Hnk94Xu3s9Nd3Hty8\nwOo2SrhRMyOhcDla1Bh2gLazEmcjk+yq+2t9rXcLfWBjxOUUBnVwKWegGQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA4KgjqWJj9PR55PeF7t7PTXdx7cvMDqNkq4UTMjoXA5WtQYdoC2sxJnI5Psqvtrfa13C31gY8TlFAZ1cClnoBk=", + "private": null +} + diff --git a/test/fixtures/keyParser/openssh_old_ecdsa_enc.result b/test/fixtures/keyParser/openssh_old_ecdsa_enc.result new file mode 100644 index 00000000..423f6e20 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_ecdsa_enc.result @@ -0,0 +1,7 @@ +{ + "type": "ecdsa-sha2-nistp256", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDgqCOpYmP09Hnk94Xu3s9Nd3Hty8\nwOo2SrhRMyOhcDla1Bh2gLazEmcjk+yq+2t9rXcLfWBjxOUUBnVwKWegGQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA4KgjqWJj9PR55PeF7t7PTXdx7cvMDqNkq4UTMjoXA5WtQYdoC2sxJnI5Psqvtrfa13C31gY8TlFAZ1cClnoBk=", + "private": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIA7fGKE3wZkXb6jMcMriZujktUQ6FTC0SoTAa6fKDXY8oAoGCCqGSM49\nAwEHoUQDQgAEDgqCOpYmP09Hnk94Xu3s9Nd3Hty8wOo2SrhRMyOhcDla1Bh2gLaz\nEmcjk+yq+2t9rXcLfWBjxOUUBnVwKWegGQcHBwcHBwc=\n-----END EC PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/openssh_old_rsa b/test/fixtures/keyParser/openssh_old_rsa new file mode 100644 index 00000000..2eadbb0b --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA8wISnx2xWoeZur8yn/8NPykUY2mYyxn1n0mE5WJSo+mclFFS +mnN08WCt856AO8PMPuAn9cw0j3qJe0SKTnoMYSp+4fBsq6YHOGJvlRATF9SJkSIx +wBCYsMT+cf78vzhKRJrXAfJ/LWzV7b5gThHxj+Jby+fE/yePi8+Mb39UwYWGEFf+ +uRxcQIeuDX/VjPNtNKQPuO+HRi67WNXPFoUNmFXv1Ymn61S5duvVCxL8XdHXHdnZ +gIJ87CTlLBGPV/U9HrGJfl0AQ/jvMsvAV4IhMZMlV5QS2QigK7rkfBVe7k0NIWQ7 +Vwk5iunUpmUNVhKARdznvb8CJJm0ZEx4F2n8cQIDAQABAoIBAQCtZR46cSp6qWU1 +DnamGYyvM7W7lb6TtYtAxGnSb0z+bpPudPSXBqk8DrswqTlg674SY0nAJpyegFYX +Ifn6MzYgIv10ZGR2OjrOrdZmq5ikGWCrsZWEMZNyFq5kUwivvQ+pUj72wbyjghRH +1t7K9hzCiUbtAQzc77KKlWbkrBujFSp5EPNT67j5vV29WnZFbkPdUmfkM/ca/CZc +CWwvyAx19aFGyw3BsFhWQP5C9waT+QI9QZrVOA+8wTT11OcR6PT0oKdEmSYCKgHJ +JuYDWZ2XX2R2d5YNoxiqIZbCqQ/ayJuLOjLgQ1mx17pUyMNP3PoZCQXOi4jZWHZZ ++3/jqvJNAoGBAPmoL03KPvLVtHByEdxzPPfnonpYjfjlD4FvXgSQjdAcrTy4O06t +bDf4hMgUHQmDCyUakO45wyYwP0ISapQSBWniryjR/7U7/G/dX45fKRUeNoMvpmSC +qSEMAbd31Inpzuu5k0Y8p3hvoexeYlhbRkBL1ryx1LgIvC0TkWR+e6EvAoGBAPku +pHcpi3t2wewmP6f1krxtOLyvVt5RKaRjZ/2gNtzLPXL6ulQR5hufYlLKgyyyf2gJ +HxVFhCkfRjwVHV8qdIJc+Q4mjnjOeNfvqnzWOlSfZFegyWvOPW7hTX0/jZYGOb4I +7fzYyUPHnlu73twmshJMTzE1Ju7RdJXyLtg8xpRfAoGBAKjlyELXTWjZfP4Jnd3H +NHr+gSRGHp5A0RGe9zsdVGNz0xteA/mBR9JB1grJ2K8jsXmDlIMmHskKIPGhJetQ +mcr9qcRy9Yx1rZ08ZbYa2N9JllV/+hDLeII77jlh3y8CN5Ov81u0ExReaWxQmjXu +YgODix4TLLboae4Q6+7Rxu/PAoGAOZ04N7kqX/ygb+qUE1Crgde7I51i93pKp5C4 +baMKrFhtt9UTGfcdfkuG31+lnsMSxEo/npp5KUzq319+cA+P6sh2aXguvu32cO8g +O0cJK6HDAKPTjpKcD7QWR5xXL1X3KeJErI6vUnWoPsuchsiHqcVtFhKVEujpDPZ3 +MFY1D/8CgYBvv5mBb2kBf2/2JHp3lP/Q6LepEBkZk9dvoEU6/5xLvA5gEXR0MUj8 +g97Z1duGdXD/uEVRuRuOJkk4p8YmSM7t34st3lF06wdJUGcKvmZpp2ee+CdLwESi +GDCwcP5pcii56TVr09uHITWei4jFm+3Ye3h092dvPyNoEiJOgk2lsg== +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_old_rsa.pub b/test/fixtures/keyParser/openssh_old_rsa.pub new file mode 100644 index 00000000..1eaa7e03 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDzAhKfHbFah5m6vzKf/w0/KRRjaZjLGfWfSYTlYlKj6ZyUUVKac3TxYK3znoA7w8w+4Cf1zDSPeol7RIpOegxhKn7h8Gyrpgc4Ym+VEBMX1ImRIjHAEJiwxP5x/vy/OEpEmtcB8n8tbNXtvmBOEfGP4lvL58T/J4+Lz4xvf1TBhYYQV/65HFxAh64Nf9WM8200pA+474dGLrtY1c8WhQ2YVe/ViafrVLl269ULEvxd0dcd2dmAgnzsJOUsEY9X9T0esYl+XQBD+O8yy8BXgiExkyVXlBLZCKAruuR8FV7uTQ0hZDtXCTmK6dSmZQ1WEoBF3Oe9vwIkmbRkTHgXafxx old openssh format diff --git a/test/fixtures/keyParser/openssh_old_rsa.pub.result b/test/fixtures/keyParser/openssh_old_rsa.pub.result new file mode 100644 index 00000000..720438a0 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "old openssh format", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8wISnx2xWoeZur8yn/8N\nPykUY2mYyxn1n0mE5WJSo+mclFFSmnN08WCt856AO8PMPuAn9cw0j3qJe0SKTnoM\nYSp+4fBsq6YHOGJvlRATF9SJkSIxwBCYsMT+cf78vzhKRJrXAfJ/LWzV7b5gThHx\nj+Jby+fE/yePi8+Mb39UwYWGEFf+uRxcQIeuDX/VjPNtNKQPuO+HRi67WNXPFoUN\nmFXv1Ymn61S5duvVCxL8XdHXHdnZgIJ87CTlLBGPV/U9HrGJfl0AQ/jvMsvAV4Ih\nMZMlV5QS2QigK7rkfBVe7k0NIWQ7Vwk5iunUpmUNVhKARdznvb8CJJm0ZEx4F2n8\ncQIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDzAhKfHbFah5m6vzKf/w0/KRRjaZjLGfWfSYTlYlKj6ZyUUVKac3TxYK3znoA7w8w+4Cf1zDSPeol7RIpOegxhKn7h8Gyrpgc4Ym+VEBMX1ImRIjHAEJiwxP5x/vy/OEpEmtcB8n8tbNXtvmBOEfGP4lvL58T/J4+Lz4xvf1TBhYYQV/65HFxAh64Nf9WM8200pA+474dGLrtY1c8WhQ2YVe/ViafrVLl269ULEvxd0dcd2dmAgnzsJOUsEY9X9T0esYl+XQBD+O8yy8BXgiExkyVXlBLZCKAruuR8FV7uTQ0hZDtXCTmK6dSmZQ1WEoBF3Oe9vwIkmbRkTHgXafxx", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_old_rsa.result b/test/fixtures/keyParser/openssh_old_rsa.result new file mode 100644 index 00000000..affc996c --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8wISnx2xWoeZur8yn/8N\nPykUY2mYyxn1n0mE5WJSo+mclFFSmnN08WCt856AO8PMPuAn9cw0j3qJe0SKTnoM\nYSp+4fBsq6YHOGJvlRATF9SJkSIxwBCYsMT+cf78vzhKRJrXAfJ/LWzV7b5gThHx\nj+Jby+fE/yePi8+Mb39UwYWGEFf+uRxcQIeuDX/VjPNtNKQPuO+HRi67WNXPFoUN\nmFXv1Ymn61S5duvVCxL8XdHXHdnZgIJ87CTlLBGPV/U9HrGJfl0AQ/jvMsvAV4Ih\nMZMlV5QS2QigK7rkfBVe7k0NIWQ7Vwk5iunUpmUNVhKARdznvb8CJJm0ZEx4F2n8\ncQIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDzAhKfHbFah5m6vzKf/w0/KRRjaZjLGfWfSYTlYlKj6ZyUUVKac3TxYK3znoA7w8w+4Cf1zDSPeol7RIpOegxhKn7h8Gyrpgc4Ym+VEBMX1ImRIjHAEJiwxP5x/vy/OEpEmtcB8n8tbNXtvmBOEfGP4lvL58T/J4+Lz4xvf1TBhYYQV/65HFxAh64Nf9WM8200pA+474dGLrtY1c8WhQ2YVe/ViafrVLl269ULEvxd0dcd2dmAgnzsJOUsEY9X9T0esYl+XQBD+O8yy8BXgiExkyVXlBLZCKAruuR8FV7uTQ0hZDtXCTmK6dSmZQ1WEoBF3Oe9vwIkmbRkTHgXafxx", + "private": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA8wISnx2xWoeZur8yn/8NPykUY2mYyxn1n0mE5WJSo+mclFFS\nmnN08WCt856AO8PMPuAn9cw0j3qJe0SKTnoMYSp+4fBsq6YHOGJvlRATF9SJkSIx\nwBCYsMT+cf78vzhKRJrXAfJ/LWzV7b5gThHxj+Jby+fE/yePi8+Mb39UwYWGEFf+\nuRxcQIeuDX/VjPNtNKQPuO+HRi67WNXPFoUNmFXv1Ymn61S5duvVCxL8XdHXHdnZ\ngIJ87CTlLBGPV/U9HrGJfl0AQ/jvMsvAV4IhMZMlV5QS2QigK7rkfBVe7k0NIWQ7\nVwk5iunUpmUNVhKARdznvb8CJJm0ZEx4F2n8cQIDAQABAoIBAQCtZR46cSp6qWU1\nDnamGYyvM7W7lb6TtYtAxGnSb0z+bpPudPSXBqk8DrswqTlg674SY0nAJpyegFYX\nIfn6MzYgIv10ZGR2OjrOrdZmq5ikGWCrsZWEMZNyFq5kUwivvQ+pUj72wbyjghRH\n1t7K9hzCiUbtAQzc77KKlWbkrBujFSp5EPNT67j5vV29WnZFbkPdUmfkM/ca/CZc\nCWwvyAx19aFGyw3BsFhWQP5C9waT+QI9QZrVOA+8wTT11OcR6PT0oKdEmSYCKgHJ\nJuYDWZ2XX2R2d5YNoxiqIZbCqQ/ayJuLOjLgQ1mx17pUyMNP3PoZCQXOi4jZWHZZ\n+3/jqvJNAoGBAPmoL03KPvLVtHByEdxzPPfnonpYjfjlD4FvXgSQjdAcrTy4O06t\nbDf4hMgUHQmDCyUakO45wyYwP0ISapQSBWniryjR/7U7/G/dX45fKRUeNoMvpmSC\nqSEMAbd31Inpzuu5k0Y8p3hvoexeYlhbRkBL1ryx1LgIvC0TkWR+e6EvAoGBAPku\npHcpi3t2wewmP6f1krxtOLyvVt5RKaRjZ/2gNtzLPXL6ulQR5hufYlLKgyyyf2gJ\nHxVFhCkfRjwVHV8qdIJc+Q4mjnjOeNfvqnzWOlSfZFegyWvOPW7hTX0/jZYGOb4I\n7fzYyUPHnlu73twmshJMTzE1Ju7RdJXyLtg8xpRfAoGBAKjlyELXTWjZfP4Jnd3H\nNHr+gSRGHp5A0RGe9zsdVGNz0xteA/mBR9JB1grJ2K8jsXmDlIMmHskKIPGhJetQ\nmcr9qcRy9Yx1rZ08ZbYa2N9JllV/+hDLeII77jlh3y8CN5Ov81u0ExReaWxQmjXu\nYgODix4TLLboae4Q6+7Rxu/PAoGAOZ04N7kqX/ygb+qUE1Crgde7I51i93pKp5C4\nbaMKrFhtt9UTGfcdfkuG31+lnsMSxEo/npp5KUzq319+cA+P6sh2aXguvu32cO8g\nO0cJK6HDAKPTjpKcD7QWR5xXL1X3KeJErI6vUnWoPsuchsiHqcVtFhKVEujpDPZ3\nMFY1D/8CgYBvv5mBb2kBf2/2JHp3lP/Q6LepEBkZk9dvoEU6/5xLvA5gEXR0MUj8\ng97Z1duGdXD/uEVRuRuOJkk4p8YmSM7t34st3lF06wdJUGcKvmZpp2ee+CdLwESi\nGDCwcP5pcii56TVr09uHITWei4jFm+3Ye3h092dvPyNoEiJOgk2lsg==\n-----END RSA PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/openssh_old_rsa_enc b/test/fixtures/keyParser/openssh_old_rsa_enc new file mode 100644 index 00000000..e5b63986 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa_enc @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,1380F5ADA0E0B636860A6BF78D47D6B2 + +ICLCebZN4+91mTQwByj210FD3D7kCxFA6kZ5fZ1TG34RzGynOSgUSdxPaXBHO4hC +DjS7lv6vrtaXPxlz5MVAhb6n0+X1pZDu5Wh5xjtkOt9yt4YPNKkvDPazSFBTHDth +jURe/aCLwXa+N5g5v1G/asb5dufA96tiPD8jjsBPm6RPq/444jAnLkid6YwTRLCk +a+IZZ+sX8onOq2xJM5NhsJxCEp5yquQCdyjvBBEBk5PExvWDHz4BIkK0WDR86IX/ +j4baAbTREiwP+EmVw1uogijvS+9nWPv3dQrtWwNQQNdWE2jJnsuDv44VAh1QQD7A +Txz2Y2A6IyzQsDxr6fL4JidVZOeeOXagYOBceZMs4IdNVJ52LJ9fqWtH1Eavj0za +c9zLgFN547l/Uqc334BQTkWhA9zGNkYJo4GCl/zjL2C6ce9gp0l6aBwSRyBmfH21 +pxFYqO/LQOSTbupeGzkNOpeNm/XtdOHe2N+2fiMO8hqEr6tOR4dsEUVBLCWt3tpD +C4jT3TtXKvx4qPV5N0w/umgXDd01Npk02k+wiQRnPBczFYLfRpceV/7MGtttV86/ +Ldl2p00q+JB6TzkHfOa4dA8oZJAwz3RPwmGYt/riJS4JOIpQGCs9lU3zgi7NTt4L +T9YAlAP3fjG1n3vy1uktKfRa+AoSqgS/pTJgB2srs6vxw3kt/V825cVyzSfGdQM/ +2oinmrbMAs0tRpTiHyqc/FjVRTW/8+LoSv/o6ZpN2x4jxkW/7cBH0Pcytvw8svd2 +Q+h92cfHuiNWi5tuiUkeDfjWEXo6ssVFA2t1ebTOk5y2wwPVAURf/jVxhEpFm4iY +PCIqwCwNSs1S05zAaIQ39ltBETj4Y46715GuYKsqLDYhv2lAIAlWXWgQ/N0tYVS6 +Oi/Qp9XFSEeyym5vzX/2ck1SJePvHvHewiABJjYzq3wlvIdWE6V3tJ3MRXRhlKNf +4bG8caItSG1n/QIeguNZI+A2Pu8AIdKjjKsnVcn3mikuKfcCAICZwf6m3Bd+Al7G +lrsJxyPqhhbPN1/t0w30tu8QuTSV+uMx4ZCKoUc6yJMQRmoM0RJ626re51IT2ikk +gB4f3Ms1VbB176it2L/zbXUAaxeE7Cbdcp/5058ksbuE0yA7JB+a2vQHFqw8Xxsb +qdKc4m4jkCdvaA5oNnGoG4milYukp1WVCGJeLD7gspTHR5dDYsOQgHvkxu+Ukor+ +0+1yvf8R6pRJWMoV0VvNkuSBUqcx94A+xLaEYCkB78Koum8xlPA1OA7VVkMVjQSk +r6c/iANbNV20IVz1TBpg9J1rQOGisbE43yRkH+aMgHnAnhk+UgK584QOH6eJpGZT +yBwes57P1kgT5VubavoJbeZLL6B70Sn/sKoLzxxruzPKmsmufJNK9klB7lu5f4gj +lvKtuNaiWbux1+fQmU+05nM1WW7s5Nm9MVUCfS5RxUq8SRqC7W17ouHEssW6mJT1 +jHK1xhoxX05X/T0NfdPzbG7S7+DG18Q6jnyHb28LeKLXv33sEUrT5z7+Nx4JS4XM +ZeMzPdRgYJw9vLQSYdksj0cNBb8UpAiG410lICrbPGWJh9d2KzhNKlfk/vLHia1V +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_old_rsa_enc.pub b/test/fixtures/keyParser/openssh_old_rsa_enc.pub new file mode 100644 index 00000000..806ab915 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa_enc.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHLfm98g0aHjbQJcjbamutwkWTMY426a4IdwGrpAOv806h6wBXNcOj2VJbgeQ2/XkQ0RY78fGrHSacaadGsT9E5sRGyvkr/WtDpBokXrgpP15OvhfTaSMVTcty6qknndpu7P5nmSipdn9fQR9TyNRyAajhn+UINuquGfxyLL30W4IBqSISOcXKc0pScTdMOIOmkxxY+vQFydQpWF0a3TopKKa4b3sQJgqc0MJkREllT6U+0U4+YufoW6zZyMNIS2gxWUlGUiA5XveWSaYIXCaPQmps4WoO9AlrM7z1sTcG5yXn0kEUvTmBYUOUlffiBgXzArt4Pmm8gVklR5UH98y5 diff --git a/test/fixtures/keyParser/openssh_old_rsa_enc.pub.result b/test/fixtures/keyParser/openssh_old_rsa_enc.pub.result new file mode 100644 index 00000000..b4aad7b8 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa_enc.pub.result @@ -0,0 +1,8 @@ +{ + "type": "ssh-rsa", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxy35vfINGh420CXI22pr\nrcJFkzGONumuCHcBq6QDr/NOoesAVzXDo9lSW4HkNv15ENEWO/Hxqx0mnGmnRrE/\nRObERsr5K/1rQ6QaJF64KT9eTr4X02kjFU3LcuqpJ53abuz+Z5koqXZ/X0EfU8jU\ncgGo4Z/lCDbqrhn8ciy99FuCAakiEjnFynNKUnE3TDiDppMcWPr0BcnUKVhdGt06\nKSimuG97ECYKnNDCZERJZU+lPtFOPmLn6Fus2cjDSEtoMVlJRlIgOV73lkmmCFwm\nj0JqbOFqDvQJazO89bE3Bucl59JBFL05gWFDlJX34gYF8wK7eD5pvIFZJUeVB/fM\nuQIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDHLfm98g0aHjbQJcjbamutwkWTMY426a4IdwGrpAOv806h6wBXNcOj2VJbgeQ2/XkQ0RY78fGrHSacaadGsT9E5sRGyvkr/WtDpBokXrgpP15OvhfTaSMVTcty6qknndpu7P5nmSipdn9fQR9TyNRyAajhn+UINuquGfxyLL30W4IBqSISOcXKc0pScTdMOIOmkxxY+vQFydQpWF0a3TopKKa4b3sQJgqc0MJkREllT6U+0U4+YufoW6zZyMNIS2gxWUlGUiA5XveWSaYIXCaPQmps4WoO9AlrM7z1sTcG5yXn0kEUvTmBYUOUlffiBgXzArt4Pmm8gVklR5UH98y5", + "private": null +} + diff --git a/test/fixtures/keyParser/openssh_old_rsa_enc.result b/test/fixtures/keyParser/openssh_old_rsa_enc.result new file mode 100644 index 00000000..fddff0f2 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa_enc.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxy35vfINGh420CXI22pr\nrcJFkzGONumuCHcBq6QDr/NOoesAVzXDo9lSW4HkNv15ENEWO/Hxqx0mnGmnRrE/\nRObERsr5K/1rQ6QaJF64KT9eTr4X02kjFU3LcuqpJ53abuz+Z5koqXZ/X0EfU8jU\ncgGo4Z/lCDbqrhn8ciy99FuCAakiEjnFynNKUnE3TDiDppMcWPr0BcnUKVhdGt06\nKSimuG97ECYKnNDCZERJZU+lPtFOPmLn6Fus2cjDSEtoMVlJRlIgOV73lkmmCFwm\nj0JqbOFqDvQJazO89bE3Bucl59JBFL05gWFDlJX34gYF8wK7eD5pvIFZJUeVB/fM\nuQIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDHLfm98g0aHjbQJcjbamutwkWTMY426a4IdwGrpAOv806h6wBXNcOj2VJbgeQ2/XkQ0RY78fGrHSacaadGsT9E5sRGyvkr/WtDpBokXrgpP15OvhfTaSMVTcty6qknndpu7P5nmSipdn9fQR9TyNRyAajhn+UINuquGfxyLL30W4IBqSISOcXKc0pScTdMOIOmkxxY+vQFydQpWF0a3TopKKa4b3sQJgqc0MJkREllT6U+0U4+YufoW6zZyMNIS2gxWUlGUiA5XveWSaYIXCaPQmps4WoO9AlrM7z1sTcG5yXn0kEUvTmBYUOUlffiBgXzArt4Pmm8gVklR5UH98y5", + "private": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxy35vfINGh420CXI22prrcJFkzGONumuCHcBq6QDr/NOoesA\nVzXDo9lSW4HkNv15ENEWO/Hxqx0mnGmnRrE/RObERsr5K/1rQ6QaJF64KT9eTr4X\n02kjFU3LcuqpJ53abuz+Z5koqXZ/X0EfU8jUcgGo4Z/lCDbqrhn8ciy99FuCAaki\nEjnFynNKUnE3TDiDppMcWPr0BcnUKVhdGt06KSimuG97ECYKnNDCZERJZU+lPtFO\nPmLn6Fus2cjDSEtoMVlJRlIgOV73lkmmCFwmj0JqbOFqDvQJazO89bE3Bucl59JB\nFL05gWFDlJX34gYF8wK7eD5pvIFZJUeVB/fMuQIDAQABAoIBAGuSfhZDGyZm+Q2T\nypYONNekW7Uyh29K5640r9dGfqNRkb9LT2TKab4dSiiXz2yPmwolEpAPjIjw9oB1\nY11/rv8Eby8YwlgqxvrCL0hDS80jJ0j5y55nYwZHfMC00eTOkUFlh8Tl6BsWH5aP\ncl7q0So9kTtCAw1bs4WSDVCQr4q/x7fZRQWeWudi4IjnCv5vn1Pgot7XxDwdFNQG\nDrkUHvYXv0M2OCdl7YN0D/bHQon5ney0YU10mtqGbkcEmu0woykW1Bc539b9AoD3\nxI6LVyY6/OEwGu5ctKolIVJjsguwfLJ9WR7SenR5nTzjJyxMdSfXtXkKPX2NZxpO\nziNYnm0CgYEA/afEFBu5Ld/TjYatdf7ezZe9iDx6vBzWmMtwkhr3OHCzVP1OIaB0\nSTsCWrTdoLFTMOizUHjj71vX5v5G4aCgaMXQnSDf13mxrFzR36w5oyJOBLjkHhol\nf0ROO7QCXK1hjBAUvnKwLPQvx1CAkDB9z+cT/BJwRCarfeLhrd/sGEMCgYEAyQVN\nOGIdRVBs3Q/8dbtaz+7LOv6IBZm2y9TKHKmfBm1txAsgkqRl7cfVTyczgAZfS/RB\nzrAje5UA+phCSPtyb5B+K1i/eHw7xDZrw8wauAKY8ILSadS9ZA0mU+7XCqsWhNqN\nrvuB5dttsTDgyXnMxCbYqCWAcyKn8jBh1cDo5VMCgYBe3iMQnjnI9YCK2wb/LZ6o\n6Aqj7HK+7k44gUYN7vXtbwEzVTWmj/tN9DryL9kAI7IIhc+i1kPxnrkGFK3v7wJv\njSRzz/rH/SS9YU3BSQmZgNgLHhd7Rq4lhid4Xt/PR61HFDCd9gj8FyvTcMFUrD4x\nxqwLx92jL49OGs/rFueXPwKBgBi46jJQ/sCTj4/wc2AXVqfT+nKa8yedK/oNhX3Y\n7pHfy2wc4jimt1JzDSza6V6JahbxR1agGv0L6j7nkt9e7UgDQUEbfRDYVpFfEAnY\nhEC1MRIDRNV3MIOpilkwOoo5WF+mcV5f2C3ouqjcFgkxTZmiHWswkYeXb4g9owqi\n2wG5AoGAb6/btpj3Ql+qYXRUH/cWPlLeFbKiGaAJ+Kn5RwlEW8D//FjKuV7fL7BN\nhvaDJUpwP9klNRny3IK6FWuFI0KDup0nyrIbS07h2rOCl/+g2erDuS5sofpu2zWU\nZDArpSmpU9EF6S8CvbbZmYvWzYUhYD/sEqIR+KSowNM4PA7g7fwKCgoKCgoKCgoK\n-----END RSA PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/openssh_old_rsa_enc_aes256 b/test/fixtures/keyParser/openssh_old_rsa_enc_aes256 new file mode 100644 index 00000000..ceed0702 --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa_enc_aes256 @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,5C3291414FA514EE9647898A2D5315FD + +PaIGvR7qnyhxjz7RI4YEeua59B0XJm6cTW6xv2tbSVIC/njwljgo+UzXJLPUH1gv +suhNs8X+U9n0aCME6vFQvSJcloo6B1NLhGL1DVNhMAiGu+byCSJtq4cTFR5+7YUQ +HGB0a5vGBtHhL86TBeaBcKYfgCE4jo6/FAvzfk/eyy3S/oHXjMMUfCqo3WrBcgLu +SwhOEGd2k7vRp4gZzsCZy9xMM4Gykrgxc/hLzyvGQF8isF5Nz6xtPLNlW9xPdD48 +Fq5CTfljR4+7mM/kruJRysnbXEoYAaA8oZhZsRD5cw2tMOartA4qUieR9sKfrMo0 +Ci7OXNkdDK83OWGZs3NUYT/BzMmIPf982Ws46RHZLQOad3qTHvFdCUeQKwPJtQZM +D40xdbw462KQi+yr8+dPs9q3yxS5lr1rG+SmCAe/s+5Ta6E6VMb30Jb9FBP+91Z8 +6XxrF9jl67xaOwz/8rqUfiYm0C31YHiSsGlewe7lmvr4W+f47kn+lxEVXJD6UG+M +l/iJMZR39nr305K4GHG29NVS/9h1XC63/FZbL+50YCfavBikvEEDZoBKSKzs3Rlh +LZOTt6netyFuJtW0Z2CsYnvyyBioztD2yVGStS1MX652uDutFuqcBgm5FCdC1XDD +c5y83mH6ZWAjJSai8ap80XATO+xImb86paJ7u96mSAq7t+ziTCjlFTl3Mmtfoyrr +yy4IWSFIZ7BGdk2yeOR+kW+UNij5tS0J8s+Ug8hIh0ax9pvIB9opB0HmCVRg8reY +KUZNUwPlBtP2Y9dU63fKf0LNkzzMZduiqN8iD/lC771TxEU/tvl9cwr0rP3Shqm7 +UYkhPG+l0iXmX9fJwfJ+sfnT6zRfUKqeuN58YpoJ1zliv+4g9wDZWDzPlcktW3RU +CcZ9nKxRzQ9WppZzPoN0OMr+POt+S9hKufGfKP0D4pvIJ3KOGvk0A31iyQ5Ua6mI +emC67kES+3djSaLBeYax0AOzxnuHc/9dcC4meGzPy3RWRknxYxt0KMlo4zR42ZTR +Qh3eA1h6POEtrwsCMUD+tI3W6QwcCCmaJW6gfwZPSqPqbLwI21NVfUS++0V8qB1X +ugRUwN7gvX9rf+2jp+IjTVXai6xPkN4LqIX8jULYihKdR+tMm/sKTEGfc+peMrN9 +iljwsPztLzAIEjr4UppaLyhJJCp4BHdveVg0/uhgBqQTuMHZtKX31IHdDjeJc2n+ +iITqdh20lwLl6fwKsAl1oBf1GaRSOsd+oi2IyXqpfMpoXlo2q/r7ExPAzDXXg0bb +tA5Awa6Fndu5BR4e4UUDDVnj3AU175D4Nz6ZyXmCBC9nB6rvXXAa/gdh8+/HL1nS +7gDU9rws6dDRP3BAE7xM8QQ4VNaPikPPlikKNTY9rom32kKMmGD64dEPuZHl/i1i +gNcQfQoLDdwQJzqYcn9ZtcsE3hPJgqwO+wvK4MhnKlYXsRnGIQVNgEt9ler98Wap +eFKgl+Jf9z+T4uJka7yFzni1HQ1kvanYpI0w5ili46te/yPE96uzhRGqn1bu+/QI +e1rC5IMHu8cYV1a/baX8r9iwIXislq6fia5ivj0fmFTYbBJZ/M4LlY3zVXlJnIQj +iykB31s5faMTUE8CDdj+fr1Dc5ERPWgiI4PxXTquPUHq7B6dhsJ6RbYkgIoBhuam +Ok39LZ/R4nTiJY9VaJwQPvdrNnQEZ9lmMnh7d8jcsja4SVNFObC2ONQ2MVbr+dXd +jTHIlOF3aqWM0ZP4dh/Zg0rIy4koL1G88/vOpKAoVPZVmftqww63TPao3uny9nTK +BjwqzalPZwt0KFSoAGr3e8psQpCXwHoP98/GyE9NVBhR4X/I2IST4Sk0x62Gz1pg +ZrGpz7VDEySYAd8GBvdOuAawjmE41YwEofRrBb7ZPbwIxrO3ei/G8f5LBABRCmjO +ikzAk9mADCnfY08nfQ8mjZAIiTth6MuFG92TeJSi1W56p+krikVh0SUxEfXeR1CR +XOyshItaWQNx7OojB3P7JG7nrY046144oQuUb7mVUi4oQE+TxKLRvc0jLkWyKmjf +Ii+BZFHuU3lQiAarQ+mZEA0pXCGCT+NDZK5Eo4LBCrYBfaviWcWB6LQoovAF2T42 +zzx5qZP4ANS7l4SUCd5qS7/h3/ftCkLHTEcV0KBJ2n3blF6wdP/7C3wQD6AcRHWQ +132dnFlD/agQ6VOFjTg4hnw7BrUtIHATKlmMg36CtXtqJYnb9DQEQDo24dBNVNjM +W1Tpgw8xkyVb9kICiZoy+kXnqqkGOnLHT12h+/pgkJqZO61kfvwUeNAK0USG/h/L +TRop17tZjsg2O6R9/aS0SnyANBJ8xgxRAneWuX6ry7t9IDYOH1Ybcn4riLe+tgCM +YPpxTOCUw49dqdkTU6/n2vEXXIRPXIefxBhIk1bKelX/owIwe+3kNSL96HzEDvI9 +GwaLRxNgMLi5diI3yJmevantJJWIKUFhz3ud2viaSWNWdAvfVmEcb7APs67cJAr9 +4oKnhF4TuD2oowFnH3nykczXRqAqHn+N/XynH0QJrQDJYwnYWH/+5YFWaYwHACSK +ppOnWNqQ28zjA7w5GLI//rHRT0NBbFbVF7s0TQlBu0Rukc042eW692BX/2qVSCpZ +8aVjhBFFbnt4YmRSYlj/X7ICnddWqHSiRNy5RMokdMlZD1ELxYf2XkkZionTlT/E +hHSsA2c4HgP5Ep9svM7EWtFO7Melk2vjgXZxyELbLPZUQuYImuK2ziKqL05YH/lc +/yfAdXOKLWjrUgVBk8r6QqoAWP0iCTZDOmB7IwVu9nLClpnJDfT3kRrai2GIpg/D +1+eBAcr6aoRfpLTduW3EAit7SNzLZSkcBvaLkkqJ3tCPTq9GR05DzOaTGCf+vyve +kRNdTMSX5E6IH3xomKTIPZrEgOMEJpdsJ4wBCt2ginYZvHOFsn+YiLL212+rLOTH +qqSKZb0O5lxgQNjl62PJ9bzDEJkiuUinz83OlLAe7fSZiT3wwKG2x8PsZugRSk6K +txDqENJGzcDsX/MLNYdy6y2MnAWRYKNsmp0luX5Exw7L8ls8jUAMnd0DS0RFB0gN +14UzNa+ZO35Xr4vj8n2URnX9O2vs+A9jcDQU7TOp1/ejq3ISATrIAjeyqUHNZ+Xq +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub b/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub new file mode 100644 index 00000000..0df674aa --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDOHBVYZ041e+MqbjY+oSRiNNO1nDK1l0P6blyAyi4gwWfEOGSkqBZr+vCGnSj3/BNWsCcECbsG2TBMkoxCmXLc32rgQRz76/vON0gRaAzm+oi4N1hSSN7S/xX3XEwkH5OM/g9WKJvzerGejIExT9C6jzCvznlRdSTnntJrgwuf8ubyOfffXht66X/klC5+XeDI8SORiIr/E7q8QUpjcYYjgRgJHTjjh47xDGQOkcL+ceZb1/ufbU+4r7m8Ume/+fQuOTXcD13yKd1Na7auXMHL8Orh8YHvgwbFCVPOFDDEyReNgLLEaGGHJOuWaf55N+7J4CSDRkRqjz1tFqdqqHwdRr0/pJP77mjEe89Cx4iu+BDrT72/SPKo5bwDoXBa2TxSdoVHT9idjsUPDfwshD4eHtwyhrZkEiNY8Qp+F57I9MSgRT62zdO/vZ5wzEYDJC3DBXPw3owvpGdJEjSOhTD0rOFctNs/dKlwAXnU6QUAE1qgd9P+O3GIhYiPkrw3XsbF39VeouroYIbljv4KyDb+wQMk2U2wzUE/ZV/AVjV0OK/3colr43uxEhl6D48pWejzpQ6DMFKuL5pcxzZzIlBVmwwNsNb3DoWQ+a1gepyimp1ocUlv28JPqMMiarm/Kka86KY+fzcHUUPQXpz8R4edOLA2hQSudYdTcNmcAOpwMw== diff --git a/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub.result b/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub.result new file mode 100644 index 00000000..3f23304a --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub.result @@ -0,0 +1,6 @@ +{ "type": "ssh-rsa", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzhwVWGdONXvjKm42PqEk\nYjTTtZwytZdD+m5cgMouIMFnxDhkpKgWa/rwhp0o9/wTVrAnBAm7BtkwTJKMQply\n3N9q4EEc++v7zjdIEWgM5vqIuDdYUkje0v8V91xMJB+TjP4PViib83qxnoyBMU/Q\nuo8wr855UXUk557Sa4MLn/Lm8jn3314beul/5JQufl3gyPEjkYiK/xO6vEFKY3GG\nI4EYCR0444eO8QxkDpHC/nHmW9f7n21PuK+5vFJnv/n0Ljk13A9d8indTWu2rlzB\ny/Dq4fGB74MGxQlTzhQwxMkXjYCyxGhhhyTrlmn+eTfuyeAkg0ZEao89bRanaqh8\nHUa9P6ST++5oxHvPQseIrvgQ60+9v0jyqOW8A6FwWtk8UnaFR0/YnY7FDw38LIQ+\nHh7cMoa2ZBIjWPEKfheeyPTEoEU+ts3Tv72ecMxGAyQtwwVz8N6ML6RnSRI0joUw\n9KzhXLTbP3SpcAF51OkFABNaoHfT/jtxiIWIj5K8N17Gxd/VXqLq6GCG5Y7+Csg2\n/sEDJNlNsM1BP2VfwFY1dDiv93KJa+N7sRIZeg+PKVno86UOgzBSri+aXMc2cyJQ\nVZsMDbDW9w6FkPmtYHqcopqdaHFJb9vCT6jDImq5vypGvOimPn83B1FD0F6c/EeH\nnTiwNoUErnWHU3DZnADqcDMCAwEAAQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAACAQDOHBVYZ041e+MqbjY+oSRiNNO1nDK1l0P6blyAyi4gwWfEOGSkqBZr+vCGnSj3/BNWsCcECbsG2TBMkoxCmXLc32rgQRz76/vON0gRaAzm+oi4N1hSSN7S/xX3XEwkH5OM/g9WKJvzerGejIExT9C6jzCvznlRdSTnntJrgwuf8ubyOfffXht66X/klC5+XeDI8SORiIr/E7q8QUpjcYYjgRgJHTjjh47xDGQOkcL+ceZb1/ufbU+4r7m8Ume/+fQuOTXcD13yKd1Na7auXMHL8Orh8YHvgwbFCVPOFDDEyReNgLLEaGGHJOuWaf55N+7J4CSDRkRqjz1tFqdqqHwdRr0/pJP77mjEe89Cx4iu+BDrT72/SPKo5bwDoXBa2TxSdoVHT9idjsUPDfwshD4eHtwyhrZkEiNY8Qp+F57I9MSgRT62zdO/vZ5wzEYDJC3DBXPw3owvpGdJEjSOhTD0rOFctNs/dKlwAXnU6QUAE1qgd9P+O3GIhYiPkrw3XsbF39VeouroYIbljv4KyDb+wQMk2U2wzUE/ZV/AVjV0OK/3colr43uxEhl6D48pWejzpQ6DMFKuL5pcxzZzIlBVmwwNsNb3DoWQ+a1gepyimp1ocUlv28JPqMMiarm/Kka86KY+fzcHUUPQXpz8R4edOLA2hQSudYdTcNmcAOpwMw==", + "private": null +} diff --git a/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.result b/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.result new file mode 100644 index 00000000..8cf3c91a --- /dev/null +++ b/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.result @@ -0,0 +1,6 @@ +{ "type": "ssh-rsa", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzhwVWGdONXvjKm42PqEk\nYjTTtZwytZdD+m5cgMouIMFnxDhkpKgWa/rwhp0o9/wTVrAnBAm7BtkwTJKMQply\n3N9q4EEc++v7zjdIEWgM5vqIuDdYUkje0v8V91xMJB+TjP4PViib83qxnoyBMU/Q\nuo8wr855UXUk557Sa4MLn/Lm8jn3314beul/5JQufl3gyPEjkYiK/xO6vEFKY3GG\nI4EYCR0444eO8QxkDpHC/nHmW9f7n21PuK+5vFJnv/n0Ljk13A9d8indTWu2rlzB\ny/Dq4fGB74MGxQlTzhQwxMkXjYCyxGhhhyTrlmn+eTfuyeAkg0ZEao89bRanaqh8\nHUa9P6ST++5oxHvPQseIrvgQ60+9v0jyqOW8A6FwWtk8UnaFR0/YnY7FDw38LIQ+\nHh7cMoa2ZBIjWPEKfheeyPTEoEU+ts3Tv72ecMxGAyQtwwVz8N6ML6RnSRI0joUw\n9KzhXLTbP3SpcAF51OkFABNaoHfT/jtxiIWIj5K8N17Gxd/VXqLq6GCG5Y7+Csg2\n/sEDJNlNsM1BP2VfwFY1dDiv93KJa+N7sRIZeg+PKVno86UOgzBSri+aXMc2cyJQ\nVZsMDbDW9w6FkPmtYHqcopqdaHFJb9vCT6jDImq5vypGvOimPn83B1FD0F6c/EeH\nnTiwNoUErnWHU3DZnADqcDMCAwEAAQ==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAACAQDOHBVYZ041e+MqbjY+oSRiNNO1nDK1l0P6blyAyi4gwWfEOGSkqBZr+vCGnSj3/BNWsCcECbsG2TBMkoxCmXLc32rgQRz76/vON0gRaAzm+oi4N1hSSN7S/xX3XEwkH5OM/g9WKJvzerGejIExT9C6jzCvznlRdSTnntJrgwuf8ubyOfffXht66X/klC5+XeDI8SORiIr/E7q8QUpjcYYjgRgJHTjjh47xDGQOkcL+ceZb1/ufbU+4r7m8Ume/+fQuOTXcD13yKd1Na7auXMHL8Orh8YHvgwbFCVPOFDDEyReNgLLEaGGHJOuWaf55N+7J4CSDRkRqjz1tFqdqqHwdRr0/pJP77mjEe89Cx4iu+BDrT72/SPKo5bwDoXBa2TxSdoVHT9idjsUPDfwshD4eHtwyhrZkEiNY8Qp+F57I9MSgRT62zdO/vZ5wzEYDJC3DBXPw3owvpGdJEjSOhTD0rOFctNs/dKlwAXnU6QUAE1qgd9P+O3GIhYiPkrw3XsbF39VeouroYIbljv4KyDb+wQMk2U2wzUE/ZV/AVjV0OK/3colr43uxEhl6D48pWejzpQ6DMFKuL5pcxzZzIlBVmwwNsNb3DoWQ+a1gepyimp1ocUlv28JPqMMiarm/Kka86KY+fzcHUUPQXpz8R4edOLA2hQSudYdTcNmcAOpwMw==", + "private": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKgIBAAKCAgEAzhwVWGdONXvjKm42PqEkYjTTtZwytZdD+m5cgMouIMFnxDhk\npKgWa/rwhp0o9/wTVrAnBAm7BtkwTJKMQply3N9q4EEc++v7zjdIEWgM5vqIuDdY\nUkje0v8V91xMJB+TjP4PViib83qxnoyBMU/Quo8wr855UXUk557Sa4MLn/Lm8jn3\n314beul/5JQufl3gyPEjkYiK/xO6vEFKY3GGI4EYCR0444eO8QxkDpHC/nHmW9f7\nn21PuK+5vFJnv/n0Ljk13A9d8indTWu2rlzBy/Dq4fGB74MGxQlTzhQwxMkXjYCy\nxGhhhyTrlmn+eTfuyeAkg0ZEao89bRanaqh8HUa9P6ST++5oxHvPQseIrvgQ60+9\nv0jyqOW8A6FwWtk8UnaFR0/YnY7FDw38LIQ+Hh7cMoa2ZBIjWPEKfheeyPTEoEU+\nts3Tv72ecMxGAyQtwwVz8N6ML6RnSRI0joUw9KzhXLTbP3SpcAF51OkFABNaoHfT\n/jtxiIWIj5K8N17Gxd/VXqLq6GCG5Y7+Csg2/sEDJNlNsM1BP2VfwFY1dDiv93KJ\na+N7sRIZeg+PKVno86UOgzBSri+aXMc2cyJQVZsMDbDW9w6FkPmtYHqcopqdaHFJ\nb9vCT6jDImq5vypGvOimPn83B1FD0F6c/EeHnTiwNoUErnWHU3DZnADqcDMCAwEA\nAQKCAgEAtDuwmr6zkGeGbYs02i2VoF8rpssxOMRPCIZLU7/4+GHH+LmLoMTv3nrw\nq/ZwZfJDgvHFHG3Z45I2/y7DglWnMOgaEII/8zgX2OtUlQwVBEKfHeAf1sysNXwk\n3EsUth36rDdad/BI93AaNFgPfWybTRh77bCzO/0hSX4D6UoN90+0jqsMS4KCq0fW\ns660vYIgV/cuMExjp8y75XV+tFkIgLGZsBaisazP3ZAFSwxBpLk7RKMpAO/Y39qi\no8C3wxOzaFxU8dtqPqJHSDVh6TVproo9C1liU2yTleejJjlXsC1c8DtTmBbi+gfa\nD40enye/Iz9jDnx6xWf+wg8mVUmCRZGxRvjvS/CjIEwNB6EX5vSCe4oM8lOg2V7f\nFynGpRYlP3vkcelNly5qf7mlb/Wkd7F8PnJ8JTHdTcGSalMJ3DKn063jn5eYFUQm\na40NkVAlDYOdeQZumCY7+v9Vontx4+0IrXlZJWr9EYyAEm80LlwenJ2s/YoTRwqV\nTfpWPaEcrNlZUq/2A9JM0m91gLktQaRLRyNnBvFap9504aWnLrD8m7tCWkzFf4wJ\nA+v9yN+lCveJPmNi5nW3Pzd1Xy3n126BN+yH7VUWVARKs8ZOUdOUMAg6ZVM0GnKe\nOt7AEAG5jsmGA5UQywtbGYOMonKBQqsqoWNKWTbbqbNbe4FyEqkCggEBAOzuRMHh\nI58/iv/TjG/t32CLdWRDM6cVvycoZL37zqObpzXo1LJHr1iCqxrEZnSzX5DCW3e5\nMbr3dq2GL02RfDb9Nta16dy/V6VzwK9bdavk9+CzJFoIvMVDqEZ6mVtTwvGrd6Aa\nnrTJjZjDG0dwfNe9LuNLmLSVE8p7WSWjU0E5XB45y1m53pIoQbqsR6cJPkiUGO2S\nUaR+xzxHeHv82zTavpf3T5+O+6UX6SRG5lx5Tk4ucUPzzwrry0PVdRERyZhJTjC3\nlG449RSrr1UOdwGE3Kkz4zEL14L661nDWogf9Yc6xsatSHSzVVdN9UY63e1pb8DN\ndEaNu0qddORTsD0CggEBAN6yxtydA+YMJ02C70PJZId3BeTLOzyk5ZCHU8kEEIFl\nRK4jW1kTXpxito2sSwDLYWnjHExnQiLFaYsVAhaxEFhS9bXrm9SWCjk7pD8XwdQ5\nBsi8uNz62W9cTNCHszwLCZ0HWKfsoQjodEbbAs3XS+F60i/e1RKAMsDm38tmO6rU\nSE1+81fgPpLTdT3cnAtwF51rxlgVQWFiohLxrSFvWS0X5eEvnoXZhqLMwdwQgUob\nuKuFDhMMleP17wbmA0QGdSWmUEgshx8g0Fx++BLqAcvSveQCkRGSn4RAfLq6I38S\n8ERJbbW9c2LjBKU2YyiuqdLcw0hVUJf0bGrOqHIFuS8CggEBAN4Z4bSJk3YGAAwf\nSGfoady7/pi1cmcvuJhBgmah9SxjjlS35SMWleX33+Pgtlaxi2VM/Kd3oImuzr6N\nqiwhtHpr1gtiTk8Tw1qi7r6zktRHeKJX7DzGMgcNjGI9LSNymq0nWqVoLtw0kJri\nUuEeLrmia6DAze6CTSIjjQb+Wt4qohLnhJug5GbIfA080JJh1NP+mHukDQqRmb0F\nFonMF8UtRTt5p1dglr9FcdUC7ZFEWcZqPN1BYRXQwiPdprcQoJU0Kqr6fJbyp7Pv\n2RV30NFItf2bWV8xxZ3QD+1+dpBivSw+SfYWnHRhZB/KQaHLLx0OGKd5MYWt+SNS\nBTDAztUCggEAEGOJvPyVJ93nGo4zO4LbshhxR5gpQNpFxrAe22FAMbWZK1OQymph\ngRLGqoBueJ1/CsLa90h2Fob+sGyYXcEkGcvpJz0yl59/Gx0nhjkiW5Liy+0Pkbuo\nOsjJImOKLjYFvBepT5pbc4Nf40ME3s8kV3CpfTph5d2nXojfGWHprDW5KHLaR7JK\nCJYlP6s3lKPoKP6gdyUBTcBrewdsHVTLdEUY9syBVwpeHScCcUaJrUGDAqRlF8PK\nWB9mOtS8ksoC3wVxTQ5x1zmb++KgMQwlm7Fjph8GPAvVT29LfpZqPFTRd8ULnN9X\nWYSpd9sbywenmcwDVxRoPPaQ7/9LaDDBowKCAQEAsoY/cxW+RrzavJYUZW8Zcdq5\n1/JHEGzr08FMRR+3UbeH8kpFQkc4/e/pobz/6ZRQkCasYGYc+5wZquYanbVsyJME\nkWBVREJ22kr3vjyueqgk1KQqYmOToOYNLYYS4TB0io84+HZTqsUKWXzJETc1TI6b\nMqslgSqd/jpK/BMTUUT9IrbAP7oGQcdcQ48R95LbjWlx/Mqe5mwmsSCex6b6ZKyk\nMGQJ7BG2Fjljs1NiHB3rwc50/wvUBsZmqMNQm/1/t3Nc9LQzJkUVe4IUSlQ56eBZ\n8k0JExiCAooRJNwUPAb8+GU+adYN7b4oPDCmAEgICojoX2PNLjAj9T1v4xPPcAIC\n-----END RSA PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/ppk_dsa_enc b/test/fixtures/keyParser/ppk_dsa_enc new file mode 100644 index 00000000..915508bd --- /dev/null +++ b/test/fixtures/keyParser/ppk_dsa_enc @@ -0,0 +1,17 @@ +PuTTY-User-Key-File-2: ssh-dss +Encryption: aes256-cbc +Comment: dsa-key-20141202 +Public-Lines: 10 +AAAAB3NzaC1kc3MAAACBAJn2I8YefRo3BsEeinQt8KQ4cEyArAs7Y/W733oRSYOI +zWF1Ju124ysKrmg2okv+05CYcjV3Yp4AzQeomYAlgmB/7xCEnWaEnxCwAxmrrJMm +PrkwNjHOIi7yM5QOE90IM/Q+IJA4EPBfSb+Xr8fYhrp53KNHVSnc2KkOqpo2FsIj +AAAAFQC4NlP50GqyUqq2B82Vh/w5j3TzwQAAAIAeSGom9LLNdzcwCHnGfxKNnEz3 +55KITADTxiIpBvnQW+eDHwQvIw6V2Oc73bKCu5ZirZmIMW5w6KjQVwkuQBoF9Koq +/2u6VeevtL9pD6TBzSLMVw5pV3PmE4/C/eLiaUxZLIHdbzpqPkAvAUBrXKkj0ijz +cNzCp1fuF8H0pvR8yQAAAIAmvV+kqWhUgDYwNNz1qDaoS8XdsOponutZ/0stRQ66 +mKAy8kNVNNQ6oUx1XFl1WUt4iyFY/2Rz2fZhLz5/TbZRK5ygo666WgnxB/Ud4GAx +/BPQTghOJJOL00vJk+8jVCGNDc942V6nFXznDMXwqxhRCW6dm+2lTh7ntrli8mCk +5g== +Private-Lines: 1 +BytvbK+jNyMjiVxCO5lcE4YbW7q293oC+LZjkZ8Ajlw= +Private-MAC: c3da536ea28851fc32d5d1ff01498c8fcebc1170 diff --git a/test/fixtures/keyParser/ppk_dsa_enc.result b/test/fixtures/keyParser/ppk_dsa_enc.result new file mode 100644 index 00000000..68edda76 --- /dev/null +++ b/test/fixtures/keyParser/ppk_dsa_enc.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-dss", + "comment": "dsa-key-20141202", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBtjCCASsGByqGSM44BAEwggEeAoGBAJn2I8YefRo3BsEeinQt8KQ4cEyArAs7\nY/W733oRSYOIzWF1Ju124ysKrmg2okv+05CYcjV3Yp4AzQeomYAlgmB/7xCEnWaE\nnxCwAxmrrJMmPrkwNjHOIi7yM5QOE90IM/Q+IJA4EPBfSb+Xr8fYhrp53KNHVSnc\n2KkOqpo2FsIjAhUAuDZT+dBqslKqtgfNlYf8OY9088ECgYAeSGom9LLNdzcwCHnG\nfxKNnEz355KITADTxiIpBvnQW+eDHwQvIw6V2Oc73bKCu5ZirZmIMW5w6KjQVwku\nQBoF9Koq/2u6VeevtL9pD6TBzSLMVw5pV3PmE4/C/eLiaUxZLIHdbzpqPkAvAUBr\nXKkj0ijzcNzCp1fuF8H0pvR8yQOBhAACgYAmvV+kqWhUgDYwNNz1qDaoS8XdsOpo\nnutZ/0stRQ66mKAy8kNVNNQ6oUx1XFl1WUt4iyFY/2Rz2fZhLz5/TbZRK5ygo666\nWgnxB/Ud4GAx/BPQTghOJJOL00vJk+8jVCGNDc942V6nFXznDMXwqxhRCW6dm+2l\nTh7ntrli8mCk5g==\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1kc3MAAACBAJn2I8YefRo3BsEeinQt8KQ4cEyArAs7Y/W733oRSYOIzWF1Ju124ysKrmg2okv+05CYcjV3Yp4AzQeomYAlgmB/7xCEnWaEnxCwAxmrrJMmPrkwNjHOIi7yM5QOE90IM/Q+IJA4EPBfSb+Xr8fYhrp53KNHVSnc2KkOqpo2FsIjAAAAFQC4NlP50GqyUqq2B82Vh/w5j3TzwQAAAIAeSGom9LLNdzcwCHnGfxKNnEz355KITADTxiIpBvnQW+eDHwQvIw6V2Oc73bKCu5ZirZmIMW5w6KjQVwkuQBoF9Koq/2u6VeevtL9pD6TBzSLMVw5pV3PmE4/C/eLiaUxZLIHdbzpqPkAvAUBrXKkj0ijzcNzCp1fuF8H0pvR8yQAAAIAmvV+kqWhUgDYwNNz1qDaoS8XdsOponutZ/0stRQ66mKAy8kNVNNQ6oUx1XFl1WUt4iyFY/2Rz2fZhLz5/TbZRK5ygo666WgnxB/Ud4GAx/BPQTghOJJOL00vJk+8jVCGNDc942V6nFXznDMXwqxhRCW6dm+2lTh7ntrli8mCk5g==", + "private": "-----BEGIN DSA PRIVATE KEY-----\nMIIBugIBAAKBgQCZ9iPGHn0aNwbBHop0LfCkOHBMgKwLO2P1u996EUmDiM1hdSbt\nduMrCq5oNqJL/tOQmHI1d2KeAM0HqJmAJYJgf+8QhJ1mhJ8QsAMZq6yTJj65MDYx\nziIu8jOUDhPdCDP0PiCQOBDwX0m/l6/H2Ia6edyjR1Up3NipDqqaNhbCIwIVALg2\nU/nQarJSqrYHzZWH/DmPdPPBAoGAHkhqJvSyzXc3MAh5xn8SjZxM9+eSiEwA08Yi\nKQb50Fvngx8ELyMOldjnO92ygruWYq2ZiDFucOio0FcJLkAaBfSqKv9rulXnr7S/\naQ+kwc0izFcOaVdz5hOPwv3i4mlMWSyB3W86aj5ALwFAa1ypI9Io83DcwqdX7hfB\n9Kb0fMkCgYAmvV+kqWhUgDYwNNz1qDaoS8XdsOponutZ/0stRQ66mKAy8kNVNNQ6\noUx1XFl1WUt4iyFY/2Rz2fZhLz5/TbZRK5ygo666WgnxB/Ud4GAx/BPQTghOJJOL\n00vJk+8jVCGNDc942V6nFXznDMXwqxhRCW6dm+2lTh7ntrli8mCk5gIUCJZKAMAz\nkyr2vl2Pe48adi8Vs9s=\n-----END DSA PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/ppk_rsa b/test/fixtures/keyParser/ppk_rsa new file mode 100644 index 00000000..4504f18c --- /dev/null +++ b/test/fixtures/keyParser/ppk_rsa @@ -0,0 +1,26 @@ +PuTTY-User-Key-File-2: ssh-rsa +Encryption: none +Comment: rsa-key-20150522 +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAABJQAAAQB1quqP0rhl78NOLD4lj+1x5FGAqZ3aqo6GiEPz +KOaQmy86FuJMK0nHj3gUKTa/Kvaa+8PZyeu+uVseHg47YrynCOcJEEnpqvbArc8M +xMWuUnTUMrjvokGDOBBiQu4UAE4bybpgXkNHJfbrcDVgivmv3Ikn8PVIZ1rLBMLZ +6Lzn0rjPjFD0X4WqsAJW2SFiZnsjMZtVL2TWadNTyyfjjm2NCRBvd32VLohkSe9Q +BZBD6MW8YQyBKUnEF/7WNY0eehDVrfx1YqPOV1bDwFUhRaAYpLDLDR0KCAPvx7qb +8G5Cq0TIBsEr3H8ztNRcOTQoaKgn0T18M7cyS4ykoNLYW4Zx +Private-Lines: 14 +AAABACyF3DZraF3sBLXLjSL4MFSblHXfUHxAiPSiQzlpa/9dUCPRTrUJddzOgHZU +yJtcXU9mLm4VDRe7wZyxbSs6Hd5WZUGzIuLLEUH8k4hKdE/MLDSdkhV7qhX5iaij +tAeRaammRoVUGXTd7rnzGx2cXnnkvkZ22VmqkQ6MLg1DTmWNfOO9cdwFGdQawf/n +yUV0nTkWsHXy5Qrozq9wRFk8eyw+pFllxqavsNftZX8VDiQt27JLZPTU4LGkH660 +3gq1KhNS/l05TlXnMZGjlcPN8UEaBzmCWRezhJSttjs5Kgp1K3yDf4ozMR/HWOCj +Jq8fd3VIgli6ML8yjr/c0A0T9MUAAACBAL1/byxHiCvY/2C+/L5T+ZZq13jdZuYK +MmOFaNITgEdNGWSIFYRzhLKGXj7awQWOIW6chj470GNOfQjFL1TvXhbwfqW6esDa +kETOYQPYQHZijABcn7uurMUm/bu5x/z9gYkAfniOCI5vmvMvJ09JcZ0iUmFWDZZY +fAutBvrt+n/vAAAAgQCe9jrA51wn1/wzKmWF+2+OWFUG9usheIcEbHB8mxLguLfU ++x4i+2vLo0FtXEPAw+Bt7Tge4t0m6USiVZXtW/QKsh0kMj4mNVHFz+XXw4l1QOYv +n5TjnLepiP7majXv4GHI2eOcHkyly4sIkj4jNLYqvT86hMxW4IC+jtJEWhn/nwAA +AIEAlJ8cExu2WrWukTDJQHrVegtvdJUhNjol2wLucPuWwSxKuB8FHYwaPRYRkf3d +DkZ53hhjJZ0BVkAaQ28uqM09xKD+q1H4/r0nnbtlV4uHLl3cCD5mGrH8I/iDPJX4 +fFIqCa0+n1D6RzvDqs1QIu+PGSp0K6vHOOS5fP0ZpuT025E= +Private-MAC: 4ca26008c85b901f4d2766b0924c25e527678d7e diff --git a/test/fixtures/keyParser/ppk_rsa.result b/test/fixtures/keyParser/ppk_rsa.result new file mode 100644 index 00000000..9ac4d2fe --- /dev/null +++ b/test/fixtures/keyParser/ppk_rsa.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "rsa-key-20150522", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBHzANBgkqhkiG9w0BAQEFAAOCAQwAMIIBBwKCAQB1quqP0rhl78NOLD4lj+1x\n5FGAqZ3aqo6GiEPzKOaQmy86FuJMK0nHj3gUKTa/Kvaa+8PZyeu+uVseHg47Yryn\nCOcJEEnpqvbArc8MxMWuUnTUMrjvokGDOBBiQu4UAE4bybpgXkNHJfbrcDVgivmv\n3Ikn8PVIZ1rLBMLZ6Lzn0rjPjFD0X4WqsAJW2SFiZnsjMZtVL2TWadNTyyfjjm2N\nCRBvd32VLohkSe9QBZBD6MW8YQyBKUnEF/7WNY0eehDVrfx1YqPOV1bDwFUhRaAY\npLDLDR0KCAPvx7qb8G5Cq0TIBsEr3H8ztNRcOTQoaKgn0T18M7cyS4ykoNLYW4Zx\nAgEl\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAABJQAAAQB1quqP0rhl78NOLD4lj+1x5FGAqZ3aqo6GiEPzKOaQmy86FuJMK0nHj3gUKTa/Kvaa+8PZyeu+uVseHg47YrynCOcJEEnpqvbArc8MxMWuUnTUMrjvokGDOBBiQu4UAE4bybpgXkNHJfbrcDVgivmv3Ikn8PVIZ1rLBMLZ6Lzn0rjPjFD0X4WqsAJW2SFiZnsjMZtVL2TWadNTyyfjjm2NCRBvd32VLohkSe9QBZBD6MW8YQyBKUnEF/7WNY0eehDVrfx1YqPOV1bDwFUhRaAYpLDLDR0KCAPvx7qb8G5Cq0TIBsEr3H8ztNRcOTQoaKgn0T18M7cyS4ykoNLYW4Zx", + "private": "-----BEGIN RSA PRIVATE KEY-----\nMIIEoAIBAAKCAQB1quqP0rhl78NOLD4lj+1x5FGAqZ3aqo6GiEPzKOaQmy86FuJM\nK0nHj3gUKTa/Kvaa+8PZyeu+uVseHg47YrynCOcJEEnpqvbArc8MxMWuUnTUMrjv\nokGDOBBiQu4UAE4bybpgXkNHJfbrcDVgivmv3Ikn8PVIZ1rLBMLZ6Lzn0rjPjFD0\nX4WqsAJW2SFiZnsjMZtVL2TWadNTyyfjjm2NCRBvd32VLohkSe9QBZBD6MW8YQyB\nKUnEF/7WNY0eehDVrfx1YqPOV1bDwFUhRaAYpLDLDR0KCAPvx7qb8G5Cq0TIBsEr\n3H8ztNRcOTQoaKgn0T18M7cyS4ykoNLYW4ZxAgElAoIBACyF3DZraF3sBLXLjSL4\nMFSblHXfUHxAiPSiQzlpa/9dUCPRTrUJddzOgHZUyJtcXU9mLm4VDRe7wZyxbSs6\nHd5WZUGzIuLLEUH8k4hKdE/MLDSdkhV7qhX5iaijtAeRaammRoVUGXTd7rnzGx2c\nXnnkvkZ22VmqkQ6MLg1DTmWNfOO9cdwFGdQawf/nyUV0nTkWsHXy5Qrozq9wRFk8\neyw+pFllxqavsNftZX8VDiQt27JLZPTU4LGkH6603gq1KhNS/l05TlXnMZGjlcPN\n8UEaBzmCWRezhJSttjs5Kgp1K3yDf4ozMR/HWOCjJq8fd3VIgli6ML8yjr/c0A0T\n9MUCgYEAvX9vLEeIK9j/YL78vlP5lmrXeN1m5goyY4Vo0hOAR00ZZIgVhHOEsoZe\nPtrBBY4hbpyGPjvQY059CMUvVO9eFvB+pbp6wNqQRM5hA9hAdmKMAFyfu66sxSb9\nu7nH/P2BiQB+eI4Ijm+a8y8nT0lxnSJSYVYNllh8C60G+u36f+8CgYEAnvY6wOdc\nJ9f8MyplhftvjlhVBvbrIXiHBGxwfJsS4Li31PseIvtry6NBbVxDwMPgbe04HuLd\nJulEolWV7Vv0CrIdJDI+JjVRxc/l18OJdUDmL5+U45y3qYj+5mo17+BhyNnjnB5M\npcuLCJI+IzS2Kr0/OoTMVuCAvo7SRFoZ/58CgYBM0sw0i7O+v8F6P5blYFBtaZWf\ns7QYEfjAkAfmdpvJ4Px03Tkn284DL439zk5AhbqGydWO2fqJH9HTH4HkKbCF1x6W\nNtfRpLcHIzwWUMAv/nAb0oXyJDg0QD1Z8V7qBehn+UgHXCz7eVp+Q4x6FtsIWgW4\nwgWCI99DAPT98cZrNwKBgBEvYEw0sAROs3snLZHxqzH7tipENRhgDpixxvi20ZvA\n8Ud1GAPIJ1RJACd/mJ83xTxQ/yXvAybNHC05r1fuQ+V7ChHAPhH37SokUDMAYeMp\nnFgs7YBj+C4A+PZQq+KUzE0Qovwe84eLoFP0ImSrwKqsrOO7VFxrTDBGyp+bCbrF\nAoGBAJSfHBMbtlq1rpEwyUB61XoLb3SVITY6JdsC7nD7lsEsSrgfBR2MGj0WEZH9\n3Q5Ged4YYyWdAVZAGkNvLqjNPcSg/qtR+P69J527ZVeLhy5d3Ag+Zhqx/CP4gzyV\n+HxSKgmtPp9Q+kc7w6rNUCLvjxkqdCurxzjkuXz9Gabk9NuR\n-----END RSA PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/ppk_rsa_enc b/test/fixtures/keyParser/ppk_rsa_enc new file mode 100644 index 00000000..6f2f7f77 --- /dev/null +++ b/test/fixtures/keyParser/ppk_rsa_enc @@ -0,0 +1,18 @@ +PuTTY-User-Key-File-2: ssh-rsa +Encryption: aes256-cbc +Comment: rsa-key-20141119 +Public-Lines: 4 +AAAAB3NzaC1yc2EAAAABJQAAAIBrBWETAVAyJmuNG53jwTNDlbIcH5lrEvcx6lx5 +bM6EKg0XmOIH96VqUjS7eRRTTD9lpBA8hYhkrOjOx93/JWB/pcVN8/B3DYHshT9O +BW1DCkrNwut2pbJ2oZOBirhhAr+xqWFr3551FqbzaCIXpOKubr4EcIwCipBl6PxL +USfHgw== +Private-Lines: 8 +8O3NrBePR4+4RHHys8wrRKCmgx3Gsdz1cKoRJJDgnnrQxuAxBTVUlVTC2vzSOXrP +jlKdRP9DbtrL5YF8g9HkMPpzzTdgpiEAGikpIc+L0sJhN+S9VvMoXRRKqyuB7o1C +xZhAeRaZ68izdUUbFd7ajUwBNpGoFppOznGXyf/3/Ao9FfoTKReZzeBd/e2/JFhc +nsYkSbtWfKQBVXF1Fhr10UwRWSMaVJSDkcSuk8ghICoKBBCgRBnZFap0SR77oIJh +DKgmNFktoKzEqh111vYPhQyEEyGNxpD0aEPaGUJEjPEd3C5a46n7mIiqrNX7QJoo +xxZtkueGdXWaoe5mBf1tFc+nCA1l72nUlghJZooQhnO9NPpieu6NNZ8X+tFQ1Rq/ +xvOZHzpDOOeOgWdV7oAmRDbDjYPh0H67z2OKCFaP0Z9kgmnwqV2IJvTDrexj1VwY +6kFaPldnK+ohXl37oVIlWA== +Private-MAC: 9d09a15a122e48955682ba969d33c75ba8e4be2c diff --git a/test/fixtures/keyParser/ppk_rsa_enc.result b/test/fixtures/keyParser/ppk_rsa_enc.result new file mode 100644 index 00000000..f9ff9587 --- /dev/null +++ b/test/fixtures/keyParser/ppk_rsa_enc.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "rsa-key-20141119", + "public": "-----BEGIN PUBLIC KEY-----\nMIGcMA0GCSqGSIb3DQEBAQUAA4GKADCBhgKBgGsFYRMBUDIma40bnePBM0OVshwf\nmWsS9zHqXHlszoQqDReY4gf3pWpSNLt5FFNMP2WkEDyFiGSs6M7H3f8lYH+lxU3z\n8HcNgeyFP04FbUMKSs3C63alsnahk4GKuGECv7GpYWvfnnUWpvNoIhek4q5uvgRw\njAKKkGXo/EtRJ8eDAgEl\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAABJQAAAIBrBWETAVAyJmuNG53jwTNDlbIcH5lrEvcx6lx5bM6EKg0XmOIH96VqUjS7eRRTTD9lpBA8hYhkrOjOx93/JWB/pcVN8/B3DYHshT9OBW1DCkrNwut2pbJ2oZOBirhhAr+xqWFr3551FqbzaCIXpOKubr4EcIwCipBl6PxLUSfHgw==", + "private": "-----BEGIN RSA PRIVATE KEY-----\nMIICWQIBAAKBgGsFYRMBUDIma40bnePBM0OVshwfmWsS9zHqXHlszoQqDReY4gf3\npWpSNLt5FFNMP2WkEDyFiGSs6M7H3f8lYH+lxU3z8HcNgeyFP04FbUMKSs3C63al\nsnahk4GKuGECv7GpYWvfnnUWpvNoIhek4q5uvgRwjAKKkGXo/EtRJ8eDAgElAoGA\nU+GfHLvXEozQ1mHA8MfcEmCShL7SMVQN2wPL8HfgImYl7+aHpWE8de1nmdtwy6p2\n4PY2PUYQ9PY57i3zL8NZd8WQ7Rg0RBHDlndaFeF4Ef0uLboqYd/xN0rzfy55z7hW\nOL+8VhoxTrBUvveOhZwBPkOeHfxmkVz3xbbrg3kNlo0CQQDJYPKtCs/l46KJmN3l\nUANdI4QIuWQ+Zllz7p94FfdotnkvqG++Bp1wOqJSCih6UViwLfvpNZtGMCtk46WN\nhc0zAkEAiAyN4WUs/0x4WovG956J1A+uSEKeWzuqfpGGbWgZ9XfnPnk+1Al8FOW1\ntu9WWrMPIavQnZW/dXxhkeNWTH78cQJBALkM+qzZgMVpZO0ksDqA4H8Zt5lQafQm\nsxCWFf+le5CnraFqWNghwRsFcpCTtn486bamy89hsUdqiL2S6ygaFoECQFDk3r1e\nwM8mjMA3b2LM+AGMyH3+GPf59qwfLVXPMgeTZubgTt7w4f6WbAvoQS8Crw0aDVbH\nvfLUVbCwr9p1BM0CQFSBjCa/fzeICVkPFBaKQUmXjQ3IcPTOr90mSAiPnAAppSwT\nj5SYSfE9rSVb+EhQ0hk2VKWIfocNHBD1MAN9zb4=\n-----END RSA PRIVATE KEY-----" +} diff --git a/test/fixtures/keyParser/rfc4716_rsa.pub b/test/fixtures/keyParser/rfc4716_rsa.pub new file mode 100644 index 00000000..3bfd6e84 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa.pub @@ -0,0 +1,9 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +Comment: "2048-bit RSA" +AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKC +nbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCH +S/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1m +gJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW +61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc +9lK/C2jItA3fwq9PHfCM1D +---- END SSH2 PUBLIC KEY ---- diff --git a/test/fixtures/keyParser/rfc4716_rsa.pub.result b/test/fixtures/keyParser/rfc4716_rsa.pub.result new file mode 100644 index 00000000..fcc05534 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "2048-bit RSA", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4q6eZdx7LYh46PcZNcS3\nCnO7GuYsEJZeTj5LQSgp21IyTelaBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoO\nKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYq\nRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64\nYiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKX\nkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjN\nQwIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D", + "private": null +} diff --git a/test/fixtures/keyParser/rfc4716_rsa2.pub b/test/fixtures/keyParser/rfc4716_rsa2.pub new file mode 100644 index 00000000..e42f5c1c --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa2.pub @@ -0,0 +1,10 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +Comment: 2048-bit RSA +AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKC +nbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCH +S/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1m +gJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW +61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc +9lK/C2jItA3fwq9PHfCM1D +---- END SSH2 PUBLIC KEY ---- + diff --git a/test/fixtures/keyParser/rfc4716_rsa2.pub.result b/test/fixtures/keyParser/rfc4716_rsa2.pub.result new file mode 100644 index 00000000..fcc05534 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa2.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "2048-bit RSA", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4q6eZdx7LYh46PcZNcS3\nCnO7GuYsEJZeTj5LQSgp21IyTelaBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoO\nKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYq\nRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64\nYiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKX\nkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjN\nQwIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D", + "private": null +} diff --git a/test/fixtures/keyParser/rfc4716_rsa3.pub b/test/fixtures/keyParser/rfc4716_rsa3.pub new file mode 100644 index 00000000..24a107b0 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa3.pub @@ -0,0 +1,11 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +Comment: this is a special \ +multi-line comment\ + how cool is that not? +AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKC +nbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCH +S/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1m +gJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW +61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc +9lK/C2jItA3fwq9PHfCM1D +---- END SSH2 PUBLIC KEY ---- diff --git a/test/fixtures/keyParser/rfc4716_rsa3.pub.result b/test/fixtures/keyParser/rfc4716_rsa3.pub.result new file mode 100644 index 00000000..25dae671 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa3.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "this is a special multi-line comment how cool is that not?", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4q6eZdx7LYh46PcZNcS3\nCnO7GuYsEJZeTj5LQSgp21IyTelaBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoO\nKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYq\nRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64\nYiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKX\nkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjN\nQwIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D", + "private": null +} diff --git a/test/fixtures/keyParser/rfc4716_rsa4.pub b/test/fixtures/keyParser/rfc4716_rsa4.pub new file mode 100644 index 00000000..0454a858 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa4.pub @@ -0,0 +1,11 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +Comment: "this is a special \ +multi-line comment\ + how cool is that not?" +AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKC +nbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCH +S/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1m +gJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW +61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc +9lK/C2jItA3fwq9PHfCM1D +---- END SSH2 PUBLIC KEY ---- diff --git a/test/fixtures/keyParser/rfc4716_rsa4.pub.result b/test/fixtures/keyParser/rfc4716_rsa4.pub.result new file mode 100644 index 00000000..25dae671 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa4.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "this is a special multi-line comment how cool is that not?", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4q6eZdx7LYh46PcZNcS3\nCnO7GuYsEJZeTj5LQSgp21IyTelaBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoO\nKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYq\nRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64\nYiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKX\nkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjN\nQwIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D", + "private": null +} diff --git a/test/fixtures/keyParser/rfc4716_rsa5.pub b/test/fixtures/keyParser/rfc4716_rsa5.pub new file mode 100644 index 00000000..14608c11 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa5.pub @@ -0,0 +1,8 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKC +nbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCH +S/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1m +gJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW +61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc +9lK/C2jItA3fwq9PHfCM1D +---- END SSH2 PUBLIC KEY ---- diff --git a/test/fixtures/keyParser/rfc4716_rsa5.pub.result b/test/fixtures/keyParser/rfc4716_rsa5.pub.result new file mode 100644 index 00000000..a61d7792 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa5.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4q6eZdx7LYh46PcZNcS3\nCnO7GuYsEJZeTj5LQSgp21IyTelaBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoO\nKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYq\nRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64\nYiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKX\nkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjN\nQwIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D", + "private": null +} diff --git a/test/fixtures/keyParser/rfc4716_rsa6.pub b/test/fixtures/keyParser/rfc4716_rsa6.pub new file mode 100644 index 00000000..24a22612 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa6.pub @@ -0,0 +1,13 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +Subject: "nodejs" +x-foo: something\ +completely\ +different +Comment: "Foo bar baz" +AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKC +nbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCH +S/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1m +gJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW +61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc +9lK/C2jItA3fwq9PHfCM1D +---- END SSH2 PUBLIC KEY ---- diff --git a/test/fixtures/keyParser/rfc4716_rsa6.pub.result b/test/fixtures/keyParser/rfc4716_rsa6.pub.result new file mode 100644 index 00000000..ed7a4f52 --- /dev/null +++ b/test/fixtures/keyParser/rfc4716_rsa6.pub.result @@ -0,0 +1,7 @@ +{ + "type": "ssh-rsa", + "comment": "Foo bar baz", + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4q6eZdx7LYh46PcZNcS3\nCnO7GuYsEJZeTj5LQSgp21IyTelaBPprijnMwKa+pLQt5TEobpKFFNecPdT6oPoO\nKKMe6oH/pX0BNyAEB9KFZfZgh0v4J4IOiO0KHMBNkoTFeGrursPkqYRJ0HL4CqYq\nRdINy1sgDU6jUIOuDD5XZzlpDXb1ftZoCei9OHSWrMKbzibJc64JFM7tUoK6Vl64\nYiPgxsNXOJYMTrelVJYebtsNrJFmh3XXQABDVutWMYb8I6IrNs8zjxsf6c6N2tKX\nkk9G4EDKKip4g0bzDmD/fREPQ9vLi59N+ZsyjWCKKE3PZSvwtoyLQN38KvTx3wjN\nQwIDAQAB\n-----END PUBLIC KEY-----", + "publicSSH": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDirp5l3HstiHjo9xk1xLcKc7sa5iwQll5OPktBKCnbUjJN6VoE+muKOczApr6ktC3lMShukoUU15w91Pqg+g4oox7qgf+lfQE3IAQH0oVl9mCHS/gngg6I7QocwE2ShMV4au6uw+SphEnQcvgKpipF0g3LWyANTqNQg64MPldnOWkNdvV+1mgJ6L04dJaswpvOJslzrgkUzu1SgrpWXrhiI+DGw1c4lgxOt6VUlh5u2w2skWaHdddAAENW61Yxhvwjois2zzOPGx/pzo3a0peST0bgQMoqKniDRvMOYP99EQ9D28uLn035mzKNYIooTc9lK/C2jItA3fwq9PHfCM1D", + "private": null +} diff --git a/test/test-client-server.js b/test/test-client-server.js index 5bfd1257..fa476a11 100644 --- a/test/test-client-server.js +++ b/test/test-client-server.js @@ -1,133 +1,137 @@ -var Client = require('../lib/client'); -var Server = require('../lib/server'); -var OPEN_MODE = require('ssh2-streams').SFTPStream.OPEN_MODE; -var STATUS_CODE = require('ssh2-streams').SFTPStream.STATUS_CODE; -var utils = require('ssh2-streams').utils; - -var net = require('net'); -var fs = require('fs'); -var crypto = require('crypto'); -var path = require('path'); -var join = path.join; -var inspect = require('util').inspect; -var assert = require('assert'); - -var t = -1; -var group = path.basename(__filename, '.js') + '/'; -var fixturesdir = join(__dirname, 'fixtures'); - -var USER = 'nodejs'; -var PASSWORD = 'FLUXCAPACITORISTHEPOWER'; -var MD5_HOST_FINGERPRINT = '64254520742d3d0792e918f3ce945a64'; -var KEY_RSA_BAD = fs.readFileSync(join(fixturesdir, 'bad_rsa_private_key')); -var HOST_KEY_RSA = fs.readFileSync(join(fixturesdir, 'ssh_host_rsa_key')); -var HOST_KEY_DSA = fs.readFileSync(join(fixturesdir, 'ssh_host_dsa_key')); -var HOST_KEY_ECDSA = fs.readFileSync(join(fixturesdir, 'ssh_host_ecdsa_key')); -var CLIENT_KEY_ENC_RSA_RAW = fs.readFileSync(join(fixturesdir, 'id_rsa_enc')); -var CLIENT_KEY_ENC_RSA = utils.parseKey(CLIENT_KEY_ENC_RSA_RAW, 'foobarbaz'); -var CLIENT_KEY_PPK_RSA_RAW = fs.readFileSync(join(fixturesdir, 'id_rsa.ppk')); -var CLIENT_KEY_PPK_RSA = utils.parseKey(CLIENT_KEY_PPK_RSA_RAW); -var CLIENT_KEY_RSA_RAW = fs.readFileSync(join(fixturesdir, 'id_rsa')); -var CLIENT_KEY_RSA = utils.parseKey(CLIENT_KEY_RSA_RAW); -var CLIENT_KEY_RSA_NEW_RAW = - fs.readFileSync(join(fixturesdir, 'openssh_new_rsa')); -var CLIENT_KEY_RSA_NEW = utils.parseKey(CLIENT_KEY_RSA_NEW_RAW)[0]; -var CLIENT_KEY_DSA_RAW = fs.readFileSync(join(fixturesdir, 'id_dsa')); -var CLIENT_KEY_DSA = utils.parseKey(CLIENT_KEY_DSA_RAW); -var CLIENT_KEY_ECDSA_RAW = fs.readFileSync(join(fixturesdir, 'id_ecdsa')); -var CLIENT_KEY_ECDSA = utils.parseKey(CLIENT_KEY_ECDSA_RAW); -var DEBUG = false; -var DEFAULT_TEST_TIMEOUT = 30 * 1000; - -var tests = [ - { run: function() { - var client; - var server; - var r; - - r = setup( +// TODO: DRY shared code across tests and test files +'use strict'; + +const assert = require('assert'); +const { readFileSync } = require('fs'); +const { Socket } = require('net'); +const { join, basename } = require('path'); +const { inspect } = require('util'); + +const Client = require('../lib/client.js'); +const Server = require('../lib/server.js'); +const { parseKey } = require('../lib/protocol/keyParser.js'); +const { OPEN_MODE, STATUS_CODE } = require('../lib/protocol/SFTP.js'); + +const { mustCall, mustCallAtLeast, mustNotCall } = require('./common.js'); + +let t = -1; +const THIS_FILE = basename(__filename, '.js'); +const fixturesDir = join(__dirname, 'fixtures'); +const fixture = (file) => readFileSync(join(fixturesDir, file)); + +const USER = 'nodejs'; +const PASSWORD = 'FLUXCAPACITORISTHEPOWER'; +const MD5_HOST_FINGERPRINT = '64254520742d3d0792e918f3ce945a64'; +const KEY_RSA_BAD = fixture('bad_rsa_private_key'); +const HOST_KEY_RSA = fixture('ssh_host_rsa_key'); +const HOST_KEY_DSA = fixture('ssh_host_dsa_key'); +const HOST_KEY_ECDSA = fixture('ssh_host_ecdsa_key'); +const CLIENT_KEY_ENC_RSA_RAW = fixture('id_rsa_enc'); +const CLIENT_KEY_ENC_RSA = parseKey(CLIENT_KEY_ENC_RSA_RAW, 'foobarbaz'); +const CLIENT_KEY_PPK_RSA_RAW = fixture('id_rsa.ppk'); +const CLIENT_KEY_PPK_RSA = parseKey(CLIENT_KEY_PPK_RSA_RAW); +const CLIENT_KEY_RSA_RAW = fixture('id_rsa'); +const CLIENT_KEY_RSA = parseKey(CLIENT_KEY_RSA_RAW); +const CLIENT_KEY_RSA_NEW_RAW = fixture('openssh_new_rsa'); +const CLIENT_KEY_RSA_NEW = parseKey(CLIENT_KEY_RSA_NEW_RAW)[0]; +const CLIENT_KEY_DSA_RAW = fixture('id_dsa'); +const CLIENT_KEY_DSA = parseKey(CLIENT_KEY_DSA_RAW); +const CLIENT_KEY_ECDSA_RAW = fixture('id_ecdsa'); +const CLIENT_KEY_ECDSA = parseKey(CLIENT_KEY_ECDSA_RAW); +const DEBUG = false; +const DEFAULT_TEST_TIMEOUT = 30 * 1000; + +const tests = [ + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, privateKey: CLIENT_KEY_RSA_RAW }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ssh-rsa', - makeMsg('Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(), - ctx.key.data, - makeMsg('Public key mismatch')); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature, msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-rsa', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + } if (ctx.signature) { assert(CLIENT_KEY_RSA.verify(ctx.blob, ctx.signature) === true, - makeMsg('Could not verify PK signature')); - ctx.accept(); - } else - ctx.accept(); - }).on('ready', function() { + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Authenticate with an RSA key (old OpenSSH)' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, privateKey: CLIENT_KEY_RSA_NEW_RAW }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ssh-rsa', - makeMsg('Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_RSA_NEW.getPublicSSH(), - ctx.key.data, - makeMsg('Public key mismatch')); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature, msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-rsa', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_RSA_NEW.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + } if (ctx.signature) { assert(CLIENT_KEY_RSA_NEW.verify(ctx.blob, ctx.signature) === true, - makeMsg('Could not verify PK signature')); - ctx.accept(); - } else - ctx.accept(); - }).on('ready', function() { + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Authenticate with an RSA key (new OpenSSH)' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, privateKey: CLIENT_KEY_ENC_RSA_RAW, @@ -135,161 +139,176 @@ var tests = [ }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ssh-rsa', - makeMsg('Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_ENC_RSA.getPublicSSH(), - ctx.key.data, - makeMsg('Public key mismatch')); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature, msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-rsa', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_ENC_RSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + } if (ctx.signature) { assert(CLIENT_KEY_ENC_RSA.verify(ctx.blob, ctx.signature) === true, - makeMsg('Could not verify PK signature')); - ctx.accept(); - } else - ctx.accept(); - }).on('ready', function() { + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Authenticate with an encrypted RSA key' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, privateKey: CLIENT_KEY_PPK_RSA_RAW }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ssh-rsa', - makeMsg('Unexpected key algo: ' + ctx.key.algo)); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature, msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-rsa', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_PPK_RSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + } if (ctx.signature) { assert(CLIENT_KEY_PPK_RSA.verify(ctx.blob, ctx.signature) === true, - makeMsg('Could not verify PK signature')); - ctx.accept(); - } else - ctx.accept(); - }).on('ready', function() { + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Authenticate with an RSA key (PPK)' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, privateKey: CLIENT_KEY_DSA_RAW }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ssh-dss', - makeMsg('Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_DSA.getPublicSSH(), - ctx.key.data, - makeMsg('Public key mismatch')); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature, msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-dss', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_DSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + } if (ctx.signature) { assert(CLIENT_KEY_DSA.verify(ctx.blob, ctx.signature) === true, - makeMsg('Could not verify PK signature')); - ctx.accept(); - } else - ctx.accept(); - }).on('ready', function() { + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Authenticate with a DSA key' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, privateKey: CLIENT_KEY_ECDSA_RAW }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ecdsa-sha2-nistp256', - makeMsg('Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_ECDSA.getPublicSSH(), - ctx.key.data, - makeMsg('Public key mismatch')); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature, msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ecdsa-sha2-nistp256', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_ECDSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + } if (ctx.signature) { assert(CLIENT_KEY_ECDSA.verify(ctx.blob, ctx.signature) === true, - makeMsg('Could not verify PK signature')); - ctx.accept(); - } else - ctx.accept(); - }).on('ready', function() { + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Authenticate with a ECDSA key' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, password: 'asdf', @@ -299,66 +318,71 @@ var tests = [ }, { hostKeys: [HOST_KEY_DSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'password', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.password === 'asdf', - makeMsg('Unexpected password: ' + ctx.password)); - ctx.accept(); - }).on('ready', function() { + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 2: + assert(ctx.method === 'password', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.password === 'asdf', + msg(`Unexpected password: ${ctx.password}`)); + ctx.accept(); + break; + } + }, 2)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Server with DSA host key' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, - password: 'asdf' + password: 'asdf', + algorithms: { + serverHostKey: ['ecdsa-sha2-nistp256'] + }, }, { hostKeys: [HOST_KEY_ECDSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'password', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.password === 'asdf', - makeMsg('Unexpected password: ' + ctx.password)); - ctx.accept(); - }).on('ready', function() { + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 2: + assert(ctx.method === 'password', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.password === 'asdf', + msg(`Unexpected password: ${ctx.password}`)); + ctx.accept(); + break; + } + }, 2)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Server with ECDSA host key' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, password: 'asdf', @@ -368,33 +392,34 @@ var tests = [ }, { hostKeys: [HOST_KEY_RSA, HOST_KEY_DSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'password', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.password === 'asdf', - makeMsg('Unexpected password: ' + ctx.password)); - ctx.accept(); - }).on('ready', function() { + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 2: + assert(ctx.method === 'password', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.password === 'asdf', + msg(`Unexpected password: ${ctx.password}`)); + ctx.accept(); + break; + } + }, 2)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Server with multiple host keys (RSA selected)' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, password: 'asdf', @@ -404,35 +429,36 @@ var tests = [ }, { hostKeys: [HOST_KEY_RSA, HOST_KEY_DSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'password', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.password === 'asdf', - makeMsg('Unexpected password: ' + ctx.password)); - ctx.accept(); - }).on('ready', function() { + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 2: + assert(ctx.method === 'password', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.password === 'asdf', + msg(`Unexpected password: ${ctx.password}`)); + ctx.accept(); + break; + } + }, 2)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Server with multiple host keys (DSA selected)' }, - { run: function() { - var client; - var server; - var r; - var hostname = 'foo'; - var username = 'bar'; - - r = setup( + { run: mustCall(function(msg) { + const hostname = 'foo'; + const username = 'bar'; + const { server } = setup( this, { username: USER, privateKey: CLIENT_KEY_RSA_RAW, @@ -441,458 +467,397 @@ var tests = [ }, { hostKeys: [ HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method !== 'hostbased') - return ctx.reject(); - assert(ctx.method === 'hostbased', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ssh-rsa', - makeMsg('Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(), - ctx.key.data, - makeMsg('Public key mismatch')); - assert(ctx.signature, - makeMsg('Expected signature')); - assert(ctx.localHostname === hostname, - makeMsg('Wrong local hostname')); - assert(ctx.localUsername === username, - makeMsg('Wrong local username')); - assert(CLIENT_KEY_RSA.verify(ctx.blob, ctx.signature) === true, - makeMsg('Could not verify hostbased signature')); - ctx.accept(); - }).on('ready', function() { + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 2: + assert(ctx.method === 'publickey', + msg(`Wrong auth method: ${ctx.method}`)); + return ctx.reject(); + case 3: + assert(ctx.method === 'hostbased', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-rsa', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + assert(ctx.signature, + msg('Expected signature')); + assert(ctx.localHostname === hostname, + msg('Wrong local hostname')); + assert(ctx.localUsername === username, + msg('Wrong local username')); + assert(CLIENT_KEY_RSA.verify(ctx.blob, ctx.signature) === true, + msg('Could not verify hostbased signature')); + ctx.accept(); + break; + } + }, 3)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Authenticate with hostbased' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'password', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); - assert(ctx.password === PASSWORD, - makeMsg('Unexpected password: ' + ctx.password)); - ctx.accept(); - }).on('ready', function() { + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 2: + assert(ctx.method === 'password', + msg(`Wrong auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.password === PASSWORD, + msg(`Unexpected password: ${ctx.password}`)); + ctx.accept(); + break; + } + }, 2)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Authenticate with a password' }, - { run: function() { - var client; - var server; - var r; - var calls = 0; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, password: PASSWORD, privateKey: CLIENT_KEY_RSA_RAW, - authHandler: function(methodsLeft, partial, cb) { - assert(calls++ === 0, makeMsg('authHandler called multiple times')); - assert(methodsLeft === null, makeMsg('expected null methodsLeft')); - assert(partial === null, makeMsg('expected null partial')); + authHandler: mustCall((methodsLeft, partial, cb) => { + assert(methodsLeft === null, msg('expected null methodsLeft')); + assert(partial === null, msg('expected null partial')); return 'none'; - } + }) }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - var attempts = 0; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - assert(++attempts === 1, makeMsg('too many auth attempts')); + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { assert(ctx.method === 'none', - makeMsg('Unexpected auth method: ' + ctx.method)); + msg(`Wrong auth method: ${ctx.method}`)); ctx.accept(); - }).on('ready', function() { + })).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Custom authentication order (sync)' }, - { run: function() { - var client; - var server; - var r; - var calls = 0; - - r = setup( + { run: mustCall(function(msg) { + const { server } = setup( this, { username: USER, password: PASSWORD, privateKey: CLIENT_KEY_RSA_RAW, - authHandler: function(methodsLeft, partial, cb) { - assert(calls++ === 0, makeMsg('authHandler called multiple times')); - assert(methodsLeft === null, makeMsg('expected null methodsLeft')); - assert(partial === null, makeMsg('expected null partial')); - process.nextTick(cb, 'none'); - } + authHandler: mustCall((methodsLeft, partial, cb) => { + assert(methodsLeft === null, msg('expected null methodsLeft')); + assert(partial === null, msg('expected null partial')); + process.nextTick(mustCall(cb), 'none'); + }) }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - var attempts = 0; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - assert(++attempts === 1, makeMsg('too many auth attempts')); + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { assert(ctx.method === 'none', - makeMsg('Unexpected auth method: ' + ctx.method)); + msg(`Wrong auth method: ${ctx.method}`)); ctx.accept(); - }).on('ready', function() { + })).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Custom authentication order (async)' }, - { run: function() { - var client; - var server; - var r; - var cliError; - var calls = 0; - - r = setup( + { run: mustCall(function(msg) { + let cliError; + const { client, server } = setup( this, { username: USER, password: PASSWORD, privateKey: CLIENT_KEY_RSA_RAW, - authHandler: function(methodsLeft, partial, cb) { - assert(calls++ === 0, makeMsg('authHandler called multiple times')); - assert(methodsLeft === null, makeMsg('expected null methodsLeft')); - assert(partial === null, makeMsg('expected null partial')); + authHandler: mustCall((methodsLeft, partial, cb) => { + assert(methodsLeft === null, msg('expected null methodsLeft')); + assert(partial === null, msg('expected null partial')); return false; - } + }) }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; // Remove default client error handler added by `setup()` since we are // expecting an error in this case client.removeAllListeners('error'); - client.on('error', function(err) { + client.on('error', mustCall((err) => { cliError = err; assert.strictEqual(err.level, 'client-authentication'); assert(/configured authentication methods failed/i.test(err.message), - makeMsg('Wrong error message')); - }).on('close', function() { - assert(cliError, makeMsg('Expected client error')); - }); - - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - assert(false, makeMsg('should not see auth attempt')); - }).on('ready', function() { + msg('Wrong error message')); + })).on('close', mustCall(() => { + assert(cliError, msg('Expected client error')); + })); + + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustNotCall()) + .on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Custom authentication order (no methods)' }, - { run: function() { - var client; - var server; - var r; - var calls = 0; - - r = setup( + { run: mustCall(function(msg) { + let calls = 0; + const { server } = setup( this, { username: USER, password: PASSWORD, privateKey: CLIENT_KEY_RSA_RAW, - authHandler: function(methodsLeft, partial, cb) { - switch (calls++) { - case 0: + authHandler: mustCall((methodsLeft, partial, cb) => { + switch (++calls) { + case 1: assert(methodsLeft === null, - makeMsg('expected null methodsLeft')); - assert(partial === null, makeMsg('expected null partial')); + msg('expected null methodsLeft')); + assert(partial === null, msg('expected null partial')); return 'publickey'; - case 1: + case 2: assert.deepStrictEqual(methodsLeft, ['password'], - makeMsg('expected password method left' - + ', saw: ' + methodsLeft)); - assert(partial === true, makeMsg('expected partial success')); + msg('expected password method left' + + `, saw: ${methodsLeft}`)); + assert(partial === true, msg('expected partial success')); return 'password'; - default: - assert(false, makeMsg('authHandler called too many times')); } - } + }, 2) }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - var attempts = 0; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + let attempts = 0; + conn.on('authentication', mustCall((ctx) => { assert(++attempts === calls, - makeMsg('server<->client state mismatch')); + msg('server<->client state mismatch')); switch (calls) { case 1: assert(ctx.method === 'publickey', - makeMsg('Unexpected auth method: ' + ctx.method)); + msg(`Wrong auth method: ${ctx.method}`)); assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); + msg(`Unexpected username: ${ctx.username}`)); assert(ctx.key.algo === 'ssh-rsa', - makeMsg('Unexpected key algo: ' + ctx.key.algo)); + msg(`Unexpected key algo: ${ctx.key.algo}`)); assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(), ctx.key.data, - makeMsg('Public key mismatch')); + msg('Public key mismatch')); ctx.reject(['password'], true); break; case 2: assert(ctx.method === 'password', - makeMsg('Unexpected auth method: ' + ctx.method)); + msg(`Wrong auth method: ${ctx.method}`)); assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); + msg(`Unexpected username: ${ctx.username}`)); assert(ctx.password === PASSWORD, - makeMsg('Unexpected password: ' + ctx.password)); + msg(`Unexpected password: ${ctx.password}`)); ctx.accept(); break; - default: - assert(false, makeMsg('bad client auth state')); } - }).on('ready', function() { + }, 2)).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Custom authentication order (multi-step)' }, - { run: function() { - var client; - var server; - var r; - var verified = false; - - r = setup( + { run: mustCall(function(msg) { + let verified = false; + const { server } = setup( this, { username: USER, password: PASSWORD, hostHash: 'md5', - hostVerifier: function(hash) { + hostVerifier: (hash) => { assert(hash === MD5_HOST_FINGERPRINT, - makeMsg('Host fingerprint mismatch')); + msg('Host fingerprint mismatch')); return (verified = true); } }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { + })).on('ready', mustCall(() => { conn.end(); - }); - }).on('close', function() { - assert(verified, makeMsg('Failed to verify host fingerprint')); - }); - }, + })); + })).on('close', mustCall(() => { + assert(verified, msg('Failed to verify host fingerprint')); + })); + }), what: 'Verify host fingerprint' }, - { run: function() { - var client; - var server; - var r; - var out = ''; - var outErr = ''; - var exitArgs; - var closeArgs; - - r = setup( + { run: mustCall(function(msg) { + let out = ''; + let outErr = ''; + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - session.once('exec', function(accept, reject, info) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + session.once('exec', mustCall((accept, reject, info) => { assert(info.command === 'foo --bar', - makeMsg('Wrong exec command: ' + info.command)); - var stream = accept(); + msg(`Wrong exec command: ${info.command}`)); + const stream = accept(); stream.stderr.write('stderr data!\n'); stream.write('stdout data!\n'); stream.exit(100); stream.end(); conn.end(); - }); - }); - }); - }); - client.on('ready', function() { - client.exec('foo --bar', function(err, stream) { - assert(!err, makeMsg('Unexpected exec error: ' + err)); - stream.on('data', function(d) { + })); + })); + })); + })); + client.on('ready', mustCall(() => { + client.exec('foo --bar', mustCall((err, stream) => { + assert(!err, msg(`Unexpected exec error: ${err}`)); + stream.on('data', mustCallAtLeast((d) => { out += d; - }).on('exit', function(code) { - exitArgs = new Array(arguments.length); - for (var i = 0; i < exitArgs.length; ++i) - exitArgs[i] = arguments[i]; - }).on('close', function(code) { - closeArgs = new Array(arguments.length); - for (var i = 0; i < closeArgs.length; ++i) - closeArgs[i] = arguments[i]; - }).stderr.on('data', function(d) { + })).on('exit', mustCall((...args) => { + assert.deepStrictEqual(args, + [100], + msg(`Wrong exit args: ${inspect(args)}`)); + })).on('close', mustCall((...args) => { + assert.deepStrictEqual(args, + [100], + msg(`Wrong close args: ${inspect(args)}`)); + })).stderr.on('data', mustCallAtLeast((d) => { outErr += d; - }); - }); - }).on('end', function() { - assert.deepEqual(exitArgs, - [100], - makeMsg('Wrong exit args: ' + inspect(exitArgs))); - assert.deepEqual(closeArgs, - [100], - makeMsg('Wrong close args: ' + inspect(closeArgs))); + })); + })); + })).on('end', mustCall(() => { assert(out === 'stdout data!\n', - makeMsg('Wrong stdout data: ' + inspect(out))); + msg(`Wrong stdout data: ${inspect(out)}`)); assert(outErr === 'stderr data!\n', - makeMsg('Wrong stderr data: ' + inspect(outErr))); - }); - }, + msg(`Wrong stderr data: ${inspect(outErr)}`)); + })); + }), what: 'Simple exec' }, - { run: function() { - var client; - var server; - var r; - var serverEnv = {}; - var clientEnv = { SSH2NODETEST: 'foo' }; - - r = setup( + { run: mustCall(function(msg) { + const serverEnv = {}; + const clientEnv = { SSH2NODETEST: 'foo' }; + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - session.once('env', function(accept, reject, info) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + session.once('env', mustCall((accept, reject, info) => { serverEnv[info.key] = info.val; accept && accept(); - }).once('exec', function(accept, reject, info) { + })).once('exec', mustCall((accept, reject, info) => { assert(info.command === 'foo --bar', - makeMsg('Wrong exec command: ' + info.command)); - var stream = accept(); + msg(`Wrong exec command: ${info.command}`)); + const stream = accept(); stream.exit(100); stream.end(); conn.end(); - }); - }); - }); - }); - client.on('ready', function() { + })); + })); + })); + })); + client.on('ready', mustCall(() => { client.exec('foo --bar', { env: clientEnv }, - function(err, stream) { - assert(!err, makeMsg('Unexpected exec error: ' + err)); + mustCall((err, stream) => { + assert(!err, msg(`Unexpected exec error: ${err}`)); stream.resume(); - }); - }).on('end', function() { + })); + })).on('end', mustCall(() => { assert.deepEqual(serverEnv, clientEnv, - makeMsg('Environment mismatch')); - }); - }, + msg('Environment mismatch')); + })); + }), what: 'Exec with environment set' }, - { run: function() { - var client; - var server; - var r; - var out = ''; - - r = setup( + { run: mustCall(function(msg) { + let out = ''; + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - var ptyInfo; - session.once('pty', function(accept, reject, info) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + let ptyInfo; + session.once('pty', mustCall((accept, reject, info) => { ptyInfo = info; accept && accept(); - }).once('exec', function(accept, reject, info) { + })).once('exec', mustCall((accept, reject, info) => { assert(info.command === 'foo --bar', - makeMsg('Wrong exec command: ' + info.command)); - var stream = accept(); + msg(`Wrong exec command: ${info.command}`)); + const stream = accept(); stream.write(JSON.stringify(ptyInfo)); stream.exit(100); stream.end(); conn.end(); - }); - }); - }); - }); - var pty = { + })); + })); + })); + })); + const pty = { rows: 2, cols: 4, width: 0, @@ -900,30 +865,26 @@ var tests = [ term: 'vt220', modes: {} }; - client.on('ready', function() { + client.on('ready', mustCall(() => { client.exec('foo --bar', { pty: pty }, - function(err, stream) { - assert(!err, makeMsg('Unexpected exec error: ' + err)); - stream.on('data', function(d) { + mustCall((err, stream) => { + assert(!err, msg(`Unexpected exec error: ${err}`)); + stream.on('data', mustCallAtLeast((d) => { out += d; - }); - }); - }).on('end', function() { + })); + })); + })).on('end', mustCall(() => { assert.deepEqual(JSON.parse(out), pty, - makeMsg('Wrong stdout data: ' + inspect(out))); - }); - }, + msg(`Wrong stdout data: ${inspect(out)}`)); + })); + }), what: 'Exec with pty set' }, - { run: function() { - var client; - var server; - var r; - var out = ''; - - r = setup( + { run: mustCall(function(msg) { + let out = ''; + const { client, server } = setup( this, { username: USER, password: PASSWORD, @@ -931,797 +892,683 @@ var tests = [ }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - var authAgentReq = false; - session.once('auth-agent', function(accept, reject) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + let authAgentReq = false; + session.once('auth-agent', mustCall((accept, reject) => { authAgentReq = true; accept && accept(); - }).once('exec', function(accept, reject, info) { + })).once('exec', mustCall((accept, reject, info) => { assert(info.command === 'foo --bar', - makeMsg('Wrong exec command: ' + info.command)); - var stream = accept(); + msg(`Wrong exec command: ${info.command}`)); + const stream = accept(); stream.write(inspect(authAgentReq)); stream.exit(100); stream.end(); conn.end(); - }); - }); - }); - }); - client.on('ready', function() { + })); + })); + })); + })); + client.on('ready', mustCall(() => { client.exec('foo --bar', { agentForward: true }, - function(err, stream) { - assert(!err, makeMsg('Unexpected exec error: ' + err)); - stream.on('data', function(d) { + mustCall((err, stream) => { + assert(!err, msg(`Unexpected exec error: ${err}`)); + stream.on('data', mustCallAtLeast((d) => { out += d; - }); - }); - }).on('end', function() { + })); + })); + })).on('end', mustCall(() => { assert(out === 'true', - makeMsg('Wrong stdout data: ' + inspect(out))); - }); - }, + msg(`Wrong stdout data: ${inspect(out)}`)); + })); + }), what: 'Exec with OpenSSH agent forwarding' }, - { run: function() { - var client; - var server; - var r; - var out = ''; - - r = setup( + { run: mustCall(function(msg) { + let out = ''; + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - var x11 = false; - session.once('x11', function(accept, reject, info) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + let x11 = false; + session.once('x11', mustCall((accept, reject, info) => { assert.strictEqual(info.single, false, - makeMsg('Wrong client x11.single: ' + msg('Wrong client x11.single: ' + info.single)); assert.strictEqual(info.screen, 0, - makeMsg('Wrong client x11.screen: ' + msg('Wrong client x11.screen: ' + info.screen)); assert.strictEqual(info.protocol, 'MIT-MAGIC-COOKIE-1', - makeMsg('Wrong client x11.protocol: ' + msg('Wrong client x11.protocol: ' + info.protocol)); assert.strictEqual(info.cookie.length, 32, - makeMsg('Invalid client x11.cookie: ' + msg('Invalid client x11.cookie: ' + info.cookie)); x11 = true; accept && accept(); - }).once('exec', function(accept, reject, info) { + })).once('exec', mustCall((accept, reject, info) => { assert(info.command === 'foo --bar', - makeMsg('Wrong exec command: ' + info.command)); - var stream = accept(); - conn.x11('127.0.0.1', 4321, function(err, xstream) { - assert(!err, makeMsg('Unexpected x11() error: ' + err)); + msg(`Wrong exec command: ${info.command}`)); + const stream = accept(); + conn.x11('127.0.0.1', 4321, mustCall((err, xstream) => { + assert(!err, msg(`Unexpected x11() error: ${err}`)); xstream.resume(); - xstream.on('end', function() { + xstream.on('end', mustCall(() => { stream.write(JSON.stringify(x11)); stream.exit(100); stream.end(); conn.end(); - }).end(); - }); - }); - }); - }); - }); - client.on('ready', function() { - client.on('x11', function(info, accept, reject) { + })).end(); + })); + })); + })); + })); + })); + client.on('ready', mustCall(() => { + client.on('x11', mustCall((info, accept, reject) => { assert.strictEqual(info.srcIP, '127.0.0.1', - makeMsg('Invalid server x11.srcIP: ' + msg('Invalid server x11.srcIP: ' + info.srcIP)); assert.strictEqual(info.srcPort, 4321, - makeMsg('Invalid server x11.srcPort: ' + msg('Invalid server x11.srcPort: ' + info.srcPort)); accept(); - }).exec('foo --bar', + })).exec('foo --bar', { x11: true }, - function(err, stream) { - assert(!err, makeMsg('Unexpected exec error: ' + err)); - stream.on('data', function(d) { + mustCall((err, stream) => { + assert(!err, msg(`Unexpected exec error: ${err}`)); + stream.on('data', mustCallAtLeast((d) => { out += d; - }); - }); - }).on('end', function() { + })); + })); + })).on('end', mustCall(() => { assert(out === 'true', - makeMsg('Wrong stdout data: ' + inspect(out))); - }); - }, + msg(`Wrong stdout data: ${inspect(out)}`)); + })); + }), what: 'Exec with X11 forwarding' }, - { run: function() { - var client; - var server; - var r; - var out = ''; - var x11ClientConfig = { + { run: mustCall(function(msg) { + let out = ''; + const x11ClientConfig = { single: true, screen: 1234, protocol: 'YUMMY-MAGIC-COOKIE-1', cookie: '00112233445566778899001122334455' }; - - r = setup( + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - var x11 = false; - session.once('x11', function(accept, reject, info) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + let x11 = false; + session.once('x11', mustCall((accept, reject, info) => { assert.strictEqual(info.single, true, - makeMsg('Wrong client x11.single: ' + msg('Wrong client x11.single: ' + info.single)); assert.strictEqual(info.screen, 1234, - makeMsg('Wrong client x11.screen: ' + msg('Wrong client x11.screen: ' + info.screen)); assert.strictEqual(info.protocol, 'YUMMY-MAGIC-COOKIE-1', - makeMsg('Wrong client x11.protocol: ' + msg('Wrong client x11.protocol: ' + info.protocol)); - assert.strictEqual(info.cookie, + assert(Buffer.isBuffer(info.cookie)); + assert.strictEqual(info.cookie.toString(), '00112233445566778899001122334455', - makeMsg('Wrong client x11.cookie: ' + msg('Wrong client x11.cookie: ' + info.cookie)); x11 = info; + x11.cookie = x11.cookie.toString(); accept && accept(); - }).once('exec', function(accept, reject, info) { + })).once('exec', mustCall((accept, reject, info) => { assert(info.command === 'foo --bar', - makeMsg('Wrong exec command: ' + info.command)); - var stream = accept(); - conn.x11('127.0.0.1', 4321, function(err, xstream) { - assert(!err, makeMsg('Unexpected x11() error: ' + err)); + msg(`Wrong exec command: ${info.command}`)); + const stream = accept(); + conn.x11('127.0.0.1', 4321, mustCall((err, xstream) => { + assert(!err, msg(`Unexpected x11() error: ${err}`)); xstream.resume(); - xstream.on('end', function() { + xstream.on('end', mustCall(() => { stream.write(JSON.stringify(x11)); stream.exit(100); stream.end(); conn.end(); - }).end(); - }); - }); - }); - }); - }); - client.on('ready', function() { - client.on('x11', function(info, accept, reject) { + })).end(); + })); + })); + })); + })); + })); + client.on('ready', mustCall(() => { + client.on('x11', mustCall((info, accept, reject) => { assert.strictEqual(info.srcIP, '127.0.0.1', - makeMsg('Invalid server x11.srcIP: ' + msg('Invalid server x11.srcIP: ' + info.srcIP)); assert.strictEqual(info.srcPort, 4321, - makeMsg('Invalid server x11.srcPort: ' + msg('Invalid server x11.srcPort: ' + info.srcPort)); accept(); - }).exec('foo --bar', - { x11: x11ClientConfig }, - function(err, stream) { - assert(!err, makeMsg('Unexpected exec error: ' + err)); - stream.on('data', function(d) { + })).exec('foo --bar', + { x11: x11ClientConfig }, + mustCall((err, stream) => { + assert(!err, msg(`Unexpected exec error: ${err}`)); + stream.on('data', mustCallAtLeast((d) => { out += d; - }); - }); - }).on('end', function() { - var result = JSON.parse(out); + })); + })); + })).on('end', mustCall(() => { + const result = JSON.parse(out); assert.deepStrictEqual(result, x11ClientConfig, - makeMsg('Wrong stdout data: ' + result)); - }); - }, + msg(`Wrong stdout data: ${result}`)); + })); + }), what: 'Exec with X11 forwarding (custom X11 settings)' }, - { run: function() { - var client; - var server; - var r; - var out = ''; - - r = setup( + { run: mustCall(function(msg) { + let out = ''; + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - var sawPty = false; - session.once('pty', function(accept, reject, info) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + let sawPty = false; + session.once('pty', mustCall((accept, reject, info) => { sawPty = true; accept && accept(); - }).once('shell', function(accept, reject) { - var stream = accept(); - stream.write('Cowabunga dude! ' + inspect(sawPty)); + })).once('shell', mustCall((accept, reject) => { + const stream = accept(); + stream.write(`Cowabunga dude! ${inspect(sawPty)}`); stream.end(); conn.end(); - }); - }); - }); - }); - client.on('ready', function() { - client.shell(function(err, stream) { - assert(!err, makeMsg('Unexpected shell error: ' + err)); - stream.on('data', function(d) { + })); + })); + })); + })); + client.on('ready', mustCall(() => { + client.shell(mustCall((err, stream) => { + assert(!err, msg(`Unexpected shell error: ${err}`)); + stream.on('data', mustCallAtLeast((d) => { out += d; - }); - }); - }).on('end', function() { + })); + })); + })).on('end', mustCall(() => { assert(out === 'Cowabunga dude! true', - makeMsg('Wrong stdout data: ' + inspect(out))); - }); - }, + msg(`Wrong stdout data: ${inspect(out)}`)); + })); + }), what: 'Simple shell' }, - { run: function() { - var client; - var server; - var r; - var serverEnv = {}; - var clientEnv = { SSH2NODETEST: 'foo' }; - var sawPty = false; - - r = setup( + { run: mustCall(function(msg) { + const serverEnv = {}; + const clientEnv = { SSH2NODETEST: 'foo' }; + let sawPty = false; + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - session.once('env', function(accept, reject, info) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + session.once('env', mustCall((accept, reject, info) => { serverEnv[info.key] = info.val; accept && accept(); - }).once('pty', function(accept, reject, info) { + })).once('pty', mustCall((accept, reject, info) => { sawPty = true; accept && accept(); - }).once('shell', function(accept, reject) { - var stream = accept(); + })).once('shell', mustCall((accept, reject) => { + const stream = accept(); stream.end(); conn.end(); - }); - }); - }); - }); - client.on('ready', function() { - client.shell({ env: clientEnv }, function(err, stream) { - assert(!err, makeMsg('Unexpected shell error: ' + err)); + })); + })); + })); + })); + client.on('ready', mustCall(() => { + client.shell({ env: clientEnv }, mustCall((err, stream) => { + assert(!err, msg(`Unexpected shell error: ${err}`)); stream.resume(); - }); - }).on('end', function() { + })); + })).on('end', mustCall(() => { assert.deepEqual(serverEnv, clientEnv, - makeMsg('Environment mismatch')); + msg('Environment mismatch')); assert.strictEqual(sawPty, true); - }); - }, + })); + }), what: 'Shell with environment set' }, - { run: function() { - var client; - var server; - var r; - var expHandle = Buffer.from([1, 2, 3, 4]); - var sawOpenS = false; - var sawCloseS = false; - var sawOpenC = false; - var sawCloseC = false; - - r = setup( + { run: mustCall(function(msg) { + const expHandle = Buffer.from([1, 2, 3, 4]); + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.once('session', function(accept, reject) { - var session = accept(); - session.once('sftp', function(accept, reject) { + })).on('ready', mustCall(() => { + conn.once('session', mustCall((accept, reject) => { + const session = accept(); + session.once('sftp', mustCall((accept, reject) => { if (accept) { - var sftp = accept(); - sftp.once('OPEN', function(id, filename, flags, attrs) { + const sftp = accept(); + sftp.once('OPEN', mustCall((id, filename, flags, attrs) => { assert(id === 0, - makeMsg('Unexpected sftp request ID: ' + id)); + msg(`Unexpected sftp request ID: ${id}`)); assert(filename === 'node.js', - makeMsg('Unexpected filename: ' + filename)); + msg(`Unexpected filename: ${filename}`)); assert(flags === OPEN_MODE.READ, - makeMsg('Unexpected flags: ' + flags)); - sawOpenS = true; + msg(`Unexpected flags: ${flags}`)); sftp.handle(id, expHandle); - sftp.once('CLOSE', function(id, handle) { + sftp.once('CLOSE', mustCall((id, handle) => { assert(id === 1, - makeMsg('Unexpected sftp request ID: ' + id)); + msg(`Unexpected sftp request ID: ${id}`)); assert.deepEqual(handle, expHandle, - makeMsg('Wrong sftp file handle: ' + msg('Wrong sftp file handle: ' + inspect(handle))); - sawCloseS = true; sftp.status(id, STATUS_CODE.OK); conn.end(); - }); - }); + })); + })); } - }); - }); - }); - }); - client.on('ready', function() { - client.sftp(function(err, sftp) { - assert(!err, makeMsg('Unexpected sftp error: ' + err)); - sftp.open('node.js', 'r', function(err, handle) { - assert(!err, makeMsg('Unexpected sftp error: ' + err)); + })); + })); + })); + })); + client.on('ready', mustCall(() => { + client.sftp(mustCall((err, sftp) => { + assert(!err, msg(`Unexpected sftp error: ${err}`)); + sftp.open('node.js', 'r', mustCall((err, handle) => { + assert(!err, msg(`Unexpected sftp error: ${err}`)); assert.deepEqual(handle, expHandle, - makeMsg('Wrong sftp file handle: ' + msg('Wrong sftp file handle: ' + inspect(handle))); - sawOpenC = true; - sftp.close(handle, function(err) { - assert(!err, makeMsg('Unexpected sftp error: ' + err)); - sawCloseC = true; - }); - }); - }); - }).on('end', function() { - assert(sawOpenS, makeMsg('Expected sftp open()')); - assert(sawOpenC, makeMsg('Expected sftp open() callback')); - assert(sawCloseS, makeMsg('Expected sftp open()')); - assert(sawOpenC, makeMsg('Expected sftp close() callback')); - }); - }, + sftp.close(handle, mustCall((err) => { + assert(!err, msg(`Unexpected sftp error: ${err}`)); + })); + })); + })); + })); + }), what: 'Simple SFTP' }, - { run: function() { - var client; - var server; - var state = { + { run: mustCall(function(msg, next) { + const state = { readies: 0, closes: 0 }; - var clientcfg = { + const clientCfg = { username: USER, password: PASSWORD }; - var servercfg = { + const serverCfg = { hostKeys: [HOST_KEY_RSA] }; - var reconnect = false; - - client = new Client(), - server = new Server(servercfg); + let reconnect = false; + const client = new Client(); + const server = new Server(serverCfg); function onReady() { assert(++state.readies <= 4, - makeMsg('Wrong ready count: ' + state.readies)); + msg(`Wrong ready count: ${state.readies}`)); } + function onClose() { assert(++state.closes <= 3, - makeMsg('Wrong close count: ' + state.closes)); + msg(`Wrong close count: ${state.closes}`)); if (state.closes === 2) server.close(); else if (state.closes === 3) next(); } - server.listen(0, 'localhost', function() { - clientcfg.host = 'localhost'; - clientcfg.port = server.address().port; - client.connect(clientcfg); - }); + server.listen(0, 'localhost', mustCall(() => { + clientCfg.host = 'localhost'; + clientCfg.port = server.address().port; + client.connect(clientCfg); + })); - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', onReady); - }).on('close', onClose); - client.on('ready', function() { + })).on('ready', onReady); + })).on('close', onClose); + client.on('ready', mustCall(() => { onReady(); - if (reconnect) + if (reconnect) { client.end(); - else { + } else { reconnect = true; - client.connect(clientcfg); + client.connect(clientCfg); } - }).on('close', onClose); - }, + })).on('close', onClose); + }), what: 'connect() on connected client' }, - { run: function() { - var client = new Client({ + { run: mustCall(function(msg, next) { + const client = new Client({ username: USER, password: PASSWORD }); - assert.throws(function() { - client.exec('uptime', function(err, stream) { - assert(false, makeMsg('Callback unexpectedly called')); - }); - }); + assert.throws(mustCall(() => { + client.exec('uptime', mustNotCall()); + })); next(); - }, + }), what: 'Throw when not connected' }, - { run: function() { - var client; - var server; - var r; - var calledBack = 0; - - r = setup( + { run: mustCall(function(msg) { + let calledBack = 0; + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }); - }); - client.on('ready', function() { + })); + conn.on('session', mustCall(() => {})); + })); + client.on('ready', mustCall(() => { function callback(err, stream) { - assert(err, makeMsg('Expected error')); + assert(err, msg('Expected error')); assert(err.message === 'No response from server', - makeMsg('Wrong error message: ' + err.message)); + msg(`Wrong error message: ${err.message}`)); ++calledBack; } client.exec('uptime', callback); client.shell(callback); client.sftp(callback); client.end(); - }).on('close', function() { - // give the callbacks a chance to execute - process.nextTick(function() { + })).on('close', mustCall(() => { + // Give the callbacks a chance to execute + process.nextTick(mustCall(() => { assert(calledBack === 3, - makeMsg('Only ' - + calledBack - + '/3 outstanding callbacks called')); - }); - }); - }, + msg(`${calledBack}/3 outstanding callbacks called`)); + })); + })); + }), what: 'Outstanding callbacks called on disconnect' }, - { run: function() { - var client; - var server; - var r; - var calledBack = 0; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - session.once('exec', function(accept, reject, info) { - var stream = accept(); + })).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.once('exec', mustCall((accept, reject, info) => { + const stream = accept(); stream.exit(0); stream.end(); - }); - }); - }); - }); - client.on('ready', function() { - function callback(err, stream) { - assert(!err, makeMsg('Unexpected error: ' + err)); + })); + })); + })); + })); + client.on('ready', mustCall(() => { + let calledBack = 0; + const callback = mustCall((err, stream) => { + assert(!err, msg(`Unexpected error: ${err}`)); stream.resume(); if (++calledBack === 3) client.end(); - } + }, 3); client.exec('foo', callback); client.exec('bar', callback); client.exec('baz', callback); - }).on('end', function() { - assert(calledBack === 3, - makeMsg('Only ' - + calledBack - + '/3 callbacks called')); - }); - }, + })); + }), what: 'Pipelined requests' }, - { run: function() { - var client; - var server; - var r; - var calledBack = 0; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - var reqs = []; - conn.on('session', function(accept, reject) { + })).on('ready', mustCall(() => { + const reqs = []; + conn.on('session', mustCall((accept, reject) => { if (reqs.length === 0) { - conn.rekey(function(err) { - assert(!err, makeMsg('Unexpected rekey error: ' + err)); - reqs.forEach(function(accept) { - var session = accept(); - session.once('exec', function(accept, reject, info) { - var stream = accept(); + conn.rekey(mustCall((err) => { + assert(!err, msg(`Unexpected rekey error: ${err}`)); + reqs.forEach((accept) => { + const session = accept(); + session.once('exec', mustCall((accept, reject, info) => { + const stream = accept(); stream.exit(0); stream.end(); - }); + })); }); - }); + })); } reqs.push(accept); - }); - }); - }); - client.on('ready', function() { - function callback(err, stream) { - assert(!err, makeMsg('Unexpected error: ' + err)); + })); + })); + })); + client.on('ready', mustCall(() => { + let calledBack = 0; + const callback = mustCall((err, stream) => { + assert(!err, msg(`Unexpected error: ${err}`)); stream.resume(); if (++calledBack === 3) client.end(); - } + }, 3); client.exec('foo', callback); client.exec('bar', callback); client.exec('baz', callback); - }).on('end', function() { - assert(calledBack === 3, - makeMsg('Only ' - + calledBack - + '/3 callbacks called')); - }); - }, + })); + }), what: 'Pipelined requests with intermediate rekeying' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - session.once('exec', function(accept, reject, info) { - var stream = accept(); + })).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.once('exec', mustCall((accept, reject, info) => { + const stream = accept(); stream.exit(0); stream.end(); - }); - }); - }); - }); - client.on('ready', function() { - client.exec('foo', function(err, stream) { - assert(!err, makeMsg('Unexpected error: ' + err)); - stream.on('exit', function(code, signal) { + })); + })); + })); + })); + client.on('ready', mustCall(() => { + client.exec('foo', mustCall((err, stream) => { + assert(!err, msg(`Unexpected error: ${err}`)); + stream.on('exit', mustCall((code, signal) => { client.end(); - }); - }); - }); - }, + })); + })); + })); + }), what: 'Ignore outgoing after stream close' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - accept().on('sftp', function(accept, reject) { - var sftp = accept(); - // XXX: hack to get channel ... - var channel = sftp._readableState.pipes; - if (Array.isArray(channel)) - channel = channel[0]; - - channel.unpipe(sftp); - sftp.unpipe(channel); - - channel.exit(127); - channel.close(); - }); - }); - }); - }); - client.on('ready', function() { - var timeout = setTimeout(function() { - assert(false, makeMsg('Unexpected SFTP timeout')); - }, 1000); - client.sftp(function(err, sftp) { + })).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + accept().on('sftp', mustCall((accept, reject) => { + const sftp = accept(); + + // XXX: hack + sftp._protocol.exitStatus(sftp.outgoing.id, 127); + sftp._protocol.channelClose(sftp.outgoing.id); + })); + })); + })); + })); + client.on('ready', mustCall(() => { + const timeout = setTimeout(mustNotCall(), 1000); + client.sftp(mustCall((err, sftp) => { clearTimeout(timeout); - assert(err, makeMsg('Expected error')); + assert(err, msg('Expected error')); assert(err.code === 127, - makeMsg('Expected exit code 127, saw: ' + err.code)); + msg(`Expected exit code 127, saw: ${err.code}`)); client.end(); - }); - }); - }, + })); + })); + }), what: 'SFTP server aborts with exit-status' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD, - sock: new net.Socket() + sock: new Socket() }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() {}); - }); - client.on('ready', function() { + })).on('ready', mustCall(() => {})); + })); + client.on('ready', mustCall(() => { client.end(); - }); - }, + })); + }), what: 'Double pipe on unconnected, passed in net.Socket' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }); - conn.on('request', function(accept, reject, name, info) { + })); + conn.on('request', mustCall((accept, reject, name, info) => { accept(); - conn.forwardOut('good', 0, 'remote', 12345, function(err, ch) { - if (err) { - assert(!err, makeMsg('Unexpected error: ' + err)); - } - conn.forwardOut('bad', 0, 'remote', 12345, function(err, ch) { - assert(err, makeMsg('Should receive error')); + conn.forwardOut('good', 0, 'remote', 12345, mustCall((err, ch) => { + if (err) + assert(!err, msg(`Unexpected error: ${err}`)); + conn.forwardOut('bad', 0, 'remote', 12345, mustCall((err, ch) => { + assert(err, msg('Should receive error')); client.end(); - }); - }); - }); - }); + })); + })); + })); + })); - client.on('ready', function() { + client.on('ready', mustCall(() => { // request forwarding - client.forwardIn('good', 0, function(err, port) { - if (err) { - assert(!err, makeMsg('Unexpected error: ' + err)); - } - }); - }); - client.on('tcp connection', function(details, accept, reject) { + client.forwardIn('good', 0, mustCall((err, port) => { + if (err) + assert(!err, msg(`Unexpected error: ${err}`)); + })); + })); + client.on('tcp connection', mustCall((details, accept, reject) => { accept(); - }); - }, + })); + }), what: 'Client auto-rejects unrequested, allows requested forwarded-tcpip' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD @@ -1730,44 +1577,38 @@ var tests = [ greeting: 'Hello world!' } ); - client = r.client; - server = r.server; - var sawGreeting = false; + let sawGreeting = false; - client.on('greeting', function(greeting) { + client.on('greeting', mustCall((greeting) => { assert.strictEqual(greeting, 'Hello world!\r\n'); sawGreeting = true; - }); - client.on('banner', function(message) { - assert.fail(null, null, makeMsg('Unexpected banner')); - }); - - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - assert(sawGreeting, makeMsg('Client did not see greeting')); + })); + client.on('banner', mustCall((message) => { + assert.fail(null, null, msg('Unexpected banner')); + })); + + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { + assert(sawGreeting, msg('Client did not see greeting')); if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', - makeMsg('Unexpected auth method: ' + ctx.method)); + msg(`Wrong auth method: ${ctx.method}`)); assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); + msg(`Unexpected username: ${ctx.username}`)); assert(ctx.password === PASSWORD, - makeMsg('Unexpected password: ' + ctx.password)); + msg(`Unexpected password: ${ctx.password}`)); ctx.accept(); - }).on('ready', function() { + })).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Server greeting' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD @@ -1776,43 +1617,38 @@ var tests = [ banner: 'Hello world!' } ); - client = r.client; - server = r.server; - var sawBanner = false; + let sawBanner = false; - client.on('greeting', function(greeting) { - assert.fail(null, null, makeMsg('Unexpected greeting')); - }); - client.on('banner', function(message) { + client.on('greeting', mustCall((greeting) => { + assert.fail(null, null, msg('Unexpected greeting')); + })); + client.on('banner', mustCall((message) => { assert.strictEqual(message, 'Hello world!\r\n'); sawBanner = true; - }); + })); - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - assert(sawBanner, makeMsg('Client did not see banner')); + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { + assert(sawBanner, msg('Client did not see banner')); if (ctx.method === 'none') return ctx.reject(); assert(ctx.method === 'password', - makeMsg('Unexpected auth method: ' + ctx.method)); + msg(`Wrong auth method: ${ctx.method}`)); assert(ctx.username === USER, - makeMsg('Unexpected username: ' + ctx.username)); + msg(`Unexpected username: ${ctx.username}`)); assert(ctx.password === PASSWORD, - makeMsg('Unexpected password: ' + ctx.password)); + msg(`Unexpected password: ${ctx.password}`)); ctx.accept(); - }).on('ready', function() { + })).on('ready', mustCall(() => { conn.end(); - }); - }); - }, + })); + })); + }), what: 'Server banner' }, - { run: function() { - var client; - var server; - var r; - var fastRejectSent = false; + { run: mustCall(function(msg) { + let fastRejectSent = false; function sendAcceptLater(accept) { if (fastRejectSent) @@ -1820,208 +1656,135 @@ var tests = [ else setImmediate(sendAcceptLater, accept); } - - r = setup( + const { client, server } = setup( this, { username: USER }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }); + })); - conn.on('request', function(accept, reject, name, info) { + conn.on('request', mustCall((accept, reject, name, info) => { if (info.bindAddr === 'fastReject') { // Will call reject on 'fastReject' soon reject(); fastRejectSent = true; - } else - // but accept on 'slowAccept' later + } else { + // ... but accept on 'slowAccept' later sendAcceptLater(accept); - }); - }); + } + })); + })); - client.on('ready', function() { - var replyCnt = 0; + client.on('ready', mustCall(() => { + let replyCnt = 0; - client.forwardIn('slowAccept', 0, function(err) { - assert(!err, makeMsg('Unexpected error: ' + err)); + client.forwardIn('slowAccept', 0, mustCall((err) => { + assert(!err, msg(`Unexpected error: ${err}`)); if (++replyCnt === 2) client.end(); - }); + }, 2)); - client.forwardIn('fastReject', 0, function(err) { - assert(err, makeMsg('Should receive error')); + client.forwardIn('fastReject', 0, mustCall((err) => { + assert(err, msg('Should receive error')); if (++replyCnt === 2) client.end(); - }); - }); - }, + }, 2)); + })); + }), what: 'Server responds to global requests in the right order' }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - var timer; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + let timer; + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - session.once('subsystem', function(accept, reject, info) { + })).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.once('subsystem', mustCall((accept, reject, info) => { assert.equal(info.name, 'netconf'); - // Prevent success reply from being sent - conn._sshstream.channelSuccess = function() {}; + // XXX: hack to prevent success reply from being sent + conn._protocol.channelSuccess = () => {}; - var stream = accept(); + const stream = accept(); stream.close(); - timer = setTimeout(function() { - throw new Error(makeMsg('Expected client callback')); + timer = setTimeout(() => { + throw new Error(msg('Expected client callback')); }, 50); - }); - }); - }); - }); - client.on('ready', function() { - client.subsys('netconf', function(err, stream) { + })); + })); + })); + })); + client.on('ready', mustCall(() => { + client.subsys('netconf', mustCall((err, stream) => { clearTimeout(timer); assert(err); client.end(); - }); - }); - }, + })); + })); + }), what: 'Cleanup outstanding channel requests on channel close' }, - { run: function() { - var client; - var server; - var r; - - r = setup( - this, - { username: USER, - password: PASSWORD - }, - { hostKeys: [HOST_KEY_RSA] } - ); - client = r.client; - server = r.server; - - var timer; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - session.once('exec', function(accept, reject, info) { - var stream = accept(); - // Write enough to bring the Client's channel window to 0 - // (currently 1MB) - var buf = Buffer.allocUnsafe(2048); - for (var i = 0; i < 1000; ++i) - stream.write(buf); - stream.exit(0); - stream.close(); - }); - }); - }); - }); - client.on('ready', function() { - client.exec('foo', function(err, stream) { - var sawClose = false; - assert(!err, makeMsg('Unexpected error')); - client._sshstream.on('CHANNEL_CLOSE:' + stream.incoming.id, onClose); - function onClose() { - // This handler gets called *after* the internal handler, so we - // should have seen `stream`'s `close` event already if the bug - // exists - assert(!sawClose, makeMsg('Premature close event')); - client.end(); - } - stream.on('close', function() { - sawClose = true; - }); - }); - }); - }, - what: 'Channel emits close prematurely' - }, - { run: function() { - var client; - var server; - var r; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER }, { hostKeys: [HOST_KEY_RSA], ident: 'OpenSSH_5.3' } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }); - conn.once('request', function(accept, reject, name, info) { + })); + conn.once('request', mustCall((accept, reject, name, info) => { assert(name === 'tcpip-forward', - makeMsg('Unexpected request: ' + name)); + msg(`Unexpected request: ${name}`)); accept(1337); - conn.forwardOut('good', 0, 'remote', 12345, function(err, ch) { - assert(!err, makeMsg('Unexpected error: ' + err)); + conn.forwardOut('good', 0, 'remote', 12345, mustCall((err, ch) => { + assert(!err, msg(`Unexpected error: ${err}`)); client.end(); - }); - }); - }); + })); + })); + })); - client.on('ready', function() { + client.on('ready', mustCall(() => { // request forwarding - client.forwardIn('good', 0, function(err, port) { - assert(!err, makeMsg('Unexpected error: ' + err)); - assert(port === 1337, makeMsg('Bad bound port: ' + port)); - }); - }); - client.on('tcp connection', function(details, accept, reject) { + client.forwardIn('good', 0, mustCall((err, port) => { + assert(!err, msg(`Unexpected error: ${err}`)); + assert(port === 1337, msg(`Bad bound port: ${port}`)); + })); + })); + client.on('tcp connection', mustCall((details, accept, reject) => { assert(details.destIP === 'good', - makeMsg('Bad incoming destIP: ' + details.destIP)); + msg(`Bad incoming destIP: ${details.destIP}`)); assert(details.destPort === 1337, - makeMsg('Bad incoming destPort: ' + details.destPort)); + msg(`Bad incoming destPort: ${details.destPort}`)); assert(details.srcIP === 'remote', - makeMsg('Bad incoming srcIP: ' + details.srcIP)); + msg(`Bad incoming srcIP: ${details.srcIP}`)); assert(details.srcPort === 12345, - makeMsg('Bad incoming srcPort: ' + details.srcPort)); + msg(`Bad incoming srcPort: ${details.srcPort}`)); accept(); - }); - }, + })); + }), what: 'OpenSSH 5.x workaround for binding on port 0' }, - { run: function() { - var client; - var server; - var r; - var srvError; - var cliError; - - r = setup( + { run: mustCall(function(msg) { + let srvError; + let cliError; + const { client, server } = setup( this, { username: USER, algorithms: { @@ -2034,8 +1797,6 @@ var tests = [ } } ); - client = r.client; - server = r.server; // Remove default client error handler added by `setup()` since we are // expecting an error in this case @@ -2043,268 +1804,223 @@ var tests = [ function onError(err) { if (this === client) { - assert(!cliError, makeMsg('Unexpected multiple client errors')); + assert(!cliError, msg('Unexpected multiple client errors')); cliError = err; } else { - assert(!srvError, makeMsg('Unexpected multiple server errors')); + assert(!srvError, msg('Unexpected multiple server errors')); srvError = err; } assert.strictEqual(err.level, 'handshake'); assert(/handshake failed/i.test(err.message), - makeMsg('Wrong error message')); + msg('Wrong error message')); } - server.on('connection', function(conn) { + server.on('connection', mustCall((conn) => { // Remove default server connection error handler added by `setup()` // since we are expecting an error in this case conn.removeAllListeners('error'); - function onGoodHandshake() { - assert(false, makeMsg('Handshake should have failed')); - } - conn.on('authentication', onGoodHandshake); - conn.on('ready', onGoodHandshake); + conn.on('authentication', mustNotCall()); + conn.on('ready', mustNotCall()); conn.on('error', onError); - }); - - client.on('ready', function() { - assert(false, makeMsg('Handshake should have failed')); - }); - client.on('error', onError); - client.on('close', function() { - assert(cliError, makeMsg('Expected client error')); - assert(srvError, makeMsg('Expected server error')); - }); - }, + })); + + client.on('ready', mustNotCall()) + .on('error', onError) + .on('close', mustCall(() => { + assert(cliError, msg('Expected client error')); + assert(srvError, msg('Expected server error')); + })); + }), what: 'Handshake errors are emitted' }, - { run: function() { - var client; - var server; - var r; - var cliError; - - r = setup( + { run: mustCall(function(msg) { + let cliError; + const { client, server } = setup( this, { username: USER, privateKey: KEY_RSA_BAD }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; // Remove default client error handler added by `setup()` since we are // expecting an error in this case client.removeAllListeners('error'); - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { assert(ctx.method === 'publickey' || ctx.method === 'none', - makeMsg('Unexpected auth method: ' + ctx.method)); - assert(!ctx.signature, makeMsg('Unexpected signature')); + msg(`Wrong auth method: ${ctx.method}`)); + assert(!ctx.signature, msg('Unexpected signature')); if (ctx.method === 'none') return ctx.reject(); ctx.accept(); - }); - conn.on('ready', function() { - assert(false, makeMsg('Authentication should have failed')); - }); - }); + })); + conn.on('ready', mustNotCall()); + })); - client.on('ready', function() { - assert(false, makeMsg('Authentication should have failed')); - }); - client.on('error', function(err) { + client.on('ready', mustNotCall()).on('error', mustCall((err) => { if (cliError) { assert(/all configured/i.test(err.message), - makeMsg('Wrong error message')); + msg('Wrong error message')); } else { cliError = err; - assert(/signing/i.test(err.message), makeMsg('Wrong error message')); + assert(/signing/i.test(err.message), msg('Wrong error message')); } - }); - client.on('close', function() { - assert(cliError, makeMsg('Expected client error')); - }); - }, + })).on('close', mustCall(() => { + assert(cliError, msg('Expected client error')); + })); + }), what: 'Client signing errors are caught and emitted' }, - { run: function() { - var client; - var server; - var r; - var srvError; - var cliError; - - r = setup( + { run: mustCall(function(msg) { + let srvError; + let cliError; + const { client, server } = setup( this, { username: USER, password: 'foo' }, { hostKeys: [KEY_RSA_BAD] } ); - client = r.client; - server = r.server; // Remove default client error handler added by `setup()` since we are // expecting an error in this case client.removeAllListeners('error'); - server.on('connection', function(conn) { + server.on('connection', mustCall((conn) => { // Remove default server connection error handler added by `setup()` // since we are expecting an error in this case conn.removeAllListeners('error'); - conn.once('error', function(err) { - assert(/signing/i.test(err.message), makeMsg('Wrong error message')); + conn.once('error', mustCall((err) => { + assert(/signature generation failed/i.test(err.message), + msg('Wrong error message')); srvError = err; - }); - conn.on('authentication', function(ctx) { - assert(false, makeMsg('Handshake should have failed')); - }); - conn.on('ready', function() { - assert(false, makeMsg('Authentication should have failed')); - }); - }); + })).on('authentication', mustNotCall()) + .on('ready', mustNotCall()); + })); - client.on('ready', function() { - assert(false, makeMsg('Handshake should have failed')); - }); - client.on('error', function(err) { - assert(!cliError, makeMsg('Unexpected multiple client errors')); + client.on('ready', mustNotCall()).on('error', mustCall((err) => { + assert(!cliError, msg('Unexpected multiple client errors')); assert(/KEY_EXCHANGE_FAILED/.test(err.message), - makeMsg('Wrong error message')); + msg('Wrong error message')); cliError = err; - }); - client.on('close', function() { - assert(srvError, makeMsg('Expected server error')); - assert(cliError, makeMsg('Expected client error')); - }); - }, + })).on('close', mustCall(() => { + assert(srvError, msg('Expected server error')); + assert(cliError, msg('Expected client error')); + })); + }), what: 'Server signing errors are caught and emitted' }, - { run: function() { - var client; - var server; - var r; - var sawReady = false; - - r = setup( + { run: mustCall(function(msg) { + let sawReady = false; + const { client, server } = setup( this, { username: '', password: 'foo' }, { hostKeys: [HOST_KEY_RSA] } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { assert.strictEqual(ctx.username, '', - makeMsg('Expected empty username')); + msg('Expected empty username')); ctx.accept(); - }).on('ready', function() { + })).on('ready', mustCall(() => { conn.end(); - }); - }); + })); + })); - client.on('ready', function() { + client.on('ready', mustCall(() => { sawReady = true; - }).on('close', function() { - assert.strictEqual(sawReady, true, makeMsg('Expected ready event')); - }); - }, + })).on('close', mustCall(() => { + assert.strictEqual(sawReady, true, msg('Expected ready event')); + })); + }), what: 'Empty username string works' }, - { run: function() { - var client; - var server; - var r; - var socketPath = '/foo'; - var events = []; - var expected = [ + { run: mustCall(function(msg) { + const socketPath = '/foo'; + const events = []; + const expected = [ ['client', 'openssh_forwardInStreamLocal'], ['server', 'streamlocal-forward@openssh.com', - { socketPath: socketPath }], + { socketPath }], ['client', 'forward callback'], - ['client', 'unix connection', { socketPath: socketPath }], + ['client', 'unix connection', { socketPath }], ['client', 'socket data', '1'], ['server', 'socket data', '2'], ['client', 'socket end'], ['server', 'cancel-streamlocal-forward@openssh.com', - { socketPath: socketPath }], + { socketPath }], ['client', 'cancel callback'] ]; - - r = setup( + const { client, server } = setup( this, { username: USER }, { hostKeys: [HOST_KEY_RSA], ident: 'OpenSSH_7.1' } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }); - conn.on('request', function(accept, reject, name, info) { + })); + conn.on('request', mustCall((accept, reject, name, info) => { events.push(['server', name, info]); if (name === 'streamlocal-forward@openssh.com') { accept(); - conn.openssh_forwardOutStreamLocal(socketPath, function(err, ch) { - assert(!err, makeMsg('Unexpected error: ' + err)); + conn.openssh_forwardOutStreamLocal(socketPath, + mustCall((err, ch) => { + assert(!err, msg(`Unexpected error: ${err}`)); ch.write('1'); - ch.on('data', function(data) { + ch.on('data', mustCallAtLeast((data) => { events.push(['server', 'socket data', data.toString()]); ch.close(); - }); - }); + })); + })); } else if (name === 'cancel-streamlocal-forward@openssh.com') { accept(); } else { reject(); } - }); - }); + })); + })); - client.on('ready', function() { + client.on('ready', mustCall(() => { // request forwarding events.push(['client', 'openssh_forwardInStreamLocal']); - client.openssh_forwardInStreamLocal(socketPath, function(err) { - assert(!err, makeMsg('Unexpected error: ' + err)); + client.openssh_forwardInStreamLocal(socketPath, mustCall((err) => { + assert(!err, msg(`Unexpected error: ${err}`)); events.push(['client', 'forward callback']); - }); - client.on('unix connection', function(info, accept, reject) { + })); + client.on('unix connection', mustCall((info, accept, reject) => { events.push(['client', 'unix connection', info]); - var stream = accept(); - stream.on('data', function(data) { + const stream = accept(); + stream.on('data', mustCallAtLeast((data) => { events.push(['client', 'socket data', data.toString()]); stream.write('2'); - }).on('end', function() { + })).on('end', mustCall(() => { events.push(['client', 'socket end']); - client.openssh_unforwardInStreamLocal(socketPath, function(err) { - assert(!err, makeMsg('Unexpected error: ' + err)); + client.openssh_unforwardInStreamLocal(socketPath, + mustCall((err) => { + assert(!err, msg(`Unexpected error: ${err}`)); events.push(['client', 'cancel callback']); client.end(); - }); - }); - }); - }); - client.on('end', function() { - var msg = 'Events mismatch\nActual:\n' + inspect(events) - + '\nExpected:\n' + inspect(expected); - assert.deepEqual(events, expected, makeMsg(msg)); - }); - }, + })); + })); + })); + })); + client.on('end', mustCall(() => { + assert.deepEqual(events, + expected, + msg(`Events mismatch\nActual:\n${inspect(events)}` + + `\nExpected:\n${inspect(expected)}`)); + })); + }), what: 'OpenSSH forwarded UNIX socket connection' }, - { run: function() { - var client; - var server; - var r; - var calledBack = 0; - - r = setup( + { run: mustCall(function(msg) { + const { client, server } = setup( this, { username: USER, password: PASSWORD, @@ -2318,149 +2034,162 @@ var tests = [ }, } ); - client = r.client; - server = r.server; - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - var reqs = []; - conn.on('session', function(accept, reject) { + })).on('ready', mustCall(() => { + const reqs = []; + conn.on('session', mustCall((accept, reject) => { if (reqs.length === 0) { - conn.rekey(function(err) { - assert(!err, makeMsg('Unexpected rekey error: ' + err)); - reqs.forEach(function(accept) { - var session = accept(); - session.once('exec', function(accept, reject, info) { - var stream = accept(); + conn.rekey(mustCall((err) => { + assert(!err, msg(`Unexpected rekey error: ${err}`)); + reqs.forEach((accept) => { + const session = accept(); + session.once('exec', mustCall((accept, reject, info) => { + const stream = accept(); stream.exit(0); stream.end(); - }); + })); }); - }); + })); } reqs.push(accept); - }); - }); - }); - client.on('ready', function() { - function callback(err, stream) { - assert(!err, makeMsg('Unexpected error: ' + err)); + })); + })); + })); + client.on('ready', mustCall(() => { + let calledBack = 0; + const callback = mustCall((err, stream) => { + assert(!err, msg(`Unexpected error: ${err}`)); stream.resume(); if (++calledBack === 3) client.end(); - } + }, 3); client.exec('foo', callback); client.exec('bar', callback); client.exec('baz', callback); - }).on('end', function() { - assert(calledBack === 3, - makeMsg('Only ' - + calledBack - + '/3 callbacks called')); - }); - }, + })); + }), what: 'Rekeying with AES-GCM' }, ]; -function setup(self, clientcfg, servercfg, timeout) { +function setup(self, clientCfg, serverCfg, timeout) { + const { next, msg } = self; self.state = { clientReady: false, serverReady: false, clientClose: false, - serverClose: false + serverClose: false, }; if (DEBUG) { console.log('========================================================\n' - + '[TEST] ' - + self.what - + '\n========================================================'); - clientcfg.debug = function() { - var args = new Array(arguments.length + 1); - args[0] = '[CLIENT]'; - for (var i = 0; i < arguments.length; ++i) - args[i + 1] = arguments[i]; - console.log.apply(null, args); + + `[TEST] ${self.what}\n` + + '========================================================'); + clientCfg.debug = (...args) => { + console.log(`[${self.what}][CLIENT]`, ...args); }; - servercfg.debug = function() { - var args = new Array(arguments.length + 1); - args[0] = '[SERVER]'; - for (var i = 0; i < arguments.length; ++i) - args[i + 1] = arguments[i]; - console.log.apply(null, args); + serverCfg.debug = (...args) => { + console.log(`[${self.what}][SERVER]`, ...args); }; } - var client = new Client(); - var server = new Server(servercfg); + const client = new Client(); + const server = new Server(serverCfg); if (timeout === undefined) timeout = DEFAULT_TEST_TIMEOUT; - var timer; + let timer; server.on('error', onError) - .on('connection', function(conn) { + .on('connection', mustCall((conn) => { conn.on('error', onError) - .on('ready', onReady); + .on('ready', mustCall(onReady)); server.close(); - }) - .on('close', onClose); + })) + .on('close', mustCall(onClose)); client.on('error', onError) - .on('ready', onReady) - .on('close', onClose); + .on('ready', mustCall(onReady)) + .on('close', mustCall(onClose)); function onError(err) { - var which = (this === client ? 'client' : 'server'); - assert(false, makeMsg('Unexpected ' + which + ' error: ' + err)); + const which = (this === client ? 'client' : 'server'); + assert(false, msg(`Unexpected ${which} error: ${err}`)); } + function onReady() { if (this === client) { assert(!self.state.clientReady, - makeMsg('Received multiple ready events for client')); + msg('Received multiple ready events for client')); self.state.clientReady = true; } else { assert(!self.state.serverReady, - makeMsg('Received multiple ready events for server')); + msg('Received multiple ready events for server')); self.state.serverReady = true; } if (self.state.clientReady && self.state.serverReady) self.onReady && self.onReady(); } + function onClose() { if (this === client) { assert(!self.state.clientClose, - makeMsg('Received multiple close events for client')); + msg('Received multiple close events for client')); self.state.clientClose = true; } else { assert(!self.state.serverClose, - makeMsg('Received multiple close events for server')); + msg('Received multiple close events for server')); self.state.serverClose = true; } - if (self.state.clientClose && self.state.serverClose) { + if (self.state.clientClose + && self.state.serverClose + && !getParamNames(self.run.origFn || self.run).includes('next')) { clearTimeout(timer); next(); } } - process.nextTick(function() { - server.listen(0, 'localhost', function() { + process.nextTick(mustCall(() => { + server.listen(0, 'localhost', mustCall(() => { if (timeout >= 0) { - timer = setTimeout(function() { - assert(false, makeMsg('Test timed out')); + timer = setTimeout(() => { + assert(false, msg('Test timed out')); }, timeout); } - if (clientcfg.sock) - clientcfg.sock.connect(server.address().port, 'localhost'); - else { - clientcfg.host = 'localhost'; - clientcfg.port = server.address().port; + if (clientCfg.sock) { + clientCfg.sock.connect(server.address().port, 'localhost'); + } else { + clientCfg.host = 'localhost'; + clientCfg.port = server.address().port; } - client.connect(clientcfg); - }); - }); - return { client: client, server: server }; + client.connect(clientCfg); + })); + })); + + return { client, server }; +} + +const getParamNames = (() => { + const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; + const ARGUMENT_NAMES = /([^\s,]+)/g; + const toString = Function.prototype.toString; + return (fn) => { + const s = toString.call(fn).replace(STRIP_COMMENTS, ''); + const result = s.slice(s.indexOf('(') + 1, s.indexOf(')')) + .match(ARGUMENT_NAMES); + return (result || []); + }; +})(); + +function once(fn) { + let called = false; + return (...args) => { + if (called) + return; + called = true; + fn(...args); + }; } function next() { @@ -2469,29 +2198,20 @@ function next() { if (++t === tests.length) return; - var v = tests[t]; - v.run.call(v); + const v = tests[t]; + v.next = once(next); + v.msg = msg.bind(null, v.what); + v.run(v.msg, v.next); } -function makeMsg(what, msg) { - if (msg === undefined) - msg = what; - if (tests[t]) - what = tests[t].what; - else - what = ''; - return '[' + group + what + ']: ' + msg; +function msg(what, desc) { + return `[${THIS_FILE}/${what}]: ${desc}`; } -process.once('uncaughtException', function(err) { - if (t > -1 && !/(?:^|\n)AssertionError: /i.test(''+err)) - console.log(makeMsg('Unexpected Exception:')); - throw err; -}); -process.once('exit', function() { - assert(t === tests.length, - makeMsg('_exit', - 'Only finished ' + t + '/' + tests.length + ' tests')); +process.once('exit', () => { + const ran = Math.max(t, 0); + assert(ran === tests.length, + msg('(exit)', `Finished ${ran}/${tests.length} tests`)); }); next(); diff --git a/test/test-openssh.js b/test/test-openssh.js index 545129e4..b4feda1d 100644 --- a/test/test-openssh.js +++ b/test/test-openssh.js @@ -1,361 +1,437 @@ -var Server = require('../lib/server'); -var utils = require('ssh2-streams').utils; - -var fs = require('fs'); -var path = require('path'); -var join = path.join; -var assert = require('assert'); -var spawn = require('child_process').spawn; -var exec = require('child_process').exec; - -var t = -1; -var group = path.basename(__filename, '.js') + '/'; -var fixturesdir = join(__dirname, 'fixtures'); - -var CLIENT_TIMEOUT = 5000; -var USER = 'nodejs'; -var HOST_KEY_RSA = fs.readFileSync(join(fixturesdir, 'ssh_host_rsa_key')); -var HOST_KEY_DSA = fs.readFileSync(join(fixturesdir, 'ssh_host_dsa_key')); -var HOST_KEY_ECDSA = fs.readFileSync(join(fixturesdir, 'ssh_host_ecdsa_key')); -var CLIENT_KEY_RSA_PATH = join(fixturesdir, 'id_rsa'); -var CLIENT_KEY_RSA_RAW = fs.readFileSync(CLIENT_KEY_RSA_PATH); -var CLIENT_KEY_RSA = utils.parseKey(CLIENT_KEY_RSA_RAW); -var CLIENT_KEY_DSA_PATH = join(fixturesdir, 'id_dsa'); -var CLIENT_KEY_DSA_RAW = fs.readFileSync(CLIENT_KEY_DSA_PATH); -var CLIENT_KEY_DSA = utils.parseKey(CLIENT_KEY_DSA_RAW); -var CLIENT_KEY_ECDSA_PATH = join(fixturesdir, 'id_ecdsa'); -var CLIENT_KEY_ECDSA_RAW = fs.readFileSync(CLIENT_KEY_ECDSA_PATH); -var CLIENT_KEY_ECDSA = utils.parseKey(CLIENT_KEY_ECDSA_RAW); - -var opensshPath = 'ssh'; -var opensshVer; -var DEBUG = false; +// TODO: add more rekey tests that at least include switching from no +// compression to compression and vice versa +'use strict'; + +const assert = require('assert'); +const { spawn, spawnSync } = require('child_process'); +const { chmodSync, readdirSync, readFileSync } = require('fs'); +const { basename, join } = require('path'); + +const Server = require('../lib/server.js'); +const { parseKey } = require('../lib/protocol/keyParser.js'); + +const { mustCall, mustCallAtLeast } = require('./common.js'); + +let t = -1; +const THIS_FILE = basename(__filename, '.js'); +const SPAWN_OPTS = { windowsHide: true }; +const fixturesDir = join(__dirname, 'fixtures'); +const fixture = (file) => readFileSync(join(fixturesDir, file)); + +const HOST_KEY_RSA = fixture('ssh_host_rsa_key'); +const HOST_KEY_DSA = fixture('ssh_host_dsa_key'); +const HOST_KEY_ECDSA = fixture('ssh_host_ecdsa_key'); +const CLIENT_KEY_RSA_PATH = join(fixturesDir, 'id_rsa'); +const CLIENT_KEY_RSA_RAW = readFileSync(CLIENT_KEY_RSA_PATH); +const CLIENT_KEY_RSA = parseKey(CLIENT_KEY_RSA_RAW); +const CLIENT_KEY_DSA_PATH = join(fixturesDir, 'id_dsa'); +const CLIENT_KEY_DSA_RAW = readFileSync(CLIENT_KEY_DSA_PATH); +const CLIENT_KEY_DSA = parseKey(CLIENT_KEY_DSA_RAW); +const CLIENT_KEY_ECDSA_PATH = join(fixturesDir, 'id_ecdsa'); +const CLIENT_KEY_ECDSA_RAW = readFileSync(CLIENT_KEY_ECDSA_PATH); +const CLIENT_KEY_ECDSA = parseKey(CLIENT_KEY_ECDSA_RAW); +const CLIENT_TIMEOUT = 5000; +const USER = 'nodejs'; +const DEBUG = false; + +const opensshPath = 'ssh'; +let opensshVer; // Fix file modes to avoid OpenSSH client complaints about keys' permissions -fs.readdirSync(fixturesdir).forEach(function(file) { - fs.chmodSync(join(fixturesdir, file), '0600'); -}); - -var tests = [ - { run: function() { - var what = this.what; - var server; +for (const file of readdirSync(fixturesDir, { withFileTypes: true })) { + if (file.isFile()) + chmodSync(join(fixturesDir, file.name), 0o600); +} - server = setup( +const tests = [ + { run: mustCall(function(msg) { + const server = setup( this, { privateKeyPath: CLIENT_KEY_RSA_PATH }, { hostKeys: [HOST_KEY_RSA] } ); - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg(what, 'Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg(what, 'Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ssh-rsa', - makeMsg(what, 'Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(), - ctx.key.data, - makeMsg(what, 'Public key mismatch')); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature !== undefined, + msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Unexpected auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-rsa', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + default: + assert(false, msg('Too many auth attempts')); + } if (ctx.signature) { assert(CLIENT_KEY_RSA.verify(ctx.blob, ctx.signature) === true, - makeMsg(what, 'Could not verify PK signature')); - ctx.accept(); - } else - ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - if (session) { - session.on('exec', function(accept, reject) { - var stream = accept(); - if (stream) { - stream.exit(0); - stream.end(); - } - }).on('pty', function(accept, reject) { - accept && accept(); - }); - } - }); - }); - }); - }, + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 2)).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.on('exec', mustCall((accept, reject) => { + const stream = accept(); + stream.exit(0); + stream.end(); + })).on('pty', mustCall((accept, reject) => { + accept && accept(); + })); + })); + })); + })); + }), what: 'Authenticate with an RSA key' }, - { run: function() { - var what = this.what; - var server; - - server = setup( + { run: mustCall(function(msg) { + const server = setup( this, { privateKeyPath: CLIENT_KEY_DSA_PATH }, { hostKeys: [HOST_KEY_RSA] } ); - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg(what, 'Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg(what, 'Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ssh-dss', - makeMsg(what, 'Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_DSA.getPublicSSH(), - ctx.key.data, - makeMsg(what, 'Public key mismatch')); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature !== undefined, + msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Unexpected auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-dss', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_DSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + default: + assert(false, msg('Too many auth attempts')); + } if (ctx.signature) { assert(CLIENT_KEY_DSA.verify(ctx.blob, ctx.signature) === true, - makeMsg(what, 'Could not verify PK signature')); + msg('Could not verify PK signature')); } ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - if (session) { - session.on('exec', function(accept, reject) { - var stream = accept(); - if (stream) { - stream.exit(0); - stream.end(); - } - }).on('pty', function(accept, reject) { - accept && accept(); - }); - } - }); - }); - }); - }, + }, 2)).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.on('exec', mustCall((accept, reject) => { + const stream = accept(); + stream.exit(0); + stream.end(); + })).on('pty', mustCall((accept, reject) => { + accept && accept(); + })); + })); + })); + })); + }), what: 'Authenticate with a DSA key' }, - { run: function() { - var what = this.what; - var server; - - server = setup( + { run: mustCall(function(msg) { + const server = setup( this, { privateKeyPath: CLIENT_KEY_ECDSA_PATH }, { hostKeys: [HOST_KEY_RSA] } ); - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { - if (ctx.method === 'none') - return ctx.reject(); - assert(ctx.method === 'publickey', - makeMsg(what, 'Unexpected auth method: ' + ctx.method)); - assert(ctx.username === USER, - makeMsg(what, 'Unexpected username: ' + ctx.username)); - assert(ctx.key.algo === 'ecdsa-sha2-nistp256', - makeMsg(what, 'Unexpected key algo: ' + ctx.key.algo)); - assert.deepEqual(CLIENT_KEY_ECDSA.getPublicSSH(), - ctx.key.data, - makeMsg(what, 'Public key mismatch')); + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature !== undefined, + msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Unexpected auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ecdsa-sha2-nistp256', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_ECDSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + default: + assert(false, msg('Too many auth attempts')); + } if (ctx.signature) { assert(CLIENT_KEY_ECDSA.verify(ctx.blob, ctx.signature) === true, - makeMsg(what, 'Could not verify PK signature')); - ctx.accept(); - } else - ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - if (session) { - session.on('exec', function(accept, reject) { - var stream = accept(); - if (stream) { - stream.exit(0); - stream.end(); - } - }).on('pty', function(accept, reject) { - accept && accept(); - }); - } - }); - }); - }); - }, - what: 'Authenticate with a ECDSA key' + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.on('exec', mustCall((accept, reject) => { + const stream = accept(); + stream.exit(0); + stream.end(); + })).on('pty', mustCall((accept, reject) => { + accept && accept(); + })); + })); + })); + })); + }), + what: 'Authenticate with an ECDSA key' }, - { run: function() { - var server; - - server = setup( + { run: mustCall(function(msg) { + const server = setup( this, { privateKeyPath: CLIENT_KEY_RSA_PATH }, { hostKeys: [HOST_KEY_DSA] } ); - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - if (session) { - session.on('exec', function(accept, reject) { - var stream = accept(); - if (stream) { - stream.exit(0); - stream.end(); - } - }).on('pty', function(accept, reject) { - accept && accept(); - }); - } - }); - }); - }); - }, + })).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.on('exec', mustCall((accept, reject) => { + const stream = accept(); + stream.exit(0); + stream.end(); + })).on('pty', mustCall((accept, reject) => { + accept && accept(); + })); + })); + })); + })); + }), what: 'Server with DSA host key' }, - { run: function() { - var server; - - server = setup( + { run: mustCall(function(msg) { + const server = setup( this, { privateKeyPath: CLIENT_KEY_RSA_PATH }, { hostKeys: [HOST_KEY_ECDSA] } ); - server.on('connection', function(conn) { - conn.on('authentication', function(ctx) { + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - if (session) { - session.on('exec', function(accept, reject) { - var stream = accept(); - if (stream) { - stream.exit(0); - stream.end(); - } - }).on('pty', function(accept, reject) { - accept && accept(); - }); - } - }); - }); - }); - }, + })).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.on('exec', mustCall((accept, reject) => { + const stream = accept(); + stream.exit(0); + stream.end(); + })).on('pty', mustCall((accept, reject) => { + accept && accept(); + })); + })); + })); + })); + }), what: 'Server with ECDSA host key' }, - { run: function() { - var server; - var what = this.what; - - server = setup( + { run: mustCall(function(msg) { + const server = setup( this, { privateKeyPath: CLIENT_KEY_RSA_PATH }, { hostKeys: [HOST_KEY_RSA] } ); - server.on('_child', function(childProc) { + server.on('_child', mustCall((childProc) => { childProc.stderr.once('data', function(data) { childProc.stdin.end(); }); childProc.stdin.write('ping'); - }).on('connection', function(conn) { - conn.on('authentication', function(ctx) { + })).on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { ctx.accept(); - }).on('ready', function() { - conn.on('session', function(accept, reject) { - var session = accept(); - assert(session, makeMsg(what, 'Missing session')); - session.on('exec', function(accept, reject) { - var stream = accept(); - assert(stream, makeMsg(what, 'Missing exec stream')); - stream.stdin.on('data', function(data) { + })).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + session.on('exec', mustCall((accept, reject) => { + const stream = accept(); + stream.stdin.on('data', mustCallAtLeast((data) => { stream.stdout.write('pong on stdout'); stream.stderr.write('pong on stderr'); - }).on('end', function() { + })).on('end', mustCall(() => { stream.stdout.write('pong on stdout'); stream.stderr.write('pong on stderr'); stream.exit(0); stream.close(); - }); - }).on('pty', function(accept, reject) { + })); + })).on('pty', mustCall((accept, reject) => { accept && accept(); - }); - }); - }); - }); - }, + })); + })); + })); + })); + }), what: 'Server closes stdin too early' }, + { run: mustCall(function(msg) { + const server = setup( + this, + { privateKeyPath: CLIENT_KEY_RSA_PATH }, + { hostKeys: [HOST_KEY_RSA] } + ); + + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none'), + msg(`Wrong method: ${ctx.method}`); + return ctx.reject(); + case 3: + assert(ctx.signature !== undefined, + msg('Missing publickey signature')); + // FALLTHROUGH + case 2: + assert(ctx.method === 'publickey', + msg(`Unexpected auth method: ${ctx.method}`)); + assert(ctx.username === USER, + msg(`Unexpected username: ${ctx.username}`)); + assert(ctx.key.algo === 'ssh-rsa', + msg(`Unexpected key algo: ${ctx.key.algo}`)); + assert.deepEqual(CLIENT_KEY_RSA.getPublicSSH(), + ctx.key.data, + msg('Public key mismatch')); + break; + default: + assert(false, msg('Too many auth attempts')); + } + if (ctx.signature) { + assert(CLIENT_KEY_RSA.verify(ctx.blob, ctx.signature) === true, + msg('Could not verify PK signature')); + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { + conn.on('session', mustCall((accept, reject) => { + const session = accept(); + conn.rekey(); + session.on('exec', mustCall((accept, reject) => { + const stream = accept(); + stream.exit(0); + stream.end(); + })).on('pty', mustCall((accept, reject) => { + accept && accept(); + })); + })); + })); + })); + }), + what: 'Rekey' + }, ]; -function setup(self, clientcfg, servercfg) { +function setup(self, clientCfg, serverCfg) { + const { next, msg } = self; self.state = { serverReady: false, clientClose: false, - serverClose: false + serverClose: false, }; - var client; - var server = new Server(servercfg); + let client; + if (DEBUG) { + console.log('========================================================\n' + + `[TEST] ${self.what}\n` + + '========================================================'); + serverCfg.debug = (...args) => { + console.log(`[${self.what}][SERVER]`, ...args); + }; + } + const server = new Server(serverCfg); server.on('error', onError) - .on('connection', function(conn) { + .on('connection', mustCall((conn) => { conn.on('error', onError) - .on('ready', onReady); + .on('ready', mustCall(onReady)); server.close(); - }) - .on('close', onClose); + })) + .on('close', mustCall(onClose)); function onError(err) { - var which = (arguments.length >= 3 ? 'client' : 'server'); - assert(false, makeMsg(self.what, 'Unexpected ' + which + ' error: ' + err)); + const which = (arguments.length >= 3 ? 'client' : 'server'); + assert(false, msg(`Unexpected ${which} error: ${err}`)); } + function onReady() { assert(!self.state.serverReady, - makeMsg(self.what, 'Received multiple ready events for server')); + msg('Received multiple ready events for server')); self.state.serverReady = true; self.onReady && self.onReady(); } + function onClose() { if (arguments.length >= 3) { assert(!self.state.clientClose, - makeMsg(self.what, 'Received multiple close events for client')); + msg('Received multiple close events for client')); self.state.clientClose = true; } else { assert(!self.state.serverClose, - makeMsg(self.what, 'Received multiple close events for server')); + msg('Received multiple close events for server')); self.state.serverClose = true; } - if (self.state.clientClose && self.state.serverClose) + if (self.state.clientClose + && self.state.serverClose + && !getParamNames(self.run.origFn || self.run).includes('next')) { next(); + } } - process.nextTick(function() { - server.listen(0, 'localhost', function() { - var cmd = opensshPath; - var args = ['-o', 'UserKnownHostsFile=/dev/null', - '-o', 'StrictHostKeyChecking=no', - '-o', 'CheckHostIP=no', - '-o', 'ConnectTimeout=3', - '-o', 'GlobalKnownHostsFile=/dev/null', - '-o', 'GSSAPIAuthentication=no', - '-o', 'IdentitiesOnly=yes', - '-o', 'BatchMode=yes', - '-o', 'VerifyHostKeyDNS=no', - - '-vvvvvv', - '-T', - '-o', 'KbdInteractiveAuthentication=no', - '-o', 'HostbasedAuthentication=no', - '-o', 'PasswordAuthentication=no', - '-o', 'PubkeyAuthentication=yes', - '-o', 'PreferredAuthentications=publickey']; - if (clientcfg.privateKeyPath) - args.push('-o', 'IdentityFile=' + clientcfg.privateKeyPath); + process.nextTick(mustCall(() => { + server.listen(0, 'localhost', mustCall(() => { + const args = [ + '-o', 'UserKnownHostsFile=/dev/null', + '-o', 'StrictHostKeyChecking=no', + '-o', 'CheckHostIP=no', + '-o', 'ConnectTimeout=3', + '-o', 'GlobalKnownHostsFile=/dev/null', + '-o', 'GSSAPIAuthentication=no', + '-o', 'IdentitiesOnly=yes', + '-o', 'BatchMode=yes', + '-o', 'VerifyHostKeyDNS=no', + + '-vvvvvv', + '-T', + '-o', 'KbdInteractiveAuthentication=no', + '-o', 'HostbasedAuthentication=no', + '-o', 'PasswordAuthentication=no', + '-o', 'PubkeyAuthentication=yes', + '-o', 'PreferredAuthentications=publickey' + ]; + if (clientCfg.privateKeyPath) + args.push('-o', 'IdentityFile=' + clientCfg.privateKeyPath); if (!/^[0-6]\./.test(opensshVer)) { // OpenSSH 7.0+ disables DSS/DSA host (and user) key support by // default, so we explicitly enable it here @@ -367,7 +443,7 @@ function setup(self, clientcfg, servercfg) { 'localhost', 'uptime'); - client = spawn(cmd, args); + client = spawn(opensshPath, args, SPAWN_OPTS); server.emit('_child', client); if (DEBUG) { client.stdout.pipe(process.stdout); @@ -376,66 +452,91 @@ function setup(self, clientcfg, servercfg) { client.stdout.resume(); client.stderr.resume(); } - client.on('error', function(err) { + client.on('error', (err) => { onError(err, null, null); - }).on('exit', function(code) { + }).on('exit', (code) => { clearTimeout(client.timer); if (code !== 0) - return onError(new Error('Non-zero exit code ' + code), null, null); + return onError(new Error(`Non-zero exit code ${code}`), null, null); onClose(null, null, null); }); - client.timer = setTimeout(function() { - assert(false, makeMsg(self.what, 'Client timeout')); + client.timer = setTimeout(() => { + assert(false, msg('Client timeout')); }, CLIENT_TIMEOUT); - }); - }); + })); + })); + return server; } +const getParamNames = (() => { + const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; + const ARGUMENT_NAMES = /([^\s,]+)/g; + const toString = Function.prototype.toString; + return (fn) => { + const s = toString.call(fn).replace(STRIP_COMMENTS, ''); + const result = s.slice(s.indexOf('(') + 1, s.indexOf(')')) + .match(ARGUMENT_NAMES); + return (result || []); + }; +})(); + +function once(fn) { + let called = false; + return (...args) => { + if (called) + return; + called = true; + fn(...args); + }; +} + function next() { if (Array.isArray(process._events.exit)) process._events.exit = process._events.exit[1]; if (++t === tests.length) return; - var v = tests[t]; - v.run.call(v); + const v = tests[t]; + v.next = once(next); + v.msg = msg.bind(null, v.what); + v.run(v.msg, v.next); } -function makeMsg(what, msg) { - return '[' + group + what + ']: ' + msg; +function msg(what, desc) { + return `[${THIS_FILE}/${what}]: ${desc}`; } -process.once('uncaughtException', function(err) { - if (t > -1 && !/(?:^|\n)AssertionError: /i.test(''+err)) - console.log(makeMsg(tests[t].what, 'Unexpected Exception:')); - throw err; -}); -process.once('exit', function() { - assert(t === tests.length, - makeMsg('_exit', - 'Only finished ' + t + '/' + tests.length + ' tests')); +process.once('exit', () => { + const ran = Math.max(t, 0); + assert(ran === tests.length, + msg('(exit)', `Finished ${ran}/${tests.length} tests`)); }); -// Get OpenSSH client version first -exec(opensshPath + ' -V', function(err, stdout, stderr) { - if (err) { - console.log('OpenSSH client is required for these tests'); +{ + // Get OpenSSH client version first + const { + error, stderr, stdout + } = spawnSync(opensshPath, ['-V'], SPAWN_OPTS); + + if (error) { + console.error('OpenSSH client is required for these tests'); process.exitCode = 5; return; } - var re = /^OpenSSH_([\d\.]+)/; - var m = re.exec(stdout.toString()); + const re = /^OpenSSH_([\d.]+)/; + let m = re.exec(stdout.toString()); if (!m || !m[1]) { m = re.exec(stderr.toString()); if (!m || !m[1]) { - console.log('OpenSSH client is required for these tests'); + console.error('OpenSSH client is required for these tests'); process.exitCode = 5; return; } } opensshVer = m[1]; + console.log(`Testing with OpenSSH version: ${opensshVer}`); next(); -}); +} diff --git a/test/test-protocol-crypto.js b/test/test-protocol-crypto.js new file mode 100644 index 00000000..e5853fbf --- /dev/null +++ b/test/test-protocol-crypto.js @@ -0,0 +1,603 @@ +'use strict'; + +const assert = require('assert'); +const { randomBytes } = require('crypto'); + +const { + CIPHER_INFO, + MAC_INFO, + bindingAvailable, + NullCipher, + createCipher, + NullDecipher, + createDecipher, +} = require('../lib/protocol/crypto.js'); + + +console.log(`Crypto binding ${bindingAvailable ? '' : 'not '}available`); +{ + const PAIRS = [ + // cipher, decipher + ['native', 'native'], + ['binding', 'native'], + ['native', 'binding'], + ['binding', 'binding'], + ].slice(0, bindingAvailable ? 4 : 1); + + [ + { cipher: null }, + { cipher: 'chacha20-poly1305@openssh.com' }, + { cipher: 'aes128-gcm@openssh.com' }, + { cipher: 'aes128-cbc', mac: 'hmac-sha1-etm@openssh.com' }, + { cipher: 'aes128-ctr', mac: 'hmac-sha1' }, + { cipher: 'arcfour', mac: 'hmac-sha2-256-96' }, + ].forEach((testConfig) => { + for (const pair of PAIRS) { + function onCipherData(data) { + ciphered = Buffer.concat([ciphered, data]); + } + + function onDecipherPayload(payload) { + deciphered.push(payload); + } + + function reset() { + ciphered = Buffer.alloc(0); + deciphered = []; + } + + function reinit() { + if (testConfig.cipher === null) { + cipher = new NullCipher(1, onCipherData); + decipher = new NullDecipher(1, onDecipherPayload); + } else { + cipher = createCipher(config); + decipher = createDecipher(config); + } + } + + let ciphered; + let deciphered; + let cipher; + let decipher; + let macSize; + let packet; + let payload; + let cipherInfo; + let config; + + console.log('Testing cipher: %s, mac: %s (%s encrypt, %s decrypt) ...', + testConfig.cipher, + testConfig.mac + || (testConfig.cipher === null ? '' : ''), + pair[0], + pair[1]); + + if (testConfig.cipher === null) { + cipher = new NullCipher(1, onCipherData); + decipher = new NullDecipher(1, onDecipherPayload); + macSize = 0; + } else { + cipherInfo = CIPHER_INFO[testConfig.cipher]; + let macInfo; + let macKey; + if (testConfig.mac) { + macInfo = MAC_INFO[testConfig.mac]; + macKey = randomBytes(macInfo.len); + macSize = macInfo.actualLen; + } else if (cipherInfo.authLen) { + macSize = cipherInfo.authLen; + } else { + throw new Error('Missing MAC for cipher'); + } + const key = randomBytes(cipherInfo.keyLen); + const iv = (cipherInfo.ivLen + ? randomBytes(cipherInfo.ivLen) + : Buffer.alloc(0)); + config = { + outbound: { + onWrite: onCipherData, + cipherInfo, + cipherKey: Buffer.from(key), + cipherIV: Buffer.from(iv), + seqno: 1, + macInfo, + macKey: (macKey && Buffer.from(macKey)), + forceNative: (pair[0] === 'native'), + }, + inbound: { + onPayload: onDecipherPayload, + decipherInfo: cipherInfo, + decipherKey: Buffer.from(key), + decipherIV: Buffer.from(iv), + seqno: 1, + macInfo, + macKey: (macKey && Buffer.from(macKey)), + forceNative: (pair[1] === 'native'), + }, + }; + cipher = createCipher(config); + decipher = createDecipher(config); + + if (pair[0] === 'binding') + assert(/binding/i.test(cipher.constructor.name)); + else + assert(/native/i.test(cipher.constructor.name)); + if (pair[1] === 'binding') + assert(/binding/i.test(decipher.constructor.name)); + else + assert(/native/i.test(decipher.constructor.name)); + } + + let expectedSeqno; + // Test zero-length payload ============================================== + payload = Buffer.alloc(0); + expectedSeqno = 2; + + reset(); + packet = cipher.allocPacket(payload.length); + payload.copy(packet, 5); + cipher.encrypt(packet); + assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length), + undefined); + + assert.strictEqual(cipher.outSeqno, expectedSeqno); + assert(ciphered.length >= 9 + macSize); + assert.strictEqual(decipher.inSeqno, cipher.outSeqno); + assert.strictEqual(deciphered.length, 1); + assert.deepStrictEqual(deciphered[0], payload); + + // Test single byte payload ============================================== + payload = Buffer.from([ 0xEF ]); + expectedSeqno = 3; + + reset(); + packet = cipher.allocPacket(payload.length); + payload.copy(packet, 5); + cipher.encrypt(packet); + assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length), + undefined); + + assert.strictEqual(cipher.outSeqno, 3); + assert(ciphered.length >= 9 + macSize); + assert.strictEqual(decipher.inSeqno, cipher.outSeqno); + assert.strictEqual(deciphered.length, 1); + assert.deepStrictEqual(deciphered[0], payload); + + // Test large payload ==================================================== + payload = randomBytes(32 * 1024); + expectedSeqno = 4; + + reset(); + packet = cipher.allocPacket(payload.length); + payload.copy(packet, 5); + cipher.encrypt(packet); + assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length), + undefined); + + assert.strictEqual(cipher.outSeqno, expectedSeqno); + assert(ciphered.length >= 9 + macSize); + assert.strictEqual(decipher.inSeqno, cipher.outSeqno); + assert.strictEqual(deciphered.length, 1); + assert.deepStrictEqual(deciphered[0], payload); + + // Test sequnce number rollover ========================================== + payload = randomBytes(4); + expectedSeqno = 0; + cipher.outSeqno = decipher.inSeqno = (2 ** 32) - 1; + + reset(); + packet = cipher.allocPacket(payload.length); + payload.copy(packet, 5); + cipher.encrypt(packet); + assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length), + undefined); + + assert.strictEqual(cipher.outSeqno, expectedSeqno); + assert(ciphered.length >= 9 + macSize); + assert.strictEqual(decipher.inSeqno, cipher.outSeqno); + assert.strictEqual(deciphered.length, 1); + assert.deepStrictEqual(deciphered[0], payload); + + // Test chunked input -- split length bytes ============================== + payload = randomBytes(32 * 768); + expectedSeqno = 1; + + reset(); + packet = cipher.allocPacket(payload.length); + payload.copy(packet, 5); + cipher.encrypt(packet); + assert.strictEqual(decipher.decrypt(ciphered, 0, 2), undefined); + assert.strictEqual(decipher.decrypt(ciphered, 2, ciphered.length), + undefined); + + assert.strictEqual(cipher.outSeqno, expectedSeqno); + assert(ciphered.length >= 9 + macSize); + assert.strictEqual(decipher.inSeqno, cipher.outSeqno); + assert.strictEqual(deciphered.length, 1); + assert.deepStrictEqual(deciphered[0], payload); + + // Test chunked input -- split length from payload ======================= + payload = randomBytes(32 * 768); + expectedSeqno = 2; + + reset(); + packet = cipher.allocPacket(payload.length); + payload.copy(packet, 5); + cipher.encrypt(packet); + assert.strictEqual(decipher.decrypt(ciphered, 0, 4), undefined); + assert.strictEqual(decipher.decrypt(ciphered, 4, ciphered.length), + undefined); + + assert.strictEqual(cipher.outSeqno, expectedSeqno); + assert(ciphered.length >= 9 + macSize); + assert.strictEqual(decipher.inSeqno, cipher.outSeqno); + assert.strictEqual(deciphered.length, 1); + assert.deepStrictEqual(deciphered[0], payload); + + // Test chunked input -- split length and payload from MAC =============== + payload = randomBytes(32 * 768); + expectedSeqno = 3; + + reset(); + packet = cipher.allocPacket(payload.length); + payload.copy(packet, 5); + cipher.encrypt(packet); + assert.strictEqual( + decipher.decrypt(ciphered, 0, ciphered.length - macSize), + undefined + ); + assert.strictEqual( + decipher.decrypt(ciphered, ciphered.length - macSize, ciphered.length), + undefined + ); + + assert.strictEqual(cipher.outSeqno, expectedSeqno); + assert(ciphered.length >= 9 + macSize); + assert.strictEqual(decipher.inSeqno, cipher.outSeqno); + assert.strictEqual(deciphered.length, 1); + assert.deepStrictEqual(deciphered[0], payload); + + // Test packet length checks ============================================= + [0, 2 ** 32 - 1].forEach((n) => { + reset(); + packet = cipher.allocPacket(0); + packet.writeUInt32BE(n, 0); // Overwrite packet length field + cipher.encrypt(packet); + let threw = false; + try { + decipher.decrypt(ciphered, 0, ciphered.length); + } catch (ex) { + threw = true; + assert(ex instanceof Error); + assert(/packet length/i.test(ex.message)); + } + if (!threw) + throw new Error('Expected error'); + + // Recreate deciphers since errors leave them in an unusable state. + // We recreate the ciphers as well so that internal states of both ends + // match again. + reinit(); + }); + + // Test minimum padding length check ===================================== + if (testConfig.cipher !== null) { + let payloadLen; + const blockLen = cipherInfo.blockLen; + if (/chacha|gcm/i.test(testConfig.cipher) + || /etm/i.test(testConfig.mac)) { + payloadLen = blockLen - 2; + } else { + payloadLen = blockLen - 6; + } + const minLen = 4 + 1 + payloadLen + (blockLen + 1); + // We don't do strict equality checks here since the length of the + // returned Buffer can vary due to implementation details. + assert(cipher.allocPacket(payloadLen).length >= minLen); + } + + // ======================================================================= + cipher.free(); + decipher.free(); + if (testConfig.cipher === null) + break; + } + }); +} + +// Test createCipher()/createDecipher() exceptions +{ + [ + [ + [true, null], + /invalid config/i + ], + [ + [{}], + [/invalid outbound/i, /invalid inbound/i] + ], + [ + [{ outbound: {}, inbound: {} }], + [/invalid outbound\.onWrite/i, /invalid inbound\.onPayload/i] + ], + [ + [ + { outbound: { + onWrite: () => {}, + cipherInfo: true + }, + inbound: { + onPayload: () => {}, + decipherInfo: true + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: null + }, + inbound: { + onPayload: () => {}, + decipherInfo: null + }, + }, + ], + [/invalid outbound\.cipherInfo/i, /invalid inbound\.decipherInfo/i] + ], + [ + [ + { outbound: { + onWrite: () => {}, + cipherInfo: {}, + cipherKey: {}, + }, + inbound: { + onPayload: () => {}, + decipherInfo: {}, + decipherKey: {}, + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 32 }, + cipherKey: Buffer.alloc(8), + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 32 }, + decipherKey: Buffer.alloc(8), + }, + }, + ], + [/invalid outbound\.cipherKey/i, /invalid inbound\.decipherKey/i] + ], + [ + [ + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 12 }, + cipherKey: Buffer.alloc(1), + cipherIV: true + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 12 }, + decipherKey: Buffer.alloc(1), + cipherIV: true + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 12 }, + cipherKey: Buffer.alloc(1), + cipherIV: null + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 12 }, + decipherKey: Buffer.alloc(1), + cipherIV: null + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 12 }, + cipherKey: Buffer.alloc(1), + cipherIV: {} + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 12 }, + decipherKey: Buffer.alloc(1), + cipherIV: {} + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 12 }, + cipherKey: Buffer.alloc(1), + cipherIV: Buffer.alloc(1) + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 12 }, + decipherKey: Buffer.alloc(1), + cipherIV: Buffer.alloc(1) + }, + }, + ], + [/invalid outbound\.cipherIV/i, /invalid inbound\.decipherIV/i] + ], + [ + [ + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0 }, + cipherKey: Buffer.alloc(1), + seqno: true + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0 }, + decipherKey: Buffer.alloc(1), + seqno: true + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0 }, + cipherKey: Buffer.alloc(1), + seqno: -1 + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0 }, + decipherKey: Buffer.alloc(1), + seqno: -1 + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0 }, + cipherKey: Buffer.alloc(1), + seqno: 2 ** 32 + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0 }, + decipherKey: Buffer.alloc(1), + seqno: 2 ** 32 + }, + }, + ], + [/invalid outbound\.seqno/i, /invalid inbound\.seqno/i] + ], + [ + [ + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + cipherKey: Buffer.alloc(1), + seqno: 0 + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + decipherKey: Buffer.alloc(1), + seqno: 0 + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + cipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: true + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + decipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: true + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + cipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: null + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + decipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: null + }, + }, + ], + [/invalid outbound\.macInfo/i, /invalid inbound\.macInfo/i] + ], + [ + [ + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + cipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: { keyLen: 16 } + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + decipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: { keyLen: 16 } + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + cipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: { keyLen: 16 }, + macKey: true + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + decipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: { keyLen: 16 }, + macKey: true + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + cipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: { keyLen: 16 }, + macKey: null + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + decipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: { keyLen: 16 }, + macKey: null + }, + }, + { outbound: { + onWrite: () => {}, + cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + cipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: { keyLen: 16 }, + macKey: Buffer.alloc(1) + }, + inbound: { + onPayload: () => {}, + decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' }, + decipherKey: Buffer.alloc(1), + seqno: 0, + macInfo: { keyLen: 16 }, + macKey: Buffer.alloc(1) + }, + }, + ], + [/invalid outbound\.macKey/i, /invalid inbound\.macKey/i] + ], + ].forEach((testCase) => { + let errorChecks = testCase[1]; + if (!Array.isArray(errorChecks)) + errorChecks = [errorChecks[0], errorChecks[0]]; + for (const input of testCase[0]) { + assert.throws(() => createCipher(input), errorChecks[0]); + assert.throws(() => createDecipher(input), errorChecks[1]); + } + }); +} diff --git a/test/test-protocol-keyparser.js b/test/test-protocol-keyparser.js new file mode 100644 index 00000000..850d4058 --- /dev/null +++ b/test/test-protocol-keyparser.js @@ -0,0 +1,145 @@ +'use strict'; + +const assert = require('assert'); +const { readdirSync, readFileSync } = require('fs'); +const { inspect } = require('util'); + +const { parseKey } = require('../lib/protocol/keyParser.js'); + +const { EDDSA_SUPPORTED } = require('../lib/protocol/constants.js'); + +const BASE_PATH = `${__dirname}/fixtures/keyParser`; + +function failMsg(name, message, exit) { + const msg = `[${name}] ${message}`; + if (!exit) + return msg; + console.error(msg); + process.exit(1); +} + +readdirSync(BASE_PATH).forEach((name) => { + if (/\.result$/i.test(name)) + return; + if (/ed25519/i.test(name) && !EDDSA_SUPPORTED) + return; + + const isPublic = /\.pub$/i.test(name); + const isEncrypted = /_enc/i.test(name); + const isPPK = /^ppk_/i.test(name); + const key = readFileSync(`${BASE_PATH}/${name}`); + let res; + if (isEncrypted) + res = parseKey(key, (isPPK ? 'node.js' : 'password')); + else + res = parseKey(key); + let expected = JSON.parse( + readFileSync(`${BASE_PATH}/${name}.result`, 'utf8') + ); + if (typeof expected === 'string') { + if (!(res instanceof Error)) + failMsg(name, `Expected error: ${expected}`, true); + assert.strictEqual( + expected, + res.message, + failMsg(name, + 'Error message mismatch.\n' + + `Expected: ${inspect(expected)}\n` + + `Received: ${inspect(res.message)}`) + ); + } else if (res instanceof Error) { + failMsg(name, `Unexpected error: ${res.stack}`, true); + } else { + if (Array.isArray(expected) && !Array.isArray(res)) + failMsg(name, 'Expected array but did not receive one', true); + if (!Array.isArray(expected) && Array.isArray(res)) + failMsg(name, 'Received array but did not expect one', true); + + if (!Array.isArray(res)) { + res = [res]; + expected = [expected]; + } else if (res.length !== expected.length) { + failMsg(name, + `Expected ${expected.length} keys, but received ${res.length}`, + true); + } + + res.forEach((curKey, i) => { + const details = { + type: curKey.type, + comment: curKey.comment, + public: curKey.getPublicPEM(), + publicSSH: curKey.getPublicSSH() + && curKey.getPublicSSH().toString('base64'), + private: curKey.getPrivatePEM() + }; + assert.deepStrictEqual( + details, + expected[i], + failMsg(name, + 'Parser output mismatch.\n' + + `Expected: ${inspect(expected[i])}\n\n` + + `Received: ${inspect(details)}`) + ); + }); + } + + if (isEncrypted && !isPublic) { + // Make sure parsing encrypted keys without a passhprase or incorrect + // passphrase results in an appropriate error + const err = parseKey(key); + if (!(err instanceof Error)) + failMsg(name, 'Expected error during parse without passphrase', true); + if (!/no passphrase/i.test(err.message)) { + failMsg(name, + `Error during parse without passphrase: ${err.message}`, + true); + } + } + + if (!isPublic) { + // Try signing and verifying to make sure the private/public key PEMs are + // correct + const data = Buffer.from('hello world'); + res.forEach((curKey) => { + let result = curKey.sign(data); + if (result instanceof Error) { + failMsg(name, + `Error while signing data with key: ${result.message}`, + true); + } + result = curKey.verify(data, result); + if (result instanceof Error) { + failMsg(name, + `Error while verifying signed data with key: ${result.message}`, + true); + } + if (!result) + failMsg(name, 'Failed to verify signed data with key', true); + }); + if (res.length === 1 && !isPPK) { + const pubFile = readFileSync(`${BASE_PATH}/${name}.pub`); + const pubParsed = parseKey(pubFile); + if (!(pubParsed instanceof Error)) { + let result = res[0].sign(data); + if (result instanceof Error) { + failMsg(name, + `Error while signing data with key: ${result.message}`, + true); + } + result = pubParsed.verify(data, result); + if (result instanceof Error) { + failMsg(name, + 'Error while verifying signed data with separate public key: ' + + result.message, + true); + } + if (!result) { + failMsg(name, + 'Failed to verify signed data with separate public key', + true); + } + } + } + } +}); diff --git a/test/test-protocol-sftp.js b/test/test-protocol-sftp.js new file mode 100644 index 00000000..90262cbb --- /dev/null +++ b/test/test-protocol-sftp.js @@ -0,0 +1,944 @@ +'use strict'; + +const assert = require('assert'); +const { readFileSync, constants } = require('fs'); +const { join, basename } = require('path'); + +const { mustCall, mustCallAtLeast } = require('./common.js'); + +const Client = require('../lib/client.js'); +const Server = require('../lib/server.js'); +const { OPEN_MODE, Stats, STATUS_CODE } = require('../lib/protocol/SFTP.js'); + +let t = -1; +const THIS_FILE = basename(__filename, '.js'); +const fixturesDir = join(__dirname, 'fixtures'); +const fixture = (file) => readFileSync(join(fixturesDir, file)); + +const USER = 'nodejs'; +const PASSWORD = 'FLUXCAPACITORISTHEPOWER'; +const HOST_KEY_RSA = fixture('ssh_host_rsa_key'); +const DEBUG = false; +const DEFAULT_TEST_TIMEOUT = 30 * 1000; + +const tests = [ + // Successful client requests + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/tmp/foo.txt'; + const handle_ = Buffer.from('node.js'); + server.on('OPEN', mustCall((id, path, pflags, attrs) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + assert(pflags === (OPEN_MODE.TRUNC + | OPEN_MODE.CREAT + | OPEN_MODE.WRITE), + msg(`Wrong flags: ${flagsToHuman(pflags)}`)); + server.handle(id, handle_); + server.end(); + })); + client.open(path_, 'w', mustCall((err, handle) => { + assert(!err, msg('Unexpected open() error: ' + err)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + })); + }); + }), + what: 'open' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + server.on('CLOSE', mustCall((id, handle) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.close(handle_, mustCall((err) => { + assert(!err, msg('Unexpected close() error: ' + err)); + })); + }); + }), + what: 'close' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const expected = + Buffer.from('node.jsnode.jsnode.jsnode.jsnode.jsnode.js'); + const handle_ = Buffer.from('node.js'); + const buf = Buffer.alloc(expected.length); + server.on('READ', mustCall((id, handle, offset, len) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + assert(offset === 5, msg(`Wrong read offset: ${offset}`)); + assert(len === buf.length, msg(`Wrong read len: ${len}`)); + server.data(id, expected); + server.end(); + })); + client.read(handle_, buf, 0, buf.length, 5, mustCall((err, nb) => { + assert(!err, msg('Unexpected read() error: ' + err)); + assert.deepStrictEqual(buf, expected, msg('read data mismatch')); + })); + }); + }), + what: 'read' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + const buf = Buffer.from('node.jsnode.jsnode.jsnode.jsnode.jsnode.js'); + server.on('WRITE', mustCall((id, handle, offset, data) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + assert(offset === 5, msg(`Wrong write offset: ${offset}`)); + assert.deepStrictEqual(data, buf, msg('write data mismatch')); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.write(handle_, buf, 0, buf.length, 5, mustCall((err, nb) => { + assert(!err, msg(`Unexpected write() error: ${err}`)); + assert.strictEqual(nb, buf.length, msg('wrong bytes written')); + })); + }); + }), + what: 'write' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + const buf = Buffer.allocUnsafe(3 * 32 * 1024); + let reqs = 0; + server.on('WRITE', mustCall((id, handle, offset, data) => { + ++reqs; + assert.strictEqual(id, reqs - 1, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + assert.strictEqual(offset, + (reqs - 1) * 32 * 1024, + msg(`Wrong write offset: ${offset}`)); + assert((offset + data.length) <= buf.length, msg('bad offset')); + assert.deepStrictEqual(data, + buf.slice(offset, offset + data.length), + msg('write data mismatch')); + server.status(id, STATUS_CODE.OK); + if (reqs === 3) + server.end(); + }, 3)); + client.write(handle_, buf, 0, buf.length, 0, mustCall((err, nb) => { + assert(!err, msg('Unexpected write() error: ' + err)); + assert.strictEqual(nb, buf.length, msg('wrote bytes written')); + })); + }); + }), + what: 'write (overflow)' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + const attrs_ = new Stats({ + size: 10 * 1024, + uid: 9001, + gid: 9001, + atime: (Date.now() / 1000) | 0, + mtime: (Date.now() / 1000) | 0 + }); + server.on('LSTAT', mustCall((id, path) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + server.attrs(id, attrs_); + server.end(); + })); + client.lstat(path_, mustCall((err, attrs) => { + assert(!err, msg(`Unexpected lstat() error: ${err}`)); + assert.deepStrictEqual(attrs, attrs_, msg('attrs mismatch')); + })); + }); + }), + what: 'lstat' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + const attrs_ = new Stats({ + size: 10 * 1024, + uid: 9001, + gid: 9001, + atime: (Date.now() / 1000) | 0, + mtime: (Date.now() / 1000) | 0 + }); + server.on('FSTAT', mustCall((id, handle) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.attrs(id, attrs_); + server.end(); + })); + client.fstat(handle_, mustCall((err, attrs) => { + assert(!err, msg(`Unexpected fstat() error: ${err}`)); + assert.deepStrictEqual(attrs, attrs_, msg('attrs mismatch')); + })); + }); + }), + what: 'fstat' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + const attrs_ = new Stats({ + uid: 9001, + gid: 9001, + atime: (Date.now() / 1000) | 0, + mtime: (Date.now() / 1000) | 0 + }); + server.on('SETSTAT', mustCall((id, path, attrs) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + assert.deepStrictEqual(attrs, attrs_, msg('attrs mismatch')); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.setstat(path_, attrs_, mustCall((err) => { + assert(!err, msg(`Unexpected setstat() error: ${err}`)); + })); + }); + }), + what: 'setstat' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + const attrs_ = new Stats({ + uid: 9001, + gid: 9001, + atime: (Date.now() / 1000) | 0, + mtime: (Date.now() / 1000) | 0 + }); + server.on('FSETSTAT', mustCall((id, handle, attrs) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + assert.deepStrictEqual(attrs, attrs_, msg('attrs mismatch')); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.fsetstat(handle_, attrs_, mustCall((err) => { + assert(!err, msg(`Unexpected fsetstat() error: ${err}`)); + })); + }); + }), + what: 'fsetstat' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + const path_ = '/tmp'; + server.on('OPENDIR', mustCall((id, path) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + server.handle(id, handle_); + server.end(); + })); + client.opendir(path_, mustCall((err, handle) => { + assert(!err, msg(`Unexpected opendir() error: ${err}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + })); + }); + }), + what: 'opendir' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + const list_ = [ + { filename: '.', + longname: 'drwxr-xr-x 56 nodejs nodejs 4096 Nov 10 01:05 .', + attrs: new Stats({ + mode: 0o755 | constants.S_IFDIR, + size: 4096, + uid: 9001, + gid: 8001, + atime: 1415599549, + mtime: 1415599590 + }) + }, + { filename: '..', + longname: 'drwxr-xr-x 4 root root 4096 May 16 2013 ..', + attrs: new Stats({ + mode: 0o755 | constants.S_IFDIR, + size: 4096, + uid: 0, + gid: 0, + atime: 1368729954, + mtime: 1368729999 + }) + }, + { filename: 'foo', + longname: 'drwxrwxrwx 2 nodejs nodejs 4096 Mar 8 2009 foo', + attrs: new Stats({ + mode: 0o777 | constants.S_IFDIR, + size: 4096, + uid: 9001, + gid: 8001, + atime: 1368729954, + mtime: 1368729999 + }) + }, + { filename: 'bar', + longname: '-rw-r--r-- 1 nodejs nodejs 513901992 Dec 4 2009 bar', + attrs: new Stats({ + mode: 0o644 | constants.S_IFREG, + size: 513901992, + uid: 9001, + gid: 8001, + atime: 1259972199, + mtime: 1259972199 + }) + } + ]; + server.on('READDIR', mustCall((id, handle) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.name(id, list_); + server.end(); + })); + client.readdir(handle_, mustCall((err, list) => { + assert(!err, msg(`Unexpected readdir() error: ${err}`)); + assert.deepStrictEqual(list, + list_.slice(2), + msg('dir list mismatch')); + })); + }); + }), + what: 'readdir' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + const list_ = [ + { filename: '.', + longname: 'drwxr-xr-x 56 nodejs nodejs 4096 Nov 10 01:05 .', + attrs: new Stats({ + mode: 0o755 | constants.S_IFDIR, + size: 4096, + uid: 9001, + gid: 8001, + atime: 1415599549, + mtime: 1415599590 + }) + }, + { filename: '..', + longname: 'drwxr-xr-x 4 root root 4096 May 16 2013 ..', + attrs: new Stats({ + mode: 0o755 | constants.S_IFDIR, + size: 4096, + uid: 0, + gid: 0, + atime: 1368729954, + mtime: 1368729999 + }) + }, + { filename: 'foo', + longname: 'drwxrwxrwx 2 nodejs nodejs 4096 Mar 8 2009 foo', + attrs: new Stats({ + mode: 0o777 | constants.S_IFDIR, + size: 4096, + uid: 9001, + gid: 8001, + atime: 1368729954, + mtime: 1368729999 + }) + }, + { filename: 'bar', + longname: '-rw-r--r-- 1 nodejs nodejs 513901992 Dec 4 2009 bar', + attrs: new Stats({ + mode: 0o644 | constants.S_IFREG, + size: 513901992, + uid: 9001, + gid: 8001, + atime: 1259972199, + mtime: 1259972199 + }) + } + ]; + server.on('READDIR', mustCall((id, handle) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.name(id, list_); + server.end(); + })); + client.readdir(handle_, { full: true }, mustCall((err, list) => { + assert(!err, msg(`Unexpected readdir() error: ${err}`)); + assert.deepStrictEqual(list, list_, msg('dir list mismatch')); + })); + }); + }), + what: 'readdir (full)' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + server.on('REMOVE', mustCall((id, path) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.unlink(path_, mustCall((err) => { + assert(!err, msg(`Unexpected unlink() error: ${err}`)); + })); + }); + }), + what: 'unlink' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + server.on('MKDIR', mustCall((id, path) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.mkdir(path_, mustCall((err) => { + assert(!err, msg(`Unexpected mkdir() error: ${err}`)); + })); + }); + }), + what: 'mkdir' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + server.on('RMDIR', mustCall((id, path) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.rmdir(path_, mustCall((err) => { + assert(!err, msg(`Unexpected rmdir() error: ${err}`)); + })); + }); + }), + what: 'rmdir' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + const name_ = { filename: '/tmp/foo' }; + server.on('REALPATH', mustCall((id, path) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + server.name(id, name_); + server.end(); + })); + client.realpath(path_, mustCall((err, name) => { + assert(!err, msg(`Unexpected realpath() error: ${err}`)); + assert.deepStrictEqual(name, name_.filename, msg('name mismatch')); + })); + }); + }), + what: 'realpath' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + const attrs_ = new Stats({ + size: 10 * 1024, + uid: 9001, + gid: 9001, + atime: (Date.now() / 1000) | 0, + mtime: (Date.now() / 1000) | 0 + }); + server.on('STAT', mustCall((id, path) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + server.attrs(id, attrs_); + server.end(); + })); + client.stat(path_, mustCall((err, attrs) => { + assert(!err, msg(`Unexpected stat() error: ${err}`)); + assert.deepStrictEqual(attrs, attrs_, msg('attrs mismatch')); + })); + }); + }), + what: 'stat' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const oldPath_ = '/foo/bar/baz'; + const newPath_ = '/tmp/foo'; + server.on('RENAME', mustCall((id, oldPath, newPath) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(oldPath === oldPath_, msg(`Wrong old path: ${oldPath}`)); + assert(newPath === newPath_, msg(`Wrong new path: ${newPath}`)); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.rename(oldPath_, newPath_, mustCall((err) => { + assert(!err, msg(`Unexpected rename() error: ${err}`)); + })); + }); + }), + what: 'rename' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const linkPath_ = '/foo/bar/baz'; + const name = { filename: '/tmp/foo' }; + server.on('READLINK', mustCall((id, linkPath) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(linkPath === linkPath_, msg(`Wrong link path: ${linkPath}`)); + server.name(id, name); + server.end(); + })); + client.readlink(linkPath_, mustCall((err, targetPath) => { + assert(!err, msg(`Unexpected readlink() error: ${err}`)); + assert(targetPath === name.filename, + msg(`Wrong target path: ${targetPath}`)); + })); + }); + }), + what: 'readlink' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const linkPath_ = '/foo/bar/baz'; + const targetPath_ = '/tmp/foo'; + server.on('SYMLINK', mustCall((id, linkPath, targetPath) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(linkPath === linkPath_, + msg(`Wrong link path: ${linkPath}`)); + assert(targetPath === targetPath_, + msg(`Wrong target path: ${targetPath}`)); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.symlink(targetPath_, linkPath_, mustCall((err) => { + assert(!err, msg(`Unexpected symlink() error: ${err}`)); + })); + }); + }), + what: 'symlink' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + const handle_ = Buffer.from('hi mom!'); + const data_ = Buffer.from('hello world'); + server.on('OPEN', mustCall((id, path, pflags, attrs) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + assert(pflags === OPEN_MODE.READ, + msg(`Wrong flags: ${flagsToHuman(pflags)}`)); + server.handle(id, handle_); + })).on('FSTAT', mustCall((id, handle) => { + assert(id === 1, msg(`Wrong request id: ${id}`)); + const attrs = new Stats({ + size: data_.length, + uid: 9001, + gid: 9001, + atime: (Date.now() / 1000) | 0, + mtime: (Date.now() / 1000) | 0 + }); + server.attrs(id, attrs); + })).on('READ', mustCall((id, handle, offset, len) => { + assert(id === 2, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + assert(offset === 0, msg(`Wrong read offset: ${offset}`)); + server.data(id, data_); + })).on('CLOSE', mustCall((id, handle) => { + assert(id === 3, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.readFile(path_, mustCall((err, buf) => { + assert(!err, msg(`Unexpected error: ${err}`)); + assert.deepStrictEqual(buf, data_, msg('data mismatch')); + })); + }); + }), + what: 'readFile' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + const handle_ = Buffer.from('hi mom!'); + const data_ = Buffer.from('hello world'); + let reads = 0; + server.on('OPEN', mustCall((id, path, pflags, attrs) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + assert(pflags === OPEN_MODE.READ, + msg(`Wrong flags: ${flagsToHuman(pflags)}`)); + server.handle(id, handle_); + })).on('FSTAT', mustCall((id, handle) => { + assert(id === 1, msg(`Wrong request id: ${id}`)); + const attrs = new Stats({ + uid: 9001, + gid: 9001, + atime: (Date.now() / 1000) | 0, + mtime: (Date.now() / 1000) | 0 + }); + server.attrs(id, attrs); + })).on('READ', mustCall((id, handle, offset, len) => { + assert(++reads + 1 === id, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + switch (id) { + case 2: + assert(offset === 0, + msg(`Wrong read offset for first read: ${offset}`)); + server.data(id, data_); + break; + case 3: + assert(offset === data_.length, + msg(`Wrong read offset for second read: ${offset}`)); + server.status(id, STATUS_CODE.EOF); + break; + } + }, 2)).on('CLOSE', mustCall((id, handle) => { + assert(id === 4, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + client.readFile(path_, mustCall((err, buf) => { + assert(!err, msg(`Unexpected error: ${err}`)); + assert.deepStrictEqual(buf, data_, msg('data mismatch')); + })); + }); + }), + what: 'readFile (no size from fstat)' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + let reads = 0; + const path_ = '/foo/bar/baz'; + const handle_ = Buffer.from('hi mom!'); + const data_ = Buffer.from('hello world'); + server.on('OPEN', mustCall((id, path, pflags, attrs) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + assert(pflags === OPEN_MODE.READ, + msg(`Wrong flags: ${flagsToHuman(pflags)}`)); + server.handle(id, handle_); + })).on('READ', mustCall((id, handle, offset, len) => { + assert(id === ++reads, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + if (reads === 1) { + assert(offset === 0, msg(`Wrong read offset: ${offset}`)); + server.data(id, data_); + } else { + server.status(id, STATUS_CODE.EOF); + } + }, 2)).on('CLOSE', mustCall((id, handle) => { + assert(id === 3, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + let buf = []; + client.createReadStream(path_) + .on('readable', mustCallAtLeast(function() { + let chunk; + while ((chunk = this.read()) !== null) + buf.push(chunk); + })).on('end', mustCall(() => { + buf = Buffer.concat(buf); + assert.deepStrictEqual(buf, data_, msg('data mismatch')); + })); + }); + }), + what: 'ReadStream' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + const handle_ = Buffer.from('hi mom!'); + const data_ = Buffer.from('hello world'); + server.on('OPEN', mustCall((id, path, pflags, attrs) => { + server.handle(id, handle_); + })).on('READ', mustCallAtLeast((id, handle, offset, len) => { + if (offset > data_.length) { + server.status(id, STATUS_CODE.EOF); + } else { + // Only read 4 bytes at a time + server.data(id, data_.slice(offset, offset + 4)); + } + })).on('CLOSE', mustCall((id, handle) => { + server.status(id, STATUS_CODE.OK); + server.end(); + })); + let buf = []; + client.createReadStream(path_) + .on('readable', mustCallAtLeast(function() { + let chunk; + while ((chunk = this.read()) !== null) + buf.push(chunk); + })).on('end', mustCall(() => { + buf = Buffer.concat(buf); + assert.deepStrictEqual(buf, data_, msg('data mismatch')); + })); + }); + }), + what: 'ReadStream (fewer bytes than requested)' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const path_ = '/foo/bar/baz'; + server.on('OPEN', mustCall((id, path, pflags, attrs) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + assert(pflags === OPEN_MODE.READ, + msg(`Wrong flags: ${flagsToHuman(pflags)}`)); + server.status(id, STATUS_CODE.NO_SUCH_FILE); + server.end(); + })); + client.createReadStream(path_).on('error', mustCall((err) => { + assert(err.code === STATUS_CODE.NO_SUCH_FILE); + })); + }); + }), + what: 'ReadStream (error)' + }, + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + let writes = 0; + const path_ = '/foo/bar/baz'; + const handle_ = Buffer.from('hi mom!'); + const data_ = Buffer.from('hello world'); + const expFlags = OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE; + server.on('OPEN', mustCall((id, path, pflags, attrs) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert(path === path_, msg(`Wrong path: ${path}`)); + assert(pflags === expFlags, + msg(`Wrong flags: ${flagsToHuman(pflags)}`)); + server.handle(id, handle_); + })).on('FSETSTAT', mustCall((id, handle, attrs) => { + assert(id === 1, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + assert.strictEqual(attrs.mode, 0o666, msg('Wrong file mode')); + server.status(id, STATUS_CODE.OK); + })).on('WRITE', mustCall((id, handle, offset, data) => { + assert(id === ++writes + 1, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + assert(offset === ((writes - 1) * data_.length), + msg(`Wrong write offset: ${offset}`)); + assert.deepStrictEqual(data, data_, msg('Wrong data')); + server.status(id, STATUS_CODE.OK); + }, 3)).on('CLOSE', mustCall((id, handle) => { + assert(id === 5, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.status(id, STATUS_CODE.OK); + server.end(); + })); + + const writer = client.createWriteStream(path_); + writer.cork && writer.cork(); + writer.write(data_); + writer.write(data_); + writer.write(data_); + writer.uncork && writer.uncork(); + writer.end(); + }); + }), + what: 'WriteStream' + }, + + // Other client request scenarios + { run: mustCall(function(msg) { + this.onReady = mustCall((client, server) => { + const handle_ = Buffer.from('node.js'); + server.on('READDIR', mustCall((id, handle) => { + assert(id === 0, msg(`Wrong request id: ${id}`)); + assert.deepStrictEqual(handle, handle_, msg('handle mismatch')); + server.status(id, STATUS_CODE.EOF); + server.end(); + })); + client.readdir(handle_, mustCall((err, list) => { + assert(err && err.code === STATUS_CODE.EOF, + msg(`Expected EOF, got: ${err}`)); + })); + }); + }), + what: 'readdir (EOF)' + }, +]; + +function setup(self, clientCfg, serverCfg, timeout) { + const { next, msg } = self; + let clientReady = false; + let serverReady = false; + let clientSFTP = false; + let serverSFTP = false; + let clientClose = false; + let serverClose = false; + + if (DEBUG) { + console.log('========================================================\n' + + `[TEST] ${self.what}\n` + + '========================================================'); + clientCfg.debug = (...args) => { + console.log(`[${self.what}][CLIENT]`, ...args); + }; + serverCfg.debug = (...args) => { + console.log(`[${self.what}][SERVER]`, ...args); + }; + } + + const client = new Client(); + const server = new Server(serverCfg); + if (timeout === undefined) + timeout = DEFAULT_TEST_TIMEOUT; + let timer; + + server.on('error', onError) + .on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { + ctx.accept(); + })).on('error', onError) + .on('ready', mustCall(onReady)); + server.close(); + })) + .on('close', mustCall(onClose)); + client.on('error', onError) + .on('ready', mustCall(onReady)) + .on('close', mustCall(onClose)); + + function onError(err) { + const which = (this === client ? 'client' : 'server'); + assert(false, msg(`Unexpected ${which} error: ${err}`)); + } + + function onSFTP() { + if (clientSFTP && serverSFTP) + self.onReady(clientSFTP, serverSFTP); + } + + function onReady() { + if (this === client) { + assert(!clientReady, + msg('Received multiple ready events for client')); + clientReady = true; + this.sftp(mustCall((err, sftp) => { + assert(!err, msg(`Unexpected client sftp start error: ${err}`)); + clientSFTP = sftp; + sftp.on('end', mustCall(() => { + this.end(); + })); + onSFTP.call(this); + })); + } else { + assert(!serverReady, + msg('Received multiple ready events for server')); + serverReady = true; + this.once('session', mustCall((accept, reject) => { + accept().once('sftp', mustCall((accept, reject) => { + const sftp = accept(); + serverSFTP = sftp; + sftp.on('end', mustCall(() => { + this.end(); + })); + onSFTP.call(this); + })); + })); + } + } + + function onClose() { + if (this === client) { + assert(!clientClose, + msg('Received multiple close events for client')); + clientClose = true; + } else { + assert(!serverClose, + msg('Received multiple close events for server')); + serverClose = true; + } + if (clientClose + && serverClose + && !getParamNames(self.run.origFn || self.run).includes('next')) { + clearTimeout(timer); + next(); + } + } + + process.nextTick(mustCall(() => { + server.listen(0, 'localhost', mustCall(() => { + if (timeout >= 0) { + timer = setTimeout(() => { + assert(false, msg('Test timed out')); + }, timeout); + } + if (clientCfg.sock) { + clientCfg.sock.connect(server.address().port, 'localhost'); + } else { + clientCfg.host = 'localhost'; + clientCfg.port = server.address().port; + } + client.connect(clientCfg); + })); + })); + + return { client, server }; +} + +function flagsToHuman(flags) { + const ret = []; + + for (const [name, value] of Object.entries(OPEN_MODE)) { + if (flags & value) + ret.push(name); + } + + return ret.join(' | '); +} + +const getParamNames = (() => { + const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; + const ARGUMENT_NAMES = /([^\s,]+)/g; + const toString = Function.prototype.toString; + return (fn) => { + const s = toString.call(fn).replace(STRIP_COMMENTS, ''); + const result = s.slice(s.indexOf('(') + 1, s.indexOf(')')) + .match(ARGUMENT_NAMES); + return (result || []); + }; +})(); + +function once(fn) { + let called = false; + return (...args) => { + if (called) + return; + called = true; + fn(...args); + }; +} + +function next() { + if (Array.isArray(process._events.exit)) + process._events.exit = process._events.exit[1]; + if (++t === tests.length) + return; + + const v = tests[t]; + v.next = once(next); + v.msg = msg.bind(null, v.what); + v.run(v.msg, v.next); + setup( + v, + { username: USER, password: PASSWORD }, + { hostKeys: [HOST_KEY_RSA] } + ); +} + +function msg(what, desc) { + return `[${THIS_FILE}/${what}]: ${desc}`; +} + +process.once('exit', () => { + const ran = Math.max(t, 0); + assert(ran === tests.length, + msg('(exit)', `Finished ${ran}/${tests.length} tests`)); +}); + +next(); diff --git a/test/test.js b/test/test.js index 4a91765c..c20e8af8 100644 --- a/test/test.js +++ b/test/test.js @@ -1,22 +1,13 @@ -var spawn = require('child_process').spawn, - join = require('path').join; +'use strict'; -var files = require('fs').readdirSync(__dirname).filter(function(f) { - return (f.substr(0, 5) === 'test-'); - }).map(function(f) { - return join(__dirname, f); - }), - f = -1; +const { execSync } = require('child_process'); +const { readdirSync } = require('fs'); +const { join } = require('path'); -function next() { - if (++f < files.length) { - spawn(process.argv[0], [ files[f] ], { stdio: 'inherit' }) - .on('exit', function(code) { - if (code === 0) - process.nextTick(next); - else - process.exit(code); - }); +for (const filename of readdirSync(__dirname)) { + if (filename.startsWith('test-')) { + const path = join(__dirname, filename); + console.log(`> Running ${filename} ...`); + execSync(`${process.argv[0]} ${path}`, { stdio: 'inherit' }); } } -next();