ssh.js 7 KB
Newer Older
liang ce committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
/**
 * Functions to output keys in SSH-friendly formats.
 *
 * This is part of the Forge project which may be used under the terms of
 * either the BSD License or the GNU General Public License (GPL) Version 2.
 *
 * See: https://github.com/digitalbazaar/forge/blob/cbebca3780658703d925b61b2caffb1d263a6c1d/LICENSE
 *
 * @author https://github.com/shellac
 */
var forge = require('./forge');
require('./aes');
require('./hmac');
require('./md5');
require('./sha1');
require('./util');

var ssh = module.exports = forge.ssh = forge.ssh || {};

/**
 * Encodes (and optionally encrypts) a private RSA key as a Putty PPK file.
 *
 * @param privateKey the key.
 * @param passphrase a passphrase to protect the key (falsy for no encryption).
 * @param comment a comment to include in the key file.
 *
 * @return the PPK file as a string.
 */
ssh.privateKeyToPutty = function(privateKey, passphrase, comment) {
  comment = comment || '';
  passphrase = passphrase || '';
  var algorithm = 'ssh-rsa';
  var encryptionAlgorithm = (passphrase === '') ? 'none' : 'aes256-cbc';

  var ppk = 'PuTTY-User-Key-File-2: ' + algorithm + '\r\n';
  ppk += 'Encryption: ' + encryptionAlgorithm + '\r\n';
  ppk += 'Comment: ' + comment + '\r\n';

  // public key into buffer for ppk
  var pubbuffer = forge.util.createBuffer();
  _addStringToBuffer(pubbuffer, algorithm);
  _addBigIntegerToBuffer(pubbuffer, privateKey.e);
  _addBigIntegerToBuffer(pubbuffer, privateKey.n);

  // write public key
  var pub = forge.util.encode64(pubbuffer.bytes(), 64);
  var length = Math.floor(pub.length / 66) + 1; // 66 = 64 + \r\n
  ppk += 'Public-Lines: ' + length + '\r\n';
  ppk += pub;

  // private key into a buffer
  var privbuffer = forge.util.createBuffer();
  _addBigIntegerToBuffer(privbuffer, privateKey.d);
  _addBigIntegerToBuffer(privbuffer, privateKey.p);
  _addBigIntegerToBuffer(privbuffer, privateKey.q);
  _addBigIntegerToBuffer(privbuffer, privateKey.qInv);

  // optionally encrypt the private key
  var priv;
  if(!passphrase) {
    // use the unencrypted buffer
    priv = forge.util.encode64(privbuffer.bytes(), 64);
  } else {
    // encrypt RSA key using passphrase
    var encLen = privbuffer.length() + 16 - 1;
    encLen -= encLen % 16;

    // pad private key with sha1-d data -- needs to be a multiple of 16
    var padding = _sha1(privbuffer.bytes());

    padding.truncate(padding.length() - encLen + privbuffer.length());
    privbuffer.putBuffer(padding);

    var aeskey = forge.util.createBuffer();
    aeskey.putBuffer(_sha1('\x00\x00\x00\x00', passphrase));
    aeskey.putBuffer(_sha1('\x00\x00\x00\x01', passphrase));

    // encrypt some bytes using CBC mode
    // key is 40 bytes, so truncate *by* 8 bytes
    var cipher = forge.aes.createEncryptionCipher(aeskey.truncate(8), 'CBC');
    cipher.start(forge.util.createBuffer().fillWithByte(0, 16));
    cipher.update(privbuffer.copy());
    cipher.finish();
    var encrypted = cipher.output;

    // Note: this appears to differ from Putty -- is forge wrong, or putty?
    // due to padding we finish as an exact multiple of 16
    encrypted.truncate(16); // all padding

    priv = forge.util.encode64(encrypted.bytes(), 64);
  }

  // output private key
  length = Math.floor(priv.length / 66) + 1; // 64 + \r\n
  ppk += '\r\nPrivate-Lines: ' + length + '\r\n';
  ppk += priv;

  // MAC
  var mackey = _sha1('putty-private-key-file-mac-key', passphrase);

  var macbuffer = forge.util.createBuffer();
  _addStringToBuffer(macbuffer, algorithm);
  _addStringToBuffer(macbuffer, encryptionAlgorithm);
  _addStringToBuffer(macbuffer, comment);
  macbuffer.putInt32(pubbuffer.length());
  macbuffer.putBuffer(pubbuffer);
  macbuffer.putInt32(privbuffer.length());
  macbuffer.putBuffer(privbuffer);

  var hmac = forge.hmac.create();
  hmac.start('sha1', mackey);
  hmac.update(macbuffer.bytes());

  ppk += '\r\nPrivate-MAC: ' + hmac.digest().toHex() + '\r\n';

  return ppk;
};

