Changes for #89

Now it does not fetch the PGP signature, because validation was broken anyway.
Instead it validates multipart/signed according to RFC 3156 section 5 and returns details for the signed part:
* BodyPartId
* SigPartId
* MicAlg

So in the future several implementations (GnuPG, OpenPGP.js, etc.) can use the correct data for verification.
This commit is contained in:
the-djmaze 2022-01-17 15:58:23 +01:00
parent ba49d06d1a
commit 8dcd0cf833
14 changed files with 514 additions and 356 deletions

View file

@ -347,7 +347,7 @@ class AppUser extends AbstractApp {
}
reloadOpenPgpKeys() {
if (PgpUserStore.capaOpenPGP()) {
if (PgpUserStore.openpgp) {
const keys = [],
email = new EmailModel(),
openpgpKeyring = PgpUserStore.openpgpKeyring,
@ -784,7 +784,6 @@ class AppUser extends AbstractApp {
}
}
PgpUserStore.openpgpKeyring = new openpgp.Keyring();
PgpUserStore.capaOpenPGP(true);
this.reloadOpenPgpKeys();
};
script.onerror = () => console.error(script.src);

View file

@ -20,8 +20,6 @@ import { AbstractModel } from 'Knoin/AbstractModel';
import PreviewHTML from 'Html/PreviewMessage.html';
import { PgpUserStore } from 'Stores/User/Pgp';
const
/*eslint-disable max-len*/
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
@ -102,7 +100,7 @@ export class MessageModel extends AbstractModel {
hasImages: false,
hasExternals: false,
isPgpSigned: false,
pgpSigned: null, // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" }
isPgpEncrypted: false,
pgpSignedVerifyStatus: SignedVerifyStatus.None,
pgpSignedVerifyUser: '',
@ -181,7 +179,7 @@ export class MessageModel extends AbstractModel {
this.hasExternals(false);
this.attachments(new AttachmentCollectionModel);
this.isPgpSigned(false);
this.pgpSigned(null);
this.isPgpEncrypted(false);
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
this.pgpSignedVerifyUser('');
@ -404,7 +402,7 @@ export class MessageModel extends AbstractModel {
viewHtml() {
const body = this.body;
if (body && this.html()) {
let html = this.html().toString()
let html = this.html()
.replace(/font-size:\s*[0-9]px/g, 'font-size:11px')
// Strip utm_* tracking
.replace(/(\\?|&|&)utm_[a-z]+=[a-z0-9_-]*/si, '$1');
@ -465,7 +463,7 @@ export class MessageModel extends AbstractModel {
if (body && this.plain()) {
body.classList.toggle('html', 0);
body.classList.toggle('plain', 1);
body.innerHTML = plainToHtml(this.plain().toString())
body.innerHTML = plainToHtml(this.plain())
// Strip utm_* tracking
.replace(/(\\?|&|&)utm_[a-z]+=[a-z0-9_-]*/si, '$1')
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
@ -479,8 +477,6 @@ export class MessageModel extends AbstractModel {
}
initView() {
PgpUserStore.initMessageBodyControls(this.body, this);
// init BlockquoteSwitcher
this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {

View file

@ -370,7 +370,7 @@ export const MessageUserStore = new class {
message.hasImages(body.rlHasImages);
} else {
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
+ (message.isPgpSigned() ? ' openpgp-signed' : '')
+ (message.pgpSigned() ? ' openpgp-signed' : '')
+ (message.isPgpEncrypted() ? ' openpgp-encrypted' : '')
+ '">'
+ '</div>');

View file

@ -1,6 +1,5 @@
import ko from 'ko';
import { i18n } from 'Common/Translator';
import { isArray, arrayLength, pString, addComputablesTo } from 'Common/Utils';
import { AccountUserStore } from 'Stores/User/Account';
@ -9,132 +8,8 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { MessageOpenPgpPopupView } from 'View/Popup/MessageOpenPgp';
function controlsHelper(dom, verControl, success, title, text)
{
dom.classList.toggle('error', !success);
dom.classList.toggle('success', success);
verControl.classList.toggle('error', !success);
verControl.classList.toggle('success', success);
dom.title = verControl.title = title;
if (undefined !== text) {
dom.textContent = text.trim();
}
}
function domControlEncryptedClickHelper(store, dom, armoredMessage, recipients) {
return function() {
let message = null;
if (this.classList.contains('success')) {
return false;
}
try {
message = store.openpgp.message.readArmored(armoredMessage);
} catch (e) {
console.log(e);
}
if (message && message.getText && message.verify && message.decrypt) {
store.decryptMessage(
message,
recipients,
(validPrivateKey, decryptedMessage, validPublicKey, signingKeyIds) => {
if (decryptedMessage) {
if (validPublicKey) {
controlsHelper(
dom,
this,
true,
i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', {
USER: validPublicKey.user + ' (' + validPublicKey.id + ')'
}),
decryptedMessage.getText()
);
} else if (validPrivateKey) {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ')
: '';
controlsHelper(
dom,
this,
false,
i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : ''),
decryptedMessage.getText()
);
} else {
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
}
} else {
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
}
}
);
return false;
}
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
return false;
};
}
function domControlSignedClickHelper(store, dom, armoredMessage) {
return function() {
let message = null;
if (this.classList.contains('success') || this.classList.contains('error')) {
return false;
}
try {
message = store.openpgp.cleartext.readArmored(armoredMessage);
} catch (e) {
console.log(e);
}
if (message && message.getText && message.verify) {
store.verifyMessage(message, (validKey, signingKeyIds) => {
if (validKey) {
controlsHelper(
dom,
this,
true,
i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')'
}),
message.getText()
);
} else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ')
: '';
controlsHelper(
dom,
this,
false,
i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : '')
);
}
});
return false;
}
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
return false;
};
}
export const PgpUserStore = new class {
constructor() {
this.capaOpenPGP = ko.observable(false);
this.openpgp = null;
this.openpgpkeys = ko.observableArray();
@ -335,28 +210,4 @@ export const PgpUserStore = new class {
return false;
}
/**
* @param {*} dom
* @param {MessageModel} rainLoopMessage
*/
initMessageBodyControls(dom, rainLoopMessage) {
const cl = dom.classList,
signed = cl.contains('openpgp-signed'),
encrypted = cl.contains('openpgp-encrypted');
if (encrypted || signed) {
const
domText = dom.textContent,
recipients = rainLoopMessage ? rainLoopMessage.getEmails(['from', 'to', 'cc']) : [],
verControl = Element.fromHTML('<div class="b-openpgp-control"><i class="fontastic">🔒</i></div>');
if (encrypted) {
verControl.title = i18n('MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC');
verControl.addEventListener('click', domControlEncryptedClickHelper(this, dom, domText, recipients));
} else {
verControl.title = i18n('MESSAGE/PGP_SIGNED_MESSAGE_DESC');
verControl.addEventListener('click', domControlSignedClickHelper(this, dom, domText));
}
dom.prepend(verControl);
}
}
};

View file

@ -429,27 +429,28 @@ html.rl-no-preview-pane {
}
}
*/
.b-openpgp-control {
}
color: #FA0;
cursor: pointer;
display: block;
opacity: 0.5;
margin: 15px;
.b-openpgp-control {
&:hover {
opacity: 1;
}
color: #FA0;
cursor: pointer;
display: block;
opacity: 0.5;
margin: 15px;
&.success {
color: green;
opacity: 1;
}
&:hover {
opacity: 1;
}
&.error {
color: red;
opacity: 1;
}
&.success {
color: green;
opacity: 1;
}
&.error {
color: red;
opacity: 1;
}
}
}

View file

@ -131,7 +131,7 @@ class ComposePopupView extends AbstractViewPopup {
this.bSkipNextHide = false;
this.capaOpenPGP = PgpUserStore.capaOpenPGP;
this.capaOpenPGP = !!PgpUserStore.openpgp;
this.identities = IdentityUserStore;
@ -552,7 +552,7 @@ class ComposePopupView extends AbstractViewPopup {
}
openOpenPgpPopup() {
if (PgpUserStore.capaOpenPGP() && !this.oEditor.isHtml()) {
if (PgpUserStore.openpgp && !this.oEditor.isHtml()) {
showScreenPopup(ComposeOpenPgpPopupView, [
result => this.editor(editor => editor.setPlain(result)),
this.oEditor.getData(false),

View file

@ -40,6 +40,67 @@ import Remote from 'Remote/User/Fetch';
import { decorateKoCommands, createCommand } from 'Knoin/Knoin';
import { AbstractViewRight } from 'Knoin/AbstractViews';
import { PgpUserStore } from 'Stores/User/Pgp';
function controlsHelper(dom, verControl, success, title, text)
{
dom.classList.toggle('error', !success);
dom.classList.toggle('success', success);
// verControl.classList.toggle('error', !success);
// verControl.classList.toggle('success', success);
dom.title = verControl.title = title;
if (undefined !== text) {
dom.textContent = text.trim();
}
}
function pgpClickHelper(dom, armoredMessage) {
if (dom.classList.contains('success') || dom.classList.contains('error')) {
return;
}
let message = null;
try {
message = PgpUserStore.openpgp.cleartext.readArmored(armoredMessage);
} catch (e) {
console.log(e);
}
if (message && message.getText && message.verify) {
PgpUserStore.verifyMessage(message, (validKey, signingKeyIds) => {
console.dir([validKey, signingKeyIds]);
/*
if (validKey) {
controlsHelper(
dom,
this,
true,
i18n('PGP_NOTIFICATIONS/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')'
}),
message.getText()
);
} else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
? keyIds.map(item => (item && item.toHex ? item.toHex() : null)).filter(v => v).join(', ')
: '';
controlsHelper(
dom,
this,
false,
i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : '')
);
}
*/
});
} else {
controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR'));
}
}
export class MailMessageView extends AbstractViewRight {
constructor() {
super('MailMessageView');
@ -176,6 +237,12 @@ export class MailMessageView extends AbstractViewRight {
return '';
},
pgpSigned: () => PgpUserStore.openpgp
&& MessageUserStore.message() && !!MessageUserStore.message().pgpSigned(),
pgpEncrypted: () => PgpUserStore.openpgp
&& MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(),
messageListOrViewLoading:
() => MessageUserStore.listIsLoading() | MessageUserStore.messageLoading()
});
@ -614,4 +681,14 @@ export class MailMessageView extends AbstractViewRight {
rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
}
}
pgpDecrypt(self/*, event*/) {
const message = self.message();
message && pgpClickHelper(message.body, message.plain(), message.getEmails(['from', 'to', 'cc']));
}
pgpVerify(self/*, event*/) {
const message = self.message();
message && pgpClickHelper(message.body, message.plain());
}
}