mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-07-25 23:48:13 +01:00
391 lines
12 KiB
TypeScript
391 lines
12 KiB
TypeScript
// Copyright 2026 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import type { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability.std.ts';
|
|
import type { VerificationTransport } from './VerificationTransport.std.ts';
|
|
|
|
// Some example simple workflows:
|
|
// - Totally new account creation:
|
|
// PHONE_NUMBER -> CAPTCHA -> VERIFICATION_CODE -> PROFILE_ENTRY -> CREATE_PIN -> CREATE_PIN_CONFIRM -> <complete>
|
|
// - Re-registration, with profile and phone from previous registration:
|
|
// PHONE_NUMBER (phone prepopulated) -> CAPTCHA -> VERIFICATION_CODE -> PROFILE_ENTRY (data prepopulated) -> VERIFY_PIN -> <complete>
|
|
// - Unable to provide correct verification code:
|
|
// PHONE_NUMBER -> CAPTCHA -> VERIFICATION_CODE -> PHONE_NUMBER (in 'waiting' status)
|
|
// - Unable to provide correct PIN:
|
|
// PHONE_NUMBER -> CAPTCHA -> VERIFICATION_CODE -> PROFILE_ENTRY -> VERIFY_PIN -> CREATE_PIN -> CREATE_PIN_CONFIRM -> <complete>
|
|
// - Unable to provide correct PIN with reglock:
|
|
// PHONE_NUMBER -> CAPTCHA -> VERIFICATION_CODE -> PROFILE_ENTRY -> VERIFY_PIN -> ACCOUNT_LOCKED -> PHONE_NUMBER
|
|
|
|
export enum RegistrationStage {
|
|
PHONE_NUMBER = 'PHONE_NUMBER',
|
|
CAPTCHA = 'CAPTCHA',
|
|
VERIFICATION_CODE = 'VERIFICATION_CODE',
|
|
PROFILE_ENTRY = 'PROFILE_ENTRY',
|
|
VERIFY_PIN = 'VERIFY_PIN',
|
|
CREATE_PIN = 'CREATE_PIN',
|
|
CREATE_PIN_CONFIRM = 'CREATE_PIN_CONFIRM',
|
|
ACCOUNT_LOCKED = 'ACCOUNT_LOCKED',
|
|
}
|
|
|
|
export const StageOrder: Record<RegistrationStage, number> = {
|
|
[RegistrationStage.PHONE_NUMBER]: 0,
|
|
[RegistrationStage.CAPTCHA]: 1,
|
|
[RegistrationStage.VERIFICATION_CODE]: 2,
|
|
[RegistrationStage.PROFILE_ENTRY]: 3,
|
|
[RegistrationStage.VERIFY_PIN]: 4,
|
|
[RegistrationStage.CREATE_PIN]: 5,
|
|
[RegistrationStage.CREATE_PIN_CONFIRM]: 6,
|
|
[RegistrationStage.ACCOUNT_LOCKED]: 7,
|
|
};
|
|
|
|
// A few special-cases are always allowed:
|
|
// 1. You can always return to the initial PHONE_NUMBER stage
|
|
// 2. You can only start from the initial PHONE_NUMBER stage
|
|
// 3. You can always make an update to the existing stage
|
|
export const ValidNextStages: Record<
|
|
RegistrationStage,
|
|
Set<RegistrationStage>
|
|
> = {
|
|
[RegistrationStage.PHONE_NUMBER]: new Set([RegistrationStage.CAPTCHA]),
|
|
[RegistrationStage.CAPTCHA]: new Set([RegistrationStage.VERIFICATION_CODE]),
|
|
[RegistrationStage.VERIFICATION_CODE]: new Set([
|
|
RegistrationStage.PROFILE_ENTRY,
|
|
]),
|
|
[RegistrationStage.PROFILE_ENTRY]: new Set([
|
|
RegistrationStage.VERIFY_PIN,
|
|
RegistrationStage.CREATE_PIN,
|
|
]),
|
|
[RegistrationStage.VERIFY_PIN]: new Set([
|
|
RegistrationStage.CREATE_PIN, // if no reg-lock, can choose to create a new PIN
|
|
RegistrationStage.ACCOUNT_LOCKED,
|
|
]),
|
|
[RegistrationStage.CREATE_PIN]: new Set([
|
|
RegistrationStage.CREATE_PIN_CONFIRM,
|
|
]),
|
|
[RegistrationStage.CREATE_PIN_CONFIRM]: new Set([
|
|
RegistrationStage.CREATE_PIN, // so we can go back
|
|
]),
|
|
[RegistrationStage.ACCOUNT_LOCKED]: new Set([]),
|
|
};
|
|
|
|
// There's no 'complete' stage, just a check we do when we are done
|
|
export const ValidStepsBeforeComplete = new Set([
|
|
RegistrationStage.VERIFY_PIN, // successfully verified PIN
|
|
RegistrationStage.CREATE_PIN, // opted out of creating a PIN
|
|
RegistrationStage.CREATE_PIN_CONFIRM, // new PIN successfully created
|
|
]);
|
|
|
|
export enum FatalErrorType {
|
|
OFFLINE = 'OFFLINE',
|
|
UNEXPECTED = 'UNEXPECTED',
|
|
TIMEOUT = 'TIMEOUT',
|
|
UPDATE_REQUIRED = 'UPDATE_REQUIRED',
|
|
}
|
|
|
|
export enum Direction {
|
|
FORWARD = 1,
|
|
BACKWARD = -1,
|
|
}
|
|
|
|
// May come from PROFILE_ENTRY stage, or from prior account info
|
|
export type ProfileData = {
|
|
firstName: string;
|
|
lastName?: string;
|
|
avatarData?: Uint8Array<ArrayBuffer>;
|
|
phoneNumberDiscoverability: PhoneNumberDiscoverability;
|
|
};
|
|
|
|
export type PhoneNumberStage = {
|
|
// Where the process starts - first, the user needs to enter a phone number.
|
|
// Prerequisites: <none>
|
|
// Behaviors:
|
|
// - if phone number is provided in initial state, it will be prepopulated in the UI
|
|
// - if waitUntil is set, user will need to wait that long until starting process again
|
|
// Completion requirements:
|
|
// - a valid phone number that user has confirmed
|
|
stage: RegistrationStage.PHONE_NUMBER;
|
|
|
|
status:
|
|
| {
|
|
type: 'ready';
|
|
}
|
|
| {
|
|
type: 'waiting';
|
|
readyAt: number;
|
|
}
|
|
| {
|
|
type: 'in-progress';
|
|
};
|
|
|
|
phoneNumber?: string;
|
|
profileData?: ProfileData;
|
|
|
|
// After PHONE_NUMBER:
|
|
// 1. Create a verification session
|
|
// 2. Verify data in session: allowedToRequestCode=false, and requestedInformation includes 'captcha'
|
|
};
|
|
|
|
export type CaptchaStage = {
|
|
// Because Desktop cannot receive push notifications, manual validation is required
|
|
// Prerequisites:
|
|
// - phone number
|
|
// - verification session has started
|
|
// - captcha process has started
|
|
// Behaviors:
|
|
// - a button to allow user to open the browser again, shown after a delay
|
|
// - If type=waiting, UI explaining to the user that they need to wait to try another captcha
|
|
// - If error, tell the user and allow them to try again
|
|
// Completion requirements:
|
|
// - captcha is completed
|
|
stage: RegistrationStage.CAPTCHA;
|
|
verificationSessionId: string;
|
|
|
|
status:
|
|
| {
|
|
type: 'ready';
|
|
}
|
|
| {
|
|
type: 'waiting';
|
|
canDoCaptchaAt: number;
|
|
}
|
|
| {
|
|
type: 'in-progress';
|
|
startedAt: number;
|
|
}
|
|
| {
|
|
type: 'another-needed';
|
|
}
|
|
| {
|
|
type: 'failed';
|
|
};
|
|
|
|
captchaCompleteCount: number;
|
|
|
|
phoneNumber: string;
|
|
profileData?: ProfileData;
|
|
|
|
// After CAPTCHA:
|
|
// 1. Update verification session with the token that resulted from the captcha
|
|
// 2. Verify that allowedToRequestCode=true is returned on the session
|
|
// 3. Attempt to request a SMS code - if that fails, set voiceCallNeeded=true
|
|
};
|
|
|
|
export type RawTimings = {
|
|
nextSmsSecs?: number | undefined;
|
|
nextCallSecs?: number | undefined;
|
|
nextVerificationAttemptSecs?: number | undefined;
|
|
};
|
|
export type Timings = {
|
|
smsCanBeRequestedAt?: number;
|
|
callCanBeRequestedAt?: number;
|
|
codeCanBeSubmittedAt?: number;
|
|
};
|
|
|
|
export type VerificationCodeStage = {
|
|
// We need to verify that the user has access to the target phone number
|
|
// Prerequisites:
|
|
// - phone number
|
|
// - timestamp of when the the request to send the code was made
|
|
// - a boolean telling us whether the server failed in sending the SMS, and a voice call is necessary
|
|
// Behaviors:
|
|
// - if failedToSendSMS=true, immediately pop a dialog saying that we couldn't send sms
|
|
// - after a 1-minute timer, can request to have the code resent, or get a voice call
|
|
// - after too many incorrect PIN attempts, dialog allows user to request code resend
|
|
// - after too many resends and incorrect PIN attempts, process is started over with waitNeeded set
|
|
// Completion requirements:
|
|
// - user has entered the proper code
|
|
stage: RegistrationStage.VERIFICATION_CODE;
|
|
verificationSessionId: string;
|
|
|
|
timings?: Timings;
|
|
|
|
failedToSendSMS: boolean;
|
|
failedToCall: boolean;
|
|
|
|
status:
|
|
| {
|
|
type: 'ready';
|
|
}
|
|
| {
|
|
type: 'requesting-code';
|
|
transport: VerificationTransport;
|
|
}
|
|
| {
|
|
type: 'submitting-code';
|
|
}
|
|
| {
|
|
type: 'failed';
|
|
error:
|
|
| 'cannot-send-code-permanent'
|
|
| 'cannot-send-code-temporary'
|
|
| 'incorrect-code'
|
|
| 'invalid-code';
|
|
};
|
|
|
|
codeSendCount: number;
|
|
failedSubmitCodeCount: number;
|
|
|
|
phoneNumber: string;
|
|
profileData?: ProfileData;
|
|
|
|
// After VERIFICATION_CODE:
|
|
// 1. Submit the verification code on the session
|
|
// 2. Ensure that session has verified=true
|
|
// 3. Create the account, using the verification session id (with skipDeviceTransfer=true)
|
|
// - If we get a 423, we know the account has a reglock, and we need to harvest SVR credentials from that response
|
|
// - If we get a 200, and result has storageCapable=true that means user has data in SVR
|
|
|
|
// If we have no profile information from prior account info, go to PROFILE_ENTRY
|
|
// else if storageCapable=true, go to VERIFY_PIN
|
|
// else go to CREATE_PIN
|
|
};
|
|
|
|
// Fetched after account is created, or from a failed account create due to reglock (code 423)
|
|
type SVRCredentials = {
|
|
username: string;
|
|
password: string;
|
|
};
|
|
|
|
export type LateStageAccountState =
|
|
| {
|
|
created: true;
|
|
hasPin: boolean;
|
|
}
|
|
| {
|
|
created: false;
|
|
phoneNumber: string;
|
|
verificationSessionId: string;
|
|
svrCredentials: SVRCredentials;
|
|
};
|
|
|
|
export type ProfileEntryStage = {
|
|
// If we don't have information from a prior registration, the user sets up a profile
|
|
// Prerequisites:
|
|
// - none; we could easily move this stage around
|
|
// - if we already have profile information, this stage can be skipped
|
|
// Behaviors:
|
|
// - we should save all of the draft avatars the user created but didn't use
|
|
// Completion requirements:
|
|
// - only first name is required
|
|
stage: RegistrationStage.PROFILE_ENTRY;
|
|
|
|
// Prepopulates fields; so user has a chance to modify their details if they want
|
|
profileData?: ProfileData;
|
|
|
|
status:
|
|
| {
|
|
type: 'ready';
|
|
}
|
|
| {
|
|
type: 'in-progress';
|
|
};
|
|
|
|
// If we get a 423, we can't create the user's account until after the right PIN is entered!
|
|
accountState: LateStageAccountState;
|
|
|
|
// After PROFILE_ENTRY:
|
|
// 1. if accountState.created=true, can upload profile here since account is created
|
|
// 2. if accountState.created=false or accountState.hasPin=true, go to VERIFY_PIN. Otherwise, CREATE_PIN.
|
|
};
|
|
|
|
export type VerifyPINStage = {
|
|
// Server has told us that this account has a PIN, the user needs to enter the right one!
|
|
// Prerequisites:
|
|
// - hasPIN=true or account not created yet
|
|
// - full profile information is ready to be uploaded
|
|
// Behaviors:
|
|
// - if there are too many incorrect attempts show red text, then dialog
|
|
// - if account does not have reglock, and too many attempts, can reset state via CREATE_PIN
|
|
// - if account has reglock and too many attempts, go to ACCOUNT_LOCKED stage
|
|
// Completion requirements:
|
|
// - correct PIN was entered
|
|
stage: RegistrationStage.VERIFY_PIN;
|
|
|
|
dataForReglockAccountCreate?: DataForReglockAccountCreate;
|
|
|
|
triesRemaining?: number;
|
|
|
|
status:
|
|
| {
|
|
type: 'ready';
|
|
}
|
|
| {
|
|
type: 'in-progress';
|
|
}
|
|
| { type: 'failed'; error: 'nothing-in-svr' };
|
|
|
|
// After VERIFY_PIN:
|
|
// - save PIN in local storage
|
|
// - if accountState.created=false, fetch data from SVR, create account with proper reglock data, upload profile info, then <complete>
|
|
// - otherwise fetch data from SVR and initialize storage service with it, then <complete>
|
|
};
|
|
|
|
export type CreatePINStage = {
|
|
// If server tells us there was no PIN for this user, we will have them create a new one
|
|
// Prerequisites:
|
|
// - profile has been updated
|
|
// - account is already created, and hasPIN=false
|
|
// Behaviors:
|
|
// - an initial entry screen
|
|
// Completion requirements:
|
|
// - a valid PIN has been entered
|
|
stage: RegistrationStage.CREATE_PIN;
|
|
|
|
// Go to CREATE_PIN_CONFIRM
|
|
};
|
|
|
|
export type CreatePINConfirmStage = {
|
|
// If server tells us there was no PIN for this user, we will have them create a new one
|
|
// Prerequisites:
|
|
// - profile has been updated
|
|
// - account is already created, and hasPIN=false
|
|
// - initial new PIN has been created
|
|
// Behaviors:
|
|
// - the second part, a confirmation screen
|
|
// - once this is done, the registration process is complete!
|
|
// Completion requirements:
|
|
// - a valid PIN has been entered which matches the previous PIN
|
|
stage: RegistrationStage.CREATE_PIN_CONFIRM;
|
|
|
|
pin: string;
|
|
|
|
status:
|
|
| {
|
|
type: 'ready';
|
|
}
|
|
| {
|
|
type: 'in-progress';
|
|
};
|
|
|
|
// After CREATE_PIN_CONFIRM
|
|
// 1. initialize storage service, generating keys
|
|
// 3. Save PIN and key in local storage
|
|
// 3. Go to COMPLETE
|
|
};
|
|
|
|
export type DataForReglockAccountCreate = {
|
|
phoneNumber: string;
|
|
verificationSessionId: string;
|
|
svrCredentials: SVRCredentials;
|
|
profileData: ProfileData;
|
|
};
|
|
|
|
export type AccountLockedStage = {
|
|
// Server has told us that the phone number in question is locked (reglock and no more PIN tries)
|
|
// Prerequisites:
|
|
// - server has told us that this account is locked - earlier stages can let us know about this as well
|
|
// Behaviors:
|
|
// - user can try again with a different phone number
|
|
stage: RegistrationStage.ACCOUNT_LOCKED;
|
|
};
|
|
|
|
export type RegistrationWorkflow =
|
|
| PhoneNumberStage
|
|
| CaptchaStage
|
|
| VerificationCodeStage
|
|
| ProfileEntryStage
|
|
| VerifyPINStage
|
|
| CreatePINStage
|
|
| CreatePINConfirmStage
|
|
| AccountLockedStage;
|