VYPR
High severityNVD Advisory· Published Sep 20, 2021· Updated Aug 4, 2024

Command injection in mscdex/ssh2

CVE-2020-26301

Description

ssh2 is client and server modules written in pure JavaScript for node.js. In ssh2 before version 1.4.0 there is a command injection vulnerability. The issue only exists on Windows. This issue may lead to remote code execution if a client of the library calls the vulnerable method with untrusted input. This is fixed in version 1.4.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ssh2npm
< 1.4.01.4.0

Affected products

1

Patches

1
f763271f4132

examples,lib,test: switch to code rewrite

https://github.com/mscdex/ssh2Brian WhiteOct 7, 2020via ghsa
129 files changed · +23296 4446
  • .eslintignore+4 0 added
    @@ -0,0 +1,4 @@
    +node_modules
    +lib/protocol/crypto/poly1305.js
    +.eslint-plugins
    +!.eslintrc.js
    
  • .eslint-plugins/eslint-plugin-mscdex/index.js+22 0 added
    @@ -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;
    +  },
    +};
    
  • .eslint-plugins/eslint-plugin-mscdex/rules/curly.js+500 0 added
    @@ -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();
    +            }
    +        };
    +    }
    +};
    
  • .eslint-plugins/eslint-plugin-mscdex/rules/quotes.js+332 0 added
    @@ -0,0 +1,332 @@
    +/**
    + * @fileoverview A rule to choose between single and double quote marks
    + * @author Matt DuVall <http://www.mattduvall.com/>, 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:
    +         *
    +         * - `<div className="foo"></div>` ... If the literal is an attribute value, the parent of the literal is `JSXAttribute`.
    +         * - `<div>foo</div>` ... 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:
    +         *
    +         * - `<div className={"foo"}></div>`
    +         * - `<div>{"foo"}</div>`
    +         *
    +         * 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)));
    +                    }
    +                });
    +            }
    +        };
    +
    +    }
    +};
    
  • .eslintrc.js+212 0 added
    @@ -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',
    +  },
    +};
    
  • examples/server-chat.js+59 64 modified
    @@ -4,34 +4,35 @@
     //      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;
       return msg;
     }
     
     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,41 +41,43 @@ 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;
             }
           }
           if (ok) {
             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();
    -}
    
  • examples/sftp-server-download-only.js+90 69 modified
    @@ -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}`);
     });
    
  • install.js+17 0 added
    @@ -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);
    
  • lib/agent.js+202 195 modified
    @@ -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 = /^\!<socket >(\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 = /^!<socket >(\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,15 +56,15 @@ module.exports = function(sockPath, key, keyType, data, cb) {
       }
     
       function onconnect() {
    -    var buf;
    +    let buf;
         if (isSigning) {
           /*
             byte        SSH2_AGENTC_SIGN_REQUEST
             string      key_blob
             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,71 +200,65 @@ 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() {
                 bc = 0;
                 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;
                     state = 'creds';
                     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,24 +280,29 @@ 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);
                 sock.on('data', _ondata);
                 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');
    +    }
       };
     }
    
  • lib/buffer-helpers.js+0 22 removed
    @@ -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;
    -  }
    -};
    
  • lib/Channel.js+234 450 modified
    @@ -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;
    
  • lib/client.js+1390 1263 modified
    @@ -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
    -
    ... [truncated]
    
  • lib/http-agents.js+54 51 modified
    @@ -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);
     }
    
  • lib/index.js+26 0 added
    @@ -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,
    +    },
    +  },
    +};
    
  • lib/keepalivemgr.js+0 80 removed
    @@ -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;
    
  • lib/protocol/constants.js+345 0 added
    @@ -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 }), {});
    
  • lib/protocol/crypto/binding.gyp+14 0 added
    @@ -0,0 +1,14 @@
    +{
    +  'targets': [
    +    {
    +      'target_name': 'sshcrypto',
    +      'include_dirs': [
    +        "<!(node -e \"require('nan')\")",
    +      ],
    +      'sources': [
    +        'src/binding.cc'
    +      ],
    +      'cflags': [ '-O3' ],
    +    },
    +  ],
    +}
    
  • lib/protocol/crypto.js+1607 0 added
    @@ -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,
    +};
    
  • lib/protocol/crypto/poly1305.js+21 0 added
    @@ -0,0 +1,21 @@
    +
    +var b;b||(b=typeof Module !== 'undefined' ? Module : {});var q={},t;for(t in b)b.hasOwnProperty(t)&&(q[t]=b[t]);var v=!1,w=!1,x=!1,y=!1;v="object"===typeof window;w="function"===typeof importScripts;x="object"===typeof process&&"object"===typeof process.versions&&"string"===typeof process.versions.node;y=!v&&!x&&!w;var z="",B,C,D,E;
    +if(x)z=w?require("path").dirname(z)+"/":__dirname+"/",B=function(a,c){var d=F(a);if(d)return c?d:d.toString();D||(D=require("fs"));E||(E=require("path"));a=E.normalize(a);return D.readFileSync(a,c?null:"utf8")},C=function(a){a=B(a,!0);a.buffer||(a=new Uint8Array(a));assert(a.buffer);return a},1<process.argv.length&&process.argv[1].replace(/\\/g,"/"),process.argv.slice(2),"undefined"!==typeof module&&(module.exports=b),process.on("unhandledRejection",G),b.inspect=function(){return"[Emscripten Module object]"};
    +else if(y)"undefined"!=typeof read&&(B=function(a){var c=F(a);return c?H(c):read(a)}),C=function(a){var c;if(c=F(a))return c;if("function"===typeof readbuffer)return new Uint8Array(readbuffer(a));c=read(a,"binary");assert("object"===typeof c);return c},"undefined"!==typeof print&&("undefined"===typeof console&&(console={}),console.log=print,console.warn=console.error="undefined"!==typeof printErr?printErr:print);else if(v||w)w?z=self.location.href:document.currentScript&&(z=document.currentScript.src),
    +z=0!==z.indexOf("blob:")?z.substr(0,z.lastIndexOf("/")+1):"",B=function(a){try{var c=new XMLHttpRequest;c.open("GET",a,!1);c.send(null);return c.responseText}catch(d){if(a=F(a))return H(a);throw d;}},w&&(C=function(a){try{var c=new XMLHttpRequest;c.open("GET",a,!1);c.responseType="arraybuffer";c.send(null);return new Uint8Array(c.response)}catch(d){if(a=F(a))return a;throw d;}});var ba=b.print||console.log.bind(console),I=b.printErr||console.warn.bind(console);
    +for(t in q)q.hasOwnProperty(t)&&(b[t]=q[t]);q=null;var J;b.wasmBinary&&(J=b.wasmBinary);var noExitRuntime;b.noExitRuntime&&(noExitRuntime=b.noExitRuntime);"object"!==typeof WebAssembly&&I("no native wasm support detected");var K,ca=new WebAssembly.Table({initial:1,maximum:1,element:"anyfunc"}),L=!1;function assert(a,c){a||G("Assertion failed: "+c)}function M(a){var c=b["_"+a];assert(c,"Cannot call unknown function "+a+", make sure it is exported");return c}
    +function da(a,c,d,e){var k={string:function(f){var n=0;if(null!==f&&void 0!==f&&0!==f){var m=(f.length<<2)+1;n=N(m);var h=n,g=O;if(0<m){m=h+m-1;for(var u=0;u<f.length;++u){var l=f.charCodeAt(u);if(55296<=l&&57343>=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<e.length;r++){var aa=k[d[r]];aa?(0===a&&(a=ea()),A[r]=aa(e[r])):A[r]=e[r]}d=p.apply(null,A);d=function(f){if("string"===c)if(f){for(var n=O,m=f+NaN,h=f;n[h]&&!(h>=m);)++h;if(16<h-f&&n.subarray&&fa)f=fa.decode(n.subarray(f,h));else{for(m="";f<h;){var g=n[f++];if(g&128){var u=n[f++]&63;if(192==(g&224))m+=String.fromCharCode((g&31)<<6|u);else{var l=n[f++]&63;g=224==(g&240)?(g&15)<<12|u<<6|l:(g&7)<<
    +18|u<<12|l<<6|n[f++]&63;65536>g?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<a.length;){var c=a.shift();if("function"==typeof c)c(b);else{var d=c.j;"number"===typeof d?void 0===c.i?b.dynCall_v(d):b.dynCall_vi(d,c.i):d(void 0===c.i?null:c.i)}}}var la=[],ma=[],oa=[],pa=[];function qa(){var a=b.preRun.shift();la.unshift(a)}var S=0,T=null,U=null;b.preloadedImages={};b.preloadedAudios={};function G(a){if(b.onAbort)b.onAbort(a);ba(a);I(a);L=!0;throw new WebAssembly.RuntimeError("abort("+a+"). Build with -s ASSERTIONS=1 for more info.");}
    +var V="data:application/octet-stream;base64,",W="data:application/octet-stream;base64,AGFzbQEAAAABJQdgAX8Bf2ACf38AYAF/AGADf39/AGAAAGAGf39/f39/AGAAAX8CFwIBYQFhAAABYQZtZW1vcnkCAYACgIACAw4NAAMDAgABBQECAAYBBAYJAX8BQYCMwAILBx0HAWIADQFjAAcBZAAFAWUABAFmAAsBZwAKAWgACQrkSw1BAQJ/PwAhAQJAQYAMKAIAIgIgAEEDakF8cWoiACABQRB0TQ0AIAAQAA0AQYAIQTA2AgBBfw8LQYAMIAA2AgAgAguVBQIKfw5+IAAoAiQhBiAAKAIgIQcgACgCHCEDIAAoAhghBCAAKAIUIQUgAkEQTwRAIAAtAExFQRh0IQkgACgCBCIIQQVsrSEZIAAoAggiCkEFbK0hFyAAKAIMIgtBBWytIRUgACgCECIMQQVsrSETIAytIRogC60hGCAKrSEWIAitIRQgADUCACESA0AgBCABLQADIgQgAS0ABEEIdHIgAS0ABUEQdHIgAS0ABiIIQRh0ckECdkH///8fcWqtIg0gGH4gBEEYdEGAgIAYcSABLwAAIAEtAAJBEHRyciAFaq0iDiAafnwgAyABLQAHQQh0IAhyIAEtAAhBEHRyIAEtAAkiA0EYdHJBBHZB////H3FqrSIPIBZ+fCABLQAKQQh0IANyIAEtAAtBEHRyIAEtAAxBGHRyQQZ2IAdqrSIQIBR+fCAJIAEtAA1yIAEtAA5BCHRyIAEtAA9BEHRyIAZqrSIRIBJ+fCANIBZ+IA4gGH58IA8gFH58IBAgEn58IBEgE358IA0gFH4gDiAWfnwgDyASfnwgECATfnwgESAVfnwgDSASfiAOIBR+fCAPIBN+fCAQIBV+fCARIBd+fCANIBN+IA4gEn58IA8gFX58IBAgF358IBEgGX58Ig1CGohC/////w+DfCIOQhqIQv////8Pg3wiD0IaiEL/////D4N8IhBCGohC/////w+DfCIRQhqIp0EFbCANp0H///8fcWoiBUEadiAOp0H///8fcWohBCAPp0H///8fcSEDIBCnQf///x9xIQcgEadB////H3EhBiAFQf///x9xIQUgAUEQaiEBIAJBcGoiAkEPSw0ACwsgACAGNgIkIAAgBzYCICAAIAM2AhwgACAENgIYIAAgBTYCFAvxAQEEfwJAIAAoAjgiBARAIAJBECAEayIDIAMgAksbIgUEQCAAQTxqIQZBACEDA0AgBiADIARqaiABIANqLQAAOgAAIAAoAjghBCADQQFqIgMgBUcNAAsLIAAgBCAFaiIDNgI4IANBEEkNASAAIABBPGpBEBACIABBADYCOCACIAVrIQIgASAFaiEBCyACQRBPBEAgACABIAJBcHEiAxACIAJBD3EhAiABIANqIQELIAJFDQAgAEE8aiEEQQAhAwNAIAQgACgCOCADamogASADai0AADoAACADQQFqIgMgAkcNAAsgACAAKAI4IAJqNgI4CwuBDQEHfwJAIABFDQAgAEF4aiIDIABBfGooAgAiAUF4cSIAaiEFAkAgAUEBcQ0AIAFBA3FFDQEgAyADKAIAIgJrIgNBlAgoAgAiBEkNASAAIAJqIQAgA0GYCCgCAEcEQCACQf8BTQRAIAMoAggiBCACQQN2IgJBA3RBrAhqRxogBCADKAIMIgFGBEBBhAhBhAgoAgBBfiACd3E2AgAMAwsgBCABNgIMIAEgBDYCCAwCCyADKAIYIQYCQCADIAMoAgwiAUcEQCAEIAMoAggiAk0EQCACKAIMGgsgAiABNgIMIAEgAjYCCAwBCwJAIANBFGoiAigCACIEDQAgA0EQaiICKAIAIgQNAEEAIQEMAQsDQCACIQcgBCIBQRRqIgIoAgAiBA0AIAFBEGohAiABKAIQIgQNAAsgB0EANgIACyAGRQ0BAkAgAyADKAIcIgJBAnRBtApqIgQoAgBGBEAgBCABNgIAIAENAUGICEGICCgCAEF+IAJ3cTYCAAwDCyAGQRBBFCAGKAIQIANGG2ogATYCACABRQ0CCyABIAY2AhggAygCECICBEAgASACNgIQIAIgATYCGAsgAygCFCICRQ0BIAEgAjYCFCACIAE2AhgMAQsgBSgCBCIBQQNxQQNHDQBBjAggADYCACAFIAFBfnE2AgQgAyAAQQFyNgIEIAAgA2ogADYCAA8LIAUgA00NACAFKAIEIgFBAXFFDQACQCABQQJxRQRAIAVBnAgoAgBGBEBBnAggAzYCAEGQCEGQCCgCACAAaiIANgIAIAMgAEEBcjYCBCADQZgIKAIARw0DQYwIQQA2AgBBmAhBADYCAA8LIAVBmAgoAgBGBEBBmAggAzYCAEGMCEGMCCgCACAAaiIANgIAIAMgAEEBcjYCBCAAIANqIAA2AgAPCyABQXhxIABqIQACQCABQf8BTQRAIAUoAgwhAiAFKAIIIgQgAUEDdiIBQQN0QawIaiIHRwRAQZQIKAIAGgsgAiAERgRAQYQIQYQIKAIAQX4gAXdxNgIADAILIAIgB0cEQEGUCCgCABoLIAQgAjYCDCACIAQ2AggMAQsgBSgCGCEGAkAgBSAFKAIMIgFHBEBBlAgoAgAgBSgCCCICTQRAIAIoAgwaCyACIAE2AgwgASACNgIIDAELAkAgBUEUaiICKAIAIgQNACAFQRBqIgIoAgAiBA0AQQAhAQwBCwNAIAIhByAEIgFBFGoiAigCACIEDQAgAUEQaiECIAEoAhAiBA0ACyAHQQA2AgALIAZFDQACQCAFIAUoAhwiAkECdEG0CmoiBCgCAEYEQCAEIAE2AgAgAQ0BQYgIQYgIKAIAQX4gAndxNgIADAILIAZBEEEUIAYoAhAgBUYbaiABNgIAIAFFDQELIAEgBjYCGCAFKAIQIgIEQCABIAI2AhAgAiABNgIYCyAFKAIUIgJFDQAgASACNgIUIAIgATYCGAsgAyAAQQFyNgIEIAAgA2ogADYCACADQZgIKAIARw0BQYwIIAA2AgAPCyAFIAFBfnE2AgQgAyAAQQFyNgIEIAAgA2ogADYCAAsgAEH/AU0EQCAAQQN2IgFBA3RBrAhqIQACf0GECCgCACICQQEgAXQiAXFFBEBBhAggASACcjYCACAADAELIAAoAggLIQIgACADNgIIIAIgAzYCDCADIAA2AgwgAyACNgIIDwsgA0IANwIQIAMCf0EAIABBCHYiAUUNABpBHyAAQf///wdLDQAaIAEgAUGA/j9qQRB2QQhxIgF0IgIgAkGA4B9qQRB2QQRxIgJ0IgQgBEGAgA9qQRB2QQJxIgR0QQ92IAEgAnIgBHJrIgFBAXQgACABQRVqdkEBcXJBHGoLIgI2AhwgAkECdEG0CmohAQJAAkACQEGICCgCACIEQQEgAnQiB3FFBEBBiAggBCAHcjYCACABIAM2AgAgAyABNgIYDAELIABBAEEZIAJBAXZrIAJBH0YbdCECIAEoAgAhAQNAIAEiBCgCBEF4cSAARg0CIAJBHXYhASACQQF0IQIgBCABQQRxaiIHQRBqKAIAIgENAAsgByADNgIQIAMgBDYCGAsgAyADNgIMIAMgAzYCCAwBCyAEKAIIIgAgAzYCDCAEIAM2AgggA0EANgIYIAMgBDYCDCADIAA2AggLQaQIQaQIKAIAQX9qIgA2AgAgAA0AQcwLIQMDQCADKAIAIgBBCGohAyAADQALQaQIQX82AgALC8YtAQt/IwBBEGsiCyQAAkACQAJAAkACQAJAAkACQAJAAkACQCAAQfQBTQRAQYQIKAIAIgZBECAAQQtqQXhxIABBC0kbIgVBA3YiAHYiAUEDcQRAIAFBf3NBAXEgAGoiAkEDdCIEQbQIaigCACIBQQhqIQACQCABKAIIIgMgBEGsCGoiBEYEQEGECCAGQX4gAndxNgIADAELQZQIKAIAGiADIAQ2AgwgBCADNgIICyABIAJBA3QiAkEDcjYCBCABIAJqIgEgASgCBEEBcjYCBAwMCyAFQYwIKAIAIghNDQEgAQRAAkBBAiAAdCICQQAgAmtyIAEgAHRxIgBBACAAa3FBf2oiACAAQQx2QRBxIgB2IgFBBXZBCHEiAiAAciABIAJ2IgBBAnZBBHEiAXIgACABdiIAQQF2QQJxIgFyIAAgAXYiAEEBdkEBcSIBciAAIAF2aiICQQN0IgNBtAhqKAIAIgEoAggiACADQawIaiIDRgRAQYQIIAZBfiACd3EiBjYCAAwBC0GUCCgCABogACADNgIMIAMgADYCCAsgAUEIaiEAIAEgBUEDcjYCBCABIAVqIgcgAkEDdCICIAVrIgNBAXI2AgQgASACaiADNgIAIAgEQCAIQQN2IgRBA3RBrAhqIQFBmAgoAgAhAgJ/IAZBASAEdCIEcUUEQEGECCAEIAZyNgIAIAEMAQsgASgCCAshBCABIAI2AgggBCACNgIMIAIgATYCDCACIAQ2AggLQZgIIAc2AgBBjAggAzYCAAwMC0GICCgCACIKRQ0BIApBACAKa3FBf2oiACAAQQx2QRBxIgB2IgFBBXZBCHEiAiAAciABIAJ2IgBBAnZBBHEiAXIgACABdiIAQQF2QQJxIgFyIAAgAXYiAEEBdkEBcSIBciAAIAF2akECdEG0CmooAgAiASgCBEF4cSAFayEDIAEhAgNAAkAgAigCECIARQRAIAIoAhQiAEUNAQsgACgCBEF4cSAFayICIAMgAiADSSICGyEDIAAgASACGyEBIAAhAgwBCwsgASgCGCEJIAEgASgCDCIERwRAQZQIKAIAIAEoAggiAE0EQCAAKAIMGgsgACAENgIMIAQgADYCCAwLCyABQRRqIgIoAgAiAEUEQCABKAIQIgBFDQMgAUEQaiECCwNAIAIhByAAIgRBFGoiAigCACIADQAgBEEQaiECIAQoAhAiAA0ACyAHQQA2AgAMCgtBfyEFIABBv39LDQAgAEELaiIAQXhxIQVBiAgoAgAiB0UNAEEAIAVrIQICQAJAAkACf0EAIABBCHYiAEUNABpBHyAFQf///wdLDQAaIAAgAEGA/j9qQRB2QQhxIgB0IgEgAUGA4B9qQRB2QQRxIgF0IgMgA0GAgA9qQRB2QQJxIgN0QQ92IAAgAXIgA3JrIgBBAXQgBSAAQRVqdkEBcXJBHGoLIghBAnRBtApqKAIAIgNFBEBBACEADAELIAVBAEEZIAhBAXZrIAhBH0YbdCEBQQAhAANAAkAgAygCBEF4cSAFayIGIAJPDQAgAyEEIAYiAg0AQQAhAiADIQAMAwsgACADKAIUIgYgBiADIAFBHXZBBHFqKAIQIgNGGyAAIAYbIQAgASADQQBHdCEBIAMNAAsLIAAgBHJFBEBBAiAIdCIAQQAgAGtyIAdxIgBFDQMgAEEAIABrcUF/aiIAIABBDHZBEHEiAHYiAUEFdkEIcSIDIAByIAEgA3YiAEECdkEEcSIBciAAIAF2IgBBAXZBAnEiAXIgACABdiIAQQF2QQFxIgFyIAAgAXZqQQJ0QbQKaigCACEACyAARQ0BCwNAIAAoAgRBeHEgBWsiAyACSSEBIAMgAiABGyECIAAgBCABGyEEIAAoAhAiAQR/IAEFIAAoAhQLIgANAAsLIARFDQAgAkGMCCgCACAFa08NACAEKAIYIQggBCAEKAIMIgFHBEBBlAgoAgAgBCgCCCIATQRAIAAoAgwaCyAAIAE2AgwgASAANgIIDAkLIARBFGoiAygCACIARQRAIAQoAhAiAEUNAyAEQRBqIQMLA0AgAyEGIAAiAUEUaiIDKAIAIgANACABQRBqIQMgASgCECIADQALIAZBADYCAAwIC0GMCCgCACIBIAVPBEBBmAgoAgAhAAJAIAEgBWsiAkEQTwRAQYwIIAI2AgBBmAggACAFaiIDNgIAIAMgAkEBcjYCBCAAIAFqIAI2AgAgACAFQQNyNgIEDAELQZgIQQA2AgBBjAhBADYCACAAIAFBA3I2AgQgACABaiIBIAEoAgRBAXI2AgQLIABBCGohAAwKC0GQCCgCACIBIAVLBEBBkAggASAFayIBNgIAQZwIQZwIKAIAIgAgBWoiAjYCACACIAFBAXI2AgQgACAFQQNyNgIEIABBCGohAAwKC0EAIQAgBUEvaiIEAn9B3AsoAgAEQEHkCygCAAwBC0HoC0J/NwIAQeALQoCggICAgAQ3AgBB3AsgC0EMakFwcUHYqtWqBXM2AgBB8AtBADYCAEHAC0EANgIAQYAgCyICaiIGQQAgAmsiB3EiAiAFTQ0JQbwLKAIAIgMEQEG0CygCACIIIAJqIgkgCE0NCiAJIANLDQoLQcALLQAAQQRxDQQCQAJAQZwIKAIAIgMEQEHECyEAA0AgACgCACIIIANNBEAgCCAAKAIEaiADSw0DCyAAKAIIIgANAAsLQQAQASIBQX9GDQUgAiEGQeALKAIAIgBBf2oiAyABcQRAIAIgAWsgASADakEAIABrcWohBgsgBiAFTQ0FIAZB/v///wdLDQVBvAsoAgAiAARAQbQLKAIAIgMgBmoiByADTQ0GIAcgAEsNBgsgBhABIgAgAUcNAQwHCyAGIAFrIAdxIgZB/v///wdLDQQgBhABIgEgACgCACAAKAIEakYNAyABIQALAkAgBUEwaiAGTQ0AIABBf0YNAEHkCygCACIBIAQgBmtqQQAgAWtxIgFB/v///wdLBEAgACEBDAcLIAEQAUF/RwRAIAEgBmohBiAAIQEMBwtBACAGaxABGgwECyAAIgFBf0cNBQwDC0EAIQQMBwtBACEBDAULIAFBf0cNAgtBwAtBwAsoAgBBBHI2AgALIAJB/v///wdLDQEgAhABIgFBABABIgBPDQEgAUF/Rg0BIABBf0YNASAAIAFrIgYgBUEoak0NAQtBtAtBtAsoAgAgBmoiADYCACAAQbgLKAIASwRAQbgLIAA2AgALAkACQAJAQZwIKAIAIgMEQEHECyEAA0AgASAAKAIAIgIgACgCBCIEakYNAiAAKAIIIgANAAsMAgtBlAgoAgAiAEEAIAEgAE8bRQRAQZQIIAE2AgALQQAhAEHICyAGNgIAQcQLIAE2AgBBpAhBfzYCAEGoCEHcCygCADYCAEHQC0EANgIAA0AgAEEDdCICQbQIaiACQawIaiIDNgIAIAJBuAhqIAM2AgAgAEEBaiIAQSBHDQALQZAIIAZBWGoiAEF4IAFrQQdxQQAgAUEIakEHcRsiAmsiAzYCAEGcCCABIAJqIgI2AgAgAiADQQFyNgIEIAAgAWpBKDYCBEGgCEHsCygCADYCAAwCCyAALQAMQQhxDQAgASADTQ0AIAIgA0sNACAAIAQgBmo2AgRBnAggA0F4IANrQQdxQQAgA0EIakEHcRsiAGoiATYCAEGQCEGQCCgCACAGaiICIABrIgA2AgAgASAAQQFyNgIEIAIgA2pBKDYCBEGgCEHsCygCADYCAAwBCyABQZQIKAIAIgRJBEBBlAggATYCACABIQQLIAEgBmohAkHECyEAAkACQAJAAkACQAJAA0AgAiAAKAIARwRAIAAoAggiAA0BDAILCyAALQAMQQhxRQ0BC0HECyEAA0AgACgCACICIANNBEAgAiAAKAIEaiIEIANLDQMLIAAoAgghAAwAAAsACyAAIAE2AgAgACAAKAIEIAZqNgIEIAFBeCABa0EHcUEAIAFBCGpBB3EbaiIJIAVBA3I2AgQgAkF4IAJrQQdxQQAgAkEIakEHcRtqIgEgCWsgBWshACAFIAlqIQcgASADRgRAQZwIIAc2AgBBkAhBkAgoAgAgAGoiADYCACAHIABBAXI2AgQMAwsgAUGYCCgCAEYEQEGYCCAHNgIAQYwIQYwIKAIAIABqIgA2AgAgByAAQQFyNgIEIAAgB2ogADYCAAwDCyABKAIEIgJBA3FBAUYEQCACQXhxIQoCQCACQf8BTQRAIAEoAggiAyACQQN2IgRBA3RBrAhqRxogAyABKAIMIgJGBEBBhAhBhAgoAgBBfiAEd3E2AgAMAgsgAyACNgIMIAIgAzYCCAwBCyABKAIYIQgCQCABIAEoAgwiBkcEQCAEIAEoAggiAk0EQCACKAIMGgsgAiAGNgIMIAYgAjYCCAwBCwJAIAFBFGoiAygCACIFDQAgAUEQaiIDKAIAIgUNAEEAIQYMAQsDQCADIQIgBSIGQRRqIgMoAgAiBQ0AIAZBEGohAyAGKAIQIgUNAAsgAkEANgIACyAIRQ0AAkAgASABKAIcIgJBAnRBtApqIgMoAgBGBEAgAyAGNgIAIAYNAUGICEGICCgCAEF+IAJ3cTYCAAwCCyAIQRBBFCAIKAIQIAFGG2ogBjYCACAGRQ0BCyAGIAg2AhggASgCECICBEAgBiACNgIQIAIgBjYCGAsgASgCFCICRQ0AIAYgAjYCFCACIAY2AhgLIAEgCmohASAAIApqIQALIAEgASgCBEF+cTYCBCAHIABBAXI2AgQgACAHaiAANgIAIABB/wFNBEAgAEEDdiIBQQN0QawIaiEAAn9BhAgoAgAiAkEBIAF0IgFxRQRAQYQIIAEgAnI2AgAgAAwBCyAAKAIICyEBIAAgBzYCCCABIAc2AgwgByAANgIMIAcgATYCCAwDCyAHAn9BACAAQQh2IgFFDQAaQR8gAEH///8HSw0AGiABIAFBgP4/akEQdkEIcSIBdCICIAJBgOAfakEQdkEEcSICdCIDIANBgIAPakEQdkECcSIDdEEPdiABIAJyIANyayIBQQF0IAAgAUEVanZBAXFyQRxqCyIBNgIcIAdCADcCECABQQJ0QbQKaiECAkBBiAgoAgAiA0EBIAF0IgRxRQRAQYgIIAMgBHI2AgAgAiAHNgIADAELIABBAEEZIAFBAXZrIAFBH0YbdCEDIAIoAgAhAQNAIAEiAigCBEF4cSAARg0DIANBHXYhASADQQF0IQMgAiABQQRxaiIEKAIQIgENAAsgBCAHNgIQCyAHIAI2AhggByAHNgIMIAcgBzYCCAwCC0GQCCAGQVhqIgBBeCABa0EHcUEAIAFBCGpBB3EbIgJrIgc2AgBBnAggASACaiICNgIAIAIgB0EBcjYCBCAAIAFqQSg2AgRBoAhB7AsoAgA2AgAgAyAEQScgBGtBB3FBACAEQVlqQQdxG2pBUWoiACAAIANBEGpJGyICQRs2AgQgAkHMCykCADcCECACQcQLKQIANwIIQcwLIAJBCGo2AgBByAsgBjYCAEHECyABNgIAQdALQQA2AgAgAkEYaiEAA0AgAEEHNgIEIABBCGohASAAQQRqIQAgBCABSw0ACyACIANGDQMgAiACKAIEQX5xNgIEIAMgAiADayIEQQFyNgIEIAIgBDYCACAEQf8BTQRAIARBA3YiAUEDdEGsCGohAAJ/QYQIKAIAIgJBASABdCIBcUUEQEGECCABIAJyNgIAIAAMAQsgACgCCAshASAAIAM2AgggASADNgIMIAMgADYCDCADIAE2AggMBAsgA0IANwIQIAMCf0EAIARBCHYiAEUNABpBHyAEQf///wdLDQAaIAAgAEGA/j9qQRB2QQhxIgB0IgEgAUGA4B9qQRB2QQRxIgF0IgIgAkGAgA9qQRB2QQJxIgJ0QQ92IAAgAXIgAnJrIgBBAXQgBCAAQRVqdkEBcXJBHGoLIgA2AhwgAEECdEG0CmohAQJAQYgIKAIAIgJBASAAdCIGcUUEQEGICCACIAZyNgIAIAEgAzYCACADIAE2AhgMAQsgBEEAQRkgAEEBdmsgAEEfRht0IQAgASgCACEBA0AgASICKAIEQXhxIARGDQQgAEEddiEBIABBAXQhACACIAFBBHFqIgYoAhAiAQ0ACyAGIAM2AhAgAyACNgIYCyADIAM2AgwgAyADNgIIDAMLIAIoAggiACAHNgIMIAIgBzYCCCAHQQA2AhggByACNgIMIAcgADYCCAsgCUEIaiEADAULIAIoAggiACADNgIMIAIgAzYCCCADQQA2AhggAyACNgIMIAMgADYCCAtBkAgoAgAiACAFTQ0AQZAIIAAgBWsiATYCAEGcCEGcCCgCACIAIAVqIgI2AgAgAiABQQFyNgIEIAAgBUEDcjYCBCAAQQhqIQAMAwtBgAhBMDYCAEEAIQAMAgsCQCAIRQ0AAkAgBCgCHCIAQQJ0QbQKaiIDKAIAIARGBEAgAyABNgIAIAENAUGICCAHQX4gAHdxIgc2AgAMAgsgCEEQQRQgCCgCECAERhtqIAE2AgAgAUUNAQsgASAINgIYIAQoAhAiAARAIAEgADYCECAAIAE2AhgLIAQoAhQiAEUNACABIAA2AhQgACABNgIYCwJAIAJBD00EQCAEIAIgBWoiAEEDcjYCBCAAIARqIgAgACgCBEEBcjYCBAwBCyAEIAVBA3I2AgQgBCAFaiIDIAJBAXI2AgQgAiADaiACNgIAIAJB/wFNBEAgAkEDdiIBQQN0QawIaiEAAn9BhAgoAgAiAkEBIAF0IgFxRQRAQYQIIAEgAnI2AgAgAAwBCyAAKAIICyEBIAAgAzYCCCABIAM2AgwgAyAANgIMIAMgATYCCAwBCyADAn9BACACQQh2IgBFDQAaQR8gAkH///8HSw0AGiAAIABBgP4/akEQdkEIcSIAdCIBIAFBgOAfakEQdkEEcSIBdCIFIAVBgIAPakEQdkECcSIFdEEPdiAAIAFyIAVyayIAQQF0IAIgAEEVanZBAXFyQRxqCyIANgIcIANCADcCECAAQQJ0QbQKaiEBAkACQCAHQQEgAHQiBXFFBEBBiAggBSAHcjYCACABIAM2AgAMAQsgAkEAQRkgAEEBdmsgAEEfRht0IQAgASgCACEFA0AgBSIBKAIEQXhxIAJGDQIgAEEddiEFIABBAXQhACABIAVBBHFqIgYoAhAiBQ0ACyAGIAM2AhALIAMgATYCGCADIAM2AgwgAyADNgIIDAELIAEoAggiACADNgIMIAEgAzYCCCADQQA2AhggAyABNgIMIAMgADYCCAsgBEEIaiEADAELAkAgCUUNAAJAIAEoAhwiAEECdEG0CmoiAigCACABRgRAIAIgBDYCACAEDQFBiAggCkF+IAB3cTYCAAwCCyAJQRBBFCAJKAIQIAFGG2ogBDYCACAERQ0BCyAEIAk2AhggASgCECIABEAgBCAANgIQIAAgBDYCGAsgASgCFCIARQ0AIAQgADYCFCAAIAQ2AhgLAkAgA0EPTQRAIAEgAyAFaiIAQQNyNgIEIAAgAWoiACAAKAIEQQFyNgIEDAELIAEgBUEDcjYCBCABIAVqIgQgA0EBcjYCBCADIARqIAM2AgAgCARAIAhBA3YiBUEDdEGsCGohAEGYCCgCACECAn9BASAFdCIFIAZxRQRAQYQIIAUgBnI2AgAgAAwBCyAAKAIICyEFIAAgAjYCCCAFIAI2AgwgAiAANgIMIAIgBTYCCAtBmAggBDYCAEGMCCADNgIACyABQQhqIQALIAtBEGokACAAC9YCAQF/AkAgAUUNACAAIAFqIgJBf2pBADoAACAAQQA6AAAgAUEDSQ0AIAJBfmpBADoAACAAQQA6AAEgAkF9akEAOgAAIABBADoAAiABQQdJDQAgAkF8akEAOgAAIABBADoAAyABQQlJDQAgAEEAIABrQQNxIgJqIgBBADYCACAAIAEgAmtBfHEiAmoiAUF8akEANgIAIAJBCUkNACAAQQA2AgggAEEANgIEIAFBeGpBADYCACABQXRqQQA2AgAgAkEZSQ0AIABBADYCGCAAQQA2AhQgAEEANgIQIABBADYCDCABQXBqQQA2AgAgAUFsakEANgIAIAFBaGpBADYCACABQWRqQQA2AgAgAiAAQQRxQRhyIgJrIgFBIEkNACAAIAJqIQADQCAAQgA3AxggAEIANwMQIABCADcDCCAAQgA3AwAgAEEgaiEAIAFBYGoiAUEfSw0ACwsLNwEBfyMAQZABayIGJAAgBiAFEAwgBiABIAIQAyAEBEAgBiADIAQQAwsgBiAAEAggBkGQAWokAAvJBAILfwR+IAAoAjgiAwRAIABBPGoiAiADakEBOgAAIANBAWpBD00EQCAAIANqQT1qQQ8gA2sQBgsgAEEBOgBMIAAgAkEQEAILIAA1AjQhECAANQIwIQ4gADUCLCENIAEgADUCKCAAKAIkIAAoAiAgACgCHCAAKAIYIgZBGnZqIgNBGnZqIgJBGnZqIghBgICAYHIgAkH///8fcSIKIANB////H3EiCyAAKAIUIAhBGnZBBWxqIgJB////H3EiBEEFaiIHQRp2IAZB////H3EgAkEadmoiDGoiAkEadmoiA0EadmoiBkEadmoiCUEfdSIFIARxIAcgCUEfdkF/aiIHQf///x9xIgRxciAFIAxxIAIgBHFyIgJBGnRyrXwiDzwAACABIA0gBSALcSADIARxciIDQRR0IAJBBnZyrXwgD0IgiHwiDTwABCABIA+nIgJBGHY6AAMgASACQRB2OgACIAEgAkEIdjoAASABIA4gBSAKcSAEIAZxciICQQ50IANBDHZyrXwgDUIgiHwiDjwACCABIA2nIgNBGHY6AAcgASADQRB2OgAGIAEgA0EIdjoABSABIBAgByAJcSAFIAhxckEIdCACQRJ2cq18IA5CIIh8Ig08AAwgASAOpyICQRh2OgALIAEgAkEQdjoACiABIAJBCHY6AAkgASANpyICQRh2OgAPIAEgAkEQdjoADiABIAJBCHY6AA0gAEIANwIwIABCADcCKCAAQgA3AiAgAEIANwIYIABCADcCECAAQgA3AgggAEIANwIACwYAIAAkAAsQACMAIABrQXBxIgAkACAACwQAIwALzgEBAn8gACABLQADQRh0QYCAgBhxIAEvAAAgAS0AAkEQdHJyNgIAIAAgASgAA0ECdkGD/v8fcTYCBCAAIAEoAAZBBHZB/4H/H3E2AgggACABKAAJQQZ2Qf//wB9xNgIMIAEvAA0hAiABLQAPIQMgAEIANwIUIABCADcCHCAAQQA2AiQgACACIANBEHRBgIA8cXI2AhAgACABKAAQNgIoIAAgASgAFDYCLCAAIAEoABg2AjAgASgAHCEBIABBADoATCAAQQA2AjggACABNgI0CwMAAQs=";if(String.prototype.startsWith?!W.startsWith(V):0!==W.indexOf(V)){var ra=W;W=b.locateFile?b.locateFile(ra,z):z+ra}function sa(){try{if(J)return new Uint8Array(J);var a=F(W);if(a)return a;if(C)return C(W);throw"sync fetching of the wasm failed: you can preload it to Module['wasmBinary'] manually, or emcc.py will do that for you when generating HTML (but not JS)";}catch(c){G(c)}}ma.push({j:function(){ta()}});
    +var ua=!1;function H(a){for(var c=[],d=0;d<a.length;d++){var e=a[d];255<e&&(ua&&assert(!1,"Character code "+e+" ("+String.fromCharCode(e)+")  at offset "+d+" not in 0x00-0xFF."),e&=255);c.push(String.fromCharCode(e))}return c.join("")}
    +var va="function"===typeof atob?atob:function(a){var c="",d=0;a=a.replace(/[^A-Za-z0-9\+\/=]/g,"");do{var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(a.charAt(d++));var k="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(a.charAt(d++));var p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(a.charAt(d++));var A="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".indexOf(a.charAt(d++));e=e<<2|k>>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<a.length);return c};
    +function F(a){if(String.prototype.startsWith?a.startsWith(V):0===a.indexOf(V)){a=a.slice(V.length);if("boolean"===typeof x&&x){try{var c=Buffer.from(a,"base64")}catch(p){c=new Buffer(a,"base64")}var d=new Uint8Array(c.buffer,c.byteOffset,c.byteLength)}else try{var e=va(a),k=new Uint8Array(e.length);for(c=0;c<e.length;++c)k[c]=e.charCodeAt(c);d=k}catch(p){throw Error("Converting base64 string to bytes failed.");}return d}}
    +var wa={a:function(a){var c=O.length;if(2147483648<a)return!1;for(var d=1;4>=d;d*=2){var e=c*(1+.2/d);e=Math.min(e,a+100663296);e=Math.max(16777216,a,e);0<e%65536&&(e+=65536-e%65536);a:{try{K.grow(Math.min(2147483648,e)-Q.byteLength+65535>>>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<S)){if(b.preRun)for("function"==typeof b.preRun&&(b.preRun=[b.preRun]);b.preRun.length;)qa();R(la);0<S||(b.setStatus?(b.setStatus("Running..."),setTimeout(function(){setTimeout(function(){b.setStatus("")},1);a()},1)):a())}}b.run=Z;
    +if(b.preInit)for("function"==typeof b.preInit&&(b.preInit=[b.preInit]);0<b.preInit.length;)b.preInit.pop()();noExitRuntime=!0;Z();
    
  • lib/protocol/crypto/src/binding.cc+2081 0 added
    @@ -0,0 +1,2081 @@
    +// TODO: switch from obsolete EVP_* APIs in CCP
    +#include <stdio.h>
    +#include <string.h>
    +#include <assert.h>
    +
    +#include <node.h>
    +#include <node_buffer.h>
    +#include <nan.h>
    +
    +#include <openssl/err.h>
    +#include <openssl/evp.h>
    +#include <openssl/hmac.h>
    +
    +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<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(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:
    +    //   <packet length> <padding length> <payload> <padding> <poly1305 mac>
    +    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<size_t>(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<size_t>(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<size_t>(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<unsigned char*>(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<ChaChaPolyCipher>(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<unsigned char*>(Buffer::Data(info[0])),
    +      Buffer::Length(info[0]),
    +      Nan::To<uint32_t>(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<ChaChaPolyCipher>(info.Holder());
    +    obj->clear();
    +  }
    +
    +  static inline Nan::Persistent<Function> & constructor() {
    +    static Nan::Persistent<Function> 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<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(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<size_t>(EVP_CIPHER_CTX_iv_length(ctx_))) {
    +      //~ r = kErrBadIVLen;
    +      //~ goto out;
    +    //~ }
    +
    +    if (key_len != static_cast<size_t>(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:
    +    //   <packet length> <padding length> <payload> <padding> <mac>
    +    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<size_t>(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<unsigned char*>(Buffer::Data(info[1])),
    +      Buffer::Length(info[1]),
    +      reinterpret_cast<unsigned char*>(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<AESGCMCipher>(info.Holder());
    +
    +    if (!Buffer::HasInstance(info[0]))
    +      return Nan::ThrowTypeError("Missing/Invalid packet");
    +
    +    ErrorType r = obj->encrypt(
    +      reinterpret_cast<unsigned char*>(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<AESGCMCipher>(info.Holder());
    +    obj->clear();
    +  }
    +
    +  static inline Nan::Persistent<Function> & constructor() {
    +    static Nan::Persistent<Function> my_constructor;
    +    return my_constructor;
    +  }
    +
    +  EVP_CIPHER_CTX* ctx_;
    +};
    +
    +class GenericCipher : public ObjectWrap {
    + public:
    +  static NAN_MODULE_INIT(Init) {
    +    Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(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<size_t>(EVP_CIPHER_CTX_iv_length(ctx_))) {
    +      r = kErrBadIVLen;
    +      goto out;
    +    }
    +
    +    if (key_len != static_cast<size_t>(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<size_t>(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:
    +    //   <packet length> <padding length> <payload> <padding> <mac>
    +    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<size_t>(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<size_t>(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<bool>(info[5]).FromJust() ? 1 : 0);
    +
    +    GenericCipher* obj = new GenericCipher();
    +    ErrorType r = obj->init(
    +      *cipher_name,
    +      reinterpret_cast<unsigned char*>(Buffer::Data(info[1])),
    +      Buffer::Length(info[1]),
    +      reinterpret_cast<unsigned char*>(Buffer::Data(info[2])),
    +      Buffer::Length(info[2]),
    +      *mac_name,
    +      reinterpret_cast<unsigned char*>(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<GenericCipher>(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<unsigned char*>(Buffer::Data(info[0])),
    +      Buffer::Length(info[0]),
    +      Nan::To<uint32_t>(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<GenericCipher>(info.Holder());
    +    obj->clear();
    +  }
    +
    +  static inline Nan::Persistent<Function> & constructor() {
    +    static Nan::Persistent<Function> 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<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(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<size_t>(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:
    +    //   <padding length> <payload> <padding>
    +
    +    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<size_t>(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<size_t>(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<unsigned char*>(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<ChaChaPolyDecipher>(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<unsigned char*>(Buffer::Data(info[0]));
    +
    +    uint32_t dec_packet_length;
    +    ErrorType r = obj->decrypt_length(
    +      length_bytes,
    +      Buffer::Length(info[0]),
    +      Nan::To<uint32_t>(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<ChaChaPolyDecipher>(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<unsigned char*>(Buffer::Data(info[0])),
    +      Buffer::Length(info[0]),
    +      reinterpret_cast<unsigned char*>(Buffer::Data(info[1])),
    +      Nan::To<uint32_t>(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<ChaChaPolyDecipher>(info.Holder());
    +    obj->clear();
    +  }
    +
    +  static inline Nan::Persistent<Function> & constructor() {
    +    static Nan::Persistent<Function> 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<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(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<size_t>(EVP_CIPHER_CTX_iv_length(ctx_))) {
    +      //~ r = kErrBadIVLen;
    +      //~ goto out;
    +    //~ }
    +
    +    if (key_len != static_cast<size_t>(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:
    +    //   <padding length> <payload> <padding>
    +
    +    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<size_t>(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<unsigned char*>(Buffer::Data(info[1])),
    +      Buffer::Length(info[1]),
    +      reinterpret_cast<unsigned char*>(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<AESGCMDecipher>(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<uint32_t>(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<unsigned char*>(Buffer::Data(info[0])),
    +      Buffer::Length(info[0]),
    +      length_bytes,
    +      reinterpret_cast<unsigned char*>(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<AESGCMDecipher>(info.Holder());
    +    obj->clear();
    +  }
    +
    +  static inline Nan::Persistent<Function> & constructor() {
    +    static Nan::Persistent<Function> my_constructor;
    +    return my_constructor;
    +  }
    +
    +  EVP_CIPHER_CTX* ctx_;
    +};
    +
    +class GenericDecipher : public ObjectWrap {
    + public:
    +  static NAN_MODULE_INIT(Init) {
    +    Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(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<size_t>(EVP_CIPHER_CTX_iv_length(ctx_))) {
    +      r = kErrBadIVLen;
    +      goto out;
    +    }
    +
    +    if (key_len != static_cast<size_t>(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<size_t>(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<size_t>(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<size_t>(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<size_t>(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<bool>(info[5]).FromJust() ? 1 : 0);
    +
    +    GenericDecipher* obj = new GenericDecipher();
    +    ErrorType r = obj->init(
    +      *cipher_name,
    +      reinterpret_cast<unsigned char*>(Buffer::Data(info[1])),
    +      Buffer::Length(info[1]),
    +      reinterpret_cast<unsigned char*>(Buffer::Data(info[2])),
    +      Buffer::Length(info[2]),
    +      *mac_name,
    +      reinterpret_cast<unsigned char*>(Buffer::Data(info[4])),
    +      Buffer::Length(info[4]),
    +      is_etm,
    +      Nan::To<uint32_t>(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<GenericDecipher>(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<unsigned char*>(Buffer::Data(info[0])),
    +      Buffer::Length(info[0]),
    +      Nan::To<uint32_t>(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<GenericDecipher>(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<unsigned char*>(Buffer::Data(info[0])),
    +      Buffer::Length(info[0]),
    +      Nan::To<uint32_t>(info[1]).FromJust(),
    +      reinterpret_cast<unsigned char*>(Buffer::Data(info[2])),
    +      Buffer::Length(info[2]),
    +      reinterpret_cast<unsigned char*>(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<GenericDecipher>(info.Holder());
    +    obj->clear();
    +  }
    +
    +  static inline Nan::Persistent<Function> & constructor() {
    +    static Nan::Persistent<Function> 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)
    
  • lib/protocol/handlers.js+16 0 added
    @@ -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;
    
  • lib/protocol/handlers.misc.js+1194 0 added
    @@ -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   <new password follows (old) plaintext password?>
    +          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   <signature follows public key blob?>
    +          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);
    +  },
    +};
    
  • lib/protocol/kex.js+1792 0 added
    @@ -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: <implicit>');
    +  } 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: <implicit>');
    +  } 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    <method-specific data>
    +          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   <method-specific data>
    +            */
    +            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    <method-specific data>
    +              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,
    +  },
    +};
    
  • lib/protocol/keyParser.js+1389 0 added
    @@ -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');
    +  }
    +};
    
  • lib/protocol/node-fs-compat.js+115 0 added
    @@ -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);
    +};
    
  • lib/protocol/Protocol.js+1997 0 added
    @@ -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;
    
  • lib/protocol/SFTP.js+3531 0 added
  • lib/protocol/utils.js+376 0 added
    @@ -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;
    +  },
    +};
    
  • lib/protocol/zlib.js+255 0 added
    @@ -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,
    +};
    
  • lib/server.js+0 0 modified
  • lib/SFTPWrapper.js+0 145 removed
    @@ -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;
    
  • lib/utils.js+316 0 added
  • package.json+34 7 modified
  • README.md+414 238 modified
    @@ -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,30 +55,32 @@ 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);
         });
       });
     }).connect({
       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 = '<?xml version="1.0" encoding="UTF-8"?>' +
    -               '<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">' +
    -               '    <capabilities>' +
    -               '		<capability>urn:ietf:params:netconf:base:1.0</capability>' +
    -               '	</capabilities>' +
    -               '</hello>]]>]]>';
    +const { Client } = require('ssh2');
    +
    +const xmlhello = `
    +  <?xml version="1.0" encoding="UTF-8"?>
    +  <hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
    +    <capabilities>
    +      <capability>urn:ietf:params:netconf:base:1.0</capability>
    +    </capabilities>
    +  </hello>]]>]]>`;
     
    -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: <Buffer/string>, passphrase: <string> }` 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)
    
  • SFTP.md+403 0 added
    @@ -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()`
    
  • test/common.js+107 0 added
  • test/fixtures/keyParser/openssh_new_dsa+21 0 added
  • test/fixtures/keyParser/openssh_new_dsa_enc+22 0 added
  • test/fixtures/keyParser/openssh_new_dsa_enc_gcm+23 0 added
  • test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_new_dsa_enc_gcm.result+7 0 added
  • test/fixtures/keyParser/openssh_new_dsa_enc.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_dsa_enc.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_new_dsa_enc.result+7 0 added
  • test/fixtures/keyParser/openssh_new_dsa.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_dsa.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_new_dsa.result+7 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa+9 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa_enc+10 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm+10 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub.result+8 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.result+7 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa_enc.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa_enc.pub.result+8 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa_enc.result+7 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_new_ecdsa.result+7 0 added
  • test/fixtures/keyParser/openssh_new_ed25519+7 0 added
  • test/fixtures/keyParser/openssh_new_ed25519.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_ed25519.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_new_ed25519.result+7 0 added
  • test/fixtures/keyParser/openssh_new_rsa+27 0 added
  • test/fixtures/keyParser/openssh_new_rsa_enc+28 0 added
  • test/fixtures/keyParser/openssh_new_rsa_enc_gcm+29 0 added
  • test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub.result+8 0 added
  • test/fixtures/keyParser/openssh_new_rsa_enc_gcm.result+7 0 added
  • test/fixtures/keyParser/openssh_new_rsa_enc.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_rsa_enc.pub.result+8 0 added
  • test/fixtures/keyParser/openssh_new_rsa_enc.result+7 0 added
  • test/fixtures/keyParser/openssh_new_rsa.pub+1 0 added
  • test/fixtures/keyParser/openssh_new_rsa.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_new_rsa.result+7 0 added
  • test/fixtures/keyParser/openssh_old_dsa+12 0 added
  • test/fixtures/keyParser/openssh_old_dsa_enc+15 0 added
  • test/fixtures/keyParser/openssh_old_dsa_enc.pub+1 0 added
  • test/fixtures/keyParser/openssh_old_dsa_enc.pub.result+8 0 added
  • test/fixtures/keyParser/openssh_old_dsa_enc.result+7 0 added
  • test/fixtures/keyParser/openssh_old_dsa.pub+1 0 added
  • test/fixtures/keyParser/openssh_old_dsa.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_old_dsa.result+7 0 added
  • test/fixtures/keyParser/openssh_old_ecdsa+5 0 added
  • test/fixtures/keyParser/openssh_old_ecdsa_enc+8 0 added
  • test/fixtures/keyParser/openssh_old_ecdsa_enc.pub+1 0 added
  • test/fixtures/keyParser/openssh_old_ecdsa_enc.pub.result+8 0 added
  • test/fixtures/keyParser/openssh_old_ecdsa_enc.result+7 0 added
  • test/fixtures/keyParser/openssh_old_ecdsa.pub+1 0 added
  • test/fixtures/keyParser/openssh_old_ecdsa.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_old_ecdsa.result+7 0 added
  • test/fixtures/keyParser/openssh_old_rsa+27 0 added
  • test/fixtures/keyParser/openssh_old_rsa_enc+30 0 added
  • test/fixtures/keyParser/openssh_old_rsa_enc_aes256+54 0 added
  • test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub+1 0 added
  • test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub.result+6 0 added
  • test/fixtures/keyParser/openssh_old_rsa_enc_aes256.result+6 0 added
  • test/fixtures/keyParser/openssh_old_rsa_enc.pub+1 0 added
  • test/fixtures/keyParser/openssh_old_rsa_enc.pub.result+8 0 added
  • test/fixtures/keyParser/openssh_old_rsa_enc.result+7 0 added
  • test/fixtures/keyParser/openssh_old_rsa.pub+1 0 added
  • test/fixtures/keyParser/openssh_old_rsa.pub.result+7 0 added
  • test/fixtures/keyParser/openssh_old_rsa.result+7 0 added
  • test/fixtures/keyParser/ppk_dsa_enc+17 0 added
  • test/fixtures/keyParser/ppk_dsa_enc.result+7 0 added
  • test/fixtures/keyParser/ppk_rsa+26 0 added
  • test/fixtures/keyParser/ppk_rsa_enc+18 0 added
  • test/fixtures/keyParser/ppk_rsa_enc.result+7 0 added
  • test/fixtures/keyParser/ppk_rsa.result+7 0 added
  • test/fixtures/keyParser/rfc4716_rsa2.pub+10 0 added
  • test/fixtures/keyParser/rfc4716_rsa2.pub.result+7 0 added
  • test/fixtures/keyParser/rfc4716_rsa3.pub+11 0 added
  • test/fixtures/keyParser/rfc4716_rsa3.pub.result+7 0 added
  • test/fixtures/keyParser/rfc4716_rsa4.pub+11 0 added
  • test/fixtures/keyParser/rfc4716_rsa4.pub.result+7 0 added
  • test/fixtures/keyParser/rfc4716_rsa5.pub+8 0 added
  • test/fixtures/keyParser/rfc4716_rsa5.pub.result+7 0 added
  • test/fixtures/keyParser/rfc4716_rsa6.pub+13 0 added
  • test/fixtures/keyParser/rfc4716_rsa6.pub.result+7 0 added
  • test/fixtures/keyParser/rfc4716_rsa.pub+9 0 added
  • test/fixtures/keyParser/rfc4716_rsa.pub.result+7 0 added
  • test/test-client-server.js+1270 1550 modified
  • test/test.js+9 18 modified
  • test/test-openssh.js+392 291 modified
  • test/test-protocol-crypto.js+603 0 added
  • test/test-protocol-keyparser.js+145 0 added
  • test/test-protocol-sftp.js+944 0 added
  • .travis.yml+1 3 modified
    @@ -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
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.