1 import { c } from 'ttag';
3 import { ARGON2_PARAMS } from '@proton/crypto/lib';
4 import { importKey } from '@proton/crypto/lib/subtle/aesGcm';
5 import type { TabId } from '@proton/pass/types';
6 import { type Api, AuthMode, type MaybeNull } from '@proton/pass/types';
7 import { getErrorMessage } from '@proton/pass/utils/errors/get-error-message';
8 import { getEpoch } from '@proton/pass/utils/time/epoch';
9 import { pullForkSession, setRefreshCookies as refreshTokens, setCookies } from '@proton/shared/lib/api/auth';
10 import { getUser } from '@proton/shared/lib/api/user';
11 import { getAppHref } from '@proton/shared/lib/apps/helper';
12 import { getWelcomeToText } from '@proton/shared/lib/apps/text';
13 import { InvalidForkConsumeError } from '@proton/shared/lib/authentication/error';
14 import { getForkDecryptedBlob } from '@proton/shared/lib/authentication/fork/blob';
15 import { ForkSearchParameters, type ForkType } from '@proton/shared/lib/authentication/fork/constants';
16 import { getValidatedForkType, getValidatedRawKey } from '@proton/shared/lib/authentication/fork/validation';
17 import type { PullForkResponse, RefreshSessionResponse } from '@proton/shared/lib/authentication/interface';
18 import type { APP_NAMES } from '@proton/shared/lib/constants';
19 import { APPS, MAIL_APP_NAME, PASS_APP_NAME, SSO_PATHS } from '@proton/shared/lib/constants';
20 import { withAuthHeaders, withUIDHeaders } from '@proton/shared/lib/fetch/headers';
21 import { encodeBase64URL, uint8ArrayToString } from '@proton/shared/lib/helpers/encoding';
22 import type { User } from '@proton/shared/lib/interfaces';
23 import getRandomString from '@proton/utils/getRandomString';
25 import { AUTH_MODE } from './flags';
26 import { LockMode } from './lock/types';
27 import { type AuthSession, type AuthSessionVersion, SESSION_VERSION } from './session';
28 import { encodeUserData } from './store';
30 export type RequestForkOptions = {
36 payloadType?: 'offline';
37 payloadVersion?: AuthSessionVersion;
39 export type RequestForkResult = { state: string; url: string };
41 export const requestFork = ({
43 host = getAppHref('/', APPS.PROTONACCOUNT),
47 payloadType = 'offline',
49 }: RequestForkOptions): RequestForkResult => {
50 const state = encodeBase64URL(uint8ArrayToString(crypto.getRandomValues(new Uint8Array(32))));
52 const searchParams = new URLSearchParams();
53 searchParams.append(ForkSearchParameters.App, app);
54 searchParams.append(ForkSearchParameters.State, state);
55 searchParams.append(ForkSearchParameters.Independent, '0');
56 if (prompt === 'login') {
57 searchParams.append(ForkSearchParameters.Prompt, 'login'); /* force re-auth */
58 searchParams.append(ForkSearchParameters.PromptType, 'offline'); /* compute offline params */
60 if (payloadType === 'offline') {
61 searchParams.append(ForkSearchParameters.PayloadType, payloadType); /* offline payload */
63 if (payloadVersion === 2) searchParams.append(ForkSearchParameters.PayloadVersion, `${payloadVersion}`);
64 if (localID !== undefined) searchParams.append(ForkSearchParameters.LocalID, `${localID}`);
65 if (forkType) searchParams.append(ForkSearchParameters.ForkType, forkType);
67 return { url: `${host}${SSO_PATHS.AUTHORIZE}?${searchParams.toString()}`, state };
70 export type PullForkCall = (payload: ConsumeForkPayload) => Promise<PullForkResponse>;
71 export type ConsumedFork = { session: AuthSession; Scopes: string[] };
72 export type ConsumeForkParameters = ReturnType<typeof getConsumeForkParameters>;
74 export type ConsumeForkOptions = {
76 payload: ConsumeForkPayload;
78 pullFork?: PullForkCall;
81 export type ConsumeForkPayload =
85 localState: MaybeNull<string>;
86 payloadVersion: AuthSessionVersion;
98 /** FIXME: support passing offline key components
99 * when consuming a "secure" extension fork */
103 * If `keyPassword` is not provided to `ConsumeForkOptions`, it will attempt to recover it from
104 * the `Payload` property of the `PullForkResponse`. `keyPassword` will always be omitted when
105 * retrieving the fork options from the url parameters. This is not the case when using secure
106 * extension messaging where `keyPassword` can safely be passed.
107 * ⚠️ Only validates the fork state in SSO mode.
109 export const consumeFork = async (options: ConsumeForkOptions): Promise<ConsumedFork> => {
110 const { payload, apiUrl, api } = options;
111 const cookies = AUTH_MODE === AuthMode.COOKIE;
114 (payload.mode === 'secure' || (payload.localState !== null && payload.key)) &&
118 if (!validFork) throw new InvalidForkConsumeError('Invalid fork state');
120 const pullFork: PullForkCall =
123 const pullForkParams = pullForkSession(selector);
124 pullForkParams.url = apiUrl ? `${apiUrl}/${pullForkParams.url}` : pullForkParams.url;
125 return api<PullForkResponse>(pullForkParams);
128 const { UID, RefreshToken, LocalID, Payload, Scopes } = await pullFork(payload);
129 const refresh = await api<RefreshSessionResponse>(withUIDHeaders(UID, refreshTokens({ RefreshToken })));
130 const { User } = await api<{ User: User }>(withAuthHeaders(UID, refresh.AccessToken, getUser()));
139 RefreshToken: refresh.RefreshToken,
140 State: getRandomString(24),
141 Persistent: payload.persistent,
148 payload.mode === 'secure'
149 ? { keyPassword: payload.keyPassword, payloadVersion: SESSION_VERSION }
150 : await (async () => {
152 const { payloadVersion, key } = payload;
153 const clientKey = await importKey(key!);
154 const decryptedBlob = await getForkDecryptedBlob(clientKey, Payload, payloadVersion);
155 if (!decryptedBlob?.keyPassword) throw new Error('Missing `keyPassword`');
158 keyPassword: decryptedBlob.keyPassword,
160 ...(decryptedBlob.type === 'offline'
163 salt: atob(decryptedBlob.offlineKeySalt),
164 params: ARGON2_PARAMS.RECOMMENDED,
166 offlineKD: atob(decryptedBlob.offlineKeyPassword),
171 throw new InvalidForkConsumeError(getErrorMessage(err));
175 const session: AuthSession = {
180 userData: encodeUserData(User.Email, User.DisplayName),
181 lastUsedAt: getEpoch(),
182 AccessToken: cookies ? '' : refresh.AccessToken,
183 RefreshToken: cookies ? '' : refresh.RefreshToken,
184 lockMode: LockMode.NONE,
185 persistent: payload.persistent,
189 return { session, Scopes };
192 export enum AccountForkResponse {
198 export const getAccountForkResponsePayload = (type: AccountForkResponse, error?: any) => {
199 const additionalMessage = getErrorMessage(error);
201 const payload = (() => {
203 case AccountForkResponse.CONFLICT: {
205 title: c('Error').t`Authentication error`,
207 .t`It seems you are already logged in to ${PASS_APP_NAME}. If you're trying to login with a different account, please logout from the extension first.`,
210 case AccountForkResponse.SUCCESS: {
212 title: getWelcomeToText(PASS_APP_NAME),
214 .t`More than a password manager, ${PASS_APP_NAME} protects your password and your personal email address via email aliases. Powered by the same technology behind ${MAIL_APP_NAME}, your data is end-to-end encrypted and is only accessible by you.`,
217 case AccountForkResponse.ERROR: {
219 title: c('Error').t`Something went wrong`,
220 message: c('Warning').t`Unable to sign in to ${PASS_APP_NAME}. ${additionalMessage}`,
229 export const getConsumeForkParameters = () => {
230 const sliceIndex = window.location.hash.lastIndexOf('#') + 1;
231 const hashParams = new URLSearchParams(window.location.hash.slice(sliceIndex));
232 const selector = hashParams.get(ForkSearchParameters.Selector) || '';
233 const state = hashParams.get(ForkSearchParameters.State) || '';
234 const base64StringKey = hashParams.get(ForkSearchParameters.Base64Key) || '';
235 const type = hashParams.get(ForkSearchParameters.ForkType) || '';
236 const persistent = hashParams.get(ForkSearchParameters.Persistent) || '';
237 const trusted = hashParams.get(ForkSearchParameters.Trusted) || '';
238 const payloadVersion = hashParams.get(ForkSearchParameters.PayloadVersion) || '';
239 const payloadType = hashParams.get(ForkSearchParameters.PayloadType) || '';
242 state: state.slice(0, 100),
244 key: base64StringKey.length ? getValidatedRawKey(base64StringKey) : undefined,
245 type: getValidatedForkType(type),
246 persistent: persistent === '1',
247 trusted: trusted === '1',
248 payloadVersion: payloadVersion === '2' ? 2 : 1,
249 payloadType: payloadType === 'offline' ? payloadType : 'default',