Atomic linking

This commit is contained in:
Fedor Indutny 2023-08-29 02:41:32 +02:00 committed by GitHub
parent cbd16b90bb
commit ccb5eb0dd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 735 additions and 383 deletions

View file

@ -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",

View file

@ -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;

View file

@ -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,

View file

@ -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),

View file

@ -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);
}); });
}); });

View file

@ -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`, () => {

View file

@ -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,
}; };
} }

View file

@ -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({

View file

@ -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)

View file

@ -0,0 +1,7 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export enum VerificationTransport {
SMS = 'SMS',
Voice = 'Voice',
}

View file

@ -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"