206 lines
7.4 KiB
TypeScript
206 lines
7.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Head, router } from '@inertiajs/react';
|
|
import axios from 'axios';
|
|
|
|
import InputError from '@/components/input-error';
|
|
import { Button } from '@/components/ui/button';
|
|
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 AuthLayout from '@/layouts/auth-layout';
|
|
import {
|
|
bufferToBase64,
|
|
encrypt,
|
|
exportKey,
|
|
generateSalt,
|
|
getAESKeyFromPBKDF,
|
|
getKeyFromPassword,
|
|
} from '@/lib/crypto';
|
|
import { storeKey } from '@/lib/key-storage';
|
|
import { dashboard } from '@/routes';
|
|
|
|
export default function SetupEncryption() {
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [storagePreference, setStoragePreference] = useState<
|
|
'session' | 'persistent'
|
|
>('session');
|
|
const [processing, setProcessing] = useState(false);
|
|
const [cryptoAvailable, setCryptoAvailable] = useState(true);
|
|
const [errors, setErrors] = useState<{
|
|
password?: string;
|
|
confirmPassword?: string;
|
|
general?: string;
|
|
}>({});
|
|
|
|
useEffect(() => {
|
|
if (!window.crypto || !window.crypto.subtle) {
|
|
setCryptoAvailable(false);
|
|
setErrors({
|
|
general:
|
|
'Web Crypto API is not available. Please ensure you are accessing this page via HTTPS or localhost.',
|
|
});
|
|
}
|
|
}, []);
|
|
|
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
setErrors({});
|
|
|
|
if (password.length < 12) {
|
|
setErrors({
|
|
password: 'Encryption password must be at least 12 characters',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (password !== confirmPassword) {
|
|
setErrors({
|
|
confirmPassword: 'Passwords do not match',
|
|
});
|
|
return;
|
|
}
|
|
|
|
setProcessing(true);
|
|
|
|
try {
|
|
const salt = generateSalt();
|
|
|
|
const pbkdfKey = await getKeyFromPassword(password);
|
|
|
|
const aesKey = await getAESKeyFromPBKDF(pbkdfKey, salt);
|
|
|
|
const { encrypted, iv } = await encrypt('Hello, world', aesKey);
|
|
|
|
const exportedKey = await exportKey(aesKey);
|
|
|
|
await axios.post('/api/encryption/setup', {
|
|
salt: bufferToBase64(salt),
|
|
encrypted_content: encrypted,
|
|
iv: iv,
|
|
});
|
|
|
|
storeKey(exportedKey, storagePreference === 'persistent');
|
|
|
|
router.visit(dashboard().url);
|
|
} catch (error) {
|
|
console.error('Encryption setup error:', error);
|
|
setErrors({
|
|
general:
|
|
'Failed to setup encryption. Please try again or contact support.',
|
|
});
|
|
setProcessing(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AuthLayout
|
|
title="Setup Encryption"
|
|
description="Create a strong encryption password to secure your data"
|
|
>
|
|
<Head title="Setup Encryption" />
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
<div className="grid gap-6">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="password">Encryption Password</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
required
|
|
autoFocus
|
|
tabIndex={1}
|
|
autoComplete="new-password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Enter a strong encryption password"
|
|
disabled={processing}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Use a strong password (minimum 12 characters). This
|
|
password will encrypt your data.
|
|
</p>
|
|
<InputError
|
|
message={errors.password}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="confirmPassword">
|
|
Confirm Password
|
|
</Label>
|
|
<Input
|
|
id="confirmPassword"
|
|
type="password"
|
|
required
|
|
tabIndex={2}
|
|
autoComplete="new-password"
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
placeholder="Confirm your encryption password"
|
|
disabled={processing}
|
|
/>
|
|
<InputError message={errors.confirmPassword} />
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="storagePreference">
|
|
Storage Preference
|
|
</Label>
|
|
<Select
|
|
value={storagePreference}
|
|
onValueChange={(value) =>
|
|
setStoragePreference(
|
|
value as 'session' | 'persistent'
|
|
)
|
|
}
|
|
disabled={processing}
|
|
>
|
|
<SelectTrigger id="storagePreference" tabIndex={3}>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="session">
|
|
Session only (more secure)
|
|
</SelectItem>
|
|
<SelectItem value="persistent">
|
|
Keep me logged in (less secure)
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-xs text-muted-foreground">
|
|
Session storage clears when you close your browser.
|
|
Persistent storage keeps your key until manually
|
|
cleared.
|
|
</p>
|
|
</div>
|
|
|
|
{errors.general && (
|
|
<div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
|
{errors.general}
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
className="mt-2 w-full"
|
|
tabIndex={4}
|
|
disabled={processing || !cryptoAvailable}
|
|
>
|
|
{processing && <Spinner />}
|
|
Setup Encryption
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</AuthLayout>
|
|
);
|
|
}
|
|
|