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:
nmemmertandClaude Sonnet 4.6 committed 2026-08-28 10:10:40 -04:00
1 parent 937bf8ad2d
commit 46f0a4b71e
10 files changed
+308 -185

No files matched your search

+11 -8
View File
@@ -8,12 +8,14 @@ import {
deriveKeyFromPassword,
generateMasterKey,
encryptMasterKey,
generateX25519Keypair,
encryptPrivateKey,
} from '../crypto';
import { socket } from '../lib/ws';
export default function RegisterPage() {
const navigate = useNavigate();
const { setAuth, setMasterKey, setUsername } = useAuthStore();
const { setAuth, setMasterKey, setKeyPair, setUsername } = useAuthStore();
const [username, setUsernameField] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -29,15 +31,15 @@ export default function RegisterPage() {
setLoading(true);
try {
// Generate all crypto material client-side
// All crypto material generated client-side — nothing sensitive leaves the browser in plaintext
const kdfSalt = await generateKdfSalt();
const derivedKey = await deriveKeyFromPassword(password, kdfSalt);
const masterKey = await generateMasterKey();
const encryptedMasterKey = await encryptMasterKey(masterKey, derivedKey);
// Minimal public key placeholder (full ECDH exchange would use X25519 via libsodium)
const publicKey = 'placeholder-pubkey';
const keySignature = 'placeholder-sig';
// X25519 keypair for ECDH conversation key exchange
const { publicKeyB64, privateKey, pkcs8Bytes } = await generateX25519Keypair();
const encryptedPrivKey = await encryptPrivateKey(pkcs8Bytes, masterKey);
const res = await api.post('/auth/register', {
username,
@@ -45,13 +47,14 @@ export default function RegisterPage() {
password,
kdfSalt,
encryptedMasterKey,
publicKey,
keySignature,
publicKey: publicKeyB64,
encryptedPrivateKey: encryptedPrivKey,
});
const { accessToken, refreshToken, userId } = res.data;
setAuth({ accessToken, refreshToken, userId });
setMasterKey(masterKey);
setKeyPair(publicKeyB64, privateKey);
setUsername(username);
socket.connect(accessToken);
@@ -86,7 +89,7 @@ export default function RegisterPage() {
</p>
<button type="submit" disabled={loading} className="btn-primary w-full">
{loading ? 'Creating account…' : 'Create account'}
{loading ? 'Generating keys…' : 'Create account'}
</button>
</form>