OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-ipc / registry.js

410 lines · javascript ref:
import b4a from 'b4a'
import c from 'compact-encoding'
import {
  FAMILY,
  OUTER_CLASS,
  STREAM_WIRE_CLASS,
  TRANSPORT_ID,
  TRANSPORT_SUPPORT
} from '@hiverelay/blind-protocol/wire-runtime-authority'
import {
  hashImportedWireAbi,
  privateBlake2b256
} from './private-hashes.js'
import {
  LOCAL_ABORT_CODE,
  LOCAL_CIPHERTEXT_BYTES,
  LOCAL_CIPHERTEXT_PHASE,
  LOCAL_STREAM_ADJACENT_POLICY,
  LOCAL_STREAM_CONTEXT_KIND,
  LOCAL_STREAM_CONTROL_BYTES,
  LOCAL_STREAM_CONTROL_KIND,
  LOCAL_STREAM_DIRECTION,
  LOCAL_STREAM_FLAG,
  LOCAL_STREAM_FRAME_KIND,
  LOCAL_STREAM_MODE,
  LOCAL_STREAM_OPEN_COMBINATION,
  LOCAL_STREAM_OPEN_KIND,
  LOCAL_STREAM_OPEN_TABLE,
  PRIVATE_IPC_LIMITS,
  PRIVATE_IPC_TIMING_MILLIS
} from './policy.js'

const FORMAT_DOMAIN = b4a.from('hiverelay.blind.private-ipc-format-hash.v1', 'ascii')
const VECTOR_DOMAIN = b4a.from('hiverelay.blind.private-ipc-vector-set-hash.v1', 'ascii')
const MAX_PRIVATE_IPC_REGISTRY_BYTES = 1024 * 1024
const MAX_PRIVATE_IPC_LIST_ITEMS = 4096

function registryFailure (message) {
  const error = new Error(message)
  error.code = 'BAD_PRIVATE_IPC_REGISTRY'
  throw error
}

function asBuffer (value, field) {
  if (!value || typeof value.byteLength !== 'number') registryFailure(`${field} must be bytes`)
  if (b4a.isBuffer(value)) return b4a.from(value)
  if (ArrayBuffer.isView(value)) return b4a.from(value.buffer, value.byteOffset, value.byteLength)
  return b4a.from(value)
}

