520 lines
12 KiB
TypeScript
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;
|
|
}
|
|
}
|