mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 13:27:46 +00:00
247 lines
6.9 KiB
TypeScript
247 lines
6.9 KiB
TypeScript
import crypto from "node:crypto";
|
|
|
|
import argon2 from "argon2";
|
|
import nacl from "tweetnacl";
|
|
import naclUtils from "tweetnacl-util";
|
|
|
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
|
|
|
import { getConfig } from "../config/env";
|
|
|
|
export type TDecryptSymmetricInput = {
|
|
ciphertext: string;
|
|
iv: string;
|
|
tag: string;
|
|
key: string;
|
|
};
|
|
export const IV_BYTES_SIZE = 12;
|
|
export const BLOCK_SIZE_BYTES_16 = 16;
|
|
|
|
export const decryptSymmetric = ({ ciphertext, iv, tag, key }: TDecryptSymmetricInput): string => {
|
|
const secretKey = crypto.createSecretKey(key, "base64");
|
|
|
|
const decipher = crypto.createDecipheriv(
|
|
SecretEncryptionAlgo.AES_256_GCM,
|
|
secretKey,
|
|
Buffer.from(iv, "base64")
|
|
);
|
|
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
|
let cleartext = decipher.update(ciphertext, "base64", "utf8");
|
|
cleartext += decipher.final("utf8");
|
|
|
|
return cleartext;
|
|
};
|
|
|
|
export const encryptSymmetric = (plaintext: string, key: string) => {
|
|
const iv = crypto.randomBytes(IV_BYTES_SIZE);
|
|
|
|
const secretKey = crypto.createSecretKey(key, "base64");
|
|
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, secretKey, iv);
|
|
|
|
let ciphertext = cipher.update(plaintext, "utf8", "base64");
|
|
ciphertext += cipher.final("base64");
|
|
|
|
return {
|
|
ciphertext,
|
|
iv: iv.toString("base64"),
|
|
tag: cipher.getAuthTag().toString("base64")
|
|
};
|
|
};
|
|
|
|
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) => {
|
|
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
|
|
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
|
|
|
|
let ciphertext = cipher.update(plaintext, "utf8", "base64");
|
|
ciphertext += cipher.final("base64");
|
|
|
|
return {
|
|
ciphertext,
|
|
iv: iv.toString("base64"),
|
|
tag: cipher.getAuthTag().toString("base64")
|
|
};
|
|
};
|
|
|
|
export const decryptSymmetric128BitHexKeyUTF8 = ({
|
|
ciphertext,
|
|
iv,
|
|
tag,
|
|
key
|
|
}: TDecryptSymmetricInput): string => {
|
|
const decipher = crypto.createDecipheriv(
|
|
SecretEncryptionAlgo.AES_256_GCM,
|
|
key,
|
|
Buffer.from(iv, "base64")
|
|
);
|
|
|
|
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
|
|
|
let cleartext = decipher.update(ciphertext, "base64", "utf8");
|
|
cleartext += decipher.final("utf8");
|
|
|
|
return cleartext;
|
|
};
|
|
|
|
export const encryptAsymmetric = (plaintext: string, publicKey: string, privateKey: string) => {
|
|
const nonce = nacl.randomBytes(24);
|
|
const ciphertext = nacl.box(
|
|
naclUtils.decodeUTF8(plaintext),
|
|
nonce,
|
|
naclUtils.decodeBase64(publicKey),
|
|
naclUtils.decodeBase64(privateKey)
|
|
);
|
|
|
|
return {
|
|
ciphertext: naclUtils.encodeBase64(ciphertext),
|
|
nonce: naclUtils.encodeBase64(nonce)
|
|
};
|
|
};
|
|
|
|
export type TDecryptAsymmetricInput = {
|
|
ciphertext: string;
|
|
nonce: string;
|
|
publicKey: string;
|
|
privateKey: string;
|
|
};
|
|
|
|
export const decryptAsymmetric = ({
|
|
ciphertext,
|
|
nonce,
|
|
publicKey,
|
|
privateKey
|
|
}: TDecryptAsymmetricInput) => {
|
|
const plaintext: Uint8Array | null = nacl.box.open(
|
|
naclUtils.decodeBase64(ciphertext),
|
|
naclUtils.decodeBase64(nonce),
|
|
naclUtils.decodeBase64(publicKey),
|
|
naclUtils.decodeBase64(privateKey)
|
|
);
|
|
|
|
if (plaintext == null) throw Error("Invalid ciphertext or keys");
|
|
|
|
return naclUtils.encodeUTF8(plaintext);
|
|
};
|
|
|
|
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
|
|
|
|
export const generateAsymmetricKeyPair = () => {
|
|
const pair = nacl.box.keyPair();
|
|
|
|
return {
|
|
publicKey: naclUtils.encodeBase64(pair.publicKey),
|
|
privateKey: naclUtils.encodeBase64(pair.secretKey)
|
|
};
|
|
};
|
|
|
|
export type TGenSecretBlindIndex = {
|
|
secretName: string;
|
|
keyEncoding: SecretKeyEncoding;
|
|
rootEncryptionKey?: string;
|
|
encryptionKey?: string;
|
|
iv: string;
|
|
tag: string;
|
|
ciphertext: string;
|
|
};
|
|
|
|
export const buildSecretBlindIndexFromName = async ({
|
|
secretName,
|
|
ciphertext,
|
|
keyEncoding,
|
|
iv,
|
|
tag,
|
|
encryptionKey,
|
|
rootEncryptionKey
|
|
}: TGenSecretBlindIndex) => {
|
|
if (!encryptionKey && !rootEncryptionKey) throw new Error("Missing secret blind index key");
|
|
let salt = "";
|
|
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
|
|
salt = decryptSymmetric({ iv, ciphertext, key: rootEncryptionKey, tag });
|
|
} else if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) {
|
|
salt = decryptSymmetric128BitHexKeyUTF8({ iv, ciphertext, key: encryptionKey, tag });
|
|
}
|
|
if (!salt) throw new Error("Missing secret blind index key");
|
|
|
|
const secretBlindIndex = await argon2.hash(secretName, {
|
|
type: argon2.argon2id,
|
|
salt: Buffer.from(salt, "base64"),
|
|
saltLength: 16, // default 16 bytes
|
|
memoryCost: 65536, // default pool of 64 MiB per thread.
|
|
hashLength: 32,
|
|
parallelism: 1,
|
|
raw: true
|
|
});
|
|
|
|
return secretBlindIndex.toString("base64");
|
|
};
|
|
|
|
export const createSecretBlindIndex = (rootEncryptionKey?: string, encryptionKey?: string) => {
|
|
if (!encryptionKey && !rootEncryptionKey) throw new Error("Atleast one encryption key needed");
|
|
const salt = crypto.randomBytes(16).toString("base64");
|
|
if (rootEncryptionKey) {
|
|
const data = encryptSymmetric(salt, rootEncryptionKey);
|
|
return {
|
|
...data,
|
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
|
keyEncoding: SecretKeyEncoding.BASE64
|
|
};
|
|
}
|
|
if (encryptionKey) {
|
|
const data = encryptSymmetric128BitHexKeyUTF8(salt, encryptionKey);
|
|
return {
|
|
...data,
|
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
|
keyEncoding: SecretKeyEncoding.UTF8
|
|
};
|
|
}
|
|
throw new Error("Failed to generate blind index due to encryption key missing");
|
|
};
|
|
|
|
export const infisicalSymmetricEncypt = (data: string) => {
|
|
const appCfg = getConfig();
|
|
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
|
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
|
if (rootEncryptionKey) {
|
|
const { iv, tag, ciphertext } = encryptSymmetric(data, rootEncryptionKey);
|
|
return {
|
|
iv,
|
|
tag,
|
|
ciphertext,
|
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
|
encoding: SecretKeyEncoding.BASE64
|
|
};
|
|
}
|
|
if (encryptionKey) {
|
|
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(data, encryptionKey);
|
|
return {
|
|
iv,
|
|
tag,
|
|
ciphertext,
|
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
|
encoding: SecretKeyEncoding.UTF8
|
|
};
|
|
}
|
|
throw new Error("Missing both encryption keys");
|
|
};
|
|
|
|
export const infisicalSymmetricDecrypt = <T extends any = string>({
|
|
keyEncoding,
|
|
ciphertext,
|
|
tag,
|
|
iv
|
|
}: Omit<TDecryptSymmetricInput, "key"> & {
|
|
keyEncoding: SecretKeyEncoding;
|
|
}) => {
|
|
const appCfg = getConfig();
|
|
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
|
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
|
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
|
|
const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext });
|
|
return data as T;
|
|
}
|
|
if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) {
|
|
const data = decryptSymmetric128BitHexKeyUTF8({ key: encryptionKey, iv, tag, ciphertext });
|
|
return data as T;
|
|
}
|
|
throw new Error("Missing both encryption keys");
|
|
};
|