export const PRIVATE_IPC_SCHEMAS = Object.freeze([
  Object.freeze({
    schemaId: 1,
    schemaName: 'LocalDispatchV1',
    fields: Object.freeze([
      'totalLength:u32be', 'version:u8=1', 'family:wire-family-u8',
      'transportId:wire-transport-u8-or-ready-zero',
      'transportSupportBit:u16be[one-hot-wire-support-or-ready-zero]',
      'endpointId:u8[1..255]', 'outerClass:wire-outer-class-u8-or-ready-zero',
      'acceptedMonotonicMillis:u64be', 'absoluteDeadlineMonotonicMillis:u64be',
      'adjacentRelayKeyPresent:u8[0..1]', 'adjacentRelayKey:optional-fixed32[nonzero]',
      'bodyLength:u32be[class-exact-or-ready65]', 'externalCanonicalBytes:bytes[bodyLength]',
      'readyVariant:DESCRIBE|transport0|support0|outer0|no-adjacent|deadline=t0+2000'
    ])
  }),
  Object.freeze({
    schemaId: 2,
    schemaName: 'LocalUnaryResponseV1',
    fields: Object.freeze([
      'totalLength:u32be', 'version:u8=1', 'responseKind:u8[1..3]', 'localBrokerError:u8[0..6]',
      'bodyLength:u32be[class-exact-or-zero-or-ready120]', 'externalCanonicalBytes:bytes[bodyLength]'
    ])
  }),
  Object.freeze({
    schemaId: 3,
    schemaName: 'LocalStreamOpenV1',
    fields: Object.freeze([
      'totalLength:u32be', 'version:u8=1', 'openKind:u8[1..4]', 'transportId:wire-transport-u8',
      'transportSupportBit:u16be[one-hot-wire-support]', 'endpointId:u8[1..255]',
      'streamMode:u8[1..5]', 'channelClass:u8[closed-combination]',
      'acceptedMonotonicMillis:u64be', 'openDeadlineMonotonicMillis:u64be',
      'adjacentRelayKeyPresent:u8[0..1]', 'adjacentRelayKey:optional-fixed32[nonzero]',
      'contextLength:u32be[exact-by-open-combination]', 'context:closed-canonical-private-context',
      'combination1:open1|mode1|class1..3|context1:225|adjacent-optional',
      'combination2:open1|mode2|class1..3|context1:225|adjacent-optional',
      'combination3:open2|mode3|class1..3|context2:137|adjacent-required',
      'combination4:open3|mode4|class0|context2:137|adjacent-forbidden',
      'combination5:open4|mode5|class1..3|context2:137|adjacent-forbidden'
    ])
  }),
  Object.freeze({
    schemaId: 4,
    schemaName: 'LocalStreamFrameV1',
    fields: Object.freeze([
      'totalLength:u32be', 'version:u8=1', 'direction:u8[1..2]', 'frameKind:u8[1..5]',
      'sequence:u64be[first0-exact+1-per-physical-direction]', 'wireClass:u8[0..3]',
      'flags:u8[FIN-only]', 'bodyLength:u32be[kind/class/phase-bound]', 'bytes:bytes[bodyLength]',
      'CONTENT:class1..3|max=classBytes-23|zero-only-with-FIN',
      'CORE_RAW:class0|bytes0..65535|zero-only-with-FIN',
      'CIPHERTEXT:class0=flights32,96,64|class1..3=exact-class|FIN-forbidden',
      'CONTROL:class0|flags0|exact-LocalStreamControlV1',
      'ABORT:class0|flags0|one-registered-generic-code'
    ])
  }),
  Object.freeze({
    schemaId: 5,
    schemaName: 'LocalAuthenticatedChannelV1',
    fields: Object.freeze([
      'version:u8=1', 'edgeProcessNonce:fixed32[nonzero]', 'localChannelNonce:fixed32[nonzero]',
      'parentSessionId:fixed32[derived-nonzero]', 'transportProfileHash:fixed32[nonzero]',
      'finalNoiseHandshakeHash:fixed64[nonzero]', 'channelBindingMac:fixed32[keyed-blake2b]',
      'exporter-domain:hiverelay.blind.native-session-exporter.v1',
      'parent-domain:hiverelay.blind.private-parent-session.v1',
      'binding-domain:hiverelay.blind.private-channel-binding.v1',
      'authority:constant-time-verify-to-opaque-branded-handle'
    ])
  }),
  Object.freeze({
    schemaId: 6,
    schemaName: 'LocalStreamAttachContextV1',
    fields: Object.freeze([
      'version:u8=1', 'ticket:fixed32[nonzero-one-use]', 'parentSessionId:fixed32[nonzero]',
      'descriptorSequence:u64be[nonzero]', 'descriptorHash:fixed32[nonzero]', 'bindingHash:fixed32[nonzero]',
      'ticket:delete-before-compare|ttl<=2000ms|pending<=1024',
      'authority:successful-consume-to-opaque-branded-handle'
    ])
  }),
  Object.freeze({
    schemaId: 7,
    schemaName: 'LocalStreamControlV1',
    fields: Object.freeze([
      'version:u8=1', 'controlKind:u8[closed-1..7]', 'controlId:u64be[nonzero]',
      'CHANNEL_ACCEPT:bindingHash32|total42',
      'CHANNEL_REJECT:localBrokerError-u8|total11',
      'ATTACH_TICKET:ticket32|bindingHash32|total74',
      'EGRESS_DIAL:endpointBindingHash32|bindingTableHash32|transportProfileHash32|wireClass-u8|deadline-u64|maxOpen-u32|maxStream-u64|idle-u32|lifetime-u32|ticket32|total167',
      'EGRESS_RESULT:status-u8|endpointBindingHash32|adjacent-tag-and-optional32|ticket32|failure76-or-success108',
      'CORE_CHILD_OPEN:streamId-u64|ticket32|bindingHash32|total82',
      'NOISE_SESSION_OPEN:endpointBindingHash32|handshakeProfileHash32|prologueHash32|wireClass-u8|ticket32|total139',
      'no-freeform-payload-or-text'
    ])
  })
])

const LOCAL_BINDINGS = Object.freeze({
  responseKind: Object.freeze({ EXTERNAL_CANONICAL: 1, LOCAL_BROKER_ERROR: 2, LOCAL_READY_ACK: 3 }),
  controlKind: Object.freeze({ EDGE_READY: 1 }),
  localBrokerError: Object.freeze({
    MALFORMED_IPC: 1,
    UNAUTHORIZED_EDGE_PEER: 2,
    TOPOLOGY_PROFILE_ENDPOINT_MISMATCH: 3,
    CLASS_LENGTH_CAP: 4,
    DAEMON_DRAINING: 5,
    INTERNAL_IPC_FAILURE: 6
  }),
  streamOpenKind: LOCAL_STREAM_OPEN_KIND,
  streamMode: LOCAL_STREAM_MODE,
  streamContextKind: LOCAL_STREAM_CONTEXT_KIND,
  streamAdjacentPolicy: LOCAL_STREAM_ADJACENT_POLICY,
  streamOpenCombination: LOCAL_STREAM_OPEN_COMBINATION,
  streamDirection: LOCAL_STREAM_DIRECTION,
  streamFlag: LOCAL_STREAM_FLAG,
  streamFrameKind: LOCAL_STREAM_FRAME_KIND,
  streamControlKind: LOCAL_STREAM_CONTROL_KIND,
  ciphertextPhase: LOCAL_CIPHERTEXT_PHASE,
  ciphertextBytes: LOCAL_CIPHERTEXT_BYTES,
  streamControlBytes: LOCAL_STREAM_CONTROL_BYTES,
  localAbortCode: LOCAL_ABORT_CODE,
  readyRules: Object.freeze({ REQUIRED_DESCRIBE_OPERATION_BITS: 0x00000007, ENABLED_OPERATION_BITS_MASK: 0x003fffff }),
  limits: PRIVATE_IPC_LIMITS,
  timingMillis: PRIVATE_IPC_TIMING_MILLIS
})

