diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 531cf19..d3e0bc3 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -18,16 +18,18 @@ - [x] Mesaj düzenleme/silme - [x] IDOR güvenlik açığı düzeltildi -### Faz 3: Dosya Yükleme (R2) 🔲 +### Faz 3: Dosya Yükleme (R2) ✅ - [x] Cloudflare R2 entegrasyonu - [x] Dosya upload/download API - [x] Dosya listeleme UI -### Faz 4: Client-side Encryption 🔲 -- [ ] Web Crypto API wrapper -- [ ] Encrypt/decrypt utilities -- [ ] Şifreli mesaj oluşturma akışı -- [ ] Decrypt sayfası (alıcı için) +### Faz 4: Client-side Encryption ✅ +- [x] Web Crypto API wrapper +- [x] Encrypt/decrypt utilities +- [x] Şifreli mesaj oluşturma akışı +- [x] Decrypt sayfası (alıcı için) +- [x] Şifreli mesaj düzenleme +- [x] Dosya adı şifreleme ### Faz 5: Check-in & Cron 🔲 - [ ] Check-in API endpoint @@ -54,6 +56,23 @@ - [ ] Production deployment - [ ] Monitoring setup +### Sonradan Yapılacaklar + 1. Static Salt in Key Derivation + - Severity: Medium + - Location: src/lib/crypto.ts (Lines 13-13) + - Line Content: + + 1 salt: encoder.encode("dead-man-switch-salt"), + - Description: + The deriveKeyFromPassword function uses a hardcoded, static salt ("dead-man-switch-salt") for all users and messages. In the event of a database compromise, this allows an attacker to + perform mass rainbow table or dictionary attacks to crack passwords for all users simultaneously. A unique salt per encryption operation is required to force attackers to crack each + password individually. + - Recommendation: + 1. Generate a random, cryptographically secure salt (e.g., 16 bytes) for each new message or file encryption operation. + 2. Store this salt alongside the ciphertext (e.g., as a prefix to the encrypted string or in a separate database column). + 3. Update deriveKeyFromPassword to accept salt as a parameter. + 4. Update the encrypt and decrypt flows to pass this unique salt during key derivation. + ## Tamamlanan - ✅ Teknoloji stack seçimi - ✅ Memory bank oluşturma @@ -61,6 +80,7 @@ - ✅ Faz 1: Auth sistemi - ✅ Faz 2: Mesaj CRUD - ✅ Faz 3: Dosya Yükleme (R2) +- ✅ Faz 4: Client-side Encryption ## Bilinen Sorunlar (henüz yok) diff --git a/prisma/migrations/20251224123000_add_encryption/migration.sql b/prisma/migrations/20251224123000_add_encryption/migration.sql new file mode 100644 index 0000000..a76319c --- /dev/null +++ b/prisma/migrations/20251224123000_add_encryption/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Message" ADD COLUMN "isEncrypted" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4a362b8..c51f057 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -50,6 +50,7 @@ model Message { title String content String? @db.Text status String @default("DRAFT") + isEncrypted Boolean @default(false) lastPing DateTime @default(now()) checkInterval Int @default(24) user User @relation(fields: [userId], references: [id], onDelete: Cascade) diff --git a/src/app/api/messages/[messageId]/attachments/route.ts b/src/app/api/messages/[messageId]/attachments/route.ts new file mode 100644 index 0000000..6d462dd --- /dev/null +++ b/src/app/api/messages/[messageId]/attachments/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth/options"; +import { prisma } from "@/lib/prisma"; + +export async function GET( + req: Request, + { params }: { params: Promise<{ messageId: string }> } +) { + const { messageId } = await params; + const session = await getServerSession(authOptions); + + if (!session?.user?.id) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const message = await prisma.message.findUnique({ + where: { id: messageId, userId: session.user.id }, + include: { attachments: true }, + }); + + if (!message) { + return new NextResponse("Not found", { status: 404 }); + } + + return NextResponse.json( + message.attachments.map((a) => ({ id: a.id, fileName: a.fileName })) + ); +} diff --git a/src/app/api/messages/[messageId]/route.ts b/src/app/api/messages/[messageId]/route.ts index e824650..a2347aa 100644 --- a/src/app/api/messages/[messageId]/route.ts +++ b/src/app/api/messages/[messageId]/route.ts @@ -16,7 +16,7 @@ export async function PATCH( try { const body = await req.json(); - const { title, content, recipients, checkInterval } = body; + const { title, content, recipients, checkInterval, isEncrypted } = body; const existingMessage = await prisma.message.findUnique({ where: { id: messageId, userId: session.user.id }, @@ -40,6 +40,7 @@ export async function PATCH( title, content, checkInterval, + isEncrypted: isEncrypted || existingMessage.isEncrypted, recipients: { create: recipients.map((email: string) => ({ email })), }, diff --git a/src/app/api/messages/route.ts b/src/app/api/messages/route.ts index b5e8fd8..f90896d 100644 --- a/src/app/api/messages/route.ts +++ b/src/app/api/messages/route.ts @@ -12,7 +12,7 @@ export async function POST(req: Request) { try { const body = await req.json(); - const { title, content, recipients, checkInterval } = body; + const { title, content, recipients, checkInterval, isEncrypted } = body; const message = await prisma.message.create({ data: { @@ -20,6 +20,7 @@ export async function POST(req: Request) { title, content, checkInterval, + isEncrypted: isEncrypted || false, recipients: { create: recipients.map((email: string) => ({ email })), }, diff --git a/src/app/dashboard/messages/[messageId]/page.tsx b/src/app/dashboard/messages/[messageId]/page.tsx index 0be2715..e987a00 100644 --- a/src/app/dashboard/messages/[messageId]/page.tsx +++ b/src/app/dashboard/messages/[messageId]/page.tsx @@ -38,6 +38,7 @@ export default async function EditMessagePage({ checkInterval: message.checkInterval, recipients: message.recipients.map((r) => r.email), attachments: message.attachments.map((a) => ({ id: a.id, fileName: a.fileName })), + isEncrypted: message.isEncrypted, }; return ( diff --git a/src/app/dashboard/messages/new/page.tsx b/src/app/dashboard/messages/new/page.tsx index 39bf59e..b2ddbe7 100644 --- a/src/app/dashboard/messages/new/page.tsx +++ b/src/app/dashboard/messages/new/page.tsx @@ -2,9 +2,10 @@ import MessageForm from "@/components/messages/message-form"; export default function NewMessagePage() { return ( -
-

