From 286426b2403ec37e62b9161ec6b1f38c762e9163 Mon Sep 17 00:00:00 2001 From: ShubhamPalriwala Date: Wed, 29 May 2024 14:09:23 +0530 Subject: [PATCH] feat: use hash as pw & move to symmetric encrpytion --- .../20240528190137_secret_sharing.ts | 5 +- backend/src/db/schemas/secret-sharing.ts | 5 +- .../server/routes/v1/secret-sharing-router.ts | 26 +++++-- .../secret-sharing/secret-sharing-service.ts | 13 ++-- .../secret-sharing/secret-sharing-types.ts | 5 +- .../utilities/cryptography/crypto.ts | 73 ------------------- .../src/hooks/api/secretSharing/queries.ts | 8 +- frontend/src/hooks/api/secretSharing/types.ts | 14 +++- .../components/AddShareSecretModal.tsx | 34 +++++---- .../ShareSecretPublicPage.tsx | 25 ++++--- 10 files changed, 89 insertions(+), 119 deletions(-) diff --git a/backend/src/db/migrations/20240528190137_secret_sharing.ts b/backend/src/db/migrations/20240528190137_secret_sharing.ts index c602905cc..c1eab2ea6 100644 --- a/backend/src/db/migrations/20240528190137_secret_sharing.ts +++ b/backend/src/db/migrations/20240528190137_secret_sharing.ts @@ -8,7 +8,10 @@ export async function up(knex: Knex): Promise { await knex.schema.createTable(TableName.SecretSharing, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("name").notNullable(); - t.text("signedValue").notNullable(); + t.text("encryptedValue").notNullable(); + t.text("iv").notNullable(); + t.text("tag").notNullable(); + t.text("hashedHex").notNullable(); t.timestamp("expiresAt").notNullable(); t.uuid("userId").notNullable(); t.uuid("orgId").notNullable(); diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index 532f1e310..a412221b2 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -10,7 +10,10 @@ import { TImmutableDBKeys } from "./models"; export const SecretSharingSchema = z.object({ id: z.string().uuid(), name: z.string(), - signedValue: z.string(), + encryptedValue: z.string(), + iv: z.string(), + tag: z.string(), + hashedHex: z.string(), expiresAt: z.date(), userId: z.string().uuid(), orgId: z.string().uuid(), diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index f5f47d345..67751395b 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -41,16 +41,24 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => params: z.object({ id: z.string().uuid() }), + querystring: z.object({ + hashedHex: z.string() + }), response: { - 200: SecretSharingSchema.pick({ name: true, signedValue: true, expiresAt: true }) + 200: SecretSharingSchema.pick({ name: true, encryptedValue: true, iv: true, tag: true, expiresAt: true }) } }, handler: async (req) => { - const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById(req.params.id); + const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex( + req.params.id, + req.query.hashedHex + ); if (!sharedSecret) return undefined; return { name: sharedSecret.name, - signedValue: sharedSecret.signedValue, + encryptedValue: sharedSecret.encryptedValue, + iv: sharedSecret.iv, + tag: sharedSecret.tag, expiresAt: sharedSecret.expiresAt }; } @@ -65,7 +73,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => schema: { body: z.object({ name: z.string(), - signedValue: z.string(), + encryptedValue: z.string(), + iv: z.string(), + tag: z.string(), + hashedHex: z.string(), expiresAt: z.string().refine((date) => new Date(date) > new Date(), { message: "Expires at should be a future date" }) @@ -78,7 +89,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { name, signedValue, expiresAt } = req.body; + const { name, encryptedValue, iv, tag, hashedHex, expiresAt } = req.body; const sharedSecret = await req.server.services.secretSharing.createSharedSecret({ actor: req.permission.type, actorId: req.permission.id, @@ -86,7 +97,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, name, - signedValue, + encryptedValue, + iv, + tag, + hashedHex, expiresAt: new Date(expiresAt) }); return { id: sharedSecret.id }; diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index d4b40d309..85cfe97f6 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -16,13 +16,16 @@ export const secretSharingServiceFactory = ({ secretSharingDAL }: TSecretSharingServiceFactoryDep) => { const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => { - const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, signedValue, expiresAt } = + const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, encryptedValue, iv, tag, hashedHex, expiresAt } = createSharedSecretInput; const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); if (!permission) throw new UnauthorizedError({ name: "User not in org" }); const newSharedSecret = await secretSharingDAL.create({ name, - signedValue, + encryptedValue, + iv, + tag, + hashedHex, expiresAt, userId: actorId, orgId @@ -38,8 +41,8 @@ export const secretSharingServiceFactory = ({ return userSharedSecrets; }; - const getActiveSharedSecretById = async (sharedSecretId: string) => { - const sharedSecret = await secretSharingDAL.findById(sharedSecretId); + const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => { + const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex }); if (sharedSecret && sharedSecret.expiresAt < new Date()) { return; } @@ -58,6 +61,6 @@ export const secretSharingServiceFactory = ({ createSharedSecret, getSharedSecrets, deleteSharedSecretById, - getActiveSharedSecretById + getActiveSharedSecretByIdAndHashedHex }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 643aec230..2d14ed12c 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -10,7 +10,10 @@ export type TSharedSecretPermission = { export type TCreateSharedSecretDTO = { name: string; - signedValue: string; + encryptedValue: string; + iv: string; + tag: string; + hashedHex: string; expiresAt: Date; } & TSharedSecretPermission; diff --git a/frontend/src/components/utilities/cryptography/crypto.ts b/frontend/src/components/utilities/cryptography/crypto.ts index c8383d617..c0e5d4c21 100644 --- a/frontend/src/components/utilities/cryptography/crypto.ts +++ b/frontend/src/components/utilities/cryptography/crypto.ts @@ -224,76 +224,6 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): return plaintext; }; -/** - * Return new base64, NaCl, public-secret key pair for signing. - * @returns {Object} obj - * @returns {String} obj.publicKey - base64, NaCl, public key - * @returns {String} obj.secretKey - base64, NaCl, secret key - */ -const generateSignKeyPair = (): { - publicKey: string; - secretKey: string; -} => { - const pair = nacl.sign.keyPair(); - - return { - publicKey: nacl.util.encodeBase64(pair.publicKey), - secretKey: nacl.util.encodeBase64(pair.secretKey) - }; -}; - -type SignAsymmetricProps = { - message: string; - privateKey: string; -}; - -/** - * Returns asymmetrically signed [message] using [privateKey] - * @param {Object} obj - * @param {String} obj.message - message to sign - * @param {String} obj.privateKey - base64-encoded private key - * @returns {String} signedMessage - base64-encoded signed message - */ -const signAssymmetric = ({ message, privateKey }: SignAsymmetricProps): string => { - let signedMessage; - try { - signedMessage = nacl.sign(nacl.util.decodeUTF8(message), nacl.util.decodeBase64(privateKey)); - } catch (err) { - console.log("Failed to sign message", err); - process.exit(1); - } - return nacl.util.encodeBase64(signedMessage); -}; - -type OpenSignedAsymmetricProps = { - signedMessage: string; - publicKey: string; -}; - -/** - * Returns asymmetrically decrypted [message] using [publicKey] - * @param {Object} obj - * @param {String} obj.signedMessage - signed message to decrypt - * @param {String} obj.publicKey - base64-encoded public key - * @returns {String} signedMessage - base64-encoded decrypted message - */ -const openSignedAssymmetric = ({ signedMessage, publicKey }: OpenSignedAsymmetricProps): string => { - let originalMessage; - try { - originalMessage = nacl.sign.open( - nacl.util.decodeBase64(signedMessage), - nacl.util.decodeBase64(publicKey) - ); - if (!originalMessage) { - throw new Error("Signature verification failed"); - } - originalMessage = nacl.util.encodeUTF8(originalMessage); - } catch (err) { - console.log("Failed to verify signature", err); - } - return originalMessage; -}; - export { decryptAssymmetric, decryptSymmetric, @@ -301,8 +231,5 @@ export { encryptAssymmetric, encryptSymmetric, generateKeyPair, - generateSignKeyPair, - openSignedAssymmetric, - signAssymmetric, verifyPrivateKey }; diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index ad5bbee74..b4cf71531 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -16,15 +16,17 @@ export const useGetSharedSecrets = () => { }); }; -export const useGetActiveSharedSecretById = (id: string) => { +export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => { return useQuery({ queryFn: async () => { const { data } = await apiRequest.get( - `/api/v1/secret-sharing/public/${id}` + `/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}` ); return { name: data.name, - signedValue: data.signedValue, + encryptedValue: data.encryptedValue, + iv: data.iv, + tag: data.tag, expiresAt: data.expiresAt }; } diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index ca9cb76b8..2fbddfb35 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -1,7 +1,10 @@ export type TSharedSecret = { id: string; name: string; - signedValue: string; + encryptedValue: string; + iv: string; + tag: string; + hashedHex: string; userId: string; expiresAt: Date; createdAt: Date; @@ -10,13 +13,18 @@ export type TSharedSecret = { export type TCreateSharedSecretRequest = { name: string; - signedValue: string; + encryptedValue: string; + iv: string; + tag: string; + hashedHex: string; expiresAt: Date; }; export type TViewSharedSecretResponse = { name: string; - signedValue: string; + encryptedValue: string; + iv: string; + tag: string; expiresAt: Date; }; diff --git a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx index 3795bb3e9..2bdbfaf99 100644 --- a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx +++ b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx @@ -1,3 +1,5 @@ +import crypto from "crypto"; + import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; @@ -8,8 +10,7 @@ import * as yup from "yup"; import { createNotification } from "@app/components/notifications"; import { - generateSignKeyPair, - signAssymmetric + encryptSymmetric, } from "@app/components/utilities/cryptography/crypto"; import { Button, @@ -91,7 +92,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentOrg } = useOrganization(); const [newSharedSecret, setnewSharedSecret] = useState(""); const hasSharedSecret = Boolean(newSharedSecret); - const [isUrlCopied,, setIsUrlCopied] = useTimedReset({ + const [isUrlCopied, , setIsUrlCopied] = useTimedReset({ initialState: false, }); @@ -109,12 +110,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { try { if (!currentOrg?.id) return; - const signingKeyPair = generateSignKeyPair(); - const signedMessage = signAssymmetric({ - message: value, - privateKey: signingKeyPair.secretKey + const key = crypto.randomBytes(16).toString("hex"); + const hashedHex = crypto.createHash("sha256").update(key).digest("hex"); + const { ciphertext, iv, tag } = encryptSymmetric({ + plaintext: value, + key }); + const expiresAt = new Date(); const updateExpiresAt = expirationUnitsAndActions.find( (item) => item.unit === expiresInUnit @@ -125,13 +128,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { const { id } = await createSharedSecret.mutateAsync({ name, - signedValue: signedMessage, + encryptedValue: ciphertext, + iv, + tag, + hashedHex, expiresAt, }); setnewSharedSecret( - `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent( - signingKeyPair.publicKey - )}` + `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(hashedHex)}-${encodeURIComponent(key)}` ); createNotification({ @@ -195,10 +199,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { errorText={error?.message} > + isVisible + {...field} + containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2" + /> )} /> diff --git a/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx b/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx index 703b8faf0..e3d6fcabb 100644 --- a/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx +++ b/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx @@ -4,15 +4,16 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { openSignedAssymmetric } from "@app/components/utilities/cryptography/crypto"; +import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto"; import { useTimedReset } from "@app/hooks"; -import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; +import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing"; import { DragonMainImage, SecretTable } from "./components"; export const ShareSecretPublicPage = () => { const router = useRouter(); const { id, key: urlEncodedPublicKey } = router.query; + const [hashedHex, key] = urlEncodedPublicKey!.toString().split("-"); const publicKey = decodeURIComponent(urlEncodedPublicKey as string); useEffect(() => { @@ -21,12 +22,14 @@ export const ShareSecretPublicPage = () => { } }, [id, publicKey]); - const { isLoading, data } = useGetActiveSharedSecretById(id as string); + const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(id as string, hashedHex as string ); const decryptedSecret = useMemo(() => { - if (data && data.signedValue && publicKey) { - const res = openSignedAssymmetric({ - signedMessage: data.signedValue, - publicKey: publicKey as string + if (data && data.encryptedValue && publicKey) { + const res = decryptSymmetric({ + ciphertext: data.encryptedValue, + iv: data.iv, + tag: data.tag, + key, }); return res; } @@ -34,7 +37,7 @@ export const ShareSecretPublicPage = () => { }, [data, publicKey]); const [timeLeft, setTimeLeft] = useState(""); - const [isUrlCopied,, setIsUrlCopied] = useTimedReset({ + const [isUrlCopied, , setIsUrlCopied] = useTimedReset({ initialState: false, }); @@ -44,11 +47,11 @@ export const ShareSecretPublicPage = () => { useEffect(() => { const updateTimer = () => { - if (data && data.expiresAt) { + if (data && data.expiresAt) { const expirationTime = new Date(data.expiresAt).getTime(); const currentTime = new Date().getTime(); const timeDifference = expirationTime - currentTime; - + if (timeDifference < 0) { setTimeLeft("Expired"); } else { @@ -59,7 +62,7 @@ export const ShareSecretPublicPage = () => { } } }; - + const timer = setInterval(updateTimer, 1000); return () => clearInterval(timer); }, [data?.expiresAt]);