function list (encoding) {
  return {
    preencode (state, values) {
      c.uint.preencode(state, values.length)
      for (const value of values) encoding.preencode(state, value)
    },
    encode (state, values) {
      c.uint.encode(state, values.length)
      for (const value of values) encoding.encode(state, value)
    },
    decode (state) {
      const count = c.uint.decode(state)
      if (!Number.isSafeInteger(count) || count < 0 || count > MAX_PRIVATE_IPC_LIST_ITEMS) {
        registryFailure('private IPC registry list count exceeds its bound')
      }
      const values = new Array(count)
      for (let index = 0; index < count; index++) values[index] = encoding.decode(state)
      return values
    }
  }
}

const stringList = list(c.string)
const schemaEncoding = {
  preencode (state, value) {
    c.uint.preencode(state, value.schemaId)
    c.string.preencode(state, value.schemaName)
    stringList.preencode(state, value.fields)
  },
  encode (state, value) {
    c.uint.encode(state, value.schemaId)
    c.string.encode(state, value.schemaName)
    stringList.encode(state, value.fields)
  },
  decode (state) {
    return {
      schemaId: c.uint.decode(state),
      schemaName: c.string.decode(state),
      fields: stringList.decode(state)
    }
  }
}
const bindingEntryEncoding = {
  preencode (state, value) {
    c.string.preencode(state, value.name)
    c.uint.preencode(state, value.id)
    c.uint.preencode(state, value.value)
  },
  encode (state, value) {
    c.string.encode(state, value.name)
    c.uint.encode(state, value.id)
    c.uint.encode(state, value.value)
  },
  decode (state) {
    return {
      name: c.string.decode(state),
      id: c.uint.decode(state),
      value: c.uint.decode(state)
    }
  }
}
const bindingTableEncoding = {
  preencode (state, value) {
    c.string.preencode(state, value.name)
    list(bindingEntryEncoding).preencode(state, value.entries)
  },
  encode (state, value) {
    c.string.encode(state, value.name)
    list(bindingEntryEncoding).encode(state, value.entries)
  },
  decode (state) {
    return {
      name: c.string.decode(state),
      entries: list(bindingEntryEncoding).decode(state)
    }
  }
}

const registryEncoding = {
  preencode (state, value) {
    c.string.preencode(state, value.magic)
    c.uint.preencode(state, value.formatVersion)
    c.fixed32.preencode(state, value.wireAbiHash)
    list(schemaEncoding).preencode(state, value.schemas)
    list(bindingTableEncoding).preencode(state, value.importedWireBindings)
    list(bindingTableEncoding).preencode(state, value.localBindings)
  },
  encode (state, value) {
    c.string.encode(state, value.magic)
    c.uint.encode(state, value.formatVersion)
    c.fixed32.encode(state, value.wireAbiHash)
    list(schemaEncoding).encode(state, value.schemas)
    list(bindingTableEncoding).encode(state, value.importedWireBindings)
    list(bindingTableEncoding).encode(state, value.localBindings)
  },
  decode (state) {
    return {
      magic: c.string.decode(state),
      formatVersion: c.uint.decode(state),
      wireAbiHash: b4a.from(c.fixed32.decode(state)),
      schemas: list(schemaEncoding).decode(state),
      importedWireBindings: list(bindingTableEncoding).decode(state),
      localBindings: list(bindingTableEncoding).decode(state)
    }
  }
}

function enumBindings (name, values) {
  return {
    name,
    entries: Object.entries(values)
      .map(([entryName, raw]) => ({ name: entryName, id: Number(raw), value: Number(raw) }))
      .sort((left, right) => left.id - right.id || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))
  }
}

