/** * OpenPGP.js */ import ko from 'ko'; import { arrayLength } from 'Common/Utils'; import Remote from 'Remote/User/Fetch'; import { showScreenPopup } from 'Knoin/Knoin'; import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey'; import { Passphrases } from 'Storage/Passphrases'; import { baseCollator } from 'Common/Translator'; const loaded = () => !!window.openpgp, findOpenPGPKey = (keys, query/*, sign*/) => keys.find(key => key.for(query) || query == key.id || query == key.fingerprint ), decryptKey = async (privateKey, btnTxt = 'SIGN') => { if (privateKey.key.isDecrypted()) { return privateKey.key; } const key = privateKey.id, pass = await Passphrases.ask(privateKey, 'OpenPGP.js key
' + key + ' ' + privateKey.emails[0], 'CRYPTO/'+btnTxt ); if (pass) { const passphrase = pass.password, result = await openpgp.decryptKey({ privateKey: privateKey.key, passphrase }); result && pass.remember && Passphrases.handle(privateKey, passphrase); return result; } }, collator = baseCollator(), sort = keys => keys.sort( // (a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.fingerprint, b.fingerprint) (a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id) ), dedup = keys => sort((keys || []) .filter((v, i, a) => a.findIndex(entry => entry.fingerprint == v.fingerprint) === i) ), /** * OpenPGP.js v5 removed the localStorage (keyring) * This should be compatible with the old OpenPGP.js v2 */ publicKeysItem = 'openpgp-public-keys', privateKeysItem = 'openpgp-private-keys', storage = window.localStorage, loadOpenPgpKeys = async itemname => { let keys = [], key, armoredKeys = JSON.parse(storage.getItem(itemname)), i = arrayLength(armoredKeys); while (i--) try { key = await openpgp.readKey({armoredKey:armoredKeys[i]}); key.err || keys.push(new OpenPgpKeyModel(armoredKeys[i], key)); } catch (e) { console.error(e); } return keys; }, storeOpenPgpKeys = (keys, section) => { let armoredKeys = keys.map(item => item.armor); if (armoredKeys.length) { storage.setItem(section, JSON.stringify(armoredKeys)); } else { storage.removeItem(section); } }; class OpenPgpKeyModel { constructor(armor, key) { this.key = key; this.id = key.getKeyID().toHex().toUpperCase(); this.fingerprint = key.getFingerprint(); this.can_encrypt = !!key.getEncryptionKey(); this.can_sign = !!key.getSigningKey(); this.emails = key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email); this.armor = armor; this.askDelete = ko.observable(false); this.openForDeletion = ko.observable(null).askDeleteHelper(); // key.getUserIDs() // key.getPrimaryUser() } /* get id() { return this.key.getKeyID().toHex().toUpperCase(); } get fingerprint() { return this.key.getFingerprint(); } get can_encrypt() { return !!this.key.getEncryptionKey(); } get can_sign() { return !!this.key.getSigningKey(); } get emails() { return this.key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email); } get armor() { return this.key.armor(); } */ for(email) { return this.emails.includes(IDN.toASCII(email)); } view() { showScreenPopup(OpenPgpKeyPopupView, [this]); } remove() { if (this.askDelete()) { if (this.key.isPrivate()) { OpenPGPUserStore.privateKeys.remove(this); storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem); } else { OpenPGPUserStore.publicKeys.remove(this); storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem); } } } /* toJSON() { return this.armor; } */ } export const OpenPGPUserStore = new class { constructor() { this.publicKeys = ko.observableArray(); this.privateKeys = ko.observableArray(); } loadKeyrings() { if (loaded()) { loadOpenPgpKeys(publicKeysItem) .then(keys => { this.publicKeys(dedup(keys)); console.log('openpgp.js public keys loaded'); }) .finally(() => { loadOpenPgpKeys(privateKeysItem) .then(keys => { this.privateKeys(dedup(keys)); console.log('openpgp.js private keys loaded'); }) .finally(() => { /*SettingsGet('loadBackupKeys') && */this.loadBackupKeys(); }); }); } } loadBackupKeys() { Remote.request('GetPGPKeys', (iError, oData) => !iError && oData.Result && this.importKeys(oData.Result) ); } /** * @returns {boolean} */ isSupported() { return loaded(); } importKey(armoredKey) { this.importKeys([armoredKey]); } async importKeys(keys) { if (loaded()) { const privateKeys = this.privateKeys(), publicKeys = this.publicKeys(); for (const armoredKey of keys) try { let key = await openpgp.readKey({armoredKey:armoredKey}); if (!key.err) { key = new OpenPgpKeyModel(armoredKey, key); const keys = key.key.isPrivate() ? privateKeys : publicKeys; keys.find(entry => entry.fingerprint == key.fingerprint) || keys.push(key); } } catch (e) { console.error(e, armoredKey); } this.privateKeys(sort(privateKeys)); this.publicKeys(sort(publicKeys)); storeOpenPgpKeys(privateKeys, privateKeysItem); storeOpenPgpKeys(publicKeys, publicKeysItem); } } /** keyPair.privateKey keyPair.publicKey keyPair.revocationCertificate */ storeKeyPair(keyPair) { if (loaded()) { openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => { this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key)); storeOpenPgpKeys(this.publicKeys, publicKeysItem); }); openpgp.readKey({armoredKey:keyPair.privateKey}).then(key => { this.privateKeys.push(new OpenPgpKeyModel(keyPair.privateKey, key)); storeOpenPgpKeys(this.privateKeys, privateKeysItem); }); } } /** * Checks if verifying/encrypting a message is possible with given email addresses. */ hasPublicKeyForEmails(recipients) { const count = recipients.length, length = count ? recipients.filter(email => this.publicKeys().find(key => key.for(email)) ).length : 0; return length && length === count; } getPrivateKeyFor(query/*, sign*/) { return findOpenPGPKey(this.privateKeys, query/*, sign*/); } /** * https://docs.openpgpjs.org/#encrypt-and-decrypt-string-data-with-pgp-keys */ async decrypt(armoredText, sender) { const message = await openpgp.readMessage({ armoredMessage: armoredText }), privateKeys = this.privateKeys(), msgEncryptionKeyIDs = message.getEncryptionKeyIDs().map(key => key.bytes); // Find private key that can decrypt message let i = privateKeys.length, privateKey; while (i--) { if ((await privateKeys[i].key.getDecryptionKeys()).find( key => msgEncryptionKeyIDs.includes(key.getKeyID().bytes) )) { privateKey = privateKeys[i]; break; } } if (privateKey) try { const decryptedKey = await decryptKey(privateKey, 'DECRYPT'); if (decryptedKey) { const publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/); return await openpgp.decrypt({ message, verificationKeys: publicKey?.key, // expectSigned: true, // signature: '', // Detached signature decryptionKeys: decryptedKey }); } } catch (err) { alert(err); console.error(err); } } /** * https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages */ async verify(message) { const data = message.pgpSigned(), // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" } publicKey = this.publicKeys().find(key => key.for(message.from[0].email)); if (data && publicKey) { data.folder = message.folder; data.uid = message.uid; data.tryGnuPG = 0; let response; if (data.sigPartId) { response = await Remote.post('PgpVerifyMessage', null, data); } else if (data.bodyPart) { // MimePart response = { Result: { text: data.bodyPart.raw, signature: data.sigPart.body } }; } else { response = { Result: { text: message.plain(), signature: null } }; } if (response) { const signature = response.Result.signature ? await openpgp.readSignature({ armoredSignature: response.Result.signature }) : null; const signedMessage = signature ? await openpgp.createMessage({ text: response.Result.text }) : await openpgp.readCleartextMessage({ cleartextMessage: response.Result.text }); // (signature||signedMessage).getSigningKeyIDs(); let result = await openpgp.verify({ message: signedMessage, verificationKeys: publicKey.key, // expectSigned: true, // !!detachedSignature signature: signature }); return { fingerprint: publicKey.fingerprint, success: result && !!result.signatures.length }; } } } /** * https://docs.openpgpjs.org/global.html#sign */ async sign(text, privateKey, detached) { const signingKey = await decryptKey(privateKey); if (signingKey) { const message = detached ? await openpgp.createMessage({ text: text }) : await openpgp.createCleartextMessage({ text: text }); return await openpgp.sign({ message: message, signingKeys: signingKey, detached: !!detached }); } throw 'Sign cancelled'; } /** * https://docs.openpgpjs.org/global.html#encrypt */ async encrypt(text, recipients, signPrivateKey) { const count = recipients.length; recipients = recipients.map(email => this.publicKeys().find(key => key.for(email))).filter(key => key); if (count === recipients.length) { if (signPrivateKey) { signPrivateKey = await decryptKey(signPrivateKey); if (!signPrivateKey) { return; } } return await openpgp.encrypt({ message: await openpgp.createMessage({ text: text }), encryptionKeys: recipients.map(pkey => pkey.key), signingKeys: signPrivateKey // signature }); } throw 'Encrypt failed'; } };