Skip to content

Instantly share code, notes, and snippets.

@ciekawy
Created April 19, 2024 15:43
Show Gist options
  • Save ciekawy/0a2c2e6ab5a0f47b6b17eea9c267fcb8 to your computer and use it in GitHub Desktop.
Save ciekawy/0a2c2e6ab5a0f47b6b17eea9c267fcb8 to your computer and use it in GitHub Desktop.
casual LLM Clash - evaluated responses

Client-Side Encryption:

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);
}

Granular Field-Level Encryption:

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;
} 

Creating a comprehensive, commented implementation for client-side field-based encryption using JavaScript or TypeScript involves several steps. In this response, I'll provide you with a high-level overview and a basic implementation using the Web Crypto API. However, please note that you should adapt this code to your specific use case and thoroughly test it before using it in a production environment.

Client-Side Encryption:

To encrypt sensitive data on the client-side, we'll use the Web Crypto API, which provides a set of cryptographic primitives for developers. In this example, we'll use the AES-GCM algorithm for encryption and decryption.

// Generate a random 256-bit key
window.crypto.subtle.generateKey(
  {
    name: "AES-GCM",
    length: 256,
  },
  true,
  ["encrypt", "decrypt"]
)
  .then(function (key) {
    // Export the key for storage (not recommended for production)
    return window.crypto.subtle.exportKey("jwk", key);
  })
  .then(function (keyData) {
    console.log(keyData);
  })
  .catch(function (err) {
    console.error(err);
  });

Granular Field-Level Encryption:

To encrypt specific fields or objects within the data model, you can create a helper function that takes the data, the fields to be encrypted, and the encryption key as input. This function should iterate over the specified fields, encrypt their values, and return the encrypted data.

async function encryptFields(data, fieldsToEncrypt, key) {
  const encryptedData = {};

  for (const [field, value] of Object.entries(data)) {
    if (fieldsToEncrypt.includes(field)) {
      const encryptedField = await window.crypto.subtle.encrypt(
        {
          name: "AES-GCM",
          iv: window.crypto.getRandomValues(new Uint8Array(12)), // Generate a random IV
        },
        key,
        new TextEncoder().encode(value)
      );

      encryptedData[field] = encryptedField;
    } else {
      encryptedData[field] = value;
    }
  }

  return encryptedData;
}

Secure Key Generation and Storage:

In the previous code snippets, we generated a random encryption key and exported it for storage. However, storing the key in this manner is not recommended for production environments. Instead, consider using secure storage mechanisms provided by the platform, such as the Web Crypto API's CryptoKey object or browser-specific secure storage APIs like the WebAuthn API or the Web Cryptography API's KeyStorage interface. Here's an example of how to store the key using the CryptoKey object:

// Generate a random 256-bit key and store it in a CryptoKey object
window.crypto.subtle.generateKey(
  {
    name: "AES-GCM",
    length: 256,
  },
  true,
  ["encrypt", "decrypt"]
)
  .then(function (key) {
    // Store the key in a variable or a secure storage mechanism
    window.encryptionKey = key;
  })
  .catch(function (err) {
    console.error(err);
  });

When implementing client-side encryption, keep in mind that it introduces additional complexity and potential security risks. Ensure that you follow best practices for secure key management, data validation, and error handling. Additionally, consider consulting with a security expert to review your implementation and identify any potential vulnerabilities.

Here is a comprehensive implementation of client-side field-based encryption of user data in JavaScript/TypeScript: // Import the Web Crypto API const crypto = window.crypto;

// Define a configuration object to specify which fields to encrypt interface EncryptionConfig { [key: string]: boolean; // field name: boolean indicating whether to encrypt }

// Define a data model interface interface UserData { name: string; email: string; password: string; address: { street: string; city: string; state: string; zip: string; }; }

// Generate a secure encryption key on the client-side async function generateKey(): Promise { const key = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256, }, true, ["encrypt", "decrypt"] ); return key; }

// Encrypt a single field using the Web Crypto API async function encryptField(field: string, data: string, key: CryptoKey): Promise { const iv = crypto.getRandomValues(new Uint8Array(12)); const encryptedData = await crypto.subtle.encrypt( { name: "AES-GCM", iv, }, key, new TextEncoder().encode(data) ); const encryptedDataArray = new Uint8Array(encryptedData); const encryptedDataBase64 = btoa(String.fromCharCode(...encryptedDataArray)); return ${iv.toString("hex")}:${encryptedDataBase64}; }

