1 /* Inspired from packages/shared/lib/authentication/persistedSessionHelper.ts */
2 import { stringToUtf8Array } from '@proton/crypto/lib/utils';
3 import { type OfflineConfig, getOfflineVerifier } from '@proton/pass/lib/cache/crypto';
4 import type { Api, Maybe, MaybeNull } from '@proton/pass/types';
5 import { getErrorMessage } from '@proton/pass/utils/errors/get-error-message';
6 import { prop } from '@proton/pass/utils/fp/lens';
7 import { getLocalKey, getLocalSessions, setCookies } from '@proton/shared/lib/api/auth';
8 import { InactiveSessionError } from '@proton/shared/lib/api/helpers/errors';
9 import { getUser } from '@proton/shared/lib/api/user';
10 import { getClientKey } from '@proton/shared/lib/authentication/clientKey';
11 import { InvalidPersistentSessionError } from '@proton/shared/lib/authentication/error';
12 import type { LocalKeyResponse, LocalSessionResponse } from '@proton/shared/lib/authentication/interface';
13 import { getDecryptedBlob, getEncryptedBlob } from '@proton/shared/lib/authentication/sessionBlobCryptoHelper';
14 import { stringToUint8Array } from '@proton/shared/lib/helpers/encoding';
15 import type { User as UserType } from '@proton/shared/lib/interfaces';
16 import getRandomString from '@proton/utils/getRandomString';
18 import { SESSION_DIGEST_VERSION, digestSession, getSessionDigestVersion } from './integrity';
19 import type { LockMode } from './lock/types';
20 import type { AuthOptions, AuthServiceConfig } from './service';
21 import type { AuthStore } from './store';
23 export type AuthSessionVersion = 1 | 2;
24 export const SESSION_VERSION: AuthSessionVersion = 1;
26 export type AuthSession = {
29 encryptedOfflineKD?: string;
30 extraPassword?: boolean;
36 offlineConfig?: OfflineConfig;
38 offlineVerifier?: string;
39 payloadVersion?: AuthSessionVersion;
43 sessionLockToken?: string;
45 unlockRetryCount?: number;
50 /** The following values of the `AuthSession` are locally stored in
51 * an encrypted blob using the BE local key for the user's session */
52 export type EncryptedSessionKeys = 'keyPassword' | 'offlineKD' | 'sessionLockToken';
53 export type EncryptedAuthSession = Omit<AuthSession, EncryptedSessionKeys> & { blob: string };
54 export type DecryptedAuthSessionBlob = Pick<AuthSession, EncryptedSessionKeys> & { digest?: string };
56 export const SESSION_KEYS: (keyof AuthSession)[] = [
75 export const getSessionEncryptionTag = (version?: AuthSessionVersion): Maybe<Uint8Array> =>
76 version === 2 ? stringToUtf8Array('session') : undefined;
78 /* Given a local session key, encrypts sensitive session components of
79 * the `AuthSession` before persisting. Additionally stores a SHA-256
80 * integrity digest of the session data to validate when resuming */
81 export const encryptPersistedSessionWithKey = async (session: AuthSession, clientKey: CryptoKey): Promise<string> => {
82 const { keyPassword, offlineKD, payloadVersion = SESSION_VERSION, sessionLockToken, ...rest } = session;
83 const digest = await digestSession(session, SESSION_DIGEST_VERSION);
84 const blob: DecryptedAuthSessionBlob = { keyPassword, offlineKD, sessionLockToken, digest };
86 const value: EncryptedAuthSession = {
88 blob: await getEncryptedBlob(clientKey, JSON.stringify(blob), getSessionEncryptionTag(payloadVersion)),
92 return JSON.stringify(value);
95 /** Synchronizes an `AuthSession` with the latest auth data from the authentication store */
96 export const syncAuthSession = (session: AuthSession, authStore: AuthStore): AuthSession => ({
98 AccessToken: authStore.getAccessToken() ?? session.AccessToken,
99 RefreshTime: authStore.getRefreshTime() ?? session.RefreshTime,
100 RefreshToken: authStore.getRefreshToken() ?? session.RefreshToken,
101 UID: authStore.getUID() ?? session.UID,
102 cookies: authStore.getCookieAuth() ?? session.cookies,
103 persistent: authStore.getPersistent() ?? session.persistent,
106 /** Retrieves the current local key to decrypt the persisted session */
107 export const getPersistedSessionKey = async (api: Api, authStore: AuthStore): Promise<CryptoKey> => {
109 authStore.getClientKey() ??
111 await api<LocalKeyResponse>({
117 authStore.setClientKey(clientKey);
118 return getClientKey(clientKey);
121 export const decryptSessionBlob = async (
122 clientKey: CryptoKey,
124 payloadVersion: AuthSessionVersion
125 ): Promise<DecryptedAuthSessionBlob> => {
127 const decryptedBlob = await getDecryptedBlob(clientKey, blob, getSessionEncryptionTag(payloadVersion));
128 const parsedValue = JSON.parse(decryptedBlob);
130 if (!parsedValue.keyPassword) throw new Error('Missing `keyPassword`');
133 keyPassword: parsedValue.keyPassword,
134 offlineKD: parsedValue.offlineKD,
135 sessionLockToken: parsedValue.sessionLockToken,
136 digest: parsedValue.digest,
139 throw new InvalidPersistentSessionError(getErrorMessage(err));
143 export type ResumeSessionResult = {
144 clientKey: CryptoKey;
146 session: AuthSession;
149 /** Resumes an encrypted session by decrypting the session blob.
150 * - Ensure the authentication store has been hydrated.
151 * - Session tokens may be refreshed during this sequence.
152 * - If applicable, upgrades the session to cookie-based auth. */
153 export const resumeSession = async (
154 persistedSession: EncryptedAuthSession,
155 localID: Maybe<number>,
156 config: AuthServiceConfig,
158 ): Promise<ResumeSessionResult> => {
159 const { api, authStore, onSessionInvalid } = config;
160 const { blob, ...session } = persistedSession;
161 const { UID } = session;
163 const cookieUpgrade = authStore.shouldCookieUpgrade(persistedSession);
166 const [clientKey, { User }] = await Promise.all([
167 getPersistedSessionKey(api, authStore),
168 api<{ User: UserType }>(getUser()),
171 if (!persistedSession || persistedSession.UserID !== User.ID) throw InactiveSessionError();
173 const payloadVersion = session.payloadVersion ?? SESSION_VERSION;
174 const decryptedBlob = await decryptSessionBlob(clientKey, blob, payloadVersion);
176 if (decryptedBlob.digest) {
177 const version = getSessionDigestVersion(decryptedBlob.digest);
178 const digest = await digestSession(persistedSession, version);
179 if (digest !== decryptedBlob.digest) throw new InvalidPersistentSessionError();
183 /** Upgrade the session to cookie-based authentication.
184 * This occurs after a successful token-based API call to ensure
185 * tokens are refreshed before setting the refresh cookies.
186 * We assume the session was persistent for this upgrade.*/
187 const RefreshToken = authStore.getRefreshToken()!;
188 await api(setCookies({ UID, RefreshToken, State: getRandomString(24), Persistent: true }));
189 authStore.setAccessToken('');
190 authStore.setCookieAuth(true);
191 authStore.setPersistent(true);
192 authStore.setRefreshToken('');
195 /** Synchronize the session with the auth store to capture any mutations
196 * that may have occurred during the resumption process (e.g., token refresh
197 * or cookie upgrade). This ensures the returned session contains the most
198 * up-to-date authentication data. */
199 const syncedSession = syncAuthSession({ ...session, ...decryptedBlob }, authStore);
203 repersist: cookieUpgrade || !decryptedBlob.digest,
204 session: syncedSession,
206 } catch (error: unknown) {
207 if (onSessionInvalid) {
208 return onSessionInvalid(error, {
210 invalidSession: persistedSession,
211 retry: (session) => resumeSession(session, localID, config, options),
219 export const getActiveSessions = (api: Api): Promise<MaybeNull<LocalSessionResponse[]>> =>
220 api<{ Sessions: LocalSessionResponse[] }>(getLocalSessions())
221 .then(prop('Sessions'))
224 export const migrateSession = async (authStore: AuthStore): Promise<boolean> => {
225 let migrated = false;
227 const offlineKD = authStore.getOfflineKD();
228 const offlineConfig = authStore.getOfflineConfig();
229 const offlineVerifier = authStore.getOfflineVerifier();
231 /** Create the `offlineVerifier` if it hasn't been generated (<1.18.0) */
232 if (offlineKD && offlineConfig && !offlineVerifier) {
233 authStore.setOfflineVerifier(await getOfflineVerifier(stringToUint8Array(offlineKD)));