mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-23 20:00:15 +01:00
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:
commit
d4621c3ea8
2 changed files with 132 additions and 0 deletions
131
client/src/hooks/SSE/encryptionHelpers.ts
Normal file
131
client/src/hooks/SSE/encryptionHelpers.ts
Normal 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 doesn’t 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 };
|
||||||
|
}
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue