2022-01-30 09:35:53 +08:00
|
|
|
/**
|
|
|
|
* OpenPGP.js
|
|
|
|
*/
|
|
|
|
|
|
|
|
import ko from 'ko';
|
|
|
|
|
2022-01-31 18:51:15 +08:00
|
|
|
import { arrayLength } from 'Common/Utils';
|
2022-01-30 09:35:53 +08:00
|
|
|
|
2022-02-02 22:24:32 +08:00
|
|
|
import Remote from 'Remote/User/Fetch';
|
|
|
|
|
2022-01-30 09:35:53 +08:00
|
|
|
import { showScreenPopup } from 'Knoin/Knoin';
|
|
|
|
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
|
|
|
|
2023-02-20 22:49:38 +08:00
|
|
|
import { Passphrases } from 'Storage/Passphrases';
|
|
|
|
|
2024-03-19 22:24:34 +08:00
|
|
|
import { baseCollator } from 'Common/Translator';
|
|
|
|
|
2022-01-30 09:35:53 +08:00
|
|
|
const
|
2024-05-26 18:53:35 +08:00
|
|
|
loaded = () => !!window.openpgp,
|
|
|
|
|
2022-01-30 09:35:53 +08:00
|
|
|
findOpenPGPKey = (keys, query/*, sign*/) =>
|
|
|
|
keys.find(key =>
|
2024-03-12 23:20:19 +08:00
|
|
|
key.for(query) || query == key.id || query == key.fingerprint
|
2022-01-30 09:35:53 +08:00
|
|
|
),
|
|
|
|
|
2024-02-19 22:14:11 +08:00
|
|
|
decryptKey = async (privateKey, btnTxt = 'SIGN') => {
|
2022-08-08 15:53:19 +08:00
|
|
|
if (privateKey.key.isDecrypted()) {
|
|
|
|
return privateKey.key;
|
|
|
|
}
|
2023-02-02 19:39:21 +08:00
|
|
|
const key = privateKey.id,
|
2024-02-23 10:22:29 +08:00
|
|
|
pass = await Passphrases.ask(privateKey,
|
2024-02-22 06:28:53 +08:00
|
|
|
'OpenPGP.js key<br>' + key + ' ' + privateKey.emails[0],
|
|
|
|
'CRYPTO/'+btnTxt
|
|
|
|
);
|
2023-02-02 19:39:21 +08:00
|
|
|
if (pass) {
|
2023-02-02 21:04:37 +08:00
|
|
|
const passphrase = pass.password,
|
|
|
|
result = await openpgp.decryptKey({
|
|
|
|
privateKey: privateKey.key,
|
|
|
|
passphrase
|
|
|
|
});
|
2024-03-18 21:36:33 +08:00
|
|
|
result && pass.remember && Passphrases.handle(privateKey, passphrase);
|
2023-02-02 21:04:37 +08:00
|
|
|
return result;
|
2022-08-08 15:53:19 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2024-03-19 22:24:34 +08:00
|
|
|
collator = baseCollator(),
|
2024-02-27 02:21:40 +08:00
|
|
|
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)
|
|
|
|
),
|
|
|
|
|
2022-01-30 09:35:53 +08:00
|
|
|
/**
|
|
|
|
* 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);
|
2024-03-13 01:44:36 +08:00
|
|
|
while (i--) try {
|
2022-01-30 09:35:53 +08:00
|
|
|
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
|
2022-10-10 19:52:56 +08:00
|
|
|
key.err || keys.push(new OpenPgpKeyModel(armoredKeys[i], key));
|
2024-03-13 01:44:36 +08:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
|
|
|
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;
|
2022-01-31 19:34:41 +08:00
|
|
|
this.id = key.getKeyID().toHex().toUpperCase();
|
2022-01-30 09:35:53 +08:00
|
|
|
this.fingerprint = key.getFingerprint();
|
|
|
|
this.can_encrypt = !!key.getEncryptionKey();
|
|
|
|
this.can_sign = !!key.getSigningKey();
|
2024-03-12 23:20:19 +08:00
|
|
|
this.emails = key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email);
|
2022-01-30 09:35:53 +08:00
|
|
|
this.armor = armor;
|
|
|
|
this.askDelete = ko.observable(false);
|
|
|
|
this.openForDeletion = ko.observable(null).askDeleteHelper();
|
|
|
|
// key.getUserIDs()
|
|
|
|
// key.getPrimaryUser()
|
|
|
|
}
|
|
|
|
|
2024-03-04 19:42:23 +08:00
|
|
|
/*
|
|
|
|
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(); }
|
2024-03-12 23:20:19 +08:00
|
|
|
get emails() { return this.key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email); }
|
2024-03-04 19:42:23 +08:00
|
|
|
get armor() { return this.key.armor(); }
|
|
|
|
*/
|
2024-03-12 23:20:19 +08:00
|
|
|
for(email) {
|
|
|
|
return this.emails.includes(IDN.toASCII(email));
|
|
|
|
}
|
2024-03-04 19:42:23 +08:00
|
|
|
|
2022-01-30 09:35:53 +08:00
|
|
|
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() {
|
2024-05-26 18:53:35 +08:00
|
|
|
if (loaded()) {
|
|
|
|
loadOpenPgpKeys(publicKeysItem)
|
|
|
|
.then(keys => {
|
2024-02-08 07:08:24 +08:00
|
|
|
this.publicKeys(dedup(keys));
|
2022-11-04 22:25:53 +08:00
|
|
|
console.log('openpgp.js public keys loaded');
|
2024-05-26 18:53:35 +08:00
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
loadOpenPgpKeys(privateKeysItem)
|
|
|
|
.then(keys => {
|
|
|
|
this.privateKeys(dedup(keys));
|
|
|
|
console.log('openpgp.js private keys loaded');
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
/*SettingsGet('loadBackupKeys') && */this.loadBackupKeys();
|
|
|
|
});
|
2022-11-04 22:25:53 +08:00
|
|
|
});
|
|
|
|
}
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
|
|
|
|
2024-05-26 18:53:35 +08:00
|
|
|
loadBackupKeys() {
|
|
|
|
Remote.request('GetPGPKeys',
|
|
|
|
(iError, oData) => !iError && oData.Result && this.importKeys(oData.Result)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-01-30 09:35:53 +08:00
|
|
|
/**
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
isSupported() {
|
2024-05-26 18:53:35 +08:00
|
|
|
return loaded();
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
importKey(armoredKey) {
|
2024-03-04 19:42:23 +08:00
|
|
|
this.importKeys([armoredKey]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async importKeys(keys) {
|
2024-05-26 18:53:35 +08:00
|
|
|
if (loaded()) {
|
2024-03-04 19:42:23 +08:00
|
|
|
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);
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
2024-03-04 19:42:23 +08:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e, armoredKey);
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
2024-03-04 19:42:23 +08:00
|
|
|
this.privateKeys(sort(privateKeys));
|
|
|
|
this.publicKeys(sort(publicKeys));
|
|
|
|
storeOpenPgpKeys(privateKeys, privateKeysItem);
|
|
|
|
storeOpenPgpKeys(publicKeys, publicKeysItem);
|
|
|
|
}
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
keyPair.privateKey
|
|
|
|
keyPair.publicKey
|
|
|
|
keyPair.revocationCertificate
|
|
|
|
*/
|
|
|
|
storeKeyPair(keyPair) {
|
2024-05-26 18:53:35 +08:00
|
|
|
if (loaded()) {
|
2022-11-04 22:25:53 +08:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if verifying/encrypting a message is possible with given email addresses.
|
|
|
|
*/
|
2022-02-04 23:21:29 +08:00
|
|
|
hasPublicKeyForEmails(recipients) {
|
2022-01-30 09:35:53 +08:00
|
|
|
const count = recipients.length,
|
|
|
|
length = count ? recipients.filter(email =>
|
2024-03-12 23:20:19 +08:00
|
|
|
this.publicKeys().find(key => key.for(email))
|
2022-01-30 09:35:53 +08:00
|
|
|
).length : 0;
|
2022-02-04 23:21:29 +08:00
|
|
|
return length && length === count;
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
getPrivateKeyFor(query/*, sign*/) {
|
|
|
|
return findOpenPGPKey(this.privateKeys, query/*, sign*/);
|
|
|
|
}
|
|
|
|
|
2022-01-31 18:51:15 +08:00
|
|
|
/**
|
|
|
|
* https://docs.openpgpjs.org/#encrypt-and-decrypt-string-data-with-pgp-keys
|
|
|
|
*/
|
2022-02-01 18:45:20 +08:00
|
|
|
async decrypt(armoredText, sender)
|
2022-01-31 15:30:46 +08:00
|
|
|
{
|
2022-02-01 18:45:20 +08:00
|
|
|
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 {
|
2024-02-19 22:14:11 +08:00
|
|
|
const decryptedKey = await decryptKey(privateKey, 'DECRYPT');
|
2022-08-08 15:26:45 +08:00
|
|
|
if (decryptedKey) {
|
|
|
|
const publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/);
|
2022-02-01 18:45:20 +08:00
|
|
|
return await openpgp.decrypt({
|
|
|
|
message,
|
2022-09-02 17:52:07 +08:00
|
|
|
verificationKeys: publicKey?.key,
|
2022-02-01 18:45:20 +08:00
|
|
|
// expectSigned: true,
|
|
|
|
// signature: '', // Detached signature
|
|
|
|
decryptionKeys: decryptedKey
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
alert(err);
|
|
|
|
console.error(err);
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
2022-01-31 15:30:46 +08:00
|
|
|
}
|
2022-01-30 09:35:53 +08:00
|
|
|
|
2022-02-02 22:24:32 +08:00
|
|
|
/**
|
|
|
|
* https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
|
|
|
|
*/
|
|
|
|
async verify(message) {
|
2024-03-03 11:14:49 +08:00
|
|
|
const data = message.pgpSigned(), // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
2024-03-12 23:20:19 +08:00
|
|
|
publicKey = this.publicKeys().find(key => key.for(message.from[0].email));
|
2022-02-02 22:24:32 +08:00
|
|
|
if (data && publicKey) {
|
2023-01-25 01:58:25 +08:00
|
|
|
data.folder = message.folder;
|
|
|
|
data.uid = message.uid;
|
2023-01-26 17:41:55 +08:00
|
|
|
data.tryGnuPG = 0;
|
2022-02-11 18:01:07 +08:00
|
|
|
let response;
|
2023-01-25 01:58:25 +08:00
|
|
|
if (data.sigPartId) {
|
2024-02-20 00:35:48 +08:00
|
|
|
response = await Remote.post('PgpVerifyMessage', null, data);
|
2023-01-26 17:41:55 +08:00
|
|
|
} else if (data.bodyPart) {
|
|
|
|
// MimePart
|
|
|
|
response = { Result: { text: data.bodyPart.raw, signature: data.sigPart.body } };
|
2022-02-11 18:01:07 +08:00
|
|
|
} else {
|
|
|
|
response = { Result: { text: message.plain(), signature: null } };
|
|
|
|
}
|
2022-02-02 22:24:32 +08:00
|
|
|
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
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2022-01-30 09:35:53 +08:00
|
|
|
}
|
|
|
|
|
2022-02-03 17:17:18 +08:00
|
|
|
/**
|
|
|
|
* https://docs.openpgpjs.org/global.html#sign
|
|
|
|
*/
|
|
|
|
async sign(text, privateKey, detached) {
|
2022-08-08 15:53:19 +08:00
|
|
|
const signingKey = await decryptKey(privateKey);
|
|
|
|
if (signingKey) {
|
2022-02-03 17:17:18 +08:00
|
|
|
const message = detached
|
|
|
|
? await openpgp.createMessage({ text: text })
|
|
|
|
: await openpgp.createCleartextMessage({ text: text });
|
2022-02-02 23:50:27 +08:00
|
|
|
return await openpgp.sign({
|
2022-02-03 17:17:18 +08:00
|
|
|
message: message,
|
2022-08-08 15:53:19 +08:00
|
|
|
signingKeys: signingKey,
|
2022-02-03 17:17:18 +08:00
|
|
|
detached: !!detached
|
2022-02-02 23:50:27 +08:00
|
|
|
});
|
|
|
|
}
|
2022-02-08 00:30:41 +08:00
|
|
|
throw 'Sign cancelled';
|
2022-02-02 23:50:27 +08:00
|
|
|
}
|
|
|
|
|
2022-02-03 17:17:18 +08:00
|
|
|
/**
|
|
|
|
* https://docs.openpgpjs.org/global.html#encrypt
|
|
|
|
*/
|
|
|
|
async encrypt(text, recipients, signPrivateKey) {
|
|
|
|
const count = recipients.length;
|
2024-03-12 23:20:19 +08:00
|
|
|
recipients = recipients.map(email => this.publicKeys().find(key => key.for(email))).filter(key => key);
|
2022-02-03 17:17:18 +08:00
|
|
|
if (count === recipients.length) {
|
|
|
|
if (signPrivateKey) {
|
2022-08-08 15:53:19 +08:00
|
|
|
signPrivateKey = await decryptKey(signPrivateKey);
|
|
|
|
if (!signPrivateKey) {
|
2022-02-03 17:17:18 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return await openpgp.encrypt({
|
|
|
|
message: await openpgp.createMessage({ text: text }),
|
|
|
|
encryptionKeys: recipients.map(pkey => pkey.key),
|
|
|
|
signingKeys: signPrivateKey
|
|
|
|
// signature
|
|
|
|
});
|
|
|
|
}
|
2022-02-08 00:30:41 +08:00
|
|
|
throw 'Encrypt failed';
|
2022-02-03 17:17:18 +08:00
|
|
|
}
|
|
|
|
|
2022-01-30 09:35:53 +08:00
|
|
|
};
|