/**
* Crypto Manager - Cryptographic operations for Hyperident
* Uses bare-crypto for all cryptographic functions
*/
import crypto from 'node:crypto'
/**
* CryptoManager class for encryption, decryption, and key derivation
*/
export class CryptoManager {
/**
* Generate a cryptographically secure random salt
* @returns {Buffer} 32-byte random salt
*/
static generateSalt() {
return crypto.randomBytes(32)
}
/**
* Derive encryption key from passphrase and salt
* @param {string} passphrase - User passphrase
* @param {Buffer} salt - 32-byte salt
* @returns {Buffer} 32-byte derived key
*/
static deriveKey(passphrase, salt) {
if (typeof passphrase !== 'string') {
throw new Error('Passphrase must be a string')
}
if (!Buffer.isBuffer(salt) || salt.length !== 32) {
throw new Error('Salt must be a 32-byte Buffer')
}
// Implement SHA256-based key derivation: sha256(passphrase + salt)
const passphraseBuffer = Buffer.from(passphrase, 'utf8')
const combined = Buffer.concat([passphraseBuffer, salt])
return crypto.createHash('sha256').update(combined).digest()
}
/**
* Encrypt data using XOR with derived key
* @param {Buffer} data - Data to encrypt
* @param {Buffer} key - 32-byte encryption key
* @returns {Buffer} Encrypted data
*/
static encryptData(data, key) {
if (!Buffer.isBuffer(data)) {
throw new Error('Data must be a Buffer')
}
if (!Buffer.isBuffer(key) || key.length !== 32) {
throw new Error('Key must be a 32-byte Buffer')
}
const encrypted = Buffer.alloc(data.length)
for (let i = 0; i < data.length; i++) {
encrypted[i] = data[i] ^ key[i % key.length]
}
return encrypted
}
/**
* Decrypt data using XOR with derived key
* @param {Buffer} encryptedData - Data to decrypt
* @param {Buffer} key - 32-byte encryption key
* @returns {Buffer} Decrypted data
*/
static decryptData(encryptedData, key) {
// XOR decryption is the same as encryption for XOR cipher
return this.encryptData(encryptedData, key)
}
/**
* Hash data using SHA256
* @param {Buffer} data - Data to hash
* @returns {Buffer} 32-byte hash
*/
static hash(data) {
if (!Buffer.isBuffer(data)) {
throw new Error('Data must be a Buffer')
}
return crypto.createHash('sha256').update(data).digest()
}
/**
* Generate random bytes
* @param {number} length - Number of bytes to generate
* @returns {Buffer} Random bytes
*/
static randomBytes(length) {
if (typeof length !== 'number' || length < 0) {
throw new Error('Length must be a non-negative number')
}
return crypto.randomBytes(length)
}
/**
* Generate Ed25519 keypair for device
* @returns {Object} Object with publicKey and privateKey as Buffers
*/
static generateDeviceKeyPair() {
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519')
return {
publicKey: publicKey.export({ type: 'spki', format: 'der' }),
privateKey: privateKey.export({ type: 'pkcs8', format: 'der' })
}
}
/**
* Convert DER format keys to raw Ed25519 format (32 bytes)
* @param {Buffer} derKey - DER formatted key
* @param {string} type - 'public' or 'private'
* @returns {Buffer} Raw Ed25519 key (32 bytes)
*/
static derToRawEd25519(derKey, type) {
if (!Buffer.isBuffer(derKey)) {
throw new Error('DER key must be a Buffer')
}
if (type !== 'public' && type !== 'private') {
throw new Error('Type must be "public" or "private"')
}
if (type === 'public') {
// For Ed25519 public keys in SPKI format, the raw key is the last 32 bytes
return derKey.slice(-32)
} else {
// For Ed25519 private keys in PKCS8 format, the raw key is at a specific offset
// PKCS8 Ed25519 private key structure has the 32-byte key at offset -32
return derKey.slice(-32)
}
}
/**
* Generate raw Ed25519 keypair using sodium-universal (compatible with keet-identity-key)
* @returns {Object} Object with publicKey (32 bytes) and secretKey (64 bytes) as Buffers
*/
static async generateRawDeviceKeyPair() {
// Import sodium-universal dynamically to avoid issues if not installed
let sodium;
try {
const sodiumModule = await import('sodium-universal');
sodium = sodiumModule.default;
} catch (error) {
throw new Error('sodium-universal is required for device key generation. Run: npm install sodium-universal');
}
const publicKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES);
const secretKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES);
sodium.crypto_sign_keypair(publicKey, secretKey);
return {
publicKey,
secretKey // Note: keet-identity-key uses 'secretKey' not 'privateKey'
};
}
}