/* eslint-disable @typescript-eslint/naming-convention */ import crypto from "crypto"; import * as yup from "yup"; import { decryptAssymmetric, encryptSymmetric } from "@app/components/utilities/cryptography/crypto"; import { BatchSecretDTO, DecryptedSecret } from "@app/hooks/api/secrets/types"; export enum SecretActionType { Created = "created", Modified = "modified", Deleted = "deleted" } export const DEFAULT_SECRET_VALUE = { _id: undefined, overrideAction: undefined, idOverride: undefined, valueOverride: undefined, comment: "", key: "", value: "", tags: [] }; const secretSchema = yup.object({ _id: yup.string(), key: yup .string() .trim() .required() .label("Secret key") .test("starts-with-number", "Should start with an alphabet", (val) => Boolean(val?.charAt(0)?.match(/[a-zA-Z]/i)) ) .test({ name: "duplicate-keys", // TODO:(akhilmhdh) ts keeps throwing from not found need to see how to resolve this test: (val, ctx: any) => { const secrets: Array<{ key: string }> = ctx?.from?.[1]?.value?.secrets || []; const duplicateKeys: Record = {}; secrets?.forEach(({ key }, index) => { if (key === val) duplicateKeys[index + 1] = true; }); const pos = Object.keys(duplicateKeys); if (pos.length <= 1) { return true; } return ctx.createError({ message: `Same key in row ${pos.join(", ")}` }); } }), value: yup.string().trim(), comment: yup.string().trim(), tags: yup.array( yup.object({ _id: yup.string().required(), name: yup.string().required(), slug: yup.string().required(), tagColor: yup.string().nullable(), }) ), overrideAction: yup.string().notRequired().oneOf(Object.values(SecretActionType)), idOverride: yup.string().notRequired(), valueOverride: yup.string().trim().notRequired() }); export const schema = yup.object({ isSnapshotMode: yup.bool().notRequired(), secrets: yup.array(secretSchema) }); export type FormData = yup.InferType; export type TSecretDetailsOpen = { index: number; id: string }; export type TSecOverwriteOpt = { secrets: Record }; // to convert multi line into single line ones by quoting them and changing to string \n const formatMultiValueEnv = (val?: string) => { if (!val) return ""; if (!val.match("\n")) return val; return `"${val.replace(/\n/g, "\\n")}"`; }; export const downloadSecret = ( secrets: FormData["secrets"] = [], importedSecrets: { key: string; value?: string; comment?: string }[] = [], env: string = "unknown" ) => { const importSecPos: Record = {}; importedSecrets.forEach((el, index) => { importSecPos[el.key] = index; }); const finalSecret = [...importedSecrets]; secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => { const finalVal = overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value; const newValue = { key, value: formatMultiValueEnv(finalVal), comment }; // can also be zero thus failing if (typeof importSecPos?.[key] === "undefined") { finalSecret.push(newValue); } else { finalSecret[importSecPos[key]] = newValue; } }); let file = ""; finalSecret.forEach(({ key, value, comment }) => { if (comment) { file += `# ${comment}\n${key}=${value}\n`; return; } file += `${key}=${value}\n`; }); const blob = new Blob([file]); const fileDownloadUrl = URL.createObjectURL(blob); const alink = document.createElement("a"); alink.href = fileDownloadUrl; alink.download = `${env}.env`; alink.click(); }; /* * Below functions are used convert the dashboard secrets to the bulk secret creation request format * They are encrypted back * Formatted to [ { request: "", secret:{} } ] */ const encryptASecret = (randomBytes: string, key: string, value?: string, comment?: string) => { // encrypt key const { ciphertext: secretKeyCiphertext, iv: secretKeyIV, tag: secretKeyTag } = encryptSymmetric({ plaintext: key, key: randomBytes }); // encrypt value const { ciphertext: secretValueCiphertext, iv: secretValueIV, tag: secretValueTag } = encryptSymmetric({ plaintext: value ?? "", key: randomBytes }); // encrypt comment const { ciphertext: secretCommentCiphertext, iv: secretCommentIV, tag: secretCommentTag } = encryptSymmetric({ plaintext: comment ?? "", key: randomBytes }); return { secretKeyCiphertext, secretKeyIV, secretKeyTag, secretValueCiphertext, secretValueIV, secretValueTag, secretCommentCiphertext, secretCommentIV, secretCommentTag }; }; const deepCompareSecrets = (lhs: DecryptedSecret, rhs: any) => lhs.key === rhs.key && lhs.value === rhs.value && lhs.comment === rhs.comment && lhs?.valueOverride === rhs?.valueOverride && JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags); export const transformSecretsToBatchSecretReq = ( deletedSecretIds: { id: string; secretName: string; }[], latestFileKey: any, secrets: FormData["secrets"], intialValues: DecryptedSecret[] = [] ) => { // deleted secrets const secretsToBeDeleted: BatchSecretDTO["requests"] = deletedSecretIds.map(({ id, secretName }) => ({ method: "DELETE", secret: { _id: id, secretName } })); const secretsToBeUpdated: BatchSecretDTO["requests"] = []; const secretsToBeCreated: BatchSecretDTO["requests"] = []; const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const randomBytes = latestFileKey ? decryptAssymmetric({ ciphertext: latestFileKey.encryptedKey, nonce: latestFileKey.nonce, publicKey: latestFileKey.sender.publicKey, privateKey: PRIVATE_KEY }) : crypto.randomBytes(16).toString("hex"); secrets?.forEach((secret) => { const { _id, idOverride, value, valueOverride, overrideAction, tags = [], comment, key } = secret; if (!idOverride && overrideAction === SecretActionType.Created) { secretsToBeCreated.push({ method: "POST", secret: { type: "personal", tags, secretName: key, ...encryptASecret(randomBytes, key, valueOverride, comment) } }); } // to be created ones as they don't have server generated id if (!_id) { secretsToBeCreated.push({ method: "POST", secret: { type: "shared", tags, secretName: key, ...encryptASecret(randomBytes, key, value, comment) } }); return; // exit as updated and delete case won't happen when created } // has an id means this is updated one if (_id) { // check value has changed or not const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!; if (!deepCompareSecrets(initialSecretValue, secret)) { secretsToBeUpdated.push({ method: "PATCH", secret: { _id, type: "shared", tags, secretName: key, ...encryptASecret(randomBytes, key, value, comment) } }); } } if (idOverride) { // if action is deleted meaning override has been removed but id is kept to collect at this point if (overrideAction === SecretActionType.Deleted) { secretsToBeDeleted.push({ method: "DELETE", secret: { _id: idOverride, secretName: key } }); } else { // if not deleted action then as id is there its an updated const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!; if (!deepCompareSecrets(initialSecretValue, secret)) { secretsToBeUpdated.push({ method: "PATCH", secret: { _id: idOverride, type: "personal", tags, secretName: key, ...encryptASecret(randomBytes, key, valueOverride, comment) } }); } } } }); return secretsToBeCreated.concat(secretsToBeUpdated, secretsToBeDeleted); };