268 lines
8.6 KiB
Svelte
268 lines
8.6 KiB
Svelte
<script lang="ts">
|
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
import { Lock, X, RefreshCw, Shield, AlertTriangle, CheckCircle } from 'lucide-svelte';
|
|
import {
|
|
generateEncryptionKey,
|
|
exportKey,
|
|
storeEncryptionKey,
|
|
hasEncryptionKey,
|
|
removeStoredEncryptionKey
|
|
} from '$lib/utils/e2eEncryption';
|
|
import { armazenarChaveCriptografia, removerChaveCriptografia } from '$lib/stores/chatStore';
|
|
import { format } from 'date-fns';
|
|
import { ptBR } from 'date-fns/locale';
|
|
|
|
interface Props {
|
|
conversaId: Id<'conversas'>;
|
|
onClose: () => void;
|
|
}
|
|
|
|
let { conversaId, onClose }: Props = $props();
|
|
|
|
const client = useConvexClient();
|
|
const temCriptografiaE2E = useQuery(api.chat.verificarCriptografiaE2E, { conversaId });
|
|
const chaveAtual = useQuery(api.chat.obterChaveCriptografia, { conversaId });
|
|
const conversa = useQuery(api.chat.listarConversas, {});
|
|
|
|
let ativando = $state(false);
|
|
let regenerando = $state(false);
|
|
let desativando = $state(false);
|
|
|
|
// Obter informações da conversa
|
|
const conversaInfo = $derived(() => {
|
|
if (!conversa?.data || !Array.isArray(conversa.data)) return null;
|
|
return conversa.data.find((c: { _id: string }) => c._id === conversaId) || null;
|
|
});
|
|
|
|
async function ativarE2E() {
|
|
if (!confirm('Deseja ativar criptografia end-to-end para esta conversa?\n\nTodas as mensagens futuras serão criptografadas.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
ativando = true;
|
|
|
|
// Gerar nova chave de criptografia
|
|
const encryptionKey = await generateEncryptionKey();
|
|
const keyData = await exportKey(encryptionKey.key);
|
|
|
|
// Armazenar localmente
|
|
storeEncryptionKey(conversaId, keyData, encryptionKey.keyId);
|
|
armazenarChaveCriptografia(conversaId, encryptionKey.key);
|
|
|
|
// Compartilhar chave com outros participantes
|
|
await client.mutation(api.chat.compartilharChaveCriptografia, {
|
|
conversaId,
|
|
chaveCompartilhada: keyData, // Em produção, isso deveria ser criptografado com chave pública de cada participante
|
|
keyId: encryptionKey.keyId
|
|
});
|
|
|
|
alert('Criptografia E2E ativada com sucesso!');
|
|
} catch (error) {
|
|
console.error('Erro ao ativar E2E:', error);
|
|
alert('Erro ao ativar criptografia E2E');
|
|
} finally {
|
|
ativando = false;
|
|
}
|
|
}
|
|
|
|
async function regenerarChave() {
|
|
if (!confirm('Deseja regenerar a chave de criptografia?\n\nAs mensagens antigas continuarão legíveis, mas novas mensagens usarão a nova chave.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
regenerando = true;
|
|
|
|
// Gerar nova chave
|
|
const encryptionKey = await generateEncryptionKey();
|
|
const keyData = await exportKey(encryptionKey.key);
|
|
|
|
// Atualizar chave localmente
|
|
storeEncryptionKey(conversaId, keyData, encryptionKey.keyId);
|
|
armazenarChaveCriptografia(conversaId, encryptionKey.key);
|
|
|
|
// Compartilhar nova chave (desativa chaves antigas automaticamente)
|
|
await client.mutation(api.chat.compartilharChaveCriptografia, {
|
|
conversaId,
|
|
chaveCompartilhada: keyData,
|
|
keyId: encryptionKey.keyId
|
|
});
|
|
|
|
alert('Chave regenerada com sucesso!');
|
|
} catch (error) {
|
|
console.error('Erro ao regenerar chave:', error);
|
|
alert('Erro ao regenerar chave');
|
|
} finally {
|
|
regenerando = false;
|
|
}
|
|
}
|
|
|
|
async function desativarE2E() {
|
|
if (!confirm('Deseja desativar criptografia end-to-end para esta conversa?\n\nAs mensagens antigas continuarão criptografadas, mas novas mensagens não serão mais criptografadas.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
desativando = true;
|
|
|
|
// Remover chave localmente
|
|
removeStoredEncryptionKey(conversaId);
|
|
removerChaveCriptografia(conversaId);
|
|
|
|
// Desativar chave no servidor (marcar como inativa)
|
|
// Nota: Não removemos a chave do servidor, apenas a marcamos como inativa
|
|
// Isso permite que mensagens antigas ainda possam ser descriptografadas
|
|
if (chaveAtual?.data) {
|
|
// A mutation compartilharChaveCriptografia já desativa chaves antigas
|
|
// Mas precisamos de uma mutation específica para desativar completamente
|
|
// Por enquanto, vamos apenas remover localmente
|
|
alert('Criptografia E2E desativada localmente. As mensagens antigas ainda podem ser descriptografadas se você tiver a chave.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao desativar E2E:', error);
|
|
alert('Erro ao desativar criptografia E2E');
|
|
} finally {
|
|
desativando = false;
|
|
}
|
|
}
|
|
|
|
function formatarData(timestamp: number): string {
|
|
try {
|
|
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR });
|
|
} catch {
|
|
return 'Data inválida';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="modal modal-open"
|
|
role="dialog"
|
|
aria-labelledby="modal-title"
|
|
aria-modal="true"
|
|
onclick={(e) => {
|
|
if (e.target === e.currentTarget) onClose();
|
|
}}
|
|
>
|
|
<div
|
|
class="modal-box max-w-2xl"
|
|
onclick={(e) => e.stopPropagation()}
|
|
>
|
|
<!-- Header -->
|
|
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
|
<h2 id="modal-title" class="flex items-center gap-2 text-xl font-bold">
|
|
<Shield class="text-primary h-5 w-5" />
|
|
Criptografia End-to-End (E2E)
|
|
</h2>
|
|
<button type="button" class="btn btn-sm btn-circle" onclick={onClose} aria-label="Fechar">
|
|
<X class="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 space-y-6 overflow-y-auto p-6">
|
|
<!-- Status da Criptografia -->
|
|
<div class="card bg-base-200">
|
|
<div class="card-body">
|
|
<div class="flex items-center gap-3">
|
|
{#if temCriptografiaE2E?.data}
|
|
<CheckCircle class="text-success h-6 w-6 shrink-0" />
|
|
<div class="flex-1">
|
|
<h3 class="card-title text-lg text-success">Criptografia E2E Ativa</h3>
|
|
<p class="text-sm text-base-content/70">
|
|
Suas mensagens estão protegidas com criptografia end-to-end
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<AlertTriangle class="text-warning h-6 w-6 shrink-0" />
|
|
<div class="flex-1">
|
|
<h3 class="card-title text-lg text-warning">Criptografia E2E Desativada</h3>
|
|
<p class="text-sm text-base-content/70">
|
|
Suas mensagens não estão criptografadas
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Informações da Chave -->
|
|
{#if temCriptografiaE2E?.data && chaveAtual?.data}
|
|
<div class="card bg-base-200">
|
|
<div class="card-body">
|
|
<h3 class="card-title text-lg">Informações da Chave</h3>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-base-content/70">ID da Chave:</span>
|
|
<span class="font-mono text-xs">{chaveAtual.data.keyId.substring(0, 16)}...</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-base-content/70">Criada em:</span>
|
|
<span>{formatarData(chaveAtual.data.criadoEm)}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-base-content/70">Chave local:</span>
|
|
<span class="text-success">
|
|
{hasEncryptionKey(conversaId) ? '✓ Armazenada' : '✗ Não encontrada'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Informações sobre E2E -->
|
|
<div class="alert alert-info">
|
|
<Lock class="h-5 w-5" />
|
|
<div class="text-sm">
|
|
<p class="font-semibold">Como funciona a criptografia E2E?</p>
|
|
<ul class="mt-2 list-inside list-disc space-y-1 text-xs">
|
|
<li>Suas mensagens são criptografadas no seu dispositivo antes de serem enviadas</li>
|
|
<li>Apenas você e os participantes da conversa podem descriptografar as mensagens</li>
|
|
<li>O servidor não consegue ler o conteúdo das mensagens criptografadas</li>
|
|
<li>Mensagens antigas continuam legíveis mesmo após regenerar a chave</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ações -->
|
|
<div class="flex flex-col gap-3">
|
|
{#if temCriptografiaE2E?.data}
|
|
<button
|
|
type="button"
|
|
class="btn btn-warning"
|
|
onclick={regenerarChave}
|
|
disabled={regenerando || ativando || desativando}
|
|
>
|
|
<RefreshCw class="h-4 w-4 {regenerando ? 'animate-spin' : ''}" />
|
|
{regenerando ? 'Regenerando...' : 'Regenerar Chave'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-error"
|
|
onclick={desativarE2E}
|
|
disabled={regenerando || ativando || desativando}
|
|
>
|
|
<X class="h-4 w-4" />
|
|
{desativando ? 'Desativando...' : 'Desativar E2E'}
|
|
</button>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
onclick={ativarE2E}
|
|
disabled={regenerando || ativando || desativando}
|
|
>
|
|
<Lock class="h-4 w-4" />
|
|
{ativando ? 'Ativando...' : 'Ativar Criptografia E2E'}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|