Merge remote-tracking branch 'origin/feat/E2EE' into feat/E2EE

# Conflicts:
#	client/src/components/Nav/SettingsTabs/Chat/EncryptionPassphrase.tsx
#	client/src/hooks/SSE/useSSE.ts
#	packages/data-provider/src/types.ts
This commit is contained in:
Ruben Talstra 2025-02-16 09:48:56 +01:00
commit d4621c3ea8
No known key found for this signature in database
GPG key ID: 2A5A7174A60F3BEA
2 changed files with 132 additions and 0 deletions

View file

@ -0,0 +1,131 @@
import {
createPayload,
isAgentsEndpoint,
isAssistantsEndpoint,
removeNullishValues,
TPayload,
TSubmission,
} from 'librechat-data-provider';
/**
* Convert an ArrayBuffer to a Base64 string.
*/
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
bytes.forEach((b) => (binary += String.fromCharCode(b)));
return window.btoa(binary);
}
/**
* Convert a Base64 string to a Uint8Array.
*/
function 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;
}
/**
* Encrypts a plaintext string using RSA-OAEP.
* The public key must be provided as a Base64-encoded SPKI key.
*/
export async function encryptMessage(
plainText: string,
userEncryptionPublicKey: string
): Promise<string> {
// Convert the Base64 public key into a binary array.
const binaryKey = base64ToUint8Array(userEncryptionPublicKey);
const publicKey = await window.crypto.subtle.importKey(
'spki',
binaryKey.buffer,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
true,
['encrypt']
);
const encoder = new TextEncoder();
const encodedContent = encoder.encode(plainText);
const encryptedBuffer = await window.crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
publicKey,
encodedContent
);
// Return the encrypted data as a Base64 string.
return arrayBufferToBase64(encryptedBuffer);
}
/**
* Decrypts an encrypted string using RSA-OAEP.
* The private key must be provided as a Base64-encoded PKCS#8 key.
*/
export async function decryptMessage(
encryptedText: string,
userPrivateKey: string
): Promise<string> {
// Convert the Base64-encoded private key to a binary array.
const binaryKey = base64ToUint8Array(userPrivateKey);
const privateKey = await window.crypto.subtle.importKey(
'pkcs8',
binaryKey.buffer,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
true,
['decrypt']
);
// Convert the Base64-encoded encrypted text to a binary array.
const encryptedBuffer = base64ToUint8Array(encryptedText);
const decryptedBuffer = await window.crypto.subtle.decrypt(
{ name: 'RSA-OAEP' },
privateKey,
encryptedBuffer.buffer
);
const decoder = new TextDecoder();
return decoder.decode(decryptedBuffer);
}
/**
* Creates a payload from the submission.
* If encryption is enabled (or if a userEncryptionPublicKey is available),
* it encrypts the text and attaches a random IV (used as a flag).
*/
export async function createPayloadWithEncryption(
submission: TSubmission,
encryptionEnabled: boolean,
userEncryptionPublicKey?: string
): Promise<{ server: string; payload: TPayload }> {
// Create the standard payload.
const payloadData = createPayload(submission);
let { payload } = payloadData;
// Remove nullish values for endpoints that require it.
if (isAssistantsEndpoint(payload.endpoint) || isAgentsEndpoint(payload.endpoint)) {
payload = removeNullishValues(payload) as TPayload;
}
// Force encryption if encryptionEnabled is true OR a public key is available.
if ((encryptionEnabled || userEncryptionPublicKey) && userEncryptionPublicKey) {
const plainText = payload.text;
if (plainText !== undefined) {
// Generate a random IV (12 bytes for AES-GCM). Although RSA-OAEP doesnt need an IV,
// we attach it as a marker that the message is encrypted.
const ivArray = new Uint8Array(12);
window.crypto.getRandomValues(ivArray);
const ivString = arrayBufferToBase64(ivArray.buffer);
// Encrypt the message text.
const encryptedText = await encryptMessage(plainText, userEncryptionPublicKey);
console.log('Encryption successful:', { encryptedText, iv: ivString });
payload.text = encryptedText;
// Attach the IV as a marker.
(payload as TPayload & { messageEncryptionIV?: string }).messageEncryptionIV = ivString;
}
}
return { server: payloadData.server, payload };
}

View file

@ -505,6 +505,7 @@ export type TMessage = z.input<typeof tMessageSchema> & {
siblingIndex?: number; siblingIndex?: number;
attachments?: TAttachment[]; attachments?: TAttachment[];
clientTimestamp?: string; clientTimestamp?: string;
messageEncryptionIV?: string;
}; };
export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => { export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => {