Passcode: Use more persistent storage (#4761)

This commit is contained in:
zubiden 2024-07-15 15:52:51 +02:00 committed by Alexander Zinchuk
parent aad2ed366d
commit ae1a6da2ec
8 changed files with 42 additions and 25 deletions

View File

@ -23,6 +23,7 @@ type RequestStates = {
const HEALTH_CHECK_TIMEOUT = 150;
const HEALTH_CHECK_MIN_DELAY = 5 * 1000; // 5 sec
const NO_QUEUE_BEFORE_INIT = new Set(['destroy']);
let worker: Worker | undefined;
const requestStates = new Map<string, RequestStates>();
@ -138,6 +139,10 @@ export function setShouldEnableDebugLog(value: boolean) {
*/
export function callApiLocal<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
if (!isInited) {
if (NO_QUEUE_BEFORE_INIT.has(fnName)) {
return Promise.resolve(undefined) as MethodResponse<T>;
}
const deferred = new Deferred();
localApiRequestsQueue.push({ fnName, args, deferred });
@ -178,6 +183,10 @@ export function callApiLocal<T extends keyof Methods>(fnName: T, ...args: Method
export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
if (!isInited && isMasterTab) {
if (NO_QUEUE_BEFORE_INIT.has(fnName)) {
return Promise.resolve(undefined) as MethodResponse<T>;
}
const deferred = new Deferred();
apiRequestsQueue.push({ fnName, args, deferred });

View File

@ -6,7 +6,7 @@ import { getActions, withGlobal } from '../../global';
import type { GlobalState } from '../../global/types';
import { decryptSession } from '../../util/passcode';
import { decryptSession, UnrecoverablePasscodeError } from '../../util/passcode';
import { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets';
import useTimeout from '../../hooks/schedulers/useTimeout';
@ -70,7 +70,11 @@ const LockScreen: FC<OwnProps & StateProps> = ({
}
setValidationError('');
decryptSession(passcode).then(unlockScreen, () => {
decryptSession(passcode).then(unlockScreen, (err) => {
if (err instanceof UnrecoverablePasscodeError) {
signOut({ forceInitApi: true });
}
logInvalidUnlockAttempt();
setValidationError(lang('lng_passcode_wrong'));
});

View File

@ -33,7 +33,7 @@ export const INACTIVE_MARKER = '[Inactive]';
export const DEBUG_PAYMENT_SMART_GLOCAL = false;
export const SESSION_USER_KEY = 'user_auth';
export const PASSCODE_CACHE_NAME = 'tt-passcode';
export const LEGACY_PASSCODE_CACHE_NAME = 'tt-passcode';
export const GLOBAL_STATE_CACHE_DISABLED = false;
export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state';

View File

@ -10,12 +10,14 @@ import {
MEDIA_PROGRESSIVE_CACHE_NAME,
} from '../../../config';
import { updateAppBadge } from '../../../util/appBadge';
import { MAIN_IDB_STORE, PASSCODE_IDB_STORE } from '../../../util/browser/idb';
import * as cacheApi from '../../../util/cacheApi';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey } from '../../../util/iteratees';
import { unsubscribe } from '../../../util/notifications';
import { clearEncryptedSession, encryptSession, forgetPasscode } from '../../../util/passcode';
import { parseInitialLocationHash, resetInitialLocationHash, resetLocationHash } from '../../../util/routing';
import { pause } from '../../../util/schedulers';
import {
clearStoredSession,
loadStoredSession,
@ -166,7 +168,7 @@ addActionHandler('signOut', async (global, actions, payload): Promise<void> => {
resetInitialLocationHash();
resetLocationHash();
await unsubscribe();
await callApi('destroy');
await Promise.race([callApi('destroy'), pause(3000)]);
await forceWebsync(false);
} catch (err) {
// Do nothing
@ -194,6 +196,9 @@ addActionHandler('reset', (global, actions): ActionReturnType => {
void cacheApi.clear(MEDIA_PROGRESSIVE_CACHE_NAME);
void cacheApi.clear(CUSTOM_BG_CACHE_NAME);
MAIN_IDB_STORE.clear();
PASSCODE_IDB_STORE.clear();
const langCachePrefix = LANG_CACHE_NAME.replace(/\d+$/, '');
const langCacheVersion = Number((LANG_CACHE_NAME.match(/\d+$/) || ['0'])[0]);
for (let i = 0; i < langCacheVersion; i++) {

View File

@ -4,7 +4,7 @@ import { SettingsScreens } from '../../../types';
import { getCurrentTabId, signalPasscodeHash } from '../../../util/establishMultitabRole';
import { cloneDeep } from '../../../util/iteratees';
import {
clearEncryptedSession, decryptSession, encryptSession, forgetPasscode, setupPasscode,
clearEncryptedSession, encryptSession, forgetPasscode, setupPasscode,
} from '../../../util/passcode';
import { onBeforeUnload } from '../../../util/schedulers';
import { clearStoredSession, loadStoredSession, storeSession } from '../../../util/sessions';
@ -100,13 +100,6 @@ addActionHandler('unlockScreen', (global, actions, payload): ActionReturnType =>
actions.initApi();
});
addActionHandler('decryptSession', (global, actions, payload): ActionReturnType => {
const { passcode } = payload;
decryptSession(passcode).then(actions.unlockScreen, () => {
actions.logInvalidUnlockAttempt();
});
});
const MAX_INVALID_ATTEMPTS = 5;
const TIMEOUT_RESET_INVALID_ATTEMPTS_MS = 1000 * 15;// 180000; // 3 minutes

View File

@ -3060,7 +3060,6 @@ export interface ActionPayloads {
setPasscode: { passcode: string } & WithTabId;
clearPasscode: undefined;
lockScreen: undefined;
decryptSession: { passcode: string };
unlockScreen: { sessionJson: string; globalJson: string };
softSignIn: undefined;
logInvalidUnlockAttempt: undefined;

View File

@ -66,3 +66,4 @@ class IdbStore {
}
export const MAIN_IDB_STORE = new IdbStore('tt-data');
export const PASSCODE_IDB_STORE = new IdbStore('tt-passcode');

View File

@ -1,4 +1,5 @@
import { PASSCODE_CACHE_NAME } from '../config';
import { LEGACY_PASSCODE_CACHE_NAME } from '../config';
import { PASSCODE_IDB_STORE } from './browser/idb';
import * as cacheApi from './cacheApi';
const IV_LENGTH = 12;
@ -6,6 +7,8 @@ const SALT = 'harder better faster stronger';
let currentPasscodeHash: ArrayBuffer | undefined;
export class UnrecoverablePasscodeError extends Error {}
export function getPasscodeHash() {
return currentPasscodeHash;
}
@ -84,7 +87,7 @@ export async function decryptSession(passcode: string) {
if (!sessionEncrypted || !globalEncrypted) {
// eslint-disable-next-line no-console
console.error('[api/passcode] Missing required stored fields');
throw new Error('[api/passcode] Missing required stored fields');
throw new UnrecoverablePasscodeError('[api/passcode] Missing required stored fields');
}
try {
@ -109,24 +112,27 @@ export function forgetPasscode() {
export function clearEncryptedSession() {
forgetPasscode();
return cacheApi.clear(PASSCODE_CACHE_NAME);
PASSCODE_IDB_STORE.clear();
return cacheApi.clear(LEGACY_PASSCODE_CACHE_NAME);
}
function sha256(plaintext: string) {
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${plaintext}${SALT}`));
}
async function store(key: string, value: ArrayBuffer) {
const isSuccessful = await cacheApi.save(PASSCODE_CACHE_NAME, key, value);
if (isSuccessful) {
return;
}
throw new Error('Failed to save to cache');
function store(key: string, value: ArrayBuffer) {
const asArray = Array.from(new Uint8Array(value));
PASSCODE_IDB_STORE.set(key, asArray);
}
function load(key: string) {
return cacheApi.fetch(PASSCODE_CACHE_NAME, key, cacheApi.Type.ArrayBuffer);
async function load(key: string) {
const cached = await PASSCODE_IDB_STORE.get<number[]>(key);
if (cached) {
const asArrayBuffer = new Uint8Array(cached).buffer;
return asArrayBuffer;
}
// Fallback for old data
return cacheApi.fetch(LEGACY_PASSCODE_CACHE_NAME, key, cacheApi.Type.ArrayBuffer);
}
async function aesEncrypt(plaintext: string, pwHash: ArrayBuffer) {