feat: implement end-to-end encryption for chat messages and files, including key management and decryption functionality; enhance chat components to support encrypted content display

This commit is contained in:
2025-12-09 01:31:09 -03:00
parent cae6d886de
commit e6f380d7cc
14 changed files with 1443 additions and 203 deletions

View File

@@ -0,0 +1,301 @@
/**
* Utilitário de Criptografia End-to-End (E2E) para mensagens do chat
* Usa Web Crypto API com AES-GCM para criptografia simétrica
*/
export interface EncryptionKey {
key: CryptoKey;
keyId: string; // Identificador único da chave
createdAt: number; // Timestamp de criação
}
export interface EncryptedMessage {
encryptedContent: string; // Base64
iv: string; // Base64
keyId: string; // Identificador da chave usada
}
/**
* Gera uma chave de criptografia AES-GCM de 256 bits
*/
export async function generateEncryptionKey(): Promise<EncryptionKey> {
const key = await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true, // extractable
['encrypt', 'decrypt']
);
// Gerar um ID único para a chave
const keyId = crypto.randomUUID();
return {
key,
keyId,
createdAt: Date.now()
};
}
/**
* Exporta uma chave para formato JSON (armazenamento)
*/
export async function exportKey(key: CryptoKey): Promise<string> {
const exported = await crypto.subtle.exportKey('raw', key);
const exportedArray = new Uint8Array(exported);
return btoa(String.fromCharCode(...exportedArray));
}
/**
* Importa uma chave de formato JSON (armazenamento)
*/
export async function importKey(keyData: string): Promise<CryptoKey> {
const keyArray = Uint8Array.from(atob(keyData), (c) => c.charCodeAt(0));
return await crypto.subtle.importKey(
'raw',
keyArray,
{
name: 'AES-GCM',
length: 256
},
true, // extractable
['encrypt', 'decrypt']
);
}
/**
* Criptografa uma mensagem usando uma chave
*/
export async function encryptMessage(
message: string,
key: CryptoKey
): Promise<EncryptedMessage> {
const encoder = new TextEncoder();
const data = encoder.encode(message);
// Gerar IV (Initialization Vector) aleatório de 12 bytes para AES-GCM
const iv = crypto.getRandomValues(new Uint8Array(12));
// Criptografar
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
data
);
// Converter para base64 para armazenamento
const encryptedArray = new Uint8Array(encrypted);
const encryptedBase64 = btoa(String.fromCharCode(...encryptedArray));
const ivBase64 = btoa(String.fromCharCode(...iv));
// Gerar keyId (usaremos o hash da chave exportada como identificador)
const keyData = await exportKey(key);
const keyId = await hashString(keyData);
return {
encryptedContent: encryptedBase64,
iv: ivBase64,
keyId
};
}
/**
* Criptografa um arquivo (Blob) usando uma chave
*/
export async function encryptFile(
file: Blob,
key: CryptoKey
): Promise<{ encryptedBlob: Blob; iv: string; keyId: string }> {
const arrayBuffer = await file.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
// Gerar IV (Initialization Vector) aleatório de 12 bytes para AES-GCM
const iv = crypto.getRandomValues(new Uint8Array(12));
// Criptografar
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
data
);
// Converter para Blob
const encryptedBlob = new Blob([encrypted], { type: 'application/octet-stream' });
// Converter IV para base64
const ivBase64 = btoa(String.fromCharCode(...iv));
// Gerar keyId
const keyData = await exportKey(key);
const keyId = await hashString(keyData);
return {
encryptedBlob,
iv: ivBase64,
keyId
};
}
/**
* Descriptografa um arquivo (Blob) usando uma chave
*/
export async function decryptFile(
encryptedBlob: Blob,
iv: string,
key: CryptoKey
): Promise<Blob> {
try {
// Decodificar base64 do IV
const ivArray = Uint8Array.from(atob(iv), (c) => c.charCodeAt(0));
// Ler dados criptografados
const encryptedArrayBuffer = await encryptedBlob.arrayBuffer();
const encrypted = new Uint8Array(encryptedArrayBuffer);
// Descriptografar
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: ivArray
},
key,
encrypted
);
// Converter para Blob
return new Blob([decrypted]);
} catch (error) {
console.error('Erro ao descriptografar arquivo:', error);
throw new Error('Falha ao descriptografar arquivo. A chave pode estar incorreta.');
}
}
/**
* Descriptografa uma mensagem usando uma chave
*/
export async function decryptMessage(
encryptedMessage: EncryptedMessage,
key: CryptoKey
): Promise<string> {
try {
// Decodificar base64
const encryptedArray = Uint8Array.from(
atob(encryptedMessage.encryptedContent),
(c) => c.charCodeAt(0)
);
const iv = Uint8Array.from(atob(encryptedMessage.iv), (c) => c.charCodeAt(0));
// Descriptografar
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encryptedArray
);
// Converter para string
const decoder = new TextDecoder();
return decoder.decode(decrypted);
} catch (error) {
console.error('Erro ao descriptografar mensagem:', error);
throw new Error('Falha ao descriptografar mensagem. A chave pode estar incorreta.');
}
}
/**
* Gera hash SHA-256 de uma string (para usar como keyId)
*/
async function hashString(str: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = new Uint8Array(hashBuffer);
// Retornar apenas os primeiros 16 bytes como hex para keyId mais curto
return Array.from(hashArray.slice(0, 16))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* Armazena uma chave de criptografia no localStorage
*/
export function storeEncryptionKey(conversaId: string, keyData: string, keyId: string): void {
try {
const storageKey = `e2e_key_${conversaId}`;
const keyInfo = {
keyData,
keyId,
createdAt: Date.now()
};
localStorage.setItem(storageKey, JSON.stringify(keyInfo));
} catch (error) {
console.error('Erro ao armazenar chave de criptografia:', error);
throw new Error('Falha ao armazenar chave de criptografia');
}
}
/**
* Recupera uma chave de criptografia do localStorage
*/
export function getStoredEncryptionKey(conversaId: string): {
keyData: string;
keyId: string;
createdAt: number;
} | null {
try {
const storageKey = `e2e_key_${conversaId}`;
const stored = localStorage.getItem(storageKey);
if (!stored) {
return null;
}
return JSON.parse(stored) as { keyData: string; keyId: string; createdAt: number };
} catch (error) {
console.error('Erro ao recuperar chave de criptografia:', error);
return null;
}
}
/**
* Remove uma chave de criptografia do localStorage
*/
export function removeStoredEncryptionKey(conversaId: string): void {
try {
const storageKey = `e2e_key_${conversaId}`;
localStorage.removeItem(storageKey);
} catch (error) {
console.error('Erro ao remover chave de criptografia:', error);
}
}
/**
* Verifica se uma conversa tem criptografia E2E habilitada
*/
export function hasEncryptionKey(conversaId: string): boolean {
return getStoredEncryptionKey(conversaId) !== null;
}
/**
* Carrega e importa uma chave de criptografia do localStorage
*/
export async function loadEncryptionKey(conversaId: string): Promise<CryptoKey | null> {
const stored = getStoredEncryptionKey(conversaId);
if (!stored) {
return null;
}
try {
return await importKey(stored.keyData);
} catch (error) {
console.error('Erro ao importar chave de criptografia:', error);
return null;
}
}

View File

@@ -436,3 +436,6 @@ export function adicionarRodape(doc: jsPDF): void {