rspace-online/src/encryptid/key-derivation.ts

520 lines
12 KiB
TypeScript

/**
* EncryptID Key Derivation Module
*
* Derives application-specific cryptographic keys from WebAuthn PRF output
* or passphrase fallback. This is Layer 2 of the EncryptID architecture.
*/
import { bufferToBase64url, base64urlToBuffer } from './webauthn';
// ============================================================================
// TYPES
// ============================================================================
export interface DerivedKeys {
/** AES-256-GCM key for file/data encryption */
encryptionKey: CryptoKey;
/** ECDSA P-256 key pair for signing */
signingKeyPair: CryptoKeyPair;
/** Ed25519 seed for DID key (raw bytes) */
didSeed: Uint8Array;
/** The DID identifier (did:key:z6Mk...) */
did: string;
/** Whether keys were derived from PRF (true) or passphrase (false) */
fromPRF: boolean;
}
export interface EncryptedData {
ciphertext: ArrayBuffer;
iv: Uint8Array;
tag?: ArrayBuffer; // For AEAD modes, tag is included in ciphertext
}
export interface SignedData {
data: ArrayBuffer;
signature: ArrayBuffer;
publicKey: ArrayBuffer;
}
// ============================================================================
// KEY DERIVATION
// ============================================================================
/**
* EncryptID Key Manager
*
* Handles derivation and management of all cryptographic keys
* from a master secret (PRF output or passphrase-derived).
*/
export class EncryptIDKeyManager {
private masterKey: CryptoKey | null = null;
private derivedKeys: DerivedKeys | null = null;
private fromPRF: boolean = false;
/**
* Initialize from WebAuthn PRF output
*
* This is the preferred path - keys are derived directly from
* the hardware-backed PRF extension output.
*/
async initFromPRF(prfOutput: ArrayBuffer): Promise<void> {
// Import PRF output as HKDF key material
this.masterKey = await crypto.subtle.importKey(
'raw',
prfOutput,
{ name: 'HKDF' },
false, // Not extractable
['deriveKey', 'deriveBits']
);
this.fromPRF = true;
this.derivedKeys = null; // Clear any existing derived keys
console.log('EncryptID: Key manager initialized from PRF output');
}
/**
* Initialize from passphrase (fallback for non-PRF authenticators)
*
* Uses PBKDF2 with high iteration count to derive master key
* from user's passphrase. The salt should be stored securely.
*/
async initFromPassphrase(
passphrase: string,
salt: Uint8Array
): Promise<void> {
const encoder = new TextEncoder();
// Import passphrase as PBKDF2 key
const passphraseKey = await crypto.subtle.importKey(
'raw',
encoder.encode(passphrase),
{ name: 'PBKDF2' },
false,
['deriveBits']
);
// Derive master key material using PBKDF2
// OWASP 2023 recommends 600,000 iterations for PBKDF2-SHA256
const masterKeyMaterial = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: salt,
iterations: 600000,
hash: 'SHA-256',
},
passphraseKey,
256 // 32 bytes
);
// Import as HKDF key for further derivation
this.masterKey = await crypto.subtle.importKey(
'raw',
masterKeyMaterial,
{ name: 'HKDF' },
false,
['deriveKey', 'deriveBits']
);
this.fromPRF = false;
this.derivedKeys = null;
console.log('EncryptID: Key manager initialized from passphrase');
}
/**
* Generate a random salt for passphrase-based derivation
*/
static generateSalt(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(32));
}
/**
* Check if the key manager is initialized
*/
isInitialized(): boolean {
return this.masterKey !== null;
}
/**
* Get or derive all application keys
*/
async getKeys(): Promise<DerivedKeys> {
if (!this.masterKey) {
throw new Error('Key manager not initialized');
}
// Return cached keys if available
if (this.derivedKeys) {
return this.derivedKeys;
}
// Derive all keys
const [encryptionKey, signingKeyPair, didSeed] = await Promise.all([
this.deriveEncryptionKey(),
this.deriveSigningKeyPair(),
this.deriveDIDSeed(),
]);
// Generate DID from seed
const did = await this.generateDID(didSeed);
this.derivedKeys = {
encryptionKey,
signingKeyPair,
didSeed,
did,
fromPRF: this.fromPRF,
};
return this.derivedKeys;
}
/**
* Derive AES-256-GCM encryption key
*/
private async deriveEncryptionKey(): Promise<CryptoKey> {
if (!this.masterKey) {
throw new Error('Key manager not initialized');
}
const encoder = new TextEncoder();
return crypto.subtle.deriveKey(
{
name: 'HKDF',
hash: 'SHA-256',
salt: encoder.encode('encryptid-encryption-key-v1'),
info: encoder.encode('AES-256-GCM'),
},
this.masterKey,
{
name: 'AES-GCM',
length: 256,
},
false, // Not extractable
['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']
);
}
/**
* Derive ECDSA P-256 signing key pair
*
* Note: WebCrypto doesn't support deterministic key generation,
* so we derive a seed and use it to generate the key pair.
* For production, consider using a library like @noble/curves.
*/
private async deriveSigningKeyPair(): Promise<CryptoKeyPair> {
if (!this.masterKey) {
throw new Error('Key manager not initialized');
}
const encoder = new TextEncoder();
// Derive seed for signing key
const seed = await crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-256',
salt: encoder.encode('encryptid-signing-key-v1'),
info: encoder.encode('ECDSA-P256-seed'),
},
this.masterKey,
256
);
// For now, generate a non-deterministic key pair
// TODO: Use @noble/curves for deterministic generation from seed
// This is a placeholder - in production, use the seed deterministically
const keyPair = await crypto.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256',
},
false, // Private key not extractable
['sign', 'verify']
);
// Store seed hash for verification
console.log('EncryptID: Signing key derived (seed hash):',
bufferToBase64url(await crypto.subtle.digest('SHA-256', seed)).slice(0, 16));
return keyPair;
}
/**
* Derive Ed25519 seed for DID key
*/
private async deriveDIDSeed(): Promise<Uint8Array> {
if (!this.masterKey) {
throw new Error('Key manager not initialized');
}
const encoder = new TextEncoder();
const seed = await crypto.subtle.deriveBits(
{
name: 'HKDF',
hash: 'SHA-256',
salt: encoder.encode('encryptid-did-key-v1'),
info: encoder.encode('Ed25519-seed'),
},
this.masterKey,
256
);
return new Uint8Array(seed);
}
/**
* Generate did:key identifier from Ed25519 seed
*
* Format: did:key:z6Mk... (multicodec ed25519-pub + base58btc)
*/
private async generateDID(seed: Uint8Array): Promise<string> {
// Ed25519 public key generation would go here
// For now, we'll create a placeholder using the seed hash
// TODO: Use @noble/ed25519 for proper Ed25519 key generation
const publicKeyHash = await crypto.subtle.digest('SHA-256', seed);
const publicKeyBytes = new Uint8Array(publicKeyHash).slice(0, 32);
// Multicodec prefix for Ed25519 public key: 0xed01
const multicodecPrefix = new Uint8Array([0xed, 0x01]);
const multicodecKey = new Uint8Array(34);
multicodecKey.set(multicodecPrefix);
multicodecKey.set(publicKeyBytes, 2);
// Base58btc encode (simplified - use a proper library in production)
const base58Encoded = bufferToBase64url(multicodecKey.buffer)
.replace(/-/g, '')
.replace(/_/g, '');
return `did:key:z${base58Encoded}`;
}
/**
* Clear all keys from memory
*/
clear(): void {
this.masterKey = null;
this.derivedKeys = null;
this.fromPRF = false;
console.log('EncryptID: Key manager cleared');
}
}
// ============================================================================
// ENCRYPTION UTILITIES
// ============================================================================
/**
* Encrypt data using the encryption key
*/
export async function encryptData(
key: CryptoKey,
data: ArrayBuffer | Uint8Array | string
): Promise<EncryptedData> {
// Convert string to ArrayBuffer if needed
let plaintext: ArrayBuffer;
if (typeof data === 'string') {
plaintext = new TextEncoder().encode(data).buffer;
} else if (data instanceof Uint8Array) {
plaintext = data.buffer;
} else {
plaintext = data;
}
// Generate random IV
const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt
const ciphertext = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
},
key,
plaintext
);
return { ciphertext, iv };
}
/**
* Decrypt data using the encryption key
*/
export async function decryptData(
key: CryptoKey,
encrypted: EncryptedData
): Promise<ArrayBuffer> {
return crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: encrypted.iv,
},
key,
encrypted.ciphertext
);
}
/**
* Decrypt data and return as string
*/
export async function decryptDataAsString(
key: CryptoKey,
encrypted: EncryptedData
): Promise<string> {
const plaintext = await decryptData(key, encrypted);
return new TextDecoder().decode(plaintext);
}
// ============================================================================
// SIGNING UTILITIES
// ============================================================================
/**
* Sign data using the signing key
*/
export async function signData(
keyPair: CryptoKeyPair,
data: ArrayBuffer | Uint8Array | string
): Promise<SignedData> {
// Convert string to ArrayBuffer if needed
let dataBuffer: ArrayBuffer;
if (typeof data === 'string') {
dataBuffer = new TextEncoder().encode(data).buffer;
} else if (data instanceof Uint8Array) {
dataBuffer = data.buffer;
} else {
dataBuffer = data;
}
// Sign
const signature = await crypto.subtle.sign(
{
name: 'ECDSA',
hash: 'SHA-256',
},
keyPair.privateKey,
dataBuffer
);
// Export public key for verification
const publicKey = await crypto.subtle.exportKey('raw', keyPair.publicKey);
return {
data: dataBuffer,
signature,
publicKey,
};
}
/**
* Verify a signature
*/
export async function verifySignature(
signed: SignedData
): Promise<boolean> {
// Import public key
const publicKey = await crypto.subtle.importKey(
'raw',
signed.publicKey,
{
name: 'ECDSA',
namedCurve: 'P-256',
},
false,
['verify']
);
// Verify
return crypto.subtle.verify(
{
name: 'ECDSA',
hash: 'SHA-256',
},
publicKey,
signed.signature,
signed.data
);
}
// ============================================================================
// KEY WRAPPING (FOR SHARING)
// ============================================================================
/**
* Wrap a key for sharing with another user
*
* Used in rfiles to share encrypted files - wrap the file key
* with the recipient's public key.
*/
export async function wrapKeyForRecipient(
keyToWrap: CryptoKey,
recipientPublicKey: CryptoKey
): Promise<ArrayBuffer> {
return crypto.subtle.wrapKey(
'raw',
keyToWrap,
recipientPublicKey,
{
name: 'RSA-OAEP',
}
);
}
/**
* Unwrap a key that was shared with us
*/
export async function unwrapSharedKey(
wrappedKey: ArrayBuffer,
privateKey: CryptoKey
): Promise<CryptoKey> {
return crypto.subtle.unwrapKey(
'raw',
wrappedKey,
privateKey,
{
name: 'RSA-OAEP',
},
{
name: 'AES-GCM',
length: 256,
},
false,
['encrypt', 'decrypt']
);
}
// ============================================================================
// SINGLETON INSTANCE
// ============================================================================
// Global key manager instance
let keyManagerInstance: EncryptIDKeyManager | null = null;
/**
* Get the global EncryptID key manager instance
*/
export function getKeyManager(): EncryptIDKeyManager {
if (!keyManagerInstance) {
keyManagerInstance = new EncryptIDKeyManager();
}
return keyManagerInstance;
}
/**
* Reset the global key manager (e.g., on logout)
*/
export function resetKeyManager(): void {
if (keyManagerInstance) {
keyManagerInstance.clear();
keyManagerInstance = null;
}
}