mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-08-14 17:02:37 +01:00
Standalone Registration: Support for registration lock scenarios
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
co-authored by
Scott Nonnenberg
parent
61f462e4e5
commit
c7e5b562e2
+1
-1
@@ -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",
|
||||
|
||||
Generated
+5
-5
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -235,12 +235,6 @@ export function deriveStorageManifestKey(
|
||||
return hmacSha256(storageServiceKey, Bytes.fromString(`Manifest_${version}`));
|
||||
}
|
||||
|
||||
export function deriveRegistrationLockToken(
|
||||
masterKey: Uint8Array<ArrayBuffer>
|
||||
): Uint8Array<ArrayBuffer> {
|
||||
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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="step-body">
|
||||
<div className="header">
|
||||
{i18n('icu:StandaloneRegistration--AccountLocked--header')}
|
||||
</div>
|
||||
<div>
|
||||
{i18n('icu:StandaloneRegistration--AccountLocked--description')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="nav">
|
||||
<AxoButton.Root
|
||||
variant="strong-secondary"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
openLinkInWebBrowser(CONTACT_SUPPORT_URL);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--AccountLocked--help-button')}
|
||||
</AxoButton.Root>
|
||||
<Container>
|
||||
<Spacer className={tw('h-23')} />
|
||||
<Title text={i18n('icu:StandaloneRegistration--AccountLocked--header')} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user