Skip to content

Commit

Permalink
Require User IDs to be objects; refactor UserIDPacket (#1187)
Browse files Browse the repository at this point in the history
- `openpgp.generateKey` now expects `userIds` in object format
  (strings are no longer supported)
- Remove `util.parseUserId` and `util.formatUserId`
- Replace `UserIDPacket#format` with `UserIDPacket.fromObject`
  • Loading branch information
larabr committed Jan 13, 2021
1 parent cd82f87 commit cef1ab5
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 187 deletions.
25 changes: 11 additions & 14 deletions openpgp.d.ts
Expand Up @@ -14,10 +14,10 @@ export function readKey(data: Uint8Array): Promise<Key>;
export function readArmoredKeys(armoredText: string): Promise<Key[]>;
export function readKeys(data: Uint8Array): Promise<Key[]>;
export function generateKey(options: KeyOptions): Promise<KeyPair>;
export function generateSessionKey(options: { publicKeys: Key[], date?: Date, toUserIds?: UserId[] }): Promise<SessionKey>;
export function generateSessionKey(options: { publicKeys: Key[], date?: Date, toUserIds?: UserID[] }): Promise<SessionKey>;
export function decryptKey(options: { privateKey: Key; passphrase?: string | string[]; }): Promise<Key>;
export function encryptKey(options: { privateKey: Key; passphrase?: string | string[] }): Promise<Key>;
export function reformatKey(options: { privateKey: Key; userIds?: (string | UserId)[]; passphrase?: string; keyExpirationTime?: number; }): Promise<KeyPair>;
export function reformatKey(options: { privateKey: Key; userIds?: UserID|UserID[]; passphrase?: string; keyExpirationTime?: number; }): Promise<KeyPair>;

export class Key {
constructor(packetlist: PacketList<AnyPacket>);
Expand All @@ -29,7 +29,7 @@ export class Key {
public armor(): string;
public decrypt(passphrase: string | string[], keyId?: Keyid): Promise<void>; // throws on error
public encrypt(passphrase: string | string[]): Promise<void>; // throws on error
public getExpirationTime(capability?: 'encrypt' | 'encrypt_sign' | 'sign', keyId?: Keyid, userId?: UserId): Promise<Date | typeof Infinity | null>; // Returns null if `capabilities` is passed and the key does not have the specified capabilities or is revoked or invalid.
public getExpirationTime(capability?: 'encrypt' | 'encrypt_sign' | 'sign', keyId?: Keyid, userId?: UserID): Promise<Date | typeof Infinity | null>; // Returns null if `capabilities` is passed and the key does not have the specified capabilities or is revoked or invalid.
public getKeyIds(): Keyid[];
public getPrimaryUser(): Promise<PrimaryUser>; // throws on error
public getUserIds(): string[];
Expand All @@ -41,8 +41,8 @@ export class Key {
public isRevoked(): Promise<boolean>;
public revoke(reason: { flag?: enums.reasonForRevocation; string?: string; }, date?: Date): Promise<Key>;
public getRevocationCertificate(): Promise<Stream<string> | string | undefined>;
public getEncryptionKey(keyid?: Keyid, date?: Date | null, userId?: UserId): Promise<Key | SubKey>;
public getSigningKey(keyid?: Keyid, date?: Date | null, userId?: UserId): Promise<Key | SubKey>;
public getEncryptionKey(keyid?: Keyid, date?: Date | null, userId?: UserID): Promise<Key | SubKey>;
public getSigningKey(keyid?: Keyid, date?: Date | null, userId?: UserID): Promise<Key | SubKey>;
public getKeys(keyId?: Keyid): (Key | SubKey)[];
public isDecrypted(): boolean;
public getFingerprint(): string;
Expand Down Expand Up @@ -419,6 +419,7 @@ export class OnePassSignaturePacket extends BasePacket {
export class UserIDPacket extends BasePacket {
public tag: enums.packet.userID;
public userid: string;
static fromObject(userId: UserID): UserIDPacket;
}

export class SignaturePacket extends BasePacket {
Expand Down Expand Up @@ -530,7 +531,7 @@ export namespace stream {

/* ############## v5 GENERAL #################### */

export interface UserId { name?: string; email?: string; comment?: string; }
export interface UserID { name?: string; email?: string; comment?: string; }
export interface SessionKey { data: Uint8Array; algorithm: string; }


Expand Down Expand Up @@ -558,9 +559,9 @@ interface EncryptOptions {
/** (optional) use a key ID of 0 instead of the public key IDs */
wildcard?: boolean;
/** (optional) user ID to sign with, e.g. { name:'Steve Sender', email:'steve@openpgp.org' } */
fromUserId?: UserId;
fromUserId?: UserID;
/** (optional) user ID to encrypt for, e.g. { name:'Robert Receiver', email:'robert@openpgp.org' } */
toUserId?: UserId;
toUserId?: UserID;
}

interface DecryptOptions {
Expand Down Expand Up @@ -592,7 +593,7 @@ interface SignOptions {
dataType?: DataPacketType;
detached?: boolean;
date?: Date;
fromUserId?: UserId;
fromUserId?: UserID;
}

interface VerifyOptions {
Expand Down Expand Up @@ -620,7 +621,7 @@ interface KeyPair {
export type EllipticCurveName = 'ed25519' | 'curve25519' | 'p256' | 'p384' | 'p521' | 'secp256k1' | 'brainpoolP256r1' | 'brainpoolP384r1' | 'brainpoolP512r1';

interface KeyOptions {
userIds: UserId[]; // generating a key with no user defined results in error
userIds: UserID|UserID[];
passphrase?: string;
type?: 'ecc' | 'rsa';
curve?: EllipticCurveName;
Expand Down Expand Up @@ -890,10 +891,6 @@ declare namespace util {
*/
function hexToStr(hex: string): string;

function parseUserId(userid: string): UserId;

function formatUserId(userid: UserId): string;

function normalizeDate(date: Date | null): Date | null;

/**
Expand Down
4 changes: 1 addition & 3 deletions src/key/factory.js
Expand Up @@ -156,9 +156,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) {
return algos;
}

const userIdPacket = new UserIDPacket();
userIdPacket.format(userId);

const userIdPacket = UserIDPacket.fromObject(userId);
const dataToSign = {};
dataToSign.userId = userIdPacket;
dataToSign.key = secretKeyPacket;
Expand Down
4 changes: 2 additions & 2 deletions src/message.js
Expand Up @@ -289,7 +289,7 @@ export class Message {
* Generate a new session key object, taking the algorithm preferences of the passed public keys into account, if any.
* @param {Array<Key>} keys (optional) public key(s) to select algorithm preferences for
* @param {Date} date (optional) date to select algorithm preferences at
* @param {Array} userIds (optional) user IDs to select algorithm preferences for
* @param {Array<Object>} userIds (optional) user IDs to select algorithm preferences for
* @returns {Promise<{ data: Uint8Array, algorithm: String }>} object with session key data and algorithm
* @async
*/
Expand All @@ -309,7 +309,7 @@ export class Message {
* @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 {Array} userIds (optional) user IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }]
* @param {Array<Object>} userIds (optional) user IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }]
* @param {Boolean} streaming (optional) whether to process data as a stream
* @returns {Promise<Message>} new message with encrypted content
* @async
Expand Down
10 changes: 5 additions & 5 deletions src/openpgp.js
Expand Up @@ -64,7 +64,7 @@ if (globalThis.ReadableStream) {
/**
* Generates a new OpenPGP key pair. Supports RSA and ECC keys. By default, primary and subkeys will be of same type.
* @param {ecc|rsa} type (optional) The primary key algorithm type: ECC (default) or RSA
* @param {Array<String|Object>} userIds User IDs as strings or objects: 'Jo Doe <info@jo.com>' or { name:'Jo Doe', email:'info@jo.com' }
* @param {Object|Array<Object>} userIds User IDs as objects: { name:'Jo Doe', email:'info@jo.com' }
* @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key
* @param {Number} rsaBits (optional) Number of bits for RSA keys, defaults to 4096
* @param {String} curve (optional) Elliptic curve for ECC keys:
Expand Down Expand Up @@ -104,7 +104,7 @@ export function generateKey({ userIds = [], passphrase = "", type = "ecc", rsaBi
/**
* Reformats signature packets for a key and rewraps key object.
* @param {Key} privateKey Private key to reformat
* @param {Array<String|Object>} userIds User IDs as strings or objects: 'Jo Doe <info@jo.com>' or { name:'Jo Doe', email:'info@jo.com' }
* @param {Object|Array<Object>} userIds User IDs as objects: { name:'Jo Doe', email:'info@jo.com' }
* @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key
* @param {Number} keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires
* @returns {Promise<Object>} The generated key object in the form:
Expand Down Expand Up @@ -222,8 +222,8 @@ export function encryptKey({ privateKey, passphrase }) {
* @param {Signature} signature (optional) a detached signature to add to the encrypted message
* @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 signature
* @param {Array} fromUserIds (optional) array of user IDs to sign with, one per key in `privateKeys`, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @param {Array} toUserIds (optional) array of user IDs to encrypt for, one per key in `publicKeys`, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }]
* @param {Array<Object>} fromUserIds (optional) array of user IDs to sign with, one per key in `privateKeys`, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @param {Array<Object>} toUserIds (optional) array of user IDs to encrypt for, one per key in `publicKeys`, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }]
* @returns {Promise<String|ReadableStream<String>|NodeStream<String>|Uint8Array|ReadableStream<Uint8Array>|NodeStream<Uint8Array>>} (String if `armor` was true, the default; Uint8Array if `armor` was false)
* @async
* @static
Expand Down Expand Up @@ -312,7 +312,7 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe
* @param {'web'|'ponyfill'|'node'|false} streaming (optional) whether to return data as a stream. Defaults to the type of stream `message` was created from, if any.
* @param {Boolean} detached (optional) if the return value should contain a detached signature
* @param {Date} date (optional) override the creation date of the signature
* @param {Array} fromUserIds (optional) array of user IDs to sign with, one per key in `privateKeys`, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @param {Array<Object>} fromUserIds (optional) array of user IDs to sign with, one per key in `privateKeys`, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @returns {Promise<String|ReadableStream<String>|NodeStream<String>|Uint8Array|ReadableStream<Uint8Array>|NodeStream<Uint8Array>>} (String if `armor` was true, the default; Uint8Array if `armor` was false)
* @async
* @static
Expand Down
50 changes: 32 additions & 18 deletions src/packet/userid.js
Expand Up @@ -19,9 +19,11 @@
* @requires enums
* @requires util
*/
import emailAddresses from 'email-addresses';

import enums from '../enums';
import util from '../util';
import config from '../config';

/**
* Implementation of the User ID Packet (Tag 13)
Expand All @@ -48,19 +50,42 @@ class UserIDPacket {
}

/**
* Parsing function for a user id packet (tag 13).
* @param {Uint8Array} input payload of a tag 13 packet
* Create UserIDPacket instance from object
* @param {Object} userId object specifying userId name, email and comment
* @returns {module:userid.UserIDPacket}
* @static
*/
read(bytes) {
this.parse(util.decodeUtf8(bytes));
static fromObject(userId) {
if (util.isString(userId) ||
(userId.name && !util.isString(userId.name)) ||
(userId.email && !util.isEmailAddress(userId.email)) ||
(userId.comment && !util.isString(userId.comment))) {
throw new Error('Invalid user ID format');
}
const packet = new UserIDPacket();
Object.assign(packet, userId);
const components = [];
if (packet.name) components.push(packet.name);
if (packet.comment) components.push(`(${packet.comment})`);
if (packet.email) components.push(`<${packet.email}>`);
packet.userid = components.join(' ');
return packet;
}

/**
* Parse userid string, e.g. 'John Doe <john@example.com>'
* Parsing function for a user id packet (tag 13).
* @param {Uint8Array} input payload of a tag 13 packet
*/
parse(userid) {
read(bytes) {
const userid = util.decodeUtf8(bytes);
if (userid.length > config.maxUseridLength) {
throw new Error('User ID string is too long');
}
try {
Object.assign(this, util.parseUserId(userid));
const { name, address: email, comments } = emailAddresses.parseOneAddress({ input: userid, atInDisplayName: true });
this.comment = comments.replace(/^\(|\)$/g, '');
this.name = name;
this.email = email;
} catch (e) {}
this.userid = userid;
}
Expand All @@ -72,17 +97,6 @@ class UserIDPacket {
write() {
return util.encodeUtf8(this.userid);
}

/**
* Set userid string from object, e.g. { name:'Phil Zimmermann', email:'phil@openpgp.org' }
*/
format(userid) {
if (util.isString(userid)) {
userid = util.parseUserId(userid);
}
Object.assign(this, userid);
this.userid = util.formatUserId(userid);
}
}

export default UserIDPacket;
39 changes: 0 additions & 39 deletions src/util.js
Expand Up @@ -26,7 +26,6 @@
* @module util
*/

import emailAddresses from 'email-addresses';
import stream from 'web-stream-tools';
import config from './config';
import util from './util'; // re-import module to access util functions
Expand Down Expand Up @@ -598,44 +597,6 @@ export default {
return re.test(data);
},

/**
* Format user id for internal use.
*/
formatUserId: function(id) {
// name, email address and comment can be empty but must be of the correct type
if ((id.name && !util.isString(id.name)) ||
(id.email && !util.isEmailAddress(id.email)) ||
(id.comment && !util.isString(id.comment))) {
throw new Error('Invalid user id format');
}
const components = [];
if (id.name) {
components.push(id.name);
}
if (id.comment) {
components.push(`(${id.comment})`);
}
if (id.email) {
components.push(`<${id.email}>`);
}
return components.join(' ');
},

/**
* Parse user id.
*/
parseUserId: function(userid) {
if (userid.length > config.maxUseridLength) {
throw new Error('User id string is too long');
}
try {
const { name, address: email, comments } = emailAddresses.parseOneAddress({ input: userid, atInDisplayName: true });
return { name, email, comment: comments.replace(/^\(|\)$/g, '') };
} catch (e) {
throw new Error('Invalid user id format');
}
},

/**
* Normalize line endings to <CR><LF>
* Support any encoding where CR=0x0D, LF=0x0A
Expand Down

0 comments on commit cef1ab5

Please sign in to comment.