util.js 6.78 KB
// Copyright 2012 Joyent, Inc.  All rights reserved.

var assert = require('assert-plus');
var crypto = require('crypto');

var asn1 = require('asn1');
var ctype = require('ctype');



///--- Helpers

function readNext(buffer, offset) {
  var len = ctype.ruint32(buffer, 'big', offset);
  offset += 4;

  var newOffset = offset + len;

  return {
    data: buffer.slice(offset, newOffset),
    offset: newOffset
  };
}


function writeInt(writer, buffer) {
  writer.writeByte(0x02); // ASN1.Integer
  writer.writeLength(buffer.length);

  for (var i = 0; i < buffer.length; i++)
    writer.writeByte(buffer[i]);

  return writer;
}


function rsaToPEM(key) {
  var buffer;
  var der;
  var exponent;
  var i;
  var modulus;
  var newKey = '';
  var offset = 0;
  var type;
  var tmp;

  try {
    buffer = new Buffer(key.split(' ')[1], 'base64');

    tmp = readNext(buffer, offset);
    type = tmp.data.toString();
    offset = tmp.offset;

    if (type !== 'ssh-rsa')
      throw new Error('Invalid ssh key type: ' + type);

    tmp = readNext(buffer, offset);
    exponent = tmp.data;
    offset = tmp.offset;

    tmp = readNext(buffer, offset);
    modulus = tmp.data;
  } catch (e) {
    throw new Error('Invalid ssh key: ' + key);
  }

  // DER is a subset of BER
  der = new asn1.BerWriter();

  der.startSequence();

  der.startSequence();
  der.writeOID('1.2.840.113549.1.1.1');
  der.writeNull();
  der.endSequence();

  der.startSequence(0x03); // bit string
  der.writeByte(0x00);

  // Actual key
  der.startSequence();
  writeInt(der, modulus);
  writeInt(der, exponent);
  der.endSequence();

  // bit string
  der.endSequence();

  der.endSequence();

  tmp = der.buffer.toString('base64');
  for (i = 0; i < tmp.length; i++) {
    if ((i % 64) === 0)
      newKey += '\n';
    newKey += tmp.charAt(i);
  }

  if (!/\\n$/.test(newKey))
    newKey += '\n';

  return '-----BEGIN PUBLIC KEY-----' + newKey + '-----END PUBLIC KEY-----\n';
}


function dsaToPEM(key) {
  var buffer;
  var offset = 0;
  var tmp;
  var der;
  var newKey = '';

  var type;
  var p;
  var q;
  var g;
  var y;

  try {
    buffer = new Buffer(key.split(' ')[1], 'base64');

    tmp = readNext(buffer, offset);
    type = tmp.data.toString();
    offset = tmp.offset;

    /* JSSTYLED */
    if (!/^ssh-ds[as].*/.test(type))
      throw new Error('Invalid ssh key type: ' + type);

    tmp = readNext(buffer, offset);
    p = tmp.data;
    offset = tmp.offset;

    tmp = readNext(buffer, offset);
    q = tmp.data;
    offset = tmp.offset;

    tmp = readNext(buffer, offset);
    g = tmp.data;
    offset = tmp.offset;

    tmp = readNext(buffer, offset);
    y = tmp.data;
  } catch (e) {
    console.log(e.stack);
    throw new Error('Invalid ssh key: ' + key);
  }

  // DER is a subset of BER
  der = new asn1.BerWriter();

  der.startSequence();

  der.startSequence();
  der.writeOID('1.2.840.10040.4.1');

  der.startSequence();
  writeInt(der, p);
  writeInt(der, q);
  writeInt(der, g);
  der.endSequence();

  der.endSequence();

  der.startSequence(0x03); // bit string
  der.writeByte(0x00);
  writeInt(der, y);
  der.endSequence();

  der.endSequence();

  tmp = der.buffer.toString('base64');
  for (var i = 0; i < tmp.length; i++) {
    if ((i % 64) === 0)
      newKey += '\n';
    newKey += tmp.charAt(i);
  }

  if (!/\\n$/.test(newKey))
    newKey += '\n';

  return '-----BEGIN PUBLIC KEY-----' + newKey + '-----END PUBLIC KEY-----\n';
}


