OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_phase3_vectors.js

370 lines · javascript ref:
#!/usr/bin/env node
// Generate Phase 3 test vectors: HyperDHT messages, namespaces, Noise IK.
// Usage: node test/test_phase3_vectors.js > test/phase3_vectors.json

const c = require('compact-encoding')
const net = require('compact-encoding-net')
const sodium = require('sodium-universal')
const b4a = require('b4a')
const crypto = require('hypercore-crypto')
const m = require('hyperdht/lib/messages')
const constants = require('hyperdht/lib/constants')

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

function toHex (buf) {
  return b4a.toString(buf, 'hex')
}

const vectors = {
  namespaces: {},
  noisePayload: [],
  handshakeMessage: [],
  holepunchPayload: [],
  peerRecord: [],
  announceMessage: [],
  securePayload: [],
  noiseIK: {}
}

// ---- Namespace vectors ----
// Compute namespaces exactly as constants.js does.
const [NS_ANNOUNCE, NS_UNANNOUNCE, NS_MUTABLE_PUT, NS_PEER_HANDSHAKE, NS_PEER_HOLEPUNCH] =
  crypto.namespace('hyperswarm/dht', [
    constants.COMMANDS.ANNOUNCE,      // 4
    constants.COMMANDS.UNANNOUNCE,    // 5
    constants.COMMANDS.MUTABLE_PUT,   // 6
    constants.COMMANDS.PEER_HANDSHAKE, // 0
    constants.COMMANDS.PEER_HOLEPUNCH  // 1
  ])

vectors.namespaces = {
  NS_ANNOUNCE: toHex(NS_ANNOUNCE),
  NS_UNANNOUNCE: toHex(NS_UNANNOUNCE),
  NS_MUTABLE_PUT: toHex(NS_MUTABLE_PUT),
  NS_PEER_HANDSHAKE: toHex(NS_PEER_HANDSHAKE),
  NS_PEER_HOLEPUNCH: toHex(NS_PEER_HOLEPUNCH)
}

// ---- NoisePayload: basic (4 bytes) ----
{
  const payload = {
    version: 1,
    error: 0,
    firewall: 0,
    holepunch: null,
    addresses4: [],
    addresses6: [],
    udx: null,
    secretStream: null,
    relayThrough: null,
    relayAddresses: null
  }
  const buf = encode(m.noisePayload, payload)
  vectors.noisePayload.push({
    description: 'basic (no optional fields)',
    bytes: buf.length,
    hex: toHex(buf),
    error: 0,
    firewall: 0
  })
}

// ---- NoisePayload: with addresses and firewall ----
{
  const payload = {
    version: 1,
    error: 0,
    firewall: 2,
    holepunch: null,
    addresses4: [
      { host: '127.0.0.1', port: 10240 },
      { host: '192.168.1.1', port: 4242 }
    ],
    addresses6: [],
    udx: null,
    secretStream: null,
    relayThrough: null,
    relayAddresses: null
  }
  const buf = encode(m.noisePayload, payload)
  vectors.noisePayload.push({
    description: 'with addresses4 and firewall=CONSISTENT',
    bytes: buf.length,
    hex: toHex(buf),
    firewall: 2,
    addresses4: payload.addresses4
  })
}

// ---- NoisePayload: with UDX info ----
{
  const payload = {
    version: 1,
    error: 0,
    firewall: 1,
    holepunch: null,
    addresses4: [{ host: '10.0.0.1', port: 12345 }],
    addresses6: [],
    udx: { reusableSocket: false, id: 7, seq: 100 },
    secretStream: null,
    relayThrough: null,
    relayAddresses: null
  }
  const buf = encode(m.noisePayload, payload)
  vectors.noisePayload.push({
    description: 'with addresses4 + UDX info',
    bytes: buf.length,
    hex: toHex(buf),
    firewall: 1,
    udx_id: 7,
    udx_seq: 100
  })
}

