diff --git a/src/enums.js b/src/enums.js index aa49c972..8b98ad17 100644 --- a/src/enums.js +++ b/src/enums.js @@ -223,7 +223,8 @@ export default { userAttribute: 17, symEncryptedIntegrityProtectedData: 18, modificationDetectionCode: 19, - aeadEncryptedData: 20 // see IETF draft: https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.1 + aeadEncryptedData: 20, // see IETF draft: https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.1 + padding: 21 }, /** Data types in the literal packet diff --git a/src/packet/all_packets.js b/src/packet/all_packets.js index 6362c3af..1c3fd978 100644 --- a/src/packet/all_packets.js +++ b/src/packet/all_packets.js @@ -21,3 +21,4 @@ export { default as UserIDPacket } from './userid'; export { default as SecretSubkeyPacket } from './secret_subkey'; export { default as SignaturePacket } from './signature'; export { default as TrustPacket } from './trust'; +export { default as PaddingPacket } from './padding'; diff --git a/src/packet/packetlist.js b/src/packet/packetlist.js index b0fea7f4..6207ef99 100644 --- a/src/packet/packetlist.js +++ b/src/packet/packetlist.js @@ -74,10 +74,11 @@ class PacketList extends Array { await writer.ready; const done = await readPackets(readable, async parsed => { try { - if (parsed.tag === enums.packet.marker || parsed.tag === enums.packet.trust) { + if (parsed.tag === enums.packet.marker || parsed.tag === enums.packet.trust || parsed.tag === enums.packet.padding) { // According to the spec, these packet types should be ignored and not cause parsing errors, even if not esplicitly allowed: // - Marker packets MUST be ignored when received: https://github.com/openpgpjs/openpgpjs/issues/1145 // - Trust packets SHOULD be ignored outside of keyrings (unsupported): https://datatracker.ietf.org/doc/html/rfc4880#section-5.10 + // - [Padding Packets] MUST be ignored when received: https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-padding-packet-tag-21 return; } const packet = newPacketFromTag(parsed.tag, allowedPackets); diff --git a/src/packet/padding.js b/src/packet/padding.js new file mode 100644 index 00000000..b60e1e4b --- /dev/null +++ b/src/packet/padding.js @@ -0,0 +1,63 @@ +// OpenPGP.js - An OpenPGP implementation in javascript +// Copyright (C) 2022 Proton AG +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 3.0 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import crypto from '../crypto'; +import enums from '../enums'; + +/** + * Implementation of the Padding Packet + * + * {@link https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#name-padding-packet-tag-21}: + * Padding Packet + */ +class PaddingPacket { + static get tag() { + return enums.packet.padding; + } + + constructor() { + this.padding = null; + } + + /** + * Read a padding packet + * @param {Uint8Array | ReadableStream} bytes + */ + read(bytes) { + // Padding packets are ignored, so this function is never called. + } + + /** + * Write the padding packet + * @returns {Uint8Array} The padding packet. + */ + write() { + return this.padding; + } + + /** + * Create random padding. + * @param {Number} length - The length of padding to be generated. + * @throws {Error} if padding generation was not successful + * @async + */ + async createPadding(length) { + this.padding = await crypto.random.getRandomBytes(length); + } +} + +export default PaddingPacket; diff --git a/test/general/packet.js b/test/general/packet.js index 8d77cdfe..329e13c7 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -280,6 +280,168 @@ export default () => describe('Packet', function() { } }); + it('Sym. encrypted AEAD protected packet 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 packetBytes = util.hexToUint8Array(` + d2 69 02 07 01 06 + 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 + 4a 3d 22 6e d6 af cb 9c a9 ac 12 2c 14 70 e1 1c + 63 d4 c0 ab 24 1c 6a 93 8a d4 8b f9 9a 5a 99 b9 + 0b ba 83 25 de + 61 04 75 40 25 8a b7 95 9a 95 ad 05 1d da 96 eb + 15 43 1d fe f5 f5 e2 25 5c a7 82 61 54 6e 33 9a + `.replace(/\s+/g, '')); + + 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('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 key = util.hexToUint8Array('38 81 ba fe 98 54 12 45 9b 86 c3 6f 98 cb 9a 5e'.replace(/\s+/g, '')); + const algo = openpgp.enums.symmetric.aes128; + + const randomBytesStub = sinon.stub(nodeCrypto, 'randomBytes'); + randomBytesStub.withArgs(14).returns(padding); + randomBytesStub.withArgs(32).returns(salt); + + 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 = openpgp.enums.aead.eax; + enc.packets = new openpgp.PacketList(); + enc.packets.push(literal); + enc.packets.push(pad); + const msg = new openpgp.PacketList(); + msg.push(enc); + + const msg2 = new openpgp.PacketList(); + + try { + await enc.encrypt(algo, key, { ...openpgp.config, aeadChunkSizeByte: 6 }); + const data = msg.write(); + expect(await stream.readToEnd(stream.clone(data))).to.deep.equal(packetBytes); + await msg2.read(data, allAllowedPackets); + await msg2[0].decrypt(algo, key); + expect(await stream.readToEnd(msg2[0].packets[0].data)).to.deep.equal(literal.data); + } finally { + randomBytesStub.restore(); + } + }); + + it('Sym. encrypted AEAD protected packet test vector (SEIPDv2 - OCB)', 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 packetBytes = util.hexToUint8Array(` + d2 69 02 07 02 06 + 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 + ff 9f d3 85 62 75 80 35 bc 49 75 4c e1 bf 3f ff + a7 da d0 a3 b8 10 4f 51 33 cf 42 a4 10 0a 83 ee + f4 ca 1b 48 01 + a8 84 6b f4 2b cd a7 c8 ce 9d 65 e2 12 f3 01 cb + cd 98 fd ca de 69 4a 87 7a d4 24 73 23 f6 e8 57 + `.replace(/\s+/g, '')); + + 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('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 key = util.hexToUint8Array('28 e7 9a b8 23 97 d3 c6 3d e2 4a c2 17 d7 b7 91'.replace(/\s+/g, '')); + const algo = openpgp.enums.symmetric.aes128; + + const randomBytesStub = sinon.stub(nodeCrypto, 'randomBytes'); + randomBytesStub.withArgs(14).returns(padding); + randomBytesStub.withArgs(32).returns(salt); + + 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 = openpgp.enums.aead.ocb; + enc.packets = new openpgp.PacketList(); + enc.packets.push(literal); + enc.packets.push(pad); + const msg = new openpgp.PacketList(); + msg.push(enc); + + const msg2 = new openpgp.PacketList(); + + try { + await enc.encrypt(algo, key, { ...openpgp.config, aeadChunkSizeByte: 6 }); + const data = msg.write(); + expect(await stream.readToEnd(stream.clone(data))).to.deep.equal(packetBytes); + await msg2.read(data, allAllowedPackets); + await msg2[0].decrypt(algo, key); + expect(await stream.readToEnd(msg2[0].packets[0].data)).to.deep.equal(literal.data); + } finally { + randomBytesStub.restore(); + } + }); + + it('Sym. encrypted AEAD protected packet test vector (SEIPDv2 - GCM)', 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 packetBytes = util.hexToUint8Array(` + d2 69 02 07 03 06 + 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 + fc 6f c6 d6 5b bf d2 4d cd 07 90 96 6e 6d 1e 85 + a3 00 53 78 4c b1 d8 b6 a0 69 9e f1 21 55 a7 b2 + ad 62 58 53 1b + 57 65 1f d7 77 79 12 fa 95 e3 5d 9b 40 21 6f 69 + a4 c2 48 db 28 ff 43 31 f1 63 29 07 39 9e 6f f9 + `.replace(/\s+/g, '')); + + 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('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 key = util.hexToUint8Array('19 36 fc 85 68 98 02 74 bb 90 0d 83 19 36 0c 77'.replace(/\s+/g, '')); + const algo = openpgp.enums.symmetric.aes128; + + const randomBytesStub = sinon.stub(nodeCrypto, 'randomBytes'); + randomBytesStub.withArgs(14).returns(padding); + randomBytesStub.withArgs(32).returns(salt); + + 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 = openpgp.enums.aead.gcm; + enc.packets = new openpgp.PacketList(); + enc.packets.push(literal); + enc.packets.push(pad); + const msg = new openpgp.PacketList(); + msg.push(enc); + + const msg2 = new openpgp.PacketList(); + + try { + await enc.encrypt(algo, key, { ...openpgp.config, aeadChunkSizeByte: 6 }); + const data = msg.write(); + expect(await stream.readToEnd(stream.clone(data))).to.deep.equal(packetBytes); + await msg2.read(data, allAllowedPackets); + await msg2[0].decrypt(algo, key); + expect(await stream.readToEnd(msg2[0].packets[0].data)).to.deep.equal(literal.data); + } finally { + randomBytesStub.restore(); + } + }); + it('Sym. encrypted session key with a compressed packet', async function() { const msg = '-----BEGIN PGP MESSAGE-----\n' +