Implement version 5 Secret-Key Packet Format

This commit is contained in:
Daniel Huigens 2018-04-04 19:40:46 +02:00
parent 5d43b44e50
commit c2f898279b
5 changed files with 179 additions and 50 deletions

View File

@ -468,7 +468,7 @@ Key.prototype.getExpirationTime = async function() {
if (this.primaryKey.version === 3) { if (this.primaryKey.version === 3) {
return getExpirationTime(this.primaryKey); return getExpirationTime(this.primaryKey);
} }
if (this.primaryKey.version === 4) { if (this.primaryKey.version >= 4) {
const primaryUser = await this.getPrimaryUser(null); const primaryUser = await this.getPrimaryUser(null);
const selfCert = primaryUser.selfCertification; const selfCert = primaryUser.selfCertification;
const keyExpiry = getExpirationTime(this.primaryKey, selfCert); const keyExpiry = getExpirationTime(this.primaryKey, selfCert);
@ -1383,7 +1383,7 @@ function getExpirationTime(keyPacket, signature) {
expirationTime = keyPacket.created.getTime() + keyPacket.expirationTimeV3*24*3600*1000; expirationTime = keyPacket.created.getTime() + keyPacket.expirationTimeV3*24*3600*1000;
} }
// check V4 expiration time // 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; expirationTime = keyPacket.created.getTime() + signature.keyExpirationTime*1000;
} }
return expirationTime ? new Date(expirationTime) : Infinity; return expirationTime ? new Date(expirationTime) : Infinity;

View File