// ---- NoisePayload: with secretStream ----
{
  const payload = {
    version: 1,
    error: 0,
    firewall: 1,
    holepunch: null,
    addresses4: [],
    addresses6: [],
    udx: { reusableSocket: true, id: 1, seq: 0 },
    secretStream: {},
    relayThrough: null,
    relayAddresses: null
  }
  const buf = encode(m.noisePayload, payload)
  vectors.noisePayload.push({
    description: 'with UDX + secretStream',
    bytes: buf.length,
    hex: toHex(buf)
  })
}

// ---- Handshake message ----
{
  const noise = b4a.alloc(32, 0xAA)
  const msg = {
    mode: 1,
    noise,
    peerAddress: null,
    relayAddress: { host: '88.99.3.86', port: 49737 }
  }
  const buf = encode(m.handshake, msg)
  vectors.handshakeMessage.push({
    description: 'mode=1 with relay address',
    bytes: buf.length,
    hex: toHex(buf),
    mode: 1,
    noise_hex: toHex(noise),
    relay: msg.relayAddress
  })
}

// ---- Handshake message: with both addresses ----
{
  const noise = b4a.alloc(81, 0xBB) // Typical Noise IK msg1 size
  const msg = {
    mode: 0,
    noise,
    peerAddress: { host: '10.0.0.1', port: 12345 },
    relayAddress: { host: '88.99.3.86', port: 49737 }
  }
  const buf = encode(m.handshake, msg)
  vectors.handshakeMessage.push({
    description: 'mode=0 with peer + relay addresses',
    bytes: buf.length,
    hex: toHex(buf),
    mode: 0
  })
}

// ---- Holepunch payload: basic ----
{
  const payload = {
    error: 0,
    firewall: 2,
    round: 1,
    connected: false,
    punching: true,
    addresses: null,
    remoteAddress: null,
    token: null,
    remoteToken: null
  }
  const buf = encode(m.holepunchPayload, payload)
  vectors.holepunchPayload.push({
    description: 'punching, round=1, firewall=CONSISTENT',
    bytes: buf.length,
    hex: toHex(buf),
    firewall: 2,
    round: 1,
    punching: true
  })
}

// ---- Holepunch payload: with addresses and tokens ----
{
  const token = b4a.alloc(32, 0xCC)
  const remoteToken = b4a.alloc(32, 0xDD)
  const payload = {
    error: 0,
    firewall: 1,
    round: 3,
    connected: false,
    punching: true,
    addresses: [{ host: '10.0.0.1', port: 12345 }, { host: '192.168.1.1', port: 4242 }],
    remoteAddress: { host: '88.99.3.86', port: 49737 },
    token,
    remoteToken
  }
  const buf = encode(m.holepunchPayload, payload)
  vectors.holepunchPayload.push({
    description: 'with addresses, remoteAddress, tokens',
    bytes: buf.length,
    hex: toHex(buf)
  })
}

// ---- Peer record: no relays ----
{
  const pk = b4a.alloc(32, 0)
  for (let i = 0; i < 32; i++) pk[i] = i
  const record = {
    publicKey: pk,
    relayAddresses: []
  }
  const buf = encode(m.peer, record)
  vectors.peerRecord.push({
    description: 'no relay addresses',
    bytes: buf.length,
    hex: toHex(buf),
    pk_hex: toHex(pk)
  })
}

// ---- Peer record: with relays ----
{
  const pk = b4a.alloc(32, 0xFF)
  const record = {
    publicKey: pk,
    relayAddresses: [
      { host: '88.99.3.86', port: 49737 },
      { host: '142.93.90.113', port: 49737 }
    ]
  }
  const buf = encode(m.peer, record)
  vectors.peerRecord.push({
    description: 'with 2 relay addresses',
    bytes: buf.length,
    hex: toHex(buf)
  })
}