Yeni Ölü Adam Anahtarı Oluştur

- +
+
+ +
- ); + ); } diff --git a/src/app/decrypt/page.tsx b/src/app/decrypt/page.tsx new file mode 100644 index 0000000..2e4a9d7 --- /dev/null +++ b/src/app/decrypt/page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Lock, Unlock } from "lucide-react"; +import { deriveKeyFromPassword, decrypt, decryptFile } from "@/lib/crypto"; + +export default function DecryptPage() { + const [encryptedContent, setEncryptedContent] = useState(""); + const [password, setPassword] = useState(""); + const [decryptedContent, setDecryptedContent] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleDecrypt = async () => { + setLoading(true); + setError(null); + try { + const key = await deriveKeyFromPassword(password); + const decrypted = await decrypt(encryptedContent, key); + setDecryptedContent(decrypted); + } catch { + setError("Şifre çözme başarısız. Şifre veya içerik hatalı olabilir."); + } finally { + setLoading(false); + } + }; + + const handleFileDecrypt = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file || !password) return; + + setLoading(true); + setError(null); + try { + const key = await deriveKeyFromPassword(password); + const decryptedData = await decryptFile(file, key); + + let originalName = file.name; + if (file.name.endsWith(".enc")) { + const encryptedName = file.name.slice(0, -4); + try { + originalName = await decrypt(encryptedName, key); + } catch { + originalName = "decrypted_file"; + } + } else { + originalName = file.name.replace(".encrypted", ""); + } + + const blob = new Blob([decryptedData]); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = originalName; + a.click(); + URL.revokeObjectURL(url); + } catch { + setError("Dosya şifre çözme başarısız."); + } finally { + setLoading(false); + e.target.value = ""; + } + }; + + return ( +
+ + + + Şifreli Mesaj Çöz + + + +
+ + setPassword(e.target.value)} + placeholder="Şifreyi girin" + type="password" + /> +
+ +
+ +