From 6c2fec34501ef15fd6724f764c63b1a9abe09e66 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 26 Apr 2018 12:30:45 +0200 Subject: [PATCH 1/4] Parse user IDs Also, support comments when creating user IDs --- Gruntfile.js | 4 +- package.json | 1 + src/key.js | 2 +- src/openpgp.js | 34 +-------------- src/packet/userid.js | 27 +++++++++++- src/util.js | 25 +++++++++++ test/general/ecc_nist.js | 10 ++++- test/general/key.js | 10 ++++- test/general/openpgp.js | 90 +++++++++++++++++++++++----------------- 9 files changed, 125 insertions(+), 78 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index af6a0ba0..93ea0cee 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -29,8 +29,8 @@ module.exports = function(grunt) { transform: [ ["babelify", { global: true, - // Only babelify asmcrypto in node_modules - only: /^(?:.*\/node_modules\/asmcrypto\.js\/|(?!.*\/node_modules\/)).*$/, + // Only babelify asmcrypto and address-rfc2822 in node_modules + only: /^(?:.*\/node_modules\/asmcrypto\.js\/|.*\/node_modules\/address-rfc2822\/|(?!.*\/node_modules\/)).*$/, plugins: ["transform-async-to-generator", "syntax-async-functions", "transform-regenerator", diff --git a/package.json b/package.json index 025ab6c6..1a674fa4 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "whatwg-fetch": "^2.0.3" }, "dependencies": { + "address-rfc2822": "^2.0.3", "asmcrypto.js": "^0.22.0", "asn1.js": "^5.0.0", "bn.js": "^4.11.8", diff --git a/src/key.js b/src/key.js index 0d58a2a3..cf48a3d0 100644 --- a/src/key.js +++ b/src/key.js @@ -1244,7 +1244,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { await Promise.all(options.userIds.map(async function(userId, index) { const userIdPacket = new packet.Userid(); - userIdPacket.read(util.str_to_Uint8Array(userId)); + userIdPacket.format(userId); const dataToSign = {}; dataToSign.userid = userIdPacket; diff --git a/src/openpgp.js b/src/openpgp.js index 9f0a690e..163cf94f 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -115,7 +115,7 @@ export function destroyWorker() { */ export function generateKey({ userIds=[], passphrase="", numBits=2048, keyExpirationTime=0, curve="", date=new Date(), subkeys=[{}] }) { - userIds = formatUserIds(userIds); + userIds = toArray(userIds); const options = { userIds, passphrase, numBits, keyExpirationTime, curve, date, subkeys }; if (util.getWebCryptoAll() && numBits < 2048) { throw new Error('numBits should be 2048 or 4096, found: ' + numBits); @@ -146,7 +146,7 @@ export function generateKey({ userIds=[], passphrase="", numBits=2048, keyExpira * @static */ export function reformatKey({privateKey, userIds=[], passphrase="", keyExpirationTime=0, date}) { - userIds = formatUserIds(userIds); + userIds = toArray(userIds); const options = { privateKey, userIds, passphrase, keyExpirationTime, date}; if (asyncProxy) { return asyncProxy.delegate('reformatKey', options); @@ -484,36 +484,6 @@ function checkCleartextOrMessage(message) { } } -/** - * Format user ids for internal use. - */ -function formatUserIds(userIds) { - if (!userIds) { - return userIds; - } - userIds = toArray(userIds); // normalize to array - userIds = userIds.map(id => { - if (util.isString(id) && !util.isUserId(id)) { - throw new Error('Invalid user id format'); - } - if (util.isUserId(id)) { - return id; // user id is already in correct format... no conversion necessary - } - // name and email address can be empty but must be of the correct type - id.name = id.name || ''; - id.email = id.email || ''; - if (!util.isString(id.name) || (id.email && !util.isEmailAddress(id.email))) { - throw new Error('Invalid user id format'); - } - id.name = id.name.trim(); - if (id.name.length > 0) { - id.name += ' '; - } - return id.name + '<' + id.email + '>'; - }); - return userIds; -} - /** * Normalize parameter to an array if it is not undefined. * @param {Object} param the parameter to be normalized diff --git a/src/packet/userid.js b/src/packet/userid.js index 99a5fbdb..90ce88e6 100644 --- a/src/packet/userid.js +++ b/src/packet/userid.js @@ -41,6 +41,10 @@ function Userid() { * @type {String} */ this.userid = ''; + + this.name = ''; + this.email = ''; + this.comment = ''; } /** @@ -48,7 +52,17 @@ function Userid() { * @param {Uint8Array} input payload of a tag 13 packet */ Userid.prototype.read = function (bytes) { - this.userid = util.decode_utf8(util.Uint8Array_to_str(bytes)); + this.parse(util.decode_utf8(util.Uint8Array_to_str(bytes))); +}; + +/** + * Parse userid string, e.g. 'John Doe ' + */ +Userid.prototype.parse = function (userid) { + try { + Object.assign(this, util.parseUserId(userid)); + } catch(e) {} + this.userid = userid; }; /** @@ -59,4 +73,15 @@ Userid.prototype.write = function () { return util.str_to_Uint8Array(util.encode_utf8(this.userid)); }; +/** + * Set userid string from object, e.g. { name:'Phil Zimmermann', email:'phil@openpgp.org' } + */ +Userid.prototype.format = function (userid) { + if (util.isString(userid)) { + userid = util.parseUserId(userid); + } + Object.assign(this, userid); + this.userid = util.formatUserId(userid); +}; + export default Userid; diff --git a/src/util.js b/src/util.js index 375dc887..4cca968a 100644 --- a/src/util.js +++ b/src/util.js @@ -17,11 +17,13 @@ /** * This object contains utility functions + * @requires address-rfc2822 * @requires config * @requires encoding/base64 * @module util */ +import rfc2822 from 'address-rfc2822'; import config from './config'; import util from './util'; // re-import module to access util functions import b64 from './encoding/base64'; @@ -569,6 +571,29 @@ export default { return re.test(data); }, + /** + * Format user id for internal use. + */ + formatUserId: function(id) { + // name and email address can be empty but must be of the correct type + if ((id.name && !util.isString(id.name)) || (id.email && !util.isEmailAddress(id.email))) { + throw new Error('Invalid user id format'); + } + return new rfc2822.Address(id.name, id.email, id.comment).format(); + }, + + /** + * Parse user id. + */ + parseUserId: function(userid) { + try { + const [{ phrase: name, address: email, comment }] = rfc2822.parse(userid); + return { name, email, comment: comment.replace(/^\(|\)$/g, '') }; + } catch(e) { + throw new Error('Invalid user id format'); + } + }, + isUserId: function(data) { if (!util.isString(data)) { return false; diff --git a/test/general/ecc_nist.js b/test/general/ecc_nist.js index 264e07cd..f6766272 100644 --- a/test/general/ecc_nist.js +++ b/test/general/ecc_nist.js @@ -156,8 +156,14 @@ describe('Elliptic Curve Cryptography', function () { return data[name].priv_key; } it('Load public key', function (done) { - load_pub_key('romeo'); - load_pub_key('juliet'); + const romeoPublic = load_pub_key('romeo'); + expect(romeoPublic.users[0].userId.name).to.equal('Romeo Montague'); + expect(romeoPublic.users[0].userId.email).to.equal('romeo@example.net'); + expect(romeoPublic.users[0].userId.comment).to.equal('secp256k1'); + const julietPublic = load_pub_key('juliet'); + expect(julietPublic.users[0].userId.name).to.equal('Juliet Capulet'); + expect(julietPublic.users[0].userId.email).to.equal('juliet@example.net'); + expect(julietPublic.users[0].userId.comment).to.equal('secp256k1'); done(); }); it('Load private key', async function () { diff --git a/test/general/key.js b/test/general/key.js index 3003b8f1..c051e8fd 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1297,6 +1297,9 @@ p92yZgB3r2+f6/GIe2+7 const primUser = await key.getPrimaryUser(); expect(primUser).to.exist; expect(primUser.user.userId.userid).to.equal('Signature Test '); + expect(primUser.user.userId.name).to.equal('Signature Test'); + expect(primUser.user.userId.email).to.equal('signature@test.com'); + expect(primUser.user.userId.comment).to.equal(''); expect(primUser.selfCertification).to.be.an.instanceof(openpgp.packet.Signature); }); @@ -1315,13 +1318,16 @@ p92yZgB3r2+f6/GIe2+7 }); it('Generate key - single userid', function() { - const userId = 'test '; + const userId = { name: 'test', email: 'a@b.com', comment: 'test comment' }; const opt = {numBits: 512, userIds: userId, passphrase: '123'}; if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys return openpgp.generateKey(opt).then(function(key) { key = key.key; expect(key.users.length).to.equal(1); - expect(key.users[0].userId.userid).to.equal(userId); + expect(key.users[0].userId.userid).to.equal('test (test comment)'); + expect(key.users[0].userId.name).to.equal(userId.name); + expect(key.users[0].userId.email).to.equal(userId.email); + expect(key.users[0].userId.comment).to.equal(userId.comment); }); }); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 3f6e8ed3..7c878a37 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -371,73 +371,57 @@ describe('OpenPGP.js public api tests', function() { }); }); - describe('generateKey - unit tests', function() { - let keyGenStub; - let keyObjStub; - let getWebCryptoAllStub; + describe('generateKey - validate user ids', function() { + let rsaGenStub; + let rsaGenValue = openpgp.crypto.publicKey.rsa.generate(2048, "10001"); beforeEach(function() { - keyObjStub = { - armor: function() { - return 'priv_key'; - }, - toPublic: function() { - return { - armor: function() { - return 'pub_key'; - } - }; - } - }; - keyGenStub = stub(openpgp.key, 'generate'); - keyGenStub.returns(resolves(keyObjStub)); - getWebCryptoAllStub = stub(openpgp.util, 'getWebCryptoAll'); + rsaGenStub = stub(openpgp.crypto.publicKey.rsa, 'generate'); + rsaGenStub.returns(rsaGenValue); }); afterEach(function() { - keyGenStub.restore(); - openpgp.destroyWorker(); - getWebCryptoAllStub.restore(); + rsaGenStub.restore(); }); - it('should fail for invalid user name', function() { + it('should fail for invalid user name', async function() { const opt = { userIds: [{ name: {}, email: 'text@example.com' }] }; - const test = openpgp.generateKey.bind(null, opt); - expect(test).to.throw(/Invalid user id format/); + const test = openpgp.generateKey(opt); + await expect(test).to.eventually.be.rejectedWith(/Invalid user id format/); }); - it('should fail for invalid user email address', function() { + it('should fail for invalid user email address', async function() { const opt = { userIds: [{ name: 'Test User', email: 'textexample.com' }] }; - const test = openpgp.generateKey.bind(null, opt); - expect(test).to.throw(/Invalid user id format/); + const test = openpgp.generateKey(opt); + await expect(test).to.eventually.be.rejectedWith(/Invalid user id format/); }); - it('should fail for invalid user email address', function() { + it('should fail for invalid user email address', async function() { const opt = { userIds: [{ name: 'Test User', email: 'text@examplecom' }] }; - const test = openpgp.generateKey.bind(null, opt); - expect(test).to.throw(/Invalid user id format/); + const test = openpgp.generateKey(opt); + await expect(test).to.eventually.be.rejectedWith(/Invalid user id format/); }); - it('should fail for invalid string user id', function() { + it('should fail for invalid string user id', async function() { const opt = { userIds: ['Test User text@example.com>'] }; - const test = openpgp.generateKey.bind(null, opt); - expect(test).to.throw(/Invalid user id format/); + const test = openpgp.generateKey(opt); + await expect(test).to.eventually.be.rejectedWith(/Invalid user id format/); }); - it('should fail for invalid single string user id', function() { + it('should fail for invalid single string user id', async function() { const opt = { userIds: 'Test User text@example.com>' }; - const test = openpgp.generateKey.bind(null, opt); - expect(test).to.throw(/Invalid user id format/); + const test = openpgp.generateKey(opt); + await expect(test).to.eventually.be.rejectedWith(/Invalid user id format/); }); it('should work for valid single string user id', function() { @@ -481,6 +465,36 @@ describe('OpenPGP.js public api tests', function() { }; return openpgp.generateKey(opt); }); + }); + + describe('generateKey - unit tests', function() { + let keyGenStub; + let keyObjStub; + let getWebCryptoAllStub; + + beforeEach(function() { + keyObjStub = { + armor: function() { + return 'priv_key'; + }, + toPublic: function() { + return { + armor: function() { + return 'pub_key'; + } + }; + } + }; + keyGenStub = stub(openpgp.key, 'generate'); + keyGenStub.returns(resolves(keyObjStub)); + getWebCryptoAllStub = stub(openpgp.util, 'getWebCryptoAll'); + }); + + afterEach(function() { + keyGenStub.restore(); + openpgp.destroyWorker(); + getWebCryptoAllStub.restore(); + }); it('should have default params set', function() { const now = openpgp.util.normalizeDate(new Date()); @@ -492,7 +506,7 @@ describe('OpenPGP.js public api tests', function() { }; return openpgp.generateKey(opt).then(function(newKey) { expect(keyGenStub.withArgs({ - userIds: ['Test User '], + userIds: [{ name: 'Test User', email: 'text@example.com' }], passphrase: 'secret', numBits: 2048, keyExpirationTime: 0, From fe3c1b4f31e44cc12de8bf6ebe344606927f1a47 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 25 Apr 2018 10:37:11 +0200 Subject: [PATCH 2/4] Add fromUserId / toUserId parameters to openpgp.encrypt and sign To select the user whose algorithm preferences, expiration time etc to use. --- src/cleartext.js | 10 +++++---- src/key.js | 51 ++++++++++++++++++++++++++++--------------- src/message.js | 39 ++++++++++++++++++--------------- src/openpgp.js | 29 +++++++++++++++---------- test/general/key.js | 53 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 51 deletions(-) diff --git a/src/cleartext.js b/src/cleartext.js index 8c423b96..aff97569 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -70,11 +70,12 @@ CleartextMessage.prototype.getSigningKeyIds = function() { * @param {Array} privateKeys private keys with decrypted secret key data for signing * @param {Signature} signature (optional) any existing detached signature * @param {Date} date (optional) The creation time of the signature that should be created + * @param {Object} userId (optional) user ID to sign with, e.g. { name:'Steve Sender', email:'steve@openpgp.org' } * @returns {Promise} new cleartext message with signed content * @async */ -CleartextMessage.prototype.sign = async function(privateKeys, signature=null, date=new Date()) { - return new CleartextMessage(this.text, await this.signDetached(privateKeys, signature, date)); +CleartextMessage.prototype.sign = async function(privateKeys, signature=null, date=new Date(), userId={}) { + return new CleartextMessage(this.text, await this.signDetached(privateKeys, signature, date, userId)); }; /** @@ -82,14 +83,15 @@ CleartextMessage.prototype.sign = async function(privateKeys, signature=null, da * @param {Array} privateKeys private keys with decrypted secret key data for signing * @param {Signature} signature (optional) any existing detached signature * @param {Date} date (optional) The creation time of the signature that should be created + * @param {Object} userId (optional) user ID to sign with, e.g. { name:'Steve Sender', email:'steve@openpgp.org' } * @returns {Promise} new detached signature of message content * @async */ -CleartextMessage.prototype.signDetached = async function(privateKeys, signature=null, date=new Date()) { +CleartextMessage.prototype.signDetached = async function(privateKeys, signature=null, date=new Date(), userId={}) { const literalDataPacket = new packet.Literal(); literalDataPacket.setText(this.text); - return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date)); + return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userId)); }; /** diff --git a/src/key.js b/src/key.js index cf48a3d0..b07d6912 100644 --- a/src/key.js +++ b/src/key.js @@ -279,14 +279,15 @@ function isValidSigningKeyPacket(keyPacket, signature, date=new Date()) { * Returns first key packet or key packet by given keyId that is available for signing and verification * @param {module:type/keyid} keyId, optional * @param {Date} date use the given date for verification instead of the current time + * @param {Object} userId, optional user ID * @returns {Promise} key packet or null if no signing key has been found * @async */ -Key.prototype.getSigningKeyPacket = async function (keyId=null, date=new Date()) { +Key.prototype.getSigningKeyPacket = async function (keyId=null, date=new Date(), userId={}) { const primaryKey = this.primaryKey; - if (await this.verifyPrimaryKey(date) === enums.keyStatus.valid) { - const primaryUser = await this.getPrimaryUser(date); + if (await this.verifyPrimaryKey(date, userId) === enums.keyStatus.valid) { + const primaryUser = await this.getPrimaryUser(date, userId); if (primaryUser && (!keyId || primaryKey.getKeyId().equals(keyId)) && isValidSigningKeyPacket(primaryKey, primaryUser.selfCertification, date)) { return primaryKey; @@ -322,15 +323,16 @@ function isValidEncryptionKeyPacket(keyPacket, signature, date=new Date()) { * Returns first key packet or key packet by given keyId that is available for encryption or decryption * @param {module:type/keyid} keyId, optional * @param {Date} date, optional + * @param {String} userId, optional * @returns {Promise} key packet or null if no encryption key has been found * @async */ -Key.prototype.getEncryptionKeyPacket = async function(keyId, date=new Date()) { +Key.prototype.getEncryptionKeyPacket = async function(keyId, date=new Date(), userId={}) { const primaryKey = this.primaryKey; - if (await this.verifyPrimaryKey(date) === enums.keyStatus.valid) { + if (await this.verifyPrimaryKey(date, userId) === enums.keyStatus.valid) { // V4: by convention subkeys are preferred for encryption service // V3: keys MUST NOT have subkeys for (let i = 0; i < this.subKeys.length; i++) { @@ -345,7 +347,7 @@ Key.prototype.getEncryptionKeyPacket = async function(keyId, date=new Date()) { } } // if no valid subkey for encryption, evaluate primary key - const primaryUser = await this.getPrimaryUser(date); + const primaryUser = await this.getPrimaryUser(date, userId); if (primaryUser && (!keyId || primaryKey.getKeyId().equals(keyId)) && isValidEncryptionKeyPacket(primaryKey, primaryUser.selfCertification, date)) { return primaryKey; @@ -433,10 +435,11 @@ Key.prototype.isRevoked = async function(signature, key, date=new Date()) { * Verify primary key. Checks for revocation signatures, expiration time * and valid self signature * @param {Date} date (optional) use the given date for verification instead of the current time + * @param {Object} userId (optional) user ID * @returns {Promise} The status of the primary key * @async */ -Key.prototype.verifyPrimaryKey = async function(date=new Date()) { +Key.prototype.verifyPrimaryKey = async function(date=new Date(), userId={}) { const primaryKey = this.primaryKey; // check for key revocation signatures if (await this.isRevoked(null, null, date)) { @@ -448,7 +451,7 @@ Key.prototype.verifyPrimaryKey = async function(date=new Date()) { return enums.keyStatus.no_self_cert; } // check for valid, unrevoked, unexpired self signature - const { user, selfCertification } = await this.getPrimaryUser(date) || {}; + const { user, selfCertification } = await this.getPrimaryUser(date, userId) || {}; if (!user) { return enums.keyStatus.invalid; } @@ -482,19 +485,29 @@ Key.prototype.getExpirationTime = async function() { * - if multiple primary users exist, returns the one with the latest self signature * - otherwise, returns the user with the latest self signature * @param {Date} date use the given date for verification instead of the current time + * @param {Object} userId (optional) user ID to get instead of the primary user, if it exists * @returns {Promise<{user: module:key.User, * selfCertification: module:packet.Signature}>} The primary user and the self signature * @async */ -Key.prototype.getPrimaryUser = async function(date=new Date()) { - // sort by primary user flag and signature creation time +Key.prototype.getPrimaryUser = async function(date=new Date(), userId={}) { + if (!this.users.length) { + return null; + } + // sort by userId, primary user flag and signature creation time const primaryUser = this.users.map(function(user, index) { const selfCertification = getLatestSignature(user.selfCertifications, date); return { index, user, selfCertification }; }).sort(function(a, b) { const A = a.selfCertification; const B = b.selfCertification; - return (A.isPrimaryUserID - B.isPrimaryUserID) || (A.created - B.created); + return ( + (a.user.userId.email === userId.email) - (b.user.userId.email === userId.email) || + (a.user.userId.name === userId.name) - (b.user.userId.name === userId.name) || + (a.user.userId.comment === userId.comment) - (b.user.userId.comment === userId.comment) || + A.isPrimaryUserID - B.isPrimaryUserID || + A.created - B.created + ); }).pop(); const { user, selfCertification: cert } = primaryUser; if (!user.userId) { @@ -1403,21 +1416,22 @@ function getExpirationTime(keyPacket, signature) { * Returns the preferred signature hash algorithm of a key * @param {object} key * @param {Date} date (optional) use the given date for verification instead of the current time + * @param {Object} userId (optional) user ID * @returns {Promise} * @async */ -export async function getPreferredHashAlgo(key, date) { +export async function getPreferredHashAlgo(key, date=new Date(), userId={}) { let hash_algo = config.prefer_hash_algorithm; let pref_algo = hash_algo; if (key instanceof Key) { - const primaryUser = await key.getPrimaryUser(date); + const primaryUser = await key.getPrimaryUser(date, userId); if (primaryUser && primaryUser.selfCertification.preferredHashAlgorithms) { [pref_algo] = primaryUser.selfCertification.preferredHashAlgorithms; hash_algo = crypto.hash.getHashByteLength(hash_algo) <= crypto.hash.getHashByteLength(pref_algo) ? pref_algo : hash_algo; } // disable expiration checks - key = key.getSigningKeyPacket(undefined, null); + key = key.getSigningKeyPacket(undefined, null, userId); } switch (Object.getPrototypeOf(key)) { case packet.SecretKey.prototype: @@ -1440,15 +1454,16 @@ export async function getPreferredHashAlgo(key, date) { * @param {symmetric|aead} type Type of preference to return * @param {Array} keys Set of keys * @param {Date} date (optional) use the given date for verification instead of the current time + * @param {Object} userId (optional) user ID * @returns {Promise} Preferred symmetric algorithm * @async */ -export async function getPreferredAlgo(type, keys, date) { +export async function getPreferredAlgo(type, keys, date=new Date(), userId={}) { const prefProperty = type === 'symmetric' ? 'preferredSymmetricAlgorithms' : 'preferredAeadAlgorithms'; const defaultAlgo = type === 'symmetric' ? config.encryption_cipher : config.aead_mode; const prioMap = {}; await Promise.all(keys.map(async function(key) { - const primaryUser = await key.getPrimaryUser(date); + const primaryUser = await key.getPrimaryUser(date, userId); if (!primaryUser || !primaryUser.selfCertification[prefProperty]) { return defaultAlgo; } @@ -1480,11 +1495,11 @@ export async function getPreferredAlgo(type, keys, date) { * @returns {Promise} * @async */ -export async function isAeadSupported(keys, date) { +export async function isAeadSupported(keys, date=new Date(), userId={}) { let supported = true; // TODO replace when Promise.some or Promise.any are implemented await Promise.all(keys.map(async function(key) { - const primaryUser = await key.getPrimaryUser(date); + const primaryUser = await key.getPrimaryUser(date, userId); if (!primaryUser || !primaryUser.selfCertification.features || !(primaryUser.selfCertification.features[0] & enums.features.aead)) { supported = false; diff --git a/src/message.js b/src/message.js index 7ce2cb2f..2c3826db 100644 --- a/src/message.js +++ b/src/message.js @@ -247,10 +247,11 @@ Message.prototype.getText = function() { * @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] } * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs * @param {Date} date (optional) override the creation date of the literal package + * @param {Object} userId (optional) user ID to encrypt for, e.g. { name:'Robert Receiver', email:'robert@openpgp.org' } * @returns {Promise} new message with encrypted content * @async */ -Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard=false, date=new Date()) { +Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard=false, date=new Date(), userId={}) { let symAlgo; let aeadAlgo; let symEncryptedPacket; @@ -263,9 +264,9 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard aeadAlgo = sessionKey.aeadAlgorithm; sessionKey = sessionKey.data; } else if (keys && keys.length) { - symAlgo = enums.read(enums.symmetric, await getPreferredAlgo('symmetric', keys, date)); - if (config.aead_protect && config.aead_protect_version === 4 && await isAeadSupported(keys, date)) { - aeadAlgo = enums.read(enums.aead, await getPreferredAlgo('aead', keys, date)); + symAlgo = enums.read(enums.symmetric, await getPreferredAlgo('symmetric', keys, date, userId)); + if (config.aead_protect && config.aead_protect_version === 4 && await isAeadSupported(keys, date, userId)) { + aeadAlgo = enums.read(enums.aead, await getPreferredAlgo('aead', keys, date, userId)); } } else if (passwords && passwords.length) { symAlgo = enums.read(enums.symmetric, config.encryption_cipher); @@ -278,7 +279,7 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard sessionKey = await crypto.generateSessionKey(symAlgo); } - const msg = await encryptSessionKey(sessionKey, symAlgo, aeadAlgo, keys, passwords, wildcard, date); + const msg = await encryptSessionKey(sessionKey, symAlgo, aeadAlgo, keys, passwords, wildcard, date, userId); if (config.aead_protect && (config.aead_protect_version !== 4 || aeadAlgo)) { symEncryptedPacket = new packet.SymEncryptedAEADProtected(); @@ -312,16 +313,17 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard * @param {Array} publicKeys (optional) public key(s) for message encryption * @param {Array} passwords (optional) for message encryption * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs - * @param {Date} date (optional) override the creation date signature + * @param {Date} date (optional) override the date + * @param {Object} userId (optional) user ID to encrypt for, e.g. { name:'Robert Receiver', email:'robert@openpgp.org' } * @returns {Promise} new message with encrypted content * @async */ -export async function encryptSessionKey(sessionKey, symAlgo, aeadAlgo, publicKeys, passwords, wildcard=false, date=new Date()) { +export async function encryptSessionKey(sessionKey, symAlgo, aeadAlgo, publicKeys, passwords, wildcard=false, date=new Date(), userId={}) { const packetlist = new packet.List(); if (publicKeys) { const results = await Promise.all(publicKeys.map(async function(publicKey) { - const encryptionKeyPacket = await publicKey.getEncryptionKeyPacket(undefined, date); + const encryptionKeyPacket = await publicKey.getEncryptionKeyPacket(undefined, date, userId); if (!encryptionKeyPacket) { throw new Error('Could not find valid key packet for encryption in key ' + publicKey.primaryKey.getKeyId().toHex()); @@ -380,11 +382,12 @@ export async function encryptSessionKey(sessionKey, symAlgo, aeadAlgo, publicKey * Sign the message (the literal data packet of the message) * @param {Array} privateKeys private keys with decrypted secret key data for signing * @param {Signature} signature (optional) any existing detached signature to add to the message - * @param {Date} date} (optional) override the creation time of the signature + * @param {Date} date (optional) override the creation time of the signature + * @param {Object} userId (optional) user ID to sign with, e.g. { name:'Steve Sender', email:'steve@openpgp.org' } * @returns {Promise} new message with signed content * @async */ -Message.prototype.sign = async function(privateKeys=[], signature=null, date=new Date()) { +Message.prototype.sign = async function(privateKeys=[], signature=null, date=new Date(), userId={}) { const packetlist = new packet.List(); const literalDataPacket = this.packets.findPacket(enums.packet.literal); @@ -418,14 +421,14 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new if (privateKey.isPublic()) { throw new Error('Need private key for signing'); } - const signingKeyPacket = await privateKey.getSigningKeyPacket(undefined, date); + const signingKeyPacket = await privateKey.getSigningKeyPacket(undefined, date, userId); if (!signingKeyPacket) { throw new Error('Could not find valid key packet for signing in key ' + privateKey.primaryKey.getKeyId().toHex()); } const onePassSig = new packet.OnePassSignature(); onePassSig.type = signatureType; - onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, date); + onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, date, userId); onePassSig.publicKeyAlgorithm = signingKeyPacket.algorithm; onePassSig.signingKeyId = signingKeyPacket.getKeyId(); if (i === privateKeys.length - 1) { @@ -467,15 +470,16 @@ Message.prototype.compress = function(compression) { * @param {Array} privateKeys private keys with decrypted secret key data for signing * @param {Signature} signature (optional) any existing detached signature * @param {Date} date (optional) override the creation time of the signature + * @param {Object} userId (optional) user ID to sign with, e.g. { name:'Steve Sender', email:'steve@openpgp.org' } * @returns {Promise} new detached signature of message content * @async */ -Message.prototype.signDetached = async function(privateKeys=[], signature=null, date=new Date()) { +Message.prototype.signDetached = async function(privateKeys=[], signature=null, date=new Date(), userId={}) { const literalDataPacket = this.packets.findPacket(enums.packet.literal); if (!literalDataPacket) { throw new Error('No literal data packet to sign.'); } - return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date)); + return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userId)); }; /** @@ -484,10 +488,11 @@ Message.prototype.signDetached = async function(privateKeys=[], signature=null, * @param {Array} privateKeys private keys with decrypted secret key data for signing * @param {Signature} signature (optional) any existing detached signature to append * @param {Date} date (optional) override the creationtime of the signature + * @param {Object} userId (optional) user ID to sign with, e.g. { name:'Steve Sender', email:'steve@openpgp.org' } * @returns {Promise} list of signature packets * @async */ -export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date()) { +export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date(), userId={}) { const packetlist = new packet.List(); // If data packet was created from Uint8Array, use binary, otherwise use text @@ -498,7 +503,7 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig if (privateKey.isPublic()) { throw new Error('Need private key for signing'); } - const signingKeyPacket = await privateKey.getSigningKeyPacket(undefined, date); + const signingKeyPacket = await privateKey.getSigningKeyPacket(undefined, date, userId); if (!signingKeyPacket) { throw new Error('Could not find valid key packet for signing in key ' + privateKey.primaryKey.getKeyId().toHex()); @@ -509,7 +514,7 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig const signaturePacket = new packet.Signature(date); signaturePacket.signatureType = signatureType; signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; - signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, date); + signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, date, userId); await signaturePacket.sign(signingKeyPacket, literalDataPacket); return signaturePacket; })).then(signatureList => { diff --git a/src/openpgp.js b/src/openpgp.js index 163cf94f..7bc4706e 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -228,17 +228,19 @@ export function encryptKey({ privateKey, passphrase }) { * @param {Boolean} returnSessionKey (optional) if the unencrypted session key should be added to returned object * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs * @param {Date} date (optional) override the creation date of the message and the message signature + * @param {Object} fromUserId (optional) user ID to sign with, e.g. { name:'Steve Sender', email:'steve@openpgp.org' } + * @param {Object} toUserId (optional) user ID to encrypt for, e.g. { name:'Robert Receiver', email:'robert@openpgp.org' } * @returns {Promise} encrypted (and optionally signed message) in the form: * {data: ASCII armored message if 'armor' is true, * message: full Message object if 'armor' is false, signature: detached signature if 'detached' is true} * @async * @static */ -export function encrypt({ data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression=config.compression, armor=true, detached=false, signature=null, returnSessionKey=false, wildcard=false, date=new Date()}) { +export function encrypt({ data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression=config.compression, armor=true, detached=false, signature=null, returnSessionKey=false, wildcard=false, date=new Date(), fromUserId={}, toUserId={} }) { checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported - return asyncProxy.delegate('encrypt', { data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression, armor, detached, signature, returnSessionKey, wildcard, date }); + return asyncProxy.delegate('encrypt', { data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression, armor, detached, signature, returnSessionKey, wildcard, date, fromUserId, toUserId }); } const result = {}; return Promise.resolve().then(async function() { @@ -248,14 +250,14 @@ export function encrypt({ data, dataType, publicKeys, privateKeys, passwords, se } if (privateKeys.length || signature) { // sign the message only if private keys or signature is specified if (detached) { - const detachedSignature = await message.signDetached(privateKeys, signature, date); + const detachedSignature = await message.signDetached(privateKeys, signature, date, fromUserId); result.signature = armor ? detachedSignature.armor() : detachedSignature; } else { - message = await message.sign(privateKeys, signature, date); + message = await message.sign(privateKeys, signature, date, fromUserId); } } message = message.compress(compression); - return message.encrypt(publicKeys, passwords, sessionKey, wildcard, date); + return message.encrypt(publicKeys, passwords, sessionKey, wildcard, date, toUserId); }).then(encrypted => { if (armor) { @@ -322,19 +324,20 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe * @param {Boolean} armor (optional) if the return value should be ascii armored or the message object * @param {Boolean} detached (optional) if the return value should contain a detached signature * @param {Date} date (optional) override the creation date signature + * @param {Object} fromUserId (optional) user ID to sign with, e.g. { name:'Steve Sender', email:'steve@openpgp.org' } * @returns {Promise} signed cleartext in the form: * {data: ASCII armored message if 'armor' is true, * message: full Message object if 'armor' is false, signature: detached signature if 'detached' is true} * @async * @static */ -export function sign({ data, dataType, privateKeys, armor=true, detached=false, date=new Date() }) { +export function sign({ data, dataType, privateKeys, armor=true, detached=false, date=new Date(), fromUserId={} }) { checkData(data); privateKeys = toArray(privateKeys); if (asyncProxy) { // use web worker if available return asyncProxy.delegate('sign', { - data, dataType, privateKeys, armor, detached, date + data, dataType, privateKeys, armor, detached, date, fromUserId }); } @@ -343,10 +346,10 @@ export function sign({ data, dataType, privateKeys, armor=true, detached=false, let message = util.isString(data) ? new CleartextMessage(data) : messageLib.fromBinary(data, dataType); if (detached) { - const signature = await message.signDetached(privateKeys, undefined, date); + const signature = await message.signDetached(privateKeys, undefined, date, fromUserId); result.signature = armor ? signature.armor() : signature; } else { - message = await message.sign(privateKeys, undefined, date); + message = await message.sign(privateKeys, undefined, date, fromUserId); if (armor) { result.data = message.armor(); } else { @@ -403,20 +406,22 @@ export function verify({ message, publicKeys, signature=null, date=new Date() }) * @param {Key|Array} publicKeys (optional) array of public keys or single key, used to encrypt the key * @param {String|Array} passwords (optional) passwords for the message * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs + * @param {Date} date (optional) override the date + * @param {Object} toUserId (optional) user ID to encrypt for, e.g. { name:'Phil Zimmermann', email:'phil@openpgp.org' } * @returns {Promise} the encrypted session key packets contained in a message object * @async * @static */ -export function encryptSessionKey({ data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard=false }) { +export function encryptSessionKey({ data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard=false, date=new Date(), toUserId={} }) { checkBinary(data); checkString(algorithm, 'algorithm'); publicKeys = toArray(publicKeys); passwords = toArray(passwords); if (asyncProxy) { // use web worker if available - return asyncProxy.delegate('encryptSessionKey', { data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard }); + return asyncProxy.delegate('encryptSessionKey', { data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard, date, toUserId }); } return Promise.resolve().then(async function() { - return { message: await messageLib.encryptSessionKey(data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard) }; + return { message: await messageLib.encryptSessionKey(data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard, date, toUserId) }; }).catch(onError.bind(null, 'Error encrypting session key')); } diff --git a/test/general/key.js b/test/general/key.js index c051e8fd..3a01aa67 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1523,6 +1523,59 @@ p92yZgB3r2+f6/GIe2+7 expect(signatures[3].valid).to.be.null; }); + it('Encrypt - latest created user', async function() { + let publicKey = openpgp.key.readArmored(multi_uid_key).keys[0]; + const privateKey = openpgp.key.readArmored(priv_key_rsa).keys[0]; + await privateKey.decrypt('hello world'); + // Set second user to prefer aes128. We should select this user by default, since it was created later. + publicKey.users[1].selfCertifications[0].preferredSymmetricAlgorithms = [openpgp.enums.symmetric.aes128]; + const encrypted = await openpgp.encrypt({data: 'hello', publicKeys: publicKey, privateKeys: privateKey, armor: false}); + expect(encrypted.message.packets[0].sessionKeyAlgorithm).to.equal('aes128'); + }); + + it('Encrypt - primary user', async function() { + let publicKey = openpgp.key.readArmored(multi_uid_key).keys[0]; + const privateKey = openpgp.key.readArmored(priv_key_rsa).keys[0]; + await privateKey.decrypt('hello world'); + // Set first user to primary. We should select this user by default. + publicKey.users[0].selfCertifications[0].isPrimaryUserID = true; + // Set first user to prefer aes128. + publicKey.users[0].selfCertifications[0].preferredSymmetricAlgorithms = [openpgp.enums.symmetric.aes128]; + const encrypted = await openpgp.encrypt({data: 'hello', publicKeys: publicKey, privateKeys: privateKey, armor: false}); + expect(encrypted.message.packets[0].sessionKeyAlgorithm).to.equal('aes128'); + }); + + it('Encrypt - specific user', async function() { + let publicKey = openpgp.key.readArmored(multi_uid_key).keys[0]; + const privateKey = openpgp.key.readArmored(priv_key_rsa).keys[0]; + await privateKey.decrypt('hello world'); + // Set first user to primary. We won't select this user, this is to test that. + publicKey.users[0].selfCertifications[0].isPrimaryUserID = true; + // Set second user to prefer aes128. We will select this user. + publicKey.users[1].selfCertifications[0].preferredSymmetricAlgorithms = [openpgp.enums.symmetric.aes128]; + const encrypted = await openpgp.encrypt({data: 'hello', publicKeys: publicKey, privateKeys: privateKey, toUserId: {name: 'Test User', email: 'b@c.com'}, armor: false}); + expect(encrypted.message.packets[0].sessionKeyAlgorithm).to.equal('aes128'); + }); + + it('Sign - specific user', async function() { + let publicKey = openpgp.key.readArmored(multi_uid_key).keys[0]; + const privateKey = openpgp.key.readArmored(priv_key_rsa).keys[0]; + await privateKey.decrypt('hello world'); + const privateKeyClone = openpgp.key.readArmored(priv_key_rsa).keys[0]; + // Duplicate user + privateKey.users.push(privateKeyClone.users[0]); + // Set first user to primary. We won't select this user, this is to test that. + privateKey.users[0].selfCertifications[0].isPrimaryUserID = true; + // Change userid of the first user so that we don't select it. This also makes this user invalid. + privateKey.users[0].userId.parse('Test User '); + // Set second user to prefer aes128. We will select this user. + privateKey.users[1].selfCertifications[0].preferredHashAlgorithms = [openpgp.enums.hash.sha512]; + const signed = await openpgp.sign({data: 'hello', privateKeys: privateKey, fromUserId: {name: 'Test McTestington', email: 'test@example.com'}, armor: false}); + expect(signed.message.signature.packets[0].hashAlgorithm).to.equal(openpgp.enums.hash.sha512); + const encrypted = await openpgp.encrypt({data: 'hello', publicKeys: publicKey, privateKeys: privateKey, fromUserId: {name: 'Test McTestington', email: 'test@example.com'}, detached: true, armor: false}); + expect(encrypted.signature.packets[0].hashAlgorithm).to.equal(openpgp.enums.hash.sha512); + }); + it('Reformat key without passphrase', function() { const userId1 = 'test1 '; const userId2 = 'test2 '; From 3c224379f6606730dd66c59fe038eefdb3bd986b Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 27 Apr 2018 18:47:16 +0200 Subject: [PATCH 3/4] Remove util.isUserId() It was not really correct anyway; a user id can just be an email address without < > brackets. --- src/util.js | 7 ------- test/general/util.js | 31 ------------------------------- 2 files changed, 38 deletions(-) diff --git a/src/util.js b/src/util.js index 4cca968a..0b7fff83 100644 --- a/src/util.js +++ b/src/util.js @@ -594,13 +594,6 @@ export default { } }, - isUserId: function(data) { - if (!util.isString(data)) { - return false; - } - return /$/.test(data); - }, - /** * Normalize line endings to \r\n */ diff --git a/test/general/util.js b/test/general/util.js index bcc96f8c..cb0b4daf 100644 --- a/test/general/util.js +++ b/test/general/util.js @@ -116,37 +116,6 @@ describe('Util unit tests', function() { }); }); - describe('isUserId', function() { - it('should return true for valid user id', function() { - const data = 'Test User '; - expect(openpgp.util.isUserId(data)).to.be.true; - }); - it('should return false for invalid user id', function() { - const data = 'Test User test@example.com>'; - expect(openpgp.util.isUserId(data)).to.be.false; - }); - it('should return false for invalid user id', function() { - const data = 'Test User Date: Tue, 1 May 2018 18:57:10 +0200 Subject: [PATCH 4/4] Throw when user ID matches no users --- src/key.js | 35 ++++++++++++++++++----------------- test/general/key.js | 2 ++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/key.js b/src/key.js index b07d6912..91fd5e67 100644 --- a/src/key.js +++ b/src/key.js @@ -491,28 +491,29 @@ Key.prototype.getExpirationTime = async function() { * @async */ Key.prototype.getPrimaryUser = async function(date=new Date(), userId={}) { - if (!this.users.length) { - return null; - } - // sort by userId, primary user flag and signature creation time - const primaryUser = this.users.map(function(user, index) { + const users = this.users.map(function(user, index) { const selfCertification = getLatestSignature(user.selfCertifications, date); return { index, user, selfCertification }; - }).sort(function(a, b) { - const A = a.selfCertification; - const B = b.selfCertification; - return ( - (a.user.userId.email === userId.email) - (b.user.userId.email === userId.email) || - (a.user.userId.name === userId.name) - (b.user.userId.name === userId.name) || - (a.user.userId.comment === userId.comment) - (b.user.userId.comment === userId.comment) || - A.isPrimaryUserID - B.isPrimaryUserID || - A.created - B.created + }).filter(({ user }) => { + return user.userId && ( + (userId.name === undefined || user.userId.name === userId.name) && + (userId.email === undefined || user.userId.email === userId.email) && + (userId.comment === undefined || user.userId.comment === userId.comment) ); - }).pop(); - const { user, selfCertification: cert } = primaryUser; - if (!user.userId) { + }); + if (!users.length) { + if (userId) { + throw new Error('Could not find user that matches that user ID'); + } return null; } + // sort by primary user flag and signature creation time + const primaryUser = users.sort(function(a, b) { + const A = a.selfCertification; + const B = b.selfCertification; + return A.isPrimaryUserID - B.isPrimaryUserID || A.created - B.created; + }).pop(); + const { user, selfCertification: cert } = primaryUser; const { primaryKey } = this; const dataToVerify = { userid: user.userId , key: primaryKey }; // skip if certificates is invalid, revoked, or expired diff --git a/test/general/key.js b/test/general/key.js index 3a01aa67..7e53d1af 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1555,6 +1555,7 @@ p92yZgB3r2+f6/GIe2+7 publicKey.users[1].selfCertifications[0].preferredSymmetricAlgorithms = [openpgp.enums.symmetric.aes128]; const encrypted = await openpgp.encrypt({data: 'hello', publicKeys: publicKey, privateKeys: privateKey, toUserId: {name: 'Test User', email: 'b@c.com'}, armor: false}); expect(encrypted.message.packets[0].sessionKeyAlgorithm).to.equal('aes128'); + await expect(openpgp.encrypt({data: 'hello', publicKeys: publicKey, privateKeys: privateKey, toUserId: {name: 'Test User', email: 'c@c.com'}, armor: false})).to.be.rejectedWith('Could not find user that matches that user ID'); }); it('Sign - specific user', async function() { @@ -1574,6 +1575,7 @@ p92yZgB3r2+f6/GIe2+7 expect(signed.message.signature.packets[0].hashAlgorithm).to.equal(openpgp.enums.hash.sha512); const encrypted = await openpgp.encrypt({data: 'hello', publicKeys: publicKey, privateKeys: privateKey, fromUserId: {name: 'Test McTestington', email: 'test@example.com'}, detached: true, armor: false}); expect(encrypted.signature.packets[0].hashAlgorithm).to.equal(openpgp.enums.hash.sha512); + await expect(openpgp.encrypt({data: 'hello', publicKeys: publicKey, privateKeys: privateKey, fromUserId: {name: 'Not Test McTestington', email: 'test@example.com'}, detached: true, armor: false})).to.be.rejectedWith('Could not find user that matches that user ID'); }); it('Reformat key without passphrase', function() {