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 conf/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"outDir": "../build/test"
},
"files": [
"../extension/types/node-forge.d.ts",
"../test/source/test.ts",
"../test/source/patterns.ts",
"../test/source/async-stack.ts",
Expand Down
57 changes: 0 additions & 57 deletions extension/chrome/dev/smime.htm

This file was deleted.

41 changes: 0 additions & 41 deletions extension/chrome/dev/smime.ts

This file was deleted.

2 changes: 1 addition & 1 deletion extension/chrome/elements/add_pubkey.htm
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <h1>Add a pubkey to email address</h1>
or paste below:
</div>
<div class="line">
<textarea class="pubkey" placeholder="ASCII Armored Public Key"></textarea>
<textarea class="pubkey" placeholder="ASCII Armored Public Key" spellcheck="false" data-test="input-pubkey"></textarea>
</div>
<div class="line">
<button class="button long green action_ok" data-test="action-add-pubkey">OK</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'use strict';

import { BrowserEventErrHandler, Ui } from '../../../js/common/browser/ui.js';
import { Catch, UnreportableError } from '../../../js/common/platform/catch.js';
import { Catch } from '../../../js/common/platform/catch.js';
import { NewMsgData, SendBtnTexts } from './compose-types.js';
import { ApiErr } from '../../../js/common/api/error/api-error.js';
import { BrowserExtension } from '../../../js/common/browser/browser-extension.js';
Expand Down Expand Up @@ -87,7 +87,7 @@ export class ComposeErrModule extends ViewModule<ComposeView> {
} else if (e instanceof ComposerUserError) {
await Ui.modal.error(e.message);
} else {
if (!(e instanceof ComposerResetBtnTrigger || e instanceof UnreportableError || e instanceof ComposerNotReadyError)) {
if (!(e instanceof ComposerResetBtnTrigger || e instanceof ComposerNotReadyError)) {
Catch.reportErr(e);
await Ui.modal.error(`Failed to send message due to: ${String(e)}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { RecipientType } from '../../../js/common/api/api.js';
import { Recipients } from '../../../js/common/api/email-provider/email-provider-api.js';
import { PubkeyResult } from '../../../js/common/core/pgp-key.js';

export type RecipientStatus = 0 | 1 | 2 | 3 | 4 | 5;

Expand Down Expand Up @@ -37,7 +38,6 @@ export type MessageToReplyOrForward = {
decryptedFiles: File[]
};

export type PubkeyResult = { pubkey: string, email: string, isMine: boolean };
export type CollectPubkeysResult = { armoredPubkeys: PubkeyResult[], emailsWithoutPubkeys: string[] };

export type PopoverOpt = 'encrypt' | 'sign' | 'richtext';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { Backend, FcUuidAuth } from '../../../../js/common/api/backend.js';
import { BaseMailFormatter } from './base-mail-formatter.js';
import { ComposerResetBtnTrigger } from '../compose-err-module.js';
import { Mime, SendableMsgBody } from '../../../../js/common/core/mime.js';
import { NewMsgData, PubkeyResult } from '../compose-types.js';
import { NewMsgData } from '../compose-types.js';
import { Str, Value } from '../../../../js/common/core/common.js';
import { ApiErr } from '../../../../js/common/api/error/api-error.js';
import { Att } from '../../../../js/common/core/att.js';
import { Buf } from '../../../../js/common/core/buf.js';
import { Catch } from '../../../../js/common/platform/catch.js';
import { Lang } from '../../../../js/common/lang.js';
import { PgpKey } from '../../../../js/common/core/pgp-key.js';
import { PgpMsg } from '../../../../js/common/core/pgp-msg.js';
import { PgpKey, PubkeyResult } from '../../../../js/common/core/pgp-key.js';
import { PgpMsg, PgpMsgMethod } from '../../../../js/common/core/pgp-msg.js';
import { SendableMsg } from '../../../../js/common/api/email-provider/sendable-msg.js';
import { Settings } from '../../../../js/common/settings.js';
import { Ui } from '../../../../js/common/browser/ui.js';
Expand Down Expand Up @@ -43,7 +43,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
const authInfo = await AcctStore.authInfo(this.acctEmail);
const msgBodyWithReplyToken = await this.getPwdMsgSendableBodyWithOnlineReplyMsgToken(authInfo, newMsg);
const pgpMimeWithAtts = await Mime.encode(msgBodyWithReplyToken, { Subject: newMsg.subject }, await this.view.attsModule.attach.collectAtts());
const pwdEncryptedWithAtts = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAtts), newMsg.pwd, []); // encrypted only for pwd, not signed
const { data: pwdEncryptedWithAtts } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAtts), newMsg.pwd, []); // encrypted only for pwd, not signed
const { short, admin_code } = await Backend.messageUpload(
authInfo.uuid ? authInfo : undefined,
pwdEncryptedWithAtts,
Expand All @@ -57,23 +57,23 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter {
// encoded as: PGP/MIME-like structure but with attachments as external files due to email size limit (encrypted for pubkeys only)
const msgBody = this.richtext ? { 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml } : { 'text/plain': newMsg.plaintext };
const pgpMimeNoAtts = await Mime.encode(msgBody, { Subject: newMsg.subject }, []); // no atts, attached to email separately
const pubEncryptedNoAtts = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAtts), undefined, pubs, signingPrv); // encrypted only for pubs
const atts = this.createPgpMimeAtts(pubEncryptedNoAtts).concat(await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey))); // encrypted only for pubs
const { data: pubEncryptedNoAtts } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAtts), undefined, pubs, signingPrv); // encrypted only for pubs
const atts = this.createPgpMimeAtts(pubEncryptedNoAtts).concat(await this.view.attsModule.attach.collectEncryptAtts(pubs)); // encrypted only for pubs
const emailIntroAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(short);
return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: emailIntroAndLinkBody, atts, isDraft: this.isDraft });
}

private sendableSimpleTextMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key) => {
const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs.map(p => p.pubkey));
const encrypted = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv);
const encryptedBody = { 'text/plain': encrypted.toString() };
return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: encryptedBody, atts, isDraft: this.isDraft });
const atts = this.isDraft ? [] : await this.view.attsModule.attach.collectEncryptAtts(pubs);
const { data: encryptedBody, type } = await this.encryptDataArmor(Buf.fromUtfStr(newMsg.plaintext), undefined, pubs, signingPrv);
const mimeType = type === 'smime' ? 'smimeEncrypted' : undefined;
return await SendableMsg.create(this.acctEmail, { ...this.headers(newMsg), body: { "encrypted/buf": Buf.fromUint8(encryptedBody) }, type: mimeType, atts, isDraft: this.isDraft });
}

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

private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise<Uint8Array> => {
const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal(pubs);
const r = await PgpMsg.encrypt({ pubkeys: pubs.map(p => p.pubkey), signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as OpenPGP.EncryptArmorResult;
return Buf.fromUtfStr(r.data);
private encryptDataArmor = async (data: Buf, pwd: string | undefined, pubs: PubkeyResult[], signingPrv?: OpenPGP.key.Key): Promise<PgpMsgMethod.EncryptAnyArmorResult> => {
const pgpPubs = pubs.filter(pub => PgpKey.getKeyType(pub.pubkey) === 'openpgp');
const encryptAsOfDate = await this.encryptMsgAsOfDateIfSomeAreExpiredAndUserConfirmedModal(pgpPubs);
const pubsForEncryption = PgpKey.choosePubsBasedOnKeyTypeCombinationForPartialSmimeSupport(pubs);
return await PgpMsg.encrypt({ pubkeys: pubsForEncryption, signingPrv, pwd, data, armor: true, date: encryptAsOfDate }) as PgpMsgMethod.EncryptAnyArmorResult;
}

private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async (authInfo: FcUuidAuth, newMsgData: NewMsgData): Promise<SendableMsgBody> => {
Expand Down
1 change: 1 addition & 0 deletions extension/chrome/elements/compose.htm
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ <h1 id="header_title" data-test="header-title">New Secure Message</h1>
<script src="/lib/fine-uploader.js"></script>
<script src="/lib/zxcvbn.js"></script>
<script src="/lib/iso-8859-2.js"></script>
<script src="/lib/forge.js"></script>
<script src="compose.js" type="module"></script>

</body>
Expand Down
9 changes: 7 additions & 2 deletions extension/js/common/api/email-provider/sendable-msg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,14 @@ export class SendableMsg {
}
}
this.headers.Subject = this.subject;
if (this.type === 'pgpMimeSigned' && this.sign) {
if (this.type === 'smimeEncrypted' && this.body['encrypted/buf']) {
return await Mime.encodeSmime(this.body['encrypted/buf'], this.headers);
} else if (this.type === 'pgpMimeSigned' && this.sign) {
return await Mime.encodePgpMimeSigned(this.body, this.headers, this.atts, this.sign);
} else {
} else { // encrypted/buf is a Buf instance that is converted to single-part plain/text message
if (this.body['encrypted/buf']) {
this.body = { 'text/plain': this.body['encrypted/buf'].toString() };
}
return await Mime.encode(this.body, this.headers, this.atts, this.type);
}
}
Expand Down
26 changes: 22 additions & 4 deletions extension/js/common/core/mime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,14 @@ export type MimeContent = {
bcc: string[];
};

export type MimeEncodeType = 'pgpMimeEncrypted' | 'pgpMimeSigned' | undefined;
export type MimeEncodeType = 'pgpMimeEncrypted' | 'pgpMimeSigned' | 'smimeEncrypted' | undefined;
export type RichHeaders = Dict<string | string[]>;
export type SendableMsgBody = { [key: string]: string | undefined; 'text/plain'?: string; 'text/html'?: string; };
export type SendableMsgBody = {
[key: string]: string | Buf | undefined;
'text/plain'?: string;
'text/html'?: string;
'encrypted/buf'?: Buf;
};
export type MimeProccesedMsg = {
rawSignedContent: string | undefined,
headers: Dict<MimeContentHeader>,
Expand Down Expand Up @@ -203,7 +208,7 @@ export class Mime {
} else {
contentNode = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any
for (const type of Object.keys(body)) {
contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!)); // already present, that's why part of for loop
contentNode.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!.toString())); // already present, that's why part of for loop
}
}
rootNode.appendChild(contentNode); // tslint:disable-line:no-unsafe-any
Expand All @@ -214,6 +219,19 @@ export class Mime {
return rootNode.build(); // tslint:disable-line:no-unsafe-any
}

public static encodeSmime = async (body: Uint8Array, headers: RichHeaders): Promise<string> => {
const rootContentType = 'application/pkcs7-mime; name="smime.p7m"; smime-type=enveloped-data';
const rootNode = new MimeBuilder(rootContentType, { includeBccInHeader: true }); // tslint:disable-line:no-unsafe-any
for (const key of Object.keys(headers)) {
rootNode.addHeader(key, headers[key]); // tslint:disable-line:no-unsafe-any
}
rootNode.setContent(body); // tslint:disable-line:no-unsafe-any
rootNode.addHeader('Content-Transfer-Encoding', 'base64'); // tslint:disable-line:no-unsafe-any
rootNode.addHeader('Content-Disposition', 'attachment; filename="smime.p7m"'); // tslint:disable-line:no-unsafe-any
rootNode.addHeader('Content-Description', 'S/MIME Encrypted Message'); // tslint:disable-line:no-unsafe-any
return rootNode.build(); // tslint:disable-line:no-unsafe-any
}

public static subjectWithoutPrefixes = (subject: string): string => {
return subject.replace(/^((Re|Fwd): ?)+/g, '').trim();
}
Expand All @@ -226,7 +244,7 @@ export class Mime {
}
const bodyNodes = new MimeBuilder('multipart/alternative'); // tslint:disable-line:no-unsafe-any
for (const type of Object.keys(body)) {
bodyNodes.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!)); // tslint:disable-line:no-unsafe-any
bodyNodes.appendChild(Mime.newContentNode(MimeBuilder, type, body[type]!.toString())); // tslint:disable-line:no-unsafe-any
}
const signedContentNode = new MimeBuilder('multipart/mixed'); // tslint:disable-line:no-unsafe-any
signedContentNode.appendChild(bodyNodes); // tslint:disable-line:no-unsafe-any
Expand Down
36 changes: 35 additions & 1 deletion extension/js/common/core/pgp-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
'use strict';

import { Buf } from './buf.js';
import { Catch } from '../platform/catch.js';
import { Catch, UnreportableError } from '../platform/catch.js';
import { MsgBlockParser } from './msg-block-parser.js';
import { PgpArmor } from './pgp-armor.js';
import { opgp } from './pgp.js';
import { KeyCache } from '../platform/key-cache.js';

export type PubkeyResult = { pubkey: string, email: string, isMine: boolean };

export type Contact = {
email: string;
name: string | null;
Expand Down Expand Up @@ -352,6 +354,9 @@ export class PgpKey {
* This is used to figure out how recently was key updated, and if one key is newer than other.
*/
public static lastSig = async (key: OpenPGP.key.Key): Promise<number> => {
if (!key) { // key is undefined only for X.509 keys
return Date.now(); // todo - this definitely needs to be refactored soon #2731
}
await key.getExpirationTime(); // will force all sigs to be verified
const allSignatures: OpenPGP.packet.Signature[] = [];
for (const user of key.users) {
Expand Down Expand Up @@ -381,4 +386,33 @@ export class PgpKey {
return await opgp.stream.readToEnd(certificate);
}
}

public static getKeyType = (pubkey: string): 'openpgp' | 'x509' | 'unknown' => {
if (pubkey.startsWith('-----BEGIN CERTIFICATE-----')) {
return 'x509';
} else if (pubkey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')) {
return 'openpgp';
} else {
return 'unknown';
}
}

public static choosePubsBasedOnKeyTypeCombinationForPartialSmimeSupport = (pubs: PubkeyResult[]): string[] => {
const myPubs = pubs.filter(pub => pub.isMine); // currently this must be openpgp pub
const otherPgpPubs = pubs.filter(pub => !pub.isMine && PgpKey.getKeyType(pub.pubkey) === 'openpgp');
const otherSmimePubs = pubs.filter(pub => !pub.isMine && PgpKey.getKeyType(pub.pubkey) === 'x509');
if (otherPgpPubs.length && otherSmimePubs.length) {
let err = `Cannot use mixed OpenPGP (${otherPgpPubs.map(p => p.email).join(', ')}) and S/MIME (${otherSmimePubs.map(p => p.email).join(', ')}) public keys yet.`;
err += 'If you need to email S/MIME recipient, do not add any OpenPGP recipient at the same time.';
throw new UnreportableError(err);
}
if (otherPgpPubs.length) {
return myPubs.concat(...otherPgpPubs).map(p => p.pubkey);
}
if (otherSmimePubs.length) { // todo - currently skipping my own pgp keys when encrypting message for S/MIME
return otherSmimePubs.map(pub => pub.pubkey);
}
return myPubs.map(p => p.pubkey);
}

}
Loading