// Decrypt a single field using the Web Crypto API async function decryptField(field: string, encryptedData: string, key: CryptoKey): Promise { const [ivBase64, encryptedDataBase64] = encryptedData.split(":"); const iv = new Uint8Array(ivBase64, "hex"); const encryptedDataArray = new Uint8Array(atob(encryptedDataBase64), "base64"); const decryptedData = await crypto.subtle.decrypt( { name: "AES-GCM", iv, }, key, encryptedDataArray ); return new TextDecoder().decode(decryptedData); }

// Encrypt user data based on the encryption config async function encryptUserData(userData: UserData, encryptionConfig: EncryptionConfig, key: CryptoKey): Promise { const encryptedUserData: UserData = {...userData }; for (const field in encryptionConfig) { if (encryptionConfig[field]) { encryptedUserData[field] = await encryptField(field, userData[field], key); } } return encryptedUserData; }

// Decrypt user data based on the encryption config async function decryptUserData(encryptedUserData: UserData, encryptionConfig: EncryptionConfig, key: CryptoKey): Promise { const userData: UserData = {...encryptedUserData }; for (const field in encryptionConfig) { if (encryptionConfig[field]) { userData[field] = await decryptField(field, encryptedUserData[field], key); } } return userData; }

// Example usage: const userData: UserData = { name: "John Doe", email: "johndoe@example.com", password: "mysecretpassword", address: { street: "123 Main St", city: "Anytown", state: "CA", zip: "12345", }, };

const encryptionConfig: EncryptionConfig = { password: true, address: { street: true, city: true, }, };

async function main() { const key = await generateKey(); const encryptedUserData = await encryptUserData(userData, encryptionConfig, key); console.log("Encrypted user data:", encryptedUserData);

const decryptedUserData = await decryptUserData(encryptedUserData, encryptionConfig, key); console.log("Decrypted user data:", decryptedUserData); }

main();

This implementation provides: Client-Side Encryption: Encrypts sensitive data on the client-side before sending it to the backend using the Web Crypto API. Granular Field-Level Encryption: Allows developers to specify which fields or objects within the data model should be encrypted using a configuration object. Secure Key Generation and Storage: Generates a secure encryption key on the client-side using the Web Crypto API and stores it securely using the CryptoKey object.

Note that this implementation uses the AES-GCM encryption algorithm, which is a widely used and secure encryption algorithm. However, you should consider using a more secure algorithm like AES-256-GCM or ChaCha20-Poly1305 depending on your specific requirements. Also, this implementation assumes that the encryption key is stored securely on the client-side. In a real-world scenario, you should consider using a secure storage mechanism like the Web Crypto API's CryptoKey object or a secure storage provided by the platform (e.g., IndexedDB, LocalStorage, or a secure token storage). Remember to adapt this implementation to your specific use case and requirements.

To implement client-side, field-based encryption of user data using JavaScript or TypeScript with the Web Crypto API, you'll need to take a multi-part approach. This implementation will cover key generation, secure storage of keys, encryption of specified fields, and decryption of those fields when needed.

Step 1: Secure Key Generation and Storage

You need to generate a secure encryption key that will be used to encrypt the data. This key should be stored securely on the client-side and should not be exposed to the backend.

const generateKey = async (): Promise<CryptoKey> => {
    try {
        const key = await window.crypto.subtle.generateKey(
            {
                name: "AES-GCM",
                length: 256,
            },
            true, // whether the key is extractable (i.e., can be exported)
            ["encrypt", "decrypt"] // can be used to encrypt and decrypt
        );
        return key;
    } catch (err) {
        console.error("Error generating key:", err);
        throw err;
    }
};

Step 2: Encryption and Decryption Functions

Using the Web Crypto API, create functions to encrypt and decrypt data. AES-GCM is chosen here due to its strength and efficiency for modern applications.

const encryptData = async (key: CryptoKey, data: string): Promise<ArrayBuffer> => {
    const encoded = new TextEncoder().encode(data);
    const iv = window.crypto.getRandomValues(new Uint8Array(12)); // generate a new IV for each encryption

    try {
        const encrypted = await window.crypto.subtle.encrypt(
            {
                name: "AES-GCM",
                iv: iv,
            },
            key,
            encoded
        );

        // Combine the IV with the encrypted data (IV is needed for decryption)
        const combined = new Uint8Array(iv.length + encrypted.byteLength);
        combined.set(iv, 0);
        combined.set(new Uint8Array(encrypted), iv.length);

        return combined.buffer;
    } catch (err) {
        console.error("Error encrypting data:", err);
        throw err;
    }
};

