Fin
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user