Add PNI endpoints to WebAPI

This commit is contained in:
Fedor Indutny 2021-11-30 20:33:51 +01:00 committed by GitHub
parent 7c1ce3366d
commit 348012ef4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 321 additions and 210 deletions

View file

@ -222,10 +222,6 @@
></script>
<script type="text/javascript" src="js/wall_clock_listener.js"></script>
<script
type="text/javascript"
src="js/rotate_signed_prekey_listener.js"
></script>
</head>
<body class="overflow-hidden">
<div id="app-container">

View file

@ -1,95 +0,0 @@
// Copyright 2017-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global Whisper, storage, getAccountManager */
// eslint-disable-next-line func-names
(function () {
window.Whisper = window.Whisper || {};
const ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
let timeout;
function scheduleNextRotation() {
const now = Date.now();
const nextTime = now + ROTATION_INTERVAL;
storage.put('nextSignedKeyRotationTime', nextTime);
}
function scheduleRotationForNow() {
const now = Date.now();
storage.put('nextSignedKeyRotationTime', now);
}
async function run() {
window.SignalContext.log.info('Rotating signed prekey...');
try {
await getAccountManager().rotateSignedPreKey();
scheduleNextRotation();
setTimeoutForNextRun();
} catch (error) {
window.SignalContext.log.error(
'rotateSignedPrekey() failed. Trying again in five minutes'
);
setTimeout(setTimeoutForNextRun, 5 * 60 * 1000);
}
}
function runWhenOnline() {
if (navigator.onLine) {
run();
} else {
window.SignalContext.log.info(
'We are offline; keys will be rotated when we are next online'
);
const listener = () => {
window.removeEventListener('online', listener);
setTimeoutForNextRun();
};
window.addEventListener('online', listener);
}
}
function setTimeoutForNextRun() {
const now = Date.now();
const time = storage.get('nextSignedKeyRotationTime', now);
window.SignalContext.log.info(
'Next signed key rotation scheduled for',
new Date(time).toISOString()
);
let waitTime = time - now;
if (waitTime < 0) {
waitTime = 0;
}
clearTimeout(timeout);
timeout = setTimeout(runWhenOnline, waitTime);
}
let initComplete;
Whisper.RotateSignedPreKeyListener = {
init(events, newVersion) {
if (initComplete) {
window.SignalContext.log.info(
'Rotate signed prekey listener: Already initialized'
);
return;
}
initComplete = true;
if (newVersion) {
scheduleRotationForNow();
setTimeoutForNextRun();
} else {
setTimeoutForNextRun();
}
events.on('timetravel', () => {
if (window.Signal.Util.Registration.isDone()) {
setTimeoutForNextRun();
}
});
},
};
})();

View file

