From a7eeeb4f55dd76e28bff841ea29c3b21c86b6757 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Thu, 20 Jan 2022 16:38:27 +0100 Subject: [PATCH] Many more changes for #89 --- dev/Common/Enums.js | 2 +- dev/Model/Message.js | 29 +-- dev/Model/OpenPgpKey.js | 6 +- dev/Screen/User/Settings.js | 2 +- dev/Settings/User/OpenPgp.js | 26 +- dev/Stores/User/Pgp.js | 232 +++++++++--------- dev/View/Popup/AddOpenPgpKey.js | 43 ++-- dev/View/Popup/Compose.js | 6 +- dev/View/Popup/NewOpenPgpKey.js | 12 +- dev/View/User/MailBox/MessageView.js | 61 ++++- .../0.0.0/app/libraries/RainLoop/Actions.php | 2 +- .../app/libraries/RainLoop/Actions/Pgp.php | 62 +++-- .../libraries/RainLoop/Actions/Response.php | 2 +- .../libraries/RainLoop/Enumerations/Capa.php | 2 +- .../app/libraries/snappymail/pgp/gnupg.php | 11 + .../Views/User/PopupsAddOpenPgpKey.html | 16 ++ .../Views/User/PopupsNewOpenPgpKey.html | 4 +- .../templates/Views/User/SettingsOpenPGP.html | 28 ++- 18 files changed, 310 insertions(+), 236 deletions(-) diff --git a/dev/Common/Enums.js b/dev/Common/Enums.js index 34840ec27..53320cf89 100644 --- a/dev/Common/Enums.js +++ b/dev/Common/Enums.js @@ -4,7 +4,7 @@ * @enum {string} */ export const Capa = { - GnuPGP: 'GNUGP', + GnuPG: 'GNUPG', OpenPGP: 'OPEN_PGP', Prefetch: 'PREFETCH', Contacts: 'CONTACTS', diff --git a/dev/Model/Message.js b/dev/Model/Message.js index dafbb0862..f9e92f29f 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -21,8 +21,9 @@ import { AbstractModel } from 'Knoin/AbstractModel'; import PreviewHTML from 'Html/PreviewMessage.html'; const - /*eslint-disable max-len*/ + // eslint-disable-next-line max-len url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi, + // eslint-disable-next-line max-len email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi, // Removes background and color @@ -644,30 +645,4 @@ export class MessageModel extends AbstractModel { ].join(','); } - pgpDecrypt() { -// const message = self.message(); -// message && pgpClickHelper(message.body, message.plain(), message.getEmails(['from', 'to', 'cc'])); -/* - pgpEncrypted: () => PgpUserStore.isSupported() - && MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(), -*/ - } - - pgpVerify() { - let params = this.pgpSigned(); // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" } - if (params) { - params.Folder = this.folder; - params.Uid = this.uid; - rl.app.Remote.post('MessagePgpVerify', null, params) - .then(data => { - // TODO - console.dir(data); - }) - .catch(error => { - // TODO - console.dir(error); - }); - } - } - } diff --git a/dev/Model/OpenPgpKey.js b/dev/Model/OpenPgpKey.js index 87f5d1b65..665f7e6d2 100644 --- a/dev/Model/OpenPgpKey.js +++ b/dev/Model/OpenPgpKey.js @@ -5,20 +5,17 @@ import { AbstractModel } from 'Knoin/AbstractModel'; export class OpenPgpKeyModel extends AbstractModel { /** - * @param {string} index * @param {string} guID * @param {string} ID * @param {array} IDs * @param {array} userIDs * @param {array} emails - * @param {boolean} isPrivate * @param {string} armor * @param {string} userID */ - constructor(index, guID, ID, IDs, userIDs, emails, isPrivate, armor, userID) { + constructor(guID, ID, IDs, userIDs, emails, armor, userID) { super(); - this.index = index; this.id = ID; this.ids = arrayLength(IDs) ? IDs : [ID]; this.guid = guID; @@ -27,7 +24,6 @@ export class OpenPgpKeyModel extends AbstractModel { this.email = ''; this.emails = emails; this.armor = armor; - this.isPrivate = !!isPrivate; if (this.users) { const index = this.users.indexOf(userID); diff --git a/dev/Screen/User/Settings.js b/dev/Screen/User/Settings.js index b0f17f4b6..d1e277341 100644 --- a/dev/Screen/User/Settings.js +++ b/dev/Screen/User/Settings.js @@ -57,7 +57,7 @@ export class SettingsUserScreen extends AbstractSettingsScreen { settingsAddViewModel(ThemesUserSettings, 'SettingsThemes', 'SETTINGS_LABELS/LABEL_THEMES_NAME', 'themes'); } - if (Settings.capa(Capa.OpenPGP) || Settings.capa(Capa.GnuPGP)) { + if (Settings.capa(Capa.OpenPGP) || Settings.capa(Capa.GnuPG)) { settingsAddViewModel(OpenPgpUserSettings, 'SettingsOpenPGP', 'OpenPGP', 'openpgp'); } diff --git a/dev/Settings/User/OpenPgp.js b/dev/Settings/User/OpenPgp.js index 0d1fe9a19..1c1a0d5ab 100644 --- a/dev/Settings/User/OpenPgp.js +++ b/dev/Settings/User/OpenPgp.js @@ -1,7 +1,5 @@ import ko from 'ko'; -import { delegateRunOnDestroy } from 'Common/UtilsUser'; - import { PgpUserStore } from 'Stores/User/Pgp'; import { SettingsUserStore } from 'Stores/User/Settings'; @@ -15,12 +13,15 @@ import { ViewOpenPgpKeyPopupView } from 'View/Popup/ViewOpenPgpKey'; export class OpenPgpUserSettings /*extends AbstractViewSettings*/ { constructor() { - this.openpgpkeys = PgpUserStore.openpgpkeys; - this.openpgpkeysPublic = PgpUserStore.openpgpkeysPublic; - this.openpgpkeysPrivate = PgpUserStore.openpgpkeysPrivate; + this.gnupgkeys = PgpUserStore.gnupgKeys; + this.openpgpkeysPublic = PgpUserStore.openpgpPublicKeys; + this.openpgpkeysPrivate = PgpUserStore.openpgpPrivateKeys; this.openPgpKeyForDeletion = ko.observable(null).deleteAccessHelper(); + this.canOpenPGP = !!PgpUserStore.openpgpKeyring; +// this.canOpenPGP = Settings.capa(Capa.OpenPGP); + this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave; this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value ? 1 : 0)) @@ -47,20 +48,7 @@ export class OpenPgpUserSettings /*extends AbstractViewSettings*/ { deleteOpenPgpKey(openPgpKeyToRemove) { if (openPgpKeyToRemove && openPgpKeyToRemove.deleteAccess()) { this.openPgpKeyForDeletion(null); - - if (openPgpKeyToRemove && PgpUserStore.openpgpKeyring) { - const findedItem = PgpUserStore.openpgpkeys.find(key => openPgpKeyToRemove === key); - if (findedItem) { - PgpUserStore.openpgpkeys.remove(findedItem); - delegateRunOnDestroy(findedItem); - - PgpUserStore.openpgpKeyring[findedItem.isPrivate ? 'privateKeys' : 'publicKeys'].removeForId(findedItem.guid); - - PgpUserStore.openpgpKeyring.store(); - } - - PgpUserStore.reloadOpenPgpKeys(); - } + PgpUserStore.deleteKey(openPgpKeyToRemove); } } } diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js index 6395eae4d..74934261b 100644 --- a/dev/Stores/User/Pgp.js +++ b/dev/Stores/User/Pgp.js @@ -3,7 +3,7 @@ import ko from 'ko'; import { Capa } from 'Common/Enums'; import { doc, createElement, Settings } from 'Common/Globals'; import { openPgpJs, openPgpWorkerJs } from 'Common/Links'; -import { isArray, arrayLength, pString, addComputablesTo } from 'Common/Utils'; +import { isArray, arrayLength } from 'Common/Utils'; import { delegateRunOnDestroy } from 'Common/UtilsUser'; import { showScreenPopup } from 'Knoin/Knoin'; @@ -16,27 +16,25 @@ import { OpenPgpKeyModel } from 'Model/OpenPgpKey'; import Remote from 'Remote/User/Fetch'; const - findKeyByHex = (keys, hash, isPrivate) => - keys.find(item => item && isPrivate == item.isPrivate && (hash === item.id || item.ids.includes(hash))), - findAllKeysByEmail = (keys, email, isPrivate) => - keys.filter(item => item && isPrivate == item.isPrivate && item.emails.includes(email)); + findKeyByHex = (keys, hash) => + keys.find(item => item && (hash === item.id || item.ids.includes(hash))); export const PgpUserStore = new class { constructor() { - // PECL gnupg / PEAR Crypt_GPG - this.gnupgkeys; + /** + * PECL gnupg / PEAR Crypt_GPG + * [ {email, can_encrypt, can_sign}, ... ] + */ + this.gnupgKeyring; + this.gnupgKeys = ko.observableArray(); // OpenPGP.js - this.openpgpkeys = ko.observableArray(); this.openpgpKeyring = null; + this.openpgpPublicKeys = ko.observableArray(); + this.openpgpPrivateKeys = ko.observableArray(); // https://mailvelope.github.io/mailvelope/Keyring.html this.mailvelopeKeyring = null; - - addComputablesTo(this, { - openpgpkeysPublic: () => this.openpgpkeys.filter(item => item && !item.isPrivate), - openpgpkeysPrivate: () => this.openpgpkeys.filter(item => item && item.isPrivate) - }); } init() { @@ -88,12 +86,14 @@ export const PgpUserStore = new class { this.reloadOpenPgpKeys(); } - if (Settings.capa(Capa.GnuPGP)) { - this.gnupgkeys = []; - Remote.request('GnupgGetKeysEmails', + if (Settings.capa(Capa.GnuPG)) { + this.gnupgKeyring = null; + this.gnupgKeys([]); + Remote.request('GnupgGetKeys', (iError, oData) => { - if (oData.Result) { - this.gnupgkeys = oData.Result; + if (oData && oData.Result) { + this.gnupgKeyring = oData.Result; + this.gnupgKeys(Object.values(oData.Result)); console.log('gnupg ready'); } } @@ -103,10 +103,11 @@ export const PgpUserStore = new class { reloadOpenPgpKeys() { if (this.openpgpKeyring) { - const keys = [], + const publicKeys = [], + privateKeys = [], email = new EmailModel(); - this.openpgpKeyring.getAllKeys().forEach((oItem, iIndex) => { + this.openpgpKeyring.getAllKeys().forEach(oItem => { if (oItem && oItem.primaryKey) { const aEmails = [], aUsers = [], @@ -132,9 +133,8 @@ export const PgpUserStore = new class { } if (aEmails.length) { - keys.push( + (oItem.isPrivate() ? privateKeys : publicKeys).push( new OpenPgpKeyModel( - iIndex, oItem.primaryKey.getFingerprint(), oItem.primaryKey .getKeyId() @@ -154,8 +154,12 @@ export const PgpUserStore = new class { } }); - delegateRunOnDestroy(this.openpgpkeys()); - this.openpgpkeys(keys); + delegateRunOnDestroy(this.openpgpPublicKeys()); + this.openpgpPublicKeys(publicKeys); + + delegateRunOnDestroy(this.openpgpPrivateKeys()); + this.openpgpPrivateKeys(privateKeys); + console.log('openpgp.js ready'); } } @@ -167,48 +171,19 @@ export const PgpUserStore = new class { return !!(window.openpgp || window.mailvelope); } - findPublicKeyByHex(hash) { - return findKeyByHex(this.openpgpkeys, hash, 0); - } - - findPrivateKeyByHex(hash) { - return findKeyByHex(this.openpgpkeys, hash, 1); - } - - findPublicKeysByEmail(email) { - return this.openpgpkeysPublic().map(item => { - const key = item && item.emails.includes(email) ? item : null; - return key ? key.getNativeKeys() : [null]; - }).flat().filter(v => v); - } - - findPublicKeysBySigningKeyIds(signingKeyIds) { - return signingKeyIds.map(id => { - const key = id && id.toHex ? this.findPublicKeyByHex(id.toHex()) : null; - return key ? key.getNativeKeys() : [null]; - }).flat().filter(v => v); - } - - findPrivateKeysByEncryptionKeyIds(encryptionKeyIds, recipients, returnWrapKeys) { - let result = isArray(encryptionKeyIds) - ? encryptionKeyIds.map(id => { - const key = id && id.toHex ? this.findPrivateKeyByHex(id.toHex()) : null; - return key ? (returnWrapKeys ? [key] : key.getNativeKeys()) : [null]; - }).flat().filter(v => v) - : []; - - if (!result.length && arrayLength(recipients)) { - result = recipients.map(sEmail => { - const keys = sEmail ? this.findAllPrivateKeysByEmailNotNative(sEmail) : null; - return keys - ? returnWrapKeys - ? keys - : keys.map(key => key.getNativeKeys()).flat() - : [null]; - }).flat().validUnique(key => key.id); + gnupgImportKey(key, callback) { + if (Settings.capa(Capa.GnuPG)) { + Remote.request('GnupgImportKey', + (iError, oData) => { + if (oData && oData.Result) { +// this.gnupgKeyring = oData.Result; + } + callback && callback(iError, oData); + }, { + Key: key + } + ); } - - return result; } /** @@ -216,45 +191,43 @@ export const PgpUserStore = new class { * Returns the first library that can. */ async hasPublicKeyForEmails(recipients, all) { - const - count = recipients.length, - openpgp = count && this.openpgpkeys && recipients.filter(email => - this.openpgpkeys.find(item => item && !item.isPrivate && item.emails.includes(email)) - ); - - if (this.gnupgkeys) { - let length = recipients.filter(email => this.gnupgkeys[email] && this.gnupgkeys[email].can_encrypt).length; + const count = recipients.length; + if (count) { + let length = this.gnupgKeyring && recipients.filter(email => + this.gnupgKeyring[email] && this.gnupgKeyring[email].can_encrypt).length; if (length && (!all || length === count)) { return 'gnupg'; } - } - if (openpgp && openpgp.length && (!all || openpgp.length === count)) { - return 'openpgp'; - } + length = this.openpgpKeyring && recipients.filter(email => + this.openpgpKeyring.publicKeys.getForAddress(email).length + ).length; + if (openpgp && (!all || openpgp === count)) { + return 'openpgp'; + } - let mailvelope = count && this.mailvelopeKeyring && await this.mailvelopeKeyring.validKeyForAddress(recipients) - /*.then(LookupResult => Object.entries(LookupResult))*/; - mailvelope = Object.entries(mailvelope); - if (mailvelope && mailvelope.length - && (all ? (mailvelope.filter(([, value]) => value).length === count) : mailvelope.find(([, value]) => value)) - ) { - return 'mailvelope'; + let mailvelope = this.mailvelopeKeyring && await this.mailvelopeKeyring.validKeyForAddress(recipients) + /*.then(LookupResult => Object.entries(LookupResult))*/; + mailvelope = Object.entries(mailvelope); + if (mailvelope && mailvelope.length + && (all ? (mailvelope.filter(([, value]) => value).length === count) : mailvelope.find(([, value]) => value)) + ) { + return 'mailvelope'; + } } - return false; } /** - * Checks if signing/decrypting a message is possible with given email address. + * Checks if signing a message is possible with given email address. * Returns the first library that can. */ - async hasPrivateKeyForEmail(email) { - if (this.gnupgkeys && this.gnupgkeys[email] && this.gnupgkeys[email].can_sign) { + async hasPrivateKeyFor(email, sign) { + if (this.gnupgKeyring && this.gnupgKeyring[email] && this.gnupgKeyring[email][sign?'can_sign':'can_decrypt']) { return 'gnupg'; } - if (this.openpgpkeys && this.openpgpkeys.find(item => item && item.isPrivate && item.emails.includes(email))) { + if (this.openpgpKeyring && this.openpgpKeyring.privateKeys.getForAddress(email).length) { return 'openpgp'; } @@ -273,47 +246,70 @@ export const PgpUserStore = new class { } /** - * @param {string} email - * @returns {?} + * Checks if signing a message is possible with given email address. + * Returns the first library that can. */ - findAllPublicKeysByEmailNotNative(email) { - return findAllKeysByEmail(this.openpgpkeys, email, 0); + async hasKeyForSigning(email) { + return await this.hasPrivateKeyFor(email, 1); } /** - * @param {string} email - * @returns {?} + * Checks if decrypting a message is possible with given email address. + * Returns the first library that can. */ - findAllPrivateKeysByEmailNotNative(email) { - return findAllKeysByEmail(this.openpgpkeys, email, 1); + async hasKeyForDecrypting(email) { + return await this.hasPrivateKeyFor(email, 0); } /** - * @param {string} email - * @param {string=} password - * @returns {?} + * OpenPGP.js */ - findPrivateKeyByEmail(email, password) { - let privateKey = null; - const key = this.openpgpkeys.find(item => item && item.isPrivate && item.emails.includes(email)); - if (key) { - try { - privateKey = key.getNativeKeys()[0] || null; - if (privateKey) { - privateKey.decrypt(pString(password)); + /** + * @param {OpenPgpKeyModel} openPgpKeyToRemove + * @returns {void} + */ + deleteKey(openPgpKeyToRemove) { + if (openPgpKeyToRemove && openPgpKeyToRemove.deleteAccess() && this.openpgpKeyring) { + let findedItem = this.openpgpPublicKeys.find(key => openPgpKeyToRemove === key); + if (findedItem) { + this.openpgpPublicKeys.remove(findedItem); + this.openpgpKeyring.publicKeys.removeForId(findedItem.guid); + } else { + findedItem = this.openpgpPrivateKeys.find(key => openPgpKeyToRemove === key); + if (findedItem) { + this.openpgpPrivateKeys.remove(findedItem); + this.openpgpKeyring.privateKeys.removeForId(findedItem.guid); } - } catch (e) { - privateKey = null; } + if (findedItem) { + delegateRunOnDestroy(findedItem); + this.openpgpKeyring.store(); + } +// this.reloadOpenPgpKeys(); } - - return privateKey; } decryptMessage(message, recipients, fCallback) { if (message && message.getEncryptionKeyIds) { - const privateKeys = this.findPrivateKeysByEncryptionKeyIds(message.getEncryptionKeyIds(), recipients, true); + // findPrivateKeysByEncryptionKeyIds + const encryptionKeyIds = message.getEncryptionKeyIds(); + let privateKeys = isArray(encryptionKeyIds) + ? encryptionKeyIds.map(id => { + // openpgpKeyring.publicKeys.getForId(id.toHex()) + // openpgpKeyring.privateKeys.getForId(id.toHex()) + const key = id && id.toHex ? findKeyByHex(this.openpgpPrivateKeys, id.toHex()) : null; + return key ? [key] : [null]; + }).flat().filter(v => v) + : []; + if (!privateKeys.length && arrayLength(recipients)) { + privateKeys = recipients.map(sEmail => + (sEmail + ? this.openpgpPrivateKeys.filter(item => item && item.emails.includes(sEmail)) : 0) + || [null] + ).flat().validUnique(key => key.id); + } + if (privateKeys && privateKeys.length) { showScreenPopup(MessageOpenPgpPopupView, [ (decryptedKey) => { @@ -322,7 +318,7 @@ export const PgpUserStore = new class { (decryptedMessage) => { let privateKey = null; if (decryptedMessage) { - privateKey = this.findPrivateKeyByHex(decryptedKey.primaryKey.keyid.toHex()); + privateKey = findKeyByHex(this.openpgpPrivateKeys, decryptedKey.primaryKey.keyid.toHex()); if (privateKey) { this.verifyMessage(decryptedMessage, (oValidKey, aSigningKeyIds) => { fCallback(privateKey, decryptedMessage, oValidKey || null, aSigningKeyIds || null); @@ -358,14 +354,18 @@ export const PgpUserStore = new class { if (message && message.getSigningKeyIds) { const signingKeyIds = message.getSigningKeyIds(); if (signingKeyIds && signingKeyIds.length) { - const publicKeys = this.findPublicKeysBySigningKeyIds(signingKeyIds); + // findPublicKeysBySigningKeyIds + const publicKeys = signingKeyIds.map(id => { + const key = id && id.toHex ? findKeyByHex(this.openpgpPublicKeys, id.toHex()) : null; + return key ? key.getNativeKeys() : [null]; + }).flat().filter(v => v); if (publicKeys && publicKeys.length) { try { const result = message.verify(publicKeys), valid = (isArray(result) ? result : []).find(item => item && item.valid && item.keyid); if (valid && valid.keyid && valid.keyid && valid.keyid.toHex) { - fCallback(this.findPublicKeyByHex(valid.keyid.toHex())); + fCallback(findKeyByHex(this.openpgpPublicKeys, valid.keyid.toHex())); return true; } } catch (e) { diff --git a/dev/View/Popup/AddOpenPgpKey.js b/dev/View/Popup/AddOpenPgpKey.js index e548c391e..a7345bc94 100644 --- a/dev/View/Popup/AddOpenPgpKey.js +++ b/dev/View/Popup/AddOpenPgpKey.js @@ -3,6 +3,9 @@ import { PgpUserStore } from 'Stores/User/Pgp'; import { decorateKoCommands } from 'Knoin/Knoin'; import { AbstractViewPopup } from 'Knoin/AbstractViews'; +import { Capa } from 'Common/Enums'; +import { Settings } from 'Common/Globals'; + class AddOpenPgpKeyPopupView extends AbstractViewPopup { constructor() { super('AddOpenPgpKey'); @@ -10,9 +13,15 @@ class AddOpenPgpKeyPopupView extends AbstractViewPopup { this.addObservables({ key: '', keyError: false, - keyErrorMessage: '' + keyErrorMessage: '', + + saveGnuPG: true, + saveOpenPGP: true }); + this.canGnuPG = Settings.capa(Capa.GnuPG); + this.canOpenPGP = Settings.capa(Capa.OpenPGP); + this.key.subscribe(() => { this.keyError(false); this.keyErrorMessage(''); @@ -24,36 +33,40 @@ class AddOpenPgpKeyPopupView extends AbstractViewPopup { } addOpenPgpKeyCommand() { - // eslint-disable-next-line max-len - const reg = /[-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}[\s\S]+?[-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}/gi, - openpgpKeyring = PgpUserStore.openpgpKeyring; - let keyTrimmed = this.key().trim(); - if (/[\n]/.test(keyTrimmed)) { - keyTrimmed = keyTrimmed.replace(/[\r]+/g, '').replace(/[\n]{2,}/g, '\n\n'); + if (/\n/.test(keyTrimmed)) { + keyTrimmed = keyTrimmed.replace(/\r+/g, '').replace(/\n{2,}/g, '\n\n'); } this.keyError(!keyTrimmed); this.keyErrorMessage(''); - if (!openpgpKeyring || this.keyError()) { + if (!keyTrimmed) { return false; } let match = null, count = 30, done = false; + // eslint-disable-next-line max-len + const reg = /[-]{3,6}BEGIN[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}[\s\S]+?[-]{3,6}END[\s]PGP[\s](PRIVATE|PUBLIC)[\s]KEY[\s]BLOCK[-]{3,6}/gi, + keyring = PgpUserStore.openpgpKeyring; do { match = reg.exec(keyTrimmed); if (match && 0 < count) { if (match[0] && match[1] && match[2] && match[1] === match[2]) { let err = null; - if ('PRIVATE' === match[1]) { - err = openpgpKeyring.privateKeys.importKey(match[0]); - } else if ('PUBLIC' === match[1]) { - err = openpgpKeyring.publicKeys.importKey(match[0]); + if (this.saveGnuPG()) { + PgpUserStore.gnupgImportKey(this.key()); + } + if (this.canOpenPGP && this.saveOpenPGP()) { + if ('PRIVATE' === match[1]) { + err = keyring.privateKeys.importKey(match[0]); + } else if ('PUBLIC' === match[1]) { + err = keyring.publicKeys.importKey(match[0]); + } } if (err) { @@ -70,9 +83,11 @@ class AddOpenPgpKeyPopupView extends AbstractViewPopup { } } while (!done); - openpgpKeyring.store(); + if (this.canOpenPGP && this.saveOpenPGP()) { + keyring.store(); + } - PgpUserStore.reloadOpenPgpKeys(); +// PgpUserStore.reloadOpenPgpKeys(); if (this.keyError()) { return false; diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 011e4cbda..032a48c90 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -133,8 +133,6 @@ class ComposePopupView extends AbstractViewPopup { this.capaOpenPGP = PgpUserStore.isSupported(); - this.identities = IdentityUserStore; - this.addObservables({ identitiesDropdownTrigger: false, @@ -189,7 +187,7 @@ class ComposePopupView extends AbstractViewPopup { // div.textAreaParent composeEditorArea: null, - currentIdentity: this.identities()[0] ? this.identities()[0] : null + currentIdentity: IdentityUserStore()[0] || null }); // this.to.subscribe((v) => console.log(v)); @@ -281,7 +279,7 @@ class ComposePopupView extends AbstractViewPopup { currentIdentity: value => { this.canPgpSign(false); - value && PgpUserStore.hasPrivateKeyForEmail(value.email()).then(result => { + value && PgpUserStore.hasKeyForSigning(value.email()).then(result => { console.log({canPgpSign:result}); this.canPgpSign(result) }); diff --git a/dev/View/Popup/NewOpenPgpKey.js b/dev/View/Popup/NewOpenPgpKey.js index ae68244f1..673bfc9f8 100644 --- a/dev/View/Popup/NewOpenPgpKey.js +++ b/dev/View/Popup/NewOpenPgpKey.js @@ -1,6 +1,7 @@ import { pInt } from 'Common/Utils'; import { PgpUserStore } from 'Stores/User/Pgp'; +import { IdentityUserStore } from 'Stores/User/Identity'; import { decorateKoCommands } from 'Knoin/Knoin'; import { AbstractViewPopup } from 'Knoin/AbstractViews'; @@ -9,6 +10,8 @@ class NewOpenPgpKeyPopupView extends AbstractViewPopup { constructor() { super('NewOpenPgpKey'); + this.identities = IdentityUserStore; + this.addObservables({ email: '', emailError: false, @@ -63,6 +66,9 @@ class NewOpenPgpKeyPopupView extends AbstractViewPopup { openpgpKeyring.store(); PgpUserStore.reloadOpenPgpKeys(); + + PgpUserStore.gnupgImportKey(keyPair.privateKeyArmored); + this.cancelCommand(); } }) @@ -87,13 +93,11 @@ class NewOpenPgpKeyPopupView extends AbstractViewPopup { } onShow() { - this.name(''); + this.name(IdentityUserStore()[0].name()); this.password(''); - - this.email(''); + this.email(IdentityUserStore()[0].email()); this.emailError(false); this.keyBitLength(4096); - this.submitError(''); } } diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index f3fe4a504..50610fad7 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -180,8 +180,7 @@ export class MailMessageView extends AbstractViewRight { pgpSigned: () => MessageUserStore.message() && !!MessageUserStore.message().pgpSigned(), - pgpEncrypted: () => PgpUserStore.isSupported() - && MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(), + pgpEncrypted: () => MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(), messageListOrViewLoading: () => MessageUserStore.listIsLoading() | MessageUserStore.messageLoading() @@ -624,11 +623,65 @@ export class MailMessageView extends AbstractViewRight { pgpDecrypt(self) { const message = self.message(); - message && message.pgpDecrypt(); + if (message && PgpUserStore.isSupported()) { +// pgpClickHelper(message.body, message.plain(), message.getEmails(['from', 'to', 'cc'])); +/* + pgpEncrypted: () => PgpUserStore.isSupported() + && MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(), +*/ + } } pgpVerify(self) { const message = self.message(); - message && message.pgpVerify(); + if (message && PgpUserStore.isSupported()) { + const mode = PgpUserStore.hasPublicKeyForEmails([message.from[0].email()]); + if ('gnupg' === mode) { + let params = message.pgpSigned(); // { BodyPartId: "1", SigPartId: "2", MicAlg: "pgp-sha256" } + if (params) { + params.Folder = message.folder; + params.Uid = message.uid; + rl.app.Remote.post('MessagePgpVerify', null, params) + .then(data => { + // TODO + console.dir(data); + }) + .catch(error => { + // TODO + console.dir(error); + }); + } + } else if ('openpgp' === mode) { + let text = null; + try { + text = PgpUserStore.openpgp.cleartext.readArmored(message.plain); + } catch (e) { + console.error(e); + } + if (text && text.getText && text.verify) { + PgpUserStore.verifyMessage(text, (validKey, signingKeyIds) => { + console.dir([validKey, signingKeyIds]); +/* + if (validKey) { + 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(', ') + : ''; + + i18n('PGP_NOTIFICATIONS/UNVERIFIRED_SIGNATURE') + (additional ? ' (' + additional + ')' : ''); + } +*/ + }); + } else { +// controlsHelper(dom, this, false, i18n('PGP_NOTIFICATIONS/DECRYPTION_ERROR')); + } + } + } } + } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index 24365499b..b8499f9a8 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -1192,7 +1192,7 @@ class Actions $aResult[] = Enumerations\Capa::OPEN_PGP; } if (\SnappyMail\PGP\GnuPG::isSupported()) { - $aResult[] = Enumerations\Capa::GNUGP; + $aResult[] = Enumerations\Capa::GNUPG; } if ($bAdmin || ($oAccount && $oAccount->Domain()->UseSieve())) { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php index fba5bc918..3eb159341 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php @@ -16,50 +16,48 @@ trait Pgp return \SnappyMail\PGP\GnuPG::getInstance($pgp_dir); } - public function DoGnupgGetKeysEmails() : array + public function DoGnupgGetKeys() : array { $GPG = $this->GnuPG(); if ($GPG) { $keys = []; + /** + * PECL GnuPG can't list private + * + * gpg --list-secret-keys + * gpg --list-public-keys + */ foreach ($GPG->keyInfo('') as $info) { if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) { + $info['can_verify'] = $info['can_sign']; + $info['can_sign'] = $info['can_decrypt'] = false; + foreach ($info['subkeys'] as $key) { + $hasKey = $GPG->hasPrivateKey($key['keygrip']); + $info['can_sign'] = $info['can_sign'] || ($info['can_verify'] && $hasKey); + $info['can_decrypt'] = $info['can_decrypt'] || ($info['can_encrypt'] && $hasKey); + } foreach ($info['uids'] as $uid) { $id = $uid['email']; if (isset($keys[$id])) { - $keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign']; + // Public Key tasks + $keys[$id]['can_verify'] = $keys[$id]['can_verify'] || $info['can_verify']; $keys[$id]['can_encrypt'] = $keys[$id]['can_encrypt'] || $info['can_encrypt']; + // Private Key tasks + $keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign']; + $keys[$id]['can_decrypt'] = $keys[$id]['can_decrypt'] || $info['can_decrypt']; } else { $keys[$id] = [ 'name' => $uid['name'], 'email' => $uid['email'], + // Public Key tasks + 'can_verify' => $info['can_sign'], + 'can_encrypt' => $info['can_encrypt'], + // Private Key tasks 'can_sign' => $info['can_sign'], - 'can_encrypt' => $info['can_encrypt'] + 'can_decrypt' => $info['can_decrypt'] ]; } } - /* - foreach ($info['subkeys'] as $key) { - $key['can_authenticate'] = true - $key['can_certify'] = true - $key['can_encrypt'] = true - $key['can_sign'] = true - $key['disabled'] = false - $key['expired'] = false - $key['expires'] = 0 - $key['fingerprint'] = "99BBB6F2FDDE9E20CD78B98DC85B364A5A6CCF52" - $key['invalid'] = false - $key['is_cardkey'] = false - $key['is_de_vs'] = true - $key['is_qualified'] = false - $key['is_secret'] = false - $key['keygrip'] = "CBCCF45D4F6D300417F044A08E08F8F14522BABE" - $key['keyid'] = "C85B364A5A6CCF52" - $key['length'] = 4096 - $key['pubkey_algo'] = 1 - $key['revoked'] = false - $key['timestamp'] = 1428449321 - } - */ } } return $this->DefaultResponse(__FUNCTION__, $keys); @@ -67,13 +65,13 @@ trait Pgp return $this->FalseResponse(__FUNCTION__); } - public function DoPgpImportKey() : array + public function DoGnupgImportKey() : array { + $sKey = $this->GetActionParam('Key', ''); $sKeyId = $this->GetActionParam('KeyId', ''); - $sPublicKey = $this->GetActionParam('PublicKey', ''); $sEmail = $this->GetActionParam('Email', ''); - if (!$sPublicKey) { + if (!$sKey) { try { if (!$sKeyId) { if (\preg_match('/[^\\s<>]+@[^\\s<>]+/', $sEmail, $aMatch)) { @@ -97,16 +95,16 @@ trait Pgp } } if ($sKeyId) { - $sPublicKey = \SnappyMail\PGP\Keyservers::get($sKeyId); + $sKey = \SnappyMail\PGP\Keyservers::get($sKeyId); } } catch (\Throwable $e) { // ignore } } - $GPG = $sPublicKey ? $this->GnuPG() : null; + $GPG = $sKey ? $this->GnuPG() : null; return $GPG - ? $this->DefaultResponse(__FUNCTION__, $GPG->import($sPublicKey)) + ? $this->DefaultResponse(__FUNCTION__, $GPG->import($sKey)) : $this->FalseResponse(__FUNCTION__); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php index 4ae45530b..9962f6244 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php @@ -254,7 +254,7 @@ trait Response $mResult['Plain'] = $mResponse->Plain(); -// $this->GetCapa(false, Capa::OPEN_PGP) || $this->GetCapa(false, Capa::GNUGP) +// $this->GetCapa(false, Capa::OPEN_PGP) || $this->GetCapa(false, Capa::GNUPG) $mResult['isPgpEncrypted'] = $mResponse->isPgpEncrypted(); $mResult['PgpSigned'] = $mResponse->PgpSigned(); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php index f11ab8269..46a314a63 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php @@ -4,7 +4,7 @@ namespace RainLoop\Enumerations; class Capa { - const GNUGP = 'GNUGP'; + const GNUPG = 'GNUPG'; const OPEN_PGP = 'OPEN_PGP'; const PREFETCH = 'PREFETCH'; const THEMES = 'THEMES'; diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php index af7f8ea4b..bc8f4a08a 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php @@ -374,6 +374,17 @@ class GnuPG return false; } + /** + * Returns an array with information about all keys that matches the given pattern + */ + public function hasPrivateKey(string $keygrip) : bool + { + if ($this->GnuPG || $this->Crypt_GPG) { + return \is_file("{$this->homedir}/private-keys-v1.d/{$keygrip}.key"); + } + return false; + } + /** * Toggle armored output * When true the output is ASCII diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsAddOpenPgpKey.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsAddOpenPgpKey.html index 98d4075e8..2f468d80e 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsAddOpenPgpKey.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsAddOpenPgpKey.html @@ -8,6 +8,22 @@