OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-client / browser-artifact-v2.js

114 lines · javascript ref:
import b4a from 'b4a'
import { blake2b256 } from '@hiverelay/blind-protocol/hashes'

const MAGIC = b4a.from('HRBCBV02', 'ascii')
const MANIFEST_BYTES = 214

function fail (message) {
  const error = new Error(message)
  error.code = 'BAD_BROWSER_ARTIFACT_V2'
  throw error
}

function bytes32 (value, field) {
  if (!value || typeof value.byteLength !== 'number') fail(`${field} must be bytes`)
  value = b4a.from(value)
  if (value.byteLength !== 32) fail(`${field} must be exactly 32 bytes`)
  return value
}

function writeU32 (output, value, offset, field) {
  if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) fail(`${field} is outside u32`)
  output[offset] = value >>> 24
  output[offset + 1] = value >>> 16
  output[offset + 2] = value >>> 8
  output[offset + 3] = value
}

function readU32 (input, offset) {
  return input[offset] * 0x1000000 + input[offset + 1] * 0x10000 + input[offset + 2] * 0x100 + input[offset + 3]
}

function length64 (length) {
  const output = b4a.alloc(8)
  let value = BigInt(length)
  for (let i = 7; i >= 0; i--) {
    output[i] = Number(value & 0xffn)
    value >>= 8n
  }
  return output
}

function domainLengthHash (domain, value) {
  value = b4a.from(value)
  return blake2b256(b4a.concat([b4a.from(domain, 'ascii'), length64(value.byteLength), value]))
}

export const hashBlindClientBrowserArtifactV2 = value => domainLengthHash('hiverelay.blind.client-browser-artifact-hash.v2', value)
export const hashBlindClientBrowserArtifactManifestV2 = value => domainLengthHash('hiverelay.blind.client-browser-manifest-hash.v2', value)
export const hashBlindClientBrowserSourceClosureV2 = value => domainLengthHash('hiverelay.blind.client-browser-source-closure-hash.v2', value)

export function encodeBlindClientBrowserArtifactManifestV2 (value) {
  if (!value || value.version !== 2) fail('browser artifact v2 manifest version is invalid')
  const output = b4a.alloc(MANIFEST_BYTES)
  b4a.copy(MAGIC, output, 0)
  output[8] = 0
  output[9] = 2
  let offset = 10
  for (const field of [
    'baseBrowserV1ArtifactHash',
    'baseBrowserV1ManifestHash',
    'wireV2AbiHash',
    'clientCompositionV2FormatHash',
    'artifactHash',
    'sourceClosureHash'
  ]) {
    b4a.copy(bytes32(value[field], field), output, offset)
    offset += 32
  }
  writeU32(output, value.exactRequestBytes, offset, 'exactRequestBytes'); offset += 4
  writeU32(output, value.exactResultBytes, offset, 'exactResultBytes'); offset += 4
  writeU32(output, value.forwardReadinessOperationBits, offset, 'forwardReadinessOperationBits'); offset += 4
  if (value.exactRequestBytes !== 65_536 || value.exactResultBytes !== 65_536 || value.forwardReadinessOperationBits !== 0) {
    fail('browser artifact v2 fixed sizes or readiness are invalid')
  }
  if (offset !== MANIFEST_BYTES) fail('browser artifact v2 manifest encoder length mismatch')
  return output
}

export function decodeBlindClientBrowserArtifactManifestV2 (input) {
  input = b4a.from(input)
  if (input.byteLength !== MANIFEST_BYTES || !b4a.equals(input.subarray(0, 8), MAGIC) || input[8] !== 0 || input[9] !== 2) {
    fail('browser artifact v2 manifest fixed header is invalid')
  }
  let offset = 10
  const value = { version: 2 }
  for (const field of [
    'baseBrowserV1ArtifactHash',
    'baseBrowserV1ManifestHash',
    'wireV2AbiHash',
    'clientCompositionV2FormatHash',
    'artifactHash',
    'sourceClosureHash'
  ]) {
    value[field] = b4a.from(input.subarray(offset, offset + 32))
    offset += 32
  }
  value.exactRequestBytes = readU32(input, offset); offset += 4
  value.exactResultBytes = readU32(input, offset); offset += 4
  value.forwardReadinessOperationBits = readU32(input, offset); offset += 4
  if (!b4a.equals(encodeBlindClientBrowserArtifactManifestV2(value), input)) fail('browser artifact v2 manifest is not canonical')
  return Object.freeze(value)
}

export function verifyBlindClientBrowserArtifactV2 (artifactBytes, manifestBytes, expected = {}) {
  const manifest = decodeBlindClientBrowserArtifactManifestV2(manifestBytes)
  if (!b4a.equals(hashBlindClientBrowserArtifactV2(artifactBytes), manifest.artifactHash)) fail('browser artifact v2 hash mismatch')
  for (const field of ['wireV2AbiHash', 'clientCompositionV2FormatHash', 'baseBrowserV1ArtifactHash', 'baseBrowserV1ManifestHash']) {
    if (expected[field] != null && !b4a.equals(bytes32(expected[field], `expected ${field}`), manifest[field])) {
      fail(`browser artifact v2 ${field} mismatch`)
    }
  }
  return manifest
}