From c3ca9927771a464100a6fd5b5ddbae27df0925dc Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Sun, 12 Nov 2023 23:30:41 +0530 Subject: [PATCH] feat(onboarding): added backup key generation for admin account --- .../components/utilities/generateBackupPDF.ts | 13 +- frontend/src/lib/crypto/index.ts | 180 ++++++++------- .../src/views/admin/SignUpPage/SignUpPage.tsx | 211 +++++++++++------- .../DownloadBackupKeys/DownloadBackupKeys.tsx | 53 +++++ .../components/DownloadBackupKeys/index.tsx | 1 + 5 files changed, 305 insertions(+), 153 deletions(-) create mode 100644 frontend/src/views/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx create mode 100644 frontend/src/views/admin/SignUpPage/components/DownloadBackupKeys/index.tsx diff --git a/frontend/src/components/utilities/generateBackupPDF.ts b/frontend/src/components/utilities/generateBackupPDF.ts index 3dace44da..bf3b430c6 100644 --- a/frontend/src/components/utilities/generateBackupPDF.ts +++ b/frontend/src/components/utilities/generateBackupPDF.ts @@ -16,7 +16,6 @@ const yyyy = today.getFullYear(); const todayFormatted = `${mm}/${dd}/${yyyy}`; - function createPdfHeader(doc: jsPDF, personalName: string) { doc.setFillColor(255, 255, 255); doc.rect(0, 0, 600, 900, "F"); @@ -92,5 +91,15 @@ function generateBackupPDF({ personalName, personalEmail, generatedKey }: PDFPro doc.save("Infisical Emergency Kit.pdf"); } -export default generateBackupPDF; +/** + * This function generate a pdf with a secret key for a user. + */ +export function generateBackupPDFAsync({ personalName, personalEmail, generatedKey }: PDFProps) { + // eslint-disable-next-line new-cap + const doc = new jsPDF("p", "pt", "a4", true); + createPdfHeader(doc, personalName); + createPdfContent(doc, personalEmail, generatedKey); + return doc.save("Infisical Emergency Kit.pdf", { returnPromise: true }); +} +export default generateBackupPDF; diff --git a/frontend/src/lib/crypto/index.ts b/frontend/src/lib/crypto/index.ts index 21534cee2..d80dc086c 100644 --- a/frontend/src/lib/crypto/index.ts +++ b/frontend/src/lib/crypto/index.ts @@ -6,91 +6,121 @@ import { encodeBase64 } from "tweetnacl-util"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; +import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); +export const generateUserBackupKey = async (email: string, password: string) => { + // eslint-disable-next-line new-cap + const clientKey = new jsrp.client(); + // eslint-disable-next-line new-cap + const clientPassword = new jsrp.client(); -type TUserPassKey = { - protectedKey: string; - protectedKeyTag: string; - protectedKeyIV: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - publicKey: string; - verifier: string; - salt: string; - privateKey: string; + await new Promise((resolve) => { + clientPassword.init({ username: email, password }, () => resolve(null)); + }); + const clientPublicKey = clientPassword.getPublicKey(); + const srpKeys = await srp1({ clientPublicKey }); + clientPassword.setSalt(srpKeys.salt); + clientPassword.setServerPublicKey(srpKeys.serverPublicKey); + + const clientProof = clientPassword.getProof(); // called M1 + const generatedKey = crypto.randomBytes(16).toString("hex"); + + await new Promise((resolve) => { + clientKey.init({ username: email, password: generatedKey }, () => resolve(null)); + }); + + const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>( + (resolve, reject) => { + clientKey.createVerifier((err, res) => { + if (err) return reject(err); + return resolve(res); + }); + } + ); + const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ + text: String(localStorage.getItem("PRIVATE_KEY")), + secret: generatedKey + }); + + await issueBackupPrivateKey({ + encryptedPrivateKey: ciphertext, + iv, + tag, + salt, + verifier, + clientProof + }); + + return generatedKey; }; export const generateUserPassKey = async (email: string, password: string) => { + // eslint-disable-next-line new-cap + const client = new jsrp.client(); + const pair = nacl.box.keyPair(); const secretKeyUint8Array = pair.secretKey; const publicKeyUint8Array = pair.publicKey; const privateKey = encodeBase64(secretKeyUint8Array); const publicKey = encodeBase64(publicKeyUint8Array); - return new Promise((resolve, reject) => { - client.init({ username: email, password }, () => { - client.createVerifier( - async (err: any, { salt, verifier }: { salt: string; verifier: string }) => { - if (err) { - return reject(err); - } - try { - // TODO: moduralize into KeyService - const derivedKey = await deriveArgonKey({ - password, - salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); - - if (!derivedKey) throw new Error("Failed to derive key from password"); - - const key = crypto.randomBytes(32); - - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - return resolve({ - protectedKey, - protectedKeyTag, - protectedKeyIV, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - publicKey, - verifier, - salt, - privateKey - }); - } catch (error) { - return reject(error); - } - } - ); - }); + await new Promise((resolve) => { + client.init({ username: email, password }, () => resolve(null)); }); + const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>( + (resolve, reject) => { + client.createVerifier((err, res) => { + if (err) return reject(err); + return resolve(res); + }); + } + ); + + const derivedKey = await deriveArgonKey({ + password, + salt, + mem: 65536, + time: 3, + parallelism: 1, + hashLen: 32 + }); + + if (!derivedKey) throw new Error("Failed to derive key from password"); + + const key = crypto.randomBytes(32); + + // create encrypted private key by encrypting the private + // key with the symmetric key [key] + const { + ciphertext: encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + } = Aes256Gcm.encrypt({ + text: privateKey, + secret: key + }); + + // create the protected key by encrypting the symmetric key + // [key] with the derived key + const { + ciphertext: protectedKey, + iv: protectedKeyIV, + tag: protectedKeyTag + } = Aes256Gcm.encrypt({ + text: key.toString("hex"), + secret: Buffer.from(derivedKey.hash) + }); + + return { + protectedKey, + protectedKeyTag, + protectedKeyIV, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + publicKey, + verifier, + salt, + privateKey + }; }; diff --git a/frontend/src/views/admin/SignUpPage/SignUpPage.tsx b/frontend/src/views/admin/SignUpPage/SignUpPage.tsx index 544204b13..d3c83fc17 100644 --- a/frontend/src/views/admin/SignUpPage/SignUpPage.tsx +++ b/frontend/src/views/admin/SignUpPage/SignUpPage.tsx @@ -1,19 +1,23 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useRouter } from "next/router"; import { zodResolver } from "@hookform/resolvers/zod"; +import { AnimatePresence, motion } from "framer-motion"; import { z } from "zod"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { generateBackupPDFAsync } from "@app/components/utilities/generateBackupPDF"; // TODO(akhilmhdh): rewrite this into module functions in lib import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, ContentLoader, FormControl, Input } from "@app/components/v2"; import { useServerConfig } from "@app/context"; import { useCreateAdminUser } from "@app/hooks/api"; -import { generateUserPassKey } from "@app/lib/crypto"; +import { generateUserBackupKey, generateUserPassKey } from "@app/lib/crypto"; import { isLoggedIn } from "@app/reactQuery"; +import { DownloadBackupKeys } from "./components/DownloadBackupKeys"; + const formSchema = z .object({ email: z.string().email().trim(), @@ -29,16 +33,23 @@ const formSchema = z type TFormSchema = z.infer; +enum SignupSteps { + DetailsForm = "details-form", + BackupKey = "backup-key" +} + export const SignUpPage = () => { const router = useRouter(); const { control, handleSubmit, + getValues, formState: { isSubmitting } } = useForm({ resolver: zodResolver(formSchema) }); const { createNotification } = useNotificationContext(); + const [step, setStep] = useState(SignupSteps.DetailsForm); const { config } = useServerConfig(); @@ -50,7 +61,7 @@ export const SignUpPage = () => { router.push("/login"); } } - }, [config.initialized]); + }, []); const { mutateAsync: createAdminUser } = useCreateAdminUser(); @@ -73,6 +84,7 @@ export const SignUpPage = () => { tag: userPass.encryptedPrivateKeyTag, privateKey }); + setStep(SignupSteps.BackupKey); } catch (err) { console.log(err); createNotification({ @@ -82,83 +94,130 @@ export const SignUpPage = () => { } }; - if (config?.initialized) return ; + const handleBackupKeyGenerate = async () => { + try { + const { email, password, firstName, lastName } = getValues(); + const generatedKey = await generateUserBackupKey(email, password); + await generateBackupPDFAsync({ + generatedKey, + personalEmail: email, + personalName: `${firstName} ${lastName}` + }); + router.push("/admin"); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Faield to generate backup" + }); + } + }; + + if (config?.initialized && step === SignupSteps.DetailsForm) + return ; return (
-
-
- Infisical logo -
Welcome to Infisical
-
Create your first Admin Account
-
-
-
-
- ( - - - - )} - /> - ( - - - - )} - /> + + {step === SignupSteps.DetailsForm && ( + +
+ Infisical logo +
Welcome to Infisical
+
Create your first Admin Account
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> -
- - -
+
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ + )} + {step === SignupSteps.BackupKey && ( + + + + )} +
); }; diff --git a/frontend/src/views/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx b/frontend/src/views/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx new file mode 100644 index 000000000..d431aae44 --- /dev/null +++ b/frontend/src/views/admin/SignUpPage/components/DownloadBackupKeys/DownloadBackupKeys.tsx @@ -0,0 +1,53 @@ +import { useTranslation } from "react-i18next"; +import { faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Button } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; + +type Props = { + onGenerate: () => Promise; +}; + +export const DownloadBackupKeys = ({ onGenerate }: Props): JSX.Element => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useToggle(); + + return ( +
+

+ + {t("signup.step4-message")} +

+
+
+ + {t("signup.step4-description1")} {t("signup.step4-description3")} + +
+
+
+ +
+
+
+
+ ); +}; diff --git a/frontend/src/views/admin/SignUpPage/components/DownloadBackupKeys/index.tsx b/frontend/src/views/admin/SignUpPage/components/DownloadBackupKeys/index.tsx new file mode 100644 index 000000000..bbbd9aad9 --- /dev/null +++ b/frontend/src/views/admin/SignUpPage/components/DownloadBackupKeys/index.tsx @@ -0,0 +1 @@ +export { DownloadBackupKeys } from "./DownloadBackupKeys";