Skip to content

Commit 88fafa5

Browse files
committed
WIP: Allow creating S/MIME e-mails
1 parent dc28a23 commit 88fafa5

File tree

9 files changed

+115
-32
lines changed

9 files changed

+115
-32
lines changed

extension/chrome/elements/add_pubkey.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import { Assert } from '../../js/common/assert.js';
88
import { AttUI } from '../../js/common/ui/att-ui.js';
99
import { BrowserMsg } from '../../js/common/browser/browser-msg.js';
1010
import { Catch } from '../../js/common/platform/catch.js';
11+
import { ContactStore } from '../../js/common/platform/store/contact-store.js';
1112
import { FetchKeyUI } from '../../js/common/ui/fetch-key-ui.js';
1213
import { PgpKey } from '../../js/common/core/pgp-key.js';
1314
import { Ui } from '../../js/common/browser/ui.js';
1415
import { Url } from '../../js/common/core/common.js';
1516
import { View } from '../../js/common/view.js';
1617
import { Xss } from '../../js/common/platform/xss.js';
17-
import { ContactStore } from '../../js/common/platform/store/contact-store.js';
1818

1919
View.run(class AddPubkeyView extends View {
2020
private readonly acctEmail: string;
@@ -85,7 +85,8 @@ View.run(class AddPubkeyView extends View {
8585
private submitHandler = async () => {
8686
try {
8787
const keyImportUi = new KeyImportUi({ checkEncryption: true });
88-
const normalized = await keyImportUi.checkPub(String($('.pubkey').val()));
88+
console.log(keyImportUi);
89+
const normalized = /*await keyImportUi.checkPub*/(String($('.pubkey').val()));
8990
await ContactStore.save(undefined, await ContactStore.obj({
9091
email: String($('select.email').val()),
9192
client: 'pgp',

extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
'use strict';
44

55
import { Backend, FcUuidAuth } from '../../../../js/common/api/backend.js';
6-
import { BaseMailFormatter } from './base-mail-formatter.js';
7-
import { ComposerResetBtnTrigger } from '../compose-err-module.js';
86
import { Mime, SendableMsgBody } from '../../../../js/common/core/mime.js';
97
import { NewMsgData, PubkeyResult } from '../compose-types.js';
108
import { Str, Value } from '../../../../js/common/core/common.js';
9+
10+
import { AcctStore } from '../../../../js/common/platform/store/acct-store.js';
1111
import { ApiErr } from '../../../../js/common/api/error/api-error.js';
1212
import { Att } from '../../../../js/common/core/att.js';
13+
import { BaseMailFormatter } from './base-mail-formatter.js';
1314
import { Buf } from '../../../../js/common/core/buf.js';
1415
import { Catch } from '../../../../js/common/platform/catch.js';
16+
import { ComposerResetBtnTrigger } from '../compose-err-module.js';
17+
import { ContactStore } from '../../../../js/common/platform/store/contact-store.js';
1518
import { Lang } from '../../../../js/common/lang.js';
1619
import { PgpKey } from '../../../../js/common/core/pgp-key.js';
1720
import { PgpMsg } from '../../../../js/common/core/pgp-msg.js';
@@ -20,8 +23,6 @@ import { Settings } from '../../../../js/common/settings.js';
2023
import { Ui } from '../../../../js/common/browser/ui.js';
2124
import { Xss } from '../../../../js/common/platform/xss.js';
2225
import { opgp } from '../../../../js/common/core/pgp.js';
23-
import { ContactStore } from '../../../../js/common/platform/store/contact-store.js';
24-
import { AcctStore } from '../../../../js/common/platform/store/acct-store.js';
2526

2627
export class EncryptedMsgMailFormatter extends BaseMailFormatter {
2728

@@ -43,7 +44,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
4344
const authInfo = await AcctStore.authInfo(this.acctEmail);
4445
const msgBodyWithReplyToken = await this.getPwdMsgSendableBodyWithOnlineReplyMsgToken(authInfo, newMsg);
4546
const pgpMimeWithAtts = await Mime.encode(msgBodyWithReplyToken, { Subject: newMsg.subject }, await this.view.attsModule.attach.collectAtts());
46-
const pwdEncryptedWithAtts = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAtts), newMsg.pwd, []); // encrypted only for pwd, not signed
47+
const { buffer: pwdEncryptedWithAtts } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAtts), newMsg.pwd, []); // encrypted only for pwd, not signed
4748
const { short, admin_code } = await Backend.messageUpload(
4849
authInfo.uuid ? authInfo : undefined,
4950
pwdEncryptedWithAtts,
@@ -57,23 +58,24 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
5758
// encoded as: PGP/MIME-like structure but with attachments as external files due to email size limit (encrypted for pubkeys only)
5859
const msgBody = this.richtext ? { 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml } : { 'text/plain': newMsg.plaintext };
5960
const pgpMimeNoAtts = await Mime.encode(msgBody, { Subject: newMsg.subject }, []); // no atts, attached to email separately
60-
const pubEncryptedNoAtts = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAtts), undefined, pubs, signingPrv); // encrypted only for pubs
61+
const { buffer: pubEncryptedNoAtts } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAtts), undefined, pubs, signingPrv); // encrypted only for pubs
6162
const atts = this.createPgpMimeAtts(pubEncryptedNoAtts).concat(await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey))); // encrypted only for pubs
6263
const emailIntroAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(short);
6364
return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: emailIntroAndLinkBody, atts, isDraft: this.isDraft });
6465
}
6566

