Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -436,17 +436,17 @@ export class ComposeRenderModule extends ViewModule<ComposeView> {
return; // key is invalid
}
const key = await KeyUtil.parse(normalizedPub);
if (!key.emails.length) {
// no users is not desired
const firstUserWithEmail = key.users.find(u => u.email);
if (!firstUserWithEmail?.email) {
await Ui.modal.warning(`There are no email addresses listed in this Public Key - don't know who this key belongs to.`);
return;
}
await ContactStore.update(undefined, key.emails[0], {
name: Str.parseEmail(key.identities[0]).name,
await ContactStore.update(undefined, firstUserWithEmail.email, {
name: firstUserWithEmail.name,
pubkey: normalizedPub,
pubkeyLastCheck: Date.now(),
});
this.view.S.cached('input_to').val(key.emails[0]);
this.view.S.cached('input_to').val(firstUserWithEmail.email);
await this.view.recipientsModule.parseRenderRecipients(this.view.S.cached('input_to'));
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class GeneralMailFormatter {
// This is important to consider when an email address with a email tag is present on the
// signingKey, as technically it is the same email.
const baseSenderEmail = senderEmail.includes('+') ? senderEmail.replace(/(.+)(?=\+).*(?=@)/, '$1') : senderEmail;
return signingKey.emails.some(email => email.includes(baseSenderEmail));
return signingKey.users.some(u => u.email?.includes(baseSenderEmail));
}
return true;
};
Expand Down
10 changes: 5 additions & 5 deletions extension/chrome/elements/pgp_pubkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ View.run(
} else {
let emailText = '';
if (this.parsedPublicKeys.length === 1) {
const email = this.firstParsedPublicKey.emails[0];
const email = KeyUtil.getPrimaryEmail(this.firstParsedPublicKey);
if (email) {
emailText = email;
$('.input_email').val(email); // checked above
Expand All @@ -90,7 +90,7 @@ View.run(
Xss.escape(
' for ' +
this.parsedPublicKeys
.map(pub => pub.emails[0])
.map(pub => KeyUtil.getPrimaryEmail(pub))
.filter(e => !!e)
.join(', ')
)
Expand Down Expand Up @@ -193,9 +193,9 @@ View.run(
$('.error_introduce_label').html(`This OpenPGP key is not usable.<br/><small>(${await this.getErrorText()})</small>`); // xss-escaped
$('.hide_if_error').hide();
$('.fingerprints, .add_contact, #manual_import_warning').remove();
const email = this.firstParsedPublicKey?.emails[0];
const email = this.firstParsedPublicKey ? KeyUtil.getPrimaryEmail(this.firstParsedPublicKey) : undefined;
if (email) {
$('.error_container .input_error_email').val(`${this.firstParsedPublicKey?.emails[0]}`);
$('.error_container .input_error_email').val(email);
} else {
$('.error_container .input_error_email').hide();
}
Expand All @@ -208,7 +208,7 @@ View.run(
const emails = new Set<string>();
for (const pubkey of this.parsedPublicKeys!) {
/* eslint-enable @typescript-eslint/no-non-null-assertion */
const email = pubkey.emails[0];
const email = KeyUtil.getPrimaryEmail(pubkey);
if (email) {
await ContactStore.update(undefined, email, { pubkey: KeyUtil.armor(pubkey) });
emails.add(email);
Expand Down
2 changes: 1 addition & 1 deletion extension/chrome/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ View.run(
removeKeyBtn = `<a href="#" class="action_remove_key" style="margin-left: 5px;" data-test="action-remove-key-${originalIndex}" data-fingerprint=${ki.fingerprints[0]} data-type="${ki.family}" data-id="${ki.id}" data-longid="${ki.longid}">remove</a>`;
}

const escapedEmail = Xss.escape(prv.emails[0] || '');
const escapedEmail = Xss.escape(KeyUtil.getPrimaryEmail(prv) || '');
const escapedLink = `<a href="#" data-test="action-show-key-${originalIndex}" class="action_show_key" page="modules/my_key.htm" addurltext="&fingerprint=${ki.id}">${escapedEmail}</a>`;
const fpHtml = `<span class="good" style="font-family: monospace;">${Str.spaced(Xss.escape(ki.fingerprints[0]))}</span>`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export class AddKeyGenerateModule extends ViewModule<AddKeyView> {
const adminPubkey = this.view.clientConfiguration.getPublicKeyForPrivateKeyBackupToDesignatedMailbox();
if (adminPubkey) {
const msgEncryptionKey = await KeyUtil.parse(adminPubkey);
const destinationEmail = msgEncryptionKey.emails[0];
const destinationEmail = KeyUtil.getPrimaryEmail(msgEncryptionKey);
if (!destinationEmail) {
throw new Error('Admin public key does not have an email address');
}
try {
const privateKey = await KeyStore.get(this.view.acctEmail);
const primaryKeyId = privateKey[0].id;
Expand Down
2 changes: 1 addition & 1 deletion extension/chrome/settings/modules/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ View.run(
[
`Type: ${key.family}`,
`Fingerprint: ${Str.spaced(key.id || 'none')}`,
`Users: ${key.emails?.join(', ')}`,
`Users: ${key.users?.map(u => u.email).filter(Boolean).join(', ')}`,
`Created on: ${key.created ? new Date(key.created) : ''}`,
`Expiration: ${key.expiration ? new Date(key.expiration) : 'Does not expire'}`,
`Last signature: ${key.lastModified ? new Date(key.lastModified) : ''}`,
Expand Down
3 changes: 2 additions & 1 deletion extension/chrome/settings/modules/my_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ View.run(
$('.action_view_user_ids').attr('href', this.myKeyUserIdsUrl);
$('.action_view_update').attr('href', this.myKeyUpdateUrl);
$('.fingerprint').text(Str.spaced(this.keyInfo.fingerprints[0]));
Xss.sanitizeRender('.email', this.pubKey.emails.map(email => `<span>${Xss.escape(email)}</span>`).join(', '));
const emails = this.pubKey.users.map(u => u.email).filter((e): e is string => !!e);
Xss.sanitizeRender('.email', emails.map(email => `<span>${Xss.escape(email)}</span>`).join(', '));
const expiration = this.pubKey.expiration;
const creation = Str.datetimeToDate(Str.fromDate(new Date(this.pubKey.created)));
Xss.sanitizeRender('.key_status_contatiner', KeyUtil.statusHtml(this.keyInfo.longid, this.pubKey));
Expand Down
2 changes: 1 addition & 1 deletion extension/chrome/settings/modules/my_key_update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ View.run(
KeyImportUi.allowReselect();
if (typeof updatedKey === 'undefined') {
await Ui.modal.warning(Lang.setup.keyFormattedWell(this.prvHeaders.begin, String(this.prvHeaders.end)), Ui.getTestCompatibilityLink(this.acctEmail));
} else if (updatedKeyEncrypted.identities.length === 0) {
} else if (updatedKeyEncrypted.users.length === 0) {
throw new KeyCanBeFixed(updatedKeyEncrypted);
} else if (updatedKey.isPublic) {
await Ui.modal.warning(
Expand Down
2 changes: 1 addition & 1 deletion extension/chrome/settings/modules/my_key_user_ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ View.run(
Assert.abortAndRenderErrorIfKeyinfoEmpty(this.ki ? [this.ki] : []);
$('.action_show_public_key').attr('href', this.myKeyUrl);
const prv = await KeyUtil.parse(this.ki.private);
Xss.sanitizeRender('.user_ids', prv.identities.map((uid: string) => `<div>${Xss.escape(uid)}</div>`).join(''));
Xss.sanitizeRender('.user_ids', prv.users.map(u => `<div>${Xss.escape(u.full)}</div>`).join(''));
$('.email').text(this.acctEmail);
$('.fingerprint').text(Str.spaced(this.ki.fingerprints[0]));
};
Expand Down
5 changes: 4 additions & 1 deletion extension/chrome/settings/setup/setup-create-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export class SetupCreateKeyModule {
const adminPubkey = this.view.clientConfiguration.getPublicKeyForPrivateKeyBackupToDesignatedMailbox();
if (adminPubkey) {
const msgEncryptionKey = await KeyUtil.parse(adminPubkey);
const destinationEmail = msgEncryptionKey.emails[0];
const destinationEmail = KeyUtil.getPrimaryEmail(msgEncryptionKey);
if (!destinationEmail) {
throw new Error('Admin public key does not have an email address');
}
try {
const privateKey = await KeyStore.get(this.view.acctEmail);
const primaryKeyId = privateKey[0].id;
Expand Down
3 changes: 2 additions & 1 deletion extension/js/common/core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type UrlParam = string | number | null | undefined | boolean | string[];
export type UrlParams = Dict<UrlParam>;
export type PromiseCancellation = { cancel: boolean };
export type EmailParts = { email: string; name?: string };
export type ParsedEmail = { email: string | undefined; name: string | undefined; full: string };

export const CID_PATTERN = /^cid:(.+)/;

Expand Down Expand Up @@ -88,7 +89,7 @@ export class Str {
public static readonly ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0370-\u0590\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF';
public static readonly rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC';

public static parseEmail = (full: string, flag: 'VALIDATE' | 'DO-NOT-VALIDATE' = 'VALIDATE') => {
public static parseEmail = (full: string, flag: 'VALIDATE' | 'DO-NOT-VALIDATE' = 'VALIDATE'): ParsedEmail => {
let email: string | undefined;
let name: string | undefined;
if (full.includes('<') && full.includes('>')) {
Expand Down
18 changes: 11 additions & 7 deletions extension/js/common/core/crypto/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { OpenPGPKey } from './pgp/openpgp-key.js';
import type * as OpenPGP from 'openpgp';
import { SmimeKey } from './smime/smime-key.js';
import { MsgBlock } from '../msg-block.js';
import { EmailParts, Str } from '../common.js';
import { EmailParts, ParsedEmail, Str } from '../common.js';

/**
* This is a common Key interface for both OpenPGP and X.509 keys.
Expand All @@ -38,8 +38,7 @@ export interface Key extends KeyIdentity {
usableForSigningButExpired: boolean;
missingPrivateKeyForSigning: boolean;
missingPrivateKeyForDecryption: boolean;
emails: string[];
identities: string[];
users: ParsedEmail[];
fullyDecrypted: boolean;
fullyEncrypted: boolean;
isPublic: boolean; // isPublic and isPrivate are mutually exclusive
Expand Down Expand Up @@ -122,13 +121,14 @@ export class KeyUtil {

public static filterKeysByTypeAndSenderEmail(keys: KeyInfoWithIdentity[], email: string, type: 'openpgp' | 'x509' | undefined): KeyInfoWithIdentity[] {
let foundKeys: KeyInfoWithIdentity[] = [];
const lowerEmail = email.toLowerCase();
if (type) {
foundKeys = keys.filter(key => key.emails?.includes(email.toLowerCase()) && key.family === type);
foundKeys = keys.filter(key => key.emails?.includes(lowerEmail) && key.family === type);
if (!foundKeys.length) {
foundKeys = keys.filter(key => key.family === type);
}
} else {
foundKeys = keys.filter(key => key.emails?.includes(email.toLowerCase()));
foundKeys = keys.filter(key => key.emails?.includes(lowerEmail));
if (!foundKeys.length) {
foundKeys = [...keys];
}
Expand Down Expand Up @@ -440,7 +440,7 @@ export class KeyUtil {
private: KeyUtil.armor(prv),
public: KeyUtil.armor(pubkey),
longid: KeyUtil.getPrimaryLongid(pubkey),
emails: prv.emails,
emails: prv.users.map(u => u.email).filter((e): e is string => !!e),
fingerprints: prv.allIds,
id: prv.id,
family: prv.family,
Expand All @@ -461,6 +461,10 @@ export class KeyUtil {
return SmimeKey.getKeyLongid(pubkey);
}

public static getPrimaryEmail(key: Key): string | undefined {
return key.users.find(user => user.email)?.email;
}

public static getKeyInfoLongids(ki: KeyInfoWithIdentityAndOptionalPp): string[] {
if (ki.family !== 'x509') {
return ki.fingerprints.map(fp => OpenPGPKey.fingerprintToLongid(fp));
Expand Down Expand Up @@ -488,7 +492,7 @@ export class KeyUtil {

public static async parseAndArmorKeys(binaryKeysData: Uint8Array): Promise<ArmoredKeyIdentityWithEmails[]> {
const { keys } = await KeyUtil.readMany(Buf.fromUint8(binaryKeysData));
return keys.map(k => ({ id: k.id, emails: k.emails, armored: KeyUtil.armor(k), family: k.family }));
return keys.map(k => ({ id: k.id, emails: k.users.map(u => u.email).filter((e): e is string => !!e), armored: KeyUtil.armor(k), family: k.family }));
}

public static validateChecksum(armoredText: string): boolean {
Expand Down
28 changes: 11 additions & 17 deletions extension/js/common/core/crypto/pgp/openpgp-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Key, PrvPacket, KeyAlgo, KeyUtil, UnexpectedKeyTypeError, PubkeyInfo } from '../key.js';
import { OpenPGPDataType, opgp } from './openpgpjs-custom.js';
import { Catch } from '../../../platform/catch.js';
import { Str, Value } from '../../common.js';
import { ParsedEmail, Str, Value } from '../../common.js';
import { Buf } from '../../buf.js';
import type * as OpenPGP from 'openpgp';
import { PgpMsgMethod, VerifyRes } from './msg-util.js';
Expand Down Expand Up @@ -188,7 +188,7 @@ export class OpenPGPKey {
* is done on the original supplied object.
*/
public static convertExternalLibraryObjToKey = async (opgpKey: OpenPGP.Key, keyToUpdate?: Key): Promise<Key> => {
const { identities, emails } = await OpenPGPKey.getSortedUserids(opgpKey);
const users = await OpenPGPKey.getSortedUserids(opgpKey);
let lastModified: undefined | number;
try {
lastModified = await OpenPGPKey.getLastSigTime(opgpKey);
Expand Down Expand Up @@ -221,10 +221,7 @@ export class OpenPGPKey {
usableForSigningButExpired: !signingKey && !!signingKeyIgnoringExpiration,
missingPrivateKeyForSigning,
missingPrivateKeyForDecryption,
// valid emails extracted from uids
emails,
// full uids that have valid emails in them
identities,
users,
lastModified,
expiration,
created: opgpKey.getCreationTime().getTime(),
Expand Down Expand Up @@ -541,28 +538,25 @@ export class OpenPGPKey {
return expirationTime instanceof Date ? expirationTime : undefined; // we don't differ between Infinity and null
}

private static async getSortedUserids(key: OpenPGP.Key) {
private static async getSortedUserids(key: OpenPGP.Key): Promise<ParsedEmail[]> {
const primaryUser = await Catch.undefinedOnException(key.getPrimaryUser());
// if there is no good enough user id to serve as primary identity, we assume other user ids are even worse
if (primaryUser?.user?.userID?.userID) {
const primaryUserId = primaryUser.user.userID.userID;
const identities = [
const rawIdentities = [
primaryUserId, // put the "primary" identity first
// other identities go in indeterministic order
...Value.arr.unique((await key.verifyAllUsers()).filter(x => x.valid && x.userID !== primaryUserId).map(x => x.userID)),
];
const emails = identities
.filter(userId => !!userId)
.map(userid => Str.parseEmail(userid).email)
.filter(Boolean);
if (emails.length === identities.length || !key.isPrivate()) {
// OpenPGP.js uses RFC 5322 `email-addresses` parser, so we expect all identities to contain a valid e-mail address
// Return empty emails and identities is a way to later throw KeyCanBeFixed exception, allowing the user to reformat the key
const users = rawIdentities.filter(uid => !!uid).map(uid => Str.parseEmail(uid));
if ((users.length === rawIdentities.length && users.every(u => u.email)) || !key.isPrivate()) {
// OpenPGP.js uses RFC 5322 `email-addresses` parser, so we expect all identities to contain a valid e-mail address.
// Returning empty users is a way to later throw KeyCanBeFixed exception, allowing the user to reformat the key
// (e.g. adding signature for UserIDs etc.) But this makes sense for private keys only.
return { emails, identities };
return users;
}
}
return { emails: [], identities: [] };
return [];
}

// mimicks OpenPGP.helper.getLatestValidSignature
Expand Down
28 changes: 14 additions & 14 deletions extension/js/common/core/crypto/smime/smime-key.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
import { Key, UnexpectedKeyTypeError } from '../key.js';
import { Str } from '../../common.js';
import { ParsedEmail, Str } from '../../common.js';
import { UnreportableError } from '../../../platform/error-report.js';
import { PgpArmor } from '../pgp/pgp-armor.js';
import { Buf } from '../../buf.js';
Expand Down Expand Up @@ -242,23 +242,24 @@ export class SmimeKey {
return parsed.filter((c, i) => !parsed.some((other, j) => j !== i && other.certificate.isIssuer(c.certificate)));
}

private static getNormalizedEmailsFromCertificate(certificate: forge.pki.Certificate): string[] {
private static getUsersFromCertificate(certificate: forge.pki.Certificate): ParsedEmail[] {
const emailFromSubject = (certificate.subject.getField('CN') as { value: string }).value;
const normalizedEmail = Str.parseEmail(emailFromSubject).email;
const emails = normalizedEmail ? [normalizedEmail] : [];
const parsedSubject = Str.parseEmail(emailFromSubject);
const users: ParsedEmail[] = parsedSubject.email ? [parsedSubject] : [];
// search for e-mails in subjectAltName extension
const subjectAltName = certificate.getExtension('subjectAltName') as {
altNames: { type: number; value: string }[];
};
if (subjectAltName?.altNames) {
const emailsFromAltNames = subjectAltName.altNames
.filter(entry => entry.type === 1)
.map(entry => Str.parseEmail(entry.value).email)
.filter(Boolean);
emails.push(...(emailsFromAltNames as string[]));
for (const entry of subjectAltName.altNames.filter(e => e.type === 1)) {
const parsed = Str.parseEmail(entry.value);
if (parsed.email && !users.some(u => u.email === parsed.email)) {
users.push(parsed);
}
}
}
if (emails.length) {
return emails.filter((value, index, self) => self.indexOf(value) === index);
if (users.length) {
return users;
}
throw new UnreportableError(`This S/MIME x.509 certificate has an invalid recipient email: ${emailFromSubject}`);
}
Expand All @@ -282,7 +283,7 @@ export class SmimeKey {
}
}
const fingerprint = this.forge.pki.getPublicKeyFingerprint(certificate.publicKey, { encoding: 'hex' }).toUpperCase();
const emails = SmimeKey.getNormalizedEmailsFromCertificate(certificate);
const users = SmimeKey.getUsersFromCertificate(certificate);
const issuerAndSerialNumberAsn1 = SmimeKey.createIssuerAndSerialNumberAsn1(
this.forge.pki.distinguishedNameToAsn1(certificate.issuer),
certificate.serialNumber
Expand All @@ -300,8 +301,7 @@ export class SmimeKey {
usableForSigning: usableIgnoringExpiration && !expired,
usableForEncryptionButExpired: usableIgnoringExpiration && expired,
usableForSigningButExpired: usableIgnoringExpiration && expired,
emails,
identities: emails,
users,
created: SmimeKey.dateToNumber(certificate.validity.notBefore),
lastModified: SmimeKey.dateToNumber(certificate.validity.notBefore),
expiration,
Expand Down
2 changes: 1 addition & 1 deletion extension/js/common/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export class Settings {
passphrase: string,
backUrl: string
): Promise<Key> {
const uids = origPrv.identities;
const uids = origPrv.users.map(u => u.full);
if (!uids.length) {
uids.push(acctEmail);
}
Expand Down
7 changes: 4 additions & 3 deletions extension/js/common/ui/key-import-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,10 @@ export class KeyImportUi {
const prv = await Catch.undefinedOnException(KeyUtil.parse(String($(target).val())));
if (prv !== undefined) {
$('.action_add_private_key').removeClass('btn_disabled').removeAttr('disabled');
for (const email of prv.emails) {
for (const user of prv.users) {
if (!user.email) continue;
for (const inputCheckboxesWithEmail of $('.input_email_alias_submit_pubkey')) {
if (String($(inputCheckboxesWithEmail).data('email')) === email) {
if (String($(inputCheckboxesWithEmail).data('email')) === user.email) {
$(inputCheckboxesWithEmail).prop('checked', true);
}
}
Expand Down Expand Up @@ -265,7 +266,7 @@ export class KeyImportUi {
await this.decryptAndEncryptAsNeeded(decrypted, encrypted, passphrase, contactSubsentence);
await this.checkEncryptionPrvIfSelected(decrypted, encrypted, contactSubsentence);
await this.checkSigningIfSelected(decrypted, contactSubsentence);
if (encrypted.identities.length === 0) {
if (encrypted.users.length === 0) {
throw new KeyCanBeFixed(encrypted);
}
// mandatory checks have passed, now display warnings
Expand Down
Loading
Loading