mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-25 18:19:22 +03:00
155 lines
4.2 KiB
JavaScript
155 lines
4.2 KiB
JavaScript
import { AskPopupView } from 'View/Popup/Ask';
|
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
|
import { SettingsGet } from '../Common/Globals';
|
|
import { isArray } from '../Common/Utils';
|
|
|
|
export const Passphrases = new WeakMap();
|
|
|
|
Passphrases.ask = async (key, sAskDesc, btnText) => {
|
|
if (Passphrases.has(key)) {
|
|
return { password: Passphrases.handle(key)/*, remember:false*/ };
|
|
} else if (Passphrases.hasInLocalStorage(key)) {
|
|
return { password: await getFromLocalStorage(key) };
|
|
} else {
|
|
const pass = await AskPopupView.password(sAskDesc, btnText,
|
|
window.crypto.subtle && canUseLocalStorage(key) ? 0b1101 : 0b0101);
|
|
pass.rememberPermanent && await saveToLocalStorage(key, pass.password);
|
|
return pass;
|
|
}
|
|
};
|
|
|
|
Passphrases._deleteFromSession = Passphrases.delete;
|
|
|
|
const timeouts = {};
|
|
// get/set accessor to control deletion after N minutes of inactivity
|
|
Passphrases.handle = (key, pass) => {
|
|
const timeout = SettingsUserStore.keyPassForget();
|
|
if (timeout && !timeouts[key]) {
|
|
timeouts[key] = (() => Passphrases._deleteFromSession(key)).debounce(timeout * 60 * 1000);
|
|
}
|
|
pass && Passphrases.set(key, pass);
|
|
timeout && timeouts[key]();
|
|
return Passphrases.get(key);
|
|
};
|
|
|
|
const deleteFromLocalStorage = (key) => {
|
|
const keyId = getKeyId(key);
|
|
if (keyId) {
|
|
localStorage.removeItem(keyId);
|
|
}
|
|
};
|
|
Passphrases.delete = (key) => {
|
|
deleteFromLocalStorage(key);
|
|
return Passphrases._deleteFromSession(key);
|
|
};
|
|
|
|
Passphrases.hasInLocalStorage = (key) => {
|
|
const keyId = getKeyId(key);
|
|
return keyId && localStorage.getItem(keyId) !== null;
|
|
};
|
|
|
|
const saveToLocalStorage = async (key, pass) => {
|
|
const keyId = getKeyId(key);
|
|
if (!keyId) {
|
|
return;
|
|
}
|
|
|
|
if (!pass) {
|
|
localStorage.removeItem(keyId);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const salt = window.crypto.getRandomValues(new Uint8Array(16));
|
|
const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), salt);
|
|
|
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
const encrypted = await window.crypto.subtle.encrypt(
|
|
{ name: 'AES-GCM', iv: iv },
|
|
derivedKey,
|
|
new TextEncoder().encode(pass)
|
|
);
|
|
localStorage.setItem(keyId, JSON.stringify([
|
|
btoa(String.fromCharCode.apply(null, salt)),
|
|
btoa(String.fromCharCode.apply(null, iv)),
|
|
btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)))
|
|
]));
|
|
} catch (e) {
|
|
console.error('Passphrases.saveToLocalStorage failed', e);
|
|
}
|
|
};
|
|
|
|
const getFromLocalStorage = async (key) => {
|
|
const keyId = getKeyId(key);
|
|
if (!keyId) {
|
|
return undefined;
|
|
}
|
|
|
|
const jsonData = localStorage.getItem(keyId);
|
|
if (!jsonData) {
|
|
console.error('Passphrases.getFromLocalStorage failed: no data found');
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
const saltIvData = JSON.parse(jsonData);
|
|
if (!saltIvData || !isArray(saltIvData) || saltIvData.length !== 3) {
|
|
// noinspection ExceptionCaughtLocallyJS
|
|
throw new Error('invalid passphrase data');
|
|
}
|
|
const toUint8 = (str) => new Uint8Array(atob(str).split('').map(c => c.charCodeAt(0)));
|
|
|
|
const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), toUint8(saltIvData[0]));
|
|
const decrypted = await window.crypto.subtle.decrypt(
|
|
{
|
|
name: 'AES-GCM',
|
|
iv: toUint8(saltIvData[1])
|
|
},
|
|
derivedKey,
|
|
toUint8(saltIvData[2])
|
|
);
|
|
return String.fromCharCode.apply(null, new Uint8Array(decrypted));
|
|
} catch (e) {
|
|
localStorage.removeItem(keyId);
|
|
console.error('Passphrases.getFromLocalStorage failed', e);
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const canUseLocalStorage = (key) => getKeyId(key) !== undefined;
|
|
|
|
const getKeyId = (key) => {
|
|
if (key && typeof key.id === 'string' && key.id.length > 4 && typeof key.forgetPass === 'function') {
|
|
// only deal with keys that we can forget (OpenPGB, GnuPG)
|
|
return key.id + '_local_key';
|
|
} else {
|
|
console.info('Passphrases.getKeyId: unsupported key type');
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const deriveKeyFromHash = async (hash, salt) => {
|
|
if (!hash) {
|
|
throw new Error('empty accountHash');
|
|
}
|
|
return window.crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(hash),
|
|
{ 'name': 'PBKDF2' },
|
|
false,
|
|
['deriveKey']
|
|
).then(keyMaterial => {
|
|
return window.crypto.subtle.deriveKey(
|
|
{
|
|
'name': 'PBKDF2',
|
|
'salt': salt,
|
|
'iterations': 512,
|
|
'hash': 'SHA-256'
|
|
},
|
|
keyMaterial,
|
|
{ 'name': 'AES-GCM', 'length': 256 },
|
|
false,
|
|
['encrypt', 'decrypt']
|
|
);
|
|
});
|
|
};
|