OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-lib / pairing-protocol.cjs

211 lines · javascript ref:
// lib/pairing-protocol.cjs - Device pairing protocol using Protomux

const Protomux = require('protomux')
const b4a = require('b4a')
const c = require('compact-encoding')

// Protocol identifier
const PAIRING_PROTOCOL = 'hyper-wallet-pairing'

/**
 * Create pairing channel for device that is adding another device (server side)
 * @param {Connection} conn - Hyperswarm connection
 * @param {string} identityMnemonic - Identity mnemonic (24 words) to send to joining device
 * @param {string} walletMnemonic - Wallet mnemonic (12 words) to send to joining device
 * @param {string} deviceName - Name of this device
 * @returns {Promise<Object>} { remoteDeviceName, remoteWriterKey }
 */
async function createPairingServer(conn, identityMnemonic, walletMnemonic, deviceName) {
  return new Promise((resolve, reject) => {
    const mux = Protomux.from(conn)
    const channel = mux.createChannel({
      protocol: PAIRING_PROTOCOL,
      id: null
    })

    // Message types
    const messages = {
      // 0: Request pairing (client → server)
      request: channel.addMessage({
        encoding: {
          preencode(state, m) {
            c.string.preencode(state, m.deviceName)
            c.buffer.preencode(state, m.writerKey)
          },
          encode(state, m) {
            c.string.encode(state, m.deviceName)
            c.buffer.encode(state, m.writerKey)
          },
          decode(state) {
            return {
              deviceName: c.string.decode(state),
              writerKey: c.buffer.decode(state)
            }
          }
        },
        onmessage(msg) {
          console.log(`📨 Received pairing request from: ${msg.deviceName}`)

          // Send BOTH mnemonics to joining device
          messages.response.send({
            identityMnemonic,
            walletMnemonic,
            serverDeviceName: deviceName,
            approved: true
          })

          // Resolve with remote device info
          resolve({
            remoteDeviceName: msg.deviceName,
            remoteWriterKey: msg.writerKey
          })
        }
      }),

      // 1: Send mnemonics (server → client)
      response: channel.addMessage({
        encoding: {
          preencode(state, m) {
            c.string.preencode(state, m.identityMnemonic)
            c.string.preencode(state, m.walletMnemonic)
            c.string.preencode(state, m.serverDeviceName)
            c.bool.preencode(state, m.approved)
          },
          encode(state, m) {
            c.string.encode(state, m.identityMnemonic)
            c.string.encode(state, m.walletMnemonic)
            c.string.encode(state, m.serverDeviceName)
            c.bool.encode(state, m.approved)
          },
          decode(state) {
            return {
              identityMnemonic: c.string.decode(state),
              walletMnemonic: c.string.decode(state),
              serverDeviceName: c.string.decode(state),
              approved: c.bool.decode(state)
            }
          }
        }
      })
    }

    channel.open()

    // Timeout after 60 seconds
    const timeout = setTimeout(() => {
      reject(new Error('Pairing timeout - no response from joining device'))
    }, 60000)

    // Clear timeout when resolved
    const originalResolve = resolve
    resolve = (value) => {
      clearTimeout(timeout)
      originalResolve(value)
    }
  })
}

/**
 * Create pairing channel for device that is joining (client side)
 * @param {Connection} conn - Hyperswarm connection
 * @param {Buffer} writerKey - Writer key of this device
 * @param {string} deviceName - Name of this device
 * @returns {Promise<Object>} { identityMnemonic, walletMnemonic } received from server
 */
async function createPairingClient(conn, writerKey, deviceName) {
  return new Promise((resolve, reject) => {
    const mux = Protomux.from(conn)
    const channel = mux.createChannel({
      protocol: PAIRING_PROTOCOL,
      id: null
    })

    // Message types
    const messages = {
      // 0: Request pairing (client → server)
      request: channel.addMessage({
        encoding: {
          preencode(state, m) {
            c.string.preencode(state, m.deviceName)
            c.buffer.preencode(state, m.writerKey)
          },
          encode(state, m) {
            c.string.encode(state, m.deviceName)
            c.buffer.encode(state, m.writerKey)
          },
          decode(state) {
            return {
              deviceName: c.string.decode(state),
              writerKey: c.buffer.decode(state)
            }
          }
        }
      }),

      // 1: Receive mnemonics (server → client)
      response: channel.addMessage({
        encoding: {
          preencode(state, m) {
            c.string.preencode(state, m.identityMnemonic)
            c.string.preencode(state, m.walletMnemonic)
            c.string.preencode(state, m.serverDeviceName)
            c.bool.preencode(state, m.approved)
          },
          encode(state, m) {
            c.string.encode(state, m.identityMnemonic)
            c.string.encode(state, m.walletMnemonic)
            c.string.encode(state, m.serverDeviceName)
            c.bool.encode(state, m.approved)
          },
          decode(state) {
            return {
              identityMnemonic: c.string.decode(state),
              walletMnemonic: c.string.decode(state),
              serverDeviceName: c.string.decode(state),
              approved: c.bool.decode(state)
            }
          }
        },
        onmessage(msg) {
          if (!msg.approved) {
            reject(new Error('Pairing rejected by server'))
            return
          }

          console.log(`✅ Received mnemonics from: ${msg.serverDeviceName}`)
          resolve({
            identityMnemonic: msg.identityMnemonic,
            walletMnemonic: msg.walletMnemonic
          })
        }
      })
    }

    channel.open()

    // Send pairing request immediately (channel is ready after open())
    console.log(`📤 Sending pairing request as: ${deviceName}`)
    messages.request.send({
      deviceName,
      writerKey
    })

    // Timeout after 60 seconds
    const timeout = setTimeout(() => {
      reject(new Error('Pairing timeout - no response from server'))
    }, 60000)

    // Clear timeout when resolved
    const originalResolve = resolve
    resolve = (value) => {
      clearTimeout(timeout)
      originalResolve(value)
    }
  })
}

module.exports = {
  createPairingServer,
  createPairingClient
}