From 1c83b29536acd5621f811b51f150969347252b4e Mon Sep 17 00:00:00 2001 From: Naoto Nakamura Date: Sun, 15 Jan 2023 14:53:38 +0900 Subject: [PATCH] Implemented password encryption for pem(pkcs#1,pkcs#8) --- lib/formats/pem.js | 151 ++++++++++++++++++++++++++++++++++++++++-- test/assets/pkcs1-enc | 42 ++++++++++++ test/assets/pkcs8-enc | 42 ++++++++++++ test/private-key.js | 110 ++++++++++++++++++++++++++++++ 4 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 test/assets/pkcs1-enc create mode 100644 test/assets/pkcs8-enc diff --git a/lib/formats/pem.js b/lib/formats/pem.js index bbe78fc..10b875b 100644 --- a/lib/formats/pem.js +++ b/lib/formats/pem.js @@ -244,15 +244,141 @@ function write(key, options, type) { var der = new asn1.BerWriter(); + //Encryption + var cipher = 'none'; + var kdf = 'none'; + var kdfopts = Buffer.alloc(0); + var cinf; + var passphrase; + if (PrivateKey.isPrivateKey(key)) { + if (options !== undefined) { + passphrase = options.passphrase; + if (typeof (passphrase) === 'string') + passphrase = Buffer.from(passphrase, 'utf-8'); + if (passphrase !== undefined) { + assert.buffer(passphrase, 'options.passphrase'); + assert.optionalString(options.cipher, 'options.cipher'); + cipher = options.cipher; + if (cipher === undefined) + cipher = 'aes128-cbc'; + cinf = utils.opensshCipherInfo(cipher); + } + } + } + + var privBuf; + if (PrivateKey.isPrivateKey(key)) { if (type && type === 'pkcs8') { - header = 'PRIVATE KEY'; - pkcs8.writePkcs8(der, key); + if(cinf) header = 'ENCRYPTED PRIVATE KEY'; + else header = 'PRIVATE KEY'; + + if (cinf !== undefined) { + der.startSequence(); + der.startSequence(); + + der.writeOID(OID_PBES2); + + der.startSequence(); /* PBES2-params */ + + der.startSequence(); /* keyDerivationFunc */ + + der.writeOID(OID_PBKDF2); + + der.startSequence(); + + var salt = crypto.randomBytes(16); + + der.writeBuffer(salt, asn1.Ber.OctetString); + + //"iterations" is fixed to 1000 + var iterations = 1000; + der.writeInt(iterations); + + //"hash" is fixed to sha1 + var hashAlg = "sha1"; + + der.startSequence(); + der.writeOID(HASH_TO_OID[hashAlg]); + der.endSequence(); + + der.endSequence(); + + der.endSequence(); /* keyDerivationFunc */ + + + der.startSequence(); /* encryptionScheme */ + + //cipher + var cipherOid = CIPHER_TO_OID[cipher]; + if(cipherOid === undefined) { + throw (new Error('Unsupported PBES2 cipher: ' + cipher)); + } + + der.writeOID(cipherOid); + + //iv + var iv = crypto.randomBytes(cinf.blockSize); + der.writeBuffer(iv, asn1.Ber.OctetString); + + der.endSequence(); /* encryptionScheme */ + + der.endSequence(); /* PBES2-params */ + + der.endSequence(); + + var ckey = utils.pbkdf2(hashAlg, salt, iterations, cinf.keySize, + passphrase); + + var cipherStream = crypto.createCipheriv(cinf.opensslName, ckey, iv); + cipherStream.setAutoPadding(true); + + var eder = new asn1.BerWriter(); + pkcs8.writePkcs8(eder, key); + + var chunk, chunks = []; + cipherStream.once('error', function (e) { + throw (e); + }); + + cipherStream.write(eder.buffer); + cipherStream.end(); + while ((chunk = cipherStream.read()) !== null) + chunks.push(chunk); + var ebuf = Buffer.concat(chunks); + + der.writeBuffer(ebuf, asn1.Ber.OctetString); + + der.endSequence(); + + }else{ + pkcs8.writePkcs8(der, key); + } + } else { if (type) assert.strictEqual(type, 'pkcs1'); header = alg + ' PRIVATE KEY'; pkcs1.writePkcs1(der, key); + + if (cinf !== undefined) { + var iv = crypto.randomBytes(cinf.blockSize); + var ckey = utils.opensslKeyDeriv(cinf.opensslName, iv, passphrase, 1).key; + + var cipherStream = crypto.createCipheriv(cinf.opensslName, + ckey, iv); + cipherStream.setAutoPadding(true); + var chunk, chunks = []; + cipherStream.once('error', function (e) { + throw (e); + }); + + cipherStream.write(der.buffer); + cipherStream.end(); + while ((chunk = cipherStream.read()) !== null) + chunks.push(chunk); + privBuf = Buffer.concat(chunks); + } } } else if (Key.isKey(key)) { @@ -270,12 +396,29 @@ function write(key, options, type) { throw (new Error('key is not a Key or PrivateKey')); } - var tmp = der.buffer.toString('base64'); + if(privBuf === undefined) privBuf = der.buffer; + + var top = ""; + if (PrivateKey.isPrivateKey(key)) { + if (type === undefined || type === 'pkcs1') { + if(cinf){ + top += + 'Proc-Type: 4,ENCRYPTED\n' + + 'DEK-Info: ' + cinf.opensslName.toUpperCase() + "," + iv.toString("hex").toUpperCase() + + '\n\n'; + } + } + } + + var tmp = privBuf.toString('base64'); var len = tmp.length + (tmp.length / 64) + - 18 + 16 + header.length*2 + 10; + 18 + 16 + header.length*2 + 10 + top.length; var buf = Buffer.alloc(len); var o = 0; o += buf.write('-----BEGIN ' + header + '-----\n', o); + + o += buf.write(top, o); + for (var i = 0; i < tmp.length; ) { var limit = i + 64; if (limit > tmp.length) diff --git a/test/assets/pkcs1-enc b/test/assets/pkcs1-enc new file mode 100644 index 0000000..a4d360f --- /dev/null +++ b/test/assets/pkcs1-enc @@ -0,0 +1,42 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,8C0D86A3F866B3F73591946566938382 + +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 +-----END RSA PRIVATE KEY----- diff --git a/test/assets/pkcs8-enc b/test/assets/pkcs8-enc new file mode 100644 index 0000000..1e5538c --- /dev/null +++ b/test/assets/pkcs8-enc @@ -0,0 +1,42 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +1+45uQs68P7GdtYcNQxsjLpIQg== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/private-key.js b/test/private-key.js index 3eb25ba..3d19ada 100644 --- a/test/private-key.js +++ b/test/private-key.js @@ -212,6 +212,58 @@ test('parse pkcs8 unencrypted private keys', function (t) { t.end(); }); +test('parse and produce pkcs#1 encrypted pem rsa', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, 'pkcs1-enc')); + t.throws(function () { + sshpk.parsePrivateKey(keyPem, 'pkcs1'); + }); + t.throws(function () { + sshpk.parsePrivateKey(keyPem, 'pkcs1', + { passphrase: 'incorrect' }); + }); + var key = sshpk.parsePrivateKey(keyPem, 'pkcs1', + { passphrase: 'foobar' }); + t.strictEqual(key.type, 'rsa'); + + var keySsh2 = key.toBuffer('pkcs1', { passphrase: 'foobar2' }); + t.throws(function () { + sshpk.parsePrivateKey(keySsh2, 'pkcs1', + { passphrase: 'foobar' }); + }); + var key2 = sshpk.parsePrivateKey(keySsh2, 'pkcs1', + { passphrase: 'foobar2' }); + t.strictEqual(key2.type, 'rsa'); + + //console.log(); + + t.end(); +}); + +test('parse and produce pkcs#8 encrypted pem rsa', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, 'pkcs8-enc')); + t.throws(function () { + sshpk.parsePrivateKey(keyPem, 'pkcs8'); + }); + t.throws(function () { + sshpk.parsePrivateKey(keyPem, 'pkcs8', + { passphrase: 'incorrect' }); + }); + var key = sshpk.parsePrivateKey(keyPem, 'pkcs8', + { passphrase: 'foobar' }); + t.strictEqual(key.type, 'rsa'); + + var keySsh2 = key.toBuffer('pkcs8', { passphrase: 'foobar2' }); + t.throws(function () { + sshpk.parsePrivateKey(keySsh2, 'pkcs8', + { passphrase: 'foobar' }); + }); + var key2 = sshpk.parsePrivateKey(keySsh2, 'pkcs8', + { passphrase: 'foobar2' }); + t.strictEqual(key2.type, 'rsa'); + + t.end(); +}); + test('parse and produce encrypted ssh-private ecdsa', function (t) { var keySsh = fs.readFileSync(path.join(testDir, 'id_ecdsa_enc')); t.throws(function () { @@ -339,6 +391,64 @@ test('PrivateKey#createSign on ECDSA 256 key', function (t) { t.end(); }); +test('PrivateKey#createSign on encrypted pkcs#1 key', function (t) { + var privateKey = sshpk.parsePrivateKey(fs.readFileSync(path.join(testDir, 'id_rsa')), 'pem'); + var publicKey = privateKey.toPublic(); + + var encrypted = privateKey.toBuffer("pkcs1", {passphrase: "foobar"}); + + t.throws(function () { + sshpk.parsePrivateKey(encrypted, "pem"); + }); + + t.throws(function () { + sshpk.parsePrivateKey(encrypted, "pem", {passphrase: "incorrect"}); + }); + + var encPrivateKey = sshpk.parsePrivateKey(encrypted, "pem", {passphrase: "foobar"}); + + var s = encPrivateKey.createSign('sha256'); + s.update('foobar'); + var sig = s.sign(); + t.ok(sig); + t.ok(sig instanceof sshpk.Signature); + + var v = publicKey.createVerify('sha256'); + v.update('foobar'); + t.ok(v.verify(sig)); + + t.end(); +}); + +test('PrivateKey#createSign on encrypted pkcs#8 key', function (t) { + var privateKey = sshpk.parsePrivateKey(fs.readFileSync(path.join(testDir, 'id_rsa')), 'pem'); + var publicKey = privateKey.toPublic(); + + var encrypted = privateKey.toBuffer("pkcs8", {passphrase: "foobar"}); + + t.throws(function () { + sshpk.parsePrivateKey(encrypted, "pem"); + }); + + t.throws(function () { + sshpk.parsePrivateKey(encrypted, "pem", {passphrase: "incorrect"}); + }); + + var encPrivateKey = sshpk.parsePrivateKey(encrypted, "pem", {passphrase: "foobar"}); + + var s = encPrivateKey.createSign('sha256'); + s.update('foobar'); + var sig = s.sign(); + t.ok(sig); + t.ok(sig instanceof sshpk.Signature); + + var v = publicKey.createVerify('sha256'); + v.update('foobar'); + t.ok(v.verify(sig)); + + t.end(); +}); + test('PrivateKey.generate ecdsa default', function (t) { var key = sshpk.generatePrivateKey('ecdsa'); t.ok(sshpk.PrivateKey.isPrivateKey(key));