Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / helpers / secureSessionStorage.ts
blobf24616faf96e02468bc959f2c217902412c44a6f
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';
5 /**
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.
13  *
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.
19  *
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.
27  *
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.
36  */
38 const deserialize = (string: string) => {
39     try {
40         return JSON.parse(string);
41     } catch (e: any) {
42         return {};
43     }
46 const serialize = (data: any) => JSON.stringify(data);
48 const deserializeItem = (value: string | undefined) => {
49     if (value === undefined) {
50         return;
51     }
52     try {
53         return stringToUint8Array(atob(value));
54     } catch (e: any) {
55         return undefined;
56     }
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) {
67         return;
68     }
69     const xored = new Uint8Array(b.length);
70     for (let j = 0; j < b.length; j++) {
71         xored[j] = b[j] ^ a[j];
72     }
74     // Strip off padding
75     let unpaddedLength = b.length;
76     while (unpaddedLength > 0 && xored[unpaddedLength - 1] === 0) {
77         unpaddedLength--;
78     }
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) {
87             return acc;
88         }
89         acc[key] = value;
90         return acc;
91     }, {});
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];
102     }
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 } }>(
109         (acc, key) => {
110             const value = data[key];
111             if (value === undefined) {
112                 return acc;
113             }
114             const [share1, share2] = separatePart(value);
115             acc.share1[key] = share1;
116             acc.share2[key] = share2;
117             return acc;
118         },
119         { share1: {}, share2: {} }
120     );
122 const SESSION_STORAGE_KEY = 'proton:storage';
123 export const save = (data: any) => {
124     if (!hasSessionStorage()) {
125         return;
126     }
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()) {
134         return {};
135     }
136     try {
137         const share1 = deserialize(window.name);
138         const share2 = window.sessionStorage.getItem(SESSION_STORAGE_KEY) || '';
139         window.name = '';
140         window.sessionStorage.removeItem(SESSION_STORAGE_KEY);
141         const string = mergePart(share1, share2) || '';
142         const parsedValue = JSON.parse(string) || {};
143         if (parsedValue === Object(parsedValue)) {
144             return parsedValue;
145         }
146         return {};
147     } catch {
148         return {};
149     }