168 lines
5.7 KiB
TypeScript
168 lines
5.7 KiB
TypeScript
import { useState } from 'react';
|
|
import axios from 'axios';
|
|
|
|
import InputError from '@/components/input-error';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import {
|
|
base64ToBuffer,
|
|
decrypt,
|
|
exportKey,
|
|
getAESKeyFromPBKDF,
|
|
getKeyFromPassword,
|
|
importKey,
|
|
} from '@/lib/crypto';
|
|
import { getStoredKey, storeKey } from '@/lib/key-storage';
|
|
|
|
interface UnlockMessageDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onUnlock: (message: string) => void;
|
|
encryptedContent: string;
|
|
iv: string;
|
|
salt: string;
|
|
}
|
|
|
|
export default function UnlockMessageDialog({
|
|
open,
|
|
onOpenChange,
|
|
onUnlock,
|
|
encryptedContent,
|
|
iv,
|
|
salt,
|
|
}: UnlockMessageDialogProps) {
|
|
const [password, setPassword] = useState('');
|
|
const [storagePreference, setStoragePreference] = useState<
|
|
'session' | 'persistent'
|
|
>('session');
|
|
const [processing, setProcessing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function handleUnlock(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
setProcessing(true);
|
|
|
|
try {
|
|
const storedKey = getStoredKey();
|
|
|
|
let aesKey: CryptoKey;
|
|
|
|
if (storedKey) {
|
|
aesKey = await importKey(storedKey);
|
|
} else {
|
|
const saltBuffer = base64ToBuffer(salt);
|
|
const pbkdfKey = await getKeyFromPassword(password);
|
|
aesKey = await getAESKeyFromPBKDF(pbkdfKey, saltBuffer);
|
|
}
|
|
|
|
const decrypted = await decrypt(encryptedContent, aesKey, iv);
|
|
|
|
if (!storedKey && password) {
|
|
const exportedKey = await exportKey(aesKey);
|
|
storeKey(exportedKey, storagePreference === 'persistent');
|
|
}
|
|
|
|
onUnlock(decrypted);
|
|
onOpenChange(false);
|
|
} catch (err) {
|
|
console.error('Decryption error:', err);
|
|
setError(
|
|
'Failed to decrypt message. Please check your password and try again.'
|
|
);
|
|
} finally {
|
|
setProcessing(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Unlock Encrypted Message</DialogTitle>
|
|
<DialogDescription>
|
|
Enter your encryption password to decrypt and view your
|
|
message.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleUnlock}>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="unlock-password">
|
|
Encryption Password
|
|
</Label>
|
|
<Input
|
|
id="unlock-password"
|
|
type="password"
|
|
required
|
|
autoFocus
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Enter your encryption password"
|
|
disabled={processing}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="unlock-storage">
|
|
Storage Preference
|
|
</Label>
|
|
<Select
|
|
value={storagePreference}
|
|
onValueChange={(value) =>
|
|
setStoragePreference(
|
|
value as 'session' | 'persistent'
|
|
)
|
|
}
|
|
disabled={processing}
|
|
>
|
|
<SelectTrigger id="unlock-storage">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="session">
|
|
Session only
|
|
</SelectItem>
|
|
<SelectItem value="persistent">
|
|
Keep me logged in
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button type="submit" disabled={processing}>
|
|
{processing && <Spinner />}
|
|
Unlock
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|