Merge pull request #693 from twiss/userid

Parse user IDs & allow selecting user when encrypting/signing
This commit is contained in:
Sanjana Rajan 2018-05-03 08:05:12 -07:00 committed by GitHub
commit 4c9d025193
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 263 additions and 171 deletions

View File

@ -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",

View File

@ -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",

View File

@ -70,11 +70,12 @@ CleartextMessage.prototype.getSigningKeyIds = function() {
* @param {Array<module:key.Key>} 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<module:cleartext.CleartextMessage>} 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<module:key.Key>} 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<module:signature.Signature>} 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));
};
/**

View File

@ -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<module:packet.SecretSubkey|
* module:packet.SecretKey|null>} 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<module:packet.PublicSubkey|
* module:packet.SecretSubkey|
* module:packet.SecretKey|
* module:packet.PublicKey|null>} 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<module:enums.keyStatus>} 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,24 +485,35 @@ 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
const primaryUser = this.users.map(function(user, index) {
Key.prototype.getPrimaryUser = async function(date=new Date(), userId={}) {
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.isPrimaryUserID - B.isPrimaryUserID) || (A.created - B.created);
}).pop();
const { user, selfCertification: cert } = primaryUser;
if (!user.userId) {
}).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)
);
});
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
@ -1244,7 +1258,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;
@ -1403,21 +1417,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<String>}
* @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 +1455,16 @@ export async function getPreferredHashAlgo(key, date) {
* @param {symmetric|aead} type Type of preference to return
* @param {Array<module:key.Key>} 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<module:enums.symmetric>} 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 +1496,11 @@ export async function getPreferredAlgo(type, keys, date) {
* @returns {Promise<Boolean>}
* @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;

View File

@ -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<Message>} 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<Key>} publicKeys (optional) public key(s) for message encryption
* @param {Array<String>} 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<Message>} 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<module:key.Key>} 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<Message>} 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<module:key.Key>} 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<module:signature.Signature>} 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<module:key.Key>} 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<module:packet.List>} 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 => {

View File

@ -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);
@ -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<Object>} 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<Object>} 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<Key>} publicKeys (optional) array of public keys or single key, used to encrypt the key
* @param {String|Array<String>} 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<Message>} 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'));
}
@ -484,36 +489,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

View File

@ -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 <john@example.com>'
*/
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;

View File

@ -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,11 +571,27 @@ export default {
return re.test(data);
},
isUserId: function(data) {
if (!util.isString(data)) {
return false;
/**
* 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');
}
return /</.test(data) && />$/.test(data);
},
/**

View File

@ -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 () {

View File

@ -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 <signature@test.com>');
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 <a@b.com>';
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 <a@b.com> (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);
});
});
@ -1517,6 +1523,61 @@ 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');
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() {
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 <b@c.com>');
// 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);
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() {
const userId1 = 'test1 <a@b.com>';
const userId2 = 'test2 <b@a.com>';

View File

@ -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 <text@example.com>'],
userIds: [{ name: 'Test User', email: 'text@example.com' }],
passphrase: 'secret',
numBits: 2048,
keyExpirationTime: 0,

View File

@ -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 <test@example.com>';
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 <test@example.com';
expect(openpgp.util.isUserId(data)).to.be.false;
});
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 empty string', function() {
const data = '';
expect(openpgp.util.isUserId(data)).to.be.false;
});
it('should return false for undefined', function() {
let data;
expect(openpgp.util.isUserId(data)).to.be.false;
});
it('should return false for Object', function() {
const data = {};
expect(openpgp.util.isUserId(data)).to.be.false;
});
});
describe('getTransferables', function() {
let zero_copyVal;
const buf1 = new Uint8Array(1);