ssh-private.js (6934B)
1 // Copyright 2015 Joyent, Inc. 2 3 module.exports = { 4 read: read, 5 readSSHPrivate: readSSHPrivate, 6 write: write 7 }; 8 9 var assert = require('assert-plus'); 10 var asn1 = require('asn1'); 11 var Buffer = require('safer-buffer').Buffer; 12 var algs = require('../algs'); 13 var utils = require('../utils'); 14 var crypto = require('crypto'); 15 16 var Key = require('../key'); 17 var PrivateKey = require('../private-key'); 18 var pem = require('./pem'); 19 var rfc4253 = require('./rfc4253'); 20 var SSHBuffer = require('../ssh-buffer'); 21 var errors = require('../errors'); 22 23 var bcrypt; 24 25 function read(buf, options) { 26 return (pem.read(buf, options)); 27 } 28 29 var MAGIC = 'openssh-key-v1'; 30 31 function readSSHPrivate(type, buf, options) { 32 buf = new SSHBuffer({buffer: buf}); 33 34 var magic = buf.readCString(); 35 assert.strictEqual(magic, MAGIC, 'bad magic string'); 36 37 var cipher = buf.readString(); 38 var kdf = buf.readString(); 39 var kdfOpts = buf.readBuffer(); 40 41 var nkeys = buf.readInt(); 42 if (nkeys !== 1) { 43 throw (new Error('OpenSSH-format key file contains ' + 44 'multiple keys: this is unsupported.')); 45 } 46 47 var pubKey = buf.readBuffer(); 48 49 if (type === 'public') { 50 assert.ok(buf.atEnd(), 'excess bytes left after key'); 51 return (rfc4253.read(pubKey)); 52 } 53 54 var privKeyBlob = buf.readBuffer(); 55 assert.ok(buf.atEnd(), 'excess bytes left after key'); 56 57 var kdfOptsBuf = new SSHBuffer({ buffer: kdfOpts }); 58 switch (kdf) { 59 case 'none': 60 if (cipher !== 'none') { 61 throw (new Error('OpenSSH-format key uses KDF "none" ' + 62 'but specifies a cipher other than "none"')); 63 } 64 break; 65 case 'bcrypt': 66 var salt = kdfOptsBuf.readBuffer(); 67 var rounds = kdfOptsBuf.readInt(); 68 var cinf = utils.opensshCipherInfo(cipher); 69 if (bcrypt === undefined) { 70 bcrypt = require('bcrypt-pbkdf'); 71 } 72 73 if (typeof (options.passphrase) === 'string') { 74 options.passphrase = Buffer.from(options.passphrase, 75 'utf-8'); 76 } 77 if (!Buffer.isBuffer(options.passphrase)) { 78 throw (new errors.KeyEncryptedError( 79 options.filename, 'OpenSSH')); 80 } 81 82 var pass = new Uint8Array(options.passphrase); 83 var salti = new Uint8Array(salt); 84 /* Use the pbkdf to derive both the key and the IV. */ 85 var out = new Uint8Array(cinf.keySize + cinf.blockSize); 86 var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length, 87 out, out.length, rounds); 88 if (res !== 0) { 89 throw (new Error('bcrypt_pbkdf function returned ' + 90 'failure, parameters invalid')); 91 } 92 out = Buffer.from(out); 93 var ckey = out.slice(0, cinf.keySize); 94 var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize); 95 var cipherStream = crypto.createDecipheriv(cinf.opensslName, 96 ckey, iv); 97 cipherStream.setAutoPadding(false); 98 var chunk, chunks = []; 99 cipherStream.once('error', function (e) { 100 if (e.toString().indexOf('bad decrypt') !== -1) { 101 throw (new Error('Incorrect passphrase ' + 102 'supplied, could not decrypt key')); 103 } 104 throw (e); 105 }); 106 cipherStream.write(privKeyBlob); 107 cipherStream.end(); 108 while ((chunk = cipherStream.read()) !== null) 109 chunks.push(chunk); 110 privKeyBlob = Buffer.concat(chunks); 111 break; 112 default: 113 throw (new Error( 114 'OpenSSH-format key uses unknown KDF "' + kdf + '"')); 115 } 116 117 buf = new SSHBuffer({buffer: privKeyBlob}); 118 119 var checkInt1 = buf.readInt(); 120 var checkInt2 = buf.readInt(); 121 if (checkInt1 !== checkInt2) { 122 throw (new Error('Incorrect passphrase supplied, could not ' + 123 'decrypt key')); 124 } 125 126 var ret = {}; 127 var key = rfc4253.readInternal(ret, 'private', buf.remainder()); 128 129 buf.skip(ret.consumed); 130 131 var comment = buf.readString(); 132 key.comment = comment; 133 134 return (key); 135 } 136 137 function write(key, options) { 138 var pubKey; 139 if (PrivateKey.isPrivateKey(key)) 140 pubKey = key.toPublic(); 141 else 142 pubKey = key; 143 144 var cipher = 'none'; 145 var kdf = 'none'; 146 var kdfopts = Buffer.alloc(0); 147 var cinf = { blockSize: 8 }; 148 var passphrase; 149 if (options !== undefined) { 150 passphrase = options.passphrase; 151 if (typeof (passphrase) === 'string') 152 passphrase = Buffer.from(passphrase, 'utf-8'); 153 if (passphrase !== undefined) { 154 assert.buffer(passphrase, 'options.passphrase'); 155 assert.optionalString(options.cipher, 'options.cipher'); 156 cipher = options.cipher; 157 if (cipher === undefined) 158 cipher = 'aes128-ctr'; 159 cinf = utils.opensshCipherInfo(cipher); 160 kdf = 'bcrypt'; 161 } 162 } 163 164 var privBuf; 165 if (PrivateKey.isPrivateKey(key)) { 166 privBuf = new SSHBuffer({}); 167 var checkInt = crypto.randomBytes(4).readUInt32BE(0); 168 privBuf.writeInt(checkInt); 169 privBuf.writeInt(checkInt); 170 privBuf.write(key.toBuffer('rfc4253')); 171 privBuf.writeString(key.comment || ''); 172 173 var n = 1; 174 while (privBuf._offset % cinf.blockSize !== 0) 175 privBuf.writeChar(n++); 176 privBuf = privBuf.toBuffer(); 177 } 178 179 switch (kdf) { 180 case 'none': 181 break; 182 case 'bcrypt': 183 var salt = crypto.randomBytes(16); 184 var rounds = 16; 185 var kdfssh = new SSHBuffer({}); 186 kdfssh.writeBuffer(salt); 187 kdfssh.writeInt(rounds); 188 kdfopts = kdfssh.toBuffer(); 189 190 if (bcrypt === undefined) { 191 bcrypt = require('bcrypt-pbkdf'); 192 } 193 var pass = new Uint8Array(passphrase); 194 var salti = new Uint8Array(salt); 195 /* Use the pbkdf to derive both the key and the IV. */ 196 var out = new Uint8Array(cinf.keySize + cinf.blockSize); 197 var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length, 198 out, out.length, rounds); 199 if (res !== 0) { 200 throw (new Error('bcrypt_pbkdf function returned ' + 201 'failure, parameters invalid')); 202 } 203 out = Buffer.from(out); 204 var ckey = out.slice(0, cinf.keySize); 205 var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize); 206 207 var cipherStream = crypto.createCipheriv(cinf.opensslName, 208 ckey, iv); 209 cipherStream.setAutoPadding(false); 210 var chunk, chunks = []; 211 cipherStream.once('error', function (e) { 212 throw (e); 213 }); 214 cipherStream.write(privBuf); 215 cipherStream.end(); 216 while ((chunk = cipherStream.read()) !== null) 217 chunks.push(chunk); 218 privBuf = Buffer.concat(chunks); 219 break; 220 default: 221 throw (new Error('Unsupported kdf ' + kdf)); 222 } 223 224 var buf = new SSHBuffer({}); 225 226 buf.writeCString(MAGIC); 227 buf.writeString(cipher); /* cipher */ 228 buf.writeString(kdf); /* kdf */ 229 buf.writeBuffer(kdfopts); /* kdfoptions */ 230 231 buf.writeInt(1); /* nkeys */ 232 buf.writeBuffer(pubKey.toBuffer('rfc4253')); 233 234 if (privBuf) 235 buf.writeBuffer(privBuf); 236 237 buf = buf.toBuffer(); 238 239 var header; 240 if (PrivateKey.isPrivateKey(key)) 241 header = 'OPENSSH PRIVATE KEY'; 242 else 243 header = 'OPENSSH PUBLIC KEY'; 244 245 var tmp = buf.toString('base64'); 246 var len = tmp.length + (tmp.length / 70) + 247 18 + 16 + header.length*2 + 10; 248 buf = Buffer.alloc(len); 249 var o = 0; 250 o += buf.write('-----BEGIN ' + header + '-----\n', o); 251 for (var i = 0; i < tmp.length; ) { 252 var limit = i + 70; 253 if (limit > tmp.length) 254 limit = tmp.length; 255 o += buf.write(tmp.slice(i, limit), o); 256 buf[o++] = 10; 257 i = limit; 258 } 259 o += buf.write('-----END ' + header + '-----\n', o); 260 261 return (buf.slice(0, o)); 262 }