From c7e5b562e2e57443acaf17fc5b339c4a4e01c48e Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:39:54 -0500 Subject: [PATCH] Standalone Registration: Support for registration lock scenarios Co-authored-by: Scott Nonnenberg --- package.json | 2 +- pnpm-lock.yaml | 10 +- pnpm-workspace.yaml | 2 +- ts/CI.preload.ts | 11 + ts/Crypto.node.ts | 6 - .../stages/AccountLocked.dom.tsx | 49 +-- .../stages/CreatePIN.dom.tsx | 3 +- .../stages/VerifyPIN.dom.tsx | 4 +- .../util/StepComponents.dom.tsx | 2 +- ts/state/ducks/standaloneInstaller.preload.ts | 15 +- .../standalone/registration_test.node.ts | 300 +++++++++++++++++- ts/textsecure/AccountManager.preload.ts | 3 +- ts/textsecure/WebAPI.preload.ts | 22 +- ts/types/StandaloneRegistration.std.ts | 1 + ts/windows/main/start.preload.ts | 19 ++ 15 files changed, 401 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index b73c80a657..229be13226 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "dependencies": { "@indutny/mac-screen-share": "1.2.5", "@indutny/simple-windows-notifications": "2.0.21", - "@signalapp/libsignal-client": "0.97.5", + "@signalapp/libsignal-client": "0.98.0", "@signalapp/mute-state-change": "workspace:*", "@signalapp/ringrtc": "2.70.1", "@signalapp/sqlcipher": "4.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4717695e44..3eb9f691e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ importers: specifier: 2.0.21 version: 2.0.21 '@signalapp/libsignal-client': - specifier: 0.97.5 - version: 0.97.5 + specifier: 0.98.0 + version: 0.98.0 '@signalapp/mute-state-change': specifier: workspace:* version: link:packages/mute-state-change @@ -4472,8 +4472,8 @@ packages: '@signalapp/libsignal-client@0.92.2': resolution: {integrity: sha512-mSYKpw32Rtmm+D1y8NKzNA9wkiuU60gXRGuum6NTGRN9C3NI4R1cb6xE9w7q+6rjR4zAb4qZWb9QUG5QcLr7pg==, tarball: https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.92.2.tgz} - '@signalapp/libsignal-client@0.97.5': - resolution: {integrity: sha512-+Nsg/P2HaF3TDXjL9OlLPeRyQWPQUBIU05dL0B3XBtG89a2QMRBGwcnsJC9Nu8ze3iJ5iQ9XfQ1nvoFzxjzQtA==, tarball: https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.97.5.tgz} + '@signalapp/libsignal-client@0.98.0': + resolution: {integrity: sha512-6uEbzPPUio9frJ1w7AAW9hbTtudEdZV7n/JPz1e5y5qA70dAEZRrh7YXTzBMZoJAFn4qGrFUwpTDGaj7Awkecw==, tarball: https://registry.npmjs.org/@signalapp/libsignal-client/-/libsignal-client-0.98.0.tgz} '@signalapp/minimask@1.0.1': resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==, tarball: https://registry.npmjs.org/@signalapp/minimask/-/minimask-1.0.1.tgz} @@ -15249,7 +15249,7 @@ snapshots: node-gyp-build: 4.8.4 type-fest: 4.26.1 - '@signalapp/libsignal-client@0.97.5': + '@signalapp/libsignal-client@0.98.0': dependencies: node-gyp-build: 4.8.4 type-fest: 4.26.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5c6712a10e..5681f1b1d9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -94,7 +94,7 @@ allowBuilds: '@indutny/simple-windows-notifications@2.0.16': true '@parcel/watcher@2.5.6': true '@signalapp/libsignal-client@0.92.2': true # duplicate package - '@signalapp/libsignal-client@0.97.5': true + '@signalapp/libsignal-client@0.98.0': true '@signalapp/ringrtc@2.69.4': true '@signalapp/sqlcipher@4.0.3': true '@signalapp/windows-ucv@1.0.1': true diff --git a/ts/CI.preload.ts b/ts/CI.preload.ts index 777c41394f..07f582602f 100644 --- a/ts/CI.preload.ts +++ b/ts/CI.preload.ts @@ -18,6 +18,7 @@ import { strictAssert } from './util/assert.std.ts'; import { MessageModel } from './models/messages.preload.ts'; import { itemStorage } from './textsecure/Storage.preload.ts'; import { BackupLevel } from './services/backups/types.std.ts'; +import { fromHex } from './Bytes.std.ts'; import type { IPCResponse as ChallengeResponseType } from './challenge.dom.ts'; import type { MessageAttributesType } from './model-types.d.ts'; @@ -287,6 +288,16 @@ export function getCI({ let svr2RestoreResponse: RestoreResponseType | undefined; function saveSVR2RestoreResponse(response: RestoreResponseType): void { + if (response.success) { + svr2RestoreResponse = { + ...response, + // @ts-expect-error We need to get this data through JSON + data: fromHex(response.data), + }; + + return; + } + svr2RestoreResponse = response; } function getSVR2RestoreResponse(): RestoreResponseType | undefined { diff --git a/ts/Crypto.node.ts b/ts/Crypto.node.ts index e20f66a8e7..0511e75231 100644 --- a/ts/Crypto.node.ts +++ b/ts/Crypto.node.ts @@ -235,12 +235,6 @@ export function deriveStorageManifestKey( return hmacSha256(storageServiceKey, Bytes.fromString(`Manifest_${version}`)); } -export function deriveRegistrationLockToken( - masterKey: Uint8Array -): Uint8Array { - return hmacSha256(masterKey, Bytes.fromString('Registration Lock')); -} - const STORAGE_SERVICE_ITEM_KEY_INFO_PREFIX = '20240801_SIGNAL_STORAGE_SERVICE_ITEM_'; const STORAGE_SERVICE_ITEM_KEY_LEN = 32; diff --git a/ts/components/standaloneRegistration/stages/AccountLocked.dom.tsx b/ts/components/standaloneRegistration/stages/AccountLocked.dom.tsx index 30abd471c2..6bfbc3a673 100644 --- a/ts/components/standaloneRegistration/stages/AccountLocked.dom.tsx +++ b/ts/components/standaloneRegistration/stages/AccountLocked.dom.tsx @@ -4,9 +4,16 @@ import type { JSX } from 'react'; import { MINUTE } from '../../../util/durations/constants.std.ts'; +import { tw } from '../../../axo/tw.dom.tsx'; import { AxoButton } from '../../../axo/AxoButton.dom.tsx'; -import { CONTACT_SUPPORT_URL } from '../../../util/contactSupport.dom.tsx'; import { openLinkInWebBrowser } from '../../../util/openLinkInWebBrowser.dom.ts'; +import { CONTACT_SUPPORT_URL } from '../../../util/contactSupport.dom.tsx'; +import { + Container, + Description, + Spacer, + Title, +} from '../util/StepComponents.dom.tsx'; import type { LocalizerType } from '../../../types/I18N.std.ts'; import type { ActionCreator } from '../../../state/types.std.ts'; @@ -20,28 +27,17 @@ export function AccountLockedScreen({ i18n: LocalizerType; }): JSX.Element { return ( - <> -
-
- {i18n('icu:StandaloneRegistration--AccountLocked--header')} -
-
- {i18n('icu:StandaloneRegistration--AccountLocked--description')} -
-
-
- { - openLinkInWebBrowser(CONTACT_SUPPORT_URL); - }} - > - {i18n('icu:StandaloneRegistration--AccountLocked--help-button')} - + + + + <Description className={tw('w-125')}> + {i18n('icu:StandaloneRegistration--AccountLocked--description')} + </Description> + <Spacer className={tw('h-40 grow')} /> + <div className={tw('flex w-64 flex-col gap-2')}> <AxoButton.Root variant="strong-primary" - size="md" + size="lg" onClick={() => startRegistration({ waitUntil: Date.now() + MINUTE * 5, @@ -51,7 +47,16 @@ export function AccountLockedScreen({ > {i18n('icu:StandaloneRegistration--AccountLocked--start-over-button')} </AxoButton.Root> + <AxoButton.Root + variant="strong-secondary" + size="lg" + onClick={() => { + openLinkInWebBrowser(CONTACT_SUPPORT_URL); + }} + > + {i18n('icu:StandaloneRegistration--AccountLocked--help-button')} + </AxoButton.Root> </div> - </> + </Container> ); } diff --git a/ts/components/standaloneRegistration/stages/CreatePIN.dom.tsx b/ts/components/standaloneRegistration/stages/CreatePIN.dom.tsx index 3939fdcea1..c7c1ddd840 100644 --- a/ts/components/standaloneRegistration/stages/CreatePIN.dom.tsx +++ b/ts/components/standaloneRegistration/stages/CreatePIN.dom.tsx @@ -32,8 +32,7 @@ import type { startConfirmingPIN as doStartConfirmingPIN, } from '../../../state/ducks/standaloneInstaller.preload.ts'; import { openLinkInWebBrowser } from '../../../util/openLinkInWebBrowser.dom.ts'; - -const PIN_LENGTH_MINIMUM = 4; +import { PIN_LENGTH_MINIMUM } from './VerifyPIN.dom.tsx'; export function CreatePINScreen({ completeRegistration, diff --git a/ts/components/standaloneRegistration/stages/VerifyPIN.dom.tsx b/ts/components/standaloneRegistration/stages/VerifyPIN.dom.tsx index e5b8ab20f8..df40c90a11 100644 --- a/ts/components/standaloneRegistration/stages/VerifyPIN.dom.tsx +++ b/ts/components/standaloneRegistration/stages/VerifyPIN.dom.tsx @@ -40,6 +40,8 @@ const SECOND_REGLOCK_TRIES_THRESHOLD = 3; const FIRST_NON_REGLOCK_TRIES_THRESHOLD = 3; const SECOND_NON_REGLOCK_TRIES_THRESHOLD = 1; +export const PIN_LENGTH_MINIMUM = 4; + export function VerifyPINScreen({ verifyPIN, goToCreatePINStage, @@ -73,7 +75,7 @@ export function VerifyPINScreen({ const onChangePIN = useCallback( (value: string) => { inputRef.current?.setCustomValidity(''); - setIsValidPIN(value.length === 6); + setIsValidPIN(value.length >= PIN_LENGTH_MINIMUM); setPIN(value); }, [setIsValidPIN, setPIN] diff --git a/ts/components/standaloneRegistration/util/StepComponents.dom.tsx b/ts/components/standaloneRegistration/util/StepComponents.dom.tsx index 79f0adb254..20c8c791f8 100644 --- a/ts/components/standaloneRegistration/util/StepComponents.dom.tsx +++ b/ts/components/standaloneRegistration/util/StepComponents.dom.tsx @@ -22,7 +22,7 @@ export function Container({ return ( <div className={classNames( - tw('flex h-99.5 min-h-0 grow flex-col items-center text-center'), + tw('flex h-88 min-h-0 grow flex-col items-center text-center'), className )} > diff --git a/ts/state/ducks/standaloneInstaller.preload.ts b/ts/state/ducks/standaloneInstaller.preload.ts index 1e3ce0b6d3..915f8142ce 100644 --- a/ts/state/ducks/standaloneInstaller.preload.ts +++ b/ts/state/ducks/standaloneInstaller.preload.ts @@ -3,6 +3,7 @@ import { isNumber } from 'lodash'; import { unicodeNumber } from 'unicode-number'; +import { SvrKey } from '@signalapp/libsignal-client/dist/AccountKeys'; import type { ReadonlyDeep } from 'type-fest'; import type { ThunkAction } from 'redux-thunk'; @@ -46,9 +47,9 @@ import { openInbox } from './app.preload.ts'; import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability.std.ts'; import { itemStorage } from '../../textsecure/Storage.preload.ts'; import { updateWithNewKey } from '../../services/storage.preload.ts'; -import { deriveRegistrationLockToken } from '../../Crypto.node.ts'; import { assertDev } from '../../util/assert.std.ts'; import { FatalErrorType } from '../../types/StandaloneRegistration.std.ts'; +import { getSegmenter } from '../../util/grapheme.std.ts'; import type { AccountLockedStage, @@ -69,7 +70,6 @@ import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.s import type { StateType } from '../reducer.preload.ts'; import type { OpenInboxActionType } from './app.preload.ts'; import type { RestoreResponseType } from '../../textsecure/WebAPI.preload.ts'; -import { getSegmenter } from '../../util/grapheme.std.ts'; const log = createLogger('ducks/standaloneInstaller'); @@ -634,7 +634,10 @@ export function submitVerificationCode({ error instanceof LibSignalErrorBase && error.is(ErrorCode.RegistrationLock) ) { - if (error.timeRemainingSeconds > 0) { + if (!error.svr2Username || !error.svr2Password) { + log.error( + `${logId}: SVR credentials not returned with 423; cannot proceed` + ); const newWorkflow: AccountLockedStage = { stage: RegistrationStage.ACCOUNT_LOCKED, }; @@ -864,8 +867,9 @@ export function verifyPIN({ const { phoneNumber, verificationSessionId, profileData } = dataForReglockAccountCreate; - const regLockData = deriveRegistrationLockToken(masterKey); - const registrationLockToken = toHex(regLockData); + const svrKey = new SvrKey(masterKey); + const registrationLockData = svrKey.deriveRegistrationLock(); + const registrationLockToken = toHex(registrationLockData); try { await accountManager.registerAsPrimaryDevice({ @@ -890,6 +894,7 @@ export function verifyPIN({ fatalError: analyzeError(error), }, }); + return; } try { diff --git a/ts/test-mock/standalone/registration_test.node.ts b/ts/test-mock/standalone/registration_test.node.ts index 76ac6bd47b..cc3fc8dd60 100644 --- a/ts/test-mock/standalone/registration_test.node.ts +++ b/ts/test-mock/standalone/registration_test.node.ts @@ -6,10 +6,11 @@ import { expect } from 'playwright/test'; import type { App } from '../playwright.node.ts'; import { Bootstrap } from '../bootstrap.node.ts'; -import { MINUTE } from '../../util/durations/index.std.ts'; +import { DAY, MINUTE } from '../../util/durations/index.std.ts'; import { typeIntoInput } from '../helpers.node.ts'; import { assert } from 'chai'; import { randomBytes } from 'node:crypto'; +import { toHex } from '../../Bytes.std.ts'; export const debug = createDebug('mock:test:registration'); @@ -266,4 +267,301 @@ describe('registration', function (this: Mocha.Suite) { await expect(window.getByText('Welcome to Signal')).toBeVisible(); } }); + + it('should reregister account with reglock enabled, verifying PIN with SVR2', async () => { + const window = await app.getWindow(); + const { server } = bootstrap; + + { + debug('PHONE_NUMBER: Enter phone number'); + const phoneInput = window.getByPlaceholder('Phone number'); + await typeIntoInput(phoneInput, '+14155551111', ''); + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('PHONE_NUMBER: Confirm phone number'); + const dialogText = window.getByText( + 'Is your phone number above correct?' + ); + await expect(dialogText).toBeVisible(); + + await window.getByRole('button', { name: 'Yes' }).click(); + } + + { + debug('CAPTCHA: kick off validation'); + await window.getByRole('button', { name: 'Verify in Browser' }).click(); + } + + { + debug('CAPTCHA: complete validation'); + const { seq, reason } = await app.waitForChallenge(); + assert.strictEqual(reason, 'standalone registration'); + + await app.solveChallenge({ seq, data: { captcha: 'unused' } }); + } + + { + debug('VERIFICATION_CODE: enter code'); + const CODE = '111111'; + for (let i = 0; i < CODE.length; i += 1) { + const char = CODE[i]; + if (!char) { + continue; + } + + const codeInput = window.getByLabel(`Character ${i + 1} of 6`); + // oxlint-disable-next-line no-await-in-loop + await typeIntoInput(codeInput, char, ''); + } + + // Force server to return error telling us that reglock is active + server.setRegisterResponseError({ + code: 423, + data: { + timeRemaining: 5 * DAY, + svr2Credentials: { username: 'fake423', password: 'fake423' }, + }, + }); + + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('PROFILE_ENTRY: enter first name'); + const firstNameInput = window.getByPlaceholder('First name (required)'); + await typeIntoInput(firstNameInput, 'John', ''); + + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('VERIFY_PIN: enter incorrect PIN'); + + const INCORRECT_PIN = '123456'; + + const phoneInput = window.getByPlaceholder('Enter your PIN'); + await typeIntoInput(phoneInput, INCORRECT_PIN, ''); + + await app.saveSVR2RestoreResponse({ + success: false, + error: 'pin-incorrect', + triesRemaining: 5, + }); + + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('VERIFY_PIN: dismiss dialog'); + await expect( + window.getByText('You have 5 attempts remaining') + ).toBeVisible(); + + await window.getByRole('button', { name: 'OK' }).click(); + } + + { + debug('VERIFY_PIN: enter correct PIN'); + + const CORRECT_PIN = '876543'; + const DATA = randomBytes(32); + + const phoneInput = window.getByPlaceholder('Enter your PIN'); + + await phoneInput.clear(); + await typeIntoInput(phoneInput, CORRECT_PIN, ''); + + server.setRegisterResponseError(undefined); + await app.saveSVR2RestoreResponse({ + success: true, + // @ts-expect-error We need to get this data through JSON + data: toHex(DATA), + triesRemaining: 5, + }); + + // Force server to return storageCapable: true + server.setRegisterResponseData({ storageCapable: true }); + + server.setRegisterResponseError(undefined); + + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('COMPLETE: verify welcome screen'); + await expect(window.getByText('Welcome to Signal')).toBeVisible(); + } + }); + + it('should show account locked screen on failed PIN with reglock', async () => { + const window = await app.getWindow(); + const { server } = bootstrap; + + { + debug('PHONE_NUMBER: Enter phone number'); + const phoneInput = window.getByPlaceholder('Phone number'); + await typeIntoInput(phoneInput, '+14155551111', ''); + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('PHONE_NUMBER: Confirm phone number'); + const dialogText = window.getByText( + 'Is your phone number above correct?' + ); + await expect(dialogText).toBeVisible(); + + await window.getByRole('button', { name: 'Yes' }).click(); + } + + { + debug('CAPTCHA: kick off validation'); + await window.getByRole('button', { name: 'Verify in Browser' }).click(); + } + + { + debug('CAPTCHA: complete validation'); + const { seq, reason } = await app.waitForChallenge(); + assert.strictEqual(reason, 'standalone registration'); + + await app.solveChallenge({ seq, data: { captcha: 'unused' } }); + } + + { + debug('VERIFICATION_CODE: enter code'); + const CODE = '111111'; + for (let i = 0; i < CODE.length; i += 1) { + const char = CODE[i]; + if (!char) { + continue; + } + + const codeInput = window.getByLabel(`Character ${i + 1} of 6`); + // oxlint-disable-next-line no-await-in-loop + await typeIntoInput(codeInput, char, ''); + } + + // Force server to return error telling us that reglock is active + server.setRegisterResponseError({ + code: 423, + data: { + timeRemaining: 5 * DAY, + svr2Credentials: { username: 'fake423', password: 'fake423' }, + }, + }); + + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('PROFILE_ENTRY: enter first name'); + const firstNameInput = window.getByPlaceholder('First name (required)'); + await typeIntoInput(firstNameInput, 'John', ''); + + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('VERIFY_PIN: enter incorrect PIN #1'); + + const INCORRECT_PIN = '123456'; + + const phoneInput = window.getByPlaceholder('Enter your PIN'); + await typeIntoInput(phoneInput, INCORRECT_PIN, ''); + + await app.saveSVR2RestoreResponse({ + success: false, + error: 'pin-incorrect', + triesRemaining: 3, + }); + + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('VERIFY_PIN: dismiss dialog'); + await expect( + window.getByText('You have 3 attempts remaining') + ).toBeVisible(); + + await window.getByRole('button', { name: 'OK' }).click(); + } + + { + debug('VERIFY_PIN: enter incorrect PIN #2'); + + const INCORRECT_PIN = '223456'; + + const phoneInput = window.getByPlaceholder('Enter your PIN'); + await phoneInput.clear(); + await typeIntoInput(phoneInput, INCORRECT_PIN, ''); + + await app.saveSVR2RestoreResponse({ + success: false, + error: 'pin-incorrect', + triesRemaining: 2, + }); + + await window.getByRole('button', { name: 'Continue' }).click(); + + await expect(window.getByText('2 attempts remaining')).toBeVisible(); + } + + { + debug('VERIFY_PIN: enter incorrect PIN #3'); + + const INCORRECT_PIN = '334567'; + + const phoneInput = window.getByPlaceholder('Enter your PIN'); + await phoneInput.clear(); + await typeIntoInput(phoneInput, INCORRECT_PIN, ''); + + await app.saveSVR2RestoreResponse({ + success: false, + error: 'pin-incorrect', + triesRemaining: 1, + }); + + await window.getByRole('button', { name: 'Continue' }).click(); + + await expect(window.getByText('1 attempt remaining')).toBeVisible(); + } + + { + debug('VERIFY_PIN: enter incorrect PIN #4'); + + const INCORRECT_PIN = '434567'; + + const phoneInput = window.getByPlaceholder('Enter your PIN'); + await phoneInput.clear(); + await typeIntoInput(phoneInput, INCORRECT_PIN, ''); + + await app.saveSVR2RestoreResponse({ + success: false, + error: 'pin-incorrect', + triesRemaining: 0, + }); + + await window.getByRole('button', { name: 'Continue' }).click(); + } + + { + debug('ACCOUNT_LOCKED: verify text'); + await expect( + window.getByText('Your account has been locked') + ).toBeVisible(); + + await window + .getByRole('button', { name: 'Use a different number' }) + .click(); + } + + { + debug('PHONE_NUMBER: verify wait time'); + await expect(window.getByText('Enter your phone number')).toBeVisible(); + await expect(window.getByText('Please try again in')).toBeVisible(); + } + }); }); diff --git a/ts/textsecure/AccountManager.preload.ts b/ts/textsecure/AccountManager.preload.ts index b5760a1dfd..7d730401dc 100644 --- a/ts/textsecure/AccountManager.preload.ts +++ b/ts/textsecure/AccountManager.preload.ts @@ -104,7 +104,6 @@ type StorageKeyByServiceIdKind = Record<ServiceIdKind, keyof StorageAccessType>; const DAY = 24 * 60 * 60 * 1000; const PROFILE_KEY_LENGTH = 32; -const MASTER_KEY_LENGTH = 32; const KEY_TOO_OLD_THRESHOLD = 14 * DAY; export const KYBER_KEY_ID_KEY = { @@ -431,8 +430,8 @@ export default class AccountManager extends EventTarget { const pniKeyPair = generateKeyPair(); const profileKey = getRandomBytes(PROFILE_KEY_LENGTH); const accessKey = deriveAccessKeyFromProfileKey(profileKey); - const masterKey = getRandomBytes(MASTER_KEY_LENGTH); const accountEntropyPool = AccountEntropyPool.generate(); + const masterKey = AccountEntropyPool.deriveSvrKey(accountEntropyPool); const mediaRootBackupKey = BackupKey.generateRandom().serialize(); const result = await this.#createAccount({ diff --git a/ts/textsecure/WebAPI.preload.ts b/ts/textsecure/WebAPI.preload.ts index 192036a487..a739b048a9 100644 --- a/ts/textsecure/WebAPI.preload.ts +++ b/ts/textsecure/WebAPI.preload.ts @@ -815,7 +815,7 @@ const CHAT_CALLS = { callLinkCreateAuth: 'v1/call-link/create-auth', callQualitySurvey: 'v1/call_quality_survey', redeemReceipt: 'v1/donation/redeem-receipt', - registration: 'v1/registration', + registrationLock: 'v1/accounts/registration_lock', registerCapabilities: 'v1/devices/capabilities', reportMessage: 'v1/messages/report', setBackupId: 'v1/archives/backupid', @@ -3091,6 +3091,26 @@ export async function unlink(): Promise<void> { }); } +export async function setupRegistrationLock( + registrationLock: string +): Promise<void> { + await _ajax({ + host: 'chatService', + call: 'registrationLock', + httpType: 'PUT', + jsonData: { + registrationLock, + }, + }); +} +export async function disableRegistrationLock(): Promise<void> { + await _ajax({ + host: 'chatService', + call: 'registrationLock', + httpType: 'DELETE', + }); +} + export async function getDevices(): Promise<GetDevicesResultType> { return _ajax({ host: 'chatService', diff --git a/ts/types/StandaloneRegistration.std.ts b/ts/types/StandaloneRegistration.std.ts index 598b09579a..1453c3f01a 100644 --- a/ts/types/StandaloneRegistration.std.ts +++ b/ts/types/StandaloneRegistration.std.ts @@ -50,6 +50,7 @@ export const ValidNextStages: Record< [RegistrationStage.CAPTCHA]: new Set([RegistrationStage.VERIFICATION_CODE]), [RegistrationStage.VERIFICATION_CODE]: new Set([ RegistrationStage.PROFILE_ENTRY, + RegistrationStage.ACCOUNT_LOCKED, // if 423 from account create contains no credentials ]), [RegistrationStage.PROFILE_ENTRY]: new Set([ RegistrationStage.VERIFY_PIN, diff --git a/ts/windows/main/start.preload.ts b/ts/windows/main/start.preload.ts index 4f5ce4269c..3cfe231513 100644 --- a/ts/windows/main/start.preload.ts +++ b/ts/windows/main/start.preload.ts @@ -24,8 +24,10 @@ import type { import { cdsLookup, deleteFromSVR2, + disableRegistrationLock, getSocketStatus, restoreFromSVR2, + setupRegistrationLock, storeWithSVR2, } from '../../textsecure/WebAPI.preload.ts'; import type { FeatureFlagType } from '../../window.d.ts'; @@ -37,6 +39,7 @@ import { benchmarkConversationOpen } from '../../CI/benchmarkConversationOpen.pr import { itemStorage } from '../../textsecure/Storage.preload.ts'; import { getSelectedConversationId } from '../../state/selectors/nav.std.ts'; import * as Bytes from '../../Bytes.std.ts'; +import { SvrKey } from '@signalapp/libsignal-client/dist/AccountKeys'; const log = createLogger('start'); @@ -66,6 +69,22 @@ if ( const testKey = 'p10bLPYMs6SjewuhrdWUK2hoqR0Jc/+56GuA/+VBZRg='; const SignalDebug = { + async setupRegistrationLock() { + const masterKey = itemStorage.get('masterKey'); + if (!masterKey) { + throw new Error('missing masterKey!'); + } + + const svrKey = new SvrKey(Bytes.fromBase64(masterKey)); + + const registrationLock = svrKey.deriveRegistrationLock(); + const registrationLockString = Bytes.toHex(registrationLock); + + await setupRegistrationLock(registrationLockString); + }, + async disableRegistrationLock() { + await disableRegistrationLock(); + }, restoreFromSVR2: async (pin: string, expectedKey = testKey) => { const result = await restoreFromSVR2({ pin });