@ -18,6 +18,7 @@
/** /**
* @requires type/keyid * @requires type/keyid
* @requires type/mpi * @requires type/mpi
* @requires config
* @requires crypto * @requires crypto
* @requires enums * @requires enums
* @requires util * @requires util
@ -25,6 +26,7 @@
import type_keyid from '../type/keyid'; import type_keyid from '../type/keyid';
import type_mpi from '../type/mpi'; import type_mpi from '../type/mpi';
import config from '../config';
import crypto from '../crypto'; import crypto from '../crypto';
import enums from '../enums'; import enums from '../enums';
import util from '../util'; import util from '../util';
@ -52,7 +54,7 @@ function PublicKey(date=new Date()) {
* Packet version * Packet version
* @type {Integer} * @type {Integer}
*/ */
this.version = 4; this.version = config.aead_protect === 'draft04' ? 5 : 4;
/** /**
* Key creation date. * Key creation date.
* @type {Date} * @type {Date}
@ -88,10 +90,10 @@ function PublicKey(date=new Date()) {
*/ */
PublicKey.prototype.read = function (bytes) { PublicKey.prototype.read = function (bytes) {
let pos = 0; let pos = 0;
// A one-octet version number (3 or 4). // A one-octet version number (3, 4 or 5).
this.version = bytes[pos++]; 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. // - A four-octet number denoting the time that the key was created.
this.created = util.readDate(bytes.subarray(pos, pos + 4)); this.created = util.readDate(bytes.subarray(pos, pos + 4));
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. // - A one-octet number denoting the public-key algorithm of this key.
this.algorithm = enums.read(enums.publicKey, bytes[pos++]); this.algorithm = enums.read(enums.publicKey, bytes[pos++]);
const algo = enums.write(enums.publicKey, this.algorithm); 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); const types = crypto.getPubKeyParamTypes(algo);
this.params = crypto.constructParams(types); this.params = crypto.constructParams(types);
const b = bytes.subarray(pos, bytes.length); for (let i = 0; i < types.length && pos < bytes.length; i++) {
let p = 0; pos += this.params[i].read(bytes.subarray(pos, bytes.length));
if (pos > bytes.length) {
for (let i = 0; i < types.length && p < b.length; i++) { throw new Error('Error reading MPI @:' + pos);
p += this.params[i].read(b.subarray(p, b.length));
if (p > b.length) {
throw new Error('Error reading MPI @:' + p);
} }
} }
return p + 6; return pos;
} }
throw new Error('Version ' + this.version + ' of the key packet is unsupported.'); throw new Error('Version ' + this.version + ' of the key packet is unsupported.');
}; };
@ -143,14 +150,18 @@ PublicKey.prototype.write = function () {
if (this.version === 3) { if (this.version === 3) {
arr.push(util.writeNumber(this.expirationTimeV3, 2)); 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 algo = enums.write(enums.publicKey, this.algorithm);
const paramCount = crypto.getPubKeyParamTypes(algo).length;
arr.push(new Uint8Array([algo])); 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); return util.concatUint8Array(arr);
}; };
@ -178,7 +189,9 @@ PublicKey.prototype.getKeyId = function () {
return this.keyid; return this.keyid;
} }
this.keyid = new type_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)); this.keyid.read(util.hex_to_Uint8Array(this.getFingerprint()).subarray(12, 20));
} else if (this.version === 3) { } else if (this.version === 3) {
const arr = this.params[0].write(); const arr = this.params[0].write();
@ -195,13 +208,18 @@ PublicKey.prototype.getFingerprint = function () {
if (this.fingerprint) { if (this.fingerprint) {
return this.fingerprint; return this.fingerprint;
} }
let toHash = ''; let toHash;
if (this.version === 4) { 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(); toHash = this.writeOld();
this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha1(toHash)); this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha1(toHash));
} else if (this.version === 3) { } else if (this.version === 3) {
const algo = enums.write(enums.publicKey, this.algorithm); const algo = enums.write(enums.publicKey, this.algorithm);
const paramCount = crypto.getPubKeyParamTypes(algo).length; const paramCount = crypto.getPubKeyParamTypes(algo).length;
toHash = '';
for (let i = 0; i < paramCount; i++) { for (let i = 0; i < paramCount; i++) {
toHash += this.params[i].toString(); toHash += this.params[i].toString();
} }

View File

@ -78,6 +78,7 @@ function get_hash_fn(hash) {
// Helper function // Helper function
function parse_cleartext_params(hash_algorithm, cleartext, algorithm) { function parse_cleartext_params(hash_algorithm, cleartext, algorithm) {
if (hash_algorithm) {
const hashlen = get_hash_len(hash_algorithm); const hashlen = get_hash_len(hash_algorithm);
const hashfn = get_hash_fn(hash_algorithm); const hashfn = get_hash_fn(hash_algorithm);
@ -86,7 +87,8 @@ function parse_cleartext_params(hash_algorithm, cleartext, algorithm) {
const hash = util.Uint8Array_to_str(hashfn(cleartext)); const hash = util.Uint8Array_to_str(hashfn(cleartext));
if (hash !== hashtext) { if (hash !== hashtext) {
return new Error("Incorrect key passphrase"); throw new Error("Incorrect key passphrase");
}
} }
const algo = enums.write(enums.publicKey, algorithm); const algo = enums.write(enums.publicKey, algorithm);
@ -115,17 +117,21 @@ function write_cleartext_params(hash_algorithm, algorithm, params) {
const bytes = util.concatUint8Array(arr); const bytes = util.concatUint8Array(arr);
if (hash_algorithm) {
const hash = get_hash_fn(hash_algorithm)(bytes); const hash = get_hash_fn(hash_algorithm)(bytes);
return util.concatUint8Array([bytes, hash]); return util.concatUint8Array([bytes, hash]);
} }
return bytes;
}
// 5.5.3. Secret-Key Packet Formats // 5.5.3. Secret-Key Packet Formats
/** /**
* Internal parser for private keys as specified in * 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 * @param {String} bytes Input string to read the packet from
*/ */
SecretKey.prototype.read = function (bytes) { SecretKey.prototype.read = function (bytes) {
@ -148,9 +154,6 @@ SecretKey.prototype.read = function (bytes) {
// key data. These algorithm-specific fields are as described // key data. These algorithm-specific fields are as described
// below. // below.
const privParams = parse_cleartext_params('mod', bytes.subarray(1, bytes.length), this.algorithm); 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.params = this.params.concat(privParams);
this.isDecrypted = true; this.isDecrypted = true;
} }
@ -194,15 +197,29 @@ SecretKey.prototype.encrypt = async function (passphrase) {
const s2k = new type_s2k(); const s2k = new type_s2k();
s2k.salt = await crypto.random.getRandomBytes(8); s2k.salt = await crypto.random.getRandomBytes(8);
const symmetric = 'aes256'; 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 key = produceEncryptionKey(s2k, passphrase, symmetric);
const blockLen = crypto.cipher[symmetric].blockSize; const blockLen = crypto.cipher[symmetric].blockSize;
const iv = await crypto.random.getRandomBytes(blockLen); const iv = await crypto.random.getRandomBytes(blockLen);
const arr = [new Uint8Array([254, enums.write(enums.symmetric, symmetric)])]; 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(s2k.write());
arr.push(iv); arr.push(iv);
arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv)); arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv));
}
this.encrypted = util.concatUint8Array(arr); this.encrypted = util.concatUint8Array(arr);
return true; return true;
@ -230,17 +247,31 @@ SecretKey.prototype.decrypt = async function (passphrase) {
let i = 0; let i = 0;
let symmetric; let symmetric;
let aead;
let key; let key;
const s2k_usage = this.encrypted[i++]; const s2k_usage = this.encrypted[i++];
// - [Optional] If string-to-key usage octet was 255 or 254, a one- // - Only for a version 5 packet, a one-octet scalar octet count of
// octet symmetric encryption algorithm. // the next 4 optional fields.
if (s2k_usage === 255 || s2k_usage === 254) { 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 = this.encrypted[i++];
symmetric = enums.read(enums.symmetric, symmetric); 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 // string-to-key specifier. The length of the string-to-key
// specifier is implied by its type, as described above. // specifier is implied by its type, as described above.
const s2k = new type_s2k(); const s2k = new type_s2k();
@ -263,16 +294,32 @@ SecretKey.prototype.decrypt = async function (passphrase) {
i += iv.length; 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 ciphertext = this.encrypted.subarray(i, this.encrypted.length);
const cleartext = crypto.cfb.normalDecrypt(symmetric, key, ciphertext, iv); let cleartext;
const hash = s2k_usage === 254 ? if (aead) {
'sha1' : 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'; 'mod';
const privParams = parse_cleartext_params(hash, cleartext, this.algorithm); const privParams = parse_cleartext_params(hash, cleartext, this.algorithm);
if (privParams instanceof Error) {
throw privParams;
}
this.params = this.params.concat(privParams); this.params = this.params.concat(privParams);
this.isDecrypted = true; this.isDecrypted = true;
this.encrypted = null; this.encrypted = null;

View File

@ -6,6 +6,23 @@ chai.use(require('chai-as-promised'));
const { expect } = chai; const { expect } = chai;
describe('Key', function() { 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 = const twoKeys =
['-----BEGIN PGP PUBLIC KEY BLOCK-----', ['-----BEGIN PGP PUBLIC KEY BLOCK-----',
'Version: GnuPG v2.0.19 (GNU/Linux)', 'Version: GnuPG v2.0.19 (GNU/Linux)',
@ -893,6 +910,21 @@ p92yZgB3r2+f6/GIe2+7
done(); 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) { it('Testing key ID and fingerprint for V3 and V4 keys', function(done) {
const pubKeysV4 = openpgp.key.readArmored(twoKeys); const pubKeysV4 = openpgp.key.readArmored(twoKeys);
expect(pubKeysV4).to.exist; 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()); expect(error.message).to.equal('Error encrypting message: Could not find valid key packet for encryption in key ' + key.primaryKey.getKeyId().toHex());
}); });
}); });
}); }

View File

@ -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() { it('Writing and verification of a signature packet.', function() {
const key = new openpgp.packet.SecretKey(); const key = new openpgp.packet.SecretKey();