OverviewFilesCommitsIssuesDocsReleasesPeers

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

201 lines · javascript ref:
import b4a from 'b4a'
import { blake2b256 } from '@hiverelay/blind-protocol/hashes'
import {
  FORWARD_HTTPS_OUTSTANDING_STATE_V3,
  applyForwardHttpsResultV3,
  assertForwardHttpsPersistedSessionRecordV3,
  markForwardHttpsAwaitingDefinitiveTargetV3,
  prepareForwardHttpsOriginPersistenceV3
} from './browser-forward-state-v3.js'

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

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

function bytes (value, length, field, nonzero = false) {
  if (!value || typeof value.byteLength !== 'number') fail(`${field} must be bytes`)
  value = b4a.from(value)
  if (value.byteLength !== length) fail(`${field} must be exactly ${length} bytes`)
  if (nonzero) {
    let any = false
    for (const byte of value) any ||= byte !== 0
    if (!any) fail(`${field} must be nonzero`)
  }
  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 hashBlindClientBrowserArtifactV3 = value => domainLengthHash('hiverelay.blind.client-browser-artifact-hash.v3', value)
export const hashBlindClientBrowserArtifactManifestV3 = value => domainLengthHash('hiverelay.blind.client-browser-manifest-hash.v3', value)
export const hashBlindClientBrowserSourceClosureV3 = value => domainLengthHash('hiverelay.blind.client-browser-source-closure-hash.v3', value)

export function encodeBlindClientBrowserArtifactManifestV3 (value) {
  if (!value || value.version !== 3) fail('browser artifact v3 manifest version is invalid')
  const output = b4a.alloc(MANIFEST_BYTES)
  b4a.copy(MAGIC, output, 0)
  output[8] = 0
  output[9] = 3
  let offset = 10
  for (const field of [
    'baseBrowserV2ArtifactHash',
    'baseBrowserV2ManifestHash',
    'wireV3AbiHash',
    'clientCompositionV3FormatHash',
    'artifactHash',
    'sourceClosureHash'
  ]) {
    b4a.copy(bytes(value[field], 32, field, true), 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 v3 fixed sizes or readiness are invalid')
  }
  if (offset !== MANIFEST_BYTES) fail('browser artifact v3 manifest encoder length mismatch')
  return output
}

export function decodeBlindClientBrowserArtifactManifestV3 (input) {
  input = b4a.from(input)
  if (input.byteLength !== MANIFEST_BYTES || !b4a.equals(input.subarray(0, 8), MAGIC) || input[8] !== 0 || input[9] !== 3) {
    fail('browser artifact v3 manifest fixed header is invalid')
  }
  let offset = 10
  const value = { version: 3 }
  for (const field of [
    'baseBrowserV2ArtifactHash',
    'baseBrowserV2ManifestHash',
    'wireV3AbiHash',
    'clientCompositionV3FormatHash',
    '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(encodeBlindClientBrowserArtifactManifestV3(value), input)) fail('browser artifact v3 manifest is not canonical')
  return Object.freeze(value)
}

export function verifyBlindClientBrowserArtifactV3 (artifactBytes, manifestBytes, sourceClosureBytes, expected = {}) {
  const manifest = decodeBlindClientBrowserArtifactManifestV3(manifestBytes)
  if (!b4a.equals(hashBlindClientBrowserArtifactV3(artifactBytes), manifest.artifactHash)) fail('browser artifact v3 hash mismatch')
  if (!b4a.equals(hashBlindClientBrowserSourceClosureV3(sourceClosureBytes), manifest.sourceClosureHash)) fail('browser artifact v3 source closure hash mismatch')
  for (const field of [
    'wireV3AbiHash', 'clientCompositionV3FormatHash', 'baseBrowserV2ArtifactHash', 'baseBrowserV2ManifestHash'
  ]) {
    if (expected[field] != null && !b4a.equals(bytes(expected[field], 32, `expected ${field}`, true), manifest[field])) {
      fail(`browser artifact v3 ${field} mismatch`)
    }
  }
  return manifest
}

export class BlindClientBrowserCrashModelV3 {
  #record

  constructor () {
    this.#record = null
    this.fetchCount = 0
  }

  get outstanding () {
    if (!this.#record || this.#record.outstandingState === FORWARD_HTTPS_OUTSTANDING_STATE_V3.NONE) return null
    return Object.freeze({
      requestBytes: b4a.from(this.#record.outstandingOriginRequest),
      originRequestCommitment: b4a.from(this.#record.outstandingOriginRequestCommitment)
    })
  }

  get nextSequence () {
    return this.#record == null ? 0n : BigInt(this.#record.nextSequence)
  }

  get previousTargetResultHash () {
    return this.#record == null ? b4a.alloc(32) : b4a.from(this.#record.previousTargetResultHash)
  }

  get lastDefinitiveTargetResult () {
    return this.#record == null || this.#record.lastDefinitiveTargetResult.byteLength === 0
      ? null
      : b4a.from(this.#record.lastDefinitiveTargetResult)
  }

  get terminal () {
    return this.#record != null && this.#record.terminal === 1
  }

  get targetFin () {
    return this.#record != null && this.#record.targetFin
  }

  inspectRecord () {
    return this.#record == null ? null : assertForwardHttpsPersistedSessionRecordV3(this.#record)
  }

  persistBeforeFetch (value) {
    const prepared = prepareForwardHttpsOriginPersistenceV3(this.#record, value)
    this.#record = prepared.record
    if (prepared.disposition === 'CONFLICT_TERMINAL') {
      fail('browser changed an already-persisted outstanding request', 'TERMINAL_BROWSER_FORWARD_SESSION')
    }
    return b4a.from(this.#record.outstandingOriginRequest)
  }

  beginFetch () {
    if (!this.#record) fail('network I/O is forbidden before exact request persistence')
    this.#record = markForwardHttpsAwaitingDefinitiveTargetV3(this.#record)
    this.fetchCount++
    return b4a.from(this.#record.outstandingOriginRequest)
  }

  restartExactRetry () {
    if (!this.#record) fail('no persisted request exists for restart retry')
    this.#record = markForwardHttpsAwaitingDefinitiveTargetV3(this.#record)
    return b4a.from(this.#record.outstandingOriginRequest)
  }

  receiveResult (resultBytes) {
    if (!this.#record) fail('result has no persisted request')
    const applied = applyForwardHttpsResultV3(this.#record, resultBytes)
    this.#record = applied.record
    return applied.outcome
  }
}