diff --git a/src/config/config.js b/src/config/config.js index f1302655..1373430d 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -59,6 +59,13 @@ export default { * @property {Integer} aead_chunk_size_byte */ aead_chunk_size_byte: 46, + /** + * {@link https://tools.ietf.org/html/rfc4880#section-3.7.1.3|RFC4880 3.7.1.3}: + * Iteration Count Byte for S2K (String to Key) + * @memberof module:config + * @property {Integer} s2k_iteration_count_byte + */ + s2k_iteration_count_byte: 96, /** Use integrity protection for symmetric encryption * @memberof module:config * @property {Boolean} integrity_protect diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index cdd0a136..eea69222 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -17,12 +17,14 @@ /** * @requires type/s2k + * @requires config * @requires crypto * @requires enums * @requires util */ import type_s2k from '../type/s2k'; +import config from '../config'; import crypto from '../crypto'; import enums from '../enums'; import util from '../util'; @@ -47,12 +49,14 @@ import util from '../util'; */ function SymEncryptedSessionKey() { this.tag = enums.packet.symEncryptedSessionKey; - this.version = 4; + this.version = config.aead_protect === 'draft04' ? 5 : 4; this.sessionKey = null; this.sessionKeyEncryptionAlgorithm = null; this.sessionKeyAlgorithm = 'aes256'; + this.aeadAlgorithm = 'eax'; this.encrypted = null; this.s2k = null; + this.iv = null; } /** @@ -66,22 +70,35 @@ function SymEncryptedSessionKey() { * @returns {module:packet.SymEncryptedSessionKey} Object representation */ SymEncryptedSessionKey.prototype.read = function(bytes) { + let offset = 0; + // A one-octet version number. The only currently defined version is 4. - [this.version] = bytes; + this.version = bytes[offset++]; // A one-octet number describing the symmetric algorithm used. - const algo = enums.read(enums.symmetric, bytes[1]); + const algo = enums.read(enums.symmetric, bytes[offset++]); + + if (this.version === 5) { + // A one-octet AEAD algorithm. + this.aeadAlgorithm = enums.read(enums.aead, bytes[offset++]); + } // A string-to-key (S2K) specifier, length as defined above. this.s2k = new type_s2k(); - const s2klength = this.s2k.read(bytes.subarray(2, bytes.length)); + offset += this.s2k.read(bytes.subarray(offset, bytes.length)); - // Optionally, the encrypted session key itself, which is decrypted - // with the string-to-key object. - const done = s2klength + 2; + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; - if (done < bytes.length) { - this.encrypted = bytes.subarray(done, bytes.length); + // A starting initialization vector of size specified by the AEAD + // algorithm. + this.iv = bytes.subarray(offset, offset += mode.ivLength); + } + + // 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) { + this.encrypted = bytes.subarray(offset, bytes.length); this.sessionKeyEncryptionAlgorithm = algo; } else { this.sessionKeyAlgorithm = algo; @@ -93,11 +110,18 @@ SymEncryptedSessionKey.prototype.write = function() { this.sessionKeyAlgorithm : this.sessionKeyEncryptionAlgorithm; - let bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo)]), this.s2k.write()]); + let bytes; - if (this.encrypted !== null) { - bytes = util.concatUint8Array([bytes, this.encrypted]); + if (this.version === 5) { + bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo), enums.write(enums.aead, this.aeadAlgorithm)]), this.s2k.write(), this.iv, this.encrypted]); + } else { + bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo)]), this.s2k.write()]); + + if (this.encrypted !== null) { + bytes = util.concatUint8Array([bytes, this.encrypted]); + } } + return bytes; }; @@ -115,14 +139,19 @@ SymEncryptedSessionKey.prototype.decrypt = async function(passphrase) { const length = crypto.cipher[algo].keySize; const key = this.s2k.produce_key(passphrase, length); - if (this.encrypted === null) { - this.sessionKey = key; - } else { + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; + const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); + this.sessionKey = await mode.decrypt(algo, this.encrypted, key, this.iv, adata); + } else if (this.encrypted !== null) { const decrypted = crypto.cfb.normalDecrypt(algo, key, this.encrypted, null); this.sessionKeyAlgorithm = enums.read(enums.symmetric, decrypted[0]); this.sessionKey = decrypted.subarray(1, decrypted.length); + } else { + this.sessionKey = key; } + return true; }; @@ -145,14 +174,21 @@ SymEncryptedSessionKey.prototype.encrypt = async function(passphrase) { const length = crypto.cipher[algo].keySize; const key = this.s2k.produce_key(passphrase, length); - const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); - if (this.sessionKey === null) { this.sessionKey = await crypto.generateSessionKey(this.sessionKeyAlgorithm); } - const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); - this.encrypted = crypto.cfb.normalEncrypt(algo, key, private_key, null); + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; + this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV + const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); + this.encrypted = await mode.encrypt(algo, this.sessionKey, key, this.iv, adata); + } else { + const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); + const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); + this.encrypted = crypto.cfb.normalEncrypt(algo, key, private_key, null); + } + return true; }; diff --git a/src/type/s2k.js b/src/type/s2k.js index 340243e0..223f2f41 100644 --- a/src/type/s2k.js +++ b/src/type/s2k.js @@ -24,15 +24,17 @@ * places, currently: to encrypt the secret part of private keys in the * private keyring, and to convert passphrases to encryption keys for * symmetrically encrypted messages. + * @requires config * @requires crypto * @requires enums * @requires util * @module type/s2k */ +import config from '../config'; +import crypto from '../crypto'; import enums from '../enums.js'; import util from '../util.js'; -import crypto from '../crypto'; /** * @constructor @@ -42,7 +44,8 @@ function S2K() { this.algorithm = 'sha256'; /** @type {module:enums.s2k} */ this.type = 'iterated'; - this.c = 96; + /** @type {Integer} */ + this.c = config.s2k_iteration_count_byte; /** Eight bytes of salt in a binary string. * @type {String} */ diff --git a/test/general/packet.js b/test/general/packet.js index 4913370d..2aba6164 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -415,6 +415,119 @@ describe("Packet", function() { expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); }); + it('Sym. encrypted session key reading/writing (draft04)', async function() { + let aead_protectVal = openpgp.config.aead_protect; + openpgp.config.aead_protect = 'draft04'; + + try { + const passphrase = 'hello'; + const algo = 'aes256'; + + const literal = new openpgp.packet.Literal(); + const key_enc = new openpgp.packet.SymEncryptedSessionKey(); + const enc = new openpgp.packet.SymEncryptedAEADProtected(); + const msg = new openpgp.packet.List(); + + msg.push(key_enc); + msg.push(enc); + + key_enc.sessionKeyAlgorithm = algo; + await key_enc.encrypt(passphrase); + + const key = key_enc.sessionKey; + + literal.setText('Hello world!'); + enc.packets.push(literal); + await enc.encrypt(algo, key); + + const msg2 = new openpgp.packet.List(); + msg2.read(msg.write()); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aead_protect = aead_protectVal; + } + }); + + it('Sym. encrypted session key reading/writing test vector (draft04)', async function() { + // From https://gitlab.com/openpgp-wg/rfc4880bis/commit/00b20923e6233fb6ff1666ecd5acfefceb32907d + + let aead_protectVal = openpgp.config.aead_protect; + let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; + let s2k_iteration_count_byteVal = openpgp.config.s2k_iteration_count_byte; + openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_chunk_size_byte = 14; + openpgp.config.s2k_iteration_count_byte = 0x90; + + let salt = openpgp.util.hex_to_Uint8Array(`cd5a9f70fbe0bc65`); + let sessionKey = openpgp.util.hex_to_Uint8Array(`86 f1 ef b8 69 52 32 9f 24 ac d3 bf d0 e5 34 6d`.replace(/\s+/g, '')); + let sessionIV = openpgp.util.hex_to_Uint8Array(`bc 66 9e 34 e5 00 dc ae dc 5b 32 aa 2d ab 02 35`.replace(/\s+/g, '')); + let dataIV = openpgp.util.hex_to_Uint8Array(`b7 32 37 9f 73 c4 92 8d e2 5f ac fe 65 17 ec 10`.replace(/\s+/g, '')); + + let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes'); + randomBytesStub.onCall(0).returns(resolves(salt)); + randomBytesStub.onCall(1).returns(resolves(sessionKey)); + randomBytesStub.onCall(2).returns(resolves(sessionIV)); + randomBytesStub.onCall(3).returns(resolves(dataIV)); + + let packetBytes = openpgp.util.hex_to_Uint8Array(` + c3 3e 05 07 01 03 08 cd 5a 9f 70 fb e0 bc 65 90 + bc 66 9e 34 e5 00 dc ae dc 5b 32 aa 2d ab 02 35 + 9d ee 19 d0 7c 34 46 c4 31 2a 34 ae 19 67 a2 fb + 7e 92 8e a5 b4 fa 80 12 bd 45 6d 17 38 c6 3c 36 + + d4 4a 01 07 01 0e b7 32 37 9f 73 c4 92 8d e2 5f + ac fe 65 17 ec 10 5d c1 1a 81 dc 0c b8 a2 f6 f3 + d9 00 16 38 4a 56 fc 82 1a e1 1a e8 db cb 49 86 + 26 55 de a8 8d 06 a8 14 86 80 1b 0f f3 87 bd 2e + ab 01 3d e1 25 95 86 90 6e ab 24 76 + `.replace(/\s+/g, '')); + + try { + const passphrase = 'password'; + const algo = 'aes128'; + + const literal = new openpgp.packet.Literal(0); + const key_enc = new openpgp.packet.SymEncryptedSessionKey(); + const enc = new openpgp.packet.SymEncryptedAEADProtected(); + const msg = new openpgp.packet.List(); + + msg.push(key_enc); + msg.push(enc); + + key_enc.sessionKeyAlgorithm = algo; + await key_enc.encrypt(passphrase); + + const key = key_enc.sessionKey; + + literal.setBytes(openpgp.util.str_to_Uint8Array('Hello, world!\n'), openpgp.enums.literal.binary); + literal.filename = ''; + enc.packets.push(literal); + await enc.encrypt(algo, key); + + const data = msg.write(); + expect(data).to.deep.equal(packetBytes); + + const msg2 = new openpgp.packet.List(); + msg2.read(data); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal; + openpgp.config.s2k_iteration_count_byte = s2k_iteration_count_byteVal; + randomBytesStub.restore(); + } + }); + it('Secret key encryption/decryption test', async function() { const armored_msg = '-----BEGIN PGP MESSAGE-----\n' +