2023-11-03 22:05:11 +00:00
|
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2024-01-10 01:20:54 +00:00
|
|
|
import pTimeout from 'p-timeout';
|
2024-09-24 22:09:05 +00:00
|
|
|
import { usernames } from '@signalapp/libsignal-client';
|
2024-01-10 01:20:54 +00:00
|
|
|
|
2023-11-03 22:05:11 +00:00
|
|
|
import * as Errors from '../types/errors';
|
2024-09-24 22:09:05 +00:00
|
|
|
import { strictAssert } from '../util/assert';
|
2024-03-20 23:54:34 +00:00
|
|
|
import { isDone as isRegistrationDone } from '../util/registration';
|
2024-01-02 19:36:49 +00:00
|
|
|
import { getConversation } from '../util/getConversation';
|
2024-01-10 01:20:54 +00:00
|
|
|
import { MINUTE, DAY } from '../util/durations';
|
2023-11-03 22:05:11 +00:00
|
|
|
import { drop } from '../util/drop';
|
2024-01-10 01:20:54 +00:00
|
|
|
import { explodePromise } from '../util/explodePromise';
|
2023-11-03 22:05:11 +00:00
|
|
|
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
|
2023-11-07 23:49:38 +00:00
|
|
|
import { storageJobQueue } from '../util/JobQueue';
|
2024-01-02 19:36:49 +00:00
|
|
|
import { getProfile } from '../util/getProfile';
|
|
|
|
import { isSharingPhoneNumberWithEverybody } from '../util/phoneNumberSharingMode';
|
2024-09-24 22:09:05 +00:00
|
|
|
import { bytesToUuid } from '../util/uuidToBytes';
|
2023-11-03 22:05:11 +00:00
|
|
|
import * as log from '../logging/log';
|
2024-01-02 19:36:49 +00:00
|
|
|
import { runStorageServiceSyncJob } from './storage';
|
|
|
|
import { writeProfile } from './writeProfile';
|
2023-11-03 22:05:11 +00:00
|
|
|
|
|
|
|
const CHECK_INTERVAL = DAY;
|
|
|
|
|
2024-01-10 01:20:54 +00:00
|
|
|
const STORAGE_SERVICE_TIMEOUT = 30 * MINUTE;
|
|
|
|
|
2023-11-03 22:05:11 +00:00
|
|
|
class UsernameIntegrityService {
|
|
|
|
private isStarted = false;
|
|
|
|
private readonly backOff = new BackOff(FIBONACCI_TIMEOUTS);
|
|
|
|
|
|
|
|
async start(): Promise<void> {
|
|
|
|
if (this.isStarted) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.isStarted = true;
|
|
|
|
|
|
|
|
this.scheduleCheck();
|
|
|
|
}
|
|
|
|
|
|
|
|
private scheduleCheck(): void {
|
|
|
|
const lastCheckTimestamp = window.storage.get(
|
|
|
|
'usernameLastIntegrityCheck',
|
|
|
|
0
|
|
|
|
);
|
|
|
|
const delay = Math.max(0, lastCheckTimestamp + CHECK_INTERVAL - Date.now());
|
|
|
|
if (delay === 0) {
|
|
|
|
log.info('usernameIntegrity: running the check immediately');
|
|
|
|
drop(this.safeCheck());
|
|
|
|
} else {
|
|
|
|
log.info(`usernameIntegrity: running the check in ${delay}ms`);
|
|
|
|
setTimeout(() => drop(this.safeCheck()), delay);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async safeCheck(): Promise<void> {
|
|
|
|
try {
|
2023-11-07 23:49:38 +00:00
|
|
|
await storageJobQueue(() => this.check());
|
2023-11-03 22:05:11 +00:00
|
|
|
this.backOff.reset();
|
|
|
|
await window.storage.put('usernameLastIntegrityCheck', Date.now());
|
|
|
|
|
|
|
|
this.scheduleCheck();
|
|
|
|
} catch (error) {
|
|
|
|
const delay = this.backOff.getAndIncrement();
|
|
|
|
log.error(
|
|
|
|
'usernameIntegrity: check failed with ' +
|
|
|
|
`error: ${Errors.toLogFormat(error)} retrying in ${delay}ms`
|
|
|
|
);
|
|
|
|
setTimeout(() => drop(this.safeCheck()), delay);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async check(): Promise<void> {
|
2024-03-20 23:54:34 +00:00
|
|
|
if (!isRegistrationDone()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-01-02 19:36:49 +00:00
|
|
|
await this.checkUsername();
|
|
|
|
await this.checkPhoneNumberSharing();
|
|
|
|
}
|
|
|
|
|
|
|
|
private async checkUsername(): Promise<void> {
|
2023-11-03 22:05:11 +00:00
|
|
|
const me = window.ConversationController.getOurConversationOrThrow();
|
|
|
|
const username = me.get('username');
|
|
|
|
if (!username) {
|
|
|
|
log.info('usernameIntegrity: no username');
|
|
|
|
return;
|
|
|
|
}
|
2024-09-24 22:09:05 +00:00
|
|
|
|
|
|
|
const { server } = window.textsecure;
|
|
|
|
if (!server) {
|
|
|
|
log.info('usernameIntegrity: server interface is not available');
|
2023-11-03 22:05:11 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-09-24 22:09:05 +00:00
|
|
|
strictAssert(window.textsecure.server, 'WebAPI must be available');
|
|
|
|
const { usernameHash: remoteHash, usernameLinkHandle: remoteLink } =
|
|
|
|
await server.whoami();
|
|
|
|
|
|
|
|
let failed = false;
|
|
|
|
|
|
|
|
if (remoteHash !== usernames.hash(username).toString('base64url')) {
|
|
|
|
log.error('usernameIntegrity: remote username mismatch');
|
2023-11-03 22:05:11 +00:00
|
|
|
await window.storage.put('usernameCorrupted', true);
|
|
|
|
failed = true;
|
|
|
|
|
|
|
|
// Intentional fall-through
|
|
|
|
}
|
|
|
|
|
|
|
|
const link = window.storage.get('usernameLink');
|
|
|
|
if (!link) {
|
|
|
|
log.info('usernameIntegrity: no username link');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-09-24 22:09:05 +00:00
|
|
|
if (remoteLink !== bytesToUuid(link.serverId)) {
|
|
|
|
log.error('usernameIntegrity: username link mismatch');
|
2023-11-03 22:05:11 +00:00
|
|
|
await window.storage.put('usernameLinkCorrupted', true);
|
|
|
|
failed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!failed) {
|
|
|
|
log.info('usernameIntegrity: check pass');
|
|
|
|
}
|
|
|
|
}
|
2024-01-02 19:36:49 +00:00
|
|
|
|
|
|
|
private async checkPhoneNumberSharing(): Promise<void> {
|
|
|
|
const me = window.ConversationController.getOurConversationOrThrow();
|
|
|
|
|
|
|
|
await getProfile(me.getServiceId(), me.get('e164'));
|
|
|
|
|
|
|
|
{
|
|
|
|
const localValue = isSharingPhoneNumberWithEverybody();
|
2024-02-07 21:38:43 +00:00
|
|
|
const remoteValue = me.get('sharingPhoneNumber') === true;
|
2024-01-02 19:36:49 +00:00
|
|
|
if (localValue === remoteValue) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
log.warn(
|
|
|
|
'usernameIntegrity: phone number sharing mode conflict, running ' +
|
|
|
|
`storage service sync (local: ${localValue}, remote: ${remoteValue})`
|
|
|
|
);
|
|
|
|
|
|
|
|
runStorageServiceSyncJob();
|
|
|
|
}
|
|
|
|
|
2024-01-10 01:20:54 +00:00
|
|
|
// Since we already run on storage service job queue - don't await the
|
|
|
|
// promise below (otherwise deadlock will happen).
|
|
|
|
drop(this.fixProfile());
|
|
|
|
}
|
|
|
|
|
|
|
|
private async fixProfile(): Promise<void> {
|
|
|
|
const { promise: once, resolve } = explodePromise<void>();
|
|
|
|
|
|
|
|
window.Whisper.events.once('storageService:syncComplete', () => resolve());
|
|
|
|
|
|
|
|
await pTimeout(once, STORAGE_SERVICE_TIMEOUT);
|
|
|
|
|
|
|
|
const me = window.ConversationController.getOurConversationOrThrow();
|
|
|
|
|
|
|
|
{
|
2024-01-02 19:36:49 +00:00
|
|
|
const localValue = isSharingPhoneNumberWithEverybody();
|
2024-02-07 21:38:43 +00:00
|
|
|
const remoteValue = me.get('sharingPhoneNumber') === true;
|
2024-01-02 19:36:49 +00:00
|
|
|
if (localValue === remoteValue) {
|
|
|
|
log.info(
|
|
|
|
'usernameIntegrity: phone number sharing mode conflict resolved by ' +
|
|
|
|
'storage service sync'
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
2024-01-10 01:20:54 +00:00
|
|
|
}
|
2024-01-02 19:36:49 +00:00
|
|
|
|
2024-01-10 01:20:54 +00:00
|
|
|
log.warn(
|
|
|
|
'usernameIntegrity: phone number sharing mode conflict not resolved, ' +
|
|
|
|
'updating profile'
|
|
|
|
);
|
2024-01-02 19:36:49 +00:00
|
|
|
|
2024-01-10 01:20:54 +00:00
|
|
|
await writeProfile(getConversation(me), {
|
2024-02-06 02:29:45 +00:00
|
|
|
keepAvatar: true,
|
2024-01-02 19:36:49 +00:00
|
|
|
});
|
2024-01-10 01:20:54 +00:00
|
|
|
|
|
|
|
log.warn('usernameIntegrity: updated profile');
|
2024-01-02 19:36:49 +00:00
|
|
|
}
|
2023-11-03 22:05:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const usernameIntegrity = new UsernameIntegrityService();
|