diff --git a/.eslintrc.js b/.eslintrc.js index 5efeabffe..851a1868e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,7 +35,9 @@ module.exports = { // vendors/jua 'Jua': "readonly", // vendors/bootstrap/bootstrap.native.js - 'BSN': "readonly" + 'BSN': "readonly", + // Mailvelope + 'mailvelope': "readonly" }, // http://eslint.org/docs/rules/ rules: { diff --git a/dev/App/User.js b/dev/App/User.js index 0036860da..6e6fda3d2 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -4,7 +4,6 @@ import { isArray, arrayLength, pString, forEachObjectValue } from 'Common/Utils' import { delegateRunOnDestroy, mailToHelper, setLayoutResizer } from 'Common/UtilsUser'; import { - Capa, Notification, Scope } from 'Common/Enums'; @@ -36,11 +35,7 @@ import { getFolderFromCacheList } from 'Common/Cache'; -import { - mailBox, - openPgpWorkerJs, - openPgpJs -} from 'Common/Links'; +import { mailBox } from 'Common/Links'; import { getNotification, i18n } from 'Common/Translator'; @@ -61,7 +56,6 @@ import Remote from 'Remote/User/Fetch'; import { EmailModel } from 'Model/Email'; import { AccountModel } from 'Model/Account'; import { IdentityModel } from 'Model/Identity'; -import { OpenPgpKeyModel } from 'Model/OpenPgpKey'; import { LoginUserScreen } from 'Screen/User/Login'; import { MailBoxUserScreen } from 'Screen/User/MailBox'; @@ -346,66 +340,6 @@ class AppUser extends AbstractApp { return false; } - reloadOpenPgpKeys() { - if (PgpUserStore.openpgp) { - const keys = [], - email = new EmailModel(), - openpgpKeyring = PgpUserStore.openpgpKeyring, - openpgpKeys = openpgpKeyring ? openpgpKeyring.getAllKeys() : []; - - openpgpKeys.forEach((oItem, iIndex) => { - if (oItem && oItem.primaryKey) { - const aEmails = [], - aUsers = [], - primaryUser = oItem.getPrimaryUser(), - user = - primaryUser && primaryUser.user - ? primaryUser.user.userId.userid - : oItem.users && oItem.users[0] - ? oItem.users[0].userId.userid - : ''; - - if (oItem.users) { - oItem.users.forEach(item => { - if (item.userId) { - email.clear(); - email.parse(item.userId.userid); - if (email.validate()) { - aEmails.push(email.email); - aUsers.push(item.userId.userid); - } - } - }); - } - - if (aEmails.length) { - keys.push( - new OpenPgpKeyModel( - iIndex, - oItem.primaryKey.getFingerprint(), - oItem.primaryKey - .getKeyId() - .toHex() - .toLowerCase(), - oItem.getKeyIds() - .map(item => (item && item.toHex ? item.toHex() : null)) - .validUnique(), - aUsers, - aEmails, - oItem.isPrivate(), - oItem.armor(), - user - ) - ); - } - } - }); - - delegateRunOnDestroy(PgpUserStore.openpgpkeys()); - PgpUserStore.openpgpkeys(keys); - } - } - accountsAndIdentities() { AccountUserStore.loading(true); IdentityUserStore.loading(true); @@ -772,23 +706,7 @@ class AppUser extends AbstractApp { setInterval(this.reloadTime(), 60000); - if (window.crypto && crypto.getRandomValues && Settings.capa(Capa.OpenPGP)) { - const script = createElement('script', {src:openPgpJs()}); - script.onload = () => { - PgpUserStore.openpgp = openpgp; - if (window.Worker) { - try { - PgpUserStore.openpgp.initWorker({ path: openPgpWorkerJs() }); - } catch (e) { - console.error(e); - } - } - PgpUserStore.openpgpKeyring = new openpgp.Keyring(); - this.reloadOpenPgpKeys(); - }; - script.onerror = () => console.error(script.src); - doc.head.append(script); - } + PgpUserStore.init(); } else { this.logout(); } diff --git a/dev/Model/Message.js b/dev/Model/Message.js index 2ba35074a..dafbb0862 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -648,7 +648,7 @@ export class MessageModel extends AbstractModel { // const message = self.message(); // message && pgpClickHelper(message.body, message.plain(), message.getEmails(['from', 'to', 'cc'])); /* - pgpEncrypted: () => PgpUserStore.openpgp + pgpEncrypted: () => PgpUserStore.isSupported() && MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(), */ } @@ -660,9 +660,11 @@ export class MessageModel extends AbstractModel { 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 df5aa7449..89c1c8f1d 100644 --- a/dev/Model/OpenPgpKey.js +++ b/dev/Model/OpenPgpKey.js @@ -2,7 +2,6 @@ import ko from 'ko'; import { arrayLength } from 'Common/Utils'; import { AbstractModel } from 'Knoin/AbstractModel'; -import { PgpUserStore } from 'Stores/User/Pgp'; export class OpenPgpKeyModel extends AbstractModel { /** @@ -35,25 +34,21 @@ export class OpenPgpKeyModel extends AbstractModel { this.deleteAccess = ko.observable(false); } - getNativeKey() { - let key = null; + /** + * OpenPGP.js + */ + getNativeKeys() { try { - key = PgpUserStore.openpgp.key.readArmored(this.armor); + let key = openpgp.key.readArmored(this.armor); if (key && !key.err && key.keys && key.keys[0]) { - return key; + return key.keys; } } catch (e) { - console.log(e); + console.error(e); } - return null; } - getNativeKeys() { - const key = this.getNativeKey(); - return key && key.keys ? key.keys : null; - } - select(pattern, property) { if (this[property]) { const index = this[property].indexOf(pattern); diff --git a/dev/Settings/User/OpenPgp.js b/dev/Settings/User/OpenPgp.js index 52a5933c7..0d1fe9a19 100644 --- a/dev/Settings/User/OpenPgp.js +++ b/dev/Settings/User/OpenPgp.js @@ -59,7 +59,7 @@ export class OpenPgpUserSettings /*extends AbstractViewSettings*/ { PgpUserStore.openpgpKeyring.store(); } - rl.app.reloadOpenPgpKeys(); + PgpUserStore.reloadOpenPgpKeys(); } } } diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js index c7578284b..1b7967d58 100644 --- a/dev/Stores/User/Pgp.js +++ b/dev/Stores/User/Pgp.js @@ -1,43 +1,169 @@ 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 { AccountUserStore } from 'Stores/User/Account'; +import { delegateRunOnDestroy } from 'Common/UtilsUser'; import { showScreenPopup } from 'Knoin/Knoin'; import { MessageOpenPgpPopupView } from 'View/Popup/MessageOpenPgp'; +import { EmailModel } from 'Model/Email'; +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)); + export const PgpUserStore = new class { constructor() { - this.openpgp = null; + // PECL gnupg / PEAR Crypt_GPG + this.gnupgkeys; + // OpenPGP.js this.openpgpkeys = ko.observableArray(); this.openpgpKeyring = null; + // 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() { + if (Settings.capa(Capa.OpenPGP)) { + if (window.crypto && crypto.getRandomValues) { + const script = createElement('script', {src:openPgpJs()}); + script.onload = () => { + if (window.Worker) { + try { + openpgp.initWorker({ path: openPgpWorkerJs() }); + } catch (e) { + console.error(e); + } + } + this.loadKeyrings(); + }; + script.onerror = () => console.error(script.src); + doc.head.append(script); + } else { + this.loadKeyrings(); + } + } + } + + loadKeyrings(identifier) { + if (window.mailvelope) { + console.log('mailvelope ready'); + var fn = keyring => this.mailvelopeKeyring = keyring; + mailvelope.getKeyring().then(fn, err => { + if (identifier) { + // attempt to create a new keyring for this app/user + mailvelope.createKeyring(identifier).then(fn, err => console.error(err)); + } else { + console.error(err); + } + }); + addEventListener('mailvelope-disconnect', event => { + alert('Mailvelope is updated to version ' + event.detail.version + '. Reload page'); + }, false); + } else { + addEventListener('mailvelope', () => this.loadKeyrings(identifier)); + } + if (openpgp) { + console.log('openpgp.js ready'); + this.openpgpKeyring = new openpgp.Keyring(); + this.reloadOpenPgpKeys(); + } + + this.gnupgkeys = []; + Remote.request('PgpGetKeysEmails', + (iError, oData) => { + console.dir(oData); + } + ); + } + + reloadOpenPgpKeys() { + if (this.openpgpKeyring) { + const keys = [], + email = new EmailModel(); + + this.openpgpKeyring.getAllKeys().forEach((oItem, iIndex) => { + if (oItem && oItem.primaryKey) { + const aEmails = [], + aUsers = [], + primaryUser = oItem.getPrimaryUser(), + user = + primaryUser && primaryUser.user + ? primaryUser.user.userId.userid + : oItem.users && oItem.users[0] + ? oItem.users[0].userId.userid + : ''; + + if (oItem.users) { + oItem.users.forEach(item => { + if (item.userId) { + email.clear(); + email.parse(item.userId.userid); + if (email.validate()) { + aEmails.push(email.email); + aUsers.push(item.userId.userid); + } + } + }); + } + + if (aEmails.length) { + keys.push( + new OpenPgpKeyModel( + iIndex, + oItem.primaryKey.getFingerprint(), + oItem.primaryKey + .getKeyId() + .toHex() + .toLowerCase(), + oItem.getKeyIds() + .map(item => (item && item.toHex ? item.toHex() : null)) + .validUnique(), + aUsers, + aEmails, + oItem.isPrivate(), + oItem.armor(), + user + ) + ); + } + } + }); + + delegateRunOnDestroy(this.openpgpkeys()); + this.openpgpkeys(keys); + } + } + /** * @returns {boolean} */ isSupported() { - return !!this.openpgp; - } - - findKeyByHex(keys, hash) { - return keys.find(item => hash && item && (hash === item.id || item.ids.includes(hash))); + return !!(window.openpgp || window.mailvelope); } findPublicKeyByHex(hash) { - return this.findKeyByHex(this.openpgpkeysPublic(), hash); + return findKeyByHex(this.openpgpkeys, hash, 0); } findPrivateKeyByHex(hash) { - return this.findKeyByHex(this.openpgpkeysPrivate(), hash); + return findKeyByHex(this.openpgpkeys, hash, 1); } findPublicKeysByEmail(email) { @@ -77,19 +203,53 @@ export const PgpUserStore = new class { } /** - * @param {string} email - * @returns {?} + * Checks if verifying/encrypting a message is possible with given email addresses. + * Returns the first library that can. */ - findPublicKeyByEmailNotNative(email) { - return this.openpgpkeysPublic().find(item => item && item.emails.includes(email)) || null; + 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 (openpgp && openpgp.length && (!all || openpgp.length === 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'; + } + + return false; } /** - * @param {string} email - * @returns {?} + * Checks if signing/decrypting a message is possible with given email address. + * Returns the first library that can. */ - findPrivateKeyByEmailNotNative(email) { - return this.openpgpkeysPrivate().find(item => item && item.emails.includes(email)) || null; + async hasPrivateKeyForEmail(email) { + if (this.openpgpkeys && this.openpgpkeys.find(item => item && item.isPrivate && item.emails.includes(email))) { + return 'openpgp'; + } + + let keyring = this.mailvelopeKeyring; + if (keyring) { + /** + * Mailvelope can't find by email, so we must get the fingerprint and use that instead + */ + let keys = await keyring.validKeyForAddress([email]); + if (keys && keys[email] && await keyring.hasPrivateKey(keys[email].keys[0].fingerprint)) { + return 'mailvelope'; + } + } + + return false; } /** @@ -97,7 +257,7 @@ export const PgpUserStore = new class { * @returns {?} */ findAllPublicKeysByEmailNotNative(email) { - return this.openpgpkeysPublic().filter(item => item && item.emails.includes(email)) || null; + return findAllKeysByEmail(this.openpgpkeys, email, 0); } /** @@ -105,7 +265,7 @@ export const PgpUserStore = new class { * @returns {?} */ findAllPrivateKeysByEmailNotNative(email) { - return this.openpgpkeysPrivate().filter(item => item && item.emails.includes(email)) || null; + return findAllKeysByEmail(this.openpgpkeys, email, 1); } /** @@ -115,7 +275,7 @@ export const PgpUserStore = new class { */ findPrivateKeyByEmail(email, password) { let privateKey = null; - const key = this.openpgpkeysPrivate().find(item => item && item.emails.includes(email)); + const key = this.openpgpkeys.find(item => item && item.isPrivate && item.emails.includes(email)); if (key) { try { @@ -131,14 +291,6 @@ export const PgpUserStore = new class { return privateKey; } - /** - * @param {string=} password - * @returns {?} - */ - findSelfPrivateKey(password) { - return this.findPrivateKeyByEmail(AccountUserStore.email(), password); - } - decryptMessage(message, recipients, fCallback) { if (message && message.getEncryptionKeyIds) { const privateKeys = this.findPrivateKeysByEncryptionKeyIds(message.getEncryptionKeyIds(), recipients, true); @@ -210,4 +362,81 @@ export const PgpUserStore = new class { return false; } + /** + * Creates an iframe to display the decrypted content of the encrypted mail. + * The iframe will be injected into the container identified by selector. + */ +/* + mailvelope.createDisplayContainer(selector, armored, this.mailvelopeKeyring, {senderAddress:''}).then(status => { + if (status.error && status.error.message) { + return error_handler(status.error); + } + + ref.hide_message(msgid); + $(selector).children().not('iframe').hide(); + $(ref.gui_objects.messagebody).addClass('mailvelope'); + + // on success we can remove encrypted part from the attachments list + if (ref.env.pgp_mime_part) + $('#attach' + ref.env.pgp_mime_part).remove(); + + setTimeout(function() { $(window).resize(); }, 10); + }, error_handler); +*/ + /** + * Creates an iframe with an editor for a new encrypted mail. + * The iframe will be injected into the container identified by selector. + * https://mailvelope.github.io/mailvelope/Editor.html + */ +/* + mailvelope.createEditorContainer(selector, this.mailvelopeKeyring, { + quota: 20480, // mail content (text + attachments) limit in kilobytes (default: 20480) + signMsg: false, // if true then the mail will be signed (default: false) + armoredDraft: '', // Ascii Armored PGP Text Block + a PGP message, signed and encrypted with the default key of the user, will be used to restore a draft in the editor + The armoredDraft parameter can't be combined with the parameters: predefinedText, quotedMail... parameters, keepAttachments + predefinedText: '', // text that will be added to the editor + quotedMail: '', // Ascii Armored PGP Text Block mail that should be quoted + quotedMailIndent: true, // if true the quoted mail will be indented (default: true) + quotedMailHeader: '', // header to be added before the quoted mail + keepAttachments: false, // add attachments of quotedMail to editor (default: false) + }).then(editor => { + editor.editorId; + }, error_handler) +*/ + + /** + * Creates an iframe to display the keyring settings. + * The iframe will be injected into the container identified by selector. + */ +/* + mailvelope.createSettingsContainer(selector [, keyring], options) +*/ + + /** + * Returns headers that should be added to an outgoing email. + * So far this is only the autocrypt header. + */ +/* + this.mailvelopeKeyring.additionalHeadersForOutgoingEmail(headers) +*/ + +/* + this.mailvelopeKeyring.addSyncHandler(syncHandlerObj) +*/ +/* + this.mailvelopeKeyring.createKeyGenContainer(selector, { +// userIds: [], + keySize: 4096 + }) +*/ + +/* + exportOwnPublicKey(emailAddr).then() + + this.mailvelopeKeyring.hasPrivateKey(fingerprint) + + this.mailvelopeKeyring.importPublicKey(armored) +*/ + }; diff --git a/dev/View/Popup/AddOpenPgpKey.js b/dev/View/Popup/AddOpenPgpKey.js index 3855df629..e548c391e 100644 --- a/dev/View/Popup/AddOpenPgpKey.js +++ b/dev/View/Popup/AddOpenPgpKey.js @@ -72,7 +72,7 @@ class AddOpenPgpKeyPopupView extends AbstractViewPopup { openpgpKeyring.store(); - rl.app.reloadOpenPgpKeys(); + PgpUserStore.reloadOpenPgpKeys(); if (this.keyError()) { return false; diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index b519b4999..c7ea7d265 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -34,6 +34,7 @@ import { MessageUserStore } from 'Stores/User/Message'; import Remote from 'Remote/User/Fetch'; import { ComposeAttachmentModel } from 'Model/ComposeAttachment'; +import { EmailModel } from 'Model/Email'; import { decorateKoCommands, isPopupVisible, showScreenPopup } from 'Knoin/Knoin'; import { AbstractViewPopup } from 'Knoin/AbstractViews'; @@ -41,7 +42,6 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews'; import { FolderSystemPopupView } from 'View/Popup/FolderSystem'; import { AskPopupView } from 'View/Popup/Ask'; import { ContactsPopupView } from 'View/Popup/Contacts'; -import { ComposeOpenPgpPopupView } from 'View/Popup/ComposeOpenPgp'; import { ThemeStore } from 'Stores/Theme'; @@ -131,7 +131,7 @@ class ComposePopupView extends AbstractViewPopup { this.bSkipNextHide = false; - this.capaOpenPGP = !!PgpUserStore.openpgp; + this.capaOpenPGP = PgpUserStore.isSupported(); this.identities = IdentityUserStore; @@ -169,6 +169,11 @@ class ComposePopupView extends AbstractViewPopup { showBcc: false, showReplyTo: false, + pgpSign: false, + pgpEncrypt: false, + canPgpSign: false, + canPgpEncrypt: false, + draftsFolder: '', draftUid: 0, sending: false, @@ -265,7 +270,6 @@ class ComposePopupView extends AbstractViewPopup { }, canBeSentOrSaved: () => !this.sending() && !this.saving() - }); this.addSubscribables({ @@ -275,16 +279,26 @@ class ComposePopupView extends AbstractViewPopup { sendSuccessButSaveError: value => !value && this.savedErrorDesc(''), + currentIdentity: value => { + this.canPgpSign(false); + value && PgpUserStore.hasPrivateKeyForEmail(value.email()).then(result => { + console.log({canPgpSign:result}); + this.canPgpSign(result) + }); + }, + cc: value => { if (false === this.showCc() && value.length) { this.showCc(true); } + this.initPgpEncrypt(); }, bcc: value => { if (false === this.showBcc() && value.length) { this.showBcc(true); } + this.initPgpEncrypt(); }, replyTo: value => { @@ -303,6 +317,7 @@ class ComposePopupView extends AbstractViewPopup { if (this.emptyToError() && value.length) { this.emptyToError(false); } + this.initPgpEncrypt(); }, attachmentsInProcess: value => { @@ -551,19 +566,6 @@ class ComposePopupView extends AbstractViewPopup { rl.app.getAutocomplete(oData.term, aData => fResponse(aData.map(oEmailItem => oEmailItem.toLine(false)))); } - openOpenPgpPopup() { - if (PgpUserStore.openpgp && !this.oEditor.isHtml()) { - showScreenPopup(ComposeOpenPgpPopupView, [ - result => this.editor(editor => editor.setPlain(result)), - this.oEditor.getData(false), - this.currentIdentity(), - this.to(), - this.cc(), - this.bcc() - ]); - } - } - reloadDraftFolder() { const draftsFolder = FolderUserStore.draftsFolder(); if (draftsFolder && UNUSED_OPTION_VALUE !== draftsFolder) { @@ -1419,6 +1421,9 @@ class ComposePopupView extends AbstractViewPopup { this.showBcc(false); this.showReplyTo(false); + this.pgpSign(false); + this.pgpEncrypt(false); + delegateRunOnDestroy(this.attachments()); this.attachments([]); @@ -1442,6 +1447,31 @@ class ComposePopupView extends AbstractViewPopup { item => item.id ); } + + initPgpEncrypt() { + const email = new EmailModel(); + return PgpUserStore.hasPublicKeyForEmails([ +// this.currentIdentity.email(), + this.to(), + this.cc(), + this.bcc() + ].join(',').split(',').map(value => { + email.clear(); + email.parse(value.trim()); + return email.email || false; + }).filter(v => v), 1).then(result => { + console.log({canPgpEncrypt:result}); + this.canPgpEncrypt(result); + }); + } + + togglePgpSign() { + this.pgpSign(!this.pgpSign()/* && this.canPgpSign()*/); + } + + togglePgpEncrypt() { + this.pgpEncrypt(!this.pgpEncrypt()/* && this.canPgpEncrypt()*/); + } } export { ComposePopupView, ComposePopupView as default }; diff --git a/dev/View/Popup/ComposeOpenPgp.js b/dev/View/Popup/ComposeOpenPgp.js deleted file mode 100644 index 60096dadc..000000000 --- a/dev/View/Popup/ComposeOpenPgp.js +++ /dev/null @@ -1,385 +0,0 @@ -import ko from 'ko'; - -import { pString, defaultOptionsAfterRender } from 'Common/Utils'; - -import { Scope } from 'Common/Enums'; -import { i18n } from 'Common/Translator'; - -import { PgpUserStore } from 'Stores/User/Pgp'; - -import { EmailModel } from 'Model/Email'; - -import { decorateKoCommands } from 'Knoin/Knoin'; -import { AbstractViewPopup } from 'Knoin/AbstractViews'; - -const KEY_NAME_SUBSTR = -8, - i18nPGP = (key, params) => i18n('PGP_NOTIFICATIONS/' + key, params); - -class ComposeOpenPgpPopupView extends AbstractViewPopup { - constructor() { - super('ComposeOpenPgp'); - - this.publicKeysOptionsCaption = i18nPGP('ADD_A_PUBLICK_KEY'); - this.privateKeysOptionsCaption = i18nPGP('SELECT_A_PRIVATE_KEY'); - - this.addObservables({ - notification: '', - - sign: false, - encrypt: false, - - password: '', - - text: '', - selectedPrivateKey: null, - selectedPublicKey: null, - - signKey: null, - - submitRequest: false - }); - this.encryptKeys = ko.observableArray(); - - this.addComputables({ - encryptKeysView: () => this.encryptKeys.map(oKey => (oKey ? oKey.key : null)).filter(v => v), - - privateKeysOptions: () => { - const opts = PgpUserStore.openpgpkeysPrivate().map(oKey => { - if (this.signKey() && this.signKey().key.id === oKey.id) { - return null; - } - return oKey.users.map(user => ({ - id: oKey.guid, - name: '(' + oKey.id.slice(KEY_NAME_SUBSTR).toUpperCase() + ') ' + user, - key: oKey - })); - }); - - return opts.flat().filter(v => v); - }, - - publicKeysOptions: () => { - const opts = PgpUserStore.openpgpkeysPublic().map(oKey => { - if (this.encryptKeysView().includes(oKey)) { - return null; - } - return oKey.users.map(user => ({ - id: oKey.guid, - name: '(' + oKey.id.slice(KEY_NAME_SUBSTR).toUpperCase() + ') ' + user, - key: oKey - })); - }); - return opts.flat().filter(v => v); - } - }); - - this.resultCallback = null; - - this.selectedPrivateKey.subscribe((value) => { - if (value) { - this.selectCommand(); - this.updateCommand(); - } - }); - - this.selectedPublicKey.subscribe((value) => { - if (value) { - this.addCommand(); - } - }); - - this.defaultOptionsAfterRender = defaultOptionsAfterRender; - - this.deletePublickKey = this.deletePublickKey.bind(this); - - decorateKoCommands(this, { - doCommand: self => !self.submitRequest() && (self.sign() || self.encrypt()), - selectCommand: 1, - addCommand: 1, - updateCommand: 1, - }); - } - - doCommand() { - let result = true, - privateKey = null, - aPublicKeys = []; - - this.submitRequest(true); - - if (result && this.sign()) { - if (!this.signKey()) { - this.notification(i18nPGP('NO_PRIVATE_KEY_FOUND')); - result = false; - } else if (!this.signKey().key) { - this.notification( - i18nPGP('NO_PRIVATE_KEY_FOUND_FOR', { - EMAIL: this.signKey().email - }) - ); - - result = false; - } - - if (result) { - const privateKeys = this.signKey().key.getNativeKeys(); - privateKey = privateKeys[0] || null; - - try { - if (privateKey) { - privateKey.decrypt(pString(this.password())); - } - } catch (e) { - privateKey = null; - } - - if (!privateKey) { - this.notification(i18nPGP('NO_PRIVATE_KEY_FOUND')); - result = false; - } - } - } - - if (result && this.encrypt()) { - if (this.encryptKeys.length) { - aPublicKeys = []; - - this.encryptKeys.forEach(oKey => { - if (oKey && oKey.key) { - aPublicKeys = aPublicKeys.concat(oKey.key.getNativeKeys().flat(Infinity).filter(v => v)); - } else if (oKey && oKey.email) { - this.notification( - i18nPGP('NO_PUBLIC_KEYS_FOUND_FOR', { - EMAIL: oKey.email - }) - ); - - result = false; - } - }); - - if (result && (!aPublicKeys.length || this.encryptKeys.length !== aPublicKeys.length)) { - result = false; - } - } else { - this.notification(i18nPGP('NO_PUBLIC_KEYS_FOUND')); - result = false; - } - } - - if (result && this.resultCallback) { - setTimeout(() => { - let pgpPromise = null; - - try { - if (aPublicKeys.length) { - if (privateKey) { - pgpPromise = PgpUserStore.openpgp.encrypt({ - data: this.text(), - publicKeys: aPublicKeys, - privateKeys: [privateKey] - }); - } else { - pgpPromise = PgpUserStore.openpgp.encrypt({ - data: this.text(), - publicKeys: aPublicKeys - }); - } - } else if (privateKey) { - pgpPromise = PgpUserStore.openpgp.sign({ - data: this.text(), - privateKeys: [privateKey] - }); - } - } catch (e) { - console.log(e); - - this.notification( - i18nPGP('PGP_ERROR', { - ERROR: '' + e - }) - ); - } - - if (pgpPromise) { - try { - pgpPromise - .then((mData) => { - this.resultCallback(mData.data); - this.cancelCommand(); - }) - .catch((e) => { - this.notification( - i18nPGP('PGP_ERROR', { - ERROR: '' + e - }) - ); - }); - } catch (e) { - this.notification( - i18nPGP('PGP_ERROR', {ERROR: '' + e}) - ); - } - } - - this.submitRequest(false); - }, 20); - } else { - this.submitRequest(false); - } - - return result; - } - - selectCommand() { - const keyId = this.selectedPrivateKey(), - option = keyId ? this.privateKeysOptions().find(item => item && keyId === item.id) : null; - - if (option) { - this.signKey({ - empty: !option.key, - selected: ko.observable(!!option.key), - users: option.key.users, - hash: option.key.id.slice(KEY_NAME_SUBSTR).toUpperCase(), - key: option.key - }); - } - } - - addCommand() { - const keyId = this.selectedPublicKey(), - option = keyId ? this.publicKeysOptions().find(item => item && keyId === item.id) : null; - - if (option) { - this.encryptKeys.push({ - empty: !option.key, - selected: ko.observable(!!option.key), - removable: ko.observable(!this.sign() || !this.signKey() || this.signKey().key.id !== option.key.id), - users: option.key.users, - hash: option.key.id.slice(KEY_NAME_SUBSTR).toUpperCase(), - key: option.key - }); - } - } - - updateCommand() { - this.encryptKeys.forEach(oKey => - oKey.removable(!this.sign() || !this.signKey() || this.signKey().key.id !== oKey.key.id) - ); - } - - deletePublickKey(publicKey) { - this.encryptKeys.remove(publicKey); - } - - clearPopup() { - this.notification(''); - - this.sign(false); - this.encrypt(false); - - this.password(''); - - this.signKey(null); - this.encryptKeys([]); - this.text(''); - - this.resultCallback = null; - } - - onBuild() { -// shortcuts.add('tab', 'shift', Scope.ComposeOpenPgp, () => { - shortcuts.add('tab', '', Scope.ComposeOpenPgp, () => { - let btn = this.querySelector('.inputPassword'); - if (btn.matches(':focus')) { - btn = this.querySelector('.buttonDo'); - } - btn.focus(); - return false; - }); - } - - onHideWithDelay() { - this.clearPopup(); - } - - onShowWithDelay() { - this.querySelector(this.sign() ? '.inputPassword' : '.buttonDo').focus(); - } - - onShow(fCallback, sText, identity, sTo, sCc, sBcc) { - this.clearPopup(); - - let rec = [], - emailLine = ''; - - const email = new EmailModel(); - - this.resultCallback = fCallback; - - if (sTo) { - rec.push(sTo); - } - - if (sCc) { - rec.push(sCc); - } - - if (sBcc) { - rec.push(sBcc); - } - - rec = rec.join(', ').split(','); - rec = rec.map(value => { - email.clear(); - email.parse(value.trim()); - return email.email || false; - }).filter(v => v); - - if (identity && identity.email()) { - emailLine = identity.email(); - rec.unshift(emailLine); - - const keys = PgpUserStore.findAllPrivateKeysByEmailNotNative(emailLine); - if (keys && keys[0]) { - this.signKey({ - users: keys[0].users || [emailLine], - hash: keys[0].id.slice(KEY_NAME_SUBSTR).toUpperCase(), - key: keys[0] - }); - } - } - - if (this.signKey()) { - this.sign(true); - } - - if (rec.length) { - this.encryptKeys( - rec.map(recEmail => { - const keys = PgpUserStore.findAllPublicKeysByEmailNotNative(recEmail); - return keys - ? keys.map(publicKey => ({ - empty: !publicKey, - selected: ko.observable(!!publicKey), - removable: ko.observable( - !this.sign() || !this.signKey() || this.signKey().key.id !== publicKey.id - ), - users: publicKey ? publicKey.users || [recEmail] : [recEmail], - hash: publicKey ? publicKey.id.slice(KEY_NAME_SUBSTR).toUpperCase() : '', - key: publicKey - })) - : []; - }).flat().validUnique(encryptKey => encryptKey.hash) - ); - - if (this.encryptKeys.length) { - this.encrypt(true); - } - } - - this.text(sText); - } -} - -export { ComposeOpenPgpPopupView, ComposeOpenPgpPopupView as default }; diff --git a/dev/View/Popup/NewOpenPgpKey.js b/dev/View/Popup/NewOpenPgpKey.js index e12748652..ae68244f1 100644 --- a/dev/View/Popup/NewOpenPgpKey.js +++ b/dev/View/Popup/NewOpenPgpKey.js @@ -47,7 +47,7 @@ class NewOpenPgpKeyPopupView extends AbstractViewPopup { setTimeout(() => { try { - PgpUserStore.openpgp + openpgp .generateKey({ userIds: [userId], numBits: pInt(this.keyBitLength()), @@ -62,7 +62,7 @@ class NewOpenPgpKeyPopupView extends AbstractViewPopup { openpgpKeyring.store(); - rl.app.reloadOpenPgpKeys(); + PgpUserStore.reloadOpenPgpKeys(); this.cancelCommand(); } }) diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index f690d40e4..f3fe4a504 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -180,7 +180,7 @@ export class MailMessageView extends AbstractViewRight { pgpSigned: () => MessageUserStore.message() && !!MessageUserStore.message().pgpSigned(), - pgpEncrypted: () => PgpUserStore.openpgp + pgpEncrypted: () => PgpUserStore.isSupported() && MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(), messageListOrViewLoading: 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 78c9d856c..7402e9e5a 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 @@ -7,19 +7,46 @@ trait Pgp /** * @throws \MailSo\Base\Exceptions\Exception */ - public function GnuPG() : ?\gnupg + public function GnuPG() : ?\SnappyMail\PGP\GnuPG { - if (\class_exists('gnupg')) { - $pgp_dir = $this->StorageProvider()->GenerateFilePath( - $this->getAccountFromToken(), - \RainLoop\Providers\Storage\Enumerations\StorageType::PGP - ); - return new \gnupg(['home_dir' => \dirname($pgp_dir) . '/.gnupg']); - } - return null; + $pgp_dir = $this->StorageProvider()->GenerateFilePath( + $this->getAccountFromToken(), + \RainLoop\Providers\Storage\Enumerations\StorageType::PGP + ); + return \SnappyMail\PGP\GnuPG::getInstance($pgp_dir); } - public function DoImportKey() : array + public function DoPgpGetKeysEmails() : array + { + $GPG = $this->GnuPG(); + if ($GPG) { + $sign = $encrypt = $keys = []; + foreach ($GPG->keyInfo('') as $info) { + if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) { + if ($info['can_sign']) { + foreach ($info['uids'] as $uid) { + $private[] = $info['email']; + } + } + if ($info['can_encrypt']) { + foreach ($info['uids'] as $uid) { + $public[] = $info['email']; + } + } + } + $keys[] = $info; + } + return $this->DefaultResponse(__FUNCTION__, [ + 'sign' => $sign, + 'encrypt' => $encrypt, + 'keys' => $keys, + 'info' => $GPG->getEngineInfo() + ]); + } + return $this->FalseResponse(__FUNCTION__); + } + + public function DoPgpImportKey() : array { $sKeyId = $this->GetActionParam('KeyId', ''); $sPublicKey = $this->GetActionParam('PublicKey', ''); @@ -32,6 +59,16 @@ trait Pgp $sEmail = $aMatch[0]; } if ($sEmail) { + /** https://wiki.gnupg.org/WKD + DNS: + openpgpkey.example.org. 300 IN CNAME wkd.keys.openpgp.org. + + https://openpgpkey.example.com/.well-known/openpgpkey/example.com/hu/ + else https://example.com/.well-known/openpgpkey/hu/ + + An example: https://example.com/.well-known/openpgpkey/hu/it5sewh54rxz33fwmr8u6dy4bbz8itz4 + is the direct method URL for "bernhard.reiter@example.com" + */ $aKeys = \SnappyMail\PGP\Keyservers::index($sEmail); if ($aKeys) { $sKeyId = $aKeys[0]['keyid']; @@ -46,8 +83,9 @@ trait Pgp } } - return $sPublicKey - ? $this->DefaultResponse(__FUNCTION__, $this->GnuPG()->import($sPublicKey)) + $GPG = $sPublicKey ? $this->GnuPG() : null; + return $GPG + ? $this->DefaultResponse(__FUNCTION__, $GPG->import($sPublicKey)) : $this->FalseResponse(__FUNCTION__); } } 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 new file mode 100644 index 000000000..c0a7dcd70 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php @@ -0,0 +1,499 @@ +GnuPG = new \gnupg([ + // It is the file name of the executable program implementing this protocol which is usually path of the gpg executable. +// 'file_name' => '/usr/bin/gpg', + // It is the directory name of the configuration directory. It also overrides GNUPGHOME environment variable that is used for the same purpose. + 'home_dir' => $home + ]); + // Output is ASCII + $self->GnuPG->setarmor(1); + } else { + /** + * $binary = trim(`which gpg`) ?: trim(`which gpg2`); + * \is_executable($binary) + */ + include_once 'Crypt/GPG.php'; + if (\class_exists('Crypt_GPG')) { + $self = new self; + $self->Crypt_GPG = new \Crypt_GPG([ +// 'debug' => true, +// 'binary' => $binary, + 'homedir' => $home + ]); + } + } + if ($self) { + $self->homedir = $home; +// \putenv("GNUPGHOME={$home}"); + } + return $self; + } + + /** + * Add a key for decryption + */ + public function addDecryptKey(string $fingerprint, string $passphrase) : bool + { + if ($this->GnuPG) { + return $this->GnuPG->adddecryptkey($fingerprint, $passphrase); + } + if ($this->Crypt_GPG) { + $this->Crypt_GPG->addDecryptKey($fingerprint, $passphrase); + return true; + } + return false; + } + + /** + * Add a key for encryption + */ + public function addEncryptKey(string $fingerprint) : bool + { + if ($this->GnuPG) { + return $this->GnuPG->addencryptkey($fingerprint); + } + if ($this->Crypt_GPG) { + $this->Crypt_GPG->addEncryptKey($fingerprint); + return true; + } + return false; + } + + /** + * Add a key for signing + */ + public function addSignKey(string $fingerprint, ?string $passphrase) : bool + { + if ($this->GnuPG) { + return $this->GnuPG->addsignkey($fingerprint, $passphrase); + } + if ($this->Crypt_GPG) { + $this->Crypt_GPG->addSignKey($fingerprint, $passphrase); + return true; + } + return false; + } + + /** + * Removes all keys which were set for decryption before + */ + public function clearDecryptKeys() : bool + { + if ($this->GnuPG) { + return $this->GnuPG->cleardecryptkeys(); + } + if ($this->Crypt_GPG) { + $this->Crypt_GPG->clearDecryptKeys(); + return true; + } + return false; + } + + /** + * Removes all keys which were set for encryption before + */ + public function clearEncryptKeys() : bool + { + if ($this->GnuPG) { + return $this->GnuPG->clearencryptkeys(); + } + if ($this->Crypt_GPG) { + $this->Crypt_GPG->clearEncryptKeys(); + return true; + } + return false; + } + + /** + * Removes all keys which were set for signing before + */ + public function clearSignKeys() : bool + { + if ($this->GnuPG) { + return $this->GnuPG->clearsignkeys(); + } + if ($this->Crypt_GPG) { + $this->Crypt_GPG->clearSignKeys(); + return true; + } + return false; + } + + /** + * Decrypts a given text + */ + public function decrypt(string $text) /*: string|false */ + { + if ($this->GnuPG) { + return $this->GnuPG->decrypt($text); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->decrypt($encryptedData); + } + return false; + } + + /** + * Decrypts a given file + */ + public function decryptFile(string $filename) /*: string|false */ + { + if ($this->GnuPG) { + return $this->GnuPG->decrypt(\file_get_contents($filename)); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->decryptFile($filename, $decryptedFile = null); + } + return false; + } + + /** + * Decrypts and verifies a given text + */ + public function decryptVerify(string $text, string &$plaintext) /*: array|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->decryptverify($text, $plaintext); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->decryptAndVerify($text, $ignoreVerifyErrors = false); + } + return false; + } + + /** + * Decrypts and verifies a given file + */ + public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->decryptverify(\file_get_contents($filename), $plaintext); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->decryptAndVerifyFile($filename, $decryptedFile = null, $ignoreVerifyErrors = false); + } + return false; + } + + /** + * Encrypts a given text + */ + public function encrypt(string $plaintext) /*: string|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->encrypt($plaintext); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->encrypt($plaintext); + } + return false; + } + + /** + * Encrypts a given text + */ + public function encryptFile(string $filename) /*: string|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->encrypt(\file_get_contents($filename)); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->encryptFile($filename, $encryptedFile = null); + } + return false; + } + + /** + * Encrypts and signs a given text + */ + public function encryptSign(string $plaintext) /*: string|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->encryptsign($plaintext); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->encryptAndSign($plaintext); + } + return false; + } + + /** + * Encrypts and signs a given text + */ + public function encryptSignFile(string $filename) /*: string|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->encryptsign(\file_get_contents($filename)); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->encryptAndSignFile($filename, $signedFile = null); + } + return false; + } + + /** + * Exports a key + */ + public function export(string $fingerprint) /*: string|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->export($fingerprint); + } + if ($this->Crypt_GPG) { + $this->Crypt_GPG->exportPrivateKey($fingerprint, $armor = true); + $this->Crypt_GPG->exportPublicKey($fingerprint, $armor = true); + return true; + } + return false; + } + + /** + * Returns the engine info + */ + public function getEngineInfo() : array + { + if ($this->GnuPG) { + return $this->GnuPG->getengineinfo(); + } + if ($this->Crypt_GPG) { + return [ + 'protocol' => null, + 'file_name' => null, // $this->Crypt_GPG->binary + 'home_dir' => $this->homedir, + 'version' => $this->Crypt_GPG->getVersion() + ]; + } + return false; + } + + /** + * Returns the errortext, if a function fails + */ + public function getError() /*: string|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->geterror(); + } + if ($this->Crypt_GPG) { + return true; + } + return false; + } + + /** + * Returns the error info + */ + public function getErrorInfo() : array + { + if ($this->GnuPG) { + return $this->GnuPG->geterrorinfo(); + } + if ($this->Crypt_GPG) { + return true; + } + return false; + } + + /** + * Returns the currently active protocol for all operations + */ + public function getProtocol() : int + { + if ($this->GnuPG) { + return $this->GnuPG->getprotocol(); + } + if ($this->Crypt_GPG) { + return true; + } + return false; + } + + /** + * Imports a key + */ + public function import(string $keydata) /*: array|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->import($keydata); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->importKey($keydata); + } + return false; + } + + /** + * Imports a key + */ + public function importFile(string $filename) /*: array|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->import(\file_get_contents($filename)); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->importKeyFile($filename); + } + return false; + } + + /** + * Returns an array with information about all keys that matches the given pattern + */ + public function keyInfo(string $pattern) : array + { + if ($this->GnuPG) { + return $this->GnuPG->keyinfo($pattern); + } + if ($this->Crypt_GPG) { + return true; + } + return false; + } + + /** + * Toggle armored output + * When true the output is ASCII + */ + public function setArmor(bool $armor = true) : bool + { + if ($this->GnuPG) { + return $this->GnuPG->setarmor($armor ? 1 : 0); + } + if ($this->Crypt_GPG) { + //$armor ? \Crypt_GPG::ARMOR_ASCII : \Crypt_GPG::ARMOR_ + return true; + } + return false; + } + + /** + * Sets the mode for error_reporting + * GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT. + * By default GNUPG_ERROR_SILENT is used. + */ + public function setErrorMode(int $errormode) : void + { + if ($this->GnuPG) { + $this->GnuPG->seterrormode($errormode); + } + if ($this->Crypt_GPG) { + } + } + + /** + * Sets the mode for signing + * GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH and GNUPG_SIG_MODE_CLEAR. + * By default GNUPG_SIG_MODE_CLEAR + */ + public function setSignMode(int $signmode) : bool + { + if ($this->GnuPG) { + return $this->GnuPG->setsignmode($signmode); + } + if ($this->Crypt_GPG) { + return true; + } + return false; + } + + /** + * Signs a given text + */ + public function sign(string $plaintext) /*: string|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->sign($plaintext); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->sign($data, $mode = self::SIGN_MODE_NORMAL); + } + return false; + } + + /** + * Signs a given file + */ + public function signFile(string $filename) /*: string|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->sign(\file_get_contents($filename)); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->signFile($filename, $signedFile = null, $mode = self::SIGN_MODE_NORMAL); + } + return false; + } + + /** + * Verifies a signed text + */ + public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->verify($signed_text, $signature, $plaintext); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->verify($signed_text, $signature = ''); + } + return false; + } + + /** + * Verifies a signed file + */ + public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/ + { + if ($this->GnuPG) { + return $this->GnuPG->verify(\file_get_contents($filename), $signature, $plaintext); + } + if ($this->Crypt_GPG) { + return $this->Crypt_GPG->verifyFile($filename, $signature = ''); + } + return false; + } + + /** + * RFC 4880 + * https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.3.5 + */ + public function signatureIssuer(string $signature) /*: array|false*/ + { + if (preg_match('/-----BEGIN PGP SIGNATURE-----(.+)-----END PGP SIGNATURE-----/', $signature, $match)) { + // TODO: use https://github.com/singpolyma/openpgp-php ? + $binary = \base64_decode(\trim($match[1])); + return \strtoupper(\bin2hex(\substr($binary, 24, 8))); + } + return false; + } + +/* + $this->Crypt_GPG->deletePublicKey($keyId); + $this->Crypt_GPG->deletePrivateKey($keyId); + $this->Crypt_GPG->getKeys($keyId = ''); + $this->Crypt_GPG->getFingerprint($keyId, $format = self::FORMAT_NONE); + $this->Crypt_GPG->getLastSignatureInfo(); + $this->Crypt_GPG->addPassphrase($key, $passphrase); + $this->Crypt_GPG->clearPassphrases(); + $this->Crypt_GPG->hasEncryptKeys(); + $this->Crypt_GPG->hasSignKeys(); + $this->Crypt_GPG->getWarnings(); +*/ +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php index ad9a6d711..f6156bc41 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/keyservers.php @@ -21,6 +21,7 @@ abstract class Keyservers 'https://attester.flowcrypt.com', 'https://zimmermann.mayfirst.org', 'https://pool.sks-keyservers.net', + 'https://keys.mailvelope.com', */ 'https://keyserver.ubuntu.com', 'https://keys.fedoraproject.org', diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html index 66ff36426..24b6d7d4e 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html @@ -81,12 +81,20 @@ -
  • + +
  • - 🔑 - + +
  • +
  • + + + + +
  • + diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsComposeOpenPgp.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsComposeOpenPgp.html deleted file mode 100644 index 8f793af67..000000000 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsComposeOpenPgp.html +++ /dev/null @@ -1,85 +0,0 @@ -
    - × -

    -
    - - - -