Skip to content
Merged
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
184 changes: 99 additions & 85 deletions extension/chrome/elements/pgp_pubkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,68 +12,62 @@ import { BrowserMsg } from '../../js/common/browser/browser-msg.js';
import { Assert } from '../../js/common/assert.js';
import { Xss } from '../../js/common/platform/xss.js';
import { Url } from '../../js/common/core/common.js';
import { View } from '../../js/common/view.js';

declare const openpgp: typeof OpenPGP;

Catch.try(async () => {

// todo - this should use KeyImportUI for consistency. Needs general refactoring, hard to follow.

Ui.event.protect();

// minimized means I have to click to see details. Compact means the details take up very little space.
const uncheckedUrlParams = Url.parse(['acctEmail', 'armoredPubkey', 'parentTabId', 'minimized', 'compact', 'frameId']);
const acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail');
const parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId');
const armoredPubkey = Pgp.armor.normalize(Assert.urlParamRequire.string(uncheckedUrlParams, 'armoredPubkey'), 'publicKey');
const frameId = Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId');
const compact = uncheckedUrlParams.compact === true;
const minimized = uncheckedUrlParams.minimized === true;

const { keys: pubs } = await openpgp.key.readArmored(armoredPubkey);
const isUsableButExpired = await Pgp.key.usableButExpired(pubs[0]);
const isExpired = await Pgp.key.expired(pubs[0]);

const sendResizeMsg = () => {
const desiredHeight = $('#pgp_block').height()! + (compact ? 10 : 30); // #pgp_block is defined in template
BrowserMsg.send.setCss(parentTabId, { selector: `iframe#${frameId}`, css: { height: `${desiredHeight}px` } });
};

const setBtnText = async () => {
if (pubs.length > 1) {
$('.action_add_contact').text('import ' + pubs.length + ' public keys');
} else {
const [contact] = await Store.dbContactGet(undefined, [String($('.input_email').val())]);
$('.action_add_contact')
.text(contact?.has_pgp ? 'update key' : `import ${isExpired ? 'expired ' : ''}key`)
.css('background-color', isExpired ? '#989898' : '');
}
};

const render = async () => {
$('.pubkey').text(armoredPubkey);
if (compact) {
// todo - this should use KeyImportUI for consistency.
View.run(class PgpPubkeyView extends View {
private readonly acctEmail: string;
private readonly parentTabId: string;
private readonly armoredPubkey: string;
private readonly frameId: string;
private readonly compact: boolean; // means the details take up very little space.
private readonly minimized: boolean; // means I have to click to see details.
private publicKeys: OpenPGP.key.Key[] | undefined;
private primaryPubKey: OpenPGP.key.Key | undefined;
private isExpired: boolean | undefined;

constructor() {
super();
const uncheckedUrlParams = Url.parse(['acctEmail', 'armoredPubkey', 'parentTabId', 'minimized', 'compact', 'frameId']);
this.acctEmail = Assert.urlParamRequire.string(uncheckedUrlParams, 'acctEmail');
this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId');
this.armoredPubkey = Pgp.armor.normalize(Assert.urlParamRequire.string(uncheckedUrlParams, 'armoredPubkey'), 'publicKey');
this.frameId = Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId');
this.compact = uncheckedUrlParams.compact === true;
this.minimized = uncheckedUrlParams.minimized === true;
}

async render() {
Ui.event.protect();
this.publicKeys = (await openpgp.key.readArmored(this.armoredPubkey)).keys;
this.primaryPubKey = this.publicKeys[0];
this.isExpired = await Pgp.key.expired(this.primaryPubKey);
$('.pubkey').text(this.armoredPubkey);
if (this.compact) {
$('.hide_if_compact').remove();
$('body').css({ border: 'none', padding: 0 });
$('.line').removeClass('line');
}
$('.line.fingerprints, .line.add_contact').css('display', minimized ? 'none' : 'block');
if (pubs.length === 1) {
$('.line.fingerprints .fingerprint').text(await Pgp.key.fingerprint(pubs[0], 'spaced') || '(fingerprint error)');
$('.line.fingerprints .keywords').text(mnemonic(await Pgp.key.longid(pubs[0]) || '') || '(mnemonic error)');
$('.line.fingerprints, .line.add_contact').css('display', this.minimized ? 'none' : 'block');
if (this.publicKeys.length === 1) {
$('.line.fingerprints .fingerprint').text(await Pgp.key.fingerprint(this.primaryPubKey, 'spaced') || '(fingerprint error)');
$('.line.fingerprints .keywords').text(mnemonic(await Pgp.key.longid(this.primaryPubKey) || '') || '(mnemonic error)');
} else {
$('.line.fingerprints').css({ display: 'none' });
}
if (typeof pubs[0] !== 'undefined') {
if (!isUsableButExpired && ! await pubs[0].getEncryptionKey() && ! await pubs[0].getSigningKey()) {
showKeyNotUsableError();
if (this.primaryPubKey) {
const isUsableButExpired = await Pgp.key.usableButExpired(this.primaryPubKey);
if (!isUsableButExpired && ! await this.primaryPubKey.getEncryptionKey() && ! await this.primaryPubKey.getSigningKey()) {
this.showKeyNotUsableError();
} else {
if (compact) {
if (this.compact) {
$('.hide_if_compact_and_not_error').remove();
}
let emailText = '';
if (pubs.length === 1) {
const email = pubs[0].users[0].userId ? Str.parseEmail(pubs[0].users[0].userId ? pubs[0].users[0].userId!.userid : '').email : undefined;
if (this.publicKeys.length === 1) {
const email = Str.parseEmail(this.primaryPubKey.users[0].userId?.userid || '').email;
if (email) {
emailText = email;
$('.input_email').val(email); // checked above
Expand All @@ -82,30 +76,63 @@ Catch.try(async () => {
emailText = 'more than one person';
$('.input_email').css({ display: 'none' });
const pubToEmail = (pubkey: OpenPGP.key.Key) => Str.parseEmail(pubkey.users[0].userId ? pubkey.users[0].userId!.userid : '').email;
Xss.sanitizeAppend('.add_contact', Xss.escape(' for ' + pubs.map(pubToEmail).filter(e => !!e).join(', ')));
Xss.sanitizeAppend('.add_contact', Xss.escape(' for ' + this.publicKeys.map(pubToEmail).filter(e => !!e).join(', ')));
}
Xss.sanitizePrepend('#pgp_block.pgp_pubkey .result', `<span>This message includes a Public Key for <span class= "email">${Xss.escape(emailText)}</span>.</span>`);
$('.pubkey').addClass('good');
setBtnText().catch(Catch.reportErr);
this.setBtnText().catch(Catch.reportErr);
}
} else {
let fixed = armoredPubkey;
let fixed = this.armoredPubkey;
while (/\n> |\n>\n/.test(fixed)) {
fixed = fixed.replace(/\n> /g, '\n').replace(/\n>\n/g, '\n\n');
}
if (fixed !== armoredPubkey) { // try to re-render it after un-quoting, (minimized because it is probably their own pubkey quoted by the other guy)
window.location.href = Url.create('pgp_pubkey.htm', { armoredPubkey: fixed, minimized: true, acctEmail, parentTabId, frameId });
if (fixed !== this.armoredPubkey) { // try to re-render it after un-quoting, (minimized because it is probably their own pubkey quoted by the other guy)
window.location.href = Url.create('pgp_pubkey.htm', {
armoredPubkey: fixed, minimized: true,
acctEmail: this.acctEmail, parentTabId: this.parentTabId, frameId: this.frameId
});
} else {
showKeyNotUsableError();
this.showKeyNotUsableError();
}
}
};
this.sendResizeMsg();
}

setHandlers() {
$('.action_add_contact').click(this.setHandler(btn => this.addContactHandler(btn)));
$('.input_email').keyup(this.setHandler(() => this.setBtnText()));
$('.action_show_full').click(this.setHandler(btn => this.showFullKeyHandler(btn)));
}

private sendResizeMsg() {
const desiredHeight = $('#pgp_block').height()! + (this.compact ? 10 : 30); // #pgp_block is defined in template
BrowserMsg.send.setCss(this.parentTabId, { selector: `iframe#${this.frameId}`, css: { height: `${desiredHeight}px` } });
}

private async setBtnText() {
if (this.publicKeys!.length > 1) {
$('.action_add_contact').text('import ' + this.publicKeys!.length + ' public keys');
} else {
const [contact] = await Store.dbContactGet(undefined, [String($('.input_email').val())]);
$('.action_add_contact')
.text(contact?.has_pgp ? 'update key' : `import ${this.isExpired ? 'expired ' : ''}key`)
.css('background-color', this.isExpired ? '#989898' : '');
}
}

$('.action_add_contact').click(Ui.event.handle(async target => {
if (pubs.length > 1) {
private showKeyNotUsableError() {
$('.fingerprints, .add_contact').remove();
$('#pgp_block.pgp_pubkey .result')
.prepend('<span class="bad">This OpenPGP key is not usable.</span>'); // xss-direct
$('.pubkey').addClass('bad');
}

private async addContactHandler(addContactBtn: HTMLElement) {
if (this.publicKeys!.length > 1) {
const contacts: Contact[] = [];
for (const pubkey of pubs) {
const email = Str.parseEmail(pubkey.users[0].userId ? pubkey.users[0].userId!.userid : '').email;
for (const pubkey of this.publicKeys!) {
const email = Str.parseEmail(pubkey.users[0].userId?.userid || '').email;
if (email) {
contacts.push(await Store.dbContactObj({
email, client: 'pgp', pubkey: pubkey.armor(), lastUse: Date.now(), lastSig: await Pgp.key.lastSig(pubkey),
Expand All @@ -114,42 +141,29 @@ Catch.try(async () => {
}
}
await Store.dbContactSave(undefined, contacts);
Xss.sanitizeReplace(target, '<span class="good">added public keys</span>');
BrowserMsg.send.addToContacts(parentTabId);
Xss.sanitizeReplace(addContactBtn, '<span class="good">added public keys</span>');
BrowserMsg.send.addToContacts(this.parentTabId);
$('.input_email').remove();
} else if (pubs.length) {
} else if (this.publicKeys!.length) {
if (Str.isEmailValid(String($('.input_email').val()))) {
const contact = await Store.dbContactObj({
email: String($('.input_email').val()), client: 'pgp', pubkey: pubs[0].armor(), lastUse: Date.now(),
lastSig: await Pgp.key.lastSig(pubs[0]), expiresOn: await Pgp.key.dateBeforeExpiration(pubs[0])
email: String($('.input_email').val()), client: 'pgp', pubkey: this.publicKeys![0].armor(), lastUse: Date.now(),
lastSig: await Pgp.key.lastSig(this.publicKeys![0]), expiresOn: await Pgp.key.dateBeforeExpiration(this.publicKeys![0])
});
await Store.dbContactSave(undefined, contact);
BrowserMsg.send.addToContacts(parentTabId);
Xss.sanitizeReplace(target, `<span class="good">${Xss.escape(String($('.input_email').val()))} added</span>`);
BrowserMsg.send.addToContacts(this.parentTabId);
Xss.sanitizeReplace(addContactBtn, `<span class="good">${Xss.escape(String($('.input_email').val()))} added</span>`);
$('.input_email').remove();
} else {
await Ui.modal.error('This email is invalid, please check for typos. Not added.');
$('.input_email').focus();
}
}
}));

const showKeyNotUsableError = () => {
$('.fingerprints, .add_contact').remove();
$('#pgp_block.pgp_pubkey .result')
.prepend('<span class="bad">This OpenPGP key is not usable.</span>'); // xss-direct
$('.pubkey').addClass('bad');
};
}

$('.input_email').keyup(() => setBtnText());

$('.action_show_full').click(Ui.event.handle(target => {
$(target).css('display', 'none');
private showFullKeyHandler(showFullBtn: HTMLElement) {
$(showFullBtn).css('display', 'none');
$('pre.pubkey, .line.fingerprints, .line.add_contact').css('display', 'block');
sendResizeMsg();
}));

await render();
sendResizeMsg();

})();
this.sendResizeMsg();
}
});