diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index d3e8616f..dc5b853d 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -21,7 +21,8 @@ import crypto from '../crypto'; import enums from '../enums'; import util from '../util'; import defaultConfig from '../config'; -import { UnsupportedError } from './packet'; +import { UnsupportedError, writeTag } from './packet'; +import computeHKDF from '../crypto/hkdf'; /** * A Secret-Key packet contains all the information that is found in a @@ -140,14 +141,27 @@ class SecretKeyPacket extends PublicKeyPacket { this.symmetric = this.s2kUsage; } - // - [Optional] If secret data is encrypted (string-to-key usage octet - // not zero), an Initial Vector (IV) of the same length as the - // cipher's block size. + if (this.s2kUsage) { - this.iv = bytes.subarray( - i, - i + crypto.getCipher(this.symmetric).blockSize - ); + // - crypto-refresh: If string-to-key usage octet was 255, 254 [..], an initialization vector (IV) + // of the same length as the cipher's block size. + // - RFC4880bis (v5 keys, regardless of AEAD): an Initial Vector (IV) of the same length as the + // cipher's block size. If string-to-key usage octet was 253 the IV is used as the nonce for the AEAD algorithm. + // If the AEAD algorithm requires a shorter nonce, the high-order bits of the IV are used and the remaining bits MUST be zero + if (this.s2kUsage !== 253 || this.version === 5) { + this.iv = bytes.subarray( + i, + i + crypto.getCipher(this.symmetric).blockSize + ); + } else { + // crypto-refresh: If string-to-key usage octet was 253 (that is, the secret data is AEAD-encrypted), + // an initialization vector (IV) of size specified by the AEAD algorithm (see Section 5.13.2), which + // is used as the nonce for the AEAD algorithm. + this.iv = bytes.subarray( + i, + i + crypto.getAEADMode(this.aead).ivLength + ); + } i += this.iv.length; } @@ -342,19 +356,28 @@ class SecretKeyPacket extends PublicKeyPacket { this.s2k.generateSalt(); const cleartext = crypto.serializeParams(this.algorithm, this.privateParams); this.symmetric = enums.symmetric.aes256; - const key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); const { blockSize } = crypto.getCipher(this.symmetric); - this.iv = crypto.random.getRandomBytes(blockSize); if (config.aeadProtect) { this.s2kUsage = 253; this.aead = enums.aead.eax; const mode = crypto.getAEADMode(this.aead); + + const serializedPacketTag = writeTag(this.constructor.tag); + const key = await produceEncryptionKey(this.version, this.s2k, passphrase, this.symmetric, this.aead, serializedPacketTag); + const modeInstance = await mode(this.symmetric, key); - this.keyMaterial = await modeInstance.encrypt(cleartext, this.iv.subarray(0, mode.ivLength), new Uint8Array()); + this.iv = (this.version === 5) ? crypto.random.getRandomBytes(blockSize) : crypto.random.getRandomBytes(mode.ivLength); + const associateData = this.version === 5 ? + new Uint8Array() : + util.concatUint8Array([serializedPacketTag, this.writePublicKey()]); + + this.keyMaterial = await modeInstance.encrypt(cleartext, this.iv.subarray(0, mode.ivLength), associateData); } else { this.s2kUsage = 254; + const key = await produceEncryptionKey(this.version, this.s2k, passphrase, this.symmetric); + this.iv = crypto.random.getRandomBytes(blockSize); this.keyMaterial = await crypto.mode.cfb.encrypt(this.symmetric, key, util.concatUint8Array([ cleartext, await crypto.hash.sha1(cleartext, config) @@ -385,8 +408,10 @@ class SecretKeyPacket extends PublicKeyPacket { } let key; + const serializedPacketTag = writeTag(this.constructor.tag); // relevant for AEAD only if (this.s2kUsage === 254 || this.s2kUsage === 253) { - key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); + key = await produceEncryptionKey( + this.version, this.s2k, passphrase, this.symmetric, this.aead, serializedPacketTag); } else if (this.s2kUsage === 255) { throw new Error('Encrypted private key is authenticated using an insecure two-byte hash'); } else { @@ -398,7 +423,10 @@ class SecretKeyPacket extends PublicKeyPacket { const mode = crypto.getAEADMode(this.aead); const modeInstance = await mode(this.symmetric, key); try { - cleartext = await modeInstance.decrypt(this.keyMaterial, this.iv.subarray(0, mode.ivLength), new Uint8Array()); + const associateData = this.version === 5 ? + new Uint8Array() : + util.concatUint8Array([serializedPacketTag, this.writePublicKey()]); + cleartext = await modeInstance.decrypt(this.keyMaterial, this.iv.subarray(0, mode.ivLength), associateData); } catch (err) { if (err.message === 'Authentication tag mismatch') { throw new Error('Incorrect key passphrase: ' + err.message); @@ -425,6 +453,8 @@ class SecretKeyPacket extends PublicKeyPacket { this.isEncrypted = false; this.keyMaterial = null; this.s2kUsage = 0; + this.aead = null; + this.symmetric = null; } /** @@ -478,9 +508,27 @@ class SecretKeyPacket extends PublicKeyPacket { } } -async function produceEncryptionKey(s2k, passphrase, algorithm) { - const { keySize } = crypto.getCipher(algorithm); - return s2k.produceKey(passphrase, keySize); +/** + * Derive encryption key + * @param {Number} keyVersion - key derivation differs for v5 keys + * @param {module:type/s2k} s2k + * @param {String} passphrase + * @param {module:enums.symmetric} cipherAlgo + * @param {module:enums.aead} [aeadMode] - for AEAD-encrypted keys only (excluding v5) + * @param {Uint8Array} serializedPacketTag - for AEAD-encrypted keys only (excluding v5) + * @returns encryption key + */ +async function produceEncryptionKey(keyVersion, s2k, passphrase, cipherAlgo, aeadMode, serializedPacketTag) { + const { keySize } = crypto.getCipher(cipherAlgo); + const derivedKey = await s2k.produceKey(passphrase, keySize); + if (!aeadMode || keyVersion === 5) { + return derivedKey; + } + const info = util.concatUint8Array([ + serializedPacketTag, + new Uint8Array([keyVersion, cipherAlgo, aeadMode]) + ]); + return computeHKDF(enums.hash.sha256, derivedKey, new Uint8Array(), info, keySize); } export default SecretKeyPacket; diff --git a/test/general/key.js b/test/general/key.js index 9ed68d97..68d956d5 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -4,6 +4,7 @@ import { use as chaiUse, expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import chaiUse(chaiAsPromised); +import sinon from 'sinon'; import openpgp from '../initOpenpgp.js'; import util from '../../src/util.js'; import { getPreferredCipherSuite } from '../../src/key'; @@ -2912,7 +2913,7 @@ export default () => describe('Key', function() { let aeadProtectVal; tryTests('V4', versionSpecificTests, { - if: !openpgp.config.ci, + if: true, beforeEach: function() { v6KeysVal = openpgp.config.v6Keys; openpgp.config.v6Keys = false; @@ -2923,7 +2924,7 @@ export default () => describe('Key', function() { }); tryTests('V6', versionSpecificTests, { - if: !openpgp.config.ci, + if: true, beforeEach: function() { v6KeysVal = openpgp.config.v6Keys; aeadProtectVal = openpgp.config.aeadProtect; @@ -2987,6 +2988,172 @@ export default () => describe('Key', function() { expect(key).to.exist; }); + it('Parsing, decrypting, encrypting and serializing V5 key (AEAD-encrypted)', async function() { + const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xYwFZC7tvxYAAAAtCSsGAQQB2kcPAQEHQP/d1oBAqCKZYxb6k8foyX2Aa/VK +dHFymZPGvHRk1ncs/R0JAQMIrDnS3Bany9EAF6dwQSfPSdObc4ROYIMAnwAA +ADKV1OhGzwANnapimvODI6fK5F7/V0GxETY9WmnipnBzr4Fe9GZw4QD4Q4hd +IJMawjUBrs0MdjVAYWVhZC50ZXN0wpIFEBYKAEQFgmQu7b8ECwkHCAMVCAoE +FgACAQIZAQKbAwIeByKhBQ/Y89PNwfdXUdI/td5Q9rNrYP9mb7Dg6k/3nxTg +ugQ5AyIBAgAAf0kBAJv0OQvd4u8R0f3HAsmQeqMnwNA4or75BOn/ieApNZUt +AP9kQVmYEk4+MV57Us15l2kQEslLDr3qiH5+VCICdEprB8eRBWQu7b8SAAAA +MgorBgEEAZdVAQUBAQdA4IgEkfze3eNKRz6DgzGSJxw/CV/5Rp5u4Imn47h7 +pyADAQgH/R0JAQMIwayD3R4E0ugAyszSmOIpaLJ40YGBp5uU7wAAADKmSv4W +tio7GfZCVl8eJ7xX3J1b0iMvEm876tUeHANQlYYCWz+2ahmPVe79zzZA9OhN +FcJ6BRgWCAAsBYJkLu2/ApsMIqEFD9jz083B91dR0j+13lD2s2tg/2ZvsODq +T/efFOC6BDkAAHcjAPwIPNHnR9bKmkVop6cE05dCIpZ/W8zXDGnjKYrrC4Hb +4gEAmISD1GRkNOmCV8aHwN5svO6HuwXR4cR3o3l7HlYeag8= +=wpkQ +-----END PGP PRIVATE KEY BLOCK-----`; + const passphrase = 'password'; + const encryptedKey = await openpgp.readKey({ armoredKey }); + const decryptedKey = await openpgp.decryptKey({ + privateKey: encryptedKey, + passphrase + }); + const reecryptedKey = await openpgp.encryptKey({ + privateKey: decryptedKey, + passphrase, + config: { aeadProtect: true } + }); + expect(reecryptedKey.keyPacket.s2kUsage).to.equal(253); + const redecryptedKey = await openpgp.decryptKey({ + privateKey: reecryptedKey, + passphrase + }); + expect(redecryptedKey.write()).to.deep.equal(decryptedKey.write()); + }); + + it('Parsing, decrypting, encrypting and serializing V4 key (AEAD-encrypted)', async function() { + // key from gopenpgp + const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +oZIvKIVq9Vqf8XJVjMDbRMNTmh3a5A== +-----END PGP PRIVATE KEY BLOCK-----`; + const passphrase = 'password'; + const encryptedKey = await openpgp.readKey({ armoredKey }); + const decryptedKey = await openpgp.decryptKey({ + privateKey: encryptedKey, + passphrase + }); + const reecryptedKey = await openpgp.encryptKey({ + privateKey: decryptedKey, + passphrase, + config: { aeadProtect: true } + }); + expect(reecryptedKey.keyPacket.s2kUsage).to.equal(253); + const redecryptedKey = await openpgp.decryptKey({ + privateKey: reecryptedKey, + passphrase + }); + expect(redecryptedKey.write()).to.deep.equal(decryptedKey.write()); + }); + + it('Parsing, decrypting, encrypting and serializing V6 key (AEAD-encrypted)', async function() { + // official test vector from https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-10.html#appendix-A.5 + const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xYIGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laP9JgkC +FARdb9ccngltHraRe25uHuyuAQQVtKipJ0+r5jL4dacGWSAheCWPpITYiyfyIOPS +3gIDyg8f7strd1OB4+LZsUhcIjOMpVHgmiY/IutJkulneoBYwrEGHxsKAAAAQgWC +Y4d/4wMLCQcFFQoOCAwCFgACmwMCHgkiIQbLGGxPBgmml+TVLfpscisMHx4nwYpW +cI9lJewnutmsyQUnCQIHAgAAAACtKCAQPi19In7A5tfORHHbNr/JcIMlNpAnFJin +7wV2wH+q4UWFs7kDsBJ+xP2i8CMEWi7Ha8tPlXGpZR4UruETeh1mhELIj5UeM8T/ +0z+5oX1RHu11j8bZzFDLX9eTsgOdWATHggZjh3/jGQAAACCGkySDZ/nlAV25Ivj0 +gJXdp4SYfy1ZhbEvutFsr15ENf0mCQIUBA5hhGgp2oaavg6mFUXcFMwBBBUuE8qf +9Ock+xwusd+GAglBr5LVyr/lup3xxQvHXFSjjA2haXfoN6xUGRdDEHI6+uevKjVR +v5oAxgu7eJpaXNjCmwYYGwoAAAAsBYJjh3/jApsMIiEGyxhsTwYJppfk1S36bHIr +DB8eJ8GKVnCPZSXsJ7rZrMkAAAAABAEgpukYbZ1ZNfyP5WMUzbUnSGpaUSD5t2Ki +Nacp8DkBClZRa2c3AMQzSDXa9jGhYzxjzVb5scHDzTkjyRZWRdTq8U6L4da+/+Kt +ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG +-----END PGP PRIVATE KEY BLOCK-----`; + const passphrase = 'correct horse battery staple'; + const encryptedKey = await openpgp.readKey({ armoredKey }); + + // avoid argon2's expensive computation + const stubArgon2PrimaryKey = sinon.stub(encryptedKey.keyPacket.s2k, 'produceKey').returns( + util.hexToUint8Array('832bd2662a5c2b251ee3fc82aec349a766ca539015880133002e5a21960b3bcf')); + const stubArgon2Subkey = sinon.stub(encryptedKey.subkeys[0].keyPacket.s2k, 'produceKey').returns( + util.hexToUint8Array('f74a6ce873a089ef13a3da9ac059777bb22340d15eaa6c9dc0f8ef09035c67cd')); + + try { + const decryptedKey = await openpgp.decryptKey({ + privateKey: encryptedKey, + passphrase + }); + + const reecryptedKey = await openpgp.encryptKey({ + privateKey: decryptedKey, + passphrase, + config: { aeadProtect: true } + }); + expect(reecryptedKey.keyPacket.s2kUsage).to.equal(253); + const redecryptedKey = await openpgp.decryptKey({ + privateKey: reecryptedKey, + passphrase + }); + expect(redecryptedKey.write()).to.deep.equal(decryptedKey.write()); + } finally { + stubArgon2PrimaryKey.restore(); + stubArgon2Subkey.restore(); + } + }); + it('Parsing ECDH key with unknown kdf param version', async function() { // subkey with unknown kdfParam version 255. Parsing should not fail, the subkey should simply dropped const key = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK-----