forked from RainLoop/rainloop-webmail
-
-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an option to store(and delete) encrypted private key passphrase i…
…n the browser across multiple sessions
- Loading branch information
1 parent
486acde
commit 5973d71
Showing
13 changed files
with
227 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,155 @@ | ||
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) => | ||
Passphrases.has(key) | ||
? {password:Passphrases.handle(key)/*, remember:false*/} | ||
: await AskPopupView.password(sAskDesc, btnText, 5); | ||
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.delete(key)).debounce(timeout * 60 * 1000); | ||
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'] | ||
); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.