OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / core / platform / crypto.js

175 lines · javascript ref:
/**
 * Platform Crypto API
 * ====================
 * Encryption, decryption, and hashing primitives for HiveRelay apps.
 *
 * Uses sodium-universal (libsodium) for all operations:
 * - Symmetric encryption: XChaCha20-Poly1305 (AEAD)
 * - Hashing: BLAKE2b (32-byte output)
 * - Random: OS CSPRNG via sodium
 *
 * All functions accept and return Buffers (b4a).
 */

import sodium from 'sodium-universal'
import b4a from 'b4a'

// XChaCha20-Poly1305 constants
const NONCE_BYTES = sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES // 24
const TAG_BYTES = sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES // 16
const KEY_BYTES = sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES // 32

/**
 * Encrypt plaintext with a 32-byte key using XChaCha20-Poly1305.
 *
 * Returns: nonce (24 bytes) || ciphertext+tag
 * The nonce is randomly generated and prepended to the output.
 *
 * @param {Buffer} plaintext - Data to encrypt
 * @param {Buffer} key - 32-byte encryption key
 * @param {Buffer} [aad] - Optional additional authenticated data
 * @returns {Buffer} nonce || ciphertext (authenticated)
 */
export function encrypt (plaintext, key, aad) {
  if (!b4a.isBuffer(key) || key.length !== KEY_BYTES) {
    throw new Error(`Key must be ${KEY_BYTES} bytes`)
  }
  if (!b4a.isBuffer(plaintext)) {
    plaintext = b4a.from(plaintext)
  }

  const nonce = b4a.alloc(NONCE_BYTES)
  sodium.randombytes_buf(nonce)

  const ciphertext = b4a.alloc(plaintext.length + TAG_BYTES)

  sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
    ciphertext,
    plaintext,
    aad || null,
    null, // nsec (unused in this API)
    nonce,
    key
  )

  // Prepend nonce so decrypt can extract it
  return b4a.concat([nonce, ciphertext])
}

/**
 * Decrypt ciphertext produced by encrypt().
 *
 * @param {Buffer} sealed - nonce (24 bytes) || ciphertext+tag
 * @param {Buffer} key - 32-byte encryption key
 * @param {Buffer} [aad] - Optional additional authenticated data (must match encrypt)
 * @returns {Buffer} plaintext
 * @throws {Error} if decryption fails (wrong key, tampered data)
 */
export function decrypt (sealed, key, aad) {
  if (!b4a.isBuffer(key) || key.length !== KEY_BYTES) {
    throw new Error(`Key must be ${KEY_BYTES} bytes`)
  }
  if (!b4a.isBuffer(sealed) || sealed.length < NONCE_BYTES + TAG_BYTES) {
    throw new Error('Sealed data too short')
  }

  const nonce = sealed.subarray(0, NONCE_BYTES)
  const ciphertext = sealed.subarray(NONCE_BYTES)

  const plaintext = b4a.alloc(ciphertext.length - TAG_BYTES)

  try {
    sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
      plaintext,
      null, // nsec
      ciphertext,
      aad || null,
      nonce,
      key
    )
  } catch (err) {
    throw new Error('Decryption failed — wrong key or tampered data')
  }

  return plaintext
}

/**
 * BLAKE2b hash (32 bytes by default).
 *
 * @param {Buffer|string} data - Input data
 * @param {number} [outputLen=32] - Output length in bytes (16-64)
 * @returns {Buffer} hash
 */
export function hash (data, outputLen = 32) {
  if (!b4a.isBuffer(data)) data = b4a.from(data)
  const output = b4a.alloc(outputLen)
  sodium.crypto_generichash(output, data)
  return output
}

/**
 * Keyed BLAKE2b hash (MAC).
 *
 * @param {Buffer|string} data - Input data
 * @param {Buffer} key - 16-64 byte key
 * @param {number} [outputLen=32] - Output length in bytes
 * @returns {Buffer} MAC
 */
export function hashKeyed (data, key, outputLen = 32) {
  if (!b4a.isBuffer(data)) data = b4a.from(data)
  const output = b4a.alloc(outputLen)
  sodium.crypto_generichash(output, data, key)
  return output
}

/**
 * Generate cryptographically secure random bytes.
 *
 * @param {number} n - Number of bytes
 * @returns {Buffer}
 */
export function randomBytes (n) {
  const buf = b4a.alloc(n)
  sodium.randombytes_buf(buf)
  return buf
}

/**
 * Generate a random 32-byte encryption key.
 * @returns {Buffer}
 */
export function generateKey () {
  return randomBytes(KEY_BYTES)
}

/**
 * Constant-time buffer comparison.
 *
 * @param {Buffer} a
 * @param {Buffer} b
 * @returns {boolean}
 */
export function equal (a, b) {
  if (a.length !== b.length) return false
  return sodium.sodium_memcmp(a, b)
}

// Export constants for consumers
export const CONSTANTS = {
  KEY_BYTES,
  NONCE_BYTES,
  TAG_BYTES
}

export default {
  encrypt,
  decrypt,
  hash,
  hashKeyed,
  randomBytes,
  generateKey,
  equal,
  CONSTANTS
}