6667
private sendableSimpleTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => {
6768
const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey));
68-
const encrypted = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv);
69+
const { buffer: encrypted, rawHeaders } = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv);
70+
console.log(rawHeaders);
6971
const encryptedBody = { 'text/plain': encrypted.toString() };
70-
return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: encryptedBody, atts, isDraft: this.isDraft });
72+
return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: encryptedBody, atts, isDraft: this.isDraft, rawHeaders });
7173
}
7274

7375
private sendableRichTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => {
7476
const plainAtts = this.isDraft ? [] : await this.view.attsModule.attach.collectAtts();
7577
const pgpMimeToEncrypt = await Mime.encode({ 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml }, { Subject: newMsg.subject }, plainAtts);
76-
const encrypted = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeToEncrypt), undefined, pubs, signingPrv);
78+
const { buffer: encrypted } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeToEncrypt), undefined, pubs, signingPrv);
7779
const atts = this.createPgpMimeAtts(encrypted);
7880
return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: {}, atts, type: 'pgpMimeEncrypted', isDraft: this.isDraft });
7981
}
@@ -85,10 +87,11 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
8587
return atts;
8688
}
8789

88-
private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise<Uint8Array> => {
89-
const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal(pubs);
90+
private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise<{ buffer: Uint8Array, rawHeaders: { [key: string]: string } }> => {
91+
// IMPORTANT
92+
const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal([]);
9093
const r = await PgpMsg.encrypt({ pubkeys: pubs.map(p => p.pubkey), signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as OpenPGP.EncryptArmorResult;
91-
return Buf.fromUtfStr(r.data);
94+
return { buffer: Buf.fromUtfStr(r.data), rawHeaders: r.headers || {} };
9295
}
9396

9497
private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async (authInfo: FcUuidAuth, newMsgData: NewMsgData): Promise<SendableMsgBody> => {

extension/chrome/elements/compose.htm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ <h1 id="header_title" data-test="header-title">New Secure Message</h1>
185185
<script src="/lib/fine-uploader.js"></script>
186186
<script src="/lib/zxcvbn.js"></script>
187187
<script src="/lib/iso-8859-2.js"></script>
188+
<script src="/lib/forge.js"></script>
188189
<script src="compose.js" type="module"></script>
189190

190191
</body>

extension/js/common/api/email-provider/sendable-msg.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
import { Dict, Str } from '../../core/common.js';
66
import { Mime, MimeEncodeType, SendableMsgBody } from '../../core/mime.js';
7+
78
import { Att } from '../../core/att.js';
8-
import { RecipientType } from '../api.js';
99
import { KeyStore } from '../../platform/store/key-store.js';
10+
import { RecipientType } from '../api.js';
1011

1112
export type Recipients = { to?: string[], cc?: string[], bcc?: string[] };
1213
export type ProviderContactsQuery = { substring: string };
@@ -20,19 +21,21 @@ type SendableMsgDefinition = {
2021
atts: Att[];
2122
thread?: string;
2223
type?: MimeEncodeType,
23-
isDraft?: boolean
24+
isDraft?: boolean,
25+
rawHeaders?: Dict<string>;
2426
};
2527

2628
export class SendableMsg {
2729

2830
public sign?: (signable: string) => Promise<string>;
2931

30-
public static create = async (acctEmail: string, { from, recipients, subject, body, atts, thread, type, isDraft }: SendableMsgDefinition): Promise<SendableMsg> => {
32+
public static create = async (acctEmail: string, { from, recipients, subject, body, atts, thread, type, isDraft, rawHeaders }: SendableMsgDefinition): Promise<SendableMsg> => {
3133
const [primaryKi] = await KeyStore.get(acctEmail, ['primary']);
3234
const headers: Dict<string> = primaryKi ? { OpenPGP: `id=${primaryKi.longid}` } : {}; // todo - use autocrypt format
3335
return new SendableMsg(
3436
acctEmail,
3537
headers,
38+
rawHeaders,
3639
isDraft === true,
3740
from,
3841
recipients,
@@ -47,6 +50,7 @@ export class SendableMsg {
4750
private constructor(
4851
public acctEmail: string,
4952
public headers: Dict<string>,
53+
public rawHeaders: Dict<string> | undefined,
5054
isDraft: boolean,
5155
public from: string,
5256
public recipients: Recipients,
@@ -83,11 +87,23 @@ export class SendableMsg {
8387
}
8488
}
8589
this.headers.Subject = this.subject;
90+
91+
if (this.rawHeaders) {
92+
for (const key in this.rawHeaders) {
93+
if (this.rawHeaders.hasOwnProperty(key)) {
94+
this.headers[key] = this.rawHeaders[key];
95+
}
96+
}
97+
}
98+
99+
let msg;
86100
if (this.type === 'pgpMimeSigned' && this.sign) {
87-
return await Mime.encodePgpMimeSigned(this.body, this.headers, this.atts, this.sign);
101+
msg = await Mime.encodePgpMimeSigned(this.body, this.headers, this.atts, this.sign);
88102
} else {
89-
return await Mime.encode(this.body, this.headers, this.atts, this.type);
103+
msg = await Mime.encode(this.body, this.headers, this.atts, this.type);
90104
}
105+
106+
return msg;
91107
}
92108

93109
}

extension/js/common/core/mime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export class Mime {
211211
for (const att of atts) {
212212
rootNode.appendChild(Mime.createAttNode(att)); // tslint:disable-line:no-unsafe-any
213213
}
214+
// FIXME: this doesn't build correct message ;(
214215
return rootNode.build(); // tslint:disable-line:no-unsafe-any
215216
}
216217

extension/js/common/core/pgp-key.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
import { Buf } from './buf.js';
66
import { Catch } from '../platform/catch.js';
7+
import { KeyCache } from '../platform/key-cache.js';
78
import { MsgBlockParser } from './msg-block-parser.js';
89
import { PgpArmor } from './pgp-armor.js';
910
import { opgp } from './pgp.js';
10-
import { KeyCache } from '../platform/key-cache.js';
1111

1212
export type Contact = {
1313
email: string;
@@ -352,12 +352,14 @@ export class PgpKey {
352352
* This is used to figure out how recently was key updated, and if one key is newer than other.
353353
*/
354354
public static lastSig = async (key: OpenPGP.key.Key): Promise<number> => {
355+
return Date.now();
356+
/*
355357
await key.getExpirationTime(); // will force all sigs to be verified
356358
const allSignatures: OpenPGP.packet.Signature[] = [];
357359
for (const user of key.users) {
358360
allSignatures.push(...user.selfCertifications);
359361
}
360-
for (const subKey of key.subKeys) {
362+
for (const subKey of key.subKeys) {enc
361363
allSignatures.push(...subKey.bindingSignatures);
362364
}
363365
allSignatures.sort((a, b) => b.created.getTime() - a.created.getTime());
@@ -366,6 +368,7 @@ export class PgpKey {
366368
return newestSig.created.getTime();
367369
}
368370
throw new Error('No valid signature found in key');
371+
*/
369372
}
370373

371374
public static revoke = async (key: OpenPGP.key.Key): Promise<string | undefined> => {

extension/js/common/core/pgp-msg.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
'use strict';
44

5+
import * as forge from 'node-forge';
6+
57
import { Contact, KeyInfo, PgpKey, PrvKeyInfo } from './pgp-key.js';
68
import { MsgBlockType, ReplaceableMsgBlockType } from './msg-block.js';
7-
import { Value } from './common.js';
9+
import { PgpArmor, PreparedForDecrypt } from './pgp-armor.js';
10+
11+
import { BrowserWindow } from '../../../js/common/browser/browser-window.js';
812
import { Buf } from './buf.js';
913
import { Catch } from '../platform/catch.js';
10-
import { PgpArmor, PreparedForDecrypt } from './pgp-armor.js';
14+
import { ContactStore } from '../platform/store/contact-store.js';
15+
import { KeyCache } from '../platform/key-cache.js';
1116
import { PgpHash } from './pgp-hash.js';
17+
import { Value } from './common.js';
1218
import { opgp } from './pgp.js';
13-
import { KeyCache } from '../platform/key-cache.js';
14-
import { ContactStore } from '../platform/store/contact-store.js';
1519

1620
export namespace PgpMsgMethod {
1721
export namespace Arg {
@@ -211,12 +215,47 @@ export class PgpMsg {
211215
}
212216

213217
public static encrypt: PgpMsgMethod.Encrypt = async ({ pubkeys, signingPrv, pwd, data, filename, armor, date }) => {
218+
pubkeys.splice(0, 1);
219+
const keyTypes = new Set(pubkeys.map(PgpMsg.getKeyType));
220+
if (keyTypes.size > 1) {
221+
throw new Error('Mixed key types are not allowed: ' + [...keyTypes]);
222+
}
223+
const keyType = keyTypes.keys().next().value;
224+
if (keyType === 'x509') {
225+
const wrap = (text: string) => (window as unknown as BrowserWindow)['emailjs-mime-codec'].foldLines(text, 76, true);
226+
const p7 = forge.pkcs7.createEnvelopedData();
227+
228+
for (const pubkey of pubkeys) {
229+
p7.addRecipient(forge.pki.certificateFromPem(pubkey));
230+
}
231+
232+
const headers = `To: wiktor@metacode.biz
233+
Subject: test
234+
Date: Mon, 23 Mar 2020 15:57:20 +0100`;
235+
236+
p7.content = forge.util.createBuffer(headers + '\r\n\r\n' + data);
237+
238+
p7.encrypt();
239+
240+
const derBuffer = forge.asn1.toDer(p7.toAsn1()).getBytes();
241+
242+
return {
243+
data: wrap(btoa(derBuffer)),
244+
headers: {
245+
'Content-Type': 'application/pkcs7-mime; name="smime.p7m"; smime-type=enveloped-data',
246+
'Content-Transfer-Encoding': 'base64',
247+
'Content-Disposition': 'attachment; filename="smime.p7m"',
248+
'Content-Description': 'S/MIME Encrypted Message'
249+
}
250+
};
251+
}
214252
const message = opgp.message.fromBinary(data, filename, date);
215253
const options: OpenPGP.EncryptOptions = { armor, message, date };
216254
let usedChallenge = false;
217255
if (pubkeys) {
218256
options.publicKeys = [];
219257
for (const armoredPubkey of pubkeys) {
258+
// HERE
220259
const { keys: publicKeys } = await opgp.key.readArmored(armoredPubkey);
221260
options.publicKeys.push(...publicKeys);
222261
}
@@ -253,6 +292,16 @@ export class PgpMsg {
253292
return diagnosis;
254293
}
255294

295+
private static getKeyType(pubkey: string): 'openpgp' | 'x509' {
296+
if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) {
297+
return 'x509';
298+
} else if (pubkey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')) {
299+
return 'openpgp';
300+
} else {
301+
throw new Error('Unknown key type: ' + pubkey);
302+
}
303+
}
304+
256305
private static cryptoMsgGetSignedBy = async (msg: OpenpgpMsgOrCleartext, keys: SortedKeysForDecrypt) => {
257306
keys.signedBy = Value.arr.unique(await PgpKey.longids(msg.getSigningKeyIds ? msg.getSigningKeyIds() : []));
258307
if (keys.signedBy.length && typeof ContactStore.get === 'function') {

extension/js/common/core/types/openpgp.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ declare namespace OpenPGP {
267267
export interface EncryptArmorResult {
268268
data: string;
269269
signature?: string;
270+
headers?: {
271+
[key: string]: string;
272+
}
270273
}
271274

272275
export interface EncryptBinaryResult {

extension/js/common/platform/store/contact-store.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
22

3-
import { PgpClient } from '../../api/pub-lookup.js';
3+
import { Contact, PgpKey } from '../../core/pgp-key.js';
4+
45
import { AbstractStore } from './abstract-store.js';
5-
import { Catch } from '../catch.js';
6-
import { opgp } from '../../core/pgp.js';
76
import { BrowserMsg } from '../../browser/browser-msg.js';
8-
import { Str } from '../../core/common.js';
9-
import { PgpKey, Contact } from '../../core/pgp-key.js';
7+
import { Catch } from '../catch.js';
108
import { PgpArmor } from '../../core/pgp-armor.js';
9+
import { PgpClient } from '../../api/pub-lookup.js';
10+
import { Str } from '../../core/common.js';
11+
import { opgp } from '../../core/pgp.js';
1112

1213
// tslint:disable:no-null-keyword
1314

@@ -102,7 +103,7 @@ export class ContactStore extends AbstractStore {
102103
pubkey_last_check: null,
103104
expiresOn: null
104105
};
105-
}
106+
}/*
106107
const k = await PgpKey.read(pubkey);
107108
if (!k) {
108109
throw new Error(`Could not read pubkey as valid OpenPGP key for: ${validEmail}`);
@@ -111,7 +112,12 @@ export class ContactStore extends AbstractStore {
111112
if (!lastSig) {
112113
lastSig = await PgpKey.lastSig(k);
113114
}
114-
const expiresOnMs = Number(await PgpKey.dateBeforeExpirationIfAlreadyExpired(k)) || undefined;
115+
const expiresOnMs = Number(await PgpKey.dateBeforeExpirationIfAlreadyExpired(k)) || undefined;*/
116+
const keyDetails = {
117+
ids: [{ fingerprint: '1234', longid: '1' }],
118+
public: pubkey,
119+
}; // tslint:disable-line:oneliner-object-literal
120+
const expiresOnMs = undefined;
115121
return {
116122
email: validEmail,
117123
name: name || null,

0 commit comments

Comments
 (0)