Add encrypted key support for chat and nodes

Introduces encrypted private key storage for nodes and users, updates chat message schema to support sender-side encryption, and adds supporting libraries and tests for cryptographic signing and identity unlock flows. Includes new database migrations, API route updates, and React components for identity unlock prompts.
This commit is contained in:
Christopher
2026-01-27 17:13:28 -08:00
parent 25f71e320b
commit 5903022f8a
46 changed files with 7457 additions and 113 deletions
+128
View File
@@ -0,0 +1,128 @@
/**
* User Identity Hook
*
* Manages the user's cryptographic identity using in-memory storage.
* strict: NO localStorage for decrypted keys.
*/
import { useState, useEffect } from 'react';
import { decryptPrivateKey } from '@/lib/crypto/private-key-client';
import {
keyStore,
importPrivateKey,
generateKeyPair,
exportPrivateKey,
exportPublicKey,
base64UrlToBase64
} from '@/lib/crypto/user-signing';
export interface UserIdentity {
did: string;
handle: string;
publicKey: string;
isUnlocked: boolean;
}
export function useUserIdentity() {
const [identity, setIdentity] = useState<UserIdentity | null>(null);
const [isUnlocked, setIsUnlocked] = useState(false);
// Check status on mount / updates
useEffect(() => {
const hasKey = !!keyStore.getPrivateKey();
setIsUnlocked(hasKey);
}, []);
/**
* Initialize identity from user data & password
*/
const initializeIdentity = async (userData: {
did: string;
handle: string;
publicKey: string;
privateKeyEncrypted: string;
}, password?: string) => {
// If password provided, attempt unlock
if (password) {
await unlockIdentity(userData.privateKeyEncrypted, password);
} else {
// Just set public identity info if locked
setIdentity({
did: userData.did,
handle: userData.handle,
publicKey: userData.publicKey,
isUnlocked: !!keyStore.getPrivateKey()
});
}
};
/**
* Unlock the identity with a password
*/
const unlockIdentity = async (privateKeyEncrypted: string, password: string) => {
try {
// 1. Decrypt the PEM/String from server (which is actually a base64 encoded PKCS8 export usually?)
// Wait, existing implementation returns a string.
// We need to verify what `decryptPrivateKey` returns.
// Assuming it returns the decrypted string (Base64 of PKCS8)
const privateKeyPemOrBase64 = await decryptPrivateKey(privateKeyEncrypted, password);
// Clean up if it's PEM to get Base64
let privateKeyBase64 = privateKeyPemOrBase64;
if (privateKeyBase64.includes('-----BEGIN')) {
privateKeyBase64 = privateKeyBase64
.replace(/-----BEGIN [A-Z ]+-----/, '')
.replace(/-----END [A-Z ]+-----/, '')
.replace(/\s/g, '');
}
// 2. Import into CryptoKey
// We need ArrayBuffer
const binaryDer = Buffer.from(privateKeyBase64, 'base64');
const cryptoKey = await importPrivateKey(binaryDer); // This is P-256 specific now
// 3. Store in Memory
keyStore.setPrivateKey(cryptoKey);
// 4. Update State
setIdentity(prev => prev ? { ...prev, isUnlocked: true } : null); // We need the other data...
setIsUnlocked(true);
// If we didn't have identity wrapper set yet, we might need it.
// Usually initializeIdentity handles both.
} catch (error) {
console.error('[Identity] Failed to unlock identity:', error);
throw new Error('Failed to unlock identity. Incorrect password?');
}
};
/**
* Lock the identity
*/
const lockIdentity = () => {
keyStore.clear();
setIsUnlocked(false);
setIdentity(prev => prev ? { ...prev, isUnlocked: false } : null);
};
/**
* Clear the identity (logout)
*/
const clearIdentity = () => {
keyStore.clear();
setIdentity(null);
setIsUnlocked(false);
};
return {
identity,
isUnlocked,
initializeIdentity,
unlockIdentity,
lockIdentity,
clearIdentity,
};
}