feat: use hash as pw & move to symmetric encrpytion

This commit is contained in:
ShubhamPalriwala
2024-05-29 14:10:47 +05:30
parent b5b778e241
commit 286426b240
10 changed files with 89 additions and 119 deletions
@@ -8,7 +8,10 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TableName.SecretSharing, (t) => { await knex.schema.createTable(TableName.SecretSharing, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable(); 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.timestamp("expiresAt").notNullable();
t.uuid("userId").notNullable(); t.uuid("userId").notNullable();
t.uuid("orgId").notNullable(); t.uuid("orgId").notNullable();
+4 -1
View File
@@ -10,7 +10,10 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({ export const SecretSharingSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(), name: z.string(),
signedValue: z.string(), encryptedValue: z.string(),
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.date(), expiresAt: z.date(),
userId: z.string().uuid(), userId: z.string().uuid(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
@@ -41,16 +41,24 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
params: z.object({ params: z.object({
id: z.string().uuid() id: z.string().uuid()
}), }),
querystring: z.object({
hashedHex: z.string()
}),
response: { 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) => { 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; if (!sharedSecret) return undefined;
return { return {
name: sharedSecret.name, name: sharedSecret.name,
signedValue: sharedSecret.signedValue, encryptedValue: sharedSecret.encryptedValue,
iv: sharedSecret.iv,
tag: sharedSecret.tag,
expiresAt: sharedSecret.expiresAt expiresAt: sharedSecret.expiresAt
}; };
} }
@@ -65,7 +73,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: { schema: {
body: z.object({ body: z.object({
name: z.string(), 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(), { expiresAt: z.string().refine((date) => new Date(date) > new Date(), {
message: "Expires at should be a future date" message: "Expires at should be a future date"
}) })
@@ -78,7 +89,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { 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({ const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -86,7 +97,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
name, name,
signedValue, encryptedValue,
iv,
tag,
hashedHex,
expiresAt: new Date(expiresAt) expiresAt: new Date(expiresAt)
}); });
return { id: sharedSecret.id }; return { id: sharedSecret.id };
@@ -16,13 +16,16 @@ export const secretSharingServiceFactory = ({
secretSharingDAL secretSharingDAL
}: TSecretSharingServiceFactoryDep) => { }: TSecretSharingServiceFactoryDep) => {
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => { 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; createSharedSecretInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (!permission) throw new UnauthorizedError({ name: "User not in org" }); if (!permission) throw new UnauthorizedError({ name: "User not in org" });
const newSharedSecret = await secretSharingDAL.create({ const newSharedSecret = await secretSharingDAL.create({
name, name,
signedValue, encryptedValue,
iv,
tag,
hashedHex,
expiresAt, expiresAt,
userId: actorId, userId: actorId,
orgId orgId
@@ -38,8 +41,8 @@ export const secretSharingServiceFactory = ({
return userSharedSecrets; return userSharedSecrets;
}; };
const getActiveSharedSecretById = async (sharedSecretId: string) => { const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
const sharedSecret = await secretSharingDAL.findById(sharedSecretId); const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
if (sharedSecret && sharedSecret.expiresAt < new Date()) { if (sharedSecret && sharedSecret.expiresAt < new Date()) {
return; return;
} }
@@ -58,6 +61,6 @@ export const secretSharingServiceFactory = ({
createSharedSecret, createSharedSecret,
getSharedSecrets, getSharedSecrets,
deleteSharedSecretById, deleteSharedSecretById,
getActiveSharedSecretById getActiveSharedSecretByIdAndHashedHex
}; };
}; };
@@ -10,7 +10,10 @@ export type TSharedSecretPermission = {
export type TCreateSharedSecretDTO = { export type TCreateSharedSecretDTO = {
name: string; name: string;
signedValue: string; encryptedValue: string;
iv: string;
tag: string;
hashedHex: string;
expiresAt: Date; expiresAt: Date;
} & TSharedSecretPermission; } & TSharedSecretPermission;
@@ -224,76 +224,6 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
return plaintext; 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 { export {
decryptAssymmetric, decryptAssymmetric,
decryptSymmetric, decryptSymmetric,
@@ -301,8 +231,5 @@ export {
encryptAssymmetric, encryptAssymmetric,
encryptSymmetric, encryptSymmetric,
generateKeyPair, generateKeyPair,
generateSignKeyPair,
openSignedAssymmetric,
signAssymmetric,
verifyPrivateKey verifyPrivateKey
}; };
@@ -16,15 +16,17 @@ export const useGetSharedSecrets = () => {
}); });
}; };
export const useGetActiveSharedSecretById = (id: string) => { export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
return useQuery<TViewSharedSecretResponse, [string]>({ return useQuery<TViewSharedSecretResponse, [string]>({
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<TViewSharedSecretResponse>( const { data } = await apiRequest.get<TViewSharedSecretResponse>(
`/api/v1/secret-sharing/public/${id}` `/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
); );
return { return {
name: data.name, name: data.name,
signedValue: data.signedValue, encryptedValue: data.encryptedValue,
iv: data.iv,
tag: data.tag,
expiresAt: data.expiresAt expiresAt: data.expiresAt
}; };
} }
+11 -3
View File
@@ -1,7 +1,10 @@
export type TSharedSecret = { export type TSharedSecret = {
id: string; id: string;
name: string; name: string;
signedValue: string; encryptedValue: string;
iv: string;
tag: string;
hashedHex: string;
userId: string; userId: string;
expiresAt: Date; expiresAt: Date;
createdAt: Date; createdAt: Date;
@@ -10,13 +13,18 @@ export type TSharedSecret = {
export type TCreateSharedSecretRequest = { export type TCreateSharedSecretRequest = {
name: string; name: string;
signedValue: string; encryptedValue: string;
iv: string;
tag: string;
hashedHex: string;
expiresAt: Date; expiresAt: Date;
}; };
export type TViewSharedSecretResponse = { export type TViewSharedSecretResponse = {
name: string; name: string;
signedValue: string; encryptedValue: string;
iv: string;
tag: string;
expiresAt: Date; expiresAt: Date;
}; };
@@ -1,3 +1,5 @@
import crypto from "crypto";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; 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 { createNotification } from "@app/components/notifications";
import { import {
generateSignKeyPair, encryptSymmetric,
signAssymmetric
} from "@app/components/utilities/cryptography/crypto"; } from "@app/components/utilities/cryptography/crypto";
import { import {
Button, Button,
@@ -91,7 +92,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const [newSharedSecret, setnewSharedSecret] = useState(""); const [newSharedSecret, setnewSharedSecret] = useState("");
const hasSharedSecret = Boolean(newSharedSecret); const hasSharedSecret = Boolean(newSharedSecret);
const [isUrlCopied,, setIsUrlCopied] = useTimedReset<boolean>({ const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
initialState: false, initialState: false,
}); });
@@ -109,12 +110,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
try { try {
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
const signingKeyPair = generateSignKeyPair(); const key = crypto.randomBytes(16).toString("hex");
const signedMessage = signAssymmetric({ const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
message: value, const { ciphertext, iv, tag } = encryptSymmetric({
privateKey: signingKeyPair.secretKey plaintext: value,
key
}); });
const expiresAt = new Date(); const expiresAt = new Date();
const updateExpiresAt = expirationUnitsAndActions.find( const updateExpiresAt = expirationUnitsAndActions.find(
(item) => item.unit === expiresInUnit (item) => item.unit === expiresInUnit
@@ -125,13 +128,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
const { id } = await createSharedSecret.mutateAsync({ const { id } = await createSharedSecret.mutateAsync({
name, name,
signedValue: signedMessage, encryptedValue: ciphertext,
iv,
tag,
hashedHex,
expiresAt, expiresAt,
}); });
setnewSharedSecret( setnewSharedSecret(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent( `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(hashedHex)}-${encodeURIComponent(key)}`
signingKeyPair.publicKey
)}`
); );
createNotification({ createNotification({
@@ -195,10 +199,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={error?.message} errorText={error?.message}
> >
<SecretInput <SecretInput
isVisible isVisible
{...field} {...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" 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"
/> />
</FormControl> </FormControl>
)} )}
/> />
@@ -4,15 +4,16 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; 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 { useTimedReset } from "@app/hooks";
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
import { DragonMainImage, SecretTable } from "./components"; import { DragonMainImage, SecretTable } from "./components";
export const ShareSecretPublicPage = () => { export const ShareSecretPublicPage = () => {
const router = useRouter(); const router = useRouter();
const { id, key: urlEncodedPublicKey } = router.query; const { id, key: urlEncodedPublicKey } = router.query;
const [hashedHex, key] = urlEncodedPublicKey!.toString().split("-");
const publicKey = decodeURIComponent(urlEncodedPublicKey as string); const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
useEffect(() => { useEffect(() => {
@@ -21,12 +22,14 @@ export const ShareSecretPublicPage = () => {
} }
}, [id, publicKey]); }, [id, publicKey]);
const { isLoading, data } = useGetActiveSharedSecretById(id as string); const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(id as string, hashedHex as string );
const decryptedSecret = useMemo(() => { const decryptedSecret = useMemo(() => {
if (data && data.signedValue && publicKey) { if (data && data.encryptedValue && publicKey) {
const res = openSignedAssymmetric({ const res = decryptSymmetric({
signedMessage: data.signedValue, ciphertext: data.encryptedValue,
publicKey: publicKey as string iv: data.iv,
tag: data.tag,
key,
}); });
return res; return res;
} }
@@ -34,7 +37,7 @@ export const ShareSecretPublicPage = () => {
}, [data, publicKey]); }, [data, publicKey]);
const [timeLeft, setTimeLeft] = useState(""); const [timeLeft, setTimeLeft] = useState("");
const [isUrlCopied,, setIsUrlCopied] = useTimedReset<boolean>({ const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
initialState: false, initialState: false,
}); });
@@ -44,11 +47,11 @@ export const ShareSecretPublicPage = () => {
useEffect(() => { useEffect(() => {
const updateTimer = () => { const updateTimer = () => {
if (data && data.expiresAt) { if (data && data.expiresAt) {
const expirationTime = new Date(data.expiresAt).getTime(); const expirationTime = new Date(data.expiresAt).getTime();
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
const timeDifference = expirationTime - currentTime; const timeDifference = expirationTime - currentTime;
if (timeDifference < 0) { if (timeDifference < 0) {
setTimeLeft("Expired"); setTimeLeft("Expired");
} else { } else {
@@ -59,7 +62,7 @@ export const ShareSecretPublicPage = () => {
} }
} }
}; };
const timer = setInterval(updateTimer, 1000); const timer = setInterval(updateTimer, 1000);
return () => clearInterval(timer); return () => clearInterval(timer);
}, [data?.expiresAt]); }, [data?.expiresAt]);