mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03:00
Revamp PGP management due to implementing Mailvelop and PEAR Crypt_GPG
This commit is contained in:
parent
43a1196dbb
commit
a47397ef09
16 changed files with 885 additions and 633 deletions
|
|
@ -35,7 +35,9 @@ module.exports = {
|
||||||
// vendors/jua
|
// vendors/jua
|
||||||
'Jua': "readonly",
|
'Jua': "readonly",
|
||||||
// vendors/bootstrap/bootstrap.native.js
|
// vendors/bootstrap/bootstrap.native.js
|
||||||
'BSN': "readonly"
|
'BSN': "readonly",
|
||||||
|
// Mailvelope
|
||||||
|
'mailvelope': "readonly"
|
||||||
},
|
},
|
||||||
// http://eslint.org/docs/rules/
|
// http://eslint.org/docs/rules/
|
||||||
rules: {
|
rules: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { isArray, arrayLength, pString, forEachObjectValue } from 'Common/Utils'
|
||||||
import { delegateRunOnDestroy, mailToHelper, setLayoutResizer } from 'Common/UtilsUser';
|
import { delegateRunOnDestroy, mailToHelper, setLayoutResizer } from 'Common/UtilsUser';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Capa,
|
|
||||||
Notification,
|
Notification,
|
||||||
Scope
|
Scope
|
||||||
} from 'Common/Enums';
|
} from 'Common/Enums';
|
||||||
|
|
@ -36,11 +35,7 @@ import {
|
||||||
getFolderFromCacheList
|
getFolderFromCacheList
|
||||||
} from 'Common/Cache';
|
} from 'Common/Cache';
|
||||||
|
|
||||||
import {
|
import { mailBox } from 'Common/Links';
|
||||||
mailBox,
|
|
||||||
openPgpWorkerJs,
|
|
||||||
openPgpJs
|
|
||||||
} from 'Common/Links';
|
|
||||||
|
|
||||||
import { getNotification, i18n } from 'Common/Translator';
|
import { getNotification, i18n } from 'Common/Translator';
|
||||||
|
|
||||||
|
|
@ -61,7 +56,6 @@ import Remote from 'Remote/User/Fetch';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
import { AccountModel } from 'Model/Account';
|
import { AccountModel } from 'Model/Account';
|
||||||
import { IdentityModel } from 'Model/Identity';
|
import { IdentityModel } from 'Model/Identity';
|
||||||
import { OpenPgpKeyModel } from 'Model/OpenPgpKey';
|
|
||||||
|
|
||||||
import { LoginUserScreen } from 'Screen/User/Login';
|
import { LoginUserScreen } from 'Screen/User/Login';
|
||||||
import { MailBoxUserScreen } from 'Screen/User/MailBox';
|
import { MailBoxUserScreen } from 'Screen/User/MailBox';
|
||||||
|
|
@ -346,66 +340,6 @@ class AppUser extends AbstractApp {
|
||||||
return false;
|
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() {
|
accountsAndIdentities() {
|
||||||
AccountUserStore.loading(true);
|
AccountUserStore.loading(true);
|
||||||
IdentityUserStore.loading(true);
|
IdentityUserStore.loading(true);
|
||||||
|
|
@ -772,23 +706,7 @@ class AppUser extends AbstractApp {
|
||||||
|
|
||||||
setInterval(this.reloadTime(), 60000);
|
setInterval(this.reloadTime(), 60000);
|
||||||
|
|
||||||
if (window.crypto && crypto.getRandomValues && Settings.capa(Capa.OpenPGP)) {
|
PgpUserStore.init();
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.logout();
|
this.logout();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -648,7 +648,7 @@ export class MessageModel extends AbstractModel {
|
||||||
// const message = self.message();
|
// const message = self.message();
|
||||||
// message && pgpClickHelper(message.body, message.plain(), message.getEmails(['from', 'to', 'cc']));
|
// message && pgpClickHelper(message.body, message.plain(), message.getEmails(['from', 'to', 'cc']));
|
||||||
/*
|
/*
|
||||||
pgpEncrypted: () => PgpUserStore.openpgp
|
pgpEncrypted: () => PgpUserStore.isSupported()
|
||||||
&& MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(),
|
&& MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(),
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
@ -660,9 +660,11 @@ export class MessageModel extends AbstractModel {
|
||||||
params.Uid = this.uid;
|
params.Uid = this.uid;
|
||||||
rl.app.Remote.post('MessagePgpVerify', null, params)
|
rl.app.Remote.post('MessagePgpVerify', null, params)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
// TODO
|
||||||
console.dir(data);
|
console.dir(data);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
// TODO
|
||||||
console.dir(error);
|
console.dir(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import ko from 'ko';
|
||||||
|
|
||||||
import { arrayLength } from 'Common/Utils';
|
import { arrayLength } from 'Common/Utils';
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
|
||||||
|
|
||||||
export class OpenPgpKeyModel extends AbstractModel {
|
export class OpenPgpKeyModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,25 +34,21 @@ export class OpenPgpKeyModel extends AbstractModel {
|
||||||
this.deleteAccess = ko.observable(false);
|
this.deleteAccess = ko.observable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNativeKey() {
|
/**
|
||||||
let key = null;
|
* OpenPGP.js
|
||||||
|
*/
|
||||||
|
getNativeKeys() {
|
||||||
try {
|
try {
|
||||||
key = PgpUserStore.openpgp.key.readArmored(this.armor);
|
let key = openpgp.key.readArmored(this.armor);
|
||||||
if (key && !key.err && key.keys && key.keys[0]) {
|
if (key && !key.err && key.keys && key.keys[0]) {
|
||||||
return key;
|
return key.keys;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNativeKeys() {
|
|
||||||
const key = this.getNativeKey();
|
|
||||||
return key && key.keys ? key.keys : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
select(pattern, property) {
|
select(pattern, property) {
|
||||||
if (this[property]) {
|
if (this[property]) {
|
||||||
const index = this[property].indexOf(pattern);
|
const index = this[property].indexOf(pattern);
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export class OpenPgpUserSettings /*extends AbstractViewSettings*/ {
|
||||||
PgpUserStore.openpgpKeyring.store();
|
PgpUserStore.openpgpKeyring.store();
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.app.reloadOpenPgpKeys();
|
PgpUserStore.reloadOpenPgpKeys();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,169 @@
|
||||||
import ko from 'ko';
|
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, pString, addComputablesTo } from 'Common/Utils';
|
||||||
|
import { delegateRunOnDestroy } from 'Common/UtilsUser';
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { MessageOpenPgpPopupView } from 'View/Popup/MessageOpenPgp';
|
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 {
|
export const PgpUserStore = new class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.openpgp = null;
|
// PECL gnupg / PEAR Crypt_GPG
|
||||||
|
this.gnupgkeys;
|
||||||
|
|
||||||
|
// OpenPGP.js
|
||||||
this.openpgpkeys = ko.observableArray();
|
this.openpgpkeys = ko.observableArray();
|
||||||
this.openpgpKeyring = null;
|
this.openpgpKeyring = null;
|
||||||
|
|
||||||
|
// https://mailvelope.github.io/mailvelope/Keyring.html
|
||||||
|
this.mailvelopeKeyring = null;
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
openpgpkeysPublic: () => this.openpgpkeys.filter(item => item && !item.isPrivate),
|
openpgpkeysPublic: () => this.openpgpkeys.filter(item => item && !item.isPrivate),
|
||||||
openpgpkeysPrivate: () => 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}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isSupported() {
|
isSupported() {
|
||||||
return !!this.openpgp;
|
return !!(window.openpgp || window.mailvelope);
|
||||||
}
|
|
||||||
|
|
||||||
findKeyByHex(keys, hash) {
|
|
||||||
return keys.find(item => hash && item && (hash === item.id || item.ids.includes(hash)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findPublicKeyByHex(hash) {
|
findPublicKeyByHex(hash) {
|
||||||
return this.findKeyByHex(this.openpgpkeysPublic(), hash);
|
return findKeyByHex(this.openpgpkeys, hash, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
findPrivateKeyByHex(hash) {
|
findPrivateKeyByHex(hash) {
|
||||||
return this.findKeyByHex(this.openpgpkeysPrivate(), hash);
|
return findKeyByHex(this.openpgpkeys, hash, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
findPublicKeysByEmail(email) {
|
findPublicKeysByEmail(email) {
|
||||||
|
|
@ -77,19 +203,53 @@ export const PgpUserStore = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} email
|
* Checks if verifying/encrypting a message is possible with given email addresses.
|
||||||
* @returns {?}
|
* Returns the first library that can.
|
||||||
*/
|
*/
|
||||||
findPublicKeyByEmailNotNative(email) {
|
async hasPublicKeyForEmails(recipients, all) {
|
||||||
return this.openpgpkeysPublic().find(item => item && item.emails.includes(email)) || null;
|
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
|
* Checks if signing/decrypting a message is possible with given email address.
|
||||||
* @returns {?}
|
* Returns the first library that can.
|
||||||
*/
|
*/
|
||||||
findPrivateKeyByEmailNotNative(email) {
|
async hasPrivateKeyForEmail(email) {
|
||||||
return this.openpgpkeysPrivate().find(item => item && item.emails.includes(email)) || null;
|
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 {?}
|
* @returns {?}
|
||||||
*/
|
*/
|
||||||
findAllPublicKeysByEmailNotNative(email) {
|
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 {?}
|
* @returns {?}
|
||||||
*/
|
*/
|
||||||
findAllPrivateKeysByEmailNotNative(email) {
|
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) {
|
findPrivateKeyByEmail(email, password) {
|
||||||
let privateKey = null;
|
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) {
|
if (key) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -131,14 +291,6 @@ export const PgpUserStore = new class {
|
||||||
return privateKey;
|
return privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string=} password
|
|
||||||
* @returns {?}
|
|
||||||
*/
|
|
||||||
findSelfPrivateKey(password) {
|
|
||||||
return this.findPrivateKeyByEmail(AccountUserStore.email(), password);
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptMessage(message, recipients, fCallback) {
|
decryptMessage(message, recipients, fCallback) {
|
||||||
if (message && message.getEncryptionKeyIds) {
|
if (message && message.getEncryptionKeyIds) {
|
||||||
const privateKeys = this.findPrivateKeysByEncryptionKeyIds(message.getEncryptionKeyIds(), recipients, true);
|
const privateKeys = this.findPrivateKeysByEncryptionKeyIds(message.getEncryptionKeyIds(), recipients, true);
|
||||||
|
|
@ -210,4 +362,81 @@ export const PgpUserStore = new class {
|
||||||
return false;
|
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(<AsciiArmored, Error>)
|
||||||
|
|
||||||
|
this.mailvelopeKeyring.hasPrivateKey(fingerprint)
|
||||||
|
|
||||||
|
this.mailvelopeKeyring.importPublicKey(armored)
|
||||||
|
*/
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ class AddOpenPgpKeyPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
openpgpKeyring.store();
|
openpgpKeyring.store();
|
||||||
|
|
||||||
rl.app.reloadOpenPgpKeys();
|
PgpUserStore.reloadOpenPgpKeys();
|
||||||
|
|
||||||
if (this.keyError()) {
|
if (this.keyError()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import { MessageUserStore } from 'Stores/User/Message';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
|
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
|
||||||
|
import { EmailModel } from 'Model/Email';
|
||||||
|
|
||||||
import { decorateKoCommands, isPopupVisible, showScreenPopup } from 'Knoin/Knoin';
|
import { decorateKoCommands, isPopupVisible, showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
|
|
@ -41,7 +42,6 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||||
import { AskPopupView } from 'View/Popup/Ask';
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
import { ContactsPopupView } from 'View/Popup/Contacts';
|
import { ContactsPopupView } from 'View/Popup/Contacts';
|
||||||
import { ComposeOpenPgpPopupView } from 'View/Popup/ComposeOpenPgp';
|
|
||||||
|
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
|
|
@ -131,7 +131,7 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
this.bSkipNextHide = false;
|
this.bSkipNextHide = false;
|
||||||
|
|
||||||
this.capaOpenPGP = !!PgpUserStore.openpgp;
|
this.capaOpenPGP = PgpUserStore.isSupported();
|
||||||
|
|
||||||
this.identities = IdentityUserStore;
|
this.identities = IdentityUserStore;
|
||||||
|
|
||||||
|
|
@ -169,6 +169,11 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
showBcc: false,
|
showBcc: false,
|
||||||
showReplyTo: false,
|
showReplyTo: false,
|
||||||
|
|
||||||
|
pgpSign: false,
|
||||||
|
pgpEncrypt: false,
|
||||||
|
canPgpSign: false,
|
||||||
|
canPgpEncrypt: false,
|
||||||
|
|
||||||
draftsFolder: '',
|
draftsFolder: '',
|
||||||
draftUid: 0,
|
draftUid: 0,
|
||||||
sending: false,
|
sending: false,
|
||||||
|
|
@ -265,7 +270,6 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
},
|
},
|
||||||
|
|
||||||
canBeSentOrSaved: () => !this.sending() && !this.saving()
|
canBeSentOrSaved: () => !this.sending() && !this.saving()
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addSubscribables({
|
this.addSubscribables({
|
||||||
|
|
@ -275,16 +279,26 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
sendSuccessButSaveError: value => !value && this.savedErrorDesc(''),
|
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 => {
|
cc: value => {
|
||||||
if (false === this.showCc() && value.length) {
|
if (false === this.showCc() && value.length) {
|
||||||
this.showCc(true);
|
this.showCc(true);
|
||||||
}
|
}
|
||||||
|
this.initPgpEncrypt();
|
||||||
},
|
},
|
||||||
|
|
||||||
bcc: value => {
|
bcc: value => {
|
||||||
if (false === this.showBcc() && value.length) {
|
if (false === this.showBcc() && value.length) {
|
||||||
this.showBcc(true);
|
this.showBcc(true);
|
||||||
}
|
}
|
||||||
|
this.initPgpEncrypt();
|
||||||
},
|
},
|
||||||
|
|
||||||
replyTo: value => {
|
replyTo: value => {
|
||||||
|
|
@ -303,6 +317,7 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
if (this.emptyToError() && value.length) {
|
if (this.emptyToError() && value.length) {
|
||||||
this.emptyToError(false);
|
this.emptyToError(false);
|
||||||
}
|
}
|
||||||
|
this.initPgpEncrypt();
|
||||||
},
|
},
|
||||||
|
|
||||||
attachmentsInProcess: value => {
|
attachmentsInProcess: value => {
|
||||||
|
|
@ -551,19 +566,6 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
rl.app.getAutocomplete(oData.term, aData => fResponse(aData.map(oEmailItem => oEmailItem.toLine(false))));
|
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() {
|
reloadDraftFolder() {
|
||||||
const draftsFolder = FolderUserStore.draftsFolder();
|
const draftsFolder = FolderUserStore.draftsFolder();
|
||||||
if (draftsFolder && UNUSED_OPTION_VALUE !== draftsFolder) {
|
if (draftsFolder && UNUSED_OPTION_VALUE !== draftsFolder) {
|
||||||
|
|
@ -1419,6 +1421,9 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
this.showBcc(false);
|
this.showBcc(false);
|
||||||
this.showReplyTo(false);
|
this.showReplyTo(false);
|
||||||
|
|
||||||
|
this.pgpSign(false);
|
||||||
|
this.pgpEncrypt(false);
|
||||||
|
|
||||||
delegateRunOnDestroy(this.attachments());
|
delegateRunOnDestroy(this.attachments());
|
||||||
this.attachments([]);
|
this.attachments([]);
|
||||||
|
|
||||||
|
|
@ -1442,6 +1447,31 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
item => item.id
|
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 };
|
export { ComposePopupView, ComposePopupView as default };
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
|
||||||
|
|
@ -47,7 +47,7 @@ class NewOpenPgpKeyPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
PgpUserStore.openpgp
|
openpgp
|
||||||
.generateKey({
|
.generateKey({
|
||||||
userIds: [userId],
|
userIds: [userId],
|
||||||
numBits: pInt(this.keyBitLength()),
|
numBits: pInt(this.keyBitLength()),
|
||||||
|
|
@ -62,7 +62,7 @@ class NewOpenPgpKeyPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
openpgpKeyring.store();
|
openpgpKeyring.store();
|
||||||
|
|
||||||
rl.app.reloadOpenPgpKeys();
|
PgpUserStore.reloadOpenPgpKeys();
|
||||||
this.cancelCommand();
|
this.cancelCommand();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
|
|
||||||
pgpSigned: () => MessageUserStore.message() && !!MessageUserStore.message().pgpSigned(),
|
pgpSigned: () => MessageUserStore.message() && !!MessageUserStore.message().pgpSigned(),
|
||||||
|
|
||||||
pgpEncrypted: () => PgpUserStore.openpgp
|
pgpEncrypted: () => PgpUserStore.isSupported()
|
||||||
&& MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(),
|
&& MessageUserStore.message() && MessageUserStore.message().isPgpEncrypted(),
|
||||||
|
|
||||||
messageListOrViewLoading:
|
messageListOrViewLoading:
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,46 @@ trait Pgp
|
||||||
/**
|
/**
|
||||||
* @throws \MailSo\Base\Exceptions\Exception
|
* @throws \MailSo\Base\Exceptions\Exception
|
||||||
*/
|
*/
|
||||||
public function GnuPG() : ?\gnupg
|
public function GnuPG() : ?\SnappyMail\PGP\GnuPG
|
||||||
{
|
{
|
||||||
if (\class_exists('gnupg')) {
|
|
||||||
$pgp_dir = $this->StorageProvider()->GenerateFilePath(
|
$pgp_dir = $this->StorageProvider()->GenerateFilePath(
|
||||||
$this->getAccountFromToken(),
|
$this->getAccountFromToken(),
|
||||||
\RainLoop\Providers\Storage\Enumerations\StorageType::PGP
|
\RainLoop\Providers\Storage\Enumerations\StorageType::PGP
|
||||||
);
|
);
|
||||||
return new \gnupg(['home_dir' => \dirname($pgp_dir) . '/.gnupg']);
|
return \SnappyMail\PGP\GnuPG::getInstance($pgp_dir);
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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', '');
|
$sKeyId = $this->GetActionParam('KeyId', '');
|
||||||
$sPublicKey = $this->GetActionParam('PublicKey', '');
|
$sPublicKey = $this->GetActionParam('PublicKey', '');
|
||||||
|
|
@ -32,6 +59,16 @@ trait Pgp
|
||||||
$sEmail = $aMatch[0];
|
$sEmail = $aMatch[0];
|
||||||
}
|
}
|
||||||
if ($sEmail) {
|
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);
|
$aKeys = \SnappyMail\PGP\Keyservers::index($sEmail);
|
||||||
if ($aKeys) {
|
if ($aKeys) {
|
||||||
$sKeyId = $aKeys[0]['keyid'];
|
$sKeyId = $aKeys[0]['keyid'];
|
||||||
|
|
@ -46,8 +83,9 @@ trait Pgp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $sPublicKey
|
$GPG = $sPublicKey ? $this->GnuPG() : null;
|
||||||
? $this->DefaultResponse(__FUNCTION__, $this->GnuPG()->import($sPublicKey))
|
return $GPG
|
||||||
|
? $this->DefaultResponse(__FUNCTION__, $GPG->import($sPublicKey))
|
||||||
: $this->FalseResponse(__FUNCTION__);
|
: $this->FalseResponse(__FUNCTION__);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
499
snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php
Normal file
499
snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php
Normal file
|
|
@ -0,0 +1,499 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SnappyMail\PGP;
|
||||||
|
|
||||||
|
class GnuPG
|
||||||
|
{
|
||||||
|
private
|
||||||
|
$homedir,
|
||||||
|
// Instance of gnupg pecl extension
|
||||||
|
$GnuPG,
|
||||||
|
// Instance of PEAR Crypt_GPG
|
||||||
|
$Crypt_GPG;
|
||||||
|
|
||||||
|
public static function getInstance(string $home) : ?self
|
||||||
|
{
|
||||||
|
$self = null;
|
||||||
|
$home .= '/.gnupg';
|
||||||
|
if (\class_exists('gnupg')) {
|
||||||
|
$self = new self;
|
||||||
|
$self->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();
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ abstract class Keyservers
|
||||||
'https://attester.flowcrypt.com',
|
'https://attester.flowcrypt.com',
|
||||||
'https://zimmermann.mayfirst.org',
|
'https://zimmermann.mayfirst.org',
|
||||||
'https://pool.sks-keyservers.net',
|
'https://pool.sks-keyservers.net',
|
||||||
|
'https://keys.mailvelope.com',
|
||||||
*/
|
*/
|
||||||
'https://keyserver.ubuntu.com',
|
'https://keyserver.ubuntu.com',
|
||||||
'https://keys.fedoraproject.org',
|
'https://keys.fedoraproject.org',
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,20 @@
|
||||||
<span data-i18n="COMPOSE/BUTTON_MARK_AS_IMPORTANT"></span>
|
<span data-i18n="COMPOSE/BUTTON_MARK_AS_IMPORTANT"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="dividerbar" data-bind="visible: capaOpenPGP, click: openOpenPgpPopup, css: {'disabled': isHtml()}">
|
<!-- ko if: capaOpenPGP -->
|
||||||
|
<li class="dividerbar" data-bind="click: togglePgpSign, css: {'disabled': !canPgpSign()}">
|
||||||
<a>
|
<a>
|
||||||
<i class="fontastic">🔑</i>
|
<i class="fontastic" data-bind="text: pgpSign() ? '✍' : '☐'"></i>
|
||||||
<span data-i18n="COMPOSE/BUTTON_OPEN_PGP"></span>
|
<span data-i18n="POPUPS_COMPOSE_OPEN_PGP/LABEL_SIGN"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li data-bind="click: togglePgpEncrypt, css: {'disabled': !canPgpEncrypt()}">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: pgpEncrypt() ? '🔒' : '☐'"></i>
|
||||||
|
<span data-i18n="POPUPS_COMPOSE_OPEN_PGP/LABEL_ENCRYPT"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- /ko -->
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
<header>
|
|
||||||
<a href="#" class="close" data-bind="command: cancelCommand">×</a>
|
|
||||||
<h3 data-i18n="POPUPS_COMPOSE_OPEN_PGP/TITLE_COMPOSE_OPEN_PGP"></h3>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="alert" data-bind="visible: '' !== notification(), text: notification"></div>
|
|
||||||
<div data-bind="component: {
|
|
||||||
name: 'Checkbox',
|
|
||||||
params: {
|
|
||||||
label: 'POPUPS_COMPOSE_OPEN_PGP/LABEL_SIGN',
|
|
||||||
value: sign
|
|
||||||
}
|
|
||||||
}, click: updateCommand"></div>
|
|
||||||
|
|
||||||
<div class="key-list" data-bind="visible: sign">
|
|
||||||
<div class="key-list-wrp empty" data-bind="visible: !signKey()">
|
|
||||||
No private key found
|
|
||||||
</div>
|
|
||||||
<div class="key-list-wrp" data-bind="visible: signKey()">
|
|
||||||
<div class="key-list__item">
|
|
||||||
<div class="key-list__item-hash">
|
|
||||||
(<span data-bind="text: signKey() ? signKey().hash : ''"></span>)
|
|
||||||
</div>
|
|
||||||
<div class="key-list__item-names">
|
|
||||||
<!-- ko if: signKey() -->
|
|
||||||
<!-- ko foreach: signKey().users -->
|
|
||||||
<div class="key-list__item-name" data-bind="text: $data"></div>
|
|
||||||
<!-- /ko -->
|
|
||||||
<!-- /ko -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-bind="component: {
|
|
||||||
name: 'Checkbox',
|
|
||||||
params: {
|
|
||||||
label: 'POPUPS_COMPOSE_OPEN_PGP/LABEL_ENCRYPT',
|
|
||||||
value: encrypt
|
|
||||||
}
|
|
||||||
}"></div>
|
|
||||||
|
|
||||||
<div class="key-list" data-bind="visible: encrypt">
|
|
||||||
<div class="key-list-wrp empty" data-bind="visible: encryptKeys().length === 0">
|
|
||||||
No public keys selected
|
|
||||||
</div>
|
|
||||||
<div class="key-list-wrp" data-bind="visible: encryptKeys().length > 0, foreach: encryptKeys">
|
|
||||||
<div class="key-list__item">
|
|
||||||
<div class="key-list__item-hash" data-bind="visible: !empty">
|
|
||||||
(<span data-bind="text: hash"></span>)
|
|
||||||
</div>
|
|
||||||
<div class="key-list__item-names" data-bind="css: {'empty': empty}, foreach: users">
|
|
||||||
<div class="key-list__item-name" data-bind="text: $data"></div>
|
|
||||||
</div>
|
|
||||||
<div class="key-list__item-error" data-bind="visible: empty">
|
|
||||||
(Public key not found)
|
|
||||||
</div>
|
|
||||||
<div class="key-list__item-delete fontastic" data-bind="click: removable() ? $parent.deletePublickKey : null, css: {'disabled': !removable()}">🗑</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="key-actions">
|
|
||||||
<div data-bind="visible: sign()">
|
|
||||||
<input type="password" class="inputPassword input-block-level"
|
|
||||||
autocomplete="current-password" autocorrect="off" autocapitalize="off" spellcheck="false"
|
|
||||||
data-i18n="[placeholder]GLOBAL/PASSWORD"
|
|
||||||
data-bind="textInput: password, onEnter: doCommand" />
|
|
||||||
<select class="input-block-level" data-bind="visible: privateKeysOptions().length, options: privateKeysOptions, value: selectedPrivateKey,
|
|
||||||
optionsCaption: privateKeysOptionsCaption, optionsText: 'name', optionsValue: 'id'"></select>
|
|
||||||
</div>
|
|
||||||
<select class="input-block-level" data-bind="visible: encrypt() && publicKeysOptions().length, options: publicKeysOptions, value: selectedPublicKey,
|
|
||||||
optionsCaption: publicKeysOptionsCaption, optionsText: 'name', optionsValue: 'id'"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<button class="btn buttonDo" data-bind="command: doCommand,
|
|
||||||
enable: (sign() || encrypt()) && (!encrypt() || encrypt() && encryptKeys().length > 0)">
|
|
||||||
<i class="fontastic" data-bind="css: {'icon-spinner': submitRequest()}">🔑</i>
|
|
||||||
<span data-bind="visible: sign() && !encrypt()" data-i18n="POPUPS_COMPOSE_OPEN_PGP/BUTTON_SIGN"></span>
|
|
||||||
<span data-bind="visible: !sign() && encrypt()" data-i18n="POPUPS_COMPOSE_OPEN_PGP/BUTTON_ENCRYPT"></span>
|
|
||||||
<span data-bind="visible: (sign() && encrypt()) || (!sign() && !encrypt())" data-i18n="POPUPS_COMPOSE_OPEN_PGP/BUTTON_SIGN_AND_ENCRYPT"></span>
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue