[web] SRP related refactoring (#6239)
This commit is contained in:
@@ -14,12 +14,13 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
deriveSRPPassword,
|
||||
generateSRPSetupAttributes,
|
||||
stashSRPSetupAttributes,
|
||||
} from "ente-accounts/services/srp";
|
||||
import {
|
||||
generateAndSaveInteractiveKeyAttributes,
|
||||
generateKeysAndAttributes,
|
||||
savePartialLocalUser,
|
||||
sendOTT,
|
||||
type GenerateKeysAndAttributesResult,
|
||||
} from "ente-accounts/services/user";
|
||||
@@ -31,7 +32,6 @@ import { deriveKeyInsufficientMemoryErrorMessage } from "ente-base/crypto/types"
|
||||
import { isMuseumHTTPError } from "ente-base/http";
|
||||
import log from "ente-base/log";
|
||||
import { saveMasterKeyInSessionAndSafeStore } from "ente-base/session";
|
||||
import { setLSUser } from "ente-shared//storage/localStorage";
|
||||
import { setData } from "ente-shared/storage/localStorage";
|
||||
import {
|
||||
setJustSignedUp,
|
||||
@@ -125,7 +125,7 @@ export const SignUpContents: React.FC<SignUpContentsProps> = ({
|
||||
throw e;
|
||||
}
|
||||
|
||||
await setLSUser({ email });
|
||||
savePartialLocalUser({ email });
|
||||
|
||||
let gkResult: GenerateKeysAndAttributesResult;
|
||||
try {
|
||||
@@ -145,20 +145,15 @@ export const SignUpContents: React.FC<SignUpContentsProps> = ({
|
||||
}
|
||||
|
||||
const { masterKey, kek, keyAttributes } = gkResult;
|
||||
|
||||
const srpSetupAttributes = await generateSRPSetupAttributes(
|
||||
await deriveSRPPassword(kek),
|
||||
);
|
||||
|
||||
setData("originalKeyAttributes", keyAttributes);
|
||||
setData("srpSetupAttributes", srpSetupAttributes);
|
||||
stashSRPSetupAttributes(await generateSRPSetupAttributes(kek));
|
||||
await generateAndSaveInteractiveKeyAttributes(
|
||||
password,
|
||||
keyAttributes,
|
||||
masterKey,
|
||||
);
|
||||
|
||||
await saveMasterKeyInSessionAndSafeStore(masterKey);
|
||||
|
||||
setJustSignedUp(true);
|
||||
void router.push("/verify");
|
||||
} catch (e) {
|
||||
@@ -170,124 +165,122 @@ export const SignUpContents: React.FC<SignUpContentsProps> = ({
|
||||
|
||||
const form = (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Stack sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="username"
|
||||
label={t("enter_email")}
|
||||
value={formik.values.email}
|
||||
onChange={formik.handleChange}
|
||||
error={!!formik.errors.email}
|
||||
helperText={formik.errors.email}
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
label={t("password")}
|
||||
value={formik.values.password}
|
||||
onChange={formik.handleChange}
|
||||
error={!!formik.errors.password}
|
||||
helperText={formik.errors.password}
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<ShowHidePasswordInputAdornment
|
||||
showPassword={showPassword}
|
||||
onToggle={handleToggleShowHidePassword}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
name="confirmPassword"
|
||||
autoComplete="new-password"
|
||||
type="password"
|
||||
label={t("confirm_password")}
|
||||
value={formik.values.confirmPassword}
|
||||
onChange={formik.handleChange}
|
||||
error={!!formik.errors.confirmPassword}
|
||||
helperText={formik.errors.confirmPassword}
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
/>
|
||||
<PasswordStrengthHint password={formik.values.password} />
|
||||
<InputLabel
|
||||
htmlFor="referral"
|
||||
sx={{ color: "text.muted", mt: "24px", mx: "2px" }}
|
||||
>
|
||||
{t("referral_source_hint")}
|
||||
</InputLabel>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
id="referral"
|
||||
type="text"
|
||||
value={formik.values.referral}
|
||||
onChange={formik.handleChange}
|
||||
error={!!formik.errors.referral}
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title={t("referral_source_info")}>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
color="secondary"
|
||||
edge={"end"}
|
||||
>
|
||||
<InfoOutlinedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FormGroup sx={{ color: "text.muted", mt: 2, mx: "4px" }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="acceptedTerms"
|
||||
size="small"
|
||||
color="accent"
|
||||
checked={formik.values.acceptedTerms}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
<TextField
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="username"
|
||||
label={t("enter_email")}
|
||||
value={formik.values.email}
|
||||
onChange={formik.handleChange}
|
||||
error={!!formik.errors.email}
|
||||
helperText={formik.errors.email}
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
label={t("password")}
|
||||
value={formik.values.password}
|
||||
onChange={formik.handleChange}
|
||||
error={!!formik.errors.password}
|
||||
helperText={formik.errors.password}
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<ShowHidePasswordInputAdornment
|
||||
showPassword={showPassword}
|
||||
onToggle={handleToggleShowHidePassword}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="small">
|
||||
<Trans
|
||||
i18nKey={"terms_and_conditions"}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
href="https://ente.io/terms"
|
||||
target="_blank"
|
||||
/>
|
||||
),
|
||||
b: (
|
||||
<Link
|
||||
href="https://ente.io/privacy"
|
||||
target="_blank"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
name="confirmPassword"
|
||||
autoComplete="new-password"
|
||||
type="password"
|
||||
label={t("confirm_password")}
|
||||
value={formik.values.confirmPassword}
|
||||
onChange={formik.handleChange}
|
||||
error={!!formik.errors.confirmPassword}
|
||||
helperText={formik.errors.confirmPassword}
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
/>
|
||||
<PasswordStrengthHint password={formik.values.password} />
|
||||
<InputLabel
|
||||
htmlFor="referral"
|
||||
sx={{ color: "text.muted", mt: "24px", mx: "2px" }}
|
||||
>
|
||||
{t("referral_source_hint")}
|
||||
</InputLabel>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
id="referral"
|
||||
type="text"
|
||||
value={formik.values.referral}
|
||||
onChange={formik.handleChange}
|
||||
error={!!formik.errors.referral}
|
||||
disabled={formik.isSubmitting}
|
||||
fullWidth
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title={t("referral_source_info")}>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
color="secondary"
|
||||
edge={"end"}
|
||||
>
|
||||
<InfoOutlinedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FormGroup sx={{ color: "text.muted", mt: 2, mb: 2.5, mx: "4px" }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="acceptedTerms"
|
||||
size="small"
|
||||
color="accent"
|
||||
checked={formik.values.acceptedTerms}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="small">
|
||||
<Trans
|
||||
i18nKey={"terms_and_conditions"}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
href="https://ente.io/terms"
|
||||
target="_blank"
|
||||
/>
|
||||
),
|
||||
b: (
|
||||
<Link
|
||||
href="https://ente.io/privacy"
|
||||
target="_blank"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<LoadingButton
|
||||
fullWidth
|
||||
color="accent"
|
||||
|
||||
@@ -8,7 +8,6 @@ import SetPasswordForm, {
|
||||
} from "ente-accounts/components/SetPasswordForm";
|
||||
import { appHomeRoute, stashRedirect } from "ente-accounts/services/redirect";
|
||||
import {
|
||||
deriveSRPPassword,
|
||||
generateSRPSetupAttributes,
|
||||
getSRPAttributes,
|
||||
srpSetupOrReconfigure,
|
||||
@@ -105,13 +104,8 @@ const PageContents: React.FC<PageContentsProps> = ({ user }) => {
|
||||
memLimit,
|
||||
};
|
||||
|
||||
const loginSubKey = await deriveSRPPassword(kek);
|
||||
|
||||
const { srpUserID, srpSalt, srpVerifier } =
|
||||
await generateSRPSetupAttributes(loginSubKey);
|
||||
|
||||
await srpSetupOrReconfigure(
|
||||
{ srpSalt, srpUserID, srpVerifier, loginSubKey },
|
||||
await generateSRPSetupAttributes(kek),
|
||||
({ setupID, srpM1 }) =>
|
||||
updateSRPAndKeys(token, { setupID, srpM1, updatedKeyAttr }),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ import { checkSessionValidity } from "ente-accounts/services/session";
|
||||
import type { SRPAttributes } from "ente-accounts/services/srp";
|
||||
import {
|
||||
configureSRP,
|
||||
deriveSRPPassword,
|
||||
generateSRPSetupAttributes,
|
||||
getSRPAttributes,
|
||||
loginViaSRP,
|
||||
@@ -272,10 +271,7 @@ const Page: React.FC = () => {
|
||||
}
|
||||
log.debug(() => `userSRPSetupPending ${!srpAttributes}`);
|
||||
if (!srpAttributes) {
|
||||
const loginSubKey = await deriveSRPPassword(kek);
|
||||
const srpSetupAttributes =
|
||||
await generateSRPSetupAttributes(loginSubKey);
|
||||
await configureSRP(srpSetupAttributes);
|
||||
await configureSRP(await generateSRPSetupAttributes(kek));
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("migrate to srp failed", e);
|
||||
|
||||
@@ -10,7 +10,6 @@ import SetPasswordForm, {
|
||||
import { appHomeRoute } from "ente-accounts/services/redirect";
|
||||
import {
|
||||
configureSRP,
|
||||
deriveSRPPassword,
|
||||
generateSRPSetupAttributes,
|
||||
} from "ente-accounts/services/srp";
|
||||
import type { KeyAttributes, User } from "ente-accounts/services/user";
|
||||
@@ -74,12 +73,8 @@ const Page: React.FC = () => {
|
||||
const { masterKey, kek, keyAttributes } =
|
||||
await generateKeysAndAttributes(passphrase);
|
||||
|
||||
const srpSetupAttributes = await generateSRPSetupAttributes(
|
||||
await deriveSRPPassword(kek),
|
||||
);
|
||||
|
||||
await putUserKeyAttributes(keyAttributes);
|
||||
await configureSRP(srpSetupAttributes);
|
||||
await configureSRP(await generateSRPSetupAttributes(kek));
|
||||
await generateAndSaveInteractiveKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
import {
|
||||
configureSRP,
|
||||
getSRPAttributes,
|
||||
unstashAndUseSRPSetupAttributes,
|
||||
type SRPAttributes,
|
||||
type SRPSetupAttributes,
|
||||
} from "ente-accounts/services/srp";
|
||||
import type { KeyAttributes, User } from "ente-accounts/services/user";
|
||||
import {
|
||||
@@ -146,11 +146,7 @@ const Page: React.FC = () => {
|
||||
if (originalKeyAttributes) {
|
||||
await putUserKeyAttributes(originalKeyAttributes);
|
||||
}
|
||||
if (getData("srpSetupAttributes")) {
|
||||
const srpSetupAttributes: SRPSetupAttributes =
|
||||
getData("srpSetupAttributes");
|
||||
await configureSRP(srpSetupAttributes);
|
||||
}
|
||||
await unstashAndUseSRPSetupAttributes(configureSRP);
|
||||
}
|
||||
// TODO(RE): Temporary safety valve before removing the
|
||||
// unnecessary clear (tag: Migration)
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { HttpStatusCode } from "axios";
|
||||
import { deriveSubKeyBytes, sharedCryptoWorker, toB64 } from "ente-base/crypto";
|
||||
import { ensureOk, publicRequestHeaders } from "ente-base/http";
|
||||
import {
|
||||
deriveSubKeyBytes,
|
||||
generateDeriveKeySalt,
|
||||
toB64,
|
||||
} from "ente-base/crypto";
|
||||
import {
|
||||
authenticatedRequestHeaders,
|
||||
ensureOk,
|
||||
publicRequestHeaders,
|
||||
} from "ente-base/http";
|
||||
import log from "ente-base/log";
|
||||
import { apiURL } from "ente-base/origins";
|
||||
import { ApiError, CustomError } from "ente-shared/error";
|
||||
import HTTPService from "ente-shared/network/HTTPService";
|
||||
import { getToken } from "ente-shared/storage/localStorage/helpers";
|
||||
import { SRP, SrpClient } from "fast-srp-hap";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod/v4";
|
||||
@@ -236,27 +243,200 @@ export const saveSRPAttributes = (srpAttributes: SRPAttributes) =>
|
||||
localStorage.setItem("srpAttributes", JSON.stringify(srpAttributes));
|
||||
|
||||
/**
|
||||
* Derive a "password" (which is really an arbitrary binary value, not human
|
||||
* generated) for use as the SRP user password by applying a deterministic KDF
|
||||
* (Key Derivation Function) to the provided {@link kek}.
|
||||
* A local-only structure holding information required for SRP setup.
|
||||
*
|
||||
* [Note: SRP setup attributes]
|
||||
*
|
||||
* In some cases, there might be a step between the client having access to the
|
||||
* KEK (which we need for generating the SRP attributes, in particular the SRP
|
||||
* password) and the time where the client can proceed with the SRP setup (which
|
||||
* can only happen once we have an auth token).
|
||||
*
|
||||
* For example, when the user is signing up for a new account, the client has
|
||||
* the KEK on the signup screen since the user just set their password, but then
|
||||
* has to redirect to the screen for email verification, and it is only after
|
||||
* email verification that the client obtains an auth token and the SRP setup
|
||||
* can proceed (at which point it doesn't have access to the password and so
|
||||
* cannot derive the KEK).
|
||||
*
|
||||
* This gap is not just about different screens, but since there is an email
|
||||
* verification step involved, it might take time enough for the browser tab to
|
||||
* get closed and reopened. So instead of keeping the attributes we need to
|
||||
* continue with the SRP setup after email verification in memory, we
|
||||
* temporarily stash them in local storage using an object that conforms to the
|
||||
* following {@link SRPSetupAttributes} schema.
|
||||
*/
|
||||
const SRPSetupAttributes = z.object({
|
||||
srpUserID: z.string(),
|
||||
srpSalt: z.string(),
|
||||
srpVerifier: z.string(),
|
||||
loginSubKey: z.string(),
|
||||
});
|
||||
|
||||
export type SRPSetupAttributes = z.infer<typeof SRPSetupAttributes>;
|
||||
|
||||
/**
|
||||
* Generate {@link SRPSetupAttributes} from the provided {@link kek}.
|
||||
*
|
||||
* @param kek The designated key encryption key (base64 encoded) for the user.
|
||||
* This will be used to (deterministically) derive a SRP password.
|
||||
*/
|
||||
export const generateSRPSetupAttributes = async (
|
||||
kek: string,
|
||||
): Promise<SRPSetupAttributes> => {
|
||||
const loginSubKey = await deriveSRPLoginSubKey(kek);
|
||||
|
||||
// Museum schema requires this to be a UUID.
|
||||
const srpUserID = uuidv4();
|
||||
const srpSalt = await generateDeriveKeySalt();
|
||||
|
||||
const srpVerifier = bufferToB64(
|
||||
SRP.computeVerifier(
|
||||
SRP.params["4096"],
|
||||
b64ToBuffer(srpSalt),
|
||||
Buffer.from(srpUserID),
|
||||
b64ToBuffer(loginSubKey),
|
||||
),
|
||||
);
|
||||
|
||||
return { srpUserID, srpSalt, srpVerifier, loginSubKey };
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive a "login sub-key" (which is really an arbitrary binary value, not
|
||||
* human generated) for use as the SRP user password by applying a deterministic
|
||||
* KDF (Key Derivation Function) to the provided {@link kek}.
|
||||
*
|
||||
* @param kek The user's KEK (key encryption key) as a base64 string.
|
||||
*
|
||||
* @returns A string that can be used as the SRP user password.
|
||||
* @returns A base64 encoded key that can be used as the SRP user password.
|
||||
*/
|
||||
export const deriveSRPPassword = async (kek: string) => {
|
||||
const deriveSRPLoginSubKey = async (kek: string) => {
|
||||
const kekSubKeyBytes = await deriveSubKeyBytes(kek, 32, 1, "loginctx");
|
||||
// Use the first 16 bytes (128 bits) of the KEK's KDF subkey as the SRP
|
||||
// password (instead of entire 32 bytes).
|
||||
return toB64(kekSubKeyBytes.slice(0, 16));
|
||||
};
|
||||
|
||||
export interface SRPSetupAttributes {
|
||||
srpSalt: string;
|
||||
srpVerifier: string;
|
||||
srpUserID: string;
|
||||
loginSubKey: string;
|
||||
}
|
||||
const b64ToBuffer = (base64: string) => Buffer.from(base64, "base64");
|
||||
|
||||
const bufferToB64 = (buffer: Buffer) => buffer.toString("base64");
|
||||
|
||||
/**
|
||||
* Save {@link SRPSetupAttributes} in local storage for later use via
|
||||
* {@link unstashAndUseSRPSetupAttributes}.
|
||||
*
|
||||
* See: [Note: SRP setup attributes]
|
||||
*/
|
||||
export const stashSRPSetupAttributes = (
|
||||
srpSetupAttributes: SRPSetupAttributes,
|
||||
) =>
|
||||
localStorage.setItem(
|
||||
"srpSetupAttributes",
|
||||
JSON.stringify(srpSetupAttributes),
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve the {@link SRPSetupAttributes}, if any, that were stashed by a
|
||||
* previous call to {@link stashSRPSetupAttributes}.
|
||||
*
|
||||
* - If they are found, then invoke the provided callback ({@link cb}) with the
|
||||
* value. If the promise returned by the callback fulfills, then remove the
|
||||
* stashed value from local storage.
|
||||
*
|
||||
* - If they are not found, then the callback is not invoked.
|
||||
*/
|
||||
export const unstashAndUseSRPSetupAttributes = async (
|
||||
cb: (srpSetupAttributes: SRPSetupAttributes) => Promise<void>,
|
||||
) => {
|
||||
const jsonString = localStorage.getItem("srpSetupAttributes");
|
||||
if (!jsonString) return;
|
||||
const srpSetupAttributes = SRPSetupAttributes.parse(JSON.parse(jsonString));
|
||||
await cb(srpSetupAttributes);
|
||||
localStorage.removeItem("srpSetupAttributes");
|
||||
};
|
||||
|
||||
/**
|
||||
* Use the provided {@link SRPSetupAttributes} to, well, setup SRP.
|
||||
*
|
||||
* See: [Note: SRP setup]
|
||||
*
|
||||
* @param srpSetupAttributes SRP setup attributes.
|
||||
*/
|
||||
export const configureSRP = async (srpSetupAttributes: SRPSetupAttributes) =>
|
||||
srpSetupOrReconfigure(srpSetupAttributes, completeSRPSetup);
|
||||
|
||||
/**
|
||||
* A function that is called by {@link srpSetupOrReconfigure} to exchange the
|
||||
* evidence message M1 for the evidence message M2 from remote.
|
||||
*
|
||||
* It is passed M1, and is expected to fulfill with M2.
|
||||
*/
|
||||
type SRPSetupOrReconfigureExchangeCallback = ({
|
||||
setupID,
|
||||
srpM1,
|
||||
}: {
|
||||
setupID: string;
|
||||
srpM1: string;
|
||||
}) => Promise<{ srpM2: string }>;
|
||||
|
||||
/**
|
||||
* Use the provided {@link SRPSetupAttributes} to either setup (afresh) or
|
||||
* reconfigure SRP (when the user changes their password).
|
||||
*
|
||||
* The flow (described in [Note: SRP setup]) is mostly the same except the tail
|
||||
* end of the process where we exchange the evidence message M1 for the evidence
|
||||
* message M2 from remote. To handle this variance, we provide a callback
|
||||
* {@link exchangeCB}) that is invoked at this point in the sequence.
|
||||
*
|
||||
* @param srpSetupAttributes SRP setup attributes.
|
||||
*/
|
||||
export const srpSetupOrReconfigure = async (
|
||||
{ srpSalt, srpUserID, srpVerifier, loginSubKey }: SRPSetupAttributes,
|
||||
exchangeCB: SRPSetupOrReconfigureExchangeCallback,
|
||||
) => {
|
||||
const srpClient = await generateSRPClient(srpSalt, srpUserID, loginSubKey);
|
||||
|
||||
const srpA = bufferToB64(srpClient.computeA());
|
||||
|
||||
const { setupID, srpB } = await startSRPSetup({
|
||||
srpUserID,
|
||||
srpSalt,
|
||||
srpVerifier,
|
||||
srpA,
|
||||
});
|
||||
|
||||
srpClient.setB(b64ToBuffer(srpB));
|
||||
|
||||
const srpM1 = bufferToB64(srpClient.computeM1());
|
||||
|
||||
const { srpM2 } = await exchangeCB({ srpM1, setupID });
|
||||
|
||||
srpClient.checkM2(b64ToBuffer(srpM2));
|
||||
};
|
||||
|
||||
const generateSRPClient = async (
|
||||
srpSalt: string,
|
||||
srpUserID: string,
|
||||
loginSubKey: string,
|
||||
) =>
|
||||
new Promise<SrpClient>((resolve, reject) => {
|
||||
SRP.genKey((err, clientKey) => {
|
||||
if (err) reject(err);
|
||||
resolve(
|
||||
new SrpClient(
|
||||
SRP.params["4096"],
|
||||
b64ToBuffer(srpSalt),
|
||||
Buffer.from(srpUserID),
|
||||
b64ToBuffer(loginSubKey),
|
||||
// The random `clientKey` parameterizes the current instance
|
||||
// of the SRP client.
|
||||
clientKey!,
|
||||
false,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
interface SetupSRPRequest {
|
||||
srpUserID: string;
|
||||
@@ -265,20 +445,55 @@ interface SetupSRPRequest {
|
||||
srpA: string;
|
||||
}
|
||||
|
||||
interface SetupSRPResponse {
|
||||
setupID: string;
|
||||
srpB: string;
|
||||
}
|
||||
const SetupSRPResponse = z.object({ setupID: z.string(), srpB: z.string() });
|
||||
|
||||
type SetupSRPResponse = z.infer<typeof SetupSRPResponse>;
|
||||
|
||||
/**
|
||||
* Initiate SRP setup on remote.
|
||||
*
|
||||
* Part of the [Note: SRP setup] sequence.
|
||||
*/
|
||||
const startSRPSetup = async (
|
||||
setupSRPRequest: SetupSRPRequest,
|
||||
): Promise<SetupSRPResponse> => {
|
||||
const res = await fetch(await apiURL("/users/srp/setup"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify(setupSRPRequest),
|
||||
});
|
||||
ensureOk(res);
|
||||
return SetupSRPResponse.parse(await res.json());
|
||||
};
|
||||
|
||||
interface CompleteSRPSetupRequest {
|
||||
setupID: string;
|
||||
srpM1: string;
|
||||
}
|
||||
|
||||
interface CompleteSRPSetupResponse {
|
||||
setupID: string;
|
||||
srpM2: string;
|
||||
}
|
||||
const CompleteSRPSetupResponse = z.object({
|
||||
setupID: z.string(),
|
||||
srpM2: z.string(),
|
||||
});
|
||||
|
||||
type CompleteSRPSetupResponse = z.infer<typeof CompleteSRPSetupResponse>;
|
||||
|
||||
/**
|
||||
* Complete a previously initiated SRP setup on remote.
|
||||
*
|
||||
* Part of the [Note: SRP setup] sequence.
|
||||
*/
|
||||
const completeSRPSetup = async (
|
||||
completeSRPSetupRequest: CompleteSRPSetupRequest,
|
||||
) => {
|
||||
const res = await fetch(await apiURL("/users/srp/complete"), {
|
||||
method: "POST",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify(completeSRPSetupRequest),
|
||||
});
|
||||
ensureOk(res);
|
||||
return CompleteSRPSetupResponse.parse(await res.json());
|
||||
};
|
||||
|
||||
interface CreateSRPSessionResponse {
|
||||
sessionID: string;
|
||||
@@ -313,43 +528,6 @@ export interface UpdateSRPAndKeysResponse {
|
||||
setupID: string;
|
||||
}
|
||||
|
||||
export const startSRPSetup = async (
|
||||
token: string,
|
||||
setupSRPRequest: SetupSRPRequest,
|
||||
): Promise<SetupSRPResponse> => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
await apiURL("/users/srp/setup"),
|
||||
setupSRPRequest,
|
||||
undefined,
|
||||
{ "X-Auth-Token": token },
|
||||
);
|
||||
|
||||
return resp.data as SetupSRPResponse;
|
||||
} catch (e) {
|
||||
log.error("failed to post SRP attributes", e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const completeSRPSetup = async (
|
||||
token: string,
|
||||
completeSRPSetupRequest: CompleteSRPSetupRequest,
|
||||
) => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
await apiURL("/users/srp/complete"),
|
||||
completeSRPSetupRequest,
|
||||
undefined,
|
||||
{ "X-Auth-Token": token },
|
||||
);
|
||||
return resp.data as CompleteSRPSetupResponse;
|
||||
} catch (e) {
|
||||
log.error("failed to complete SRP setup", e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSRPAndKeys = async (
|
||||
token: string,
|
||||
updateSRPAndKeyRequest: UpdateSRPAndKeysRequest,
|
||||
@@ -368,123 +546,12 @@ export const updateSRPAndKeys = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const configureSRP = async (attr: SRPSetupAttributes) =>
|
||||
srpSetupOrReconfigure(attr, (cbAttr) =>
|
||||
completeSRPSetup(getToken(), cbAttr),
|
||||
);
|
||||
|
||||
export const srpSetupOrReconfigure = async (
|
||||
{ srpSalt, srpUserID, srpVerifier, loginSubKey }: SRPSetupAttributes,
|
||||
cb: ({
|
||||
setupID,
|
||||
srpM1,
|
||||
}: {
|
||||
setupID: string;
|
||||
srpM1: string;
|
||||
}) => Promise<{ srpM2: string }>,
|
||||
) => {
|
||||
const srpClient = await generateSRPClient(srpSalt, srpUserID, loginSubKey);
|
||||
|
||||
const srpA = convertBufferToBase64(srpClient.computeA());
|
||||
|
||||
const token = getToken();
|
||||
const { setupID, srpB } = await startSRPSetup(token, {
|
||||
srpA,
|
||||
srpUserID,
|
||||
srpSalt,
|
||||
srpVerifier,
|
||||
});
|
||||
|
||||
srpClient.setB(convertBase64ToBuffer(srpB));
|
||||
|
||||
const srpM1 = convertBufferToBase64(srpClient.computeM1());
|
||||
|
||||
const { srpM2 } = await cb({ srpM1, setupID });
|
||||
|
||||
srpClient.checkM2(convertBase64ToBuffer(srpM2));
|
||||
};
|
||||
|
||||
export const generateSRPClient = async (
|
||||
srpSalt: string,
|
||||
srpUserID: string,
|
||||
loginSubKey: string,
|
||||
) => {
|
||||
return new Promise<SrpClient>((resolve, reject) => {
|
||||
SRP.genKey(function (err, secret1) {
|
||||
try {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
if (!secret1) {
|
||||
throw Error("secret1 gen failed");
|
||||
}
|
||||
const srpClient = new SrpClient(
|
||||
SRP.params["4096"],
|
||||
convertBase64ToBuffer(srpSalt),
|
||||
Buffer.from(srpUserID),
|
||||
convertBase64ToBuffer(loginSubKey),
|
||||
secret1,
|
||||
false,
|
||||
);
|
||||
|
||||
resolve(srpClient);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const convertBufferToBase64 = (buffer: Buffer) => {
|
||||
return buffer.toString("base64");
|
||||
};
|
||||
|
||||
export const convertBase64ToBuffer = (base64: string) => {
|
||||
return Buffer.from(base64, "base64");
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param loginSubKey The user's SRP password (autogenerated, derived
|
||||
* deterministically from their KEK by {@link deriveSRPPassword}).
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const generateSRPSetupAttributes = async (
|
||||
loginSubKey: string,
|
||||
): Promise<SRPSetupAttributes> => {
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
|
||||
const srpSalt = await cryptoWorker.generateDeriveKeySalt();
|
||||
|
||||
// Museum schema requires this to be a UUID.
|
||||
const srpUserID = uuidv4();
|
||||
|
||||
const srpVerifierBuffer = SRP.computeVerifier(
|
||||
SRP.params["4096"],
|
||||
convertBase64ToBuffer(srpSalt),
|
||||
Buffer.from(srpUserID),
|
||||
convertBase64ToBuffer(loginSubKey),
|
||||
);
|
||||
|
||||
const srpVerifier = convertBufferToBase64(srpVerifierBuffer);
|
||||
|
||||
const result = { srpUserID, srpSalt, srpVerifier, loginSubKey };
|
||||
|
||||
log.debug(
|
||||
() => `SRP setup attributes generated: ${JSON.stringify(result)}`,
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const loginViaSRP = async (
|
||||
srpAttributes: SRPAttributes,
|
||||
kek: string,
|
||||
): Promise<UserVerificationResponse> => {
|
||||
try {
|
||||
const loginSubKey = await deriveSRPPassword(kek);
|
||||
const loginSubKey = await deriveSRPLoginSubKey(kek);
|
||||
const srpClient = await generateSRPClient(
|
||||
srpAttributes.srpSalt,
|
||||
srpAttributes.srpUserID,
|
||||
@@ -493,20 +560,20 @@ export const loginViaSRP = async (
|
||||
const srpA = srpClient.computeA();
|
||||
const { srpB, sessionID } = await createSRPSession(
|
||||
srpAttributes.srpUserID,
|
||||
convertBufferToBase64(srpA),
|
||||
bufferToB64(srpA),
|
||||
);
|
||||
srpClient.setB(convertBase64ToBuffer(srpB));
|
||||
srpClient.setB(b64ToBuffer(srpB));
|
||||
|
||||
const m1 = srpClient.computeM1();
|
||||
log.debug(() => `srp m1: ${convertBufferToBase64(m1)}`);
|
||||
log.debug(() => `srp m1: ${bufferToB64(m1)}`);
|
||||
const { srpM2, ...rest } = await verifySRPSession(
|
||||
sessionID,
|
||||
srpAttributes.srpUserID,
|
||||
convertBufferToBase64(m1),
|
||||
bufferToB64(m1),
|
||||
);
|
||||
log.debug(() => `srp verify session successful,srpM2: ${srpM2}`);
|
||||
|
||||
srpClient.checkM2(convertBase64ToBuffer(srpM2));
|
||||
srpClient.checkM2(b64ToBuffer(srpM2));
|
||||
|
||||
log.debug(() => `srp server verify successful`);
|
||||
return rest;
|
||||
|
||||
@@ -15,7 +15,8 @@ export type LocalStorageKey =
|
||||
| "collectionSortBy"
|
||||
// Moved to the new wrapper ente-base/local-storage
|
||||
// LOCALE = 'locale',
|
||||
| "srpSetupAttributes"
|
||||
// Moved to ente-accounts
|
||||
// "srpSetupAttributes"
|
||||
| "srpAttributes"
|
||||
| "referralSource";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user