1 // Not using openpgp to allow using this without having to depend on openpgp being loaded
2 import { stringToUint8Array, uint8ArrayToString } from './encoding';
3 import { hasStorage as hasSessionStorage } from './sessionStorage';
6 * Partially inspired by http://www.thomasfrank.se/sessionvars.html
7 * However, we aim to deliberately be non-persistent. This is useful for
8 * data that wants to be preserved across refreshes, but is too sensitive
9 * to be safely written to disk. Unfortunately, although sessionStorage is
10 * deleted when a session ends, major browsers automatically write it
11 * to disk to enable a session recovery feature, so using sessionStorage
12 * alone is inappropriate.
14 * To achieve this, we do two tricks. The first trick is to delay writing
15 * any possibly persistent data until the user is actually leaving the
16 * page (onunload). This already prevents any persistence in the face of
17 * crashes, and severely limits the lifetime of any data in possibly
18 * persistent form on refresh.
20 * The second, more important trick is to split sensitive data between
21 * window.name and sessionStorage. window.name is a property that, like
22 * sessionStorage, is preserved across refresh and navigation within the
23 * same tab - however, it seems to never be stored persistently. This
24 * provides exactly the lifetime we want. Unfortunately, window.name is
25 * readable and transferable between domains, so any sensitive data stored
26 * in it would leak to random other websites.
28 * To avoid this leakage, we split sensitive data into two shares which
29 * xor to the sensitive information but which individually are completely
30 * random and give away nothing. One share is stored in window.name, while
31 * the other share is stored in sessionStorage. This construction provides
32 * security that is the best of both worlds - random websites can't read
33 * the data since they can't access sessionStorage, while disk inspections
34 * can't read the data since they can't access window.name. The lifetime
35 * of the data is therefore the smaller lifetime, that of window.name.
38 const deserialize = (string: string) => {
40 return JSON.parse(string);
46 const serialize = (data: any) => JSON.stringify(data);
48 const deserializeItem = (value: string | undefined) => {
49 if (value === undefined) {
53 return stringToUint8Array(atob(value));
59 const serializeItem = (value: Uint8Array) => {
60 return btoa(uint8ArrayToString(value));
63 const mergePart = (serializedA: string | undefined, serializedB: string | undefined) => {
64 const a = deserializeItem(serializedA);
65 const b = deserializeItem(serializedB);
66 if (a === undefined || b === undefined || a.length !== b.length) {
69 const xored = new Uint8Array(b.length);
70 for (let j = 0; j < b.length; j++) {
71 xored[j] = b[j] ^ a[j];
75 let unpaddedLength = b.length;
76 while (unpaddedLength > 0 && xored[unpaddedLength - 1] === 0) {
80 return uint8ArrayToString(xored.slice(0, unpaddedLength));
83 export const mergeParts = (share1: any, share2: any) =>
84 Object.keys(share1).reduce<{ [key: string]: string }>((acc, key) => {
85 const value = mergePart(share1[key], share2[key]);
86 if (value === undefined) {
93 const separatePart = (value: string) => {
94 const item = stringToUint8Array(value);
95 const paddedLength = Math.ceil(item.length / 256) * 256;
97 const share1 = crypto.getRandomValues(new Uint8Array(paddedLength));
98 const share2 = new Uint8Array(share1);
100 for (let i = 0; i < item.length; i++) {
101 share2[i] ^= item[i];
104 return [serializeItem(share1), serializeItem(share2)];
107 export const separateParts = (data: any) =>
108 Object.keys(data).reduce<{ share1: { [key: string]: any }; share2: { [key: string]: any } }>(
110 const value = data[key];
111 if (value === undefined) {
114 const [share1, share2] = separatePart(value);
115 acc.share1[key] = share1;
116 acc.share2[key] = share2;
119 { share1: {}, share2: {} }
122 const SESSION_STORAGE_KEY = 'proton:storage';
123 export const save = (data: any) => {
124 if (!hasSessionStorage()) {
127 const [share1, share2] = separatePart(JSON.stringify(data));
128 window.name = serialize(share1);
129 window.sessionStorage.setItem(SESSION_STORAGE_KEY, share2);
132 export const load = () => {
133 if (!hasSessionStorage()) {
137 const share1 = deserialize(window.name);
138 const share2 = window.sessionStorage.getItem(SESSION_STORAGE_KEY) || '';
140 window.sessionStorage.removeItem(SESSION_STORAGE_KEY);
141 const string = mergePart(share1, share2) || '';
142 const parsedValue = JSON.parse(string) || {};
143 if (parsedValue === Object(parsedValue)) {