///--- API

module.exports = {

  /**
   * Converts an OpenSSH public key (rsa only) to a PKCS#8 PEM file.
   *
   * The intent of this module is to interoperate with OpenSSL only,
   * specifically the node crypto module's `verify` method.
   *
   * @param {String} key an OpenSSH public key.
   * @return {String} PEM encoded form of the RSA public key.
   * @throws {TypeError} on bad input.
   * @throws {Error} on invalid ssh key formatted data.
   */
  sshKeyToPEM: function sshKeyToPEM(key) {
    assert.string(key, 'ssh_key');

    /* JSSTYLED */
    if (/^ssh-rsa.*/.test(key))
      return rsaToPEM(key);

    /* JSSTYLED */
    if (/^ssh-ds[as].*/.test(key))
      return dsaToPEM(key);

    throw new Error('Only RSA and DSA public keys are allowed');
  },


  /**
   * Generates an OpenSSH fingerprint from an ssh public key.
   *
   * @param {String} key an OpenSSH public key.
   * @return {String} key fingerprint.
   * @throws {TypeError} on bad input.
   * @throws {Error} if what you passed doesn't look like an ssh public key.
   */
  fingerprint: function fingerprint(key) {
    assert.string(key, 'ssh_key');

    var pieces = key.split(' ');
    if (!pieces || !pieces.length || pieces.length < 2)
      throw new Error('invalid ssh key');

    var data = new Buffer(pieces[1], 'base64');

    var hash = crypto.createHash('md5');
    hash.update(data);
    var digest = hash.digest('hex');

    var fp = '';
    for (var i = 0; i < digest.length; i++) {
      if (i && i % 2 === 0)
        fp += ':';

      fp += digest[i];
    }

    return fp;
  },

  /**
  * Converts a PKGCS#8 PEM file to an OpenSSH public key (rsa)
  *
  * The reverse of the above function.
  */
  pemToRsaSSHKey: function pemToRsaSSHKey(pem, comment) {
    assert.equal('string', typeof pem, 'typeof pem');

    // chop off the BEGIN PUBLIC KEY and END PUBLIC KEY portion
    var cleaned = pem.split('\n').slice(1, -2).join('');

    var buf = new Buffer(cleaned, 'base64');

    var der = new asn1.BerReader(buf);

    der.readSequence();
    der.readSequence();

    var oid = der.readOID();
    assert.equal(oid, '1.2.840.113549.1.1.1', 'pem not in RSA format');

    // Null -- XXX this probably isn't good practice
    der.readByte();
    der.readByte();

    // bit string sequence
    der.readSequence(0x03);
    der.readByte();
    der.readSequence();

    // modulus
    assert.equal(der.peek(), asn1.Ber.Integer, 'modulus not an integer');
    der._offset = der.readLength(der.offset + 1);
    var modulus = der._buf.slice(der.offset, der.offset + der.length);
    der._offset += der.length;

    // exponent
    assert.equal(der.peek(), asn1.Ber.Integer, 'exponent not an integer');
    der._offset = der.readLength(der.offset + 1);
    var exponent = der._buf.slice(der.offset, der.offset + der.length);
    der._offset += der.length;

    // now, make the key
    var type = new Buffer('ssh-rsa');
    var buffer = new Buffer(4 + type.length + 4 + modulus.length + 4 + exponent.length);
    var i = 0;
    buffer.writeUInt32BE(type.length, i);     i += 4;
    type.copy(buffer, i);                     i += type.length;
    buffer.writeUInt32BE(exponent.length, i); i += 4;
    exponent.copy(buffer, i);                 i += exponent.length;
    buffer.writeUInt32BE(modulus.length, i);  i += 4;
    modulus.copy(buffer, i);                  i += modulus.length;

    var s = type.toString() + ' ' + buffer.toString('base64') + ' ' + (comment || '');
    return s;
  }
};