1 import { importKey } from '@proton/crypto/lib/subtle/aesGcm';
3 import { pushForkSession } from '../../api/auth';
4 import { getAppHref, getClientID } from '../../apps/helper';
5 import type { PushForkResponse } from '../../authentication/interface';
6 import type { OfflineKey } from '../../authentication/offlineKey';
7 import type { APP_NAMES } from '../../constants';
8 import { SSO_PATHS } from '../../constants';
9 import { withUIDHeaders } from '../../fetch/headers';
10 import { replaceUrl } from '../../helpers/browser';
11 import { encodeBase64URL, uint8ArrayToString } from '../../helpers/encoding';
12 import type { Api, User } from '../../interfaces';
13 import { getForkEncryptedBlob } from './blob';
14 import type { ForkType } from './constants';
15 import { ForkSearchParameters } from './constants';
17 getEmailSessionForkSearchParameter,
18 getLocalIDForkSearchParameter,
21 } from './validation';
23 export interface ProduceForkPayload {
29 forkType: ForkType | undefined;
32 encryptedPayload: { payloadVersion: 1 | 2; payloadType: 'offline' | 'default' };
35 interface ProduceForkArguments {
40 offlineKey: OfflineKey | undefined;
44 forkParameters: ProduceForkParameters;
47 export const produceFork = async ({
49 session: { UID, keyPassword, offlineKey, persistent, trusted },
50 forkParameters: { state, app, independent, forkType, forkVersion, payloadType, payloadVersion },
51 }: ProduceForkArguments): Promise<ProduceForkPayload> => {
52 const rawKey = crypto.getRandomValues(new Uint8Array(32));
53 const base64StringKey = encodeBase64URL(uint8ArrayToString(rawKey));
54 const encryptedPayload = await (async () => {
55 const forkData = (() => {
56 if (payloadType === 'offline' && offlineKey && offlineKey.salt && offlineKey.password) {
59 keyPassword: keyPassword || '',
60 offlineKeyPassword: offlineKey.password,
61 offlineKeySalt: offlineKey.salt,
64 return { type: 'default', keyPassword: keyPassword || '' } as const;
67 blob: await getForkEncryptedBlob(await importKey(rawKey), forkData, payloadVersion),
68 payloadType: forkData.type,
73 const childClientID = getClientID(app);
74 const { Selector: selector } = await api<PushForkResponse>(
78 Payload: encryptedPayload.blob,
79 ChildClientID: childClientID,
80 Independent: independent ? 1 : 0,
98 export const produceForkConsumption = (
99 { selector, state, key, persistent, trusted, forkType, forkVersion, encryptedPayload, app }: ProduceForkPayload,
100 searchParameters?: URLSearchParams
102 const fragmentSearchParams = new URLSearchParams();
103 fragmentSearchParams.append(ForkSearchParameters.Selector, selector);
104 fragmentSearchParams.append(ForkSearchParameters.State, state);
105 fragmentSearchParams.append(ForkSearchParameters.Base64Key, key);
106 fragmentSearchParams.append(ForkSearchParameters.Version, `${forkVersion}`);
108 fragmentSearchParams.append(ForkSearchParameters.Persistent, '1');
111 fragmentSearchParams.append(ForkSearchParameters.Trusted, '1');
113 if (forkType !== undefined) {
114 fragmentSearchParams.append(ForkSearchParameters.ForkType, forkType);
116 if (encryptedPayload.payloadVersion !== undefined) {
117 fragmentSearchParams.append(ForkSearchParameters.PayloadVersion, `${encryptedPayload.payloadVersion}`);
119 if (encryptedPayload.payloadType !== undefined) {
120 fragmentSearchParams.append(ForkSearchParameters.PayloadType, `${encryptedPayload.payloadType}`);
123 const searchParamsString = searchParameters?.toString() || '';
124 const search = searchParamsString ? `?${searchParamsString}` : '';
125 const fragment = `#${fragmentSearchParams.toString()}`;
127 replaceUrl(getAppHref(`${SSO_PATHS.FORK}${search}${fragment}`, app));
130 export interface ProduceForkParameters {
134 independent: boolean;
137 prompt: 'login' | undefined;
138 promptType: 'offline-bypass' | 'offline' | 'default';
139 payloadType: 'offline' | 'default';
140 payloadVersion: 1 | 2;
144 export interface ProduceForkParametersFull extends ProduceForkParameters {
148 export const getProduceForkParameters = (
149 searchParams: URLSearchParams
150 ): Omit<ProduceForkParametersFull, 'localID' | 'app'> & Partial<Pick<ProduceForkParametersFull, 'localID' | 'app'>> => {
151 const app = searchParams.get(ForkSearchParameters.App) || '';
152 const state = searchParams.get(ForkSearchParameters.State) || '';
153 const localID = getLocalIDForkSearchParameter(searchParams);
154 const forkType = searchParams.get(ForkSearchParameters.ForkType) || '';
155 const prompt = searchParams.get(ForkSearchParameters.Prompt) || '';
156 const plan = searchParams.get(ForkSearchParameters.Plan) || '';
157 const forkVersion = Number(searchParams.get(ForkSearchParameters.Version) || '1');
158 const independent = searchParams.get(ForkSearchParameters.Independent) || '0';
159 const payloadType = (() => {
160 const value = searchParams.get(ForkSearchParameters.PayloadType) || '';
161 if (value === 'offline') {
166 const payloadVersion = (() => {
167 const value = Number(searchParams.get(ForkSearchParameters.PayloadVersion) || '1');
168 if (value === 1 || value === 2) {
173 const promptType = (() => {
174 const value = searchParams.get(ForkSearchParameters.PromptType) || '';
175 if (value === 'offline' || value === 'offline-bypass') {
180 const email = getEmailSessionForkSearchParameter(searchParams);
183 state: state.slice(0, 100),
185 app: getValidatedApp(app),
186 forkType: getValidatedForkType(forkType),
187 prompt: prompt === 'login' ? 'login' : undefined,
190 independent: independent === '1' || independent === 'true',
198 export const getRequiredForkParameters = (
199 forkParameters: ReturnType<typeof getProduceForkParameters>
200 ): forkParameters is ProduceForkParametersFull => {
201 return Boolean(forkParameters.app && forkParameters.state);
204 export const getCanUserReAuth = (user: User) => {
205 return !user.Flags.sso && !user.OrganizationPrivateKey;
208 export const getShouldReAuth = (
209 forkParameters: Pick<ProduceForkParameters, 'prompt' | 'promptType'>,
212 offlineKey: OfflineKey | undefined;
215 const shouldReAuth = forkParameters.prompt === 'login';
219 if (!getCanUserReAuth(authSession.User)) {
222 if (forkParameters.promptType === 'offline-bypass' && authSession.offlineKey) {