diff --git a/src/key.js b/src/key.js index 6587bd72..5f21d9f0 100644 --- a/src/key.js +++ b/src/key.js @@ -468,7 +468,7 @@ Key.prototype.getExpirationTime = async function() { if (this.primaryKey.version === 3) { return getExpirationTime(this.primaryKey); } - if (this.primaryKey.version === 4) { + if (this.primaryKey.version >= 4) { const primaryUser = await this.getPrimaryUser(null); const selfCert = primaryUser.selfCertification; const keyExpiry = getExpirationTime(this.primaryKey, selfCert); @@ -1383,7 +1383,7 @@ function getExpirationTime(keyPacket, signature) { expirationTime = keyPacket.created.getTime() + keyPacket.expirationTimeV3*24*3600*1000; } // check V4 expiration time - if (keyPacket.version === 4 && signature.keyNeverExpires === false) { + if (keyPacket.version >= 4 && signature.keyNeverExpires === false) { expirationTime = keyPacket.created.getTime() + signature.keyExpirationTime*1000; } return expirationTime ? new Date(expirationTime) : Infinity; diff --git a/src/packet/public_key.js b/src/packet/public_key.js index de2bb5fc..f76e16fa 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -18,6 +18,7 @@ /** * @requires type/keyid * @requires type/mpi + * @requires config * @requires crypto * @requires enums * @requires util @@ -25,6 +26,7 @@ import type_keyid from '../type/keyid'; import type_mpi from '../type/mpi'; +import config from '../config'; import crypto from '../crypto'; import enums from '../enums'; import util from '../util'; @@ -52,7 +54,7 @@ function PublicKey(date=new Date()) { * Packet version * @type {Integer} */ - this.version = 4; + this.version = config.aead_protect === 'draft04' ? 5 : 4; /** * Key creation date. * @type {Date} @@ -88,10 +90,10 @@ function PublicKey(date=new Date()) { */ PublicKey.prototype.read = function (bytes) { let pos = 0; - // A one-octet version number (3 or 4). + // A one-octet version number (3, 4 or 5). this.version = bytes[pos++]; - if (this.version === 3 || this.version === 4) { + if (this.version === 3 || this.version === 4 || this.version === 5) { // - A four-octet number denoting the time that the key was created. this.created = util.readDate(bytes.subarray(pos, pos + 4)); pos += 4; @@ -106,20 +108,25 @@ PublicKey.prototype.read = function (bytes) { // - A one-octet number denoting the public-key algorithm of this key. this.algorithm = enums.read(enums.publicKey, bytes[pos++]); const algo = enums.write(enums.publicKey, this.algorithm); + + if (this.version === 5) { + // - A four-octet scalar octet count for the following key material. + pos += 4; + } + + // - A series of values comprising the key material. This is + // algorithm-specific and described in section XXXX. const types = crypto.getPubKeyParamTypes(algo); this.params = crypto.constructParams(types); - const b = bytes.subarray(pos, bytes.length); - let p = 0; - - for (let i = 0; i < types.length && p < b.length; i++) { - p += this.params[i].read(b.subarray(p, b.length)); - if (p > b.length) { - throw new Error('Error reading MPI @:' + p); + for (let i = 0; i < types.length && pos < bytes.length; i++) { + pos += this.params[i].read(bytes.subarray(pos, bytes.length)); + if (pos > bytes.length) { + throw new Error('Error reading MPI @:' + pos); } } - return p + 6; + return pos; } throw new Error('Version ' + this.version + ' of the key packet is unsupported.'); }; @@ -143,14 +150,18 @@ PublicKey.prototype.write = function () { if (this.version === 3) { arr.push(util.writeNumber(this.expirationTimeV3, 2)); } - // Algorithm-specific params + // A one-octet number denoting the public-key algorithm of this key const algo = enums.write(enums.publicKey, this.algorithm); - const paramCount = crypto.getPubKeyParamTypes(algo).length; arr.push(new Uint8Array([algo])); - for (let i = 0; i < paramCount; i++) { - arr.push(this.params[i].write()); - } + const paramCount = crypto.getPubKeyParamTypes(algo).length; + const params = util.concatUint8Array(this.params.slice(0, paramCount).map(param => param.write())); + if (this.version === 5) { + // A four-octet scalar octet count for the following key material + arr.push(util.writeNumber(params.length, 4)); + } + // Algorithm-specific params + arr.push(params); return util.concatUint8Array(arr); }; @@ -178,7 +189,9 @@ PublicKey.prototype.getKeyId = function () { return this.keyid; } this.keyid = new type_keyid(); - if (this.version === 4) { + if (this.version === 5) { + this.keyid.read(util.hex_to_Uint8Array(this.getFingerprint()).subarray(0, 8)); + } else if (this.version === 4) { this.keyid.read(util.hex_to_Uint8Array(this.getFingerprint()).subarray(12, 20)); } else if (this.version === 3) { const arr = this.params[0].write(); @@ -195,13 +208,18 @@ PublicKey.prototype.getFingerprint = function () { if (this.fingerprint) { return this.fingerprint; } - let toHash = ''; - if (this.version === 4) { + let toHash; + if (this.version === 5) { + const bytes = this.writePublicKey(); + toHash = util.concatUint8Array([new Uint8Array([0x9A]), util.writeNumber(bytes.length, 4), bytes]); + this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha256(toHash)); + } else if (this.version === 4) { toHash = this.writeOld(); this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha1(toHash)); } else if (this.version === 3) { const algo = enums.write(enums.publicKey, this.algorithm); const paramCount = crypto.getPubKeyParamTypes(algo).length; + toHash = ''; for (let i = 0; i < paramCount; i++) { toHash += this.params[i].toString(); } diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 80135a98..f764ff80 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -78,15 +78,17 @@ function get_hash_fn(hash) { // Helper function function parse_cleartext_params(hash_algorithm, cleartext, algorithm) { - const hashlen = get_hash_len(hash_algorithm); - const hashfn = get_hash_fn(hash_algorithm); + if (hash_algorithm) { + const hashlen = get_hash_len(hash_algorithm); + const hashfn = get_hash_fn(hash_algorithm); - const hashtext = util.Uint8Array_to_str(cleartext.subarray(cleartext.length - hashlen, cleartext.length)); - cleartext = cleartext.subarray(0, cleartext.length - hashlen); - const hash = util.Uint8Array_to_str(hashfn(cleartext)); + const hashtext = util.Uint8Array_to_str(cleartext.subarray(cleartext.length - hashlen, cleartext.length)); + cleartext = cleartext.subarray(0, cleartext.length - hashlen); + const hash = util.Uint8Array_to_str(hashfn(cleartext)); - if (hash !== hashtext) { - return new Error("Incorrect key passphrase"); + if (hash !== hashtext) { + throw new Error("Incorrect key passphrase"); + } } const algo = enums.write(enums.publicKey, algorithm); @@ -115,9 +117,13 @@ function write_cleartext_params(hash_algorithm, algorithm, params) { const bytes = util.concatUint8Array(arr); - const hash = get_hash_fn(hash_algorithm)(bytes); + if (hash_algorithm) { + const hash = get_hash_fn(hash_algorithm)(bytes); - return util.concatUint8Array([bytes, hash]); + return util.concatUint8Array([bytes, hash]); + } + + return bytes; } @@ -125,7 +131,7 @@ function write_cleartext_params(hash_algorithm, algorithm, params) { /** * Internal parser for private keys as specified in - * {@link https://tools.ietf.org/html/rfc4880#section-5.5.3|RFC 4880 section 5.5.3} + * {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.5.3|RFC4880bis-04 section 5.5.3} * @param {String} bytes Input string to read the packet from */ SecretKey.prototype.read = function (bytes) { @@ -148,9 +154,6 @@ SecretKey.prototype.read = function (bytes) { // key data. These algorithm-specific fields are as described // below. const privParams = parse_cleartext_params('mod', bytes.subarray(1, bytes.length), this.algorithm); - if (privParams instanceof Error) { - throw privParams; - } this.params = this.params.concat(privParams); this.isDecrypted = true; } @@ -194,15 +197,29 @@ SecretKey.prototype.encrypt = async function (passphrase) { const s2k = new type_s2k(); s2k.salt = await crypto.random.getRandomBytes(8); const symmetric = 'aes256'; - const cleartext = write_cleartext_params('sha1', this.algorithm, this.params); + const hash = this.version === 5 ? null : 'sha1'; + const cleartext = write_cleartext_params(hash, this.algorithm, this.params); const key = produceEncryptionKey(s2k, passphrase, symmetric); const blockLen = crypto.cipher[symmetric].blockSize; const iv = await crypto.random.getRandomBytes(blockLen); - const arr = [new Uint8Array([254, enums.write(enums.symmetric, symmetric)])]; - arr.push(s2k.write()); - arr.push(iv); - arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv)); + let arr; + + if (this.version === 5) { + const aead = 'eax'; + const optionalFields = util.concatUint8Array([new Uint8Array([enums.write(enums.symmetric, symmetric), enums.write(enums.aead, aead)]), s2k.write(), iv]); + arr = [new Uint8Array([253, optionalFields.length])]; + arr.push(optionalFields); + const mode = crypto[aead]; + const encrypted = await mode.encrypt(symmetric, cleartext, key, iv.subarray(0, mode.ivLength), new Uint8Array()); + arr.push(util.writeNumber(encrypted.length, 4)); + arr.push(encrypted); + } else { + arr = [new Uint8Array([254, enums.write(enums.symmetric, symmetric)])]; + arr.push(s2k.write()); + arr.push(iv); + arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv)); + } this.encrypted = util.concatUint8Array(arr); return true; @@ -230,17 +247,31 @@ SecretKey.prototype.decrypt = async function (passphrase) { let i = 0; let symmetric; + let aead; let key; const s2k_usage = this.encrypted[i++]; - // - [Optional] If string-to-key usage octet was 255 or 254, a one- - // octet symmetric encryption algorithm. - if (s2k_usage === 255 || s2k_usage === 254) { + // - Only for a version 5 packet, a one-octet scalar octet count of + // the next 4 optional fields. + if (this.version === 5) { + i++; + } + + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // one-octet symmetric encryption algorithm. + if (s2k_usage === 255 || s2k_usage === 254 || s2k_usage === 253) { symmetric = this.encrypted[i++]; symmetric = enums.read(enums.symmetric, symmetric); - // - [Optional] If string-to-key usage octet was 255 or 254, a + // - [Optional] If string-to-key usage octet was 253, a one-octet + // AEAD algorithm. + if (s2k_usage === 253) { + aead = this.encrypted[i++]; + aead = enums.read(enums.aead, aead); + } + + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a // string-to-key specifier. The length of the string-to-key // specifier is implied by its type, as described above. const s2k = new type_s2k(); @@ -263,16 +294,32 @@ SecretKey.prototype.decrypt = async function (passphrase) { i += iv.length; + // - Only for a version 5 packet, a four-octet scalar octet count for + // the following key material. + if (this.version === 5) { + i += 4; + } + const ciphertext = this.encrypted.subarray(i, this.encrypted.length); - const cleartext = crypto.cfb.normalDecrypt(symmetric, key, ciphertext, iv); - const hash = s2k_usage === 254 ? - 'sha1' : + let cleartext; + if (aead) { + const mode = crypto[aead]; + try { + cleartext = await mode.decrypt(symmetric, ciphertext, key, iv.subarray(0, mode.ivLength), new Uint8Array()); + } catch(err) { + if (err.message.startsWith('Authentication tag mismatch')) { + throw new Error('Incorrect key passphrase: ' + err.message); + } + } + } else { + cleartext = crypto.cfb.normalDecrypt(symmetric, key, ciphertext, iv); + } + const hash = + s2k_usage === 253 ? null : + s2k_usage === 254 ? 'sha1' : 'mod'; const privParams = parse_cleartext_params(hash, cleartext, this.algorithm); - if (privParams instanceof Error) { - throw privParams; - } this.params = this.params.concat(privParams); this.isDecrypted = true; this.encrypted = null; diff --git a/test/general/key.js b/test/general/key.js index 6839f963..3248edcd 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -6,6 +6,23 @@ chai.use(require('chai-as-promised')); const { expect } = chai; describe('Key', function() { + describe('V4', tests); + + describe('V5', function() { + let aead_protectVal; + beforeEach(function() { + aead_protectVal = openpgp.config.aead_protect; + openpgp.config.aead_protect = 'draft04'; + }); + afterEach(function() { + openpgp.config.aead_protect = aead_protectVal; + }); + + tests(); + }); +}); + +function tests() { const twoKeys = ['-----BEGIN PGP PUBLIC KEY BLOCK-----', 'Version: GnuPG v2.0.19 (GNU/Linux)', @@ -893,6 +910,21 @@ p92yZgB3r2+f6/GIe2+7 done(); }); + it('Parsing V5 public key packet', function() { + // Manually modified from https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b2092/back.mkd#sample-eddsa-key + let packetBytes = openpgp.util.hex_to_Uint8Array(` + 98 37 05 53 f3 5f 0b 16 00 00 00 2d 09 2b 06 01 04 01 da 47 + 0f 01 01 07 40 3f 09 89 94 bd d9 16 ed 40 53 19 + 79 34 e4 a8 7c 80 73 3a 12 80 d6 2f 80 10 99 2e + 43 ee 3b 24 06 + `.replace(/\s+/g, '')); + + let packetlist = new openpgp.packet.List(); + packetlist.read(packetBytes); + let key = packetlist[0]; + expect(key).to.exist; + }); + it('Testing key ID and fingerprint for V3 and V4 keys', function(done) { const pubKeysV4 = openpgp.key.readArmored(twoKeys); expect(pubKeysV4).to.exist; @@ -1574,4 +1606,4 @@ p92yZgB3r2+f6/GIe2+7 expect(error.message).to.equal('Error encrypting message: Could not find valid key packet for encryption in key ' + key.primaryKey.getKeyId().toHex()); }); }); -}); +} diff --git a/test/general/packet.js b/test/general/packet.js index 2aba6164..3759a6fa 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -637,6 +637,38 @@ describe("Packet", function() { }); }); + it('Writing and encryption of a secret key packet. (draft04)', function() { + let aead_protectVal = openpgp.config.aead_protect; + openpgp.config.aead_protect = 'draft04'; + + const key = new openpgp.packet.List(); + key.push(new openpgp.packet.SecretKey()); + + const rsa = openpgp.crypto.publicKey.rsa; + const keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys + + return rsa.generate(keySize, "10001").then(async function(mpiGen) { + let mpi = [mpiGen.n, mpiGen.e, mpiGen.d, mpiGen.p, mpiGen.q, mpiGen.u]; + mpi = mpi.map(function(k) { + return new openpgp.MPI(k); + }); + + key[0].params = mpi; + key[0].algorithm = "rsa_sign"; + await key[0].encrypt('hello'); + + const raw = key.write(); + + const key2 = new openpgp.packet.List(); + key2.read(raw); + await key2[0].decrypt('hello'); + + expect(key[0].params.toString()).to.equal(key2[0].params.toString()); + }).finally(function() { + openpgp.config.aead_protect = aead_protectVal; + }); + }); + it('Writing and verification of a signature packet.', function() { const key = new openpgp.packet.SecretKey();