diff --git a/src/api/gramjs/worker/connector.ts b/src/api/gramjs/worker/connector.ts index 94cc9ba92..056413383 100644 --- a/src/api/gramjs/worker/connector.ts +++ b/src/api/gramjs/worker/connector.ts @@ -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(); @@ -138,6 +139,10 @@ export function setShouldEnableDebugLog(value: boolean) { */ export function callApiLocal(fnName: T, ...args: MethodArgs) { if (!isInited) { + if (NO_QUEUE_BEFORE_INIT.has(fnName)) { + return Promise.resolve(undefined) as MethodResponse; + } + const deferred = new Deferred(); localApiRequestsQueue.push({ fnName, args, deferred }); @@ -178,6 +183,10 @@ export function callApiLocal(fnName: T, ...args: Method export function callApi(fnName: T, ...args: MethodArgs) { if (!isInited && isMasterTab) { + if (NO_QUEUE_BEFORE_INIT.has(fnName)) { + return Promise.resolve(undefined) as MethodResponse; + } + const deferred = new Deferred(); apiRequestsQueue.push({ fnName, args, deferred }); diff --git a/src/components/main/LockScreen.tsx b/src/components/main/LockScreen.tsx index 5fc3fd0d3..9a34bc345 100644 --- a/src/components/main/LockScreen.tsx +++ b/src/components/main/LockScreen.tsx @@ -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 = ({ } setValidationError(''); - decryptSession(passcode).then(unlockScreen, () => { + decryptSession(passcode).then(unlockScreen, (err) => { + if (err instanceof UnrecoverablePasscodeError) { + signOut({ forceInitApi: true }); + } + logInvalidUnlockAttempt(); setValidationError(lang('lng_passcode_wrong')); }); diff --git a/src/config.ts b/src/config.ts index e1000c5aa..efdb358c1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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'; diff --git a/src/global/actions/api/initial.ts b/src/global/actions/api/initial.ts index 83907591c..a674ca1d8 100644 --- a/src/global/actions/api/initial.ts +++ b/src/global/actions/api/initial.ts @@ -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 => { 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++) { diff --git a/src/global/actions/ui/passcode.ts b/src/global/actions/ui/passcode.ts index 470f4dbd5..f55b4913a 100644 --- a/src/global/actions/ui/passcode.ts +++ b/src/global/actions/ui/passcode.ts @@ -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 diff --git a/src/global/types.ts b/src/global/types.ts index 763b0406d..9875b49b9 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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; diff --git a/src/util/browser/idb.ts b/src/util/browser/idb.ts index 283dc89ee..7f4c99e1d 100644 --- a/src/util/browser/idb.ts +++ b/src/util/browser/idb.ts @@ -66,3 +66,4 @@ class IdbStore { } export const MAIN_IDB_STORE = new IdbStore('tt-data'); +export const PASSCODE_IDB_STORE = new IdbStore('tt-passcode'); diff --git a/src/util/passcode.ts b/src/util/passcode.ts index b497da839..9289dfedf 100644 --- a/src/util/passcode.ts +++ b/src/util/passcode.ts @@ -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(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) {