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
1 change: 1 addition & 0 deletions extension/chrome/settings/modules/add_key.htm
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ <h1>Add Private Key <span id="spinner_container"></span></h1>
<script src="/lib/jquery.min.js"></script>
<script src="/lib/sweetalert2.js"></script>
<script src="/lib/openpgp.js"></script>
<script src="/lib/forge.js"></script>
<script src="/lib/zxcvbn.js"></script>
<script src="add_key.js" type="module"></script>
</body>
Expand Down
11 changes: 6 additions & 5 deletions extension/chrome/settings/setup.htm
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,12 @@ <h1></h1>
</div>
<div class="line left">
<ul style="list-style: none; margin-left: 0; padding-left: 0;" class="source_selector">
<li><input type="radio" name="source" id="source_file" value="file"> <label for="source_file">Load from a
file</label></li>
<li><input type="radio" name="source" id="source_paste" value="paste"
data-test="input-step2bmanualenter-source-paste">
<label for="source_paste">Paste armored key directly</label></li>
<li><input type="radio" name="source" id="source_file" value="file" data-test="input-step2bmanualenter-file">
<label for="source_file">Load from a file</label>
</li>
<li><input type="radio" name="source" id="source_paste" value="paste" data-test="input-step2bmanualenter-source-paste">
<label for="source_paste">Paste armored key directly</label>
</li>
</ul>
</div>
<div class="source_file_container display_none">
Expand Down
43 changes: 39 additions & 4 deletions extension/js/common/core/crypto/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ export interface Key {
identities: string[];
fullyDecrypted: boolean;
fullyEncrypted: boolean;
// TODO: Aren't isPublic and isPrivate mutually exclusive?
isPublic: boolean;
isPrivate: boolean;
isPublic: boolean; // isPublic and isPrivate are mutually exclusive
isPrivate: boolean; // only one should be set to true
algo: {
algorithm: string,
curve?: string,
Expand Down Expand Up @@ -141,6 +140,40 @@ export class KeyUtil {
throw new UnexpectedKeyTypeError(`Key type is ${keyType}, expecting OpenPGP or x509 S/MIME`);
}

public static parseBinary = async (key: Uint8Array, passPhrase: string): Promise<Key[]> => {
const allKeys: Key[] = [], allErr: Error[] = [];
try {
const { keys, err } = await opgp.key.read(key);
if (keys.length > 0) {
for (const key of keys) {
// we should decrypt them all here to have consistent behavior between pkcs12 files and PGP
// pkcs12 files must be decrypted during parsing
// then rename this method to parseDecryptBinary
const parsed = await OpenPGPKey.convertExternalLibraryObjToKey(key);
// if (await KeyUtil.decrypt(parsed, passPhrase, undefined, 'OK-IF-ALREADY-DECRYPTED')) {
allKeys.push(parsed);
// } else {
// allErr.push(new Error(`Wrong pass phrase for OpenPGP key ${parsed.id} (${parsed.emails[0]})`));
// }
}
}
if (err) {
allErr.push(...err);
}
} catch (e) {
allErr.push(e as Error);
}
try {
allKeys.push(await SmimeKey.parseDecryptBinary(key, passPhrase));
} catch (e) {
allErr.push(e as Error);
}
if (allKeys.length > 0) {
return allKeys;
}
throw new Error(allErr ? allErr.map((err, i) => (i + 1) + '. ' + err.message).join('\n') : 'Should not happen: no keys and no errors.');
}

public static armor = (pubkey: Key): string => {
if (pubkey.type === 'openpgp') {
return OpenPGPKey.armor(pubkey);
Expand Down Expand Up @@ -236,7 +269,9 @@ export class KeyUtil {
}

public static getKeyType = (pubkey: string): 'openpgp' | 'x509' | 'unknown' => {
if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) {
if (pubkey.startsWith(PgpArmor.headers('certificate').begin)) {
return 'x509';
} else if (pubkey.startsWith(PgpArmor.headers('pkcs12').begin)) {
return 'x509';
} else if (pubkey.startsWith(PgpArmor.headers('publicKey').begin)) {
return 'openpgp';
Expand Down
1 change: 1 addition & 0 deletions extension/js/common/core/crypto/pgp/pgp-armor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class PgpArmor {
null: { begin: '-----BEGIN', end: '-----END', replace: false },
publicKey: { begin: '-----BEGIN PGP PUBLIC KEY BLOCK-----', end: '-----END PGP PUBLIC KEY BLOCK-----', replace: true },
privateKey: { begin: '-----BEGIN PGP PRIVATE KEY BLOCK-----', end: '-----END PGP PRIVATE KEY BLOCK-----', replace: true },
pkcs12: { begin: '-----BEGIN PKCS12 FILE-----', end: '-----BEGIN PKCS12 FILE-----', replace: true }, // custom format - Base64 dump of pkcs12 file bytes
certificate: { begin: '-----BEGIN CERTIFICATE-----', end: '-----END CERTIFICATE-----', replace: true },
signedMsg: { begin: '-----BEGIN PGP SIGNED MESSAGE-----', middle: '-----BEGIN PGP SIGNATURE-----', end: '-----END PGP SIGNATURE-----', replace: true },
signature: { begin: '-----BEGIN PGP SIGNATURE-----', end: '-----END PGP SIGNATURE-----', replace: false },
Expand Down
72 changes: 65 additions & 7 deletions extension/js/common/core/crypto/smime/smime-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,39 @@ import * as forge from 'node-forge';
import { Key, KeyUtil } from '../key.js';
import { Str } from '../../common.js';
import { UnreportableError } from '../../../platform/catch.js';
import { PgpArmor } from '../pgp/pgp-armor.js';
import { Buf } from '../../buf.js';

export class SmimeKey {

public static parse = async (text: string): Promise<Key> => {
const certificate = forge.pki.certificateFromPem(text);
SmimeKey.removeWeakKeys(certificate);
if (text.includes(PgpArmor.headers('certificate').begin)) {
return SmimeKey.parsePemCertificate(text);
} else if (text.includes(PgpArmor.headers('pkcs12').begin)) {
const armoredBytes = text.replace(PgpArmor.headers('pkcs12').begin, '').replace(PgpArmor.headers('pkcs12').end, '').trim();
const emptyPassPhrase = '';
return await SmimeKey.parseDecryptBinary(Buf.fromBase64Str(armoredBytes), emptyPassPhrase);
} else {
throw new Error('Could not parse S/MIME key without known headers');
}
}

public static parseDecryptBinary = async (buffer: Uint8Array, password: string): Promise<Key> => {
const bytes = String.fromCharCode.apply(undefined, new Uint8Array(buffer) as unknown as number[]) as string;
const p12Asn1 = forge.asn1.fromDer(bytes);
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);
const bags = p12.getBags({ bagType: forge.pki.oids.certBag });
if (!bags) {
throw new Error('No user certificate found.');
}
const bag = bags[forge.pki.oids.certBag];
if (!bag) {
throw new Error('No user certificate found.');
}
const certificate = bag[0]?.cert;
if (!certificate) {
throw new Error('No user certificate found.');
}
const email = (certificate.subject.getField('CN') as { value: string }).value;
const normalizedEmail = Str.parseEmail(email).email;
if (!normalizedEmail) {
Expand All @@ -18,21 +45,22 @@ export class SmimeKey {
type: 'x509',
id: certificate.serialNumber.toUpperCase(),
allIds: [certificate.serialNumber.toUpperCase()],
usableForEncryption: certificate.publicKey && SmimeKey.isEmailCertificate(certificate),
usableForSigning: certificate.publicKey && SmimeKey.isEmailCertificate(certificate),
usableForEncryption: SmimeKey.isEmailCertificate(certificate),
usableForSigning: SmimeKey.isEmailCertificate(certificate),
usableForEncryptionButExpired: false,
usableForSigningButExpired: false,
emails: [normalizedEmail],
identities: [normalizedEmail],
created: SmimeKey.dateToNumber(certificate.validity.notBefore),
lastModified: SmimeKey.dateToNumber(certificate.validity.notBefore),
expiration: SmimeKey.dateToNumber(certificate.validity.notAfter),
fullyDecrypted: false,
fullyDecrypted: true,
fullyEncrypted: false,
isPublic: true,
isPublic: false, // the way isPublic is currently used is as opposite of isPrivate, even if the Key contains both
isPrivate: true,
} as Key;
(key as unknown as { rawArmored: string }).rawArmored = text;
const headers = PgpArmor.headers('pkcs12');
(key as unknown as { raw: string }).raw = `${headers.begin}\n${forge.util.encode64(bytes)}\n${headers.end}`;
return key;
}

Expand All @@ -56,6 +84,36 @@ export class SmimeKey {
return { data: new Uint8Array(arr), type: 'smime' };
}

private static parsePemCertificate = (text: string): Key => {
const certificate = forge.pki.certificateFromPem(text);
SmimeKey.removeWeakKeys(certificate);
const email = (certificate.subject.getField('CN') as { value: string }).value;
const normalizedEmail = Str.parseEmail(email).email;
if (!normalizedEmail) {
throw new UnreportableError(`This S/MIME x.509 certificate has an invalid recipient email: ${email}`);
}
const key = {
type: 'x509',
id: certificate.serialNumber.toUpperCase(),
allIds: [certificate.serialNumber.toUpperCase()],
usableForEncryption: certificate.publicKey && SmimeKey.isEmailCertificate(certificate),
usableForSigning: certificate.publicKey && SmimeKey.isEmailCertificate(certificate),
usableForEncryptionButExpired: false,
usableForSigningButExpired: false,
emails: [normalizedEmail],
identities: [normalizedEmail],
created: SmimeKey.dateToNumber(certificate.validity.notBefore),
lastModified: SmimeKey.dateToNumber(certificate.validity.notBefore),
expiration: SmimeKey.dateToNumber(certificate.validity.notAfter),
fullyDecrypted: false,
fullyEncrypted: false,
isPublic: true,
isPrivate: true,
} as Key;
(key as unknown as { rawArmored: string }).rawArmored = text;
return key;
}

private static removeWeakKeys = (certificate: forge.pki.Certificate) => {
const publicKeyN = (certificate.publicKey as forge.pki.rsa.PublicKey)?.n;
if (publicKeyN && publicKeyN.bitLength() < 2048) {
Expand Down
2 changes: 1 addition & 1 deletion extension/js/common/core/msg-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DecryptError, VerifyRes } from './crypto/pgp/msg-util.js';
import { AttachmentMeta } from './attachment.js';
import { Buf } from './buf.js';

export type KeyBlockType = 'publicKey' | 'privateKey' | 'certificate';
export type KeyBlockType = 'publicKey' | 'privateKey' | 'certificate' | 'pkcs12';
export type ReplaceableMsgBlockType = KeyBlockType | 'signedMsg' | 'encryptedMsg';
export type MsgBlockType = ReplaceableMsgBlockType | 'plainText' | 'signedText' | 'plainHtml' | 'decryptedHtml' | 'plainAttachment' | 'encryptedAttachment'
| 'decryptedAttachment' | 'encryptedAttachmentLink' | 'decryptErr' | 'verifiedMsg' | 'signedHtml';
Expand Down
4 changes: 2 additions & 2 deletions extension/js/common/core/types/openpgp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,15 @@ declare namespace OpenPGP {
public version: number;
public expirationTimeV3: number | null;
public keyExpirationTime: number | null;
public params: object[];
public isEncrypted: boolean; // may be null, false or true
public getBitSize(): number;
public getAlgorithmInfo(): key.AlgorithmInfo;
public getFingerprint(): string;
public getFingerprintBytes(): Uint8Array | null;
public getCreationTime(): Date;
public getKeyId(): Keyid;
public params: object[];
public isDecrypted(): boolean;
public isEncrypted: boolean; // may be null, false or true
}

class BasePrimaryKeyPacket extends BaseKeyPacket {
Expand Down
19 changes: 10 additions & 9 deletions extension/js/common/ui/key-import-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,22 @@ export class KeyImportUi {
$('.line.unprotected_key_create_pass_phrase').hide();
}
}));
const attachment = new AttachmentUI(() => Promise.resolve({ count: 100, size: 1024 * 1024, size_mb: 1 }));
attachment.initAttachmentDialog('fineuploader', 'fineuploader_button', {
const attachmentUi = new AttachmentUI(() => Promise.resolve({ count: 100, size: 1024 * 1024, size_mb: 1 }));
attachmentUi.initAttachmentDialog('fineuploader', 'fineuploader_button', {
attachmentAdded: async file => {
let prv: OpenPGP.key.Key | undefined;
const utf = file.getData().toUtfStr();
let prv: Key | undefined;
const utf = file.getData().toUtfStr('ignore'); // ignore utf8 errors because this may be a binary key (in which case we use the bytes directly below)
if (utf.includes(PgpArmor.headers('privateKey').begin)) {
const firstPrv = MsgBlockParser.detectBlocks(utf).blocks.filter(b => b.type === 'privateKey')[0];
if (firstPrv) { // filter out all content except for the first encountered private key (GPGKeychain compatibility)
prv = (await opgp.key.readArmored(firstPrv.content.toString())).keys[0];
prv = (await KeyUtil.parse(firstPrv.content.toString()));
}
} else {
prv = (await opgp.key.read(file.getData())).keys[0];
const parsed = await KeyUtil.parseBinary(file.getData(), '');
prv = parsed[0];
}
if (typeof prv !== 'undefined') {
$('.input_private_key').val(prv.armor()).change().prop('disabled', true);
$('.input_private_key').val(KeyUtil.armor(prv)).change().prop('disabled', true);
$('.source_paste_container').css('display', 'block');
} else {
$('.input_private_key').val('').change().prop('disabled', false);
Expand All @@ -129,8 +130,8 @@ export class KeyImportUi {

public checkPrv = async (acctEmail: string, armored: string, passphrase: string): Promise<KeyImportUiCheckResult> => {
const { normalized } = await this.normalize('privateKey', armored);
const decrypted = await this.read('privateKey', normalized);
const encrypted = await this.read('privateKey', normalized);
const decrypted = await this.read('privateKey', normalized); // for decrypting - not decrypted yet
const encrypted = await this.read('privateKey', normalized); // original (typically encrypted)
this.rejectIfNot('privateKey', decrypted);
await this.rejectKnownIfSelected(acctEmail, decrypted);
await this.decryptAndEncryptAsNeeded(decrypted, encrypted, passphrase);
Expand Down
4 changes: 4 additions & 0 deletions flowcrypt-browser.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
},
"tslint.exclude": [
"test/**/*.ts"
],
"diffEditor.wordWrap": "on",
"editor.rulers": [
160
]
},
"folders": [
Expand Down
Binary file not shown.
Loading