OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / mint.test.js

73 lines · javascript ref:
// Minting an identity, and restoring one.
//
// ⛔ THE ASSERTION THAT MATTERS is that the vault on disk does not contain the mnemonic in the clear
// when a passphrase was given. Everything else here is behaviour; that one is the security claim the
// whole "a stolen device yields a locked vault" argument rests on (hyper-social #48).
import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import { mintIdentity } from '../lib/mint.js'
import { mintFromVault } from '../lib/device.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-mint-'))
const vaultIn = (d) => join(d, '.hyperident', 'identity')

test('★ mint creates a vault and returns a credential and the words, once', async (t) => {
  const p = vaultIn(tmp())
  const r = await mintIdentity({ vaultPath: p })
  // 24, because keet-identity-key generates 256-bit phrases and the fleet mints with it.
  t.is(r.mnemonic.split(' ').length, 24, 'a 24-word phrase, matching every other fleet identity')
  t.ok(r.credential.publicKey && r.credential.secretKey && r.credential.proof, 'a linkable credential')
  t.ok(fs.existsSync(p), 'the vault landed')
})

test('⛔ with a passphrase, the mnemonic is NOT on disk in the clear', async (t) => {
  const p = vaultIn(tmp())
  const r = await mintIdentity({ vaultPath: p, passphrase: 'a good long passphrase' })
  const raw = fs.readFileSync(p, 'utf8')
  t.absent(raw.includes(r.mnemonic), 'the words are absent from the vault file')
  for (const w of r.mnemonic.split(' ')) {
    if (raw.includes('"' + w + '"')) t.fail('a mnemonic word appears verbatim: ' + w)
  }
  const v = JSON.parse(raw)
  t.is(v.encrypted, true)
  t.is(v.kdf, 'argon2id', 'and it records the scheme, so a pre-#47 vault stays distinguishable')
  t.ok(v.opslimit && v.memlimit, 'the cost is recorded PER VAULT, not assumed from the default')
})

test('★ the vault it wrote is the vault mintFromVault reads — round trip', async (t) => {
  const p = vaultIn(tmp())
  await mintIdentity({ vaultPath: p, passphrase: 'pw' })
  const cred = await mintFromVault(p, { passphrase: 'pw' })
  t.ok(cred.proof, 'reopened and re-minted with the same passphrase')
  await t.exception(() => mintFromVault(p, { passphrase: 'nope' }), /passphrase/i)
})

test('★ RESTORE — the same words rebuild the same identity, with no peer online', async (t) => {
  const first = vaultIn(tmp())
  const a = await mintIdentity({ vaultPath: first })

  const second = vaultIn(tmp())
  const b = await mintIdentity({ vaultPath: second, mnemonic: a.mnemonic })

  t.is(b.mnemonic, a.mnemonic, 'same words')
  // ⛔ DIFFERENT device keys, SAME root. A restore must not resurrect the old device credential —
  // each device is attested separately so it can be revoked separately.
  t.not(b.credential.publicKey, a.credential.publicKey, 'a fresh device key, not the old one')
})

test('⛔ it refuses to overwrite an existing vault', async (t) => {
  const p = vaultIn(tmp())
  await mintIdentity({ vaultPath: p })
  await t.exception(() => mintIdentity({ vaultPath: p }), /already exists|refusing/i)
})

test('⛔ a mistyped recovery phrase is refused, not silently turned into a different identity', async (t) => {
  const p = vaultIn(tmp())
  await t.exception(
    () => mintIdentity({ vaultPath: p, mnemonic: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon' }),
    /not a valid recovery phrase/i)
  t.absent(fs.existsSync(p), 'and nothing was written')
})