From 606fea044ab1508919e29ed5521dcb6de8e1347f Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 15 Feb 2025 23:04:26 +0100 Subject: [PATCH] refactor: creating a starting point for `E2EE` --- .../Chat/EncryptionPassphrase.tsx | 69 ++--- client/src/hooks/SSE/encryptionHelpers.ts | 131 ++++++++ client/src/hooks/SSE/useSSE.ts | 287 +++++++++--------- packages/data-provider/src/schemas.ts | 1 + packages/data-provider/src/types.ts | 1 + 5 files changed, 301 insertions(+), 188 deletions(-) create mode 100644 client/src/hooks/SSE/encryptionHelpers.ts diff --git a/client/src/components/Nav/SettingsTabs/Chat/EncryptionPassphrase.tsx b/client/src/components/Nav/SettingsTabs/Chat/EncryptionPassphrase.tsx index 85ad8cbd05..5434dd89de 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/EncryptionPassphrase.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/EncryptionPassphrase.tsx @@ -15,24 +15,17 @@ import type { TUser } from 'librechat-data-provider'; import { useToastContext } from '~/Providers'; import { useSetUserEncryptionMutation } from '~/data-provider'; -// Helper: Convert a Base64 string to Uint8Array. -const base64ToUint8Array = (base64: string): Uint8Array => { - const binaryString = window.atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; -}; - -// Helper: Convert a Uint8Array to a hex string (for debugging). +/** + * Helper: Convert a Uint8Array to a hex string (for debugging). + */ const uint8ArrayToHex = (array: Uint8Array): string => Array.from(array) - .map(b => b.toString(16).padStart(2, '0')) + .map((b) => b.toString(16).padStart(2, '0')) .join(''); -// Derive an AES-GCM key from the passphrase using PBKDF2. +/** + * Derive an AES-GCM key from the passphrase using PBKDF2. + */ const deriveKey = async (passphrase: string, salt: Uint8Array): Promise => { const encoder = new TextEncoder(); const keyMaterial = await window.crypto.subtle.importKey( @@ -46,7 +39,7 @@ const deriveKey = async (passphrase: string, salt: Uint8Array): Promise => { - const ciphertext = base64ToUint8Array(ciphertextBase64); - const decryptedBuffer = await window.crypto.subtle.decrypt( - { name: 'AES-GCM', iv }, - derivedKey, - ciphertext - ); - const decoder = new TextDecoder(); - return decoder.decode(decryptedBuffer); -}; - const UserKeysSettings: FC = () => { const localize = useLocalize(); const { user } = useAuthContext(); @@ -122,22 +99,23 @@ const UserKeysSettings: FC = () => { true, ['encrypt', 'decrypt'] ); + // Export public and private keys. const publicKeyBuffer = await window.crypto.subtle.exportKey('spki', keyPair.publicKey); const privateKeyBuffer = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey); - const publicKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer))); - const privateKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer))); + const publicKeyBase64 = window.btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer))); + const privateKeyBase64 = window.btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer))); console.debug('New public key:', publicKeyBase64); console.debug('New private key (plaintext):', privateKeyBase64); - // Generate a salt and IV. + // Generate a salt and IV for AES-GCM. const salt = window.crypto.getRandomValues(new Uint8Array(16)); // 16 bytes salt const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 12 bytes IV // Derive a symmetric key from the passphrase. const derivedKey = await deriveKey(passphrase, salt); - // Encrypt the private key. + // Encrypt the private key using AES-GCM. const encoder = new TextEncoder(); const privateKeyBytes = encoder.encode(privateKeyBase64); const encryptedPrivateKeyBuffer = await window.crypto.subtle.encrypt( @@ -145,11 +123,13 @@ const UserKeysSettings: FC = () => { derivedKey, privateKeyBytes ); - const encryptedPrivateKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(encryptedPrivateKeyBuffer))); + const encryptedPrivateKeyBase64 = window.btoa( + String.fromCharCode(...new Uint8Array(encryptedPrivateKeyBuffer)) + ); - // Convert salt and IV to Base64. - const saltBase64 = btoa(String.fromCharCode(...salt)); - const ivBase64 = btoa(String.fromCharCode(...iv)); + // Convert salt and IV to Base64 strings. + const saltBase64 = window.btoa(String.fromCharCode(...salt)); + const ivBase64 = window.btoa(String.fromCharCode(...iv)); console.debug('Activation complete:'); console.debug('Encrypted private key:', encryptedPrivateKeyBase64); @@ -168,7 +148,7 @@ const UserKeysSettings: FC = () => { }; const handleSubmit = async (): Promise => { - // Activate encryption (or re-activate) by generating new keys. + // Activate encryption by generating new keys. const newEncryption = await activateEncryption(); if (newEncryption) { try { @@ -176,6 +156,7 @@ const UserKeysSettings: FC = () => { await setEncryption(newEncryption); showToast({ message: localize('com_ui_upload_success') }); // Update local user state with the new encryption keys. + // Later, when the user unlocks their keys, store the decrypted private key. setUser((prev) => ({ ...prev, ...newEncryption, @@ -198,9 +179,7 @@ const UserKeysSettings: FC = () => {
- - {localize('com_nav_chat_encryption_settings')} - + {localize('com_nav_chat_encryption_settings')}