function classBindings (name, values) {
  return {
    name,
    entries: Object.entries(values)
      .map(([id, value]) => ({ name: id, id: Number(id), value: Number(value) }))
      .sort((left, right) => left.id - right.id)
  }
}

function namedValueBindings (name, values) {
  return {
    name,
    entries: Object.entries(values)
      .filter(([, value]) => Number.isSafeInteger(value) && value >= 0)
      .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
      .map(([entryName, value], index) => ({ name: entryName, id: index + 1, value: Number(value) }))
  }
}

function openCombinationBindings (name, field) {
  const names = Object.fromEntries(Object.entries(LOCAL_STREAM_OPEN_COMBINATION).map(([entryName, id]) => [id, entryName]))
  return {
    name,
    entries: LOCAL_STREAM_OPEN_TABLE.map(row => ({ name: names[row.id], id: row.id, value: row[field] }))
  }
}

function encode (encoding, value) {
  const state = { start: 0, end: 0, buffer: null }
  encoding.preencode(state, value)
  state.buffer = b4a.alloc(state.end)
  state.start = 0
  encoding.encode(state, value)
  return state.buffer
}

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

export function privateIpcRegistryValue (wireAbiBytes) {
  return {
    magic: 'hiverelay-blind-private-ipc-v1',
    formatVersion: 1,
    wireAbiHash: hashImportedWireAbi(wireAbiBytes),
    schemas: PRIVATE_IPC_SCHEMAS,
    importedWireBindings: [
      enumBindings('FAMILY', FAMILY),
      enumBindings('TRANSPORT_ID', TRANSPORT_ID),
      enumBindings('TRANSPORT_SUPPORT', TRANSPORT_SUPPORT),
      classBindings('OUTER_CLASS', OUTER_CLASS),
      classBindings('STREAM_WIRE_CLASS', STREAM_WIRE_CLASS)
    ],
    localBindings: [
      ...Object.entries(LOCAL_BINDINGS).map(([name, values]) =>
        name === 'limits' || name === 'timingMillis' || name === 'readyRules' ||
        name === 'ciphertextBytes' || name === 'streamControlBytes'
          ? namedValueBindings(name, values)
          : enumBindings(name, values)),
      openCombinationBindings('streamOpenKindByCombination', 'openKind'),
      openCombinationBindings('streamModeByCombination', 'streamMode'),
      openCombinationBindings('streamClassMinimumByCombination', 'classMinimum'),
      openCombinationBindings('streamClassMaximumByCombination', 'classMaximum'),
      openCombinationBindings('streamContextKindByCombination', 'contextKind'),
      openCombinationBindings('streamAdjacentPolicyByCombination', 'adjacentPolicy')
    ]
  }
}

export function encodePrivateIpcRegistry (wireAbiBytes) {
  return encode(registryEncoding, privateIpcRegistryValue(wireAbiBytes))
}

function decodeRegistryBytes (input) {
  const bytes = asBuffer(input, 'private IPC registry')
  if (bytes.byteLength < 1 || bytes.byteLength > MAX_PRIVATE_IPC_REGISTRY_BYTES) {
    registryFailure(`private IPC registry must be bytes[1..${MAX_PRIVATE_IPC_REGISTRY_BYTES}]`)
  }
  const state = { start: 0, end: bytes.byteLength, buffer: bytes }
  let value
  try {
    value = registryEncoding.decode(state)
  } catch (error) {
    if (error && error.code === 'BAD_PRIVATE_IPC_REGISTRY') throw error
    registryFailure('private IPC registry is truncated or malformed')
  }
  if (state.start !== state.end) registryFailure('private IPC registry has trailing bytes')
  const canonical = encode(registryEncoding, value)
  if (!b4a.equals(canonical, bytes)) registryFailure('private IPC registry is not canonical')
  return { value, bytes }
}

export function decodePrivateIpcRegistry (input) {
  return decodeRegistryBytes(input).value
}

export function verifyPrivateIpcRegistry (input, wireAbiBytes) {
  const decoded = decodeRegistryBytes(input)
  const expected = encodePrivateIpcRegistry(asBuffer(wireAbiBytes, 'public WIRE ABI'))
  if (!b4a.equals(decoded.bytes, expected)) {
    registryFailure('private IPC registry does not equal the exact local schemas and final WIRE ABI bindings')
  }
  return decoded.value
}

export function hashPrivateIpcRegistry (registryBytes) {
  return privateBlake2b256(b4a.concat([FORMAT_DOMAIN, len64(registryBytes.byteLength), registryBytes]))
}

export function hashPrivateIpcVectorManifest (manifestBytes) {
  return privateBlake2b256(b4a.concat([VECTOR_DOMAIN, len64(manifestBytes.byteLength), manifestBytes]))
}

export { registryEncoding as privateIpcRegistryEncoding }