feat: X25519 ECDH sealed-box E2E encryption, zero-knowledge key exchange
- Replace libsodium with pure Web Crypto X25519 ECDH for key exchange - Sealed-box pattern: ephemeral keypair + HKDF + AES-256-GCM for each recipient - Store encrypted conversation key per-user so each party decrypts with their own private key - PBKDF2-derived master key wraps X25519 private key (pkcs8) server-side for cross-device login - Fix health endpoint ordering so it never hits auth middleware (401 bug) - Fix postgres port exposure in docker-compose for backend connectivity - Fix DrivePage react-query v5 onSuccess → useEffect migration - Fix MessagesPage: use <Link> instead of <a href> to avoid page reload losing in-memory keys - Fix formatDistanceToNow crash on WebSocket messages lacking sent_at (add fallback + null guard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
937bf8ad2d
commit
46f0a4b71e
10 files changed
+308
-185
No files matched your search
@@ -3,12 +3,12 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Shield } from 'lucide-react';
|
||||
import api from '../lib/api';
|
||||
import { useAuthStore } from '../lib/store';
|
||||
import { deriveKeyFromPassword, decryptMasterKey } from '../crypto';
|
||||
import { deriveKeyFromPassword, decryptMasterKey, decryptPrivateKeyToCryptoKey } from '../crypto';
|
||||
import { socket } from '../lib/ws';
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const { setAuth, setMasterKey, setUsername } = useAuthStore();
|
||||
const { setAuth, setMasterKey, setKeyPair, setUsername } = useAuthStore();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [totpCode, setTotpCode] = useState('');
|
||||
@@ -34,17 +34,22 @@ export default function LoginPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, userId, kdfSalt, encryptedMasterKey, username } =
|
||||
res.data;
|
||||
const { accessToken, refreshToken, userId, username, kdfSalt, encryptedMasterKey } = res.data;
|
||||
|
||||
// Derive key from password — never leaves the browser
|
||||
// Re-derive master key from password — never leaves the browser
|
||||
const derived = await deriveKeyFromPassword(password, kdfSalt);
|
||||
const masterKey = await decryptMasterKey(encryptedMasterKey, derived);
|
||||
|
||||
// Set tokens first so the next API call has a Bearer token
|
||||
setAuth({ accessToken, refreshToken, userId });
|
||||
setMasterKey(masterKey);
|
||||
setUsername(username);
|
||||
|
||||
// Fetch and decrypt the X25519 private key (stored encrypted on server for cross-device support)
|
||||
const keyRes = await api.get('/auth/my-key');
|
||||
const privateKey = await decryptPrivateKeyToCryptoKey(keyRes.data.encrypted_private_key, masterKey);
|
||||
setKeyPair(keyRes.data.public_key, privateKey);
|
||||
|
||||
socket.connect(accessToken);
|
||||
navigate('/messages');
|
||||
} catch (err: any) {
|
||||
@@ -89,36 +94,22 @@ export default function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-primary w-full"
|
||||
>
|
||||
<button type="submit" disabled={loading} className="btn-primary w-full">
|
||||
{loading ? 'Signing in…' : require2fa ? 'Verify' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center text-sm text-slate-400 mt-4">
|
||||
No account?{' '}
|
||||
<Link to="/register" className="text-accent hover:underline">
|
||||
Register
|
||||
</Link>
|
||||
<Link to="/register" className="text-accent hover:underline">Register</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
type: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
function Field({ label, type, value, onChange }: {
|
||||
label: string; type: string; value: string; onChange: (v: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
Reference in new issue
Block a user