OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / vendor / hyperident / lib / crypto.js

157 lines · javascript ref:
/**
 * 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'
    };
  }
}