[web] General improvements to code dealing with keys (#6155)
(Non functional)
This commit is contained in:
@@ -70,7 +70,7 @@ const createCollection = async (
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
const encryptionKey = await getActualKey();
|
||||
const token = getToken();
|
||||
const collectionKey = await cryptoWorker.generateEncryptionKey();
|
||||
const collectionKey = await cryptoWorker.generateKey();
|
||||
const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } =
|
||||
await cryptoWorker.encryptToB64(collectionKey, encryptionKey);
|
||||
const { encryptedData: encryptedName, nonce: nameDecryptionNonce } =
|
||||
|
||||
@@ -205,6 +205,9 @@ via [@fontsource-variable/inter](https://fontsource.org/fonts/inter/install).
|
||||
[pDebounce](https://github.com/sindresorhus/p-debounce) are used for
|
||||
debouncing operations (See also: `[Note: Throttle and debounce]`).
|
||||
|
||||
- [bip39](https://github.com/bitcoinjs/bip39) is used for generating the 24-word
|
||||
recovery key mnemonic.
|
||||
|
||||
- [zxcvbn](https://github.com/dropbox/zxcvbn) is used for password strength
|
||||
estimation.
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import * as bip39 from "bip39";
|
||||
import { type MiniDialogAttributes } from "ente-base/components/MiniDialog";
|
||||
import { SpacedRow } from "ente-base/components/containers";
|
||||
import { DialogCloseIconButton } from "ente-base/components/mui/DialogCloseIconButton";
|
||||
@@ -16,14 +15,14 @@ import { useIsSmallWidth } from "ente-base/components/utils/hooks";
|
||||
import type { ModalVisibilityProps } from "ente-base/components/utils/modal";
|
||||
import log from "ente-base/log";
|
||||
import { downloadString } from "ente-base/utils/web";
|
||||
import { getRecoveryKey } from "ente-shared/crypto/helpers";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
getUserRecoveryKeyB64,
|
||||
recoveryKeyB64ToMnemonic,
|
||||
} from "../services/recovery-key";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
|
||||
// mobile client library only supports english.
|
||||
bip39.setDefaultWordlist("english");
|
||||
|
||||
type RecoveryKeyProps = ModalVisibilityProps & {
|
||||
showMiniDialog: (attributes: MiniDialogAttributes) => void;
|
||||
};
|
||||
@@ -116,7 +115,7 @@ export const RecoveryKey: React.FC<RecoveryKeyProps> = ({
|
||||
};
|
||||
|
||||
const getRecoveryKeyMnemonic = async () =>
|
||||
bip39.entropyToMnemonic(await getRecoveryKey());
|
||||
recoveryKeyB64ToMnemonic(await getUserRecoveryKeyB64());
|
||||
|
||||
const downloadRecoveryKeyMnemonic = (key: string) =>
|
||||
downloadString(key, "ente-recovery-key.txt");
|
||||
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
AccountsPageFooter,
|
||||
AccountsPageTitle,
|
||||
} from "ente-accounts/components/layouts/centered-paper";
|
||||
import { recoveryKeyB64FromMnemonic } from "ente-accounts/services/recovery-key";
|
||||
import { appHomeRoute, stashRedirect } from "ente-accounts/services/redirect";
|
||||
import { sendOTT } from "ente-accounts/services/user";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import { sharedCryptoWorker } from "ente-base/crypto";
|
||||
import { decryptBoxB64 } from "ente-base/crypto";
|
||||
import log from "ente-base/log";
|
||||
import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
@@ -23,11 +24,6 @@ import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const bip39 = require("bip39");
|
||||
// mobile client library only supports english.
|
||||
bip39.setDefaultWordlist("english");
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const { showMiniDialog } = useBaseContext();
|
||||
|
||||
@@ -65,25 +61,13 @@ const Page: React.FC = () => {
|
||||
setFieldError,
|
||||
) => {
|
||||
try {
|
||||
recoveryKey = recoveryKey
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => !!part)
|
||||
.join(" ");
|
||||
// check if user is entering mnemonic recovery key
|
||||
if (recoveryKey.indexOf(" ") > 0) {
|
||||
if (recoveryKey.split(" ").length !== 24) {
|
||||
throw new Error("recovery code should have 24 words");
|
||||
}
|
||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||
}
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
const keyAttr = keyAttributes!;
|
||||
const masterKey = await cryptoWorker.decryptB64(
|
||||
keyAttr.masterKeyEncryptedWithRecoveryKey!,
|
||||
keyAttr.masterKeyDecryptionNonce!,
|
||||
await cryptoWorker.fromHex(recoveryKey),
|
||||
const masterKey = await decryptBoxB64(
|
||||
{
|
||||
encryptedData: keyAttr.masterKeyEncryptedWithRecoveryKey!,
|
||||
nonce: keyAttr.masterKeyDecryptionNonce!,
|
||||
},
|
||||
await recoveryKeyB64FromMnemonic(recoveryKey),
|
||||
);
|
||||
await saveKeyInSessionStore("encryptionKey", masterKey);
|
||||
await decryptAndStoreToken(keyAttr, masterKey);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AccountsPageFooter,
|
||||
AccountsPageTitle,
|
||||
} from "ente-accounts/components/layouts/centered-paper";
|
||||
import { recoveryKeyB64FromMnemonic } from "ente-accounts/services/recovery-key";
|
||||
import {
|
||||
recoverTwoFactor,
|
||||
removeTwoFactor,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import type { MiniDialogAttributes } from "ente-base/components/MiniDialog";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import { sharedCryptoWorker } from "ente-base/crypto";
|
||||
import { decryptBoxB64 } from "ente-base/crypto";
|
||||
import type { B64EncryptionResult } from "ente-base/crypto/libsodium";
|
||||
import log from "ente-base/log";
|
||||
import SingleInputForm, {
|
||||
@@ -26,11 +27,6 @@ import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const bip39 = require("bip39");
|
||||
// mobile client library only supports english.
|
||||
bip39.setDefaultWordlist("english");
|
||||
|
||||
export interface RecoverPageProps {
|
||||
twoFactorType: TwoFactorType;
|
||||
}
|
||||
@@ -95,25 +91,10 @@ const Page: React.FC<RecoverPageProps> = ({ twoFactorType }) => {
|
||||
setFieldError,
|
||||
) => {
|
||||
try {
|
||||
recoveryKey = recoveryKey
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => !!part)
|
||||
.join(" ");
|
||||
// check if user is entering mnemonic recovery key
|
||||
if (recoveryKey.indexOf(" ") > 0) {
|
||||
if (recoveryKey.split(" ").length !== 24) {
|
||||
throw new Error("recovery code should have 24 words");
|
||||
}
|
||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||
}
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
const { encryptedData, nonce } = encryptedTwoFactorSecret!;
|
||||
const twoFactorSecret = await cryptoWorker.decryptB64(
|
||||
encryptedData,
|
||||
nonce,
|
||||
await cryptoWorker.fromHex(recoveryKey),
|
||||
const twoFactorSecret = await decryptBoxB64(
|
||||
{ encryptedData, nonce },
|
||||
await recoveryKeyB64FromMnemonic(recoveryKey),
|
||||
);
|
||||
const resp = await removeTwoFactor(
|
||||
sessionID!,
|
||||
|
||||
@@ -8,11 +8,12 @@ import { CenteredFill } from "ente-base/components/containers";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { ActivityIndicator } from "ente-base/components/mui/ActivityIndicator";
|
||||
import { FocusVisibleButton } from "ente-base/components/mui/FocusVisibleButton";
|
||||
import { encryptWithRecoveryKey } from "ente-shared/crypto/helpers";
|
||||
import { encryptBoxB64 } from "ente-base/crypto";
|
||||
import { getData, setLSUser } from "ente-shared/storage/localStorage";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getUserRecoveryKeyB64 } from "../../services/recovery-key";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const [twoFactorSecret, setTwoFactorSecret] = useState<
|
||||
@@ -26,14 +27,14 @@ const Page: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (otp: string) => {
|
||||
const {
|
||||
encryptedData: encryptedTwoFactorSecret,
|
||||
nonce: twoFactorSecretDecryptionNonce,
|
||||
} = await encryptWithRecoveryKey(twoFactorSecret!.secretCode);
|
||||
const box = await encryptBoxB64(
|
||||
twoFactorSecret!.secretCode,
|
||||
await getUserRecoveryKeyB64(),
|
||||
);
|
||||
await enableTwoFactor({
|
||||
code: otp,
|
||||
encryptedTwoFactorSecret,
|
||||
twoFactorSecretDecryptionNonce,
|
||||
encryptedTwoFactorSecret: box.encryptedData,
|
||||
twoFactorSecretDecryptionNonce: box.nonce,
|
||||
});
|
||||
await setLSUser({ ...getData("user"), isTwoFactorEnabled: true });
|
||||
await router.push(appHomeRoute);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { TwoFactorAuthorizationResponse } from "ente-accounts/services/user";
|
||||
import { clientPackageName, isDesktop } from "ente-base/app";
|
||||
import { sharedCryptoWorker } from "ente-base/crypto";
|
||||
import {
|
||||
encryptToB64,
|
||||
generateEncryptionKey,
|
||||
} from "ente-base/crypto/libsodium";
|
||||
import { encryptBoxB64, generateKey } from "ente-base/crypto";
|
||||
import {
|
||||
authenticatedRequestHeaders,
|
||||
ensureOk,
|
||||
@@ -13,11 +9,11 @@ import {
|
||||
} from "ente-base/http";
|
||||
import log from "ente-base/log";
|
||||
import { apiURL } from "ente-base/origins";
|
||||
import { getRecoveryKey } from "ente-shared/crypto/helpers";
|
||||
import HTTPService from "ente-shared/network/HTTPService";
|
||||
import { getData, setData, setLSUser } from "ente-shared/storage/localStorage";
|
||||
import { getToken } from "ente-shared/storage/localStorage/helpers";
|
||||
import { z } from "zod";
|
||||
import { getUserRecoveryKeyB64 } from "./recovery-key";
|
||||
import { unstashRedirect } from "./redirect";
|
||||
|
||||
/**
|
||||
@@ -111,20 +107,15 @@ export const openAccountsManagePasskeysPage = async () => {
|
||||
if (!recoveryEnabled) {
|
||||
// If not, enable it for them by creating the necessary recovery
|
||||
// information to prevent them from getting locked out.
|
||||
const recoveryKey = await getRecoveryKey();
|
||||
|
||||
const resetSecret = await generateEncryptionKey();
|
||||
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
const encryptionResult = await encryptToB64(
|
||||
const resetSecret = await generateKey();
|
||||
const box = await encryptBoxB64(
|
||||
resetSecret,
|
||||
await cryptoWorker.fromHex(recoveryKey),
|
||||
await getUserRecoveryKeyB64(),
|
||||
);
|
||||
|
||||
await configurePasskeyRecovery(
|
||||
resetSecret,
|
||||
encryptionResult.encryptedData,
|
||||
encryptionResult.nonce,
|
||||
box.encryptedData,
|
||||
box.nonce,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
122
web/packages/accounts/services/recovery-key.ts
Normal file
122
web/packages/accounts/services/recovery-key.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import * as bip39 from "bip39";
|
||||
import {
|
||||
decryptBoxB64,
|
||||
fromHex,
|
||||
sharedCryptoWorker,
|
||||
toHex,
|
||||
} from "ente-base/crypto";
|
||||
import { masterKeyFromSession } from "ente-base/session";
|
||||
import { getData, setData } from "ente-shared/storage/localStorage";
|
||||
import type { KeyAttributes } from "ente-shared/user/types";
|
||||
import { putUserRecoveryKeyAttributes } from "./user";
|
||||
|
||||
// Mobile client library only supports English.
|
||||
bip39.setDefaultWordlist("english");
|
||||
|
||||
/**
|
||||
* Convert the provided BIP-39 mnemonic string into its base64 representation.
|
||||
*
|
||||
* @param recoveryKeyMnemonicOrHex The BIP-39 mnemonic (24 word) string
|
||||
* representing the recovery key. For legacy compatibility, the function also
|
||||
* works if provided the hex representation of the recovery key.
|
||||
*
|
||||
* @returns A base64 string representing the underlying bytes of the recovery key.
|
||||
*/
|
||||
export const recoveryKeyB64FromMnemonic = (
|
||||
recoveryKeyMnemonicOrHex: string,
|
||||
) => {
|
||||
const trimmedInput = recoveryKeyMnemonicOrHex
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => !!part)
|
||||
.join(" ");
|
||||
|
||||
let recoveryKeyHex: string;
|
||||
// Check if user is entering mnemonic recovery key.
|
||||
if (trimmedInput.indexOf(" ") > 0) {
|
||||
if (trimmedInput.split(" ").length != 24) {
|
||||
throw new Error("recovery code should have 24 words");
|
||||
}
|
||||
recoveryKeyHex = bip39.mnemonicToEntropy(trimmedInput);
|
||||
} else {
|
||||
recoveryKeyHex = trimmedInput;
|
||||
}
|
||||
|
||||
return fromHex(recoveryKeyHex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the provided base64 encoded recovery key into its BIP-39 mnemonic.
|
||||
*
|
||||
* @param recoveryKeyB64 The base64 encoded recovery key to mnemonize.
|
||||
*
|
||||
* @returns A 24-word mnemonic that serves as the user visible recovery key.
|
||||
*/
|
||||
export const recoveryKeyB64ToMnemonic = async (recoveryKeyB64: string) =>
|
||||
bip39.entropyToMnemonic(await toHex(recoveryKeyB64));
|
||||
|
||||
/**
|
||||
* Return the (decrypted) recovery key of the logged in user.
|
||||
*
|
||||
* @returns The user's base64 encoded recovery key.
|
||||
*/
|
||||
export const getUserRecoveryKeyB64 = async () => {
|
||||
const masterKey = await masterKeyFromSession();
|
||||
|
||||
const keyAttributes: KeyAttributes = getData("keyAttributes");
|
||||
const { recoveryKeyEncryptedWithMasterKey, recoveryKeyDecryptionNonce } =
|
||||
keyAttributes;
|
||||
|
||||
if (recoveryKeyEncryptedWithMasterKey && recoveryKeyDecryptionNonce) {
|
||||
return decryptBoxB64(
|
||||
{
|
||||
encryptedData: recoveryKeyEncryptedWithMasterKey,
|
||||
nonce: recoveryKeyDecryptionNonce,
|
||||
},
|
||||
masterKey,
|
||||
);
|
||||
} else {
|
||||
return createNewRecoveryKey(masterKey);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new recovery key, tell remote about it, update our local state,
|
||||
* and then return it.
|
||||
*
|
||||
* This function will be used only for legacy users for whom we did not generate
|
||||
* recovery keys during sign up.
|
||||
*
|
||||
* @returns a new base64 encoded recovery key.
|
||||
*/
|
||||
const createNewRecoveryKey = async (masterKey: Uint8Array) => {
|
||||
const existingAttributes = getData("keyAttributes");
|
||||
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
const recoveryKey = await cryptoWorker.generateKey();
|
||||
const encryptedMasterKey = await cryptoWorker.encryptBoxB64(
|
||||
masterKey,
|
||||
recoveryKey,
|
||||
);
|
||||
const encryptedRecoveryKey = await cryptoWorker.encryptBoxB64(
|
||||
recoveryKey,
|
||||
masterKey,
|
||||
);
|
||||
|
||||
const recoveryKeyAttributes = {
|
||||
masterKeyEncryptedWithRecoveryKey: encryptedMasterKey.encryptedData,
|
||||
masterKeyDecryptionNonce: encryptedMasterKey.nonce,
|
||||
recoveryKeyEncryptedWithMasterKey: encryptedRecoveryKey.encryptedData,
|
||||
recoveryKeyDecryptionNonce: encryptedRecoveryKey.nonce,
|
||||
};
|
||||
|
||||
await putUserRecoveryKeyAttributes(recoveryKeyAttributes);
|
||||
|
||||
setData("keyAttributes", {
|
||||
...existingAttributes,
|
||||
...recoveryKeyAttributes,
|
||||
});
|
||||
|
||||
return recoveryKey;
|
||||
};
|
||||
@@ -171,8 +171,8 @@ export async function generateKeyAndSRPAttributes(
|
||||
srpSetupAttributes: SRPSetupAttributes;
|
||||
}> {
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
const masterKey = await cryptoWorker.generateEncryptionKey();
|
||||
const recoveryKey = await cryptoWorker.generateEncryptionKey();
|
||||
const masterKey = await cryptoWorker.generateKey();
|
||||
const recoveryKey = await cryptoWorker.generateKey();
|
||||
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
|
||||
|
||||
|
||||
@@ -61,13 +61,6 @@ export interface UpdatedKey {
|
||||
opsLimit: number;
|
||||
}
|
||||
|
||||
export interface RecoveryKey {
|
||||
masterKeyEncryptedWithRecoveryKey: string;
|
||||
masterKeyDecryptionNonce: string;
|
||||
recoveryKeyEncryptedWithMasterKey: string;
|
||||
recoveryKeyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask remote to send a OTP / OTT to the given email to verify that the user has
|
||||
* access to it. Subsequent the app will pass this OTT back via the
|
||||
@@ -311,10 +304,28 @@ export const enableTwoFactor = async (req: EnableTwoFactorRequest) =>
|
||||
}),
|
||||
);
|
||||
|
||||
export const setRecoveryKey = async (token: string, recoveryKey: RecoveryKey) =>
|
||||
HTTPService.put(
|
||||
await apiURL("/users/recovery-key"),
|
||||
recoveryKey,
|
||||
undefined,
|
||||
{ "X-Auth-Token": token },
|
||||
export interface RecoveryKeyAttributes {
|
||||
masterKeyEncryptedWithRecoveryKey: string;
|
||||
masterKeyDecryptionNonce: string;
|
||||
recoveryKeyEncryptedWithMasterKey: string;
|
||||
recoveryKeyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the encrypted recovery key attributes for the logged in user.
|
||||
*
|
||||
* In practice, this is not expected to be called and is meant as a rare
|
||||
* fallback for very old accounts created prior to recovery key related
|
||||
* attributes being assigned on account setup. Even for these, it'll be called
|
||||
* only once.
|
||||
*/
|
||||
export const putUserRecoveryKeyAttributes = async (
|
||||
recoveryKeyAttributes: RecoveryKeyAttributes,
|
||||
) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/recovery-key"), {
|
||||
method: "PUT",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify(recoveryKeyAttributes),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ export const _toHex = libsodium.toHex;
|
||||
|
||||
export const _fromHex = libsodium.fromHex;
|
||||
|
||||
export const _generateBoxKey = libsodium.generateBoxKey;
|
||||
export const _generateKey = libsodium.generateKey;
|
||||
|
||||
export const _generateBlobOrStreamKey = libsodium.generateBlobOrStreamKey;
|
||||
|
||||
|
||||
@@ -137,13 +137,15 @@ export const fromHex = (hexString: string) =>
|
||||
: sharedWorker().then((w) => w.fromHex(hexString));
|
||||
|
||||
/**
|
||||
* Return a new randomly generated 256-bit key (as a base64 string) suitable for
|
||||
* use with the *Box encryption functions.
|
||||
* Return a new randomly generated 256-bit key (as a base64 string).
|
||||
*
|
||||
* The returned key is suitable for use with the *Box encryption functions, and
|
||||
* as a general encryption key (e.g. as the user's master key or recovery key).
|
||||
*/
|
||||
export const generateBoxKey = () =>
|
||||
export const generateKey = () =>
|
||||
inWorker()
|
||||
? ei._generateBoxKey()
|
||||
: sharedWorker().then((w) => w.generateBoxKey());
|
||||
? ei._generateKey()
|
||||
: sharedWorker().then((w) => w.generateKey());
|
||||
|
||||
/**
|
||||
* Return a new randomly generated 256-bit key (as a base64 string) suitable for
|
||||
|
||||
@@ -130,13 +130,29 @@ const bytes = async (bob: BytesOrB64) =>
|
||||
typeof bob == "string" ? fromB64(bob) : bob;
|
||||
|
||||
/**
|
||||
* Generate a new key for use with the *Box encryption functions, and return its
|
||||
* base64 string representation.
|
||||
* Generate a new randomly generated 256-bit key for use as a general encryption
|
||||
* key and return its base64 string representation.
|
||||
*
|
||||
* This returns a new randomly generated 256-bit key suitable for being used
|
||||
* with libsodium's secretbox APIs.
|
||||
* From the architecture docs:
|
||||
*
|
||||
* > [`crypto_secretbox_keygen`](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes)
|
||||
* > is used to generate all random keys within the application. Your
|
||||
* > `masterKey`, `recoveryKey`, `collectionKey`, `fileKey` are all 256-bit keys
|
||||
* > generated using this API.
|
||||
*
|
||||
* {@link generateKey} can be contrasted with {@link generateBlobOrStreamKey}
|
||||
* and can be thought of as a hypothetical "generateBoxKey". That is, the key
|
||||
* returned by this function is suitable for being used with the *Box encryption
|
||||
* functions (which eventually delegate to the libsodium's secretbox APIs).
|
||||
*
|
||||
* While this is a reasonable semantic distinction, in terms of implementation
|
||||
* there is no difference: currently both {@link generateKey} (or the
|
||||
* hypothetical "generateBoxKey") and {@link generateBlobOrStreamKey} produce
|
||||
* 256-bits of entropy that does not have any ties to a particular algorithm.
|
||||
*
|
||||
* @returns A new randomly generated 256-bit key.
|
||||
*/
|
||||
export const generateBoxKey = async () => {
|
||||
export const generateKey = async () => {
|
||||
await sodium.ready;
|
||||
return toB64(sodium.crypto_secretbox_keygen());
|
||||
};
|
||||
@@ -863,11 +879,6 @@ export const deriveInteractiveKey = async (
|
||||
return { key, opsLimit, memLimit };
|
||||
};
|
||||
|
||||
export async function generateEncryptionKey() {
|
||||
await sodium.ready;
|
||||
return await toB64(sodium.crypto_kdf_keygen());
|
||||
}
|
||||
|
||||
export async function generateSaltToDeriveKey() {
|
||||
await sodium.ready;
|
||||
return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES));
|
||||
|
||||
@@ -18,7 +18,7 @@ export class CryptoWorker {
|
||||
fromB64 = ei._fromB64;
|
||||
toHex = ei._toHex;
|
||||
fromHex = ei._fromHex;
|
||||
generateBoxKey = ei._generateBoxKey;
|
||||
generateKey = ei._generateKey;
|
||||
generateBlobOrStreamKey = ei._generateBlobOrStreamKey;
|
||||
encryptBoxB64 = ei._encryptBoxB64;
|
||||
encryptThumbnail = ei._encryptThumbnail;
|
||||
@@ -70,10 +70,6 @@ export class CryptoWorker {
|
||||
return libsodium.encryptUTF8(data, key);
|
||||
}
|
||||
|
||||
async generateEncryptionKey() {
|
||||
return libsodium.generateEncryptionKey();
|
||||
}
|
||||
|
||||
async generateSaltToDeriveKey() {
|
||||
return libsodium.generateSaltToDeriveKey();
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
* is a needed for fast refresh to work.
|
||||
*/
|
||||
|
||||
import { getUserRecoveryKeyB64 } from "ente-accounts/services/recovery-key";
|
||||
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";
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ import type { User } from "ente-shared/user/types";
|
||||
*/
|
||||
export const validateKey = async () => {
|
||||
try {
|
||||
await getRecoveryKey();
|
||||
await getUserRecoveryKeyB64();
|
||||
return true;
|
||||
} catch (e) {
|
||||
log.warn("Failed to validate key" /*, caller will logout */, e);
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { setRecoveryKey } from "ente-accounts/services/user";
|
||||
import { sharedCryptoWorker } from "ente-base/crypto";
|
||||
import log from "ente-base/log";
|
||||
import { masterKeyFromSession } from "ente-base/session";
|
||||
import { getData, setData, setLSUser } from "ente-shared/storage/localStorage";
|
||||
import { getToken } from "ente-shared/storage/localStorage/helpers";
|
||||
import { type SessionKey, setKey } from "ente-shared/storage/sessionStorage";
|
||||
import { getActualKey } from "ente-shared/user";
|
||||
import type { KeyAttributes } from "ente-shared/user/types";
|
||||
|
||||
const LOGIN_SUB_KEY_LENGTH = 32;
|
||||
@@ -108,75 +104,6 @@ export const saveKeyInSessionStore = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const encryptWithRecoveryKey = async (data: string) => {
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
const hexRecoveryKey = await getRecoveryKey();
|
||||
const recoveryKey = await cryptoWorker.fromHex(hexRecoveryKey);
|
||||
return cryptoWorker.encryptBoxB64(data, recoveryKey);
|
||||
};
|
||||
|
||||
export const getRecoveryKey = async () => {
|
||||
try {
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
|
||||
const keyAttributes: KeyAttributes = getData("keyAttributes");
|
||||
const {
|
||||
recoveryKeyEncryptedWithMasterKey,
|
||||
recoveryKeyDecryptionNonce,
|
||||
} = keyAttributes;
|
||||
const masterKey = await getActualKey();
|
||||
let recoveryKey: string;
|
||||
if (recoveryKeyEncryptedWithMasterKey) {
|
||||
recoveryKey = await cryptoWorker.decryptB64(
|
||||
recoveryKeyEncryptedWithMasterKey!,
|
||||
recoveryKeyDecryptionNonce!,
|
||||
masterKey,
|
||||
);
|
||||
} else {
|
||||
recoveryKey = await createNewRecoveryKey();
|
||||
}
|
||||
recoveryKey = await cryptoWorker.toHex(recoveryKey);
|
||||
return recoveryKey;
|
||||
} catch (e) {
|
||||
log.error("getRecoveryKey failed", e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// Used only for legacy users for whom we did not generate recovery keys during
|
||||
// sign up
|
||||
async function createNewRecoveryKey() {
|
||||
const masterKey = await getActualKey();
|
||||
const existingAttributes = getData("keyAttributes");
|
||||
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
|
||||
const recoveryKey = await cryptoWorker.generateEncryptionKey();
|
||||
const encryptedMasterKey = await cryptoWorker.encryptToB64(
|
||||
masterKey,
|
||||
recoveryKey,
|
||||
);
|
||||
const encryptedRecoveryKey = await cryptoWorker.encryptToB64(
|
||||
recoveryKey,
|
||||
masterKey,
|
||||
);
|
||||
const recoveryKeyAttributes = {
|
||||
masterKeyEncryptedWithRecoveryKey: encryptedMasterKey.encryptedData,
|
||||
masterKeyDecryptionNonce: encryptedMasterKey.nonce,
|
||||
recoveryKeyEncryptedWithMasterKey: encryptedRecoveryKey.encryptedData,
|
||||
recoveryKeyDecryptionNonce: encryptedRecoveryKey.nonce,
|
||||
};
|
||||
await setRecoveryKey(getToken(), recoveryKeyAttributes);
|
||||
|
||||
const updatedKeyAttributes = Object.assign(
|
||||
existingAttributes,
|
||||
recoveryKeyAttributes,
|
||||
);
|
||||
setData("keyAttributes", updatedKeyAttributes);
|
||||
|
||||
return recoveryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the {@link encryptedChallenge} sent by remote during the delete
|
||||
* account flow ({@link getAccountDeleteChallenge}), returning a value that can
|
||||
|
||||
Reference in New Issue
Block a user