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:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user