OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-client / test / intent.test.js

117 lines · javascript ref:
import b4a from 'b4a'
import test from 'brittle'
import {
  EncryptedIntentStore,
  INTENT_STATE,
  MemoryIntentBackend,
  createAesGcmIntentSealer,
  createClientIntent,
  decodeClientIntent,
  encodeClientIntent,
  journalSignedIntent
} from '../control.js'
import { createNodeCryptoRuntime } from '../runtime/node.js'

const runtime = createNodeCryptoRuntime()

function bytes (value) {
  return b4a.alloc(32, value)
}

function fixture () {
  return createClientIntent({
    runtime,
    intentId: bytes(1),
    logicalId: bytes(2),
    continuityRoot: bytes(3),
    storeId: bytes(4),
    descriptorHash: bytes(5),
    descriptorSequence: 7n,
    endpointId: 1,
    transportId: 1,
    transportSupportBit: 1,
    privacyProfileBit: 1,
    familyId: 2,
    operationId: 1,
    requestCommitment: bytes(6),
    clientNonce: bytes(7),
    operationBytes: b4a.from('SIGNED_OPAQUE_OPERATION_SENTINEL_62f8a7', 'ascii')
  })
}

test('client intent codec is byte-exact, bounded and rejects noncanonical reserved state', t => {
  const intent = fixture()
  const encoded = encodeClientIntent(intent)
  const decoded = decodeClientIntent(encoded)
  t.alike(decoded.intentId, intent.intentId)
  t.alike(decoded.operationBytes, intent.operationBytes)
  t.is(decoded.state, INTENT_STATE.JOURNALED)
  t.alike(encodeClientIntent(decoded), encoded)

  const reserved = b4a.from(encoded)
  reserved[262] = 1
  t.exception(() => decodeClientIntent(reserved), /reserved bytes/)
  t.exception(() => encodeClientIntent({ ...intent, operationBytes: b4a.alloc(4 * 1024 * 1024 + 1) }), /byte bound/)
})

test('encrypted intent store persists no operation plaintext and recovers exact retry bytes after restart', async t => {
  const backend = new MemoryIntentBackend()
  const sealer = createAesGcmIntentSealer(runtime, bytes(9))
  const store = new EncryptedIntentStore({ backend, sealer })
  const intent = fixture()
  const createSeed = bytes(10)
  await journalSignedIntent(store, intent, { ephemeralSecrets: [createSeed] })
  t.alike(createSeed, b4a.alloc(32))

  const raw = [...backend.records.values()][0].ciphertext
  t.absent(b4a.toString(raw, 'latin1').includes('SIGNED_OPAQUE_OPERATION_SENTINEL_62f8a7'))
  t.alike((await store.read(intent.intentId)).value.operationBytes, intent.operationBytes)

  await store.update(intent.intentId, value => ({ ...value, state: INTENT_STATE.TARGET_PREPARED }))
  await store.update(intent.intentId, value => ({
    ...value,
    state: INTENT_STATE.SENT,
    mayHaveCommitted: true,
    attemptCount: value.attemptCount + 1
  }))
  await store.update(intent.intentId, value => ({ ...value, state: INTENT_STATE.PENDING_UNKNOWN }))

  const restarted = new EncryptedIntentStore({ backend, sealer })
  const recovered = await restarted.read(intent.intentId)
  t.is(recovered.value.state, INTENT_STATE.PENDING_UNKNOWN)
  t.is(recovered.value.mayHaveCommitted, true)
  t.alike(recovered.value.operationBytes, intent.operationBytes)

  await restarted.update(intent.intentId, value => ({ ...value, state: INTENT_STATE.TARGET_PREPARED }))
  await restarted.update(intent.intentId, value => ({
    ...value,
    state: INTENT_STATE.SENT,
    attemptCount: value.attemptCount + 1
  }))
  t.is((await restarted.read(intent.intentId)).value.attemptCount, 2)
})

test('intent CAS forbids identity mutation, result-before-verification and authenticated-ciphertext replay', async t => {
  const backend = new MemoryIntentBackend()
  const store = new EncryptedIntentStore({
    backend,
    sealer: createAesGcmIntentSealer(runtime, bytes(11))
  })
  const intent = fixture()
  await store.create(intent)
  await t.exception(store.update(intent.intentId, value => ({
    ...value,
    state: INTENT_STATE.TARGET_PREPARED,
    operationBytes: b4a.from('changed')
  })), /immutable intent identity/)
  await t.exception(store.update(intent.intentId, value => ({
    ...value,
    resultBytes: b4a.from([1])
  })), /only after complete verification/)

  const key = (await backend.keys())[0]
  const record = backend.records.get(key)
  record.ciphertext[record.ciphertext.byteLength - 1] ^= 1
  await t.exception(store.read(intent.intentId), /authentication failed/)
})