To implement client-side encryption, we will use the Web Crypto API for encryption and decryption operations. We will ensure that the backend never has access to the plaintext data or encryption keys. First, let's create a function to generate a secure encryption key using the Web Crypto API:
async function generateEncryptionKey(): Promise<CryptoKey> {
// Generate a new AES-GCM key
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256, // 256-bit key
},
true, // Extractable
["encrypt", "decrypt"]
);
return key;
}
Next, let's create a function to encrypt data using the generated encryption key:
async function encryptData(key: CryptoKey, data: string): Promise<ArrayBuffer> {
const iv = crypto.getRandomValues(new Uint8Array(12)); // Generate a random initialization vector
const encrypted = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
},
key,
new TextEncoder().encode(data)
);
// Combine the initialization vector and encrypted data into a single ArrayBuffer
const combined = new ArrayBuffer(iv.byteLength + encrypted.byteLength);
const combinedView = new Uint8Array(combined);
combinedView.set(iv, 0);
combinedView.set(new Uint8Array(encrypted), iv.byteLength);
return combined;
}
Now, let's create a function to decrypt data using the encryption key:
async function decryptData(key: CryptoKey, encryptedData: ArrayBuffer): Promise<string> {
const encryptedView = new Uint8Array(encryptedData);
const iv = encryptedView.slice(0, 12);
const encrypted = encryptedView.slice(12);
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
},
key,
encrypted
);
return new TextDecoder().decode(decrypted);
}
To allow developers to specify which fields or objects within the data model should be encrypted, we can create a configuration object that defines the fields to be encrypted.
interface EncryptionConfig {
[key: string]: {
type: "string" | "object";
fields?: EncryptionConfig;
};
}
const encryptionConfig: EncryptionConfig = {
sensitiveField1: {
type: "string",
},
sensitiveObject1: {
type: "object",
fields: {
nestedField1: {
type: "string",
},
nestedObject1: {
type: "object",
fields: {
deeplyNestedField1: {
type: "string",
},
},
},
},
},
};
Now, let's create a function to encrypt the fields defined in the configuration object:
async function encryptFields(key: CryptoKey, data: any, config: EncryptionConfig): Promise<any> {
const encryptedData: any = {};
for (const [field, value] of Object.entries(data)) {
if (config[field]) {
if (config[field].type === "string") {
encryptedData[field] = await encryptData(key, value as string);
} else if (config[field].type === "object") {
encryptedData[field] = await encryptFields(key, value, config[field].fields!);
}
} else {
encryptedData[field] = value;
}
}
return encryptedData;
}