import test from 'brittle'
import Hyperswarm from 'hyperswarm'
import createTestnet from '@hyperswarm/testnet'
import IdentityKey from 'keet-identity-key'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import { DeviceLinker } from '../lib/device-linker.js'
import { verifyEnvelope, signTestEnvelope } from '../lib/auth/envelope.js'
import { createKeetVerifier } from '../lib/auth/keet-verifier.js'
import { ReplayCache } from '../lib/auth/replay-cache.js'
// Phase 2c tests — blind-pairing device linking.
//
// These tests exercise the full end-to-end flow: source creates invite,
// target joins, target gets a device chain that verifies against the
// source's identity root.
test('device-linker: source creates invite, target joins and gets valid chain', async (t) => {
// Setup: create a keet identity + device chain for the source daemon.
const mnemonic = IdentityKey.generateMnemonic()
const identityKey = await IdentityKey.from({ mnemonic })
const sourceKp = crypto.keyPair()
const sourceProof = await identityKey.bootstrap(sourceKp.publicKey)
const rootHex = b4a.toString(identityKey.identityKeyPair.publicKey, 'hex')
// Create a testnet so both swarm instances can discover each other.
const testnet = await createTestnet(2)
const sourceSwarm = new Hyperswarm(testnet[0])
const targetSwarm = new Hyperswarm(testnet[1])
t.teardown(async () => {
await sourceSwarm.destroy()
await targetSwarm.destroy()
await testnet.destroy()
})
// Source side: create invite.
const sourceLinker = new DeviceLinker({
swarm: sourceSwarm,
deviceChain: { keyPair: sourceKp, proof: sourceProof },
rootPubkeyHex: rootHex
})
const { inviteHex, waitForPeer } = await sourceLinker.createInvite({
label: 'test-device'
})
t.ok(inviteHex, 'inviteHex returned')
t.ok(inviteHex.length > 20, 'inviteHex is non-trivial')
// Target side: join with invite.
const targetLinker = new DeviceLinker({ swarm: targetSwarm })
const [peerResult, bundle] = await Promise.all([
waitForPeer(),
targetLinker.joinWithInvite(inviteHex)
])
// Source sees the new device.
t.ok(peerResult.devicePublicKeyHex, 'source got device pubkey')
t.is(peerResult.devicePublicKeyHex.length, 64, 'pubkey is 64-char hex')
// Target received a valid bundle.
t.is(bundle.rootPubkey, rootHex, 'bundle root matches source identity')
t.is(bundle.label, 'test-device', 'label preserved')
t.is(bundle.devicePublicKey.length, 64, 'device pubkey is 64-char hex')
t.is(bundle.deviceSecretKey.length, 128, 'device secret key is 128-char hex')
t.ok(bundle.deviceProof, 'chain proof present')
t.is(bundle.anKeypair.length, 132, 'anKeypair is 132-char canonical')
// The received chain should verify via keet-identity-key.
const proof = b4a.from(bundle.deviceProof, 'base64')
const verification = IdentityKey.verify(proof)
t.ok(verification, 'chain verifies via IdentityKey.verify')
t.is(
b4a.toString(verification.identityPublicKey, 'hex'),
rootHex,
'chain roots to the source identity'
)
// Target's device key in the chain matches the bundle.
t.is(
b4a.toString(verification.devicePublicKey, 'hex'),
bundle.devicePublicKey,
'chain device matches bundle device'
)
// Now verify the target can sign an envelope with chain and it resolves
// to the source's root identity (end-to-end with Phase 2b).
const { payload, sig } = await signTestEnvelope({
keypair: bundle.anKeypair,
method: 'whoami',
params: {}
})
const verified = await verifyEnvelope(
{ payload, sig, chain: bundle.deviceProof },
{ replayCache: new ReplayCache(), keetVerifier: createKeetVerifier() }
)
t.is(verified.method, 'keet', 'envelope method is keet')
t.is(verified.rootPubkey, rootHex, 'envelope resolves to source identity root')
t.is(verified.devicePubkey, bundle.devicePublicKey, 'envelope device is the linked device')
await sourceLinker.close()
await targetLinker.close()
})
test('device-linker: target cannot use a different keypair than the chain vouches for', async (t) => {
// Setup identity + source device.
const mnemonic = IdentityKey.generateMnemonic()
const identityKey = await IdentityKey.from({ mnemonic })
const sourceKp = crypto.keyPair()
const sourceProof = await identityKey.bootstrap(sourceKp.publicKey)
const rootHex = b4a.toString(identityKey.identityKeyPair.publicKey, 'hex')
const testnet = await createTestnet(2)
const sw1 = new Hyperswarm(testnet[0])
const sw2 = new Hyperswarm(testnet[1])
t.teardown(async () => {
await sw1.destroy()
await sw2.destroy()
await testnet.destroy()
})
const sourceLinker = new DeviceLinker({
swarm: sw1,
deviceChain: { keyPair: sourceKp, proof: sourceProof },
rootPubkeyHex: rootHex
})
const { inviteHex, waitForPeer } = await sourceLinker.createInvite({ label: 'test' })
const targetLinker = new DeviceLinker({ swarm: sw2 })
const [, bundle] = await Promise.all([
waitForPeer(),
targetLinker.joinWithInvite(inviteHex)
])
// Sign with a DIFFERENT keypair than the bundle's — chain verification
// should reject because the chain vouches for a different device.
const rogueKp = await (await import('anproto')).an.gen()
const { payload, sig } = await signTestEnvelope({
keypair: rogueKp,
method: 'whoami',
params: {}
})
try {
await verifyEnvelope(
{ payload, sig, chain: bundle.deviceProof },
{ replayCache: new ReplayCache(), keetVerifier: createKeetVerifier() }
)
t.fail('should reject: chain/device mismatch')
} catch (err) {
t.ok(err.code === 'CHAIN_INVALID', 'chain mismatch caught: ' + err.message)
}
await sourceLinker.close()
await targetLinker.close()
})