OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / identity-manager.test.js

163 lines · javascript ref:
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import IdentityKey from 'keet-identity-key'
import { IdentityManager } from '../../lib/identity/identity-manager.js'

const tmpDir = path.join(os.tmpdir(), 'pear-git-test-idmgr-' + Date.now())

function createStorage (name) {
  const dir = path.join(tmpDir, name)
  fs.mkdirSync(dir, { recursive: true })

  // Create a mock server-identity.json with a keypair
  const keypair = crypto.keyPair()
  const identity = {
    publicKey: keypair.publicKey.toString('hex'),
    secretKey: keypair.secretKey.toString('hex'),
    createdAt: Date.now()
  }
  fs.writeFileSync(path.join(dir, 'server-identity.json'), JSON.stringify(identity, null, 2), {
    encoding: 'utf-8',
    mode: 0o600
  })

  return { dir, keypair }
}

test('setup: create temp dir', async (t) => {
  await fs.promises.mkdir(tmpDir, { recursive: true })
  t.pass('temp dir created')
})

test('identity-manager: status before setup', (t) => {
  const { dir } = createStorage('status-before')
  const mgr = new IdentityManager({ storagePath: dir })
  const status = mgr.status()

  t.ok(status.hasKeypair, 'has keypair')
  t.is(status.hasVault, false, 'no vault')
  t.is(status.hasProof, false, 'no proof')
  t.is(status.identityPublicKey, null, 'no identity key')
})

test('identity-manager: setup creates vault and proof', async (t) => {
  const { dir, keypair } = createStorage('setup')
  const mgr = new IdentityManager({ storagePath: dir })
  const password = Buffer.from('test-password')

  const result = await mgr.setup(password)

  t.ok(result.mnemonic, 'mnemonic returned')
  t.ok(result.mnemonic.split(' ').length >= 12, 'mnemonic has at least 12 words')
  t.ok(result.identityPublicKey, 'identity key returned')
  t.is(result.identityPublicKey.length, 64, 'identity key is 64 hex chars')

  // Check status after setup
  const status = mgr.status()
  t.ok(status.hasVault, 'vault exists')
  t.ok(status.hasProof, 'proof exists')
  t.is(status.identityPublicKey, result.identityPublicKey, 'identity key matches')
})

test('identity-manager: verify proof after setup', async (t) => {
  const { dir, keypair } = createStorage('verify')
  const mgr = new IdentityManager({ storagePath: dir })
  const password = Buffer.from('test-password')

  const result = await mgr.setup(password)
  const verification = mgr.verifyProof()

  t.ok(verification.valid, 'proof is valid')
  t.is(verification.identityPublicKey, result.identityPublicKey, 'identity key matches')
  t.is(verification.devicePublicKey, keypair.publicKey.toString('hex'), 'device key matches')
})

test('identity-manager: export mnemonic', async (t) => {
  const { dir } = createStorage('export')
  const mgr = new IdentityManager({ storagePath: dir })
  const password = Buffer.from('test-password')

  const { mnemonic } = await mgr.setup(password)
  const exported = await mgr.exportMnemonic(password)

  t.is(exported, mnemonic, 'exported mnemonic matches original')
})

test('identity-manager: change password', async (t) => {
  const { dir } = createStorage('changepw')
  const mgr = new IdentityManager({ storagePath: dir })
  const oldPw = Buffer.from('old-password')
  const newPw = Buffer.from('new-password')

  const { mnemonic } = await mgr.setup(oldPw)
  await mgr.changePassword(oldPw, newPw)

  // Old password fails
  try {
    await mgr.exportMnemonic(oldPw)
    t.fail('old password should not work')
  } catch {
    t.pass('old password rejected')
  }

  // New password works
  const exported = await mgr.exportMnemonic(newPw)
  t.is(exported, mnemonic, 'new password decrypts correctly')
})

test('identity-manager: setupFromMnemonic creates matching identity', async (t) => {
  const { dir: dir1 } = createStorage('from-mnemonic-src')
  const { dir: dir2, keypair: keypair2 } = createStorage('from-mnemonic-dst')

  const mgr1 = new IdentityManager({ storagePath: dir1 })
  const mgr2 = new IdentityManager({ storagePath: dir2 })
  const password = Buffer.from('test-password')

  // Setup on device 1
  const { mnemonic, identityPublicKey } = await mgr1.setup(password)

  // Setup from mnemonic on device 2
  const result2 = await mgr2.setupFromMnemonic(mnemonic, password)

  // Same identity key
  t.is(result2.identityPublicKey, identityPublicKey, 'same identity key on both devices')

  // Both proofs valid
  const v1 = mgr1.verifyProof()
  const v2 = mgr2.verifyProof()
  t.ok(v1.valid, 'device 1 proof valid')
  t.ok(v2.valid, 'device 2 proof valid')
  t.is(v1.identityPublicKey, v2.identityPublicKey, 'same identity in both proofs')

  // Different device keys
  t.not(v1.devicePublicKey, v2.devicePublicKey, 'different device keys')
})

test('identity-manager: attestDevice creates valid proof for new device', async (t) => {
  const { dir } = createStorage('attest')
  const mgr = new IdentityManager({ storagePath: dir })
  const password = Buffer.from('test-password')

  await mgr.setup(password)

  // Generate a new device keypair
  const newDevice = crypto.keyPair()
  const proof = await mgr.attestDevice(newDevice.publicKey, password)

  // Verify the attestation
  const result = IdentityKey.verify(proof, null, {
    expectedDevice: newDevice.publicKey
  })

  t.ok(result, 'proof verifies for new device')
  t.is(result.devicePublicKey.toString('hex'), newDevice.publicKey.toString('hex'), 'device key matches')
})

test('cleanup: remove temp dir', async (t) => {
  await fs.promises.rm(tmpDir, { recursive: true, force: true })
  t.pass('cleaned up')
})