Passcode: Use more persistent storage (#4761)
This commit is contained in:
parent
aad2ed366d
commit
ae1a6da2ec
@ -23,6 +23,7 @@ type RequestStates = {
|
|||||||
|
|
||||||
const HEALTH_CHECK_TIMEOUT = 150;
|
const HEALTH_CHECK_TIMEOUT = 150;
|
||||||
const HEALTH_CHECK_MIN_DELAY = 5 * 1000; // 5 sec
|
const HEALTH_CHECK_MIN_DELAY = 5 * 1000; // 5 sec
|
||||||
|
const NO_QUEUE_BEFORE_INIT = new Set(['destroy']);
|
||||||
|
|
||||||
let worker: Worker | undefined;
|
let worker: Worker | undefined;
|
||||||
const requestStates = new Map<string, RequestStates>();
|
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>) {
|
export function callApiLocal<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
|
||||||
if (!isInited) {
|
if (!isInited) {
|
||||||
|
if (NO_QUEUE_BEFORE_INIT.has(fnName)) {
|
||||||
|
return Promise.resolve(undefined) as MethodResponse<T>;
|
||||||
|
}
|
||||||
|
|
||||||
const deferred = new Deferred();
|
const deferred = new Deferred();
|
||||||
localApiRequestsQueue.push({ fnName, args, 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>) {
|
export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>) {
|
||||||
if (!isInited && isMasterTab) {
|
if (!isInited && isMasterTab) {
|
||||||
|
if (NO_QUEUE_BEFORE_INIT.has(fnName)) {
|
||||||
|
return Promise.resolve(undefined) as MethodResponse<T>;
|
||||||
|
}
|
||||||
|
|
||||||
const deferred = new Deferred();
|
const deferred = new Deferred();
|
||||||
apiRequestsQueue.push({ fnName, args, deferred });
|
apiRequestsQueue.push({ fnName, args, deferred });
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { getActions, withGlobal } from '../../global';
|
|||||||
|
|
||||||
import type { GlobalState } from '../../global/types';
|
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 { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets';
|
||||||
|
|
||||||
import useTimeout from '../../hooks/schedulers/useTimeout';
|
import useTimeout from '../../hooks/schedulers/useTimeout';
|
||||||
@ -70,7 +70,11 @@ const LockScreen: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setValidationError('');
|
setValidationError('');
|
||||||
decryptSession(passcode).then(unlockScreen, () => {
|
decryptSession(passcode).then(unlockScreen, (err) => {
|
||||||
|
if (err instanceof UnrecoverablePasscodeError) {
|
||||||
|
signOut({ forceInitApi: true });
|
||||||
|
}
|
||||||
|
|
||||||
logInvalidUnlockAttempt();
|
logInvalidUnlockAttempt();
|
||||||
setValidationError(lang('lng_passcode_wrong'));
|
setValidationError(lang('lng_passcode_wrong'));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const INACTIVE_MARKER = '[Inactive]';
|
|||||||
export const DEBUG_PAYMENT_SMART_GLOCAL = false;
|
export const DEBUG_PAYMENT_SMART_GLOCAL = false;
|
||||||
|
|
||||||
export const SESSION_USER_KEY = 'user_auth';
|
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_DISABLED = false;
|
||||||
export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state';
|
export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state';
|
||||||
|
|||||||
@ -10,12 +10,14 @@ import {
|
|||||||
MEDIA_PROGRESSIVE_CACHE_NAME,
|
MEDIA_PROGRESSIVE_CACHE_NAME,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { updateAppBadge } from '../../../util/appBadge';
|
import { updateAppBadge } from '../../../util/appBadge';
|
||||||
|
import { MAIN_IDB_STORE, PASSCODE_IDB_STORE } from '../../../util/browser/idb';
|
||||||
import * as cacheApi from '../../../util/cacheApi';
|
import * as cacheApi from '../../../util/cacheApi';
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { unsubscribe } from '../../../util/notifications';
|
import { unsubscribe } from '../../../util/notifications';
|
||||||
import { clearEncryptedSession, encryptSession, forgetPasscode } from '../../../util/passcode';
|
import { clearEncryptedSession, encryptSession, forgetPasscode } from '../../../util/passcode';
|
||||||
import { parseInitialLocationHash, resetInitialLocationHash, resetLocationHash } from '../../../util/routing';
|
import { parseInitialLocationHash, resetInitialLocationHash, resetLocationHash } from '../../../util/routing';
|
||||||
|
import { pause } from '../../../util/schedulers';
|
||||||
import {
|
import {
|
||||||
clearStoredSession,
|
clearStoredSession,
|
||||||
loadStoredSession,
|
loadStoredSession,
|
||||||
@ -166,7 +168,7 @@ addActionHandler('signOut', async (global, actions, payload): Promise<void> => {
|
|||||||
resetInitialLocationHash();
|
resetInitialLocationHash();
|
||||||
resetLocationHash();
|
resetLocationHash();
|
||||||
await unsubscribe();
|
await unsubscribe();
|
||||||
await callApi('destroy');
|
await Promise.race([callApi('destroy'), pause(3000)]);
|
||||||
await forceWebsync(false);
|
await forceWebsync(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
@ -194,6 +196,9 @@ addActionHandler('reset', (global, actions): ActionReturnType => {
|
|||||||
void cacheApi.clear(MEDIA_PROGRESSIVE_CACHE_NAME);
|
void cacheApi.clear(MEDIA_PROGRESSIVE_CACHE_NAME);
|
||||||
void cacheApi.clear(CUSTOM_BG_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 langCachePrefix = LANG_CACHE_NAME.replace(/\d+$/, '');
|
||||||
const langCacheVersion = Number((LANG_CACHE_NAME.match(/\d+$/) || ['0'])[0]);
|
const langCacheVersion = Number((LANG_CACHE_NAME.match(/\d+$/) || ['0'])[0]);
|
||||||
for (let i = 0; i < langCacheVersion; i++) {
|
for (let i = 0; i < langCacheVersion; i++) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { SettingsScreens } from '../../../types';
|
|||||||
import { getCurrentTabId, signalPasscodeHash } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId, signalPasscodeHash } from '../../../util/establishMultitabRole';
|
||||||
import { cloneDeep } from '../../../util/iteratees';
|
import { cloneDeep } from '../../../util/iteratees';
|
||||||
import {
|
import {
|
||||||
clearEncryptedSession, decryptSession, encryptSession, forgetPasscode, setupPasscode,
|
clearEncryptedSession, encryptSession, forgetPasscode, setupPasscode,
|
||||||
} from '../../../util/passcode';
|
} from '../../../util/passcode';
|
||||||
import { onBeforeUnload } from '../../../util/schedulers';
|
import { onBeforeUnload } from '../../../util/schedulers';
|
||||||
import { clearStoredSession, loadStoredSession, storeSession } from '../../../util/sessions';
|
import { clearStoredSession, loadStoredSession, storeSession } from '../../../util/sessions';
|
||||||
@ -100,13 +100,6 @@ addActionHandler('unlockScreen', (global, actions, payload): ActionReturnType =>
|
|||||||
actions.initApi();
|
actions.initApi();
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('decryptSession', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { passcode } = payload;
|
|
||||||
decryptSession(passcode).then(actions.unlockScreen, () => {
|
|
||||||
actions.logInvalidUnlockAttempt();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const MAX_INVALID_ATTEMPTS = 5;
|
const MAX_INVALID_ATTEMPTS = 5;
|
||||||
const TIMEOUT_RESET_INVALID_ATTEMPTS_MS = 1000 * 15;// 180000; // 3 minutes
|
const TIMEOUT_RESET_INVALID_ATTEMPTS_MS = 1000 * 15;// 180000; // 3 minutes
|
||||||
|
|
||||||
|
|||||||
@ -3060,7 +3060,6 @@ export interface ActionPayloads {
|
|||||||
setPasscode: { passcode: string } & WithTabId;
|
setPasscode: { passcode: string } & WithTabId;
|
||||||
clearPasscode: undefined;
|
clearPasscode: undefined;
|
||||||
lockScreen: undefined;
|
lockScreen: undefined;
|
||||||
decryptSession: { passcode: string };
|
|
||||||
unlockScreen: { sessionJson: string; globalJson: string };
|
unlockScreen: { sessionJson: string; globalJson: string };
|
||||||
softSignIn: undefined;
|
softSignIn: undefined;
|
||||||
logInvalidUnlockAttempt: undefined;
|
logInvalidUnlockAttempt: undefined;
|
||||||
|
|||||||
@ -66,3 +66,4 @@ class IdbStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MAIN_IDB_STORE = new IdbStore('tt-data');
|
export const MAIN_IDB_STORE = new IdbStore('tt-data');
|
||||||
|
export const PASSCODE_IDB_STORE = new IdbStore('tt-passcode');
|
||||||
|
|||||||
@ -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';
|
import * as cacheApi from './cacheApi';
|
||||||
|
|
||||||
const IV_LENGTH = 12;
|
const IV_LENGTH = 12;
|
||||||
@ -6,6 +7,8 @@ const SALT = 'harder better faster stronger';
|
|||||||
|
|
||||||
let currentPasscodeHash: ArrayBuffer | undefined;
|
let currentPasscodeHash: ArrayBuffer | undefined;
|
||||||
|
|
||||||
|
export class UnrecoverablePasscodeError extends Error {}
|
||||||
|
|
||||||
export function getPasscodeHash() {
|
export function getPasscodeHash() {
|
||||||
return currentPasscodeHash;
|
return currentPasscodeHash;
|
||||||
}
|
}
|
||||||
@ -84,7 +87,7 @@ export async function decryptSession(passcode: string) {
|
|||||||
if (!sessionEncrypted || !globalEncrypted) {
|
if (!sessionEncrypted || !globalEncrypted) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('[api/passcode] Missing required stored fields');
|
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 {
|
try {
|
||||||
@ -109,24 +112,27 @@ export function forgetPasscode() {
|
|||||||
|
|
||||||
export function clearEncryptedSession() {
|
export function clearEncryptedSession() {
|
||||||
forgetPasscode();
|
forgetPasscode();
|
||||||
return cacheApi.clear(PASSCODE_CACHE_NAME);
|
PASSCODE_IDB_STORE.clear();
|
||||||
|
return cacheApi.clear(LEGACY_PASSCODE_CACHE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sha256(plaintext: string) {
|
function sha256(plaintext: string) {
|
||||||
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${plaintext}${SALT}`));
|
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${plaintext}${SALT}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function store(key: string, value: ArrayBuffer) {
|
function store(key: string, value: ArrayBuffer) {
|
||||||
const isSuccessful = await cacheApi.save(PASSCODE_CACHE_NAME, key, value);
|
const asArray = Array.from(new Uint8Array(value));
|
||||||
if (isSuccessful) {
|
PASSCODE_IDB_STORE.set(key, asArray);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Failed to save to cache');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(key: string) {
|
async function load(key: string) {
|
||||||
return cacheApi.fetch(PASSCODE_CACHE_NAME, key, cacheApi.Type.ArrayBuffer);
|
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) {
|
async function aesEncrypt(plaintext: string, pwHash: ArrayBuffer) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user