1 import { stringToUtf8Array } from '@proton/crypto/lib/utils';
2 import isTruthy from '@proton/utils/isTruthy';
3 import noop from '@proton/utils/noop';
5 import { removeLastRefreshDate } from '../api/helpers/refreshStorage';
6 import createListeners from '../helpers/listeners';
7 import { getItem, removeItem, setItem } from '../helpers/storage';
9 DefaultPersistedSession,
10 OfflinePersistedSession,
13 PersistedSessionWithLocalID,
14 } from './SessionInterface';
15 import { InvalidPersistentSessionError } from './error';
16 import { getValidatedLocalID } from './fork/validation';
17 import type { OfflineKey } from './offlineKey';
18 import { getDecryptedBlob, getEncryptedBlob } from './sessionBlobCryptoHelper';
20 // We have business logic relying on this constant, please change with caution!
21 export const STORAGE_PREFIX = 'ps-';
22 const getKey = (localID: number) => `${STORAGE_PREFIX}${localID}`;
24 const sessionRemovalListeners = createListeners<PersistedSession[], Promise<void>>();
26 export const registerSessionRemovalListener = (listener: (persistedSessions: PersistedSession) => Promise<void>) => {
27 sessionRemovalListeners.subscribe(listener);
30 export const getPersistedSession = (localID: number): PersistedSession | undefined => {
31 const itemValue = getItem(getKey(localID));
36 const parsedValue = JSON.parse(itemValue);
38 UserID: parsedValue.UserID || '',
39 UID: parsedValue.UID || '',
40 blob: parsedValue.blob || '',
41 isSubUser: parsedValue.isSubUser || false,
42 persistent: typeof parsedValue.persistent === 'boolean' ? parsedValue.persistent : true, // Default to true (old behavior)
43 trusted: parsedValue.trusted || false,
44 payloadVersion: parsedValue.payloadVersion || 1,
45 persistedAt: parsedValue.persistedAt || 0,
46 ...(parsedValue.offlineKeySalt
48 payloadType: 'offline',
49 offlineKeySalt: parsedValue.offlineKeySalt,
51 : { payloadType: 'default' }),
58 export const removePersistedSession = async (localID: number, UID: string) => {
59 const oldSession = getPersistedSession(localID);
60 if (oldSession?.UID) {
61 removeLastRefreshDate(oldSession.UID);
63 if (!oldSession || (oldSession.UID && UID !== oldSession.UID)) {
66 if (sessionRemovalListeners.length()) {
67 await Promise.all(sessionRemovalListeners.notify(oldSession)).catch(noop);
69 removeItem(getKey(localID));
72 export const getPersistedSessions = (): PersistedSessionWithLocalID[] => {
73 const localStorageKeys = Object.keys(localStorage);
74 return localStorageKeys
75 .filter((key) => key.startsWith(STORAGE_PREFIX))
77 const localID = getValidatedLocalID(key.slice(STORAGE_PREFIX.length));
78 if (localID === undefined) {
81 const result = getPersistedSession(localID);
93 export const getPersistedSessionBlob = (blob: string): PersistedSessionBlob | undefined => {
95 const parsedValue = JSON.parse(blob);
96 const keyPassword = parsedValue.keyPassword || '';
97 const offlineKeyPassword = parsedValue.offlineKeyPassword || '';
99 if (parsedValue.offlineKeyPassword) {
116 export const getDecryptedPersistedSessionBlob = async (
119 payloadVersion: PersistedSession['payloadVersion']
120 ): Promise<PersistedSessionBlob> => {
121 const decryptedBlob = await getDecryptedBlob(
124 payloadVersion === 2 ? stringToUtf8Array('session') : undefined
126 throw new InvalidPersistentSessionError('Failed to decrypt persisted blob');
128 const parsedBlob = getPersistedSessionBlob(decryptedBlob);
130 throw new InvalidPersistentSessionError('Failed to parse persisted blob');
135 export const setPersistedSessionWithBlob = async (
142 offlineKey: OfflineKey | undefined;
148 const payloadVersion =
149 1 as PersistedSession['payloadVersion']; /* Update to 2 when all clients understand it (safe for rollback) */
151 const { clearTextPayloadData, encryptedPayloadData } = ((): {
152 clearTextPayloadData:
153 | Pick<OfflinePersistedSession, 'payloadType' | 'offlineKeySalt'>
154 | Pick<DefaultPersistedSession, 'payloadType'>;
155 encryptedPayloadData: PersistedSessionBlob;
157 if (data.offlineKey) {
159 clearTextPayloadData: {
160 payloadType: 'offline',
161 offlineKeySalt: data.offlineKey.salt,
163 encryptedPayloadData: {
164 keyPassword: data.keyPassword,
165 offlineKeyPassword: data.offlineKey.password,
171 clearTextPayloadData: {
172 payloadType: 'default',
174 encryptedPayloadData: {
175 keyPassword: data.keyPassword,
180 const persistedSession: PersistedSession = {
183 isSubUser: data.isSubUser,
184 persistent: data.persistent,
185 trusted: data.trusted,
187 ...clearTextPayloadData,
188 blob: await getEncryptedBlob(
190 JSON.stringify(encryptedPayloadData),
191 payloadVersion === 2 ? stringToUtf8Array('session') : undefined
193 persistedAt: Date.now(),
195 setItem(getKey(localID), JSON.stringify(persistedSession));