@ -14,6 +14,7 @@ try {
const _ = require('lodash');
const { strictAssert } = require('./ts/util/assert');
const { parseIntWithFallback } = require('./ts/util/parseIntWithFallback');
const { UUIDKind } = require('./ts/types/UUID');
// It is important to call this as early as possible
const { SignalContext } = require('./ts/windows/context');
@ -185,7 +186,7 @@ try {
}
const ourUuid = window.textsecure.storage.user.getUuid();
const ourPni = window.textsecure.storage.user.getPni();
const ourPni = window.textsecure.storage.user.getUuid(UUIDKind.PNI);
event.sender.send('additional-log-data-response', {
capabilities: ourCapabilities || {},

View file

@ -375,7 +375,7 @@ export class SignalProtocolStore extends EventsMixin {
const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`;
try {
this.trigger('removePreKey');
this.trigger('removePreKey', ourUuid);
} catch (error) {
log.error(
'removePreKey error triggering removePreKey:',

View file

@ -83,6 +83,7 @@ import type {
import { VerifiedEvent } from './textsecure/messageReceiverEvents';
import type { WebAPIType } from './textsecure/WebAPI';
import * as KeyChangeListener from './textsecure/KeyChangeListener';
import { RotateSignedPreKeyListener } from './textsecure/RotateSignedPreKeyListener';
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
import { getSendOptions } from './util/getSendOptions';
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
@ -116,6 +117,8 @@ import { onRetryRequest, onDecryptionError } from './util/handleRetry';
import { themeChanged } from './shims/themeChanged';
import { createIPCEvents } from './util/createIPCEvents';
import { RemoveAllConfiguration } from './types/RemoveAllConfiguration';
import type { UUID } from './types/UUID';
import { UUIDKind } from './types/UUID';
import * as log from './logging/log';
import {
loadRecentEmojis,
@ -498,8 +501,9 @@ export async function startApp(): Promise<void> {
window.document.title = window.getTitle();
KeyChangeListener.init(window.textsecure.storage.protocol);
window.textsecure.storage.protocol.on('removePreKey', () => {
window.getAccountManager().refreshPreKeys();
window.textsecure.storage.protocol.on('removePreKey', (ourUuid: UUID) => {
const uuidKind = window.textsecure.storage.user.getOurUuidKind(ourUuid);
window.getAccountManager().refreshPreKeys(uuidKind);
});
window.getSocketStatus = () => {
@ -2153,6 +2157,18 @@ export async function startApp(): Promise<void> {
return unlinkAndDisconnect(RemoveAllConfiguration.Full);
}
if (!window.textsecure.storage.user.getUuid(UUIDKind.PNI)) {
log.info('PNI not captured during registration, fetching');
const { pni } = await server.whoami();
if (!pni) {
log.error('No PNI found, unlinking');
return unlinkAndDisconnect(RemoveAllConfiguration.Soft);
}
log.info('Setting PNI to', pni);
await window.textsecure.storage.user.setPni(pni);
}
if (connectCount === 1) {
try {
// Note: we always have to register our capabilities all at once, so we do this
@ -2321,10 +2337,7 @@ export async function startApp(): Promise<void> {
window.readyForUpdates();
// Start listeners here, after we get through our queue.
window.Whisper.RotateSignedPreKeyListener.init(
window.Whisper.events,
newVersion
);
RotateSignedPreKeyListener.init(window.Whisper.events, newVersion);
// Go back to main process before processing delayed actions
await window.Signal.Data.goBackToMainProcess();

View file

@ -40,7 +40,7 @@ import { SendMessageProtoError } from '../textsecure/Errors';
import * as expirationTimer from '../util/expirationTimer';
import type { ReactionType } from '../types/Reactions';
import { UUID } from '../types/UUID';
import { UUID, UUIDKind } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import * as reactionUtil from '../reactions/util';
import {
@ -1470,7 +1470,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
});
if (hadSignedPreKeyRotationError) {
promises.push(window.getAccountManager().rotateSignedPreKey());
promises.push(
window.getAccountManager().rotateSignedPreKey(UUIDKind.ACI)
);
}
attributesToUpdate.sendStateByConversationId = sendStateByConversationId;

View file

@ -10,6 +10,7 @@ import type { GroupCredentialType } from '../textsecure/WebAPI';
import * as durations from '../util/durations';
import { BackOff } from '../util/BackOff';
import { sleep } from '../util/sleep';
import { UUIDKind } from '../types/UUID';
import * as log from '../logging/log';
export const GROUP_CREDENTIALS_KEY = 'groupCredentials';
@ -142,7 +143,7 @@ export async function maybeFetchNewCredentials(): Promise<void> {
serverPublicParamsBase64
);
const newCredentials = sortCredentials(
await accountManager.getGroupCredentials(startDay, endDay)
await accountManager.getGroupCredentials(startDay, endDay, UUIDKind.ACI)
).map((item: GroupCredentialType) => {
const authCredential = clientZKAuthOperations.receiveAuthCredential(
uuid,

View file

@ -3,7 +3,7 @@
import type { ThunkAction } from 'redux-thunk';
import type { StateType as RootStateType } from '../reducer';
import { getUserLanguages } from '../../util/userLanguages';
import { UUID } from '../../types/UUID';
import type { NoopActionType } from './noop';
@ -51,13 +51,9 @@ function checkForAccount(
let hasAccount = false;
try {
await window.textsecure.messaging.getProfile(identifier, {
userLanguages: getUserLanguages(
navigator.languages,
window.getLocale()
),
});
hasAccount = true;
hasAccount = await window.textsecure.messaging.checkAccountExistence(
new UUID(identifier)
);
} catch (_error) {
// Doing nothing with this failed fetch
}

View file

@ -18,6 +18,7 @@ describe('Conversations', () => {
it('updates lastMessage even in race conditions with db', async () => {
const ourNumber = '+15550000000';
const ourUuid = UUID.generate().toString();
const ourPni = UUID.generate().toString();
// Creating a fake conversation
const conversation = new window.Whisper.Conversation({
@ -39,6 +40,7 @@ describe('Conversations', () => {
await window.textsecure.storage.user.setCredentials({
number: ourNumber,
uuid: ourUuid,
pni: ourPni,
deviceId: 2,
deviceName: 'my device',
password: 'password',

View file

@ -9,7 +9,7 @@ import { generateKeyPair } from '../../Curve';
import type { GeneratedKeysType } from '../../textsecure/AccountManager';
import AccountManager from '../../textsecure/AccountManager';
import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d';
import { UUID } from '../../types/UUID';
import { UUID, UUIDKind } from '../../types/UUID';
const { textsecure } = window;
@ -91,7 +91,7 @@ describe('Key generation', function thisNeeded() {
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
result = await accountManager.generateKeys(count, UUIDKind.ACI);
});
for (let i = 1; i <= count; i += 1) {
@ -125,7 +125,7 @@ describe('Key generation', function thisNeeded() {
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
result = await accountManager.generateKeys(count, UUIDKind.ACI);
});
for (let i = 1; i <= 2 * count; i += 1) {
@ -159,7 +159,7 @@ describe('Key generation', function thisNeeded() {
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
result = await accountManager.generateKeys(count, UUIDKind.ACI);
});
for (let i = 1; i <= 3 * count; i += 1) {

View file

@ -30,7 +30,8 @@ import {
generateSignedPreKey,
generatePreKey,
} from '../Curve';
import { UUID } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import { UUID, UUIDKind } from '../types/UUID';
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
import { ourProfileKeyService } from '../services/ourProfileKey';
import { assert, strictAssert } from '../util/assert';
@ -170,8 +171,12 @@ export default class AccountManager extends EventTarget {
);
await this.clearSessionsAndPreKeys();
const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE);
await this.server.registerKeys(keys);
// TODO: DESKTOP-2788
const keys = await this.generateKeys(
SIGNED_KEY_GEN_BATCH_SIZE,
UUIDKind.ACI
);
await this.server.registerKeys(keys, UUIDKind.ACI);
await this.confirmKeys(keys);
await this.registrationDone();
});
@ -184,11 +189,6 @@ export default class AccountManager extends EventTarget {
) {
const createAccount = this.createAccount.bind(this);
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
const generateKeys = this.generateKeys.bind(
this,
SIGNED_KEY_GEN_BATCH_SIZE,
progressCallback
);
const provisioningCipher = new ProvisioningCipher();
const pubKey = await provisioningCipher.getPublicKey();
@ -274,37 +274,40 @@ export default class AccountManager extends EventTarget {
deviceName,
provisionMessage.userAgent,
provisionMessage.readReceipts,
{ uuid: provisionMessage.uuid }
{
uuid: provisionMessage.uuid
? UUID.cast(provisionMessage.uuid)
: undefined,
}
);
await clearSessionsAndPreKeys();
const keys = await generateKeys();
await this.server.registerKeys(keys);
// TODO: DESKTOP-2794
const keys = await this.generateKeys(
SIGNED_KEY_GEN_BATCH_SIZE,
UUIDKind.ACI,
progressCallback
);
await this.server.registerKeys(keys, UUIDKind.ACI);
await this.confirmKeys(keys);
await this.registrationDone();
});
}
async refreshPreKeys() {
const generateKeys = this.generateKeys.bind(
this,
SIGNED_KEY_GEN_BATCH_SIZE
);
const registerKeys = this.server.registerKeys.bind(this.server);
return this.queueTask(async () =>
this.server.getMyKeys().then(async preKeyCount => {
log.info(`prekey count ${preKeyCount}`);
if (preKeyCount < 10) {
return generateKeys().then(registerKeys);
}
return null;
})
);
}
async rotateSignedPreKey() {
async refreshPreKeys(uuidKind: UUIDKind) {
return this.queueTask(async () => {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const preKeyCount = await this.server.getMyKeys(uuidKind);
log.info(`prekey count ${preKeyCount}`);
if (preKeyCount >= 10) {
return;
}
const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE, uuidKind);
await this.server.registerKeys(keys, uuidKind);
});
}
async rotateSignedPreKey(uuidKind: UUIDKind) {
return this.queueTask(async () => {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
if (typeof signedKeyId !== 'number') {
throw new Error('Invalid signedKeyId');
@ -350,11 +353,14 @@ export default class AccountManager extends EventTarget {
return Promise.all([
window.textsecure.storage.put('signedKeyId', signedKeyId + 1),
store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair),
server.setSignedPreKey({
server.setSignedPreKey(
{
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
signature: res.signature,
}),
},
uuidKind
),
])
.then(async () => {
const confirmed = true;
@ -456,7 +462,7 @@ export default class AccountManager extends EventTarget {
deviceName: string | null,
userAgent?: string | null,
readReceipts?: boolean | null,
options: { accessKey?: Uint8Array; uuid?: string } = {}
options: { accessKey?: Uint8Array; uuid?: UUIDStringType } = {}
): Promise<void> {
const { storage } = window.textsecure;
const { accessKey, uuid } = options;
@ -548,6 +554,7 @@ export default class AccountManager extends EventTarget {
// information.
await storage.user.setCredentials({
uuid: ourUuid,
pni: response.pni,
number,
deviceId: response.deviceId ?? 1,
deviceName: deviceName ?? undefined,
@ -617,8 +624,12 @@ export default class AccountManager extends EventTarget {
]);
}
async getGroupCredentials(startDay: number, endDay: number) {
return this.server.getGroupCredentials(startDay, endDay);
async getGroupCredentials(
startDay: number,
endDay: number,
uuidKind: UUIDKind
) {
return this.server.getGroupCredentials(startDay, endDay, uuidKind);
}
// Takes the same object returned by generateKeys
@ -636,14 +647,18 @@ export default class AccountManager extends EventTarget {
await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed);
}
async generateKeys(count: number, providedProgressCallback?: Function) {
async generateKeys(
count: number,
uuidKind: UUIDKind,
providedProgressCallback?: Function
) {
const progressCallback =
typeof providedProgressCallback === 'function'
? providedProgressCallback
: null;
const startId = window.textsecure.storage.get('maxPreKeyId', 1);
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourUuid = window.textsecure.storage.user.getCheckedUuid(uuidKind);
if (typeof startId !== 'number') {
throw new Error('Invalid maxPreKeyId');

View file

@ -0,0 +1,104 @@
// Copyright 2017-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as durations from '../util/durations';
import { UUIDKind } from '../types/UUID';
import * as log from '../logging/log';
const ROTATION_INTERVAL = 2 * durations.DAY;
export type MinimalEventsType = {
on(event: 'timetravel', callback: () => void): void;
};
let initComplete = false;
export class RotateSignedPreKeyListener {
public timeout: NodeJS.Timeout | undefined;
protected scheduleRotationForNow(): void {
const now = Date.now();
window.textsecure.storage.put('nextSignedKeyRotationTime', now);
}
protected setTimeoutForNextRun(): void {
const now = Date.now();
const time = window.textsecure.storage.get(
'nextSignedKeyRotationTime',
now
);
log.info(
'Next signed key rotation scheduled for',
new Date(time).toISOString()
);
let waitTime = time - now;
if (waitTime < 0) {
waitTime = 0;
}
if (this.timeout !== undefined) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
}
private scheduleNextRotation(): void {
const now = Date.now();
const nextTime = now + ROTATION_INTERVAL;
window.textsecure.storage.put('nextSignedKeyRotationTime', nextTime);
}
private async run(): Promise<void> {
log.info('Rotating signed prekey...');
try {
const accountManager = window.getAccountManager();
await Promise.all([
accountManager.rotateSignedPreKey(UUIDKind.ACI),
accountManager.rotateSignedPreKey(UUIDKind.PNI),
]);
this.scheduleNextRotation();
this.setTimeoutForNextRun();
} catch (error) {
log.error('rotateSignedPrekey() failed. Trying again in five minutes');
setTimeout(() => this.setTimeoutForNextRun(), 5 * durations.MINUTE);
}
}
private runWhenOnline() {
if (window.navigator.onLine) {
this.run();
} else {
log.info('We are offline; keys will be rotated when we are next online');
const listener = () => {
window.removeEventListener('online', listener);
this.setTimeoutForNextRun();
};
window.addEventListener('online', listener);
}
}
public static init(events: MinimalEventsType, newVersion: boolean): void {
if (initComplete) {
window.SignalContext.log.info(
'Rotate signed prekey listener: Already initialized'
);
return;
}
initComplete = true;
const listener = new RotateSignedPreKeyListener();
if (newVersion) {
listener.scheduleRotationForNow();
}
listener.setTimeoutForNextRun();
events.on('timetravel', () => {
if (window.Signal.Util.Registration.isDone()) {
listener.setTimeoutForNextRun();
}
});
}
}

View file

@ -23,7 +23,7 @@ import { SenderKeys } from '../LibSignalStores';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { MIMETypeToString } from '../types/MIME';
import type * as Attachment from '../types/Attachment';
import type { UUIDStringType } from '../types/UUID';
import type { UUID, UUIDStringType } from '../types/UUID';
import type {
ChallengeType,
GroupCredentialsType,
@ -2039,7 +2039,7 @@ export default class MessageSender {
number: string,
options: Readonly<{
accessKey?: string;
profileKeyVersion?: string;
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
}>
@ -2057,6 +2057,10 @@ export default class MessageSender {
return this.server.getProfile(number, options);
}
async checkAccountExistence(uuid: UUID): Promise<boolean> {
return this.server.checkAccountExistence(uuid);
}
async getProfileForUsername(
username: string
): ReturnType<WebAPIType['getProfileForUsername']> {

View file

@ -35,7 +35,8 @@ import { toWebSafeBase64 } from '../util/webSafeBase64';
import type { SocketStatus } from '../types/SocketStatus';
import { toLogFormat } from '../types/errors';
import { isPackIdValid, redactPackId } from '../types/Stickers';
import type { UUIDStringType } from '../types/UUID';
import type { UUID, UUIDStringType } from '../types/UUID';
import { UUIDKind } from '../types/UUID';
import * as Bytes from '../Bytes';
import {
constantTimeEqual,
@ -164,7 +165,7 @@ function getContentType(response: Response) {
type FetchHeaderListType = { [name: string]: string };
export type HeaderListType = { [name: string]: string | ReadonlyArray<string> };
type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
type RedactUrl = (url: string) => string;
@ -519,6 +520,7 @@ function makeHTTPError(
const URL_CALLS = {
accounts: 'v1/accounts',
accountExistence: 'v1/accounts/account',
attachmentId: 'v2/attachments/form/upload',
attestation: 'v1/attestation',
challenge: 'v1/challenge',
@ -745,10 +747,17 @@ export type MakeProxiedRequestResultType =
};
export type WhoamiResultType = Readonly<{
uuid?: string;
uuid?: UUIDStringType;
pni?: UUIDStringType;
number?: string;
}>;
export type ConfirmCodeResultType = Readonly<{
uuid: UUIDStringType;
pni: UUIDStringType;
deviceId?: number;
}>;
export type WebAPIType = {
confirmCode: (
number: string,
@ -756,8 +765,8 @@ export type WebAPIType = {
newPassword: string,
registrationId: number,
deviceName?: string | null,
options?: { accessKey?: Uint8Array; uuid?: string }
) => Promise<{ uuid?: string; deviceId?: number }>;
options?: { accessKey?: Uint8Array; uuid?: UUIDStringType }
) => Promise<ConfirmCodeResultType>;
createGroup: (
group: Proto.IGroup,
options: GroupCredentialsType
@ -775,7 +784,8 @@ export type WebAPIType = {
getGroupAvatar: (key: string) => Promise<Uint8Array>;
getGroupCredentials: (
startDay: number,
endDay: number
endDay: number,
uuidKind: UUIDKind
) => Promise<Array<GroupCredentialType>>;
getGroupExternalCredential: (
options: GroupCredentialsType
@ -794,13 +804,14 @@ export type WebAPIType = {
deviceId?: number,
options?: { accessKey?: string }
) => Promise<ServerKeysType>;
getMyKeys: () => Promise<number>;
getMyKeys: (uuidKind: UUIDKind) => Promise<number>;
getProfile: (
identifier: string,
options: {
profileKeyVersion?: string;
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
credentialType?: 'pni' | 'profileKey';
}
) => Promise<ProfileType>;
getProfileForUsername: (username: string) => Promise<ProfileType>;
@ -808,7 +819,7 @@ export type WebAPIType = {
identifier: string,
options: {
accessKey: string;
profileKeyVersion?: string;
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
}
@ -866,11 +877,12 @@ export type WebAPIType = {
) => Promise<string>;
putUsername: (newUsername: string) => Promise<void>;
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
registerKeys: (genKeys: KeysType) => Promise<void>;
registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise<void>;
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
reportMessage: (senderE164: string, serverGuid: string) => Promise<void>;
requestVerificationSMS: (number: string, token: string) => Promise<void>;
requestVerificationVoice: (number: string, token: string) => Promise<void>;
checkAccountExistence: (uuid: UUID) => Promise<boolean>;
sendMessages: (
destination: string,
messageArray: ReadonlyArray<MessageType>,
@ -890,7 +902,10 @@ export type WebAPIType = {
timestamp: number,
online?: boolean
) => Promise<MultiRecipient200ResponseType>;
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
setSignedPreKey: (
signedPreKey: SignedPreKeyType,
uuidKind: UUIDKind
) => Promise<void>;
updateDeviceName: (deviceName: string) => Promise<void>;
uploadAvatar: (
uploadAvatarRequestHeaders: UploadAvatarHeadersType,
@ -1091,6 +1106,7 @@ export function initialize({
unregisterRequestHandler,
authenticate,
logout,
checkAccountExistence,
confirmCode,
createGroup,
deleteUsername,
@ -1180,13 +1196,13 @@ export function initialize({
(param.jsonData ? JSON.stringify(param.jsonData) : undefined),
headers: param.headers,
host: param.host || url,
password: param.password || password,
password: param.password ?? password,
path: URL_CALLS[param.call] + param.urlParameters,
proxyUrl,
responseType: param.responseType,
timeout: param.timeout,
type: param.httpType,
user: param.username || username,
user: param.username ?? username,
redactUrl: param.redactUrl,
serverUrl: url,
validateResponse: param.validateResponse,
@ -1209,6 +1225,18 @@ export function initialize({
}
}
function uuidKindToQuery(kind: UUIDKind): string {
let value: string;
if (kind === UUIDKind.ACI) {
value = 'aci';
} else if (kind === UUIDKind.PNI) {
value = 'pni';
} else {
throw new Error(`Unsupported UUIDKind: ${kind}`);
}
return `identity=${value}`;
}
async function whoami() {
return (await _ajax({
call: 'whoami',
@ -1378,16 +1406,14 @@ export function initialize({
function getProfileUrl(
identifier: string,
profileKeyVersion?: string,
profileKeyCredentialRequest?: string
profileKeyVersion: string,
profileKeyCredentialRequest?: string,
credentialType: 'pni' | 'profileKey' = 'profileKey'
) {
let profileUrl = `/${identifier}`;
let profileUrl = `/${identifier}/${profileKeyVersion}`;
if (profileKeyVersion) {
profileUrl += `/${profileKeyVersion}`;
}
if (profileKeyVersion && profileKeyCredentialRequest) {
profileUrl += `/${profileKeyCredentialRequest}`;
if (profileKeyCredentialRequest) {
profileUrl += `/${profileKeyCredentialRequest}?credentialType=${credentialType}`;
}
return profileUrl;
@ -1396,13 +1422,18 @@ export function initialize({
async function getProfile(
identifier: string,
options: {
profileKeyVersion?: string;
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
credentialType?: 'pni' | 'profileKey';
}
) {
const { profileKeyVersion, profileKeyCredentialRequest, userLanguages } =
options;
const {
profileKeyVersion,
profileKeyCredentialRequest,
userLanguages,
credentialType = 'profileKey',
} = options;
return (await _ajax({
call: 'profile',
@ -1410,7 +1441,8 @@ export function initialize({
urlParameters: getProfileUrl(
identifier,
profileKeyVersion,
profileKeyCredentialRequest
profileKeyCredentialRequest,
credentialType
),
headers: {
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
@ -1425,9 +1457,13 @@ export function initialize({
}
async function getProfileForUsername(usernameToFetch: string) {
return getProfile(`username/${usernameToFetch}`, {
userLanguages: [],
});
return (await _ajax({
call: 'profile',
httpType: 'GET',
urlParameters: `username/${usernameToFetch}`,
responseType: 'json',
redactUrl: _createRedactor(usernameToFetch),
})) as ProfileType;
}
async function putProfile(
@ -1451,7 +1487,7 @@ export function initialize({
identifier: string,
options: {
accessKey: string;
profileKeyVersion?: string;
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
}
@ -1573,13 +1609,32 @@ export function initialize({
});
}
async function checkAccountExistence(uuid: UUID) {
try {
await _ajax({
httpType: 'HEAD',
call: 'accountExistence',
urlParameters: `/${uuid.toString()}`,
unauthenticated: true,
accessKey: undefined,
});
return true;
} catch (error) {
if (error instanceof HTTPError && error.code === 404) {
return false;
}
throw error;
}
}
async function confirmCode(
number: string,
code: string,
newPassword: string,
registrationId: number,
deviceName?: string | null,
options: { accessKey?: Uint8Array; uuid?: string } = {}
options: { accessKey?: Uint8Array; uuid?: UUIDStringType } = {}
) {
const capabilities: CapabilitiesUploadType = {
announcementGroup: true,
@ -1620,7 +1675,7 @@ export function initialize({
responseType: 'json',
urlParameters: urlPrefix + code,
jsonData,
})) as { uuid?: string; deviceId?: number };
})) as ConfirmCodeResultType;
// Set final REST credentials to let `registerKeys` succeed.
username = `${uuid || response.uuid || number}.${response.deviceId || 1}`;
@ -1670,7 +1725,7 @@ export function initialize({
}>;
};
async function registerKeys(genKeys: KeysType) {
async function registerKeys(genKeys: KeysType, uuidKind: UUIDKind) {
const preKeys = genKeys.preKeys.map(key => ({
keyId: key.keyId,
publicKey: Bytes.toBase64(key.publicKey),
@ -1688,14 +1743,19 @@ export function initialize({
await _ajax({
call: 'keys',
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
httpType: 'PUT',
jsonData: keys,
});
}
async function setSignedPreKey(signedPreKey: SignedPreKeyType) {
async function setSignedPreKey(
signedPreKey: SignedPreKeyType,
uuidKind: UUIDKind
) {
await _ajax({
call: 'signed',
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
httpType: 'PUT',
jsonData: {
keyId: signedPreKey.keyId,
@ -1709,9 +1769,10 @@ export function initialize({
count: number;
};
async function getMyKeys(): Promise<number> {
async function getMyKeys(uuidKind: UUIDKind): Promise<number> {
const result = (await _ajax({
call: 'keys',
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
httpType: 'GET',
responseType: 'json',
validateResponse: { count: 'number' },
@ -2233,11 +2294,12 @@ export function initialize({
async function getGroupCredentials(
startDay: number,
endDay: number
endDay: number,
uuidKind: UUIDKind
): Promise<Array<GroupCredentialType>> {
const response = (await _ajax({
call: 'getGroupCredentials',
urlParameters: `/${startDay}/${endDay}`,
urlParameters: `/${startDay}/${endDay}?${uuidKindToQuery(uuidKind)}`,
httpType: 'GET',
responseType: 'json',
})) as CredentialResponseType;

View file

@ -11,7 +11,8 @@ import * as log from '../../logging/log';
import Helpers from '../Helpers';
export type SetCredentialsOptions = {
uuid?: string;
uuid: string;
pni: string;
number: string;
deviceId: number;
deviceName?: string;
@ -58,22 +59,30 @@ export class User {
return Helpers.unencodeNumber(numberId)[0];
}
public getUuid(): UUID | undefined {
public getUuid(uuidKind = UUIDKind.ACI): UUID | undefined {
if (uuidKind === UUIDKind.PNI) {
const pni = this.storage.get('pni');
if (pni === undefined) return undefined;
return new UUID(pni);
}
strictAssert(
uuidKind === UUIDKind.ACI,
`Unsupported uuid kind: ${uuidKind}`
);
const uuid = this.storage.get('uuid_id');
if (uuid === undefined) return undefined;
return new UUID(Helpers.unencodeNumber(uuid.toLowerCase())[0]);
}
public getCheckedUuid(): UUID {
const uuid = this.getUuid();
public getCheckedUuid(uuidKind?: UUIDKind): UUID {
const uuid = this.getUuid(uuidKind);
strictAssert(uuid !== undefined, 'Must have our own uuid');
return uuid;
}
public getPni(): UUID | undefined {
const pni = this.storage.get('pni');
if (pni === undefined) return undefined;
return new UUID(pni);
public async setPni(pni: string): Promise<void> {
await this.storage.put('pni', UUID.cast(pni));
}
public getOurUuidKind(uuid: UUID): UUIDKind {
@ -83,7 +92,7 @@ export class User {
return UUIDKind.ACI;
}
const pni = this.getPni();
const pni = this.getUuid(UUIDKind.PNI);
if (pni?.toString() === uuid.toString()) {
return UUIDKind.PNI;
}
@ -118,12 +127,13 @@ export class User {
public async setCredentials(
credentials: SetCredentialsOptions
): Promise<void> {
const { uuid, number, deviceId, deviceName, password } = credentials;
const { uuid, pni, number, deviceId, deviceName, password } = credentials;
await Promise.all([
this.storage.put('number_id', `${number}.${deviceId}`),
this.storage.put('uuid_id', `${uuid}.${deviceId}`),
this.storage.put('password', password),
this.setPni(pni),
deviceName
? this.storage.put('device_name', deviceName)
: Promise.resolve(),

View file

@ -134,6 +134,7 @@ export type StorageAccessType = {
paymentAddress: string;
zoomFactor: ZoomFactorType;
preferredLeftPaneWidth: number;
nextSignedKeyRotationTime: number;
areWeASubscriber: boolean;
subscriberId: Uint8Array;
subscriberCurrencyCode: string;

1
ts/window.d.ts vendored
View file

@ -548,7 +548,6 @@ export type WhisperType = {
MessageCollection: typeof MessageModelCollectionType;
GroupMemberConversation: WhatIsThis;
RotateSignedPreKeyListener: WhatIsThis;
WallClockListener: WhatIsThis;
deliveryReceiptQueue: PQueue;