2023-11-03 22:05:11 +00:00
|
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
import * as Errors from '../types/errors';
|
|
|
|
import { DAY } from '../util/durations';
|
|
|
|
import { drop } from '../util/drop';
|
|
|
|
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
|
|
|
|
import { checkForUsername } from '../util/lookupConversationWithoutServiceId';
|
2023-11-07 23:49:38 +00:00
|
|
|
import { storageJobQueue } from '../util/JobQueue';
|
2023-11-03 22:05:11 +00:00
|
|
|
import * as log from '../logging/log';
|
|
|
|
import { resolveUsernameByLink } from './username';
|
|
|
|
|
|
|
|
const CHECK_INTERVAL = DAY;
|
|
|
|
|
|
|
|
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> {
|
|
|
|
const me = window.ConversationController.getOurConversationOrThrow();
|
|
|
|
const username = me.get('username');
|
|
|
|
const aci = me.getAci();
|
|
|
|
|
|
|
|
let failed = false;
|
|
|
|
|
|
|
|
if (!username) {
|
|
|
|
log.info('usernameIntegrity: no username');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!aci) {
|
|
|
|
log.info('usernameIntegrity: no aci');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = await checkForUsername(username);
|
|
|
|
if (result?.aci !== aci) {
|
|
|
|
log.error('usernameIntegrity: no remote username');
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
const linkUsername = await resolveUsernameByLink(link);
|
|
|
|
if (linkUsername !== username) {
|
|
|
|
log.error('usernameIntegrity: invalid username link');
|
|
|
|
await window.storage.put('usernameLinkCorrupted', true);
|
|
|
|
failed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!failed) {
|
|
|
|
log.info('usernameIntegrity: check pass');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const usernameIntegrity = new UsernameIntegrityService();
|