OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-client / capabilities.js

95 lines · javascript ref:
import b4a from 'b4a'
import sodium from './crypto.js'
import { asBytes, randomBytes, wipe } from './bytes.js'
import { fail } from './errors.js'

const PUBLIC_KEY_BYTES = 32
const PRIVATE_SEED_BYTES = 32
const SECRET_KEY_BYTES = 64
const SIGNATURE_BYTES = 64

export function publicKeyFromPrivateSeed (privateSeed) {
  privateSeed = asBytes(privateSeed, 'private seed', PRIVATE_SEED_BYTES)
  const publicKey = b4a.alloc(PUBLIC_KEY_BYTES)
  const secretKey = b4a.alloc(SECRET_KEY_BYTES)
  try {
    sodium.crypto_sign_seed_keypair(publicKey, secretKey, privateSeed)
    return publicKey
  } finally {
    wipe(secretKey)
  }
}

export function generateCapabilityKeyPair (runtime) {
  const privateSeed = randomBytes(runtime, PRIVATE_SEED_BYTES, 'capability private seed')
  const publicKey = publicKeyFromPrivateSeed(privateSeed)
  return { publicKey, privateSeed }
}

export function signCapability (privateSeed, message) {
  privateSeed = asBytes(privateSeed, 'private seed', PRIVATE_SEED_BYTES)
  message = asBytes(message, 'signature message')
  const publicKey = b4a.alloc(PUBLIC_KEY_BYTES)
  const secretKey = b4a.alloc(SECRET_KEY_BYTES)
  const signature = b4a.alloc(SIGNATURE_BYTES)
  try {
    sodium.crypto_sign_seed_keypair(publicKey, secretKey, privateSeed)
    sodium.crypto_sign_detached(signature, message, secretKey)
    return signature
  } finally {
    wipe(secretKey)
  }
}

export function verifyCapabilitySignature (publicKey, message, signature) {
  publicKey = asBytes(publicKey, 'public key', PUBLIC_KEY_BYTES)
  message = asBytes(message, 'signature message')
  signature = asBytes(signature, 'signature', SIGNATURE_BYTES)
  return sodium.crypto_sign_verify_detached(signature, message, publicKey)
}

export function generateDistinctCapabilityKeys (runtime, names, forbiddenPublicKeys = []) {
  if (!Array.isArray(names) || names.length === 0 || new Set(names).size !== names.length) {
    fail('BAD_CLIENT_INPUT', 'capability names must be a nonempty distinct array')
  }
  const result = {}
  const publicKeys = forbiddenPublicKeys.map((publicKey, index) => asBytes(publicKey, `forbiddenPublicKeys[${index}]`, 32))
  try {
    for (const name of names) {
      if (typeof name !== 'string' || name.length === 0) fail('BAD_CLIENT_INPUT', 'capability name must be nonempty')
      let pair
      for (let attempt = 0; attempt < 8; attempt++) {
        pair = generateCapabilityKeyPair(runtime)
        if (!publicKeys.some(publicKey => b4a.equals(publicKey, pair.publicKey))) break
        wipe(pair.privateSeed)
        pair = null
      }
      if (!pair) fail('RNG_COLLISION', 'could not generate distinct capability keys')
      publicKeys.push(pair.publicKey)
      result[name] = pair
    }
  } catch (error) {
    for (const value of Object.values(result)) wipe(value.privateSeed)
    throw error
  }
  return result
}

export function destroyCellWriteCapability (writeCap) {
  if (!writeCap || typeof writeCap !== 'object') return
  wipe(writeCap.createPrivateKey)
  wipe(writeCap.renewPrivateKey)
  wipe(writeCap.dropPrivateKey)
}

export function destroyCellReadCapability (readCap) {
  if (!readCap || typeof readCap !== 'object') return
  wipe(readCap.cellKey)
}

export function destroyCellCapabilities (writeCap) {
  if (!writeCap || typeof writeCap !== 'object') return
  destroyCellWriteCapability(writeCap)
  destroyCellReadCapability(writeCap.readCap)
}