/**
 * Encodes a public RSA key as an OpenSSH file.
 *
 * @param key the key.
 * @param comment a comment.
 *
 * @return the public key in OpenSSH format.
 */
ssh.publicKeyToOpenSSH = function(key, comment) {
  var type = 'ssh-rsa';
  comment = comment || '';

  var buffer = forge.util.createBuffer();
  _addStringToBuffer(buffer, type);
  _addBigIntegerToBuffer(buffer, key.e);
  _addBigIntegerToBuffer(buffer, key.n);

  return type + ' ' + forge.util.encode64(buffer.bytes()) + ' ' + comment;
};

/**
 * Encodes a private RSA key as an OpenSSH file.
 *
 * @param key the key.
 * @param passphrase a passphrase to protect the key (falsy for no encryption).
 *
 * @return the public key in OpenSSH format.
 */
ssh.privateKeyToOpenSSH = function(privateKey, passphrase) {
  if(!passphrase) {
    return forge.pki.privateKeyToPem(privateKey);
  }
  // OpenSSH private key is just a legacy format, it seems
  return forge.pki.encryptRsaPrivateKey(privateKey, passphrase,
    {legacy: true, algorithm: 'aes128'});
};

/**
 * Gets the SSH fingerprint for the given public key.
 *
 * @param options the options to use.
 *          [md] the message digest object to use (defaults to forge.md.md5).
 *          [encoding] an alternative output encoding, such as 'hex'
 *            (defaults to none, outputs a byte buffer).
 *          [delimiter] the delimiter to use between bytes for 'hex' encoded
 *            output, eg: ':' (defaults to none).
 *
 * @return the fingerprint as a byte buffer or other encoding based on options.
 */
ssh.getPublicKeyFingerprint = function(key, options) {
  options = options || {};
  var md = options.md || forge.md.md5.create();

  var type = 'ssh-rsa';
  var buffer = forge.util.createBuffer();
  _addStringToBuffer(buffer, type);
  _addBigIntegerToBuffer(buffer, key.e);
  _addBigIntegerToBuffer(buffer, key.n);

  // hash public key bytes
  md.start();
  md.update(buffer.getBytes());
  var digest = md.digest();
  if(options.encoding === 'hex') {
    var hex = digest.toHex();
    if(options.delimiter) {
      return hex.match(/.{2}/g).join(options.delimiter);
    }
    return hex;
  } else if(options.encoding === 'binary') {
    return digest.getBytes();
  } else if(options.encoding) {
    throw new Error('Unknown encoding "' + options.encoding + '".');
  }
  return digest;
};

/**
 * Adds len(val) then val to a buffer.
 *
 * @param buffer the buffer to add to.
 * @param val a big integer.
 */
function _addBigIntegerToBuffer(buffer, val) {
  var hexVal = val.toString(16);
  // ensure 2s complement +ve
  if(hexVal[0] >= '8') {
    hexVal = '00' + hexVal;
  }
  var bytes = forge.util.hexToBytes(hexVal);
  buffer.putInt32(bytes.length);
  buffer.putBytes(bytes);
}

/**
 * Adds len(val) then val to a buffer.
 *
 * @param buffer the buffer to add to.
 * @param val a string.
 */
function _addStringToBuffer(buffer, val) {
  buffer.putInt32(val.length);
  buffer.putString(val);
}

/**
 * Hashes the arguments into one value using SHA-1.
 *
 * @return the sha1 hash of the provided arguments.
 */
function _sha1() {
  var sha = forge.md.sha1.create();
  var num = arguments.length;
  for (var i = 0; i < num; ++i) {
    sha.update(arguments[i]);
  }
  return sha.digest();
}