[web] General code improvements (#5926)

This commit is contained in:
Manav Rathi
2025-05-15 20:30:29 +05:30
committed by GitHub
15 changed files with 282 additions and 183 deletions

View File

@@ -54,7 +54,6 @@ import {
import { useBaseContext } from "ente-base/context";
import {
getLocaleInUse,
pt,
setLocaleInUse,
supportedLocales,
ut,
@@ -827,9 +826,7 @@ const Preferences: React.FC<NestedSidebarDrawerVisibilityProps> = ({
</RowButtonGroupTitle>
<RowButtonGroup>
<RowSwitch
label={
/* TODO(HLS): */ pt("Streamable videos")
}
label={t("streamable_videos")}
checked={isHLSGenerationEnabled}
onClick={() => void toggleHLSGeneration()}
/>

View File

@@ -21,6 +21,7 @@ import { Upload, type UploadTypeSelectorIntent } from "components/Upload";
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog";
import { stashRedirect } from "ente-accounts/services/redirect";
import { isSessionInvalid } from "ente-accounts/services/session";
import type { MiniDialogAttributes } from "ente-base/components/MiniDialog";
import { NavbarBase } from "ente-base/components/Navbar";
import { CenteredRow } from "ente-base/components/containers";
@@ -32,6 +33,11 @@ import { useIsSmallWidth } from "ente-base/components/utils/hooks";
import { useModalVisibility } from "ente-base/components/utils/modal";
import { useBaseContext } from "ente-base/context";
import log from "ente-base/log";
import {
clearSessionStorage,
haveCredentialsInSession,
masterKeyFromSessionIfLoggedIn,
} from "ente-base/session";
import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone";
import { type Collection } from "ente-media/collection";
import { type EnteFile } from "ente-media/file";
@@ -56,6 +62,7 @@ import {
import {
constructUserIDToEmailMap,
createShareeSuggestionEmails,
validateKey,
} from "ente-new/photos/components/gallery/helpers";
import {
useGalleryReducer,
@@ -93,8 +100,6 @@ import {
} from "ente-new/photos/services/user-details";
import { usePhotosAppContext } from "ente-new/photos/types/context";
import { FlexWrapper } from "ente-shared/components/Container";
import { getRecoveryKey } from "ente-shared/crypto/helpers";
import { CustomError } from "ente-shared/error";
import { getData } from "ente-shared/storage/localStorage";
import {
getToken,
@@ -103,7 +108,7 @@ import {
setIsFirstLogin,
setJustSignedUp,
} from "ente-shared/storage/localStorage/helpers";
import { clearKeys, getKey } from "ente-shared/storage/sessionStorage";
import { getKey } from "ente-shared/storage/sessionStorage";
import { t } from "i18next";
import { useRouter, type NextRouter } from "next/router";
import { createContext, useCallback, useEffect, useRef, useState } from "react";
@@ -117,7 +122,6 @@ import {
} from "services/collectionService";
import exportService from "services/export";
import { uploadManager } from "services/upload-manager";
import { isTokenValid } from "services/userService";
import {
GalleryContextType,
SelectedState,
@@ -131,6 +135,17 @@ import {
} from "utils/collection";
import { getSelectedFiles, handleFileOp, type FileOp } from "utils/file";
/**
* Options to customize the behaviour of the sync with remote that gets
* triggered on various actions within the gallery and its descendants.
*/
interface SyncWithRemoteOpts {
/** Force a sync to happen (default: no) */
force?: boolean;
/** Perform the sync without showing a global loading bar (default: no) */
silent?: boolean;
}
const defaultGalleryContext: GalleryContextType = {
setActiveCollectionID: () => null,
syncWithRemote: () => null,
@@ -187,13 +202,11 @@ const Page: React.FC = () => {
);
const [isFileViewerOpen, setIsFileViewerOpen] = useState(false);
const syncInProgress = useRef(false);
const syncInterval = useRef<ReturnType<typeof setInterval> | undefined>(
undefined,
);
const resync = useRef<{ force: boolean; silent: boolean } | undefined>(
undefined,
);
/**`true` if a sync is currently in progress. */
const isSyncing = useRef(false);
/** Set to the {@link SyncWithRemoteOpts} of the last sync that was enqueued
while one was already in progress. */
const resyncOpts = useRef<SyncWithRemoteOpts | undefined>(undefined);
const [userIDToEmailMap, setUserIDToEmailMap] =
useState<Map<number, string>>(null);
@@ -286,32 +299,21 @@ const Page: React.FC = () => {
const router = useRouter();
// Ensure that the keys in local storage are not malformed by verifying that
// the recoveryKey can be decrypted with the masterKey.
// Note: This is not bullet-proof.
const validateKey = async () => {
try {
await getRecoveryKey();
return true;
} catch {
logout();
return false;
}
};
useEffect(() => {
const key = getKey("encryptionKey");
const token = getToken();
if (!key || !token) {
if (!haveCredentialsInSession() || !token) {
stashRedirect("/gallery");
router.push("/");
return;
}
preloadImage("/images/subscription-card-background");
const electron = globalThis.electron;
const main = async () => {
const valid = await validateKey();
if (!valid) {
let syncIntervalID: ReturnType<typeof setInterval> | undefined;
void (async () => {
if (!(await validateKey())) {
logout();
return;
}
initSettings();
@@ -335,21 +337,23 @@ const Page: React.FC = () => {
hiddenFiles: await getLocalFiles("hidden"),
trashedFiles: await getLocalTrashedFiles(),
});
await syncWithRemote(true);
await syncWithRemote({ force: true });
setIsFirstLoad(false);
setJustSignedUp(false);
syncInterval.current = setInterval(
() => syncWithRemote(false, true),
syncIntervalID = setInterval(
() => syncWithRemote({ silent: true }),
5 * 60 * 1000 /* 5 minutes */,
);
if (electron) {
electron.onMainWindowFocus(() => syncWithRemote(false, true));
electron.onMainWindowFocus(() =>
syncWithRemote({ silent: true }),
);
if (await shouldShowWhatsNew(electron)) showWhatsNew();
}
};
main();
})();
return () => {
clearInterval(syncInterval.current);
clearInterval(syncIntervalID);
if (electron) electron.onMainWindowFocus(undefined);
};
}, []);
@@ -522,7 +526,7 @@ const Page: React.FC = () => {
setTimeout(hideLoadingBar, 0);
}, [showLoadingBar, hideLoadingBar]);
const handleFileAndCollectionSyncWithRemote = useCallback(async () => {
const fileAndCollectionSyncWithRemote = useCallback(async () => {
const didUpdateFiles = await syncCollectionAndFiles({
onSetCollections: (
collections,
@@ -551,27 +555,39 @@ const Page: React.FC = () => {
}
}, []);
const handleSyncWithRemote = useCallback(
async (force = false, silent = false) => {
const syncWithRemote = useCallback(
async (opts?: SyncWithRemoteOpts) => {
const { force, silent } = opts ?? {};
// Pre-flight checks.
if (!navigator.onLine) return;
if (syncInProgress.current && !force) {
resync.current = { force, silent };
if (await isSessionInvalid()) {
showSessionExpiredDialog();
return;
}
const isForced = syncInProgress.current && force;
syncInProgress.current = true;
try {
const token = getToken();
if (!token) {
if (!(await masterKeyFromSessionIfLoggedIn())) {
clearSessionStorage();
router.push("/credentials");
return;
}
// Start or enqueue.
let isForced = false;
if (isSyncing.current) {
if (force) {
isForced = true;
} else {
resyncOpts.current = { force, silent };
return;
}
const tokenValid = await isTokenValid(token);
if (!tokenValid) {
throw new Error(CustomError.SESSION_EXPIRED);
}
!silent && showLoadingBar();
}
// The sync
isSyncing.current = true;
try {
if (!silent) showLoadingBar();
await preCollectionAndFilesSync();
await handleFileAndCollectionSyncWithRemote();
await fileAndCollectionSyncWithRemote();
// syncWithRemote is called with the force flag set to true before
// doing an upload. So it is possible, say when resuming a pending
// upload, that we get two syncWithRemotes happening in parallel.
@@ -581,26 +597,17 @@ const Page: React.FC = () => {
await postCollectionAndFilesSync();
}
} catch (e) {
switch (e.message) {
case CustomError.SESSION_EXPIRED:
showSessionExpiredDialog();
break;
case CustomError.KEY_MISSING:
clearKeys();
router.push("/credentials");
break;
default:
log.error("syncWithRemote failed", e);
}
log.error("syncWithRemote failed", e);
} finally {
dispatch({ type: "clearUnsyncedState" });
!silent && hideLoadingBar();
if (!silent) hideLoadingBar();
}
syncInProgress.current = false;
if (resync.current) {
const { force, silent } = resync.current;
setTimeout(() => handleSyncWithRemote(force, silent), 0);
resync.current = undefined;
isSyncing.current = false;
const nextOpts = resyncOpts.current;
if (nextOpts) {
resyncOpts.current = undefined;
setTimeout(() => syncWithRemote(nextOpts), 0);
}
},
[
@@ -608,13 +615,10 @@ const Page: React.FC = () => {
hideLoadingBar,
router,
showSessionExpiredDialog,
handleFileAndCollectionSyncWithRemote,
fileAndCollectionSyncWithRemote,
],
);
// Alias for existing code.
const syncWithRemote = handleSyncWithRemote;
const setupSelectAllKeyBoardShortcutHandler = () => {
const handleKeyUp = (e: KeyboardEvent) => {
switch (e.key) {
@@ -688,7 +692,7 @@ const Page: React.FC = () => {
);
}
clearSelection();
await syncWithRemote(false, true);
await syncWithRemote({ silent: true });
} catch (e) {
onGenericError(e);
} finally {
@@ -724,7 +728,7 @@ const Page: React.FC = () => {
);
}
clearSelection();
await syncWithRemote(false, true);
await syncWithRemote({ silent: true });
} catch (e) {
onGenericError(e);
} finally {
@@ -897,7 +901,8 @@ const Page: React.FC = () => {
value={{
...defaultGalleryContext,
setActiveCollectionID: handleSetActiveCollectionID,
syncWithRemote,
syncWithRemote: (force, silent) =>
syncWithRemote({ force, silent }),
setBlockingLoad,
photoListHeader,
userIDToEmailMap,
@@ -1047,7 +1052,9 @@ const Page: React.FC = () => {
<Upload
activeCollection={activeCollection}
syncWithRemote={syncWithRemote}
syncWithRemote={(force, silent) =>
syncWithRemote({ force, silent })
}
closeUploadTypeSelector={setUploadTypeSelectorView.bind(
null,
false,
@@ -1131,7 +1138,7 @@ const Page: React.FC = () => {
}
onMarkTempDeleted={handleMarkTempDeleted}
onSetOpenFileViewer={setIsFileViewerOpen}
onSyncWithRemote={handleSyncWithRemote}
onSyncWithRemote={syncWithRemote}
onVisualFeedback={handleVisualFeedback}
onSelectCollection={handleSelectCollection}
onSelectPerson={handleSelectPerson}

View File

@@ -1,15 +1,9 @@
import { HttpStatusCode } from "axios";
import { putAttributes } from "ente-accounts/services/user";
import log from "ente-base/log";
import { apiURL } from "ente-base/origins";
import type { UserDetails } from "ente-new/photos/services/user-details";
import { ApiError } from "ente-shared/error";
import HTTPService from "ente-shared/network/HTTPService";
import { getData } from "ente-shared/storage/localStorage";
import { getToken } from "ente-shared/storage/localStorage/helpers";
const HAS_SET_KEYS = "hasSetKeys";
export const getPublicKey = async (email: string) => {
const token = getToken();
@@ -21,44 +15,6 @@ export const getPublicKey = async (email: string) => {
return resp.data.publicKey;
};
export const isTokenValid = async (token: string) => {
try {
const resp = await HTTPService.get(
await apiURL("/users/session-validity/v2"),
null,
{ "X-Auth-Token": token },
);
try {
if (resp.data[HAS_SET_KEYS] === undefined) {
throw Error("resp.data.hasSetKey undefined");
}
if (!resp.data.hasSetKeys) {
try {
await putAttributes(
token,
getData("originalKeyAttributes"),
);
} catch (e) {
log.error("put attribute failed", e);
}
}
} catch (e) {
log.error("hasSetKeys not set in session validity response", e);
}
return true;
} catch (e) {
log.error("session-validity api call failed", e);
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.Unauthorized
) {
return false;
} else {
return true;
}
}
};
export const getUserDetailsV2 = async (): Promise<UserDetails> => {
try {
const token = getToken();

View File

@@ -12,7 +12,7 @@ import {
configureSRP,
generateKeyAndSRPAttributes,
} from "ente-accounts/services/srp";
import { putAttributes } from "ente-accounts/services/user";
import { putUserKeyAttributes } from "ente-accounts/services/user";
import { LinkButton } from "ente-base/components/LinkButton";
import { LoadingIndicator } from "ente-base/components/loaders";
import { useBaseContext } from "ente-base/context";
@@ -35,7 +35,6 @@ import { useEffect, useState } from "react";
const Page: React.FC = () => {
const { logout, showMiniDialog } = useBaseContext();
const [token, setToken] = useState<string>();
const [user, setUser] = useState<User>();
const [openRecoveryKey, setOpenRecoveryKey] = useState(false);
const [loading, setLoading] = useState(true);
@@ -59,7 +58,6 @@ const Page: React.FC = () => {
} else if (keyAttributes?.encryptedKey) {
void router.push("/credentials");
} else {
setToken(user.token);
setLoading(false);
}
}, [router]);
@@ -72,8 +70,7 @@ const Page: React.FC = () => {
const { keyAttributes, masterKey, srpSetupAttributes } =
await generateKeyAndSRPAttributes(passphrase);
// TODO: Refactor the code to not require this ensure
await putAttributes(token!, keyAttributes);
await putUserKeyAttributes(keyAttributes);
await configureSRP(srpSetupAttributes);
await generateAndSaveIntermediateKeyAttributes(
passphrase,

View File

@@ -23,7 +23,7 @@ import type {
} from "ente-accounts/services/srp-remote";
import { getSRPAttributes } from "ente-accounts/services/srp-remote";
import {
putAttributes,
putUserKeyAttributes,
sendOTT,
verifyEmail,
} from "ente-accounts/services/user";
@@ -31,6 +31,7 @@ import { LinkButton } from "ente-base/components/LinkButton";
import { LoadingIndicator } from "ente-base/components/loaders";
import { useBaseContext } from "ente-base/context";
import log from "ente-base/log";
import { clearSessionStorage } from "ente-base/session";
import SingleInputForm, {
type SingleInputFormProps,
} from "ente-shared/components/SingleInputForm";
@@ -41,7 +42,6 @@ import {
getLocalReferralSource,
setIsFirstLogin,
} from "ente-shared/storage/localStorage/helpers";
import { clearKeys } from "ente-shared/storage/sessionStorage";
import type { KeyAttributes, User } from "ente-shared/user/types";
import { t } from "i18next";
import { useRouter } from "next/router";
@@ -137,11 +137,11 @@ const Page: React.FC = () => {
setData("keyAttributes", keyAttributes);
setData("originalKeyAttributes", keyAttributes);
} else {
if (getData("originalKeyAttributes")) {
await putAttributes(
token!,
getData("originalKeyAttributes"),
);
const originalKeyAttributes = getData(
"originalKeyAttributes",
);
if (originalKeyAttributes) {
await putUserKeyAttributes(originalKeyAttributes);
}
if (getData("srpSetupAttributes")) {
const srpSetupAttributes: SRPSetupAttributes =
@@ -153,7 +153,7 @@ const Page: React.FC = () => {
setIsFirstLogin(true);
const redirectURL = unstashRedirect();
if (keyAttributes?.encryptedKey) {
clearKeys();
clearSessionStorage();
void router.push(redirectURL ?? "/credentials");
} else {
void router.push(redirectURL ?? "/generate");

View File

@@ -2,8 +2,8 @@ import { clearBlobCaches } from "ente-base/blob-cache";
import { clearKVDB } from "ente-base/kv";
import { clearLocalStorage } from "ente-base/local-storage";
import log from "ente-base/log";
import { clearSessionStorage } from "ente-base/session";
import localForage from "ente-shared/storage/localForage";
import { clearKeys } from "ente-shared/storage/sessionStorage";
import { clearStashedRedirect } from "./redirect";
import { remoteLogoutIfNeeded } from "./user";
@@ -34,7 +34,7 @@ export const accountLogout = async () => {
ignoreError("In-memory store", e);
}
try {
clearKeys();
clearSessionStorage();
} catch (e) {
ignoreError("Session storage", e);
}

View File

@@ -1,10 +1,14 @@
import { authenticatedRequestHeaders, HTTPError } from "ente-base/http";
import { ensureLocalUser } from "ente-base/local-user";
import { ensureLocalUser, getAuthToken } from "ente-base/local-user";
import log from "ente-base/log";
import { apiURL } from "ente-base/origins";
import { getData } from "ente-shared/storage/localStorage";
import type { KeyAttributes } from "ente-shared/user/types";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod";
import type { SRPAttributes } from "./srp-remote";
import { getSRPAttributes } from "./srp-remote";
import { putUserKeyAttributes, RemoteKeyAttributes } from "./user";
type SessionValidity =
| { status: "invalid" }
@@ -15,6 +19,11 @@ type SessionValidity =
updatedSRPAttributes: SRPAttributes;
};
const SessionValidityResponse = z.object({
hasSetKeys: z.boolean(),
keyAttributes: RemoteKeyAttributes.nullish().transform(nullToUndefined),
});
/**
* Check if the local token and/or key attributes we have are still valid.
*
@@ -72,16 +81,9 @@ export const checkSessionValidity = async (): Promise<SessionValidity> => {
// See if the response contains keyAttributes (they might not for older
// deployments).
const json = await res.json();
if (
"keyAttributes" in json &&
typeof json.keyAttributes == "object" &&
json.keyAttributes !== null
) {
// Assume it is a `KeyAttributes`.
//
// Enhancement: Convert this to a zod validation.
const remoteKeyAttributes = json.keyAttributes as KeyAttributes;
const { keyAttributes } = SessionValidityResponse.parse(await res.json());
if (keyAttributes) {
const remoteKeyAttributes = keyAttributes;
// We should have these values locally if we reach here.
const email = ensureLocalUser().email;
@@ -106,7 +108,10 @@ export const checkSessionValidity = async (): Promise<SessionValidity> => {
// changed.
return {
status: "validButPasswordChanged",
updatedKeyAttributes: remoteKeyAttributes,
// TODO:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
updatedKeyAttributes: remoteKeyAttributes as KeyAttributes,
updatedSRPAttributes: remoteSRPAttributes,
};
}
@@ -116,3 +121,51 @@ export const checkSessionValidity = async (): Promise<SessionValidity> => {
// The token is still valid (to the best of our ascertainable knowledge).
return { status: "valid" };
};
/**
* Return `true` if the user does not have a saved auth token, of it is no
* longer valid. If needed, also update the key attributes at remote.
*
* This is a subset of {@link checkSessionValidity} that has been tailored for
* use during each remote sync, to detect if the user has been logged out
* elsewhere.
*
* @returns `true` if either we don't have an auth token, or if remote tells us
* that the auth token (and the associated session) has been invalidated. In all
* other cases, return `false`.
*
* In particular, this function doesn't throw and instead returns `false` on
* errors. This is because returning `true` will trigger a blocking alert that
* ends in logging the user out, and we don't want to log the user out on on
* e.g. transient network issues.
*/
export const isSessionInvalid = async (): Promise<boolean> => {
const token = await getAuthToken();
if (!token) {
return true; /* No saved token, session is invalid */
}
try {
const res = await fetch(await apiURL("/users/session-validity/v2"), {
headers: await authenticatedRequestHeaders(),
});
if (!res.ok) {
if (res.status == 401) return true; /* session is no longer valid */
else throw new HTTPError(res);
}
const { hasSetKeys } = SessionValidityResponse.parse(await res.json());
if (!hasSetKeys) {
const originalKeyAttributes = getData("originalKeyAttributes");
if (originalKeyAttributes)
await putUserKeyAttributes(originalKeyAttributes);
}
} catch (e) {
log.warn("Failed to check session validity", e);
// Don't logout user on potentially transient errors.
return false;
}
// Everything seems ok.
return false;
};

View File

@@ -127,7 +127,7 @@ export const verifyEmail = async (
/**
* Zod schema for {@link KeyAttributes}.
*/
const RemoteKeyAttributes = z.object({
export const RemoteKeyAttributes = z.object({
kekSalt: z.string(),
encryptedKey: z.string(),
keyDecryptionNonce: z.string(),
@@ -195,17 +195,17 @@ export type TwoFactorAuthorizationResponse = z.infer<
typeof TwoFactorAuthorizationResponse
>;
export const putAttributes = async (
token: string,
keyAttributes: KeyAttributes,
) =>
HTTPService.put(
await apiURL("/users/attributes"),
{ keyAttributes },
undefined,
{ "X-Auth-Token": token },
/**
* Update or set the user's {@link KeyAttributes} on remote.
*/
export const putUserKeyAttributes = async (keyAttributes: KeyAttributes) =>
ensureOk(
await fetch(await apiURL("/users/attributes"), {
method: "PUT",
headers: await authenticatedRequestHeaders(),
body: JSON.stringify({ keyAttributes }),
}),
);
/**
* Log the user out on remote, if possible and needed.
*/

View File

@@ -47,19 +47,25 @@ export const ensureLocalUser = (): LocalUser => {
};
/**
* Return the user's auth token, or throw an error.
* Return the user's auth token, if present.
*
* The user's auth token is stored in KV DB after they have successfully logged
* in. This function returns that saved auth token.
*
* If no such token is found (which should only happen if the user is not logged
* in), then it throws an error.
*
* The underlying data is stored in IndexedDB, and can be accessed from web
* workers.
*/
export const getAuthToken = () => getKVS("token");
/**
* Return the user's auth token, or throw an error.
*
* The user's auth token can be retrieved using {@link getAuthToken}. This
* function is a wrapper which throws an error if the token is not found (which
* should only happen if the user is not logged in).
*/
export const ensureAuthToken = async () => {
const token = await getKVS("token");
const token = await getAuthToken();
if (!token) throw new Error("Not logged in");
return token;
};

View File

@@ -675,5 +675,7 @@
"theme": "Theme",
"system": "System",
"light": "Light",
"dark": "Dark"
"dark": "Dark",
"streamable_videos": "Streamable videos",
"processing_videos_status": "Processing videos..."
}

View File

@@ -2,6 +2,47 @@ import { z } from "zod";
import { decryptBox } from "./crypto";
import { toB64 } from "./crypto/libsodium";
/**
* Remove all data stored in session storage (data tied to the browser tab).
*
* See `docs/storage.md` for more details about session storage. Currently, only
* the following entries are stored in session storage:
*
* - "encryptionKey"
* - "keyEncryptionKey" (transient)
*/
export const clearSessionStorage = () => sessionStorage.clear();
/**
* Schema of JSON string value for the "encryptionKey" and "keyEncryptionKey"
* keys strings stored in session storage.
*/
const SessionKeyData = z.object({
encryptedData: z.string(),
key: z.string(),
nonce: z.string(),
});
/**
* Save the user's encrypted master key in the session storage.
*
* @param keyB64 The user's master key as a base64 encoded string.
*/
// TODO(RE):
// export const saveMasterKeyInSessionStore = async (
// keyB64: string,
// fromDesktop?: boolean,
// ) => {
// const cryptoWorker = await sharedCryptoWorker();
// const sessionKeyAttributes =
// await cryptoWorker.generateKeyAndEncryptToB64(key);
// setKey(keyType, sessionKeyAttributes);
// const electron = globalThis.electron;
// if (electron && !fromDesktop) {
// electron.saveMasterKeyB64(key);
// }
// };
/**
* Return the user's decrypted master key from session storage.
*
@@ -37,7 +78,7 @@ export const masterKeyFromSessionIfLoggedIn = async () => {
const value = sessionStorage.getItem("encryptionKey");
if (!value) return undefined;
const { encryptedData, key, nonce } = EncryptionKeyAttributes.parse(
const { encryptedData, key, nonce } = SessionKeyData.parse(
JSON.parse(value),
);
return decryptBox({ encryptedData, nonce }, key);
@@ -49,9 +90,31 @@ export const masterKeyFromSessionIfLoggedIn = async () => {
*/
export const masterKeyB64FromSession = () => masterKeyFromSession().then(toB64);
// TODO: Same as B64EncryptionResult. Revisit.
const EncryptionKeyAttributes = z.object({
encryptedData: z.string(),
key: z.string(),
nonce: z.string(),
});
/**
* Return the decrypted user's key encryption key ("kek") from session storage
* if present, otherwise return `undefined`.
*
* [Note: Stashing kek in session store]
*
* During login, if the user has set a second factor (passkey or TOTP), then we
* need to redirect them to the accounts app or TOTP page to verify the second
* factor. This second factor verification happens after password verification,
* but simply storing the decrypted kek in-memory wouldn't work because the
* second factor redirect can happen to a separate accounts app altogether.
*
* So instead, we stash the encrypted kek in session store (using
* {@link stashKeyEncryptionKeyInSessionStore}), and after redirect, retrieve
* it (after clearing it) using {@link unstashKeyEncryptionKeyFromSession}.
*/
export const unstashKeyEncryptionKeyFromSession = async () => {
// TODO: Same value as the deprecated getKey("keyEncryptionKey")
const value = sessionStorage.getItem("keyEncryptionKey");
if (!value) return undefined;
sessionStorage.removeItem("keyEncryptionKey");
const { encryptedData, key, nonce } = SessionKeyData.parse(
JSON.parse(value),
);
return decryptBox({ encryptedData, nonce }, key);
};

View File

@@ -16,7 +16,6 @@ import {
import { EnteLogo, EnteLogoBox } from "ente-base/components/EnteLogo";
import type { ButtonishProps } from "ente-base/components/mui";
import { useIsSmallWidth } from "ente-base/components/utils/hooks";
import { pt } from "ente-base/i18n";
import {
hlsGenerationStatusSnapshot,
isHLSGenerationSupported,
@@ -430,8 +429,7 @@ const EmptyState: React.FC<
case "done":
// If ML is not running, see if video processing is.
if (vpStatus?.enabled && vpStatus.status == "processing") {
// TODO(HLS):
label = pt("Processing videos...");
label = t("processing_videos_status");
}
break;
case "scheduled":

View File

@@ -10,10 +10,32 @@
* is a needed for fast refresh to work.
*/
import log from "ente-base/log";
import type { Collection } from "ente-media/collection";
import type { FamilyData } from "ente-new/photos/services/user-details";
import { getRecoveryKey } from "ente-shared/crypto/helpers";
import type { User } from "ente-shared/user/types";
/**
* Ensure that the keys in local storage are not malformed by verifying that the
* recoveryKey can be decrypted with the masterKey.
*
* This is not meant to be bullet proof, but more like an extra sanity check.
*
* @returns `true` if the sanity check passed, otherwise `false`. Since failure
* is not expected, the caller should {@link logout} on `false` to avoid
* continuing with an unexpected local state.
*/
export const validateKey = async () => {
try {
await getRecoveryKey();
return true;
} catch (e) {
log.warn("Failed to validate key" /*, caller will logout */, e);
return false;
}
};
export const constructUserIDToEmailMap = (
user: User,
collections: Collection[],

View File

@@ -65,7 +65,7 @@ export const setLSUser = async (user: object) => {
export const migrateKVToken = async (user: unknown) => {
// Throw an error if the data is in local storage but not in IndexedDB. This
// is a pre-cursor to inlining this code.
// TODO(REL): Remove this sanity check after a few days.
// TODO: Remove this sanity check eventually when this code is revisited.
const oldLSUser = getData("user");
const wasMissing =
oldLSUser &&

View File

@@ -9,5 +9,3 @@ export const getKey = (key: SessionKey) => {
};
export const removeKey = (key: SessionKey) => sessionStorage.removeItem(key);
export const clearKeys = () => sessionStorage.clear();