diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index 6e940779..2dcfea54 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -18,6 +18,7 @@ import { newS2KFromConfig, newS2KFromType } from '../type/s2k'; import defaultConfig from '../config'; import crypto from '../crypto'; +import computeHKDF from '../crypto/hkdf'; import enums from '../enums'; import util from '../util'; import { UnsupportedError } from './packet'; @@ -44,7 +45,7 @@ class SymEncryptedSessionKeyPacket { * @param {Object} [config] - Full configuration, defaults to openpgp.config */ constructor(config = defaultConfig) { - this.version = config.aeadProtect ? 5 : 4; + this.version = config.aeadProtect ? 6 : 4; this.sessionKey = null; /** * Algorithm to encrypt the session key with @@ -74,18 +75,28 @@ class SymEncryptedSessionKeyPacket { read(bytes) { let offset = 0; - // A one-octet version number. The only currently defined version is 4. + // A one-octet version number with value 4, 5 or 6. this.version = bytes[offset++]; - if (this.version !== 4 && this.version !== 5) { + if (this.version !== 4 && this.version !== 5 && this.version !== 6) { throw new UnsupportedError(`Version ${this.version} of the SKESK packet is unsupported.`); } + if (this.version === 6) { + // A one-octet scalar octet count of the following 5 fields. + offset++; + } + // A one-octet number describing the symmetric algorithm used. const algo = bytes[offset++]; - if (this.version === 5) { + if (this.version >= 5) { // A one-octet AEAD algorithm. this.aeadAlgorithm = bytes[offset++]; + + if (this.version === 6) { + // A one-octet scalar octet count of the following field. + offset++; + } } // A string-to-key (S2K) specifier, length as defined above. @@ -93,7 +104,7 @@ class SymEncryptedSessionKeyPacket { this.s2k = newS2KFromType(s2kType); offset += this.s2k.read(bytes.subarray(offset, bytes.length)); - if (this.version === 5) { + if (this.version >= 5) { const mode = crypto.getAEADMode(this.aeadAlgorithm); // A starting initialization vector of size specified by the AEAD @@ -103,7 +114,7 @@ class SymEncryptedSessionKeyPacket { // The encrypted session key itself, which is decrypted with the // string-to-key object. This is optional in version 4. - if (this.version === 5 || offset < bytes.length) { + if (this.version >= 5 || offset < bytes.length) { this.encrypted = bytes.subarray(offset, bytes.length); this.sessionKeyEncryptionAlgorithm = algo; } else { @@ -123,10 +134,15 @@ class SymEncryptedSessionKeyPacket { let bytes; - if (this.version === 5) { - bytes = util.concatUint8Array([new Uint8Array([this.version, algo, this.aeadAlgorithm]), this.s2k.write(), this.iv, this.encrypted]); + const s2k = this.s2k.write(); + if (this.version === 6) { + const s2kLen = s2k.length; + const fieldsLen = 3 + s2kLen + this.iv.length; + bytes = util.concatUint8Array([new Uint8Array([this.version, fieldsLen, algo, this.aeadAlgorithm, s2kLen]), s2k, this.iv, this.encrypted]); + } else if (this.version === 5) { + bytes = util.concatUint8Array([new Uint8Array([this.version, algo, this.aeadAlgorithm]), s2k, this.iv, this.encrypted]); } else { - bytes = util.concatUint8Array([new Uint8Array([this.version, algo]), this.s2k.write()]); + bytes = util.concatUint8Array([new Uint8Array([this.version, algo]), s2k]); if (this.encrypted !== null) { bytes = util.concatUint8Array([bytes, this.encrypted]); @@ -150,10 +166,11 @@ class SymEncryptedSessionKeyPacket { const { blockSize, keySize } = crypto.getCipher(algo); const key = await this.s2k.produceKey(passphrase, keySize); - if (this.version === 5) { + if (this.version >= 5) { const mode = crypto.getAEADMode(this.aeadAlgorithm); const adata = new Uint8Array([0xC0 | SymEncryptedSessionKeyPacket.tag, this.version, this.sessionKeyEncryptionAlgorithm, this.aeadAlgorithm]); - const modeInstance = await mode(algo, key); + const encryptionKey = this.version === 6 ? await computeHKDF(enums.hash.sha256, key, new Uint8Array(), adata, keySize) : key; + const modeInstance = await mode(algo, encryptionKey); this.sessionKey = await modeInstance.decrypt(this.encrypted, this.iv, adata); } else if (this.encrypted !== null) { const decrypted = await crypto.mode.cfb.decrypt(algo, key, this.encrypted, new Uint8Array(blockSize)); @@ -183,24 +200,25 @@ class SymEncryptedSessionKeyPacket { this.s2k.generateSalt(); const { blockSize, keySize } = crypto.getCipher(algo); - const encryptionKey = await this.s2k.produceKey(passphrase, keySize); + const key = await this.s2k.produceKey(passphrase, keySize); if (this.sessionKey === null) { this.sessionKey = crypto.generateSessionKey(this.sessionKeyAlgorithm); } - if (this.version === 5) { + if (this.version >= 5) { const mode = crypto.getAEADMode(this.aeadAlgorithm); this.iv = crypto.random.getRandomBytes(mode.ivLength); // generate new random IV - const associatedData = new Uint8Array([0xC0 | SymEncryptedSessionKeyPacket.tag, this.version, this.sessionKeyEncryptionAlgorithm, this.aeadAlgorithm]); + const adata = new Uint8Array([0xC0 | SymEncryptedSessionKeyPacket.tag, this.version, this.sessionKeyEncryptionAlgorithm, this.aeadAlgorithm]); + const encryptionKey = this.version === 6 ? await computeHKDF(enums.hash.sha256, key, new Uint8Array(), adata, keySize) : key; const modeInstance = await mode(algo, encryptionKey); - this.encrypted = await modeInstance.encrypt(this.sessionKey, this.iv, associatedData); + this.encrypted = await modeInstance.encrypt(this.sessionKey, this.iv, adata); } else { const toEncrypt = util.concatUint8Array([ new Uint8Array([this.sessionKeyAlgorithm]), this.sessionKey ]); - this.encrypted = await crypto.mode.cfb.encrypt(algo, encryptionKey, toEncrypt, new Uint8Array(blockSize), config); + this.encrypted = await crypto.mode.cfb.encrypt(algo, key, toEncrypt, new Uint8Array(blockSize), config); } } } diff --git a/test/general/config.js b/test/general/config.js index 0d5393d1..66032a91 100644 --- a/test/general/config.js +++ b/test/general/config.js @@ -284,7 +284,7 @@ n9/quqtmyOtYOA6gXNCw0Fal3iANKBmsPmYI const armored2 = await openpgp.encrypt({ message, passwords, config }); const encrypted2 = await openpgp.readMessage({ armoredMessage: armored2 }); const { packets: [skesk2, encData2] } = encrypted2; - expect(skesk2.version).to.equal(5); + expect(skesk2.version).to.equal(6); expect(encData2.constructor.tag).to.equal(openpgp.enums.packet.symEncryptedIntegrityProtectedData); expect(encData2.version).to.equal(2); const { packets: [compressed] } = await encrypted2.decrypt(null, passwords, null, encrypted2.fromStream, openpgp.config); diff --git a/test/general/packet.js b/test/general/packet.js index 329e13c7..11397a45 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -644,6 +644,45 @@ export default () => describe('Packet', function() { const aeadProtectVal = openpgp.config.aeadProtect; openpgp.config.aeadProtect = true; + try { + const passphrase = 'hello'; + const algo = openpgp.enums.symmetric.aes256; + const testText = input.createSomeMessage(); + + const literal = new openpgp.LiteralDataPacket(); + literal.setText(testText); + const skesk = new openpgp.SymEncryptedSessionKeyPacket(); + skesk.version = 5; + const aeadEnc = new openpgp.AEADEncryptedDataPacket(); + aeadEnc.packets = new openpgp.PacketList(); + aeadEnc.packets.push(literal); + const msg = new openpgp.PacketList(); + msg.push(skesk); + msg.push(aeadEnc); + + skesk.sessionKeyAlgorithm = algo; + await skesk.encrypt(passphrase, openpgp.config); + + const key = skesk.sessionKey; + await aeadEnc.encrypt(algo, key, undefined, openpgp.config); + + const msg2 = new openpgp.PacketList(); + await msg2.read(msg.write(), allAllowedPackets); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(await stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aeadProtect = aeadProtectVal; + } + }); + + it('Sym. encrypted session key reading/writing (SEIPDv2)', async function() { + const aeadProtectVal = openpgp.config.aeadProtect; + openpgp.config.aeadProtect = true; + try { const passphrase = 'hello'; const algo = openpgp.enums.symmetric.aes256; @@ -723,10 +762,12 @@ export default () => describe('Packet', function() { literal.setBytes(util.stringToUint8Array('Hello, world!\n'), openpgp.enums.literal.binary); literal.filename = ''; const skesk = new openpgp.SymEncryptedSessionKeyPacket(); + skesk.version = 5; skesk.sessionKeyAlgorithm = algo; const encData = new openpgp.AEADEncryptedDataPacket(); encData.packets = new openpgp.PacketList(); encData.packets.push(literal); + encData.aeadAlgorithm = skesk.aeadAlgorithm = openpgp.enums.aead.eax; const msg = new openpgp.PacketList(); msg.push(skesk); msg.push(encData); @@ -800,6 +841,7 @@ export default () => describe('Packet', function() { literal.setBytes(util.stringToUint8Array('Hello, world!\n'), openpgp.enums.literal.binary); literal.filename = ''; const skesk = new openpgp.SymEncryptedSessionKeyPacket(); + skesk.version = 5; skesk.sessionKeyAlgorithm = algo; const enc = new openpgp.AEADEncryptedDataPacket(); enc.packets = new openpgp.PacketList(); @@ -833,6 +875,246 @@ export default () => describe('Packet', function() { } }); + it('Sym. encrypted session key reading/writing test vector (SEIPDv2, EAX)', async function() { + // From https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#appendix-A.5 + + const nodeCrypto = util.getNodeCrypto(); + if (!nodeCrypto) return; + + const aeadProtectVal = openpgp.config.aeadProtect; + const aeadChunkSizeByteVal = openpgp.config.aeadChunkSizeByte; + const s2kIterationCountByteVal = openpgp.config.s2kIterationCountByte; + openpgp.config.aeadProtect = true; + openpgp.config.aeadChunkSizeByte = 6; + openpgp.config.s2kIterationCountByte = 255; + + const padding = util.hexToUint8Array('ae 5b f0 cd 67 05 50 03 55 81 6c b0 c8 ff'.replace(/\s+/g, '')); + const salt = util.hexToUint8Array('a5 ae 57 9d 1f c5 d8 2b'.replace(/\s+/g, '')); + const sessionKey = util.hexToUint8Array('38 81 ba fe 98 54 12 45 9b 86 c3 6f 98 cb 9a 5e'.replace(/\s+/g, '')); + const sessionIV = util.hexToUint8Array('69 22 4f 91 99 93 b3 50 6f a3 b5 9a 6a 73 cf f8'.replace(/\s+/g, '')); + const dataSalt = util.hexToUint8Array('9f f9 0e 3b 32 19 64 f3 a4 29 13 c8 dc c6 61 93 25 01 52 27 ef b7 ea ea a4 9f 04 c2 e6 74 17 5d'.replace(/\s+/g, '')); + + const randomBytesStub = sinon.stub(nodeCrypto, 'randomBytes'); + randomBytesStub.onCall(0).returns(padding); + randomBytesStub.onCall(1).returns(salt); + randomBytesStub.onCall(2).returns(sessionKey); + randomBytesStub.onCall(3).returns(sessionIV); + randomBytesStub.onCall(4).returns(dataSalt); + + const { data: packetBytes } = await openpgp.unarmor(`-----BEGIN PGP MESSAGE----- + +w0AGHgcBCwMIpa5XnR/F2Cv/aSJPkZmTs1Bvo7WaanPP+MXvxfQcV/tU4cImgV14 +KPX5LEVOtl6+AKtZhsaObnxV0mkCBwEGn/kOOzIZZPOkKRPI3MZhkyUBUifvt+rq +pJ8EwuZ0F11KPSJu1q/LnKmsEiwUcOEcY9TAqyQcapOK1Iv5mlqZuQu6gyXeYQR1 +QCWKt5Wala0FHdqW6xVDHf719eIlXKeCYVRuM5o= +-----END PGP MESSAGE----- +`); + + try { + const passphrase = 'password'; + const algo = openpgp.enums.symmetric.aes128; + + const skesk = new openpgp.SymEncryptedSessionKeyPacket(); + skesk.sessionKeyAlgorithm = algo; + const literal = new openpgp.LiteralDataPacket(0); + literal.setBytes(util.stringToUint8Array('Hello, world!'), openpgp.enums.literal.binary); + literal.filename = ''; + const pad = new openpgp.PaddingPacket(); + await pad.createPadding(14); + const enc = new openpgp.SymEncryptedIntegrityProtectedDataPacket(); + enc.version = 2; + enc.aeadAlgorithm = skesk.aeadAlgorithm = openpgp.enums.aead.eax; + enc.packets = new openpgp.PacketList(); + enc.packets.push(literal); + enc.packets.push(pad); + const msg = new openpgp.PacketList(); + msg.push(skesk); + msg.push(enc); + + await skesk.encrypt(passphrase, openpgp.config); + + const key = skesk.sessionKey; + await enc.encrypt(algo, key, undefined, openpgp.config); + + const data = msg.write(); + expect(await stream.readToEnd(stream.clone(data))).to.deep.equal(packetBytes); + + const msg2 = new openpgp.PacketList(); + await msg2.read(data, allAllowedPackets); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(await stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aeadProtect = aeadProtectVal; + openpgp.config.aeadChunkSizeByte = aeadChunkSizeByteVal; + openpgp.config.s2kIterationCountByte = s2kIterationCountByteVal; + randomBytesStub.restore(); + } + }); + + it('Sym. encrypted session key reading/writing test vector (SEIPDv2, OCB)', async function() { + // From https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#appendix-A.6 + + const nodeCrypto = util.getNodeCrypto(); + if (!nodeCrypto) return; + + const aeadProtectVal = openpgp.config.aeadProtect; + const aeadChunkSizeByteVal = openpgp.config.aeadChunkSizeByte; + const s2kIterationCountByteVal = openpgp.config.s2kIterationCountByte; + openpgp.config.aeadProtect = true; + openpgp.config.aeadChunkSizeByte = 6; + openpgp.config.s2kIterationCountByte = 255; + + const padding = util.hexToUint8Array('ae 6a a1 64 9b 56 aa 83 5b 26 13 90 2b d2'.replace(/\s+/g, '')); + const salt = util.hexToUint8Array('56 a2 98 d2 f5 e3 64 53'.replace(/\s+/g, '')); + const sessionKey = util.hexToUint8Array('28 e7 9a b8 23 97 d3 c6 3d e2 4a c2 17 d7 b7 91'.replace(/\s+/g, '')); + const sessionIV = util.hexToUint8Array('cf cc 5c 11 66 4e db 9d b4 25 90 d7 dc 46 b0'.replace(/\s+/g, '')); + const dataSalt = util.hexToUint8Array('20 a6 61 f7 31 fc 9a 30 32 b5 62 33 26 02 7e 3a 5d 8d b5 74 8e be ff 0b 0c 59 10 d0 9e cd d6 41'.replace(/\s+/g, '')); + + const randomBytesStub = sinon.stub(nodeCrypto, 'randomBytes'); + randomBytesStub.onCall(0).returns(padding); + randomBytesStub.onCall(1).returns(salt); + randomBytesStub.onCall(2).returns(sessionKey); + randomBytesStub.onCall(3).returns(sessionIV); + randomBytesStub.onCall(4).returns(dataSalt); + + const { data: packetBytes } = await openpgp.unarmor(`-----BEGIN PGP MESSAGE----- + +wz8GHQcCCwMIVqKY0vXjZFP/z8xcEWZO2520JZDX3EawckG2EsOBLP/76gDyNHsl +ZBEj+IeuYNT9YU4IN9gZ02zSaQIHAgYgpmH3MfyaMDK1YjMmAn46XY21dI6+/wsM +WRDQns3WQf+f04VidYA1vEl1TOG/P/+n2tCjuBBPUTPPQqQQCoPu9MobSAGohGv0 +K82nyM6dZeIS8wHLzZj9yt5pSod61CRzI/boVw== +-----END PGP MESSAGE----- +`); + + try { + const passphrase = 'password'; + const algo = openpgp.enums.symmetric.aes128; + + const skesk = new openpgp.SymEncryptedSessionKeyPacket(); + skesk.sessionKeyAlgorithm = algo; + const literal = new openpgp.LiteralDataPacket(0); + literal.setBytes(util.stringToUint8Array('Hello, world!'), openpgp.enums.literal.binary); + literal.filename = ''; + const pad = new openpgp.PaddingPacket(); + await pad.createPadding(14); + const enc = new openpgp.SymEncryptedIntegrityProtectedDataPacket(); + enc.version = 2; + enc.aeadAlgorithm = skesk.aeadAlgorithm = openpgp.enums.aead.ocb; + enc.packets = new openpgp.PacketList(); + enc.packets.push(literal); + enc.packets.push(pad); + const msg = new openpgp.PacketList(); + msg.push(skesk); + msg.push(enc); + + await skesk.encrypt(passphrase, openpgp.config); + + const key = skesk.sessionKey; + await enc.encrypt(algo, key, undefined, openpgp.config); + + const data = msg.write(); + expect(await stream.readToEnd(stream.clone(data))).to.deep.equal(packetBytes); + + const msg2 = new openpgp.PacketList(); + await msg2.read(data, allAllowedPackets); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(await stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aeadProtect = aeadProtectVal; + openpgp.config.aeadChunkSizeByte = aeadChunkSizeByteVal; + openpgp.config.s2kIterationCountByte = s2kIterationCountByteVal; + randomBytesStub.restore(); + } + }); + + it('Sym. encrypted session key reading/writing test vector (SEIPDv2, GCM)', async function() { + // From https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#appendix-A.7 + + const nodeCrypto = util.getNodeCrypto(); + if (!nodeCrypto) return; + + const aeadProtectVal = openpgp.config.aeadProtect; + const aeadChunkSizeByteVal = openpgp.config.aeadChunkSizeByte; + const s2kIterationCountByteVal = openpgp.config.s2kIterationCountByte; + openpgp.config.aeadProtect = true; + openpgp.config.aeadChunkSizeByte = 6; + openpgp.config.s2kIterationCountByte = 255; + + const padding = util.hexToUint8Array('1c e2 26 9a 9e dd ef 81 03 21 72 b7 ed 7c'.replace(/\s+/g, '')); + const salt = util.hexToUint8Array('e9 d3 97 85 b2 07 00 08'.replace(/\s+/g, '')); + const sessionKey = util.hexToUint8Array('19 36 fc 85 68 98 02 74 bb 90 0d 83 19 36 0c 77'.replace(/\s+/g, '')); + const sessionIV = util.hexToUint8Array('b4 2e 7c 48 3e f4 88 44 57 cb 37 26'.replace(/\s+/g, '')); + const dataSalt = util.hexToUint8Array('fc b9 44 90 bc b9 8b bd c9 d1 06 c6 09 02 66 94 0f 72 e8 9e dc 21 b5 59 6b 15 76 b1 01 ed 0f 9f'.replace(/\s+/g, '')); + + const randomBytesStub = sinon.stub(nodeCrypto, 'randomBytes'); + randomBytesStub.onCall(0).returns(padding); + randomBytesStub.onCall(1).returns(salt); + randomBytesStub.onCall(2).returns(sessionKey); + randomBytesStub.onCall(3).returns(sessionIV); + randomBytesStub.onCall(4).returns(dataSalt); + + const { data: packetBytes } = await openpgp.unarmor(`-----BEGIN PGP MESSAGE----- + +wzwGGgcDCwMI6dOXhbIHAAj/tC58SD70iERXyzcmubPbn/d25fTZpAlS4kRymIUa +v/91Jt8t1VRBdXmneZ/SaQIHAwb8uUSQvLmLvcnRBsYJAmaUD3LontwhtVlrFXax +Ae0Pn/xvxtZbv9JNzQeQlm5tHoWjAFN4TLHYtqBpnvEhVaeyrWJYUxtXZR/Xd3kS ++pXjXZtAIW9ppMJI2yj/QzHxYykHOZ5v+Q== +-----END PGP MESSAGE----- +`); + + try { + const passphrase = 'password'; + const algo = openpgp.enums.symmetric.aes128; + + const skesk = new openpgp.SymEncryptedSessionKeyPacket(); + skesk.sessionKeyAlgorithm = algo; + const literal = new openpgp.LiteralDataPacket(0); + literal.setBytes(util.stringToUint8Array('Hello, world!'), openpgp.enums.literal.binary); + literal.filename = ''; + const pad = new openpgp.PaddingPacket(); + await pad.createPadding(14); + const enc = new openpgp.SymEncryptedIntegrityProtectedDataPacket(); + enc.version = 2; + enc.aeadAlgorithm = skesk.aeadAlgorithm = openpgp.enums.aead.gcm; + enc.packets = new openpgp.PacketList(); + enc.packets.push(literal); + enc.packets.push(pad); + const msg = new openpgp.PacketList(); + msg.push(skesk); + msg.push(enc); + + await skesk.encrypt(passphrase, openpgp.config); + + const key = skesk.sessionKey; + await enc.encrypt(algo, key, undefined, openpgp.config); + + const data = msg.write(); + expect(await stream.readToEnd(stream.clone(data))).to.deep.equal(packetBytes); + + const msg2 = new openpgp.PacketList(); + await msg2.read(data, allAllowedPackets); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(await stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aeadProtect = aeadProtectVal; + openpgp.config.aeadChunkSizeByte = aeadChunkSizeByteVal; + openpgp.config.s2kIterationCountByte = s2kIterationCountByteVal; + randomBytesStub.restore(); + } + }); + it('Secret key encryption/decryption test', async function() { const armored_msg = '-----BEGIN PGP MESSAGE-----\n' +