Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v5] Require User IDs to be objects; refactor UserIDPacket #1187

Merged
merged 5 commits into from Jan 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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