You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

290 lines
7.3 KiB

  1. // Copyright 2018 Joyent, Inc.
  2. module.exports = {
  3. read: read,
  4. write: write
  5. };
  6. var assert = require('assert-plus');
  7. var asn1 = require('asn1');
  8. var crypto = require('crypto');
  9. var Buffer = require('safer-buffer').Buffer;
  10. var algs = require('../algs');
  11. var utils = require('../utils');
  12. var Key = require('../key');
  13. var PrivateKey = require('../private-key');
  14. var pkcs1 = require('./pkcs1');
  15. var pkcs8 = require('./pkcs8');
  16. var sshpriv = require('./ssh-private');
  17. var rfc4253 = require('./rfc4253');
  18. var errors = require('../errors');
  19. var OID_PBES2 = '1.2.840.113549.1.5.13';
  20. var OID_PBKDF2 = '1.2.840.113549.1.5.12';
  21. var OID_TO_CIPHER = {
  22. '1.2.840.113549.3.7': '3des-cbc',
  23. '2.16.840.1.101.3.4.1.2': 'aes128-cbc',
  24. '2.16.840.1.101.3.4.1.42': 'aes256-cbc'
  25. };
  26. var CIPHER_TO_OID = {};
  27. Object.keys(OID_TO_CIPHER).forEach(function (k) {
  28. CIPHER_TO_OID[OID_TO_CIPHER[k]] = k;
  29. });
  30. var OID_TO_HASH = {
  31. '1.2.840.113549.2.7': 'sha1',
  32. '1.2.840.113549.2.9': 'sha256',
  33. '1.2.840.113549.2.11': 'sha512'
  34. };
  35. var HASH_TO_OID = {};
  36. Object.keys(OID_TO_HASH).forEach(function (k) {
  37. HASH_TO_OID[OID_TO_HASH[k]] = k;
  38. });
  39. /*
  40. * For reading we support both PKCS#1 and PKCS#8. If we find a private key,
  41. * we just take the public component of it and use that.
  42. */
  43. function read(buf, options, forceType) {
  44. var input = buf;
  45. if (typeof (buf) !== 'string') {
  46. assert.buffer(buf, 'buf');
  47. buf = buf.toString('ascii');
  48. }
  49. var lines = buf.trim().split(/[\r\n]+/g);
  50. var m;
  51. var si = -1;
  52. while (!m && si < lines.length) {
  53. m = lines[++si].match(/*JSSTYLED*/
  54. /[-]+[ ]*BEGIN ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/);
  55. }
  56. assert.ok(m, 'invalid PEM header');
  57. var m2;
  58. var ei = lines.length;
  59. while (!m2 && ei > 0) {
  60. m2 = lines[--ei].match(/*JSSTYLED*/
  61. /[-]+[ ]*END ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/);
  62. }
  63. assert.ok(m2, 'invalid PEM footer');
  64. /* Begin and end banners must match key type */
  65. assert.equal(m[2], m2[2]);
  66. var type = m[2].toLowerCase();
  67. var alg;
  68. if (m[1]) {
  69. /* They also must match algorithms, if given */
  70. assert.equal(m[1], m2[1], 'PEM header and footer mismatch');
  71. alg = m[1].trim();
  72. }
  73. lines = lines.slice(si, ei + 1);
  74. var headers = {};
  75. while (true) {
  76. lines = lines.slice(1);
  77. m = lines[0].match(/*JSSTYLED*/
  78. /^([A-Za-z0-9-]+): (.+)$/);
  79. if (!m)
  80. break;
  81. headers[m[1].toLowerCase()] = m[2];
  82. }
  83. /* Chop off the first and last lines */
  84. lines = lines.slice(0, -1).join('');
  85. buf = Buffer.from(lines, 'base64');
  86. var cipher, key, iv;
  87. if (headers['proc-type']) {
  88. var parts = headers['proc-type'].split(',');
  89. if (parts[0] === '4' && parts[1] === 'ENCRYPTED') {
  90. if (typeof (options.passphrase) === 'string') {
  91. options.passphrase = Buffer.from(
  92. options.passphrase, 'utf-8');
  93. }
  94. if (!Buffer.isBuffer(options.passphrase)) {
  95. throw (new errors.KeyEncryptedError(
  96. options.filename, 'PEM'));
  97. } else {
  98. parts = headers['dek-info'].split(',');
  99. assert.ok(parts.length === 2);
  100. cipher = parts[0].toLowerCase();
  101. iv = Buffer.from(parts[1], 'hex');
  102. key = utils.opensslKeyDeriv(cipher, iv,
  103. options.passphrase, 1).key;
  104. }
  105. }
  106. }
  107. if (alg && alg.toLowerCase() === 'encrypted') {
  108. var eder = new asn1.BerReader(buf);
  109. var pbesEnd;
  110. eder.readSequence();
  111. eder.readSequence();
  112. pbesEnd = eder.offset + eder.length;
  113. var method = eder.readOID();
  114. if (method !== OID_PBES2) {
  115. throw (new Error('Unsupported PEM/PKCS8 encryption ' +
  116. 'scheme: ' + method));
  117. }
  118. eder.readSequence(); /* PBES2-params */
  119. eder.readSequence(); /* keyDerivationFunc */
  120. var kdfEnd = eder.offset + eder.length;
  121. var kdfOid = eder.readOID();
  122. if (kdfOid !== OID_PBKDF2)
  123. throw (new Error('Unsupported PBES2 KDF: ' + kdfOid));
  124. eder.readSequence();
  125. var salt = eder.readString(asn1.Ber.OctetString, true);
  126. var iterations = eder.readInt();
  127. var hashAlg = 'sha1';
  128. if (eder.offset < kdfEnd) {
  129. eder.readSequence();
  130. var hashAlgOid = eder.readOID();
  131. hashAlg = OID_TO_HASH[hashAlgOid];
  132. if (hashAlg === undefined) {
  133. throw (new Error('Unsupported PBKDF2 hash: ' +
  134. hashAlgOid));
  135. }
  136. }
  137. eder._offset = kdfEnd;
  138. eder.readSequence(); /* encryptionScheme */
  139. var cipherOid = eder.readOID();
  140. cipher = OID_TO_CIPHER[cipherOid];
  141. if (cipher === undefined) {
  142. throw (new Error('Unsupported PBES2 cipher: ' +
  143. cipherOid));
  144. }
  145. iv = eder.readString(asn1.Ber.OctetString, true);
  146. eder._offset = pbesEnd;
  147. buf = eder.readString(asn1.Ber.OctetString, true);
  148. if (typeof (options.passphrase) === 'string') {
  149. options.passphrase = Buffer.from(
  150. options.passphrase, 'utf-8');
  151. }
  152. if (!Buffer.isBuffer(options.passphrase)) {
  153. throw (new errors.KeyEncryptedError(
  154. options.filename, 'PEM'));
  155. }
  156. var cinfo = utils.opensshCipherInfo(cipher);
  157. cipher = cinfo.opensslName;
  158. key = utils.pbkdf2(hashAlg, salt, iterations, cinfo.keySize,
  159. options.passphrase);
  160. alg = undefined;
  161. }
  162. if (cipher && key && iv) {
  163. var cipherStream = crypto.createDecipheriv(cipher, key, iv);
  164. var chunk, chunks = [];
  165. cipherStream.once('error', function (e) {
  166. if (e.toString().indexOf('bad decrypt') !== -1) {
  167. throw (new Error('Incorrect passphrase ' +
  168. 'supplied, could not decrypt key'));
  169. }
  170. throw (e);
  171. });
  172. cipherStream.write(buf);
  173. cipherStream.end();
  174. while ((chunk = cipherStream.read()) !== null)
  175. chunks.push(chunk);
  176. buf = Buffer.concat(chunks);
  177. }
  178. /* The new OpenSSH internal format abuses PEM headers */
  179. if (alg && alg.toLowerCase() === 'openssh')
  180. return (sshpriv.readSSHPrivate(type, buf, options));
  181. if (alg && alg.toLowerCase() === 'ssh2')
  182. return (rfc4253.readType(type, buf, options));
  183. var der = new asn1.BerReader(buf);
  184. der.originalInput = input;
  185. /*
  186. * All of the PEM file types start with a sequence tag, so chop it
  187. * off here
  188. */
  189. der.readSequence();
  190. /* PKCS#1 type keys name an algorithm in the banner explicitly */
  191. if (alg) {
  192. if (forceType)
  193. assert.strictEqual(forceType, 'pkcs1');
  194. return (pkcs1.readPkcs1(alg, type, der));
  195. } else {
  196. if (forceType)
  197. assert.strictEqual(forceType, 'pkcs8');
  198. return (pkcs8.readPkcs8(alg, type, der));
  199. }
  200. }
  201. function write(key, options, type) {
  202. assert.object(key);
  203. var alg = {
  204. 'ecdsa': 'EC',
  205. 'rsa': 'RSA',
  206. 'dsa': 'DSA',
  207. 'ed25519': 'EdDSA'
  208. }[key.type];
  209. var header;
  210. var der = new asn1.BerWriter();
  211. if (PrivateKey.isPrivateKey(key)) {
  212. if (type && type === 'pkcs8') {
  213. header = 'PRIVATE KEY';
  214. pkcs8.writePkcs8(der, key);
  215. } else {
  216. if (type)
  217. assert.strictEqual(type, 'pkcs1');
  218. header = alg + ' PRIVATE KEY';
  219. pkcs1.writePkcs1(der, key);
  220. }
  221. } else if (Key.isKey(key)) {
  222. if (type && type === 'pkcs1') {
  223. header = alg + ' PUBLIC KEY';
  224. pkcs1.writePkcs1(der, key);
  225. } else {
  226. if (type)
  227. assert.strictEqual(type, 'pkcs8');
  228. header = 'PUBLIC KEY';
  229. pkcs8.writePkcs8(der, key);
  230. }
  231. } else {
  232. throw (new Error('key is not a Key or PrivateKey'));
  233. }
  234. var tmp = der.buffer.toString('base64');
  235. var len = tmp.length + (tmp.length / 64) +
  236. 18 + 16 + header.length*2 + 10;
  237. var buf = Buffer.alloc(len);
  238. var o = 0;
  239. o += buf.write('-----BEGIN ' + header + '-----\n', o);
  240. for (var i = 0; i < tmp.length; ) {
  241. var limit = i + 64;
  242. if (limit > tmp.length)
  243. limit = tmp.length;
  244. o += buf.write(tmp.slice(i, limit), o);
  245. buf[o++] = 10;
  246. i = limit;
  247. }
  248. o += buf.write('-----END ' + header + '-----\n', o);
  249. return (buf.slice(0, o));
  250. }