Atomic linking
This commit is contained in:
parent
cbd16b90bb
commit
ccb5eb0dd2
11 changed files with 735 additions and 383 deletions
|
@ -196,7 +196,7 @@
|
||||||
"@electron/fuses": "1.5.0",
|
"@electron/fuses": "1.5.0",
|
||||||
"@formatjs/intl": "2.6.7",
|
"@formatjs/intl": "2.6.7",
|
||||||
"@mixer/parallel-prettier": "2.0.3",
|
"@mixer/parallel-prettier": "2.0.3",
|
||||||
"@signalapp/mock-server": "4.0.1",
|
"@signalapp/mock-server": "4.1.0",
|
||||||
"@storybook/addon-a11y": "6.5.6",
|
"@storybook/addon-a11y": "6.5.6",
|
||||||
"@storybook/addon-actions": "6.5.6",
|
"@storybook/addon-actions": "6.5.6",
|
||||||
"@storybook/addon-controls": "6.5.6",
|
"@storybook/addon-controls": "6.5.6",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { MenuOptionsType, MenuActionType } from '../types/menu';
|
||||||
import type { AnyToast } from '../types/Toast';
|
import type { AnyToast } from '../types/Toast';
|
||||||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import type { VerificationTransport } from '../types/VerificationTransport';
|
||||||
import { ThemeType } from '../types/Util';
|
import { ThemeType } from '../types/Util';
|
||||||
import { AppViewType } from '../state/ducks/app';
|
import { AppViewType } from '../state/ducks/app';
|
||||||
import { SmartInstallScreen } from '../state/smart/InstallScreen';
|
import { SmartInstallScreen } from '../state/smart/InstallScreen';
|
||||||
|
@ -22,7 +23,11 @@ import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
appView: AppViewType;
|
appView: AppViewType;
|
||||||
openInbox: () => void;
|
openInbox: () => void;
|
||||||
registerSingleDevice: (number: string, code: string) => Promise<void>;
|
registerSingleDevice: (
|
||||||
|
number: string,
|
||||||
|
code: string,
|
||||||
|
sessionId: string
|
||||||
|
) => Promise<void>;
|
||||||
renderCallManager: () => JSX.Element;
|
renderCallManager: () => JSX.Element;
|
||||||
renderGlobalModalContainer: () => JSX.Element;
|
renderGlobalModalContainer: () => JSX.Element;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -30,10 +35,10 @@ type PropsType = {
|
||||||
renderStoryViewer: (closeView: () => unknown) => JSX.Element;
|
renderStoryViewer: (closeView: () => unknown) => JSX.Element;
|
||||||
renderLightbox: () => JSX.Element | null;
|
renderLightbox: () => JSX.Element | null;
|
||||||
requestVerification: (
|
requestVerification: (
|
||||||
type: 'sms' | 'voice',
|
|
||||||
number: string,
|
number: string,
|
||||||
token: string
|
captcha: string,
|
||||||
) => Promise<void>;
|
transport: VerificationTransport
|
||||||
|
) => Promise<{ sessionId: string }>;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
isMaximized: boolean;
|
isMaximized: boolean;
|
||||||
isFullScreen: boolean;
|
isFullScreen: boolean;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { strictAssert } from '../util/assert';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { parseNumber } from '../util/libphonenumberUtil';
|
import { parseNumber } from '../util/libphonenumberUtil';
|
||||||
import { getChallengeURL } from '../challenge';
|
import { getChallengeURL } from '../challenge';
|
||||||
|
import { VerificationTransport } from '../types/VerificationTransport';
|
||||||
|
|
||||||
function PhoneInput({
|
function PhoneInput({
|
||||||
onValidation,
|
onValidation,
|
||||||
|
@ -100,11 +101,15 @@ export function StandaloneRegistration({
|
||||||
}: {
|
}: {
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
requestVerification: (
|
requestVerification: (
|
||||||
type: 'sms' | 'voice',
|
|
||||||
number: string,
|
number: string,
|
||||||
token: string
|
captcha: string,
|
||||||
|
transport: VerificationTransport
|
||||||
|
) => Promise<{ sessionId: string }>;
|
||||||
|
registerSingleDevice: (
|
||||||
|
number: string,
|
||||||
|
code: string,
|
||||||
|
sessionId: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
registerSingleDevice: (number: string, code: string) => Promise<void>;
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.IPC.readyForUpdates();
|
window.IPC.readyForUpdates();
|
||||||
|
@ -115,10 +120,11 @@ export function StandaloneRegistration({
|
||||||
const [number, setNumber] = useState<string | undefined>(undefined);
|
const [number, setNumber] = useState<string | undefined>(undefined);
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||||
const [status, setStatus] = useState<string | undefined>(undefined);
|
const [status, setStatus] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const onRequestCode = useCallback(
|
const onRequestCode = useCallback(
|
||||||
async (type: 'sms' | 'voice') => {
|
async (transport: VerificationTransport) => {
|
||||||
if (!isValidNumber) {
|
if (!isValidNumber) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -141,7 +147,8 @@ export function StandaloneRegistration({
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
void requestVerification(type, number, token);
|
const result = await requestVerification(number, token, transport);
|
||||||
|
setSessionId(result.sessionId);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
@ -155,7 +162,7 @@ export function StandaloneRegistration({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
void onRequestCode('sms');
|
void onRequestCode(VerificationTransport.SMS);
|
||||||
},
|
},
|
||||||
[onRequestCode]
|
[onRequestCode]
|
||||||
);
|
);
|
||||||
|
@ -165,7 +172,7 @@ export function StandaloneRegistration({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
void onRequestCode('voice');
|
void onRequestCode(VerificationTransport.Voice);
|
||||||
},
|
},
|
||||||
[onRequestCode]
|
[onRequestCode]
|
||||||
);
|
);
|
||||||
|
@ -185,14 +192,14 @@ export function StandaloneRegistration({
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (!isValidNumber || !isValidCode) {
|
if (!isValidNumber || !isValidCode || !sessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
strictAssert(number != null && code.length > 0, 'Missing number or code');
|
strictAssert(number != null && code.length > 0, 'Missing number or code');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await registerSingleDevice(number, code);
|
await registerSingleDevice(number, code, sessionId);
|
||||||
onComplete();
|
onComplete();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus(err.message);
|
setStatus(err.message);
|
||||||
|
@ -203,6 +210,7 @@ export function StandaloneRegistration({
|
||||||
onComplete,
|
onComplete,
|
||||||
number,
|
number,
|
||||||
code,
|
code,
|
||||||
|
sessionId,
|
||||||
setStatus,
|
setStatus,
|
||||||
isValidNumber,
|
isValidNumber,
|
||||||
isValidCode,
|
isValidCode,
|
||||||
|
|
|
@ -6,8 +6,10 @@ import { connect } from 'react-redux';
|
||||||
import type { MenuItemConstructorOptions } from 'electron';
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
|
|
||||||
import type { MenuActionType } from '../../types/menu';
|
import type { MenuActionType } from '../../types/menu';
|
||||||
|
import type { VerificationTransport } from '../../types/VerificationTransport';
|
||||||
import { App } from '../../components/App';
|
import { App } from '../../components/App';
|
||||||
import OS from '../../util/os/osMain';
|
import OS from '../../util/os/osMain';
|
||||||
|
import { strictAssert } from '../../util/assert';
|
||||||
import { SmartCallManager } from './CallManager';
|
import { SmartCallManager } from './CallManager';
|
||||||
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
||||||
import { SmartLightbox } from './Lightbox';
|
import { SmartLightbox } from './Lightbox';
|
||||||
|
@ -61,20 +63,23 @@ const mapStateToProps = (state: StateType) => {
|
||||||
),
|
),
|
||||||
renderInbox,
|
renderInbox,
|
||||||
requestVerification: (
|
requestVerification: (
|
||||||
type: 'sms' | 'voice',
|
|
||||||
number: string,
|
number: string,
|
||||||
token: string
|
captcha: string,
|
||||||
): Promise<void> => {
|
transport: VerificationTransport
|
||||||
const accountManager = window.getAccountManager();
|
): Promise<{ sessionId: string }> => {
|
||||||
|
const { server } = window.textsecure;
|
||||||
|
strictAssert(server !== undefined, 'WebAPI not available');
|
||||||
|
|
||||||
if (type === 'sms') {
|
return server.requestVerification(number, captcha, transport);
|
||||||
return accountManager.requestSMSVerification(number, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return accountManager.requestVoiceVerification(number, token);
|
|
||||||
},
|
},
|
||||||
registerSingleDevice: (number: string, code: string): Promise<void> => {
|
registerSingleDevice: (
|
||||||
return window.getAccountManager().registerSingleDevice(number, code);
|
number: string,
|
||||||
|
code: string,
|
||||||
|
sessionId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
return window
|
||||||
|
.getAccountManager()
|
||||||
|
.registerSingleDevice(number, code, sessionId);
|
||||||
},
|
},
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ describe('AccountManager', () => {
|
||||||
|
|
||||||
it('handles falsey deviceName', () => {
|
it('handles falsey deviceName', () => {
|
||||||
const encrypted = accountManager.encryptDeviceName('', identityKey);
|
const encrypted = accountManager.encryptDeviceName('', identityKey);
|
||||||
assert.strictEqual(encrypted, null);
|
assert.strictEqual(encrypted, undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { constantTimeEqual } from '../../Crypto';
|
||||||
import { generateKeyPair } from '../../Curve';
|
import { generateKeyPair } from '../../Curve';
|
||||||
import type { UploadKeysType } from '../../textsecure/WebAPI';
|
import type { UploadKeysType } from '../../textsecure/WebAPI';
|
||||||
import AccountManager from '../../textsecure/AccountManager';
|
import AccountManager from '../../textsecure/AccountManager';
|
||||||
import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d';
|
import type { PreKeyType } from '../../textsecure/Types.d';
|
||||||
import { ServiceIdKind, normalizeAci } from '../../types/ServiceId';
|
import { ServiceIdKind, normalizeAci } from '../../types/ServiceId';
|
||||||
|
|
||||||
const { textsecure } = window;
|
const { textsecure } = window;
|
||||||
|
@ -43,15 +43,6 @@ describe('Key generation', function thisNeeded() {
|
||||||
assert(key, `kyber pre key ${keyId} not found`);
|
assert(key, `kyber pre key ${keyId} not found`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function itStoresSignedPreKey(keyId: number): void {
|
|
||||||
it(`signed prekey ${keyId} is valid`, async () => {
|
|
||||||
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
|
|
||||||
ourServiceId,
|
|
||||||
keyId
|
|
||||||
);
|
|
||||||
assert(keyPair, `SignedPreKey ${keyId} not found`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateResultPreKey(
|
async function validateResultPreKey(
|
||||||
resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'>
|
resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'>
|
||||||
|
@ -65,24 +56,6 @@ describe('Key generation', function thisNeeded() {
|
||||||
}
|
}
|
||||||
assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize());
|
assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize());
|
||||||
}
|
}
|
||||||
async function validateResultSignedKey(
|
|
||||||
resultSignedKey?: Pick<SignedPreKeyType, 'keyId' | 'publicKey'>
|
|
||||||
) {
|
|
||||||
if (!resultSignedKey) {
|
|
||||||
throw new Error('validateResultSignedKey: No signed prekey provided!');
|
|
||||||
}
|
|
||||||
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
|
|
||||||
ourServiceId,
|
|
||||||
resultSignedKey.keyId
|
|
||||||
);
|
|
||||||
if (!keyPair) {
|
|
||||||
throw new Error(`SignedPreKey ${resultSignedKey.keyId} not found`);
|
|
||||||
}
|
|
||||||
assertEqualBuffers(
|
|
||||||
resultSignedKey.publicKey,
|
|
||||||
keyPair.publicKey().serialize()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await textsecure.storage.protocol.clearPreKeyStore();
|
await textsecure.storage.protocol.clearPreKeyStore();
|
||||||
|
@ -108,17 +81,19 @@ describe('Key generation', function thisNeeded() {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const accountManager = new AccountManager({} as any);
|
const accountManager = new AccountManager({} as any);
|
||||||
result = await accountManager._generateKeys(count, ServiceIdKind.ACI);
|
result = await accountManager._generateSingleUseKeys(
|
||||||
|
ServiceIdKind.ACI,
|
||||||
|
count
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generates the basics', () => {
|
describe('generates the basics', () => {
|
||||||
for (let i = 1; i <= count; i += 1) {
|
for (let i = 1; i <= count; i += 1) {
|
||||||
itStoresPreKey(i);
|
itStoresPreKey(i);
|
||||||
}
|
}
|
||||||
for (let i = 1; i <= count + 1; i += 1) {
|
for (let i = 1; i <= count; i += 1) {
|
||||||
itStoresKyberPreKey(i);
|
itStoresKyberPreKey(i);
|
||||||
}
|
}
|
||||||
itStoresSignedPreKey(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`result contains ${count} preKeys`, () => {
|
it(`result contains ${count} preKeys`, () => {
|
||||||
|
@ -139,28 +114,24 @@ describe('Key generation', function thisNeeded() {
|
||||||
const preKeys = result.preKeys || [];
|
const preKeys = result.preKeys || [];
|
||||||
await Promise.all(preKeys.map(validateResultPreKey));
|
await Promise.all(preKeys.map(validateResultPreKey));
|
||||||
});
|
});
|
||||||
it('returns a signed prekey', () => {
|
|
||||||
assert.strictEqual(result.signedPreKey?.keyId, 1);
|
|
||||||
assert.instanceOf(result.signedPreKey?.signature, Uint8Array);
|
|
||||||
return validateResultSignedKey(result.signedPreKey);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('the second time', () => {
|
describe('the second time', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const accountManager = new AccountManager({} as any);
|
const accountManager = new AccountManager({} as any);
|
||||||
result = await accountManager._generateKeys(count, ServiceIdKind.ACI);
|
result = await accountManager._generateSingleUseKeys(
|
||||||
|
ServiceIdKind.ACI,
|
||||||
|
count
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generates the basics', () => {
|
describe('generates the basics', () => {
|
||||||
for (let i = 1; i <= 2 * count; i += 1) {
|
for (let i = 1; i <= 2 * count; i += 1) {
|
||||||
itStoresPreKey(i);
|
itStoresPreKey(i);
|
||||||
}
|
}
|
||||||
for (let i = 1; i <= 2 * count + 2; i += 1) {
|
for (let i = 1; i <= 2 * count; i += 1) {
|
||||||
itStoresKyberPreKey(i);
|
itStoresKyberPreKey(i);
|
||||||
}
|
}
|
||||||
itStoresSignedPreKey(1);
|
|
||||||
itStoresSignedPreKey(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`result contains ${count} preKeys`, () => {
|
it(`result contains ${count} preKeys`, () => {
|
||||||
|
@ -181,11 +152,6 @@ describe('Key generation', function thisNeeded() {
|
||||||
const preKeys = result.preKeys || [];
|
const preKeys = result.preKeys || [];
|
||||||
await Promise.all(preKeys.map(validateResultPreKey));
|
await Promise.all(preKeys.map(validateResultPreKey));
|
||||||
});
|
});
|
||||||
it('returns a signed prekey', () => {
|
|
||||||
assert.strictEqual(result.signedPreKey?.keyId, 2);
|
|
||||||
assert.instanceOf(result.signedPreKey?.signature, Uint8Array);
|
|
||||||
return validateResultSignedKey(result.signedPreKey);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('the third time, after keys are confirmed', () => {
|
describe('the third time, after keys are confirmed', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
@ -194,7 +160,10 @@ describe('Key generation', function thisNeeded() {
|
||||||
|
|
||||||
await accountManager._confirmKeys(result, ServiceIdKind.ACI);
|
await accountManager._confirmKeys(result, ServiceIdKind.ACI);
|
||||||
|
|
||||||
result = await accountManager._generateKeys(count, ServiceIdKind.ACI);
|
result = await accountManager._generateSingleUseKeys(
|
||||||
|
ServiceIdKind.ACI,
|
||||||
|
count
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generates the basics', () => {
|
describe('generates the basics', () => {
|
||||||
|
@ -202,11 +171,9 @@ describe('Key generation', function thisNeeded() {
|
||||||
itStoresPreKey(i);
|
itStoresPreKey(i);
|
||||||
}
|
}
|
||||||
// Note: no new last resort kyber key generated
|
// Note: no new last resort kyber key generated
|
||||||
for (let i = 1; i <= 3 * count + 2; i += 1) {
|
for (let i = 1; i <= 3 * count; i += 1) {
|
||||||
itStoresKyberPreKey(i);
|
itStoresKyberPreKey(i);
|
||||||
}
|
}
|
||||||
itStoresSignedPreKey(1);
|
|
||||||
itStoresSignedPreKey(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`result contains ${count} preKeys`, () => {
|
it(`result contains ${count} preKeys`, () => {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { isNumber, omit, orderBy } from 'lodash';
|
import { isNumber, omit, orderBy } from 'lodash';
|
||||||
|
import type { KyberPreKeyRecord } from '@signalapp/libsignal-client';
|
||||||
|
|
||||||
import EventTarget from './EventTarget';
|
import EventTarget from './EventTarget';
|
||||||
import type {
|
import type {
|
||||||
|
@ -13,6 +14,7 @@ import type {
|
||||||
WebAPIType,
|
WebAPIType,
|
||||||
} from './WebAPI';
|
} from './WebAPI';
|
||||||
import type {
|
import type {
|
||||||
|
CompatSignedPreKeyType,
|
||||||
CompatPreKeyType,
|
CompatPreKeyType,
|
||||||
KeyPairType,
|
KeyPairType,
|
||||||
KyberPreKeyType,
|
KyberPreKeyType,
|
||||||
|
@ -38,10 +40,11 @@ import {
|
||||||
generatePreKey,
|
generatePreKey,
|
||||||
generateKyberPreKey,
|
generateKyberPreKey,
|
||||||
} from '../Curve';
|
} from '../Curve';
|
||||||
import type { ServiceIdString, PniString } from '../types/ServiceId';
|
import type { ServiceIdString, AciString, PniString } from '../types/ServiceId';
|
||||||
import {
|
import {
|
||||||
ServiceIdKind,
|
ServiceIdKind,
|
||||||
normalizeAci,
|
normalizeAci,
|
||||||
|
normalizePni,
|
||||||
toTaggedPni,
|
toTaggedPni,
|
||||||
isUntaggedPniString,
|
isUntaggedPniString,
|
||||||
} from '../types/ServiceId';
|
} from '../types/ServiceId';
|
||||||
|
@ -51,6 +54,7 @@ import { assertDev, strictAssert } from '../util/assert';
|
||||||
import { getRegionCodeForNumber } from '../util/libphonenumberUtil';
|
import { getRegionCodeForNumber } from '../util/libphonenumberUtil';
|
||||||
import { getProvisioningUrl } from '../util/getProvisioningUrl';
|
import { getProvisioningUrl } from '../util/getProvisioningUrl';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type { StorageAccessType } from '../types/Storage';
|
import type { StorageAccessType } from '../types/Storage';
|
||||||
|
@ -105,18 +109,53 @@ const SIGNED_PRE_KEY_UPDATE_TIME_KEY: StorageKeyByServiceIdKind = {
|
||||||
[ServiceIdKind.PNI]: 'signedKeyUpdateTimePNI',
|
[ServiceIdKind.PNI]: 'signedKeyUpdateTimePNI',
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateAccountOptionsType = Readonly<{
|
enum AccountType {
|
||||||
|
Primary = 'Primary',
|
||||||
|
Linked = 'Linked',
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateAccountSharedOptionsType = Readonly<{
|
||||||
number: string;
|
number: string;
|
||||||
verificationCode: string;
|
verificationCode: string;
|
||||||
aciKeyPair: KeyPairType;
|
aciKeyPair: KeyPairType;
|
||||||
pniKeyPair?: KeyPairType;
|
pniKeyPair: KeyPairType;
|
||||||
profileKey?: Uint8Array;
|
profileKey: Uint8Array;
|
||||||
deviceName?: string;
|
|
||||||
userAgent?: string;
|
|
||||||
readReceipts?: boolean;
|
|
||||||
accessKey?: Uint8Array;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type CreatePrimaryDeviceOptionsType = Readonly<{
|
||||||
|
type: AccountType.Primary;
|
||||||
|
|
||||||
|
deviceName?: undefined;
|
||||||
|
ourAci?: undefined;
|
||||||
|
ourPni?: undefined;
|
||||||
|
userAgent?: undefined;
|
||||||
|
|
||||||
|
readReceipts: true;
|
||||||
|
|
||||||
|
accessKey: Uint8Array;
|
||||||
|
sessionId: string;
|
||||||
|
}> &
|
||||||
|
CreateAccountSharedOptionsType;
|
||||||
|
|
||||||
|
type CreateLinkedDeviceOptionsType = Readonly<{
|
||||||
|
type: AccountType.Linked;
|
||||||
|
|
||||||
|
deviceName: string;
|
||||||
|
ourAci: AciString;
|
||||||
|
ourPni: PniString;
|
||||||
|
userAgent?: string;
|
||||||
|
|
||||||
|
readReceipts: boolean;
|
||||||
|
|
||||||
|
accessKey?: undefined;
|
||||||
|
sessionId?: undefined;
|
||||||
|
}> &
|
||||||
|
CreateAccountSharedOptionsType;
|
||||||
|
|
||||||
|
type CreateAccountOptionsType =
|
||||||
|
| CreatePrimaryDeviceOptionsType
|
||||||
|
| CreateLinkedDeviceOptionsType;
|
||||||
|
|
||||||
function getNextKeyId(
|
function getNextKeyId(
|
||||||
kind: ServiceIdKind,
|
kind: ServiceIdKind,
|
||||||
keys: StorageKeyByServiceIdKind
|
keys: StorageKeyByServiceIdKind
|
||||||
|
@ -135,6 +174,42 @@ function getNextKeyId(
|
||||||
return STARTING_KEY_ID;
|
return STARTING_KEY_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function kyberPreKeyToUploadSignedPreKey(
|
||||||
|
record: KyberPreKeyRecord
|
||||||
|
): UploadSignedPreKeyType {
|
||||||
|
return {
|
||||||
|
keyId: record.id(),
|
||||||
|
publicKey: record.publicKey().serialize(),
|
||||||
|
signature: record.signature(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function kyberPreKeyToStoredSignedPreKey(
|
||||||
|
record: KyberPreKeyRecord,
|
||||||
|
ourServiceId: ServiceIdString
|
||||||
|
): Omit<KyberPreKeyType, 'id'> {
|
||||||
|
return {
|
||||||
|
createdAt: Date.now(),
|
||||||
|
data: record.serialize(),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: true,
|
||||||
|
keyId: record.id(),
|
||||||
|
ourServiceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function signedPreKeyToUploadSignedPreKey({
|
||||||
|
keyId,
|
||||||
|
keyPair,
|
||||||
|
signature,
|
||||||
|
}: CompatSignedPreKeyType): UploadSignedPreKeyType {
|
||||||
|
return {
|
||||||
|
keyId,
|
||||||
|
publicKey: keyPair.pubKey,
|
||||||
|
signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default class AccountManager extends EventTarget {
|
export default class AccountManager extends EventTarget {
|
||||||
pending: Promise<void>;
|
pending: Promise<void>;
|
||||||
|
|
||||||
|
@ -153,17 +228,12 @@ export default class AccountManager extends EventTarget {
|
||||||
return this.pendingQueue.add(taskWithTimeout);
|
return this.pendingQueue.add(taskWithTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestVoiceVerification(number: string, token: string): Promise<void> {
|
encryptDeviceName(
|
||||||
return this.server.requestVerificationVoice(number, token);
|
name: string,
|
||||||
}
|
identityKey: KeyPairType
|
||||||
|
): string | undefined {
|
||||||
async requestSMSVerification(number: string, token: string): Promise<void> {
|
|
||||||
return this.server.requestVerificationSMS(number, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptDeviceName(name: string, identityKey: KeyPairType): string | null {
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
const encrypted = encryptDeviceName(name, identityKey.pubKey);
|
const encrypted = encryptDeviceName(name, identityKey.pubKey);
|
||||||
|
|
||||||
|
@ -224,7 +294,8 @@ export default class AccountManager extends EventTarget {
|
||||||
|
|
||||||
async registerSingleDevice(
|
async registerSingleDevice(
|
||||||
number: string,
|
number: string,
|
||||||
verificationCode: string
|
verificationCode: string,
|
||||||
|
sessionId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.queueTask(async () => {
|
await this.queueTask(async () => {
|
||||||
const aciKeyPair = generateKeyPair();
|
const aciKeyPair = generateKeyPair();
|
||||||
|
@ -235,22 +306,16 @@ export default class AccountManager extends EventTarget {
|
||||||
const registrationBaton = this.server.startRegistration();
|
const registrationBaton = this.server.startRegistration();
|
||||||
try {
|
try {
|
||||||
await this.createAccount({
|
await this.createAccount({
|
||||||
|
type: AccountType.Primary,
|
||||||
number,
|
number,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
|
sessionId,
|
||||||
aciKeyPair,
|
aciKeyPair,
|
||||||
pniKeyPair,
|
pniKeyPair,
|
||||||
profileKey,
|
profileKey,
|
||||||
accessKey,
|
accessKey,
|
||||||
|
readReceipts: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadKeys = async (kind: ServiceIdKind) => {
|
|
||||||
const keys = await this._generateKeys(PRE_KEY_GEN_BATCH_SIZE, kind);
|
|
||||||
await this.server.registerKeys(keys, kind);
|
|
||||||
await this._confirmKeys(keys, kind);
|
|
||||||
};
|
|
||||||
|
|
||||||
await uploadKeys(ServiceIdKind.ACI);
|
|
||||||
await uploadKeys(ServiceIdKind.PNI);
|
|
||||||
} finally {
|
} finally {
|
||||||
this.server.finishRegistration(registrationBaton);
|
this.server.finishRegistration(registrationBaton);
|
||||||
}
|
}
|
||||||
|
@ -332,17 +397,27 @@ export default class AccountManager extends EventTarget {
|
||||||
if (
|
if (
|
||||||
!provisionMessage.number ||
|
!provisionMessage.number ||
|
||||||
!provisionMessage.provisioningCode ||
|
!provisionMessage.provisioningCode ||
|
||||||
!provisionMessage.aciKeyPair
|
!provisionMessage.aciKeyPair ||
|
||||||
|
!provisionMessage.pniKeyPair ||
|
||||||
|
!provisionMessage.profileKey ||
|
||||||
|
!provisionMessage.aci ||
|
||||||
|
!isUntaggedPniString(provisionMessage.pni)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'AccountManager.registerSecondDevice: Provision message was missing key data'
|
'AccountManager.registerSecondDevice: Provision message was missing key data'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const registrationBaton = this.server.startRegistration();
|
const ourAci = normalizeAci(provisionMessage.aci, 'provisionMessage.aci');
|
||||||
|
const ourPni = normalizePni(
|
||||||
|
toTaggedPni(provisionMessage.pni),
|
||||||
|
'provisionMessage.pni'
|
||||||
|
);
|
||||||
|
|
||||||
|
const registrationBaton = this.server.startRegistration();
|
||||||
try {
|
try {
|
||||||
await this.createAccount({
|
await this.createAccount({
|
||||||
|
type: AccountType.Linked,
|
||||||
number: provisionMessage.number,
|
number: provisionMessage.number,
|
||||||
verificationCode: provisionMessage.provisioningCode,
|
verificationCode: provisionMessage.provisioningCode,
|
||||||
aciKeyPair: provisionMessage.aciKeyPair,
|
aciKeyPair: provisionMessage.aciKeyPair,
|
||||||
|
@ -350,32 +425,10 @@ export default class AccountManager extends EventTarget {
|
||||||
profileKey: provisionMessage.profileKey,
|
profileKey: provisionMessage.profileKey,
|
||||||
deviceName,
|
deviceName,
|
||||||
userAgent: provisionMessage.userAgent,
|
userAgent: provisionMessage.userAgent,
|
||||||
readReceipts: provisionMessage.readReceipts,
|
ourAci,
|
||||||
|
ourPni,
|
||||||
|
readReceipts: Boolean(provisionMessage.readReceipts),
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadKeys = async (kind: ServiceIdKind) => {
|
|
||||||
const keys = await this._generateKeys(PRE_KEY_GEN_BATCH_SIZE, kind);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.server.registerKeys(keys, kind);
|
|
||||||
await this._confirmKeys(keys, kind);
|
|
||||||
} catch (error) {
|
|
||||||
if (kind === ServiceIdKind.PNI) {
|
|
||||||
log.error(
|
|
||||||
'Failed to upload PNI prekeys. Moving on',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await uploadKeys(ServiceIdKind.ACI);
|
|
||||||
if (provisionMessage.pniKeyPair) {
|
|
||||||
await uploadKeys(ServiceIdKind.PNI);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
this.server.finishRegistration(registrationBaton);
|
this.server.finishRegistration(registrationBaton);
|
||||||
}
|
}
|
||||||
|
@ -406,8 +459,10 @@ export default class AccountManager extends EventTarget {
|
||||||
|
|
||||||
private async generateNewPreKeys(
|
private async generateNewPreKeys(
|
||||||
serviceIdKind: ServiceIdKind,
|
serviceIdKind: ServiceIdKind,
|
||||||
count: number
|
count = PRE_KEY_GEN_BATCH_SIZE
|
||||||
): Promise<Array<UploadPreKeyType>> {
|
): Promise<Array<UploadPreKeyType>> {
|
||||||
|
const ourServiceId =
|
||||||
|
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
||||||
const logId = `AccountManager.generateNewPreKeys(${serviceIdKind})`;
|
const logId = `AccountManager.generateNewPreKeys(${serviceIdKind})`;
|
||||||
const { storage } = window.textsecure;
|
const { storage } = window.textsecure;
|
||||||
const store = storage.protocol;
|
const store = storage.protocol;
|
||||||
|
@ -415,7 +470,6 @@ export default class AccountManager extends EventTarget {
|
||||||
const startId = getNextKeyId(serviceIdKind, PRE_KEY_ID_KEY);
|
const startId = getNextKeyId(serviceIdKind, PRE_KEY_ID_KEY);
|
||||||
log.info(`${logId}: Generating ${count} new keys starting at ${startId}`);
|
log.info(`${logId}: Generating ${count} new keys starting at ${startId}`);
|
||||||
|
|
||||||
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
|
|
||||||
if (typeof startId !== 'number') {
|
if (typeof startId !== 'number') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${logId}: Invalid ${PRE_KEY_ID_KEY[serviceIdKind]} in storage`
|
`${logId}: Invalid ${PRE_KEY_ID_KEY[serviceIdKind]} in storage`
|
||||||
|
@ -440,7 +494,7 @@ export default class AccountManager extends EventTarget {
|
||||||
|
|
||||||
private async generateNewKyberPreKeys(
|
private async generateNewKyberPreKeys(
|
||||||
serviceIdKind: ServiceIdKind,
|
serviceIdKind: ServiceIdKind,
|
||||||
count: number
|
count = PRE_KEY_GEN_BATCH_SIZE
|
||||||
): Promise<Array<UploadKyberPreKeyType>> {
|
): Promise<Array<UploadKyberPreKeyType>> {
|
||||||
const logId = `AccountManager.generateNewKyberPreKeys(${serviceIdKind})`;
|
const logId = `AccountManager.generateNewKyberPreKeys(${serviceIdKind})`;
|
||||||
const { storage } = window.textsecure;
|
const { storage } = window.textsecure;
|
||||||
|
@ -449,13 +503,13 @@ export default class AccountManager extends EventTarget {
|
||||||
const startId = getNextKeyId(serviceIdKind, KYBER_KEY_ID_KEY);
|
const startId = getNextKeyId(serviceIdKind, KYBER_KEY_ID_KEY);
|
||||||
log.info(`${logId}: Generating ${count} new keys starting at ${startId}`);
|
log.info(`${logId}: Generating ${count} new keys starting at ${startId}`);
|
||||||
|
|
||||||
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
|
|
||||||
if (typeof startId !== 'number') {
|
if (typeof startId !== 'number') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${logId}: Invalid ${KYBER_KEY_ID_KEY[serviceIdKind]} in storage`
|
`${logId}: Invalid ${KYBER_KEY_ID_KEY[serviceIdKind]} in storage`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
|
||||||
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
|
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
|
||||||
|
|
||||||
const toSave: Array<Omit<KyberPreKeyType, 'id'>> = [];
|
const toSave: Array<Omit<KyberPreKeyType, 'id'>> = [];
|
||||||
|
@ -515,10 +569,7 @@ export default class AccountManager extends EventTarget {
|
||||||
log.info(
|
log.info(
|
||||||
`${logId}: Server prekey count is ${preKeyCount}, generating a new set`
|
`${logId}: Server prekey count is ${preKeyCount}, generating a new set`
|
||||||
);
|
);
|
||||||
preKeys = await this.generateNewPreKeys(
|
preKeys = await this.generateNewPreKeys(serviceIdKind);
|
||||||
serviceIdKind,
|
|
||||||
PRE_KEY_GEN_BATCH_SIZE
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let pqPreKeys: Array<UploadKyberPreKeyType> | undefined;
|
let pqPreKeys: Array<UploadKyberPreKeyType> | undefined;
|
||||||
|
@ -526,10 +577,7 @@ export default class AccountManager extends EventTarget {
|
||||||
log.info(
|
log.info(
|
||||||
`${logId}: Server kyber prekey count is ${kyberPreKeyCount}, generating a new set`
|
`${logId}: Server kyber prekey count is ${kyberPreKeyCount}, generating a new set`
|
||||||
);
|
);
|
||||||
pqPreKeys = await this.generateNewKyberPreKeys(
|
pqPreKeys = await this.generateNewKyberPreKeys(serviceIdKind);
|
||||||
serviceIdKind,
|
|
||||||
PRE_KEY_GEN_BATCH_SIZE
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pqLastResortPreKey = await this.maybeUpdateLastResortKyberKey(
|
const pqLastResortPreKey = await this.maybeUpdateLastResortKyberKey(
|
||||||
|
@ -608,14 +656,12 @@ export default class AccountManager extends EventTarget {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async maybeUpdateSignedPreKey(
|
private async generateSignedPreKey(
|
||||||
serviceIdKind: ServiceIdKind
|
serviceIdKind: ServiceIdKind,
|
||||||
): Promise<UploadSignedPreKeyType | undefined> {
|
identityKey: KeyPairType
|
||||||
const logId = `AccountManager.maybeUpdateSignedPreKey(${serviceIdKind})`;
|
): Promise<CompatSignedPreKeyType> {
|
||||||
const store = window.textsecure.storage.protocol;
|
const logId = `AccountManager.generateSignedPreKey(${serviceIdKind})`;
|
||||||
|
|
||||||
const ourServiceId =
|
|
||||||
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
|
||||||
const signedKeyId = getNextKeyId(serviceIdKind, SIGNED_PRE_KEY_ID_KEY);
|
const signedKeyId = getNextKeyId(serviceIdKind, SIGNED_PRE_KEY_ID_KEY);
|
||||||
if (typeof signedKeyId !== 'number') {
|
if (typeof signedKeyId !== 'number') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -623,6 +669,26 @@ export default class AccountManager extends EventTarget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const key = await generateSignedPreKey(identityKey, signedKeyId);
|
||||||
|
log.info(`${logId}: Saving new signed prekey`, key.keyId);
|
||||||
|
|
||||||
|
await window.textsecure.storage.put(
|
||||||
|
SIGNED_PRE_KEY_ID_KEY[serviceIdKind],
|
||||||
|
signedKeyId + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async maybeUpdateSignedPreKey(
|
||||||
|
serviceIdKind: ServiceIdKind
|
||||||
|
): Promise<UploadSignedPreKeyType | undefined> {
|
||||||
|
const ourServiceId =
|
||||||
|
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
||||||
|
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
|
||||||
|
const logId = `AccountManager.maybeUpdateSignedPreKey(${serviceIdKind}, ${ourServiceId})`;
|
||||||
|
const store = window.textsecure.storage.protocol;
|
||||||
|
|
||||||
const keys = await store.loadSignedPreKeys(ourServiceId);
|
const keys = await store.loadSignedPreKeys(ourServiceId);
|
||||||
const sortedKeys = orderBy(keys, ['created_at'], ['desc']);
|
const sortedKeys = orderBy(keys, ['created_at'], ['desc']);
|
||||||
const confirmedKeys = sortedKeys.filter(key => key.confirmed);
|
const confirmedKeys = sortedKeys.filter(key => key.confirmed);
|
||||||
|
@ -647,34 +713,20 @@ export default class AccountManager extends EventTarget {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
|
const key = await this.generateSignedPreKey(serviceIdKind, identityKey);
|
||||||
|
|
||||||
const key = await generateSignedPreKey(identityKey, signedKeyId);
|
|
||||||
log.info(`${logId}: Saving new signed prekey`, key.keyId);
|
log.info(`${logId}: Saving new signed prekey`, key.keyId);
|
||||||
|
|
||||||
await Promise.all([
|
await store.storeSignedPreKey(ourServiceId, key.keyId, key.keyPair);
|
||||||
window.textsecure.storage.put(
|
|
||||||
SIGNED_PRE_KEY_ID_KEY[serviceIdKind],
|
|
||||||
signedKeyId + 1
|
|
||||||
),
|
|
||||||
store.storeSignedPreKey(ourServiceId, key.keyId, key.keyPair),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return signedPreKeyToUploadSignedPreKey(key);
|
||||||
keyId: key.keyId,
|
|
||||||
publicKey: key.keyPair.pubKey,
|
|
||||||
signature: key.signature,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async maybeUpdateLastResortKyberKey(
|
private async generateLastResortKyberKey(
|
||||||
serviceIdKind: ServiceIdKind
|
serviceIdKind: ServiceIdKind,
|
||||||
): Promise<UploadSignedPreKeyType | undefined> {
|
identityKey: KeyPairType
|
||||||
const logId = `maybeUpdateLastResortKyberKey(${serviceIdKind})`;
|
): Promise<KyberPreKeyRecord> {
|
||||||
const store = window.textsecure.storage.protocol;
|
const logId = `generateLastRestortKyberKey(${serviceIdKind})`;
|
||||||
|
|
||||||
const ourServiceId =
|
|
||||||
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
|
||||||
const kyberKeyId = getNextKeyId(serviceIdKind, KYBER_KEY_ID_KEY);
|
const kyberKeyId = getNextKeyId(serviceIdKind, KYBER_KEY_ID_KEY);
|
||||||
if (typeof kyberKeyId !== 'number') {
|
if (typeof kyberKeyId !== 'number') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -682,6 +734,27 @@ export default class AccountManager extends EventTarget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyId = kyberKeyId;
|
||||||
|
const record = await generateKyberPreKey(identityKey, keyId);
|
||||||
|
log.info(`${logId}: Saving new last resort prekey`, keyId);
|
||||||
|
|
||||||
|
await window.textsecure.storage.put(
|
||||||
|
KYBER_KEY_ID_KEY[serviceIdKind],
|
||||||
|
kyberKeyId + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async maybeUpdateLastResortKyberKey(
|
||||||
|
serviceIdKind: ServiceIdKind
|
||||||
|
): Promise<UploadSignedPreKeyType | undefined> {
|
||||||
|
const ourServiceId =
|
||||||
|
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
||||||
|
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
|
||||||
|
const logId = `maybeUpdateLastResortKyberKey(${serviceIdKind}, ${ourServiceId})`;
|
||||||
|
const store = window.textsecure.storage.protocol;
|
||||||
|
|
||||||
const keys = store.loadKyberPreKeys(ourServiceId, { isLastResort: true });
|
const keys = store.loadKyberPreKeys(ourServiceId, { isLastResort: true });
|
||||||
const sortedKeys = orderBy(keys, ['createdAt'], ['desc']);
|
const sortedKeys = orderBy(keys, ['createdAt'], ['desc']);
|
||||||
const confirmedKeys = sortedKeys.filter(key => key.isConfirmed);
|
const confirmedKeys = sortedKeys.filter(key => key.isConfirmed);
|
||||||
|
@ -706,33 +779,16 @@ export default class AccountManager extends EventTarget {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const identityKey = this.getIdentityKeyOrThrow(ourServiceId);
|
const record = await this.generateLastResortKyberKey(
|
||||||
|
serviceIdKind,
|
||||||
|
identityKey
|
||||||
|
);
|
||||||
|
log.info(`${logId}: Saving new last resort prekey`, record.id());
|
||||||
|
const key = kyberPreKeyToStoredSignedPreKey(record, ourServiceId);
|
||||||
|
|
||||||
const keyId = kyberKeyId;
|
await store.storeKyberPreKeys(ourServiceId, [key]);
|
||||||
const record = await generateKyberPreKey(identityKey, keyId);
|
|
||||||
log.info(`${logId}: Saving new last resort prekey`, keyId);
|
|
||||||
const key = {
|
|
||||||
createdAt: Date.now(),
|
|
||||||
data: record.serialize(),
|
|
||||||
isConfirmed: false,
|
|
||||||
isLastResort: true,
|
|
||||||
keyId,
|
|
||||||
ourServiceId,
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all([
|
return kyberPreKeyToUploadSignedPreKey(record);
|
||||||
window.textsecure.storage.put(
|
|
||||||
KYBER_KEY_ID_KEY[serviceIdKind],
|
|
||||||
kyberKeyId + 1
|
|
||||||
),
|
|
||||||
store.storeKyberPreKeys(ourServiceId, [key]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
keyId,
|
|
||||||
publicKey: record.publicKey().serialize(),
|
|
||||||
signature: record.signature(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exposed only for tests
|
// Exposed only for tests
|
||||||
|
@ -846,10 +902,10 @@ export default class AccountManager extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _cleanPreKeys(serviceIdKind: ServiceIdKind): Promise<void> {
|
async _cleanPreKeys(serviceIdKind: ServiceIdKind): Promise<void> {
|
||||||
const ourServiceId =
|
|
||||||
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
|
||||||
const store = window.textsecure.storage.protocol;
|
const store = window.textsecure.storage.protocol;
|
||||||
const logId = `AccountManager.cleanPreKeys(${serviceIdKind})`;
|
const logId = `AccountManager.cleanPreKeys(${serviceIdKind})`;
|
||||||
|
const ourServiceId =
|
||||||
|
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
||||||
|
|
||||||
const preKeys = store.loadPreKeys(ourServiceId);
|
const preKeys = store.loadPreKeys(ourServiceId);
|
||||||
const toDelete: Array<number> = [];
|
const toDelete: Array<number> = [];
|
||||||
|
@ -874,10 +930,10 @@ export default class AccountManager extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _cleanKyberPreKeys(serviceIdKind: ServiceIdKind): Promise<void> {
|
async _cleanKyberPreKeys(serviceIdKind: ServiceIdKind): Promise<void> {
|
||||||
const ourServiceId =
|
|
||||||
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
|
||||||
const store = window.textsecure.storage.protocol;
|
const store = window.textsecure.storage.protocol;
|
||||||
const logId = `AccountManager.cleanKyberPreKeys(${serviceIdKind})`;
|
const logId = `AccountManager.cleanKyberPreKeys(${serviceIdKind})`;
|
||||||
|
const ourServiceId =
|
||||||
|
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
||||||
|
|
||||||
const preKeys = store.loadKyberPreKeys(ourServiceId, {
|
const preKeys = store.loadKyberPreKeys(ourServiceId, {
|
||||||
isLastResort: false,
|
isLastResort: false,
|
||||||
|
@ -903,17 +959,19 @@ export default class AccountManager extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAccount({
|
private async createAccount(
|
||||||
number,
|
options: CreateAccountOptionsType
|
||||||
verificationCode,
|
): Promise<void> {
|
||||||
aciKeyPair,
|
const {
|
||||||
pniKeyPair,
|
number,
|
||||||
profileKey,
|
verificationCode,
|
||||||
deviceName,
|
aciKeyPair,
|
||||||
userAgent,
|
pniKeyPair,
|
||||||
readReceipts,
|
profileKey,
|
||||||
accessKey,
|
readReceipts,
|
||||||
}: CreateAccountOptionsType): Promise<void> {
|
userAgent,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const { storage } = window.textsecure;
|
const { storage } = window.textsecure;
|
||||||
let password = Bytes.toBase64(getRandomBytes(16));
|
let password = Bytes.toBase64(getRandomBytes(16));
|
||||||
password = password.substring(0, password.length - 2);
|
password = password.substring(0, password.length - 2);
|
||||||
|
@ -924,36 +982,20 @@ export default class AccountManager extends EventTarget {
|
||||||
const previousACI = storage.user.getAci();
|
const previousACI = storage.user.getAci();
|
||||||
const previousPNI = storage.user.getPni();
|
const previousPNI = storage.user.getPni();
|
||||||
|
|
||||||
let encryptedDeviceName;
|
|
||||||
if (deviceName) {
|
|
||||||
encryptedDeviceName = this.encryptDeviceName(deviceName, aciKeyPair);
|
|
||||||
await this.deviceNameIsEncrypted();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`createAccount: Number is ${number}, password has length: ${
|
`createAccount: Number is ${number}, password has length: ${
|
||||||
password ? password.length : 'none'
|
password ? password.length : 'none'
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await this.server.confirmCode({
|
let uuidChanged: boolean;
|
||||||
number,
|
if (options.type === AccountType.Primary) {
|
||||||
code: verificationCode,
|
uuidChanged = true;
|
||||||
newPassword: password,
|
} else if (options.type === AccountType.Linked) {
|
||||||
registrationId,
|
uuidChanged = previousACI != null && previousACI !== options.ourAci;
|
||||||
pniRegistrationId,
|
} else {
|
||||||
deviceName: encryptedDeviceName,
|
throw missingCaseError(options);
|
||||||
accessKey,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const ourAci = normalizeAci(response.uuid, 'createAccount');
|
|
||||||
strictAssert(
|
|
||||||
isUntaggedPniString(response.pni),
|
|
||||||
'Response pni must be untagged'
|
|
||||||
);
|
|
||||||
const ourPni = toTaggedPni(response.pni);
|
|
||||||
|
|
||||||
const uuidChanged = previousACI && ourAci && previousACI !== ourAci;
|
|
||||||
|
|
||||||
// We only consider the number changed if we didn't have a UUID before
|
// We only consider the number changed if we didn't have a UUID before
|
||||||
const numberChanged =
|
const numberChanged =
|
||||||
|
@ -1004,6 +1046,96 @@ export default class AccountManager extends EventTarget {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ourAci: AciString;
|
||||||
|
let ourPni: PniString;
|
||||||
|
let deviceId: number;
|
||||||
|
|
||||||
|
const aciPqLastResortPreKey = await this.generateLastResortKyberKey(
|
||||||
|
ServiceIdKind.ACI,
|
||||||
|
aciKeyPair
|
||||||
|
);
|
||||||
|
const pniPqLastResortPreKey = await this.generateLastResortKyberKey(
|
||||||
|
ServiceIdKind.PNI,
|
||||||
|
pniKeyPair
|
||||||
|
);
|
||||||
|
const aciSignedPreKey = await this.generateSignedPreKey(
|
||||||
|
ServiceIdKind.ACI,
|
||||||
|
aciKeyPair
|
||||||
|
);
|
||||||
|
const pniSignedPreKey = await this.generateSignedPreKey(
|
||||||
|
ServiceIdKind.PNI,
|
||||||
|
pniKeyPair
|
||||||
|
);
|
||||||
|
|
||||||
|
const keysToUpload = {
|
||||||
|
aciPqLastResortPreKey: kyberPreKeyToUploadSignedPreKey(
|
||||||
|
aciPqLastResortPreKey
|
||||||
|
),
|
||||||
|
aciSignedPreKey: signedPreKeyToUploadSignedPreKey(aciSignedPreKey),
|
||||||
|
pniPqLastResortPreKey: kyberPreKeyToUploadSignedPreKey(
|
||||||
|
pniPqLastResortPreKey
|
||||||
|
),
|
||||||
|
pniSignedPreKey: signedPreKeyToUploadSignedPreKey(pniSignedPreKey),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.type === AccountType.Primary) {
|
||||||
|
const response = await this.server.createAccount({
|
||||||
|
number,
|
||||||
|
code: verificationCode,
|
||||||
|
newPassword: password,
|
||||||
|
registrationId,
|
||||||
|
pniRegistrationId,
|
||||||
|
accessKey: options.accessKey,
|
||||||
|
sessionId: options.sessionId,
|
||||||
|
aciPublicKey: aciKeyPair.pubKey,
|
||||||
|
pniPublicKey: pniKeyPair.pubKey,
|
||||||
|
...keysToUpload,
|
||||||
|
});
|
||||||
|
|
||||||
|
ourAci = normalizeAci(response.uuid, 'createAccount');
|
||||||
|
strictAssert(
|
||||||
|
isUntaggedPniString(response.pni),
|
||||||
|
'Response pni must be untagged'
|
||||||
|
);
|
||||||
|
ourPni = toTaggedPni(response.pni);
|
||||||
|
deviceId = 1;
|
||||||
|
} else if (options.type === AccountType.Linked) {
|
||||||
|
const encryptedDeviceName = this.encryptDeviceName(
|
||||||
|
options.deviceName,
|
||||||
|
aciKeyPair
|
||||||
|
);
|
||||||
|
await this.deviceNameIsEncrypted();
|
||||||
|
|
||||||
|
const response = await this.server.linkDevice({
|
||||||
|
number,
|
||||||
|
verificationCode,
|
||||||
|
encryptedDeviceName,
|
||||||
|
newPassword: password,
|
||||||
|
registrationId,
|
||||||
|
pniRegistrationId,
|
||||||
|
...keysToUpload,
|
||||||
|
});
|
||||||
|
|
||||||
|
ourAci = normalizeAci(response.uuid, 'createAccount');
|
||||||
|
strictAssert(
|
||||||
|
isUntaggedPniString(response.pni),
|
||||||
|
'Response pni must be untagged'
|
||||||
|
);
|
||||||
|
ourPni = toTaggedPni(response.pni);
|
||||||
|
deviceId = response.deviceId ?? 1;
|
||||||
|
|
||||||
|
strictAssert(
|
||||||
|
ourAci === options.ourAci,
|
||||||
|
'Server response has unexpected ACI'
|
||||||
|
);
|
||||||
|
strictAssert(
|
||||||
|
ourPni === options.ourPni,
|
||||||
|
'Server response has unexpected PNI'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(options);
|
||||||
|
}
|
||||||
|
|
||||||
// `setCredentials` needs to be called
|
// `setCredentials` needs to be called
|
||||||
// before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes`
|
// before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes`
|
||||||
// indirectly calls `ConversationController.getConversationId()` which
|
// indirectly calls `ConversationController.getConversationId()` which
|
||||||
|
@ -1014,8 +1146,8 @@ export default class AccountManager extends EventTarget {
|
||||||
aci: ourAci,
|
aci: ourAci,
|
||||||
pni: ourPni,
|
pni: ourPni,
|
||||||
number,
|
number,
|
||||||
deviceId: response.deviceId ?? 1,
|
deviceId,
|
||||||
deviceName: deviceName ?? undefined,
|
deviceName: options.deviceName,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1047,22 +1179,16 @@ export default class AccountManager extends EventTarget {
|
||||||
...identityAttrs,
|
...identityAttrs,
|
||||||
publicKey: aciKeyPair.pubKey,
|
publicKey: aciKeyPair.pubKey,
|
||||||
}),
|
}),
|
||||||
pniKeyPair
|
storage.protocol.saveIdentityWithAttributes(ourPni, {
|
||||||
? storage.protocol.saveIdentityWithAttributes(ourPni, {
|
...identityAttrs,
|
||||||
...identityAttrs,
|
publicKey: pniKeyPair.pubKey,
|
||||||
publicKey: pniKeyPair.pubKey,
|
}),
|
||||||
})
|
|
||||||
: Promise.resolve(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const identityKeyMap = {
|
const identityKeyMap = {
|
||||||
...(storage.get('identityKeyMap') || {}),
|
...(storage.get('identityKeyMap') || {}),
|
||||||
[ourAci]: aciKeyPair,
|
[ourAci]: aciKeyPair,
|
||||||
...(pniKeyPair
|
[ourPni]: pniKeyPair,
|
||||||
? {
|
|
||||||
[ourPni]: pniKeyPair,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
const registrationIdMap = {
|
const registrationIdMap = {
|
||||||
...(storage.get('registrationIdMap') || {}),
|
...(storage.get('registrationIdMap') || {}),
|
||||||
|
@ -1072,9 +1198,7 @@ export default class AccountManager extends EventTarget {
|
||||||
|
|
||||||
await storage.put('identityKeyMap', identityKeyMap);
|
await storage.put('identityKeyMap', identityKeyMap);
|
||||||
await storage.put('registrationIdMap', registrationIdMap);
|
await storage.put('registrationIdMap', registrationIdMap);
|
||||||
if (profileKey) {
|
await ourProfileKeyService.set(profileKey);
|
||||||
await ourProfileKeyService.set(profileKey);
|
|
||||||
}
|
|
||||||
if (userAgent) {
|
if (userAgent) {
|
||||||
await storage.put('userAgent', userAgent);
|
await storage.put('userAgent', userAgent);
|
||||||
}
|
}
|
||||||
|
@ -1084,20 +1208,82 @@ export default class AccountManager extends EventTarget {
|
||||||
const regionCode = getRegionCodeForNumber(number);
|
const regionCode = getRegionCodeForNumber(number);
|
||||||
await storage.put('regionCode', regionCode);
|
await storage.put('regionCode', regionCode);
|
||||||
await storage.protocol.hydrateCaches();
|
await storage.protocol.hydrateCaches();
|
||||||
|
|
||||||
|
const store = storage.protocol;
|
||||||
|
|
||||||
|
await store.storeSignedPreKey(
|
||||||
|
ourAci,
|
||||||
|
aciSignedPreKey.keyId,
|
||||||
|
aciSignedPreKey.keyPair
|
||||||
|
);
|
||||||
|
await store.storeSignedPreKey(
|
||||||
|
ourPni,
|
||||||
|
pniSignedPreKey.keyId,
|
||||||
|
pniSignedPreKey.keyPair
|
||||||
|
);
|
||||||
|
await store.storeKyberPreKeys(ourAci, [
|
||||||
|
kyberPreKeyToStoredSignedPreKey(aciPqLastResortPreKey, ourAci),
|
||||||
|
]);
|
||||||
|
await store.storeKyberPreKeys(ourPni, [
|
||||||
|
kyberPreKeyToStoredSignedPreKey(pniPqLastResortPreKey, ourPni),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this._confirmKeys(
|
||||||
|
{
|
||||||
|
pqLastResortPreKey: keysToUpload.aciPqLastResortPreKey,
|
||||||
|
signedPreKey: keysToUpload.aciSignedPreKey,
|
||||||
|
},
|
||||||
|
ServiceIdKind.ACI
|
||||||
|
);
|
||||||
|
await this._confirmKeys(
|
||||||
|
{
|
||||||
|
pqLastResortPreKey: keysToUpload.pniPqLastResortPreKey,
|
||||||
|
signedPreKey: keysToUpload.pniSignedPreKey,
|
||||||
|
},
|
||||||
|
ServiceIdKind.PNI
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadKeys = async (kind: ServiceIdKind) => {
|
||||||
|
try {
|
||||||
|
const keys = await this._generateSingleUseKeys(kind);
|
||||||
|
await this.server.registerKeys(keys, kind);
|
||||||
|
} catch (error) {
|
||||||
|
if (kind === ServiceIdKind.PNI) {
|
||||||
|
log.error(
|
||||||
|
'Failed to upload PNI prekeys. Moving on',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
uploadKeys(ServiceIdKind.ACI),
|
||||||
|
uploadKeys(ServiceIdKind.PNI),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exposed only for testing
|
// Exposed only for testing
|
||||||
public async _confirmKeys(
|
public async _confirmKeys(
|
||||||
keys: UploadKeysType,
|
{
|
||||||
|
signedPreKey,
|
||||||
|
pqLastResortPreKey,
|
||||||
|
}: Readonly<{
|
||||||
|
signedPreKey?: UploadSignedPreKeyType;
|
||||||
|
pqLastResortPreKey?: UploadSignedPreKeyType;
|
||||||
|
}>,
|
||||||
serviceIdKind: ServiceIdKind
|
serviceIdKind: ServiceIdKind
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const ourServiceId =
|
||||||
|
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
||||||
const logId = `AccountManager.confirmKeys(${serviceIdKind})`;
|
const logId = `AccountManager.confirmKeys(${serviceIdKind})`;
|
||||||
const { storage } = window.textsecure;
|
const { storage } = window.textsecure;
|
||||||
const store = storage.protocol;
|
const store = storage.protocol;
|
||||||
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
|
|
||||||
|
|
||||||
const updatedAt = Date.now();
|
const updatedAt = Date.now();
|
||||||
const { signedPreKey, pqLastResortPreKey } = keys;
|
|
||||||
if (signedPreKey) {
|
if (signedPreKey) {
|
||||||
log.info(`${logId}: confirming signed prekey key`, signedPreKey.keyId);
|
log.info(`${logId}: confirming signed prekey key`, signedPreKey.keyId);
|
||||||
await store.confirmSignedPreKey(ourServiceId, signedPreKey.keyId);
|
await store.confirmSignedPreKey(ourServiceId, signedPreKey.keyId);
|
||||||
|
@ -1125,47 +1311,31 @@ export default class AccountManager extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Very similar to maybeUpdateKeys, but will always generate prekeys and doesn't upload
|
// Very similar to maybeUpdateKeys, but will always generate prekeys and doesn't upload
|
||||||
async _generateKeys(
|
async _generateSingleUseKeys(
|
||||||
count: number,
|
|
||||||
serviceIdKind: ServiceIdKind,
|
serviceIdKind: ServiceIdKind,
|
||||||
maybeIdentityKey?: KeyPairType
|
count = PRE_KEY_GEN_BATCH_SIZE
|
||||||
): Promise<UploadKeysType> {
|
): Promise<UploadKeysType> {
|
||||||
const logId = `AcountManager.generateKeys(${serviceIdKind})`;
|
const ourServiceId =
|
||||||
const { storage } = window.textsecure;
|
window.textsecure.storage.user.getCheckedServiceId(serviceIdKind);
|
||||||
const store = storage.protocol;
|
const logId = `AccountManager.generateKeys(${serviceIdKind}, ${ourServiceId})`;
|
||||||
const ourServiceId = storage.user.getCheckedServiceId(serviceIdKind);
|
|
||||||
|
|
||||||
const identityKey =
|
|
||||||
maybeIdentityKey ?? store.getIdentityKeyPair(ourServiceId);
|
|
||||||
strictAssert(identityKey, 'generateKeys: No identity key pair!');
|
|
||||||
|
|
||||||
const preKeys = await this.generateNewPreKeys(serviceIdKind, count);
|
const preKeys = await this.generateNewPreKeys(serviceIdKind, count);
|
||||||
const pqPreKeys = await this.generateNewKyberPreKeys(serviceIdKind, count);
|
const pqPreKeys = await this.generateNewKyberPreKeys(serviceIdKind, count);
|
||||||
const pqLastResortPreKey = await this.maybeUpdateLastResortKyberKey(
|
|
||||||
serviceIdKind
|
|
||||||
);
|
|
||||||
const signedPreKey = await this.maybeUpdateSignedPreKey(serviceIdKind);
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`${logId}: Generated ` +
|
`${logId}: Generated ` +
|
||||||
`${preKeys.length} pre keys, ` +
|
`${preKeys.length} pre keys, ` +
|
||||||
`${pqPreKeys.length} kyber pre keys, ` +
|
`${pqPreKeys.length} kyber pre keys`
|
||||||
`${pqLastResortPreKey ? 'a' : 'NO'} last resort kyber pre key, ` +
|
|
||||||
`and ${signedPreKey ? 'a' : 'NO'} signed pre key.`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// These are primarily for the summaries they log out
|
// These are primarily for the summaries they log out
|
||||||
await this._cleanPreKeys(serviceIdKind);
|
await this._cleanPreKeys(serviceIdKind);
|
||||||
await this._cleanKyberPreKeys(serviceIdKind);
|
await this._cleanKyberPreKeys(serviceIdKind);
|
||||||
await this._cleanLastResortKeys(serviceIdKind);
|
|
||||||
await this._cleanSignedPreKeys(serviceIdKind);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identityKey: identityKey.pubKey,
|
identityKey: this.getIdentityKeyOrThrow(ourServiceId).pubKey,
|
||||||
preKeys,
|
preKeys,
|
||||||
pqPreKeys,
|
pqPreKeys,
|
||||||
pqLastResortPreKey,
|
|
||||||
signedPreKey,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { getBasicAuth } from '../util/getBasicAuth';
|
||||||
import { isPnpEnabled } from '../util/isPnpEnabled';
|
import { isPnpEnabled } from '../util/isPnpEnabled';
|
||||||
import { createHTTPSAgent } from '../util/createHTTPSAgent';
|
import { createHTTPSAgent } from '../util/createHTTPSAgent';
|
||||||
import type { SocketStatus } from '../types/SocketStatus';
|
import type { SocketStatus } from '../types/SocketStatus';
|
||||||
|
import { VerificationTransport } from '../types/VerificationTransport';
|
||||||
import { toLogFormat } from '../types/errors';
|
import { toLogFormat } from '../types/errors';
|
||||||
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
||||||
import type {
|
import type {
|
||||||
|
@ -37,7 +38,12 @@ import type {
|
||||||
AciString,
|
AciString,
|
||||||
UntaggedPniString,
|
UntaggedPniString,
|
||||||
} from '../types/ServiceId';
|
} from '../types/ServiceId';
|
||||||
import { ServiceIdKind, serviceIdSchema, aciSchema } from '../types/ServiceId';
|
import {
|
||||||
|
ServiceIdKind,
|
||||||
|
serviceIdSchema,
|
||||||
|
aciSchema,
|
||||||
|
untaggedPniSchema,
|
||||||
|
} from '../types/ServiceId';
|
||||||
import type { DirectoryConfigType } from '../types/RendererConfig';
|
import type { DirectoryConfigType } from '../types/RendererConfig';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import { randomInt } from '../Crypto';
|
import { randomInt } from '../Crypto';
|
||||||
|
@ -482,7 +488,6 @@ function makeHTTPError(
|
||||||
}
|
}
|
||||||
|
|
||||||
const URL_CALLS = {
|
const URL_CALLS = {
|
||||||
accounts: 'v1/accounts',
|
|
||||||
accountExistence: 'v1/accounts/account',
|
accountExistence: 'v1/accounts/account',
|
||||||
attachmentId: 'v3/attachments/form/upload',
|
attachmentId: 'v3/attachments/form/upload',
|
||||||
attestation: 'v1/attestation',
|
attestation: 'v1/attestation',
|
||||||
|
@ -491,7 +496,6 @@ const URL_CALLS = {
|
||||||
challenge: 'v1/challenge',
|
challenge: 'v1/challenge',
|
||||||
config: 'v1/config',
|
config: 'v1/config',
|
||||||
deliveryCert: 'v1/certificate/delivery',
|
deliveryCert: 'v1/certificate/delivery',
|
||||||
devices: 'v1/devices',
|
|
||||||
directoryAuthV2: 'v2/directory/auth',
|
directoryAuthV2: 'v2/directory/auth',
|
||||||
discovery: 'v1/discovery',
|
discovery: 'v1/discovery',
|
||||||
getGroupAvatarUpload: 'v1/groups/avatar/form',
|
getGroupAvatarUpload: 'v1/groups/avatar/form',
|
||||||
|
@ -507,10 +511,12 @@ const URL_CALLS = {
|
||||||
groupsViaLink: 'v1/groups/join/',
|
groupsViaLink: 'v1/groups/join/',
|
||||||
groupToken: 'v1/groups/token',
|
groupToken: 'v1/groups/token',
|
||||||
keys: 'v2/keys',
|
keys: 'v2/keys',
|
||||||
|
linkDevice: 'v1/devices/link',
|
||||||
messages: 'v1/messages',
|
messages: 'v1/messages',
|
||||||
multiRecipient: 'v1/messages/multi_recipient',
|
multiRecipient: 'v1/messages/multi_recipient',
|
||||||
phoneNumberDiscoverability: 'v2/accounts/phone_number_discoverability',
|
phoneNumberDiscoverability: 'v2/accounts/phone_number_discoverability',
|
||||||
profile: 'v1/profile',
|
profile: 'v1/profile',
|
||||||
|
registration: 'v1/registration',
|
||||||
registerCapabilities: 'v1/devices/capabilities',
|
registerCapabilities: 'v1/devices/capabilities',
|
||||||
reportMessage: 'v1/messages/report',
|
reportMessage: 'v1/messages/report',
|
||||||
signed: 'v2/keys/signed',
|
signed: 'v2/keys/signed',
|
||||||
|
@ -525,6 +531,7 @@ const URL_CALLS = {
|
||||||
reserveUsername: 'v1/accounts/username_hash/reserve',
|
reserveUsername: 'v1/accounts/username_hash/reserve',
|
||||||
confirmUsername: 'v1/accounts/username_hash/confirm',
|
confirmUsername: 'v1/accounts/username_hash/confirm',
|
||||||
usernameLink: 'v1/accounts/username_link',
|
usernameLink: 'v1/accounts/username_link',
|
||||||
|
verificationSession: 'v1/verification/session',
|
||||||
whoami: 'v1/accounts/whoami',
|
whoami: 'v1/accounts/whoami',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -548,7 +555,7 @@ const WEBSOCKET_CALLS = new Set<keyof typeof URL_CALLS>([
|
||||||
'getGroupCredentials',
|
'getGroupCredentials',
|
||||||
|
|
||||||
// Devices
|
// Devices
|
||||||
'devices',
|
'linkDevice',
|
||||||
'registerCapabilities',
|
'registerCapabilities',
|
||||||
'supportUnauthenticatedDelivery',
|
'supportUnauthenticatedDelivery',
|
||||||
|
|
||||||
|
@ -751,12 +758,6 @@ const whoamiResultZod = z.object({
|
||||||
});
|
});
|
||||||
export type WhoamiResultType = z.infer<typeof whoamiResultZod>;
|
export type WhoamiResultType = z.infer<typeof whoamiResultZod>;
|
||||||
|
|
||||||
export type ConfirmCodeResultType = Readonly<{
|
|
||||||
uuid: AciString;
|
|
||||||
pni: UntaggedPniString;
|
|
||||||
deviceId?: number;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type CdsLookupOptionsType = Readonly<{
|
export type CdsLookupOptionsType = Readonly<{
|
||||||
e164s: ReadonlyArray<string>;
|
e164s: ReadonlyArray<string>;
|
||||||
acis?: ReadonlyArray<AciString>;
|
acis?: ReadonlyArray<AciString>;
|
||||||
|
@ -864,16 +865,29 @@ export type ResolveUsernameLinkResultType = z.infer<
|
||||||
typeof resolveUsernameLinkResultZod
|
typeof resolveUsernameLinkResultZod
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ConfirmCodeOptionsType = Readonly<{
|
export type CreateAccountOptionsType = Readonly<{
|
||||||
|
sessionId: string;
|
||||||
number: string;
|
number: string;
|
||||||
code: string;
|
code: string;
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
registrationId: number;
|
registrationId: number;
|
||||||
pniRegistrationId: number;
|
pniRegistrationId: number;
|
||||||
deviceName?: string | null;
|
accessKey: Uint8Array;
|
||||||
accessKey?: Uint8Array;
|
aciPublicKey: Uint8Array;
|
||||||
|
pniPublicKey: Uint8Array;
|
||||||
|
aciSignedPreKey: UploadSignedPreKeyType;
|
||||||
|
pniSignedPreKey: UploadSignedPreKeyType;
|
||||||
|
aciPqLastResortPreKey: UploadSignedPreKeyType;
|
||||||
|
pniPqLastResortPreKey: UploadSignedPreKeyType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
const linkDeviceResultZod = z.object({
|
||||||
|
uuid: aciSchema,
|
||||||
|
pni: untaggedPniSchema,
|
||||||
|
deviceId: z.number(),
|
||||||
|
});
|
||||||
|
export type LinkDeviceResultType = z.infer<typeof linkDeviceResultZod>;
|
||||||
|
|
||||||
export type ReportMessageOptionsType = Readonly<{
|
export type ReportMessageOptionsType = Readonly<{
|
||||||
senderAci: AciString;
|
senderAci: AciString;
|
||||||
serverGuid: string;
|
serverGuid: string;
|
||||||
|
@ -901,14 +915,43 @@ export type ServerKeyCountType = {
|
||||||
pqCount: number;
|
pqCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LinkDeviceOptionsType = Readonly<{
|
||||||
|
number: string;
|
||||||
|
verificationCode: string;
|
||||||
|
encryptedDeviceName?: string;
|
||||||
|
newPassword: string;
|
||||||
|
registrationId: number;
|
||||||
|
pniRegistrationId: number;
|
||||||
|
aciSignedPreKey: UploadSignedPreKeyType;
|
||||||
|
pniSignedPreKey: UploadSignedPreKeyType;
|
||||||
|
aciPqLastResortPreKey: UploadSignedPreKeyType;
|
||||||
|
pniPqLastResortPreKey: UploadSignedPreKeyType;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const createAccountResultZod = z.object({
|
||||||
|
uuid: aciSchema,
|
||||||
|
pni: untaggedPniSchema,
|
||||||
|
});
|
||||||
|
export type CreateAccountResultType = z.infer<typeof createAccountResultZod>;
|
||||||
|
|
||||||
|
const verificationSessionZod = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
allowedToRequestCode: z.boolean(),
|
||||||
|
verified: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RequestVerificationResultType = Readonly<{
|
||||||
|
sessionId: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type WebAPIType = {
|
export type WebAPIType = {
|
||||||
startRegistration(): unknown;
|
startRegistration(): unknown;
|
||||||
finishRegistration(baton: unknown): void;
|
finishRegistration(baton: unknown): void;
|
||||||
cancelInflightRequests: (reason: string) => void;
|
cancelInflightRequests: (reason: string) => void;
|
||||||
cdsLookup: (options: CdsLookupOptionsType) => Promise<CDSResponseType>;
|
cdsLookup: (options: CdsLookupOptionsType) => Promise<CDSResponseType>;
|
||||||
confirmCode: (
|
createAccount: (
|
||||||
options: ConfirmCodeOptionsType
|
options: CreateAccountOptionsType
|
||||||
) => Promise<ConfirmCodeResultType>;
|
) => Promise<CreateAccountResultType>;
|
||||||
createGroup: (
|
createGroup: (
|
||||||
group: Proto.IGroup,
|
group: Proto.IGroup,
|
||||||
options: GroupCredentialsType
|
options: GroupCredentialsType
|
||||||
|
@ -928,7 +971,6 @@ export type WebAPIType = {
|
||||||
}
|
}
|
||||||
) => Promise<Uint8Array>;
|
) => Promise<Uint8Array>;
|
||||||
getAvatar: (path: string) => Promise<Uint8Array>;
|
getAvatar: (path: string) => Promise<Uint8Array>;
|
||||||
getDevices: () => Promise<GetDevicesResultType>;
|
|
||||||
getHasSubscription: (subscriberId: Uint8Array) => Promise<boolean>;
|
getHasSubscription: (subscriberId: Uint8Array) => Promise<boolean>;
|
||||||
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
|
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
|
||||||
getGroupFromLink: (
|
getGroupFromLink: (
|
||||||
|
@ -996,6 +1038,7 @@ export type WebAPIType = {
|
||||||
href: string,
|
href: string,
|
||||||
abortSignal: AbortSignal
|
abortSignal: AbortSignal
|
||||||
) => Promise<null | linkPreviewFetch.LinkPreviewImage>;
|
) => Promise<null | linkPreviewFetch.LinkPreviewImage>;
|
||||||
|
linkDevice: (options: LinkDeviceOptionsType) => Promise<LinkDeviceResultType>;
|
||||||
makeProxiedRequest: (
|
makeProxiedRequest: (
|
||||||
targetUrl: string,
|
targetUrl: string,
|
||||||
options?: ProxiedRequestOptionsType
|
options?: ProxiedRequestOptionsType
|
||||||
|
@ -1044,8 +1087,11 @@ export type WebAPIType = {
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
|
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
|
||||||
reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
|
reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
|
||||||
requestVerificationSMS: (number: string, token: string) => Promise<void>;
|
requestVerification: (
|
||||||
requestVerificationVoice: (number: string, token: string) => Promise<void>;
|
number: string,
|
||||||
|
captcha: string,
|
||||||
|
transport: VerificationTransport
|
||||||
|
) => Promise<RequestVerificationResultType>;
|
||||||
checkAccountExistence: (serviceId: ServiceIdString) => Promise<boolean>;
|
checkAccountExistence: (serviceId: ServiceIdString) => Promise<boolean>;
|
||||||
sendMessages: (
|
sendMessages: (
|
||||||
destination: ServiceIdString,
|
destination: ServiceIdString,
|
||||||
|
@ -1110,6 +1156,12 @@ export type UploadPreKeyType = {
|
||||||
};
|
};
|
||||||
export type UploadKyberPreKeyType = UploadSignedPreKeyType;
|
export type UploadKyberPreKeyType = UploadSignedPreKeyType;
|
||||||
|
|
||||||
|
type SerializedSignedPreKeyType = Readonly<{
|
||||||
|
keyId: number;
|
||||||
|
publicKey: string;
|
||||||
|
signature: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type UploadKeysType = {
|
export type UploadKeysType = {
|
||||||
identityKey: Uint8Array;
|
identityKey: Uint8Array;
|
||||||
|
|
||||||
|
@ -1321,7 +1373,7 @@ export function initialize({
|
||||||
cdsLookup,
|
cdsLookup,
|
||||||
checkAccountExistence,
|
checkAccountExistence,
|
||||||
checkSockets,
|
checkSockets,
|
||||||
confirmCode,
|
createAccount,
|
||||||
confirmUsername,
|
confirmUsername,
|
||||||
createGroup,
|
createGroup,
|
||||||
deleteUsername,
|
deleteUsername,
|
||||||
|
@ -1338,7 +1390,6 @@ export function initialize({
|
||||||
getBadgeImageFile,
|
getBadgeImageFile,
|
||||||
getBoostBadgesFromServer,
|
getBoostBadgesFromServer,
|
||||||
getConfig,
|
getConfig,
|
||||||
getDevices,
|
|
||||||
getGroup,
|
getGroup,
|
||||||
getGroupAvatar,
|
getGroupAvatar,
|
||||||
getGroupCredentials,
|
getGroupCredentials,
|
||||||
|
@ -1361,6 +1412,7 @@ export function initialize({
|
||||||
getStorageCredentials,
|
getStorageCredentials,
|
||||||
getStorageManifest,
|
getStorageManifest,
|
||||||
getStorageRecords,
|
getStorageRecords,
|
||||||
|
linkDevice,
|
||||||
logout,
|
logout,
|
||||||
makeProxiedRequest,
|
makeProxiedRequest,
|
||||||
makeSfuRequest,
|
makeSfuRequest,
|
||||||
|
@ -1381,8 +1433,7 @@ export function initialize({
|
||||||
resolveUsernameLink,
|
resolveUsernameLink,
|
||||||
replaceUsernameLink,
|
replaceUsernameLink,
|
||||||
reportMessage,
|
reportMessage,
|
||||||
requestVerificationSMS,
|
requestVerification,
|
||||||
requestVerificationVoice,
|
|
||||||
reserveUsername,
|
reserveUsername,
|
||||||
sendChallengeResponse,
|
sendChallengeResponse,
|
||||||
sendMessages,
|
sendMessages,
|
||||||
|
@ -1470,6 +1521,22 @@ export function initialize({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializeSignedPreKey(
|
||||||
|
preKey?: UploadSignedPreKeyType
|
||||||
|
): SerializedSignedPreKeyType | undefined {
|
||||||
|
if (preKey == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { keyId, publicKey, signature } = preKey;
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyId,
|
||||||
|
publicKey: Bytes.toBase64(publicKey),
|
||||||
|
signature: Bytes.toBase64(signature),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function serviceIdKindToQuery(kind: ServiceIdKind): string {
|
function serviceIdKindToQuery(kind: ServiceIdKind): string {
|
||||||
let value: string;
|
let value: string;
|
||||||
if (kind === ServiceIdKind.ACI) {
|
if (kind === ServiceIdKind.ACI) {
|
||||||
|
@ -2005,20 +2072,64 @@ export function initialize({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestVerificationSMS(number: string, token: string) {
|
async function requestVerification(
|
||||||
await _ajax({
|
number: string,
|
||||||
call: 'accounts',
|
captcha: string,
|
||||||
httpType: 'GET',
|
transport: VerificationTransport
|
||||||
urlParameters: `/sms/code/${number}?captcha=${token}`,
|
) {
|
||||||
});
|
// Create a new blank session using just a E164
|
||||||
}
|
let session = verificationSessionZod.parse(
|
||||||
|
await _ajax({
|
||||||
|
call: 'verificationSession',
|
||||||
|
httpType: 'POST',
|
||||||
|
responseType: 'json',
|
||||||
|
jsonData: {
|
||||||
|
number,
|
||||||
|
},
|
||||||
|
unauthenticated: true,
|
||||||
|
accessKey: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
async function requestVerificationVoice(number: string, token: string) {
|
// Submit a captcha solution to the session
|
||||||
await _ajax({
|
session = verificationSessionZod.parse(
|
||||||
call: 'accounts',
|
await _ajax({
|
||||||
httpType: 'GET',
|
call: 'verificationSession',
|
||||||
urlParameters: `/voice/code/${number}?captcha=${token}`,
|
httpType: 'PATCH',
|
||||||
});
|
urlParameters: `/${encodeURIComponent(session.id)}`,
|
||||||
|
responseType: 'json',
|
||||||
|
jsonData: {
|
||||||
|
captcha,
|
||||||
|
},
|
||||||
|
unauthenticated: true,
|
||||||
|
accessKey: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that captcha was accepted
|
||||||
|
if (!session.allowedToRequestCode) {
|
||||||
|
throw new Error('requestVerification: Not allowed to send code');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request an SMS or Voice confirmation
|
||||||
|
session = verificationSessionZod.parse(
|
||||||
|
await _ajax({
|
||||||
|
call: 'verificationSession',
|
||||||
|
httpType: 'POST',
|
||||||
|
urlParameters: `/${encodeURIComponent(session.id)}/code`,
|
||||||
|
responseType: 'json',
|
||||||
|
jsonData: {
|
||||||
|
client: 'ios',
|
||||||
|
transport:
|
||||||
|
transport === VerificationTransport.SMS ? 'sms' : 'voice',
|
||||||
|
},
|
||||||
|
unauthenticated: true,
|
||||||
|
accessKey: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return sessionId to be used in `createAccount`
|
||||||
|
return { sessionId: session.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkAccountExistence(serviceId: ServiceIdString) {
|
async function checkAccountExistence(serviceId: ServiceIdString) {
|
||||||
|
@ -2065,58 +2176,151 @@ export function initialize({
|
||||||
current.resolve();
|
current.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmCode({
|
async function _withNewCredentials<
|
||||||
number,
|
Result extends { uuid: AciString; deviceId?: number }
|
||||||
code,
|
>(
|
||||||
newPassword,
|
{ username: newUsername, password: newPassword }: WebAPICredentials,
|
||||||
registrationId,
|
callback: () => Promise<Result>
|
||||||
pniRegistrationId,
|
): Promise<Result> {
|
||||||
deviceName,
|
|
||||||
accessKey,
|
|
||||||
}: ConfirmCodeOptionsType) {
|
|
||||||
const capabilities: CapabilitiesUploadType = {
|
|
||||||
pni: isPnpEnabled(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const jsonData = {
|
|
||||||
capabilities,
|
|
||||||
fetchesMessages: true,
|
|
||||||
name: deviceName || undefined,
|
|
||||||
registrationId,
|
|
||||||
pniRegistrationId,
|
|
||||||
supportsSms: false,
|
|
||||||
unidentifiedAccessKey: accessKey
|
|
||||||
? Bytes.toBase64(accessKey)
|
|
||||||
: undefined,
|
|
||||||
unrestrictedUnidentifiedAccess: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const call = deviceName ? 'devices' : 'accounts';
|
|
||||||
const urlPrefix = deviceName ? '/' : '/code/';
|
|
||||||
|
|
||||||
// Reset old websocket credentials and disconnect.
|
// Reset old websocket credentials and disconnect.
|
||||||
// AccountManager is our only caller and it will trigger
|
// AccountManager is our only caller and it will trigger
|
||||||
// `registration_done` which will update credentials.
|
// `registration_done` which will update credentials.
|
||||||
await logout();
|
await logout();
|
||||||
|
|
||||||
// Update REST credentials, though. We need them for the call below
|
// Update REST credentials, though. We need them for the call below
|
||||||
username = number;
|
username = newUsername;
|
||||||
password = newPassword;
|
password = newPassword;
|
||||||
|
|
||||||
const response = (await _ajax({
|
const result = await callback();
|
||||||
isRegistration: true,
|
|
||||||
call,
|
const { uuid: aci = newUsername, deviceId = 1 } = result;
|
||||||
httpType: 'PUT',
|
|
||||||
responseType: 'json',
|
|
||||||
urlParameters: urlPrefix + code,
|
|
||||||
jsonData,
|
|
||||||
})) as ConfirmCodeResultType;
|
|
||||||
|
|
||||||
// Set final REST credentials to let `registerKeys` succeed.
|
// Set final REST credentials to let `registerKeys` succeed.
|
||||||
username = `${response.uuid || number}.${response.deviceId || 1}`;
|
username = `${aci}.${deviceId}`;
|
||||||
password = newPassword;
|
password = newPassword;
|
||||||
|
|
||||||
return response;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAccount({
|
||||||
|
sessionId,
|
||||||
|
number,
|
||||||
|
code,
|
||||||
|
newPassword,
|
||||||
|
registrationId,
|
||||||
|
pniRegistrationId,
|
||||||
|
accessKey,
|
||||||
|
aciPublicKey,
|
||||||
|
pniPublicKey,
|
||||||
|
aciSignedPreKey,
|
||||||
|
pniSignedPreKey,
|
||||||
|
aciPqLastResortPreKey,
|
||||||
|
pniPqLastResortPreKey,
|
||||||
|
}: CreateAccountOptionsType) {
|
||||||
|
const session = verificationSessionZod.parse(
|
||||||
|
await _ajax({
|
||||||
|
isRegistration: true,
|
||||||
|
call: 'verificationSession',
|
||||||
|
httpType: 'PUT',
|
||||||
|
urlParameters: `/${encodeURIComponent(sessionId)}/code`,
|
||||||
|
responseType: 'json',
|
||||||
|
jsonData: {
|
||||||
|
code,
|
||||||
|
},
|
||||||
|
unauthenticated: true,
|
||||||
|
accessKey: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!session.verified) {
|
||||||
|
throw new Error('createAccount: invalid code');
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonData = {
|
||||||
|
sessionId: session.id,
|
||||||
|
accountAttributes: {
|
||||||
|
fetchesMessages: true,
|
||||||
|
registrationId,
|
||||||
|
pniRegistrationId,
|
||||||
|
capabilities: {
|
||||||
|
pni: isPnpEnabled(),
|
||||||
|
},
|
||||||
|
unidentifiedAccessKey: Bytes.toBase64(accessKey),
|
||||||
|
},
|
||||||
|
requireAtomic: true,
|
||||||
|
skipDeviceTransfer: true,
|
||||||
|
aciIdentityKey: Bytes.toBase64(aciPublicKey),
|
||||||
|
pniIdentityKey: Bytes.toBase64(pniPublicKey),
|
||||||
|
aciSignedPreKey: serializeSignedPreKey(aciSignedPreKey),
|
||||||
|
pniSignedPreKey: serializeSignedPreKey(pniSignedPreKey),
|
||||||
|
aciPqLastResortPreKey: serializeSignedPreKey(aciPqLastResortPreKey),
|
||||||
|
pniPqLastResortPreKey: serializeSignedPreKey(pniPqLastResortPreKey),
|
||||||
|
};
|
||||||
|
|
||||||
|
return _withNewCredentials(
|
||||||
|
{
|
||||||
|
username: number,
|
||||||
|
password: newPassword,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const responseJson = await _ajax({
|
||||||
|
isRegistration: true,
|
||||||
|
call: 'registration',
|
||||||
|
httpType: 'POST',
|
||||||
|
responseType: 'json',
|
||||||
|
jsonData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return createAccountResultZod.parse(responseJson);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkDevice({
|
||||||
|
number,
|
||||||
|
verificationCode,
|
||||||
|
encryptedDeviceName,
|
||||||
|
newPassword,
|
||||||
|
registrationId,
|
||||||
|
pniRegistrationId,
|
||||||
|
aciSignedPreKey,
|
||||||
|
pniSignedPreKey,
|
||||||
|
aciPqLastResortPreKey,
|
||||||
|
pniPqLastResortPreKey,
|
||||||
|
}: LinkDeviceOptionsType) {
|
||||||
|
const jsonData = {
|
||||||
|
verificationCode,
|
||||||
|
accountAttributes: {
|
||||||
|
fetchesMessages: true,
|
||||||
|
name: encryptedDeviceName,
|
||||||
|
registrationId,
|
||||||
|
pniRegistrationId,
|
||||||
|
capabilities: {
|
||||||
|
pni: isPnpEnabled(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aciSignedPreKey: serializeSignedPreKey(aciSignedPreKey),
|
||||||
|
pniSignedPreKey: serializeSignedPreKey(pniSignedPreKey),
|
||||||
|
aciPqLastResortPreKey: serializeSignedPreKey(aciPqLastResortPreKey),
|
||||||
|
pniPqLastResortPreKey: serializeSignedPreKey(pniPqLastResortPreKey),
|
||||||
|
};
|
||||||
|
return _withNewCredentials(
|
||||||
|
{
|
||||||
|
username: number,
|
||||||
|
password: newPassword,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const responseJson = await _ajax({
|
||||||
|
isRegistration: true,
|
||||||
|
call: 'linkDevice',
|
||||||
|
httpType: 'PUT',
|
||||||
|
responseType: 'json',
|
||||||
|
jsonData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return linkDeviceResultZod.parse(responseJson);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDeviceName(deviceName: string) {
|
async function updateDeviceName(deviceName: string) {
|
||||||
|
@ -2137,14 +2341,6 @@ export function initialize({
|
||||||
})) as GetIceServersResultType;
|
})) as GetIceServersResultType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDevices() {
|
|
||||||
return (await _ajax({
|
|
||||||
call: 'devices',
|
|
||||||
httpType: 'GET',
|
|
||||||
responseType: 'json',
|
|
||||||
})) as GetDevicesResultType;
|
|
||||||
}
|
|
||||||
|
|
||||||
type JSONSignedPreKeyType = {
|
type JSONSignedPreKeyType = {
|
||||||
keyId: number;
|
keyId: number;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
@ -2203,24 +2399,8 @@ export function initialize({
|
||||||
identityKey: Bytes.toBase64(genKeys.identityKey),
|
identityKey: Bytes.toBase64(genKeys.identityKey),
|
||||||
preKeys,
|
preKeys,
|
||||||
pqPreKeys,
|
pqPreKeys,
|
||||||
...(genKeys.pqLastResortPreKey
|
pqLastResortPreKey: serializeSignedPreKey(genKeys.pqLastResortPreKey),
|
||||||
? {
|
signedPreKey: serializeSignedPreKey(genKeys.signedPreKey),
|
||||||
pqLastResortPreKey: {
|
|
||||||
keyId: genKeys.pqLastResortPreKey.keyId,
|
|
||||||
publicKey: Bytes.toBase64(genKeys.pqLastResortPreKey.publicKey),
|
|
||||||
signature: Bytes.toBase64(genKeys.pqLastResortPreKey.signature),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: null),
|
|
||||||
...(genKeys.signedPreKey
|
|
||||||
? {
|
|
||||||
signedPreKey: {
|
|
||||||
keyId: genKeys.signedPreKey.keyId,
|
|
||||||
publicKey: Bytes.toBase64(genKeys.signedPreKey.publicKey),
|
|
||||||
signature: Bytes.toBase64(genKeys.signedPreKey.signature),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: null),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await _ajax({
|
await _ajax({
|
||||||
|
|
|
@ -184,6 +184,16 @@ export const aciSchema = z
|
||||||
return x;
|
return x;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const untaggedPniSchema = z
|
||||||
|
.string()
|
||||||
|
.refine(isUntaggedPniString)
|
||||||
|
.transform(x => {
|
||||||
|
if (!isUntaggedPniString(x)) {
|
||||||
|
throw new Error('Refine did not throw!');
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
|
||||||
export const serviceIdSchema = z
|
export const serviceIdSchema = z
|
||||||
.string()
|
.string()
|
||||||
.refine(isServiceIdString)
|
.refine(isServiceIdString)
|
||||||
|
|
7
ts/types/VerificationTransport.ts
Normal file
7
ts/types/VerificationTransport.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export enum VerificationTransport {
|
||||||
|
SMS = 'SMS',
|
||||||
|
Voice = 'Voice',
|
||||||
|
}
|
|
@ -3379,10 +3379,10 @@
|
||||||
node-gyp-build "^4.2.3"
|
node-gyp-build "^4.2.3"
|
||||||
uuid "^8.3.0"
|
uuid "^8.3.0"
|
||||||
|
|
||||||
"@signalapp/mock-server@4.0.1":
|
"@signalapp/mock-server@4.1.0":
|
||||||
version "4.0.1"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-4.0.1.tgz#099d1c42ca945b25fb9fbd5642b17969ab0b0256"
|
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-4.1.0.tgz#2a96981a3e375df0cbab37476fa448b99df3d143"
|
||||||
integrity sha512-XuU9AGHR6D/aGenlSbs0s5MQve9FLs4S2h437CDJtTdtZao6tHuPYJz701m8CFcpLAujTYNV/BsWnvVgfcvvlQ==
|
integrity sha512-sVcw384ZjkymsQ4f8GSgUTaF3IIhaMBIYqW76Trzf0U46Uw8gD3hhGjBSBb5GAJQWgJKcAusirXhx/D5mF8z3Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@signalapp/libsignal-client" "^0.30.2"
|
"@signalapp/libsignal-client" "^0.30.2"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
|
|
Loading…
Add table
Reference in a new issue