const decryptData = async (key: CryptoKey, encryptedData: ArrayBuffer): Promise<string> => {
    const iv = new Uint8Array(encryptedData, 0, 12);
    const data = new Uint8Array(encryptedData, 12);

    try {
        const decrypted = await window.crypto.subtle.decrypt(
            {
                name: "AES-GCM",
                iv: iv,
            },
            key,
            data
        );

        return new TextDecoder().decode(decrypted);
    } catch (err) {
        console.error("Error decrypting data:", err);
        throw err;
    }
};

Step 3: Configurable Field-Level Encryption

To allow developers to specify which fields to encrypt, you could implement a higher-level function that accepts an object, a key, and a configuration specifying which fields to encrypt.

interface EncryptionConfig {
    fields: string[]; // list of keys in the object to encrypt
}

const encryptObjectFields = async (object: any, key: CryptoKey, config: EncryptionConfig): Promise<any> => {
    const encryptedObject = { ...object };

    for (const field of config.fields) {
        if (object[field]) {
            const encryptedField = await encryptData(key, JSON.stringify(object[field]));
            encryptedObject[field] = Buffer.from(encryptedField).toString('base64');
        }
    }

    return encryptedObject;
};

const decryptObjectFields = async (object: any, key: CryptoKey, config: EncryptionConfig): Promise<any> => {
    const decryptedObject = { ...object };

    for (const field of config.fields) {
        if (object[field]) {
            const encryptedData = Buffer.from(object[field], 'base64');
            const decryptedField = await decryptData(key, encryptedData.buffer);
            decryptedObject[field] = JSON.parse(decryptedField);
        }
    }

    return decryptedObject;
};

Usage Example

const keyPromise = generateKey(); // generate and store this key securely, e.g., IndexedDB

keyPromise.then(async (key) => {
    const userData = {
        name: "John Doe",
        email: "john@example.com",
        sensitiveData: "This is very sensitive data."
    };

    const config = {
        fields: ["sensitiveData"]
    };

    const encryptedData = await encryptObjectFields(userData, key, config);
    console.log("Encrypted:", encryptedData);

    const decryptedData = await decryptObjectFields(encryptedData, key, config);
    console.log("Decrypted:", decryptedData);
});

This setup ensures that sensitive data is encrypted client-side, the encryption keys never leave the client, and only specified fields are encrypted. For production use, consider how keys are managed, potentially integrating with platform-specific secure storage mechanisms, and handling

errors and security best practices more robustly.

Here's a comprehensive implementation of client-side field-based encryption of user data using TypeScript:

// fieldEncryption.ts

// Import required libraries
import { subtle } from 'crypto';

// Define the fields to be encrypted
type EncryptedFields<T> = {
  [K in keyof T]?: T[K] extends object ? EncryptedFields<T[K]> : boolean;
};

// Configuration interface
interface EncryptionConfig<T> {
  fields: EncryptedFields<T>;
}

// Encryption class
class FieldEncryption<T> {
  private config: EncryptionConfig<T>;
  private key: CryptoKey | null = null;

  constructor(config: EncryptionConfig<T>) {
    this.config = config;
  }

  // Generate an encryption key
  async generateKey(): Promise<void> {
    this.key = await subtle.generateKey(
      { name: 'AES-GCM', length: 256 },
      true,
      ['encrypt', 'decrypt']
    );
  }

  // Encrypt data based on the specified fields
  async encrypt(data: T): Promise<T> {
    if (!this.key) {
      throw new Error('Encryption key not generated');
    }

    const encryptedData = { ...data };

    // Recursively encrypt fields
    const encryptFields = async (obj: any, fields: EncryptedFields<any>): Promise<void> => {
      for (const field in fields) {
        if (fields[field] === true) {
          const plaintext = JSON.stringify(obj[field]);
          const ciphertext = await this.encryptValue(plaintext);
          obj[field] = ciphertext;
        } else if (typeof fields[field] === 'object') {
          await encryptFields(obj[field], fields[field]);
        }
      }
    };

    await encryptFields(encryptedData, this.config.fields);

    return encryptedData;
  }

