@vatsdev/encryption-decryption
Version:
A TypeScript library for secure encryption and decryption using Google Cloud KMS and Secret Manager
1,069 lines (1,056 loc) • 39.4 kB
JavaScript
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/utils/encryptionUtils.ts
import crypto3 from "crypto";
// src/types/errors.ts
var ConfigurationError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "ConfigurationError";
}
};
var EncryptionError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "EncryptionError";
}
};
var ValidationError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "ValidationError";
}
};
var ErrorCodes = {
CONFIGURATION: {
MISSING_CONFIG: "CONFIGURATION_MISSING_CONFIG",
MISSING_ENV_VAR: "CONFIGURATION_MISSING_ENV_VAR",
INITIALIZATION_ERROR: "CONFIGURATION_INITIALIZATION_ERROR",
INVALID_CREDENTIALS: "CONFIGURATION_INVALID_CREDENTIALS"
},
ENCRYPTION: {
FIELD_ENCRYPTION_ERROR: "ENCRYPTION_FIELD_ENCRYPTION_ERROR",
FIELD_DECRYPTION_ERROR: "ENCRYPTION_FIELD_DECRYPTION_ERROR",
DEK_RESOLUTION_ERROR: "ENCRYPTION_DEK_RESOLUTION_ERROR",
DEK_DECRYPTION_ERROR: "ENCRYPTION_DEK_DECRYPTION_ERROR",
DEK_ENCRYPTION_ERROR: "ENCRYPTION_DEK_ENCRYPTION_ERROR",
SECRET_RETRIEVAL_ERROR: "ENCRYPTION_SECRET_RETRIEVAL_ERROR",
UNKNOWN_ERROR: "ENCRYPTION_UNKNOWN_ERROR",
KEY_DETAILS_ERROR: "ENCRYPTION_KEY_DETAILS_ERROR",
KEY_VERSION_ERROR: "ENCRYPTION_KEY_VERSION_ERROR",
KEY_CREATION_ERROR: "ENCRYPTION_KEY_CREATION_ERROR",
CREATION_ERROR: "ENCRYPTION_CREATION_ERROR"
},
VALIDATION: {
MISSING_REQUIRED_FIELD: "VALIDATION_MISSING_REQUIRED_FIELD",
INVALID_DEK: "VALIDATION_INVALID_DEK"
}
};
// src/services/kmsService.ts
import { KeyManagementServiceClient as KeyManagementServiceClient2 } from "@google-cloud/kms";
import crypto from "crypto";
// src/utils/createKms.ts
var createKms = (keyName, environment) => {
const keyRingId = `kr-${keyName}-${environment}`;
const keyId = `key-${keyName}-${environment}`;
return { keyRingId, keyId };
};
var createKms_default = createKms;
// src/services/kmsService.ts
import dotenv from "dotenv";
import path from "path";
// src/utils/createKeyRing.ts
import { KeyManagementServiceClient } from "@google-cloud/kms";
async function createKeyRing(projectId, locationId, keyRingId, credentials) {
var _a, _b, _c;
const client = new KeyManagementServiceClient({
credentials
});
const parent = client.locationPath(projectId, locationId);
const keyRingName = client.keyRingPath(projectId, locationId, keyRingId);
try {
await client.getKeyRing({ name: keyRingName });
console.log("KeyRing already exists:", keyRingName);
const [keys] = await client.listCryptoKeys({ parent: keyRingName });
if (keys.length === 0) {
console.log("No keys found inside this KeyRing.");
return { latestKeyWithVersion: keyRingName, isNewCreated: false };
}
console.log("Getting the list inside the keyRing");
const keysWithVersions = await Promise.all(
keys.map(async (key) => {
const [versions] = await client.listCryptoKeyVersions({
parent: key.name
});
return {
keyName: key.name,
purpose: key.purpose,
versions: versions.map((v) => ({
name: v.name,
state: v.state
}))
};
})
);
const latestKeyWithVersion = ((_b = (_a = keysWithVersions[0]) == null ? void 0 : _a.versions[0]) == null ? void 0 : _b.name) || null;
return { keyName: ((_c = keysWithVersions[0]) == null ? void 0 : _c.keyName) || keyRingName, latestKeyWithVersion, isNewCreated: false };
} catch (err) {
if (err.code === 5) {
console.log("KeyRing does not exist, creating:", keyRingName);
await client.createKeyRing({
parent,
keyRingId,
keyRing: {}
});
return { isNewCreated: true };
} else {
console.error("Full API error response:", JSON.stringify(err, null, 2));
throw new Error(
`Failed to check/create key ring: ${err.message || "Unknown error"}`
);
}
}
}
var createKeyRing_default = createKeyRing;
// src/services/kmsService.ts
dotenv.config();
var KMSService = class {
constructor() {
try {
const configPath = path.join(process.cwd(), "config/index.js");
const config = __require(configPath);
const encryptionConfig = config.get("encryption");
this.environment = encryptionConfig.ENV || "dev";
if (encryptionConfig == null ? void 0 : encryptionConfig.credentials) {
if (!encryptionConfig.credentials.client_email || !encryptionConfig.credentials.private_key) {
throw new ConfigurationError(
"Credentials object is missing required fields (client_email or private_key)",
ErrorCodes.CONFIGURATION.INVALID_CREDENTIALS
);
}
const credentials = encryptionConfig.credentials;
this.client = new KeyManagementServiceClient2({
credentials: encryptionConfig.credentials,
fallback: true
});
this.credentials = credentials;
this.projectId = encryptionConfig.projectId;
this.locationId = encryptionConfig.locationId;
} else {
this.client = new KeyManagementServiceClient2();
this.projectId = process.env.GOOGLE_CLOUD_PROJECT || "";
this.locationId = process.env.KMS_LOCATION_ID || "global";
}
if (!this.projectId) {
throw new ConfigurationError(
"GOOGLE_CLOUD_PROJECT environment variable is required",
ErrorCodes.CONFIGURATION.MISSING_ENV_VAR
);
}
} catch (error) {
if (error instanceof ConfigurationError) {
throw error;
}
throw new ConfigurationError(
`Failed to initialize KMS service: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.CONFIGURATION.INITIALIZATION_ERROR
);
}
}
createSecretKeyWithEnv(clientName) {
return `secret-${clientName}-${this.environment}`;
}
/**
* Creates a new key ring and key in Google Cloud KMS
* @param dek - Data Encryption Key to encrypt
* @param clientName - Optional name for the KMS key
* @returns Promise resolving to encryption result with metadata
* @throws {EncryptionError} When key creation or encryption fails
*/
async createKeyRingAndKey(dek, clientName) {
var _a;
const { keyRingId, keyId } = createKms_default(clientName || crypto.randomBytes(16).toString("hex"), this.environment);
try {
const keyring = await createKeyRing_default(
this.projectId,
this.locationId,
keyRingId,
this.credentials || ""
);
console.log(`${(keyring == null ? void 0 : keyring.isNewCreated) ? "Creating new" : "Getting Existing"} crypto key`);
let keyName;
let keyKmsPath;
if (keyring.isNewCreated) {
const [key] = await this.client.createCryptoKey({
parent: this.client.keyRingPath(this.projectId, this.locationId, keyRingId),
cryptoKeyId: keyId,
cryptoKey: {
purpose: "ENCRYPT_DECRYPT",
versionTemplate: {
algorithm: "GOOGLE_SYMMETRIC_ENCRYPTION"
}
}
});
const keyVersionName = (_a = key.primary) == null ? void 0 : _a.name;
if (!keyVersionName) {
throw new EncryptionError(
"Failed to retrieve primary key version",
ErrorCodes.ENCRYPTION.KEY_VERSION_ERROR
);
}
const keyDetailsArray = keyVersionName.split("/");
const keyVersion = keyDetailsArray[keyDetailsArray.length - 1];
if (!keyVersion) {
throw new EncryptionError(
"Failed to retrieve key version",
ErrorCodes.ENCRYPTION.KEY_VERSION_ERROR
);
}
if (!key.name) {
throw new EncryptionError(
"Failed to create crypto key: key name is undefined",
ErrorCodes.ENCRYPTION.KEY_CREATION_ERROR
);
}
keyName = key.name;
keyKmsPath = keyVersionName;
} else {
keyName = keyring.keyName;
keyKmsPath = keyring.latestKeyWithVersion;
}
const [encryptResponse] = await this.client.encrypt({
name: keyName,
plaintext: dek
});
return {
encryptedDEK: encryptResponse.ciphertext,
kmsPath: keyKmsPath || ""
};
} catch (error) {
if (error instanceof EncryptionError) {
throw error;
}
throw new EncryptionError(
`Failed to create key ring and key: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.KEY_CREATION_ERROR
);
}
}
/**
* Checks if a KMS key exists for the given client name
* @param clientName - Name of the client to check for existing key
* @returns Promise resolving to KMS path if key exists, null if not found
* @throws {EncryptionError} When key check fails
*/
async getExistingKMSPath(clientName) {
var _a;
try {
if (!clientName) {
throw new ValidationError(
"Client name is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const { keyRingId, keyId } = createKms_default(clientName, this.environment);
try {
const keyName = this.client.cryptoKeyPath(this.projectId, this.locationId, keyRingId, keyId);
const [key] = await this.client.getCryptoKey({ name: keyName });
if ((_a = key.primary) == null ? void 0 : _a.name) {
console.log("Found existing KMS key for client:", clientName, "- Key path:", key.primary.name);
return key.primary.name;
}
return null;
} catch (error) {
if (error.code === 5) {
console.log("No existing KMS key found for client:", clientName);
return null;
} else {
throw error;
}
}
} catch (error) {
if (error instanceof ValidationError) {
throw error;
}
throw new EncryptionError(
`Failed to check existing KMS key: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.SECRET_RETRIEVAL_ERROR
);
}
}
/**
* Encrypts a Data Encryption Key (DEK) using Google Cloud KMS
* @param dek - Data Encryption Key to encrypt
* @param clientName - Optional name for the KMS key
* @returns Promise resolving to encryption result with metadata
* @throws {EncryptionError} When encryption fails
*/
async encryptDEK(dek, clientName) {
try {
if (!dek || dek.length !== 32) {
throw new ValidationError(
"Invalid DEK: must be 32 bytes for AES-256-GCM",
ErrorCodes.VALIDATION.INVALID_DEK
);
}
return await this.createKeyRingAndKey(dek, clientName);
} catch (error) {
if (error instanceof ValidationError || error instanceof EncryptionError) {
throw error;
}
throw new EncryptionError(
`Failed to encrypt DEK: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.DEK_ENCRYPTION_ERROR
);
}
}
/**
* Decrypts a Data Encryption Key (DEK) using Google Cloud KMS
* @param params - Parameters containing encrypted DEK and key metadata
* @returns Promise resolving to decrypted DEK
* @throws {ValidationError} When input parameters are invalid
* @throws {EncryptionError} When decryption fails
*/
async decryptDEK({ encryptedDEKData, kmsPath }) {
try {
if (!encryptedDEKData) {
throw new ValidationError(
"Encrypted DEK data is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
if (!kmsPath) {
throw new ValidationError(
"Missing required key metadata",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const keyMetadata = kmsPath.split("/").slice(0, 8).join("/");
const [decryptResponse] = await this.client.decrypt({
name: keyMetadata,
ciphertext: encryptedDEKData
});
const dek = decryptResponse.plaintext;
if (dek.length !== 32) {
throw new ValidationError(
`Invalid DEK length: ${dek.length} bytes. Expected 32 bytes for AES-256-GCM`,
ErrorCodes.VALIDATION.INVALID_DEK
);
}
return dek;
} catch (error) {
if (error instanceof ValidationError || error instanceof EncryptionError) {
throw error;
}
throw new EncryptionError(
`Failed to decrypt DEK: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.DEK_DECRYPTION_ERROR
);
}
}
};
var kmsService_default = new KMSService();
// src/services/secretManagerService.ts
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
import path2 from "path";
var SecretManagerService = class {
constructor() {
try {
const configPath = path2.join(process.cwd(), "config/index.js");
const config = __require(configPath);
const encryptionConfig = config.get("encryption");
if (encryptionConfig == null ? void 0 : encryptionConfig.credentials) {
if (!encryptionConfig.credentials.client_email || !encryptionConfig.credentials.private_key) {
throw new ConfigurationError(
"Credentials object is missing required fields (client_email or private_key)",
ErrorCodes.CONFIGURATION.INVALID_CREDENTIALS
);
}
this.secretManager = new SecretManagerServiceClient({
credentials: encryptionConfig.credentials
});
this.projectId = encryptionConfig.projectId;
} else {
this.secretManager = new SecretManagerServiceClient();
this.projectId = process.env.GOOGLE_CLOUD_PROJECT || "";
}
if (!this.projectId) {
throw new ConfigurationError(
"GOOGLE_CLOUD_PROJECT environment variable is required",
ErrorCodes.CONFIGURATION.MISSING_ENV_VAR
);
}
} catch (error) {
if (error instanceof ConfigurationError) {
throw error;
}
throw new ConfigurationError(
`Failed to initialize Secret Manager service: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.CONFIGURATION.INITIALIZATION_ERROR
);
}
}
/**
* Checks if a secret exists for a given client by trying to access it directly
* @param clientName - Name of the client to check
* @returns Promise resolving to secret name if found, null if not found
* @throws {EncryptionError} When secret check fails
*/
async getExistingSecretForClient(clientName) {
try {
if (!clientName) {
throw new ValidationError(
"Client name is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const secretId = kmsService_default.createSecretKeyWithEnv(clientName);
const secretName = `projects/${this.projectId}/secrets/${secretId}`;
try {
await this.secretManager.getSecret({ name: secretName });
return secretName;
} catch (error) {
if (error.code === 5) {
return null;
} else {
throw error;
}
}
} catch (error) {
if (error instanceof ValidationError) {
throw error;
}
throw new EncryptionError(
`Failed to check existing secrets: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.SECRET_RETRIEVAL_ERROR
);
}
}
/**
* Retrieves the structured secret data containing encryptedDEK and kmsPath
* @param secretName - Name of the secret to retrieve
* @returns Promise resolving to the structured secret data with version path
* @throws {ValidationError} When secret name is missing
* @throws {EncryptionError} When secret retrieval fails
*/
async getStructuredSecret(secretName) {
var _a;
try {
if (!secretName) {
throw new ValidationError(
"Secret name is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const [version] = await this.secretManager.accessSecretVersion({
name: `${secretName}/versions/latest`
});
const secretPayload = (_a = version.payload) == null ? void 0 : _a.data;
if (!secretPayload) {
throw new EncryptionError(
`No payload data found in secret for ${secretName}`,
ErrorCodes.ENCRYPTION.SECRET_RETRIEVAL_ERROR
);
}
const payloadBuffer = Buffer.from(secretPayload);
try {
const payloadString = payloadBuffer.toString("utf8");
const secretData = JSON.parse(payloadString);
if (secretData.encryptedDEK && secretData.kmsPath) {
return {
encryptedDEK: Buffer.from(secretData.encryptedDEK, "base64"),
kmsPath: secretData.kmsPath,
secretNamePath: version.name || `${secretName}/versions/latest`
};
}
} catch (parseError) {
console.log("\u{1F680} ~ SecretManagerService ~ getStructuredSecret ~ Legacy format detected, raw buffer:", payloadBuffer.length, "bytes");
}
return {
encryptedDEK: payloadBuffer,
kmsPath: "",
// Legacy secrets don't have kmsPath stored
secretNamePath: version.name || `${secretName}/versions/latest`
};
} catch (error) {
if (error instanceof ValidationError || error instanceof EncryptionError) {
throw error;
}
throw new EncryptionError(
`Failed to access structured secret: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.SECRET_RETRIEVAL_ERROR
);
}
}
/**
* Creates a new secret and adds the encrypted DEK as a version
* @param secretId - Unique identifier for the secret
* @param secretData - Data to be stored in the secret
* @returns Promise resolving to the created secret version
* @throws {ValidationError} When required parameters are missing
* @throws {EncryptionError} When secret creation fails
*/
async createSecret(secretId, secretData) {
try {
if (!secretId) {
throw new ValidationError(
"Secret ID is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
if (!secretData.encryptedDEK) {
throw new ValidationError(
"Encrypted DEK is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
if (!secretData.kmsPath) {
throw new ValidationError(
"KMS path is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const parent = `projects/${this.projectId}`;
const [secret] = await this.secretManager.createSecret({
parent,
secretId,
secret: {
replication: {
automatic: {}
}
}
});
if (!secret.name) {
throw new EncryptionError(
"Failed to create secret: No name returned",
ErrorCodes.ENCRYPTION.CREATION_ERROR
);
}
const structuredSecretData = {
encryptedDEK: secretData.encryptedDEK.toString("base64"),
kmsPath: secretData.kmsPath
};
const [version] = await this.secretManager.addSecretVersion({
parent: secret.name,
payload: {
data: Buffer.from(JSON.stringify(structuredSecretData))
}
});
if (!version.name) {
throw new EncryptionError(
"Failed to create secret version: No name returned",
ErrorCodes.ENCRYPTION.CREATION_ERROR
);
}
return {
secretName: secret.name,
secretNamePath: version.name
};
} catch (error) {
if (error instanceof ValidationError || error instanceof EncryptionError) {
throw error;
}
throw new EncryptionError(
`Failed to create secret: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.CREATION_ERROR
);
}
}
/**
* Retrieves the latest version of a secret (backward compatible - returns only encryptedDEK)
* @param secretNamePath - Path to the secret version to retrieve
* @returns Promise resolving to the encryptedDEK as a Buffer
* @throws {ValidationError} When secret name is missing
* @throws {EncryptionError} When secret retrieval fails
*/
async getSecret(secretNamePath) {
var _a;
try {
if (!secretNamePath) {
throw new ValidationError(
"Secret name is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const [version] = await this.secretManager.accessSecretVersion({
name: secretNamePath
});
const secretPayload = (_a = version.payload) == null ? void 0 : _a.data;
if (!secretPayload) {
throw new EncryptionError(
`No payload data found in secret version for ${secretNamePath}`,
ErrorCodes.ENCRYPTION.SECRET_RETRIEVAL_ERROR
);
}
const payloadBuffer = Buffer.from(secretPayload);
try {
const payloadString = payloadBuffer.toString("utf8");
const secretData = JSON.parse(payloadString);
if (secretData.encryptedDEK && typeof secretData.encryptedDEK === "string") {
return Buffer.from(secretData.encryptedDEK, "base64");
}
} catch (parseError) {
}
return payloadBuffer;
} catch (error) {
if (error instanceof ValidationError || error instanceof EncryptionError) {
throw error;
}
throw new EncryptionError(
`Failed to access secret: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.SECRET_RETRIEVAL_ERROR
);
}
}
};
var secretManagerService_default = new SecretManagerService();
// src/utils/createHash.ts
import crypto2 from "crypto";
var createHash = (data) => {
return crypto2.createHash("sha256").update(data).digest("hex");
};
var createHash_default = createHash;
// src/utils/configUtils.ts
import fs from "fs";
import path3 from "path";
var configCache = null;
var getEncryptionConfig = () => {
if (configCache) {
return configCache;
}
try {
const configPath = process.env.CONFIG_PATH || path3.join(process.cwd(), "config/encryption.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
configCache = config;
return config;
} catch (error) {
throw new ConfigurationError(
`Failed to read encryption configuration: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.CONFIGURATION.MISSING_CONFIG
);
}
};
var getModelConfig = (modelName) => {
const config = getEncryptionConfig();
const modelConfig = config[modelName];
if (!modelConfig) {
throw new ConfigurationError(
`Encryption configuration not found for model ${modelName}`,
ErrorCodes.CONFIGURATION.MISSING_CONFIG
);
}
return modelConfig;
};
// src/utils/encryptionUtils.ts
var generateDEK = (length = 32) => {
return crypto3.randomBytes(length);
};
var encryptField = async (fieldName, value, dek) => {
if (!value) return null;
const iv = crypto3.randomBytes(16);
const cipher = crypto3.createCipheriv("aes-256-gcm", dek, iv);
let encrypted = cipher.update(value, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
return {
// [fieldName]: encrypted,
[`${fieldName}_encrypted`]: encrypted,
[`${fieldName}_iv`]: iv.toString("hex"),
[`${fieldName}_auth_tag`]: authTag.toString("hex")
};
};
var decryptField = async (fieldName, data, dek) => {
const encryptedFieldName = `${fieldName}_encrypted`;
if (!data[encryptedFieldName]) return null;
try {
const decipher = crypto3.createDecipheriv(
"aes-256-gcm",
dek,
Buffer.from(data[`${fieldName}_iv`], "hex")
);
decipher.setAuthTag(Buffer.from(data[`${fieldName}_auth_tag`], "hex"));
let decrypted = decipher.update(data[encryptedFieldName], "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
} catch (error) {
console.error(`Error decrypting field ${fieldName}:`, error);
return data[fieldName] || null;
}
};
var handleFieldEncryption = async (modelName, data, dek) => {
try {
const modelConfig = getModelConfig(modelName);
const encryptedData = { ...data };
for (const [fieldName, fieldConfig] of Object.entries(modelConfig)) {
if (fieldConfig.shouldEncrypt && data[fieldName]) {
try {
if (fieldConfig.isObject) {
if (typeof data[fieldName] === "object") {
const nestedEncrypted = await handleFieldEncryption(
fieldName,
data[fieldName],
dek
);
encryptedData[fieldName] = nestedEncrypted;
}
} else if (fieldConfig.isArrayOfObjects) {
if (Array.isArray(data[fieldName])) {
encryptedData[fieldName] = await Promise.all(
data[fieldName].map(async (item) => {
return await handleFieldEncryption(
fieldName,
item,
dek
);
})
);
}
} else {
const fieldData = await encryptField(fieldName, data[fieldName], dek);
if (fieldConfig.shouldHash) {
const hash = createHash_default(data[fieldName]);
encryptedData[`${fieldName}_hash`] = hash;
}
if (fieldData) {
Object.assign(encryptedData, fieldData);
}
}
} catch (error) {
throw new EncryptionError(
`Failed to encrypt field ${fieldName}: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.FIELD_ENCRYPTION_ERROR
);
}
}
}
return encryptedData;
} catch (error) {
if (error instanceof EncryptionError) {
throw error;
}
throw new EncryptionError(
`Failed to handle field encryption: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.FIELD_ENCRYPTION_ERROR
);
}
};
var createNewEncryption = async (clientName) => {
try {
const existingSecretName = await secretManagerService_default.getExistingSecretForClient(clientName);
if (existingSecretName) {
console.log("Secret already exists for client:", clientName, "- Retrieving existing secret");
const existingSecretData = await secretManagerService_default.getStructuredSecret(existingSecretName);
if (!existingSecretData.kmsPath && existingSecretData.kmsPath.startsWith("projects/")) {
const secretId2 = existingSecretName.split("/").pop() || `secret-${clientName}`;
return {
kmsPath: existingSecretData.kmsPath,
secretId: secretId2,
secretNamePath: existingSecretData.secretNamePath,
encryptedDEK: existingSecretData.encryptedDEK
};
} else {
console.log("Legacy secret detected for client:", clientName, "- Missing kmsPath, checking KMS service");
const existingKMSPath = await kmsService_default.getExistingKMSPath(clientName);
if (existingKMSPath) {
const secretId2 = existingSecretName.split("/").pop() || `secret-${clientName}`;
console.log("Found existing KMS key for legacy secret - KMS Path:", existingKMSPath);
return {
kmsPath: existingKMSPath,
secretId: secretId2,
secretNamePath: existingSecretData.secretNamePath,
encryptedDEK: existingSecretData.encryptedDEK
};
} else {
console.log("No existing KMS key found for client:", clientName, "- Will create new secret");
}
}
}
console.log("No existing secret found for client:", clientName, "- Creating new secret");
const dek = generateDEK();
const { encryptedDEK, kmsPath } = await kmsService_default.encryptDEK(dek, clientName);
const secretId = kmsService_default.createSecretKeyWithEnv(clientName);
const { secretNamePath } = await secretManagerService_default.createSecret(secretId, {
encryptedDEK,
kmsPath
});
return {
kmsPath,
secretId,
secretNamePath,
encryptedDEK
};
} catch (error) {
throw new EncryptionError(
`Failed to create new encryption: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.CREATION_ERROR
);
}
};
// src/services/encryptionService.ts
import fs2 from "fs";
import path4 from "path";
var EncryptionService = class {
constructor() {
const configPath = process.env.CONFIG_PATH || path4.join(process.cwd(), "config/encryption.json");
const keyMaping = path4.join(process.cwd(), "config/keyMaping.json");
this.config = JSON.parse(fs2.readFileSync(configPath, "utf8"));
this.keyMaping = JSON.parse(fs2.readFileSync(keyMaping, "utf8"));
}
/**
* Encrypts an object's fields based on model configuration
* @param params - Object containing required parameters for encryption
* @returns Object containing encrypted data and key metadata
* @throws {ConfigurationError} When encryption configuration is missing
* @throws {EncryptionError} When encryption operations fail
* @throws {ValidationError} When required parameters are missing
*/
async encryptObject({
modelName,
data,
entityKeyDetailsResult
}) {
try {
if (!modelName) {
throw new ValidationError(
"Model name is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
if (!data) {
throw new ValidationError(
"Data to encrypt is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
if (!(entityKeyDetailsResult == null ? void 0 : entityKeyDetailsResult.kmsPath)) {
throw new ValidationError(
"Entity key details are required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
try {
const { kmsPath, secretId, secretNamePath, encryptedDEK } = entityKeyDetailsResult;
if (!kmsPath || !secretId || !secretNamePath) {
throw new ValidationError(
"Missing required key details",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const { dek, encryptedDEK: encryptedDEKFromSecret } = await this.resolveDEKFromEntityKeyDetails({
kmsPath,
secretId,
secretNamePath,
encryptedDEK
});
const encryptedData = await handleFieldEncryption(modelName, data, dek);
return {
encryptedData,
encryptedDEK: encryptedDEKFromSecret
};
} catch (error) {
throw new EncryptionError(
`Failed to use existing key details: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.KEY_DETAILS_ERROR
);
}
} catch (error) {
if (error instanceof EncryptionError || error instanceof ConfigurationError || error instanceof ValidationError) {
throw error;
}
throw new EncryptionError(
`Unexpected error during encryption: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.UNKNOWN_ERROR
);
}
}
/**
* Resolves DEK from entity key details, handling both cached and non-cached scenarios
* @param entityKeyDetails - Entity's key details containing KMS metadata
* @returns Object containing decrypted DEK and key metadata
* @throws {ValidationError} When required key details are missing
* @throws {EncryptionError} When DEK resolution fails
*/
async resolveDEKFromEntityKeyDetails(entityKeyDetails) {
try {
if (!entityKeyDetails.secretId || !entityKeyDetails.kmsPath || !entityKeyDetails.secretNamePath) {
throw new ValidationError(
"Missing required key details",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
let encryptedDEK = entityKeyDetails.encryptedDEK;
if (!encryptedDEK) {
try {
encryptedDEK = await secretManagerService_default.getSecret(
entityKeyDetails.secretNamePath
);
} catch (error) {
throw new EncryptionError(
`Failed to retrieve secret: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.SECRET_RETRIEVAL_ERROR
);
}
}
try {
const dek = await kmsService_default.decryptDEK({
encryptedDEKData: encryptedDEK,
kmsPath: entityKeyDetails.kmsPath
});
return { dek, encryptedDEK };
} catch (error) {
throw new EncryptionError(
`Failed to decrypt DEK: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.DEK_DECRYPTION_ERROR
);
}
} catch (error) {
if (error instanceof ValidationError || error instanceof EncryptionError) {
throw error;
}
throw new EncryptionError(
`Failed to resolve DEK: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.DEK_RESOLUTION_ERROR
);
}
}
/**
* Decrypts an object's fields based on model configuration
* @param params - Object containing required parameters for decryption
* @returns Object containing decrypted data and optional encrypted DEK
* @throws {ConfigurationError} When encryption configuration is missing
* @throws {EncryptionError} When decryption operations fail
* @throws {ValidationError} When required parameters are missing
*/
async decryptObject({
modelName,
data,
entityKeyDetailsResult
}) {
var _a;
try {
if (!modelName) {
throw new ValidationError(
"Model name is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
if (!data) {
throw new ValidationError(
"Data to decrypt is required",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const modelConfig = this.config[modelName];
if (!modelConfig) {
throw new ConfigurationError(
`Encryption configuration not found for model ${modelName}`,
ErrorCodes.CONFIGURATION.MISSING_CONFIG
);
}
const decryptedData = { ...data };
let encryptedDEKFromSecret;
if (entityKeyDetailsResult == null ? void 0 : entityKeyDetailsResult.kmsPath) {
try {
const { kmsPath } = entityKeyDetailsResult;
if (!kmsPath) {
throw new ValidationError(
"Missing required key details",
ErrorCodes.VALIDATION.MISSING_REQUIRED_FIELD
);
}
const { dek, encryptedDEK } = await this.resolveDEKFromEntityKeyDetails(entityKeyDetailsResult);
encryptedDEKFromSecret = encryptedDEK;
for (const [fieldName, fieldConfig] of Object.entries(modelConfig)) {
if (fieldConfig.shouldDecrypt) {
const encryptedFieldName = `${fieldName}_encrypted`;
if (fieldConfig.isObject && typeof data[fieldName] === "object") {
const dataValues = ((_a = data[fieldName]) == null ? void 0 : _a.dataValues) || data[fieldName];
const nestedDecrypted = await this.decryptObject({
modelName: fieldName,
data: dataValues,
entityKeyDetailsResult
});
decryptedData[fieldName] = nestedDecrypted.decryptedData;
} else if (fieldConfig.isArrayOfObjects && Array.isArray(data[fieldName])) {
decryptedData[fieldName] = await Promise.all(
data[fieldName].map(async (item) => {
let modalNameFromKeyMaping;
if (this.keyMaping[fieldName]) {
modalNameFromKeyMaping = this.keyMaping[fieldName];
} else {
modalNameFromKeyMaping = fieldName;
}
const decrypted = await this.decryptObject({
modelName: modalNameFromKeyMaping,
data: (item == null ? void 0 : item.dataValues) || item,
entityKeyDetailsResult
});
return decrypted.decryptedData;
})
);
} else if (data[encryptedFieldName]) {
try {
const decryptedValue = await decryptField(
fieldName,
data,
dek
);
if (decryptedValue !== null) {
decryptedData[fieldName] = decryptedValue;
decryptedData[encryptedFieldName] = decryptedValue;
delete decryptedData[`${fieldName}_iv`];
delete decryptedData[`${fieldName}_auth_tag`];
} else {
console.log(`******************Field not able to decrypt sending original value**********************`);
decryptedData[encryptedFieldName] = decryptedValue;
delete decryptedData[`${fieldName}_iv`];
delete decryptedData[`${fieldName}_auth_tag`];
}
} catch (error) {
console.warn(`Failed to decrypt field ${fieldName}:`, error);
decryptedData[encryptedFieldName] = data[fieldName];
}
}
}
}
} catch (error) {
if (error instanceof ValidationError || error instanceof EncryptionError) {
throw error;
}
}
}
return { decryptedData, encryptedDEK: encryptedDEKFromSecret };
} catch (error) {
if (error instanceof EncryptionError || error instanceof ConfigurationError || error instanceof ValidationError) {
throw error;
}
throw new EncryptionError(
`Unexpected error during decryption: ${error instanceof Error ? error.message : "Unknown error"}`,
ErrorCodes.ENCRYPTION.UNKNOWN_ERROR
);
}
}
};
var encryptionService_default = new EncryptionService();
export {
ConfigurationError,
EncryptionError,
encryptionService_default as EncryptionService,
ErrorCodes,
ValidationError,
createHash_default as createHash,
createNewEncryption,
decryptField,
encryptField,
generateDEK,
handleFieldEncryption,
kmsService_default as kmsService,
secretManagerService_default as secretManagerService
};