1// Copyright 2015 Joyent, Inc. 2 3module.exports = { 4 read: read, 5 write: write 6}; 7 8var assert = require('assert-plus'); 9var asn1 = require('asn1'); 10var crypto = require('crypto'); 11var Buffer = require('safer-buffer').Buffer; 12var algs = require('../algs'); 13var utils = require('../utils'); 14var Key = require('../key'); 15var PrivateKey = require('../private-key'); 16 17var pkcs1 = require('./pkcs1'); 18var pkcs8 = require('./pkcs8'); 19var sshpriv = require('./ssh-private'); 20var rfc4253 = require('./rfc4253'); 21 22var errors = require('../errors'); 23 24/* 25 * For reading we support both PKCS#1 and PKCS#8. If we find a private key, 26 * we just take the public component of it and use that. 27 */ 28function read(buf, options, forceType) { 29 var input = buf; 30 if (typeof (buf) !== 'string') { 31 assert.buffer(buf, 'buf'); 32 buf = buf.toString('ascii'); 33 } 34 35 var lines = buf.trim().split('\n'); 36 37 var m = lines[0].match(/*JSSTYLED*/ 38 /[-]+[ ]*BEGIN ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); 39 assert.ok(m, 'invalid PEM header'); 40 41 var m2 = lines[lines.length - 1].match(/*JSSTYLED*/ 42 /[-]+[ ]*END ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); 43 assert.ok(m2, 'invalid PEM footer'); 44 45 /* Begin and end banners must match key type */ 46 assert.equal(m[2], m2[2]); 47 var type = m[2].toLowerCase(); 48 49 var alg; 50 if (m[1]) { 51 /* They also must match algorithms, if given */ 52 assert.equal(m[1], m2[1], 'PEM header and footer mismatch'); 53 alg = m[1].trim(); 54 } 55 56 var headers = {}; 57 while (true) { 58 lines = lines.slice(1); 59 m = lines[0].match(/*JSSTYLED*/ 60 /^([A-Za-z0-9-]+): (.+)$/); 61 if (!m) 62 break; 63 headers[m[1].toLowerCase()] = m[2]; 64 } 65 66 var cipher, key, iv; 67 if (headers['proc-type']) { 68 var parts = headers['proc-type'].split(','); 69 if (parts[0] === '4' && parts[1] === 'ENCRYPTED') { 70 if (typeof (options.passphrase) === 'string') { 71 options.passphrase = Buffer.from( 72 options.passphrase, 'utf-8'); 73 } 74 if (!Buffer.isBuffer(options.passphrase)) { 75 throw (new errors.KeyEncryptedError( 76 options.filename, 'PEM')); 77 } else { 78 parts = headers['dek-info'].split(','); 79 assert.ok(parts.length === 2); 80 cipher = parts[0].toLowerCase(); 81 iv = Buffer.from(parts[1], 'hex'); 82 key = utils.opensslKeyDeriv(cipher, iv, 83 options.passphrase, 1).key; 84 } 85 } 86 } 87 88 /* Chop off the first and last lines */ 89 lines = lines.slice(0, -1).join(''); 90 buf = Buffer.from(lines, 'base64'); 91 92 if (cipher && key && iv) { 93 var cipherStream = crypto.createDecipheriv(cipher, key, iv); 94 var chunk, chunks = []; 95 cipherStream.once('error', function (e) { 96 if (e.toString().indexOf('bad decrypt') !== -1) { 97 throw (new Error('Incorrect passphrase ' + 98 'supplied, could not decrypt key')); 99 } 100 throw (e); 101 }); 102 cipherStream.write(buf); 103 cipherStream.end(); 104 while ((chunk = cipherStream.read()) !== null) 105 chunks.push(chunk); 106 buf = Buffer.concat(chunks); 107 } 108 109 /* The new OpenSSH internal format abuses PEM headers */ 110 if (alg && alg.toLowerCase() === 'openssh') 111 return (sshpriv.readSSHPrivate(type, buf, options)); 112 if (alg && alg.toLowerCase() === 'ssh2') 113 return (rfc4253.readType(type, buf, options)); 114 115 var der = new asn1.BerReader(buf); 116 der.originalInput = input; 117 118 /* 119 * All of the PEM file types start with a sequence tag, so chop it 120 * off here 121 */ 122 der.readSequence(); 123 124 /* PKCS#1 type keys name an algorithm in the banner explicitly */ 125 if (alg) { 126 if (forceType) 127 assert.strictEqual(forceType, 'pkcs1'); 128 return (pkcs1.readPkcs1(alg, type, der)); 129 } else { 130 if (forceType) 131 assert.strictEqual(forceType, 'pkcs8'); 132 return (pkcs8.readPkcs8(alg, type, der)); 133 } 134} 135 136function write(key, options, type) { 137 assert.object(key); 138 139 var alg = { 140 'ecdsa': 'EC', 141 'rsa': 'RSA', 142 'dsa': 'DSA', 143 'ed25519': 'EdDSA' 144 }[key.type]; 145 var header; 146 147 var der = new asn1.BerWriter(); 148 149 if (PrivateKey.isPrivateKey(key)) { 150 if (type && type === 'pkcs8') { 151 header = 'PRIVATE KEY'; 152 pkcs8.writePkcs8(der, key); 153 } else { 154 if (type) 155 assert.strictEqual(type, 'pkcs1'); 156 header = alg + ' PRIVATE KEY'; 157 pkcs1.writePkcs1(der, key); 158 } 159 160 } else if (Key.isKey(key)) { 161 if (type && type === 'pkcs1') { 162 header = alg + ' PUBLIC KEY'; 163 pkcs1.writePkcs1(der, key); 164 } else { 165 if (type) 166 assert.strictEqual(type, 'pkcs8'); 167 header = 'PUBLIC KEY'; 168 pkcs8.writePkcs8(der, key); 169 } 170 171 } else { 172 throw (new Error('key is not a Key or PrivateKey')); 173 } 174 175 var tmp = der.buffer.toString('base64'); 176 var len = tmp.length + (tmp.length / 64) + 177 18 + 16 + header.length*2 + 10; 178 var buf = Buffer.alloc(len); 179 var o = 0; 180 o += buf.write('-----BEGIN ' + header + '-----\n', o); 181 for (var i = 0; i < tmp.length; ) { 182 var limit = i + 64; 183 if (limit > tmp.length) 184 limit = tmp.length; 185 o += buf.write(tmp.slice(i, limit), o); 186 buf[o++] = 10; 187 i = limit; 188 } 189 o += buf.write('-----END ' + header + '-----\n', o); 190 191 return (buf.slice(0, o)); 192} 193