  // Decrypt data based on the specified fields
  async decrypt(data: T): Promise<T> {
    if (!this.key) {
      throw new Error('Encryption key not generated');
    }

    const decryptedData = { ...data };

    // Recursively decrypt fields
    const decryptFields = async (obj: any, fields: EncryptedFields<any>): Promise<void> => {
      for (const field in fields) {
        if (fields[field] === true) {
          const ciphertext = obj[field];
          const plaintext = await this.decryptValue(ciphertext);
          obj[field] = JSON.parse(plaintext);
        } else if (typeof fields[field] === 'object') {
          await decryptFields(obj[field], fields[field]);
        }
      }
    };

    await decryptFields(decryptedData, this.config.fields);

    return decryptedData;
  }

  // Encrypt a single value
  private async encryptValue(value: string): Promise<string> {
    const encoder = new TextEncoder();
    const plaintext = encoder.encode(value);

    const iv = crypto.getRandomValues(new Uint8Array(12));
    const ciphertext = await subtle.encrypt(
      { name: 'AES-GCM', iv: iv },
      this.key!,
      plaintext
    );

    const encryptedValue = {
      iv: this.arrayBufferToBase64(iv),
      ciphertext: this.arrayBufferToBase64(ciphertext),
    };

    return JSON.stringify(encryptedValue);
  }

  // Decrypt a single value
  private async decryptValue(encryptedValue: string): Promise<string> {
    const { iv, ciphertext } = JSON.parse(encryptedValue);

    const decoder = new TextDecoder();
    const plaintext = await subtle.decrypt(
      { name: 'AES-GCM', iv: this.base64ToArrayBuffer(iv) },
      this.key!,
      this.base64ToArrayBuffer(ciphertext)
    );

    return decoder.decode(plaintext);
  }

  // Utility function to convert ArrayBuffer to Base64
  private arrayBufferToBase64(buffer: ArrayBuffer): string {
    const bytes = new Uint8Array(buffer);
    const binary = String.fromCharCode.apply(null, Array.from(bytes));
    return btoa(binary);
  }

  // Utility function to convert Base64 to ArrayBuffer
  private base64ToArrayBuffer(base64: string): ArrayBuffer {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer;
  }
}

// Example usage
interface UserData {
  name: string;
  email: string;
  age: number;
  address: {
    street: string;
    city: string;
    country: string;
  };
}

const encryptionConfig: EncryptionConfig<UserData> = {
  fields: {
    email: true,
    address: {
      street: true,
      city: true,
    },
  },
};

const encryption = new FieldEncryption<UserData>(encryptionConfig);

// Generate encryption key
encryption.generateKey().then(() => {
  const userData: UserData = {
    name: 'John Doe',
    email: 'john@example.com',
    age: 30,
    address: {
      street: '123 Main St',
      city: 'New York',
      country: 'USA',
    },
  };

  // Encrypt user data
  encryption.encrypt(userData).then((encryptedData) => {
    console.log('Encrypted data:', encryptedData);

    // Send encrypted data to the backend
    // ...

    // Decrypt user data
    encryption.decrypt(encryptedData).then((decryptedData) => {
      console.log('Decrypted data:', decryptedData);
    });
  });
});

In this implementation:

  1. The EncryptionConfig interface allows developers to specify which fields should be encrypted by setting the corresponding field values to true. Nested objects can also be encrypted by specifying the nested fields.

  2. The FieldEncryption class takes the encryption configuration in its constructor and provides methods for generating an encryption key, encrypting data, and decrypting data.

  3. The generateKey method generates an AES-GCM encryption key using the Web Crypto API's subtle.generateKey method. The key is stored securely within the FieldEncryption instance.

  4. The encrypt method recursively traverses the data object based on the specified fields in the encryption configuration. It encrypts the values of the specified fields using the encryptValue method and replaces them with the encrypted ciphertext.

  5. The decrypt method recursively traverses the encrypted data object and decrypts the specified fields using the decryptValue method, restoring the original plaintext values.

  6. The encryptValue and decryptValue methods perform the actual encryption and decryption operations using the Web Crypto API's subtle.encrypt and subtle.decrypt methods, respectively. The encrypted values are stored as objects containing the initialization vector (IV) and ciphertext, serialized as JSON strings.

  7. The utility functions arrayBufferToBase64 and base64ToArrayBuffer are used to convert between ArrayBuffer and Base64 representations for serialization and deserialization.

In the example usage, an instance of FieldEncryption is created with the specified encryption configuration. The encryption key is generated, and the user data is encrypted using the encrypt method. The encrypted data can be sent to the backend, and when needed, it can be decrypted using the decrypt method.

Note: This implementation assumes a secure environment where the encryption keys are generated and stored securely on the client-side. Additional security measures, such as secure key storage and handling, should be implemented based on the specific requirements of the application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment