// 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; } };