Add PNI endpoints to WebAPI
This commit is contained in:
parent
7c1ce3366d
commit
348012ef4c
17 changed files with 321 additions and 210 deletions
|
@ -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">
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -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 || {},
|
||||
|
|
|
@ -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:',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 refreshPreKeys(uuidKind: UUIDKind) {
|
||||
return this.queueTask(async () => {
|
||||
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() {
|
||||
async rotateSignedPreKey(uuidKind: UUIDKind) {
|
||||
return this.queueTask(async () => {
|
||||
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
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({
|
||||
keyId: res.keyId,
|
||||
publicKey: res.keyPair.pubKey,
|
||||
signature: res.signature,
|
||||
}),
|
||||
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');
|
||||
|
|
104
ts/textsecure/RotateSignedPreKeyListener.ts
Normal file
104
ts/textsecure/RotateSignedPreKeyListener.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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']> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -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
1
ts/window.d.ts
vendored
|
@ -548,7 +548,6 @@ export type WhisperType = {
|
|||
MessageCollection: typeof MessageModelCollectionType;
|
||||
|
||||
GroupMemberConversation: WhatIsThis;
|
||||
RotateSignedPreKeyListener: WhatIsThis;
|
||||
WallClockListener: WhatIsThis;
|
||||
|
||||
deliveryReceiptQueue: PQueue;
|
||||
|
|
Loading…
Reference in a new issue