This commit is contained in:
Manav Rathi
2025-07-04 09:21:19 +05:30
parent 46dc71ebd2
commit 7dabd9545e
4 changed files with 36 additions and 122 deletions

View File

@@ -10,8 +10,7 @@ import {
import { recoveryKeyFromMnemonic } from "ente-accounts/services/recovery-key";
import { appHomeRoute, stashRedirect } from "ente-accounts/services/redirect";
import type { KeyAttributes } from "ente-accounts/services/user";
import { sendOTT } from "ente-accounts/services/user";
import { decryptAndStoreToken } from "ente-accounts/utils/helpers";
import { decryptAndStoreToken, sendOTT } from "ente-accounts/services/user";
import { LinkButton } from "ente-base/components/LinkButton";
import {
SingleInputForm,

View File

@@ -17,8 +17,6 @@
* - "srpAttributes"
*/
import { getKVS, removeKV, setKV } from "ente-base/kv";
import log from "ente-base/log";
import { savedAuthToken } from "ente-base/token";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
@@ -123,7 +121,9 @@ const LocalUser = z.object({
export const savedPartialLocalUser = (): PartialLocalUser | undefined => {
const jsonString = localStorage.getItem("user");
if (!jsonString) return undefined;
return PartialLocalUser.parse(JSON.parse(jsonString));
const result = PartialLocalUser.parse(JSON.parse(jsonString));
void ensureTokensMatch(result);
return result;
};
/**
@@ -133,10 +133,6 @@ export const savedPartialLocalUser = (): PartialLocalUser | undefined => {
*
* This method replaces the existing data. Use {@link updateSavedLocalUser} to
* update selected fields while keeping the other fields as it is.
*
* TODO: WARNING: This does not update the KV token. The idea is to gradually
* move over uses of setLSUser to this while explicitly setting the KV token
* where needed.
*/
export const replaceSavedLocalUser = (partialLocalUser: PartialLocalUser) =>
localStorage.setItem("user", JSON.stringify(partialLocalUser));
@@ -150,10 +146,6 @@ export const replaceSavedLocalUser = (partialLocalUser: PartialLocalUser) =>
*
* @param updates A subset of {@link PartialLocalUser} fields that we'd like to
* update. The other fields, if present in local storage, remain unchanged.
*
* TODO: WARNING: This does not update the KV token. The idea is to gradually
* move over uses of setLSUser to this while explicitly setting the KV token
* where needed.
*/
export const updateSavedLocalUser = (updates: Partial<PartialLocalUser>) =>
replaceSavedLocalUser({ ...savedPartialLocalUser(), ...updates });
@@ -177,84 +169,20 @@ export const savedLocalUser = (): LocalUser | undefined => {
if (!jsonString) return undefined;
// We might have some data, but not all of it. So do a non-throwing parse.
const { success, data } = LocalUser.safeParse(JSON.parse(jsonString));
if (success) void ensureTokensMatch(data);
return success ? data : undefined;
};
export type LocalStorageKey = "user";
export const getData = (key: LocalStorageKey) => {
try {
if (
typeof localStorage == "undefined" ||
typeof key == "undefined" ||
typeof localStorage.getItem(key) == "undefined" ||
localStorage.getItem(key) == "undefined"
) {
return null;
}
const data = localStorage.getItem(key);
return data && JSON.parse(data);
} catch (e) {
log.error(`Failed to Parse JSON for key ${key}`, e);
}
};
export const setData = (key: LocalStorageKey, value: object) =>
localStorage.setItem(key, JSON.stringify(value));
// TODO: Migrate this to `local-user.ts`, with (a) more precise optionality
// indication of the constituent fields, (b) moving any fields that need to be
// accessed from web workers to KV DB.
//
// Creating a new function here to act as a funnel point.
export const setLSUser = async (user: object) => {
await migrateKVToken(user);
setData("user", user);
};
/**
* Update the "token" KV with the token (if any) for the given {@link user}.
* Sanity check to ensure that KV token and local storage token are the same.
*
* This is an internal implementation details of {@link setLSUser} and doesn't
* need to exposed conceptually. For now though, we need to call this externally
* at an early point in the app startup to also copy over the token into KV DB
* for existing users.
*
* This was added 1 July 2024, can be removed after a while and this code
* inlined into `setLSUser` (tag: Migration).
* TODO: Added July 2025, can just be removed soon, there is already a sanity
* check `isLocalStorageAndIndexedDBMismatch` on app start (tag: Migration).
*/
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: Remove this sanity check eventually when this code is revisited.
const oldLSUser = getData("user");
const wasMissing =
oldLSUser &&
typeof oldLSUser == "object" &&
"token" in oldLSUser &&
typeof oldLSUser.token == "string" &&
!(await getKVS("token"));
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
user &&
typeof user == "object" &&
"id" in user &&
typeof user.id == "number"
? await setKV("userID", user.id)
: await removeKV("userID");
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
user &&
typeof user == "object" &&
"token" in user &&
typeof user.token == "string"
? await setKV("token", user.token)
: await removeKV("token");
if (wasMissing)
throw new Error(
"The user's token was present in local storage but not in IndexedDB",
);
export const ensureTokensMatch = async (user: PartialLocalUser | undefined) => {
if (user?.token !== (await savedAuthToken())) {
throw new Error("Token mismatch");
}
};
/**

View File

@@ -1,12 +1,10 @@
import {
getData,
replaceSavedLocalUser,
savedKeyAttributes,
savedLocalUser,
savedPartialLocalUser,
saveKeyAttributes,
saveSRPAttributes,
setLSUser,
updateSavedLocalUser,
} from "ente-accounts/services/accounts-db";
import {
@@ -25,18 +23,19 @@ import {
generateKeyPair,
toB64URLSafe,
} from "ente-base/crypto";
import { isDevBuild } from "ente-base/env";
import {
authenticatedRequestHeaders,
ensureOk,
publicRequestHeaders,
} from "ente-base/http";
import log from "ente-base/log";
import { apiURL } from "ente-base/origins";
import { ensureMasterKeyFromSession } from "ente-base/session";
import {
ensureMasterKeyFromSession,
saveMasterKeyInSessionAndSafeStore,
} from "ente-base/session";
import { removeAuthToken, savedAuthToken } from "ente-base/token";
removeAuthToken,
saveAuthToken,
savedAuthToken,
} from "ente-base/token";
import { ensure } from "ente-utils/ensure";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
@@ -730,12 +729,6 @@ export const changePassword = async (password: string) => {
{ ...keyAttributes, ...updatedKeyAttr },
masterKey,
);
// TODO(RE): This shouldn't be needed, remove me. As a soft remove,
// disabling it for dev builds. (tag: Migration)
if (!isDevBuild) {
await saveMasterKeyInSessionAndSafeStore(masterKey);
}
};
/**
@@ -780,30 +773,25 @@ export const decryptAndStoreToken = async (
keyAttributes: KeyAttributes,
masterKey: string,
) => {
const user = getData("user");
const { encryptedToken } = user;
if (encryptedToken && encryptedToken.length > 0) {
const { encryptedSecretKey, secretKeyDecryptionNonce, publicKey } =
keyAttributes;
const privateKey = await decryptBox(
{
encryptedData: encryptedSecretKey,
nonce: secretKeyDecryptionNonce,
},
masterKey,
);
const decryptedToken = await toB64URLSafe(
await boxSealOpenBytes(encryptedToken, { publicKey, privateKey }),
);
await setLSUser({
...user,
token: decryptedToken,
encryptedToken: null,
});
const { encryptedToken } = savedPartialLocalUser() ?? {};
if (!encryptedToken) {
log.info("Skipping token decryption (no encrypted token found)");
return;
}
const { encryptedSecretKey, secretKeyDecryptionNonce, publicKey } =
keyAttributes;
const privateKey = await decryptBox(
{ encryptedData: encryptedSecretKey, nonce: secretKeyDecryptionNonce },
masterKey,
);
const token = await toB64URLSafe(
await boxSealOpenBytes(encryptedToken, { publicKey, privateKey }),
);
updateSavedLocalUser({ token, encryptedToken: undefined });
return saveAuthToken(token);
};
const TwoFactorSecret = z.object({

View File

@@ -1,7 +1,6 @@
import { z } from "zod/v4";
import { decryptBox, encryptBox, generateKey } from "./crypto";
import log from "./log";
import { savedAuthToken } from "./token";
/**
* Remove all data stored in session storage (data tied to the browser tab).