// ---- Announce message: with peer + signature ----
{
  const pk = b4a.alloc(32, 0)
  for (let i = 0; i < 32; i++) pk[i] = i
  const sig = b4a.alloc(64, 0)
  for (let i = 0; i < 64; i++) sig[i] = i
  const msg = {
    peer: { publicKey: pk, relayAddresses: [] },
    refresh: null,
    signature: sig
  }
  const buf = encode(m.announce, msg)
  vectors.announceMessage.push({
    description: 'peer + signature, no refresh',
    bytes: buf.length,
    hex: toHex(buf)
  })
}

// ---- Announce message: peer + refresh + signature ----
{
  const pk = b4a.alloc(32, 0xAA)
  const refresh = b4a.alloc(32, 0xBB)
  const sig = b4a.alloc(64, 0xCC)
  const msg = {
    peer: { publicKey: pk, relayAddresses: [{ host: '88.99.3.86', port: 49737 }] },
    refresh,
    signature: sig
  }
  const buf = encode(m.announce, msg)
  vectors.announceMessage.push({
    description: 'peer + refresh + signature with relay',
    bytes: buf.length,
    hex: toHex(buf)
  })
}

// ---- SecurePayload: encrypt/decrypt round-trip with known key ----
// We can't test exact bytes (random nonce), but we test the format: nonce(24) || ciphertext+mac
{
  const key = b4a.alloc(32, 0x42)
  const plaintext = b4a.from('holepunch-test-payload')

  // Manually create a deterministic nonce for testing
  const nonce = b4a.alloc(24, 0)
  for (let i = 0; i < 24; i++) nonce[i] = i

  const ct = b4a.alloc(plaintext.length + sodium.crypto_secretbox_MACBYTES)
  sodium.crypto_secretbox_easy(ct, plaintext, nonce, key)

  const encrypted = b4a.concat([nonce, ct])

  vectors.securePayload.push({
    description: 'deterministic nonce encrypt',
    key_hex: toHex(key),
    plaintext_hex: toHex(plaintext),
    nonce_hex: toHex(nonce),
    encrypted_hex: toHex(encrypted),
    encrypted_bytes: encrypted.length
  })
}

// ---- Noise IK handshake cross-validation ----
// Use deterministic keypairs so C++ can reproduce the same handshake.
{
  // Generate deterministic keypairs from seeds.
  const initiator_seed = b4a.alloc(32, 0)
  for (let i = 0; i < 32; i++) initiator_seed[i] = i

  const responder_seed = b4a.alloc(32, 0)
  for (let i = 0; i < 32; i++) responder_seed[i] = 32 + i

  const initiator_kp = { publicKey: b4a.alloc(32), secretKey: b4a.alloc(64) }
  sodium.crypto_sign_seed_keypair(initiator_kp.publicKey, initiator_kp.secretKey, initiator_seed)

  const responder_kp = { publicKey: b4a.alloc(32), secretKey: b4a.alloc(64) }
  sodium.crypto_sign_seed_keypair(responder_kp.publicKey, responder_kp.secretKey, responder_seed)

  vectors.noiseIK = {
    initiator_pk: toHex(initiator_kp.publicKey),
    initiator_sk: toHex(initiator_kp.secretKey),
    responder_pk: toHex(responder_kp.publicKey),
    responder_sk: toHex(responder_kp.secretKey),
    ns_peer_handshake: toHex(NS_PEER_HANDSHAKE),
    ns_peer_holepunch: toHex(NS_PEER_HOLEPUNCH)
  }

  // We can't do a full Noise IK handshake cross-validation here because
  // the JS uses ephemeral keys, making each run non-deterministic.
  // Instead, we validate:
  // 1. Namespace computation (above)
  // 2. Keypair generation from seed
  // 3. Message codec round-trips (above)
  // The C++ test will validate its own internal handshake consistency.
}

console.log(JSON.stringify(vectors, null, 2))