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')
})