hashchain-log
v0.1.0
Published
tamper evident append only log. each entry hashes the previous, verify offline
Maintainers
Readme
hashchain-log
tamper evident append only log. each entry hashes the previous one, so any edit breaks the chain. verify offline with no library state.
npm i hashchain-logwhy
"prove that two people approved this before it executed" is a normal audit question. the usual answer is a database table, which anyone with write access can rewrite.
a hash chain cannot be rewritten quietly. change one byte in entry 3 and entry 4 stops linking.
use
import { HashChainLog } from 'hashchain-log'
const log = new HashChainLog()
await log.append({ action: 'withdraw', amount: '100', by: 'alice' })
await log.append({ action: 'approve', by: 'bob' })
await log.verify() // { ok: true, length: 2, brokenAt: -1 }tamper with it and it says exactly where:
const entries = await log.entries()
entries[0].body.amount = '999999'
verify(entries)
// { ok: false, brokenAt: 0, reason: 'body was changed, hash does not match' }deleting, reordering or splicing an entry all fail the same way.
sign it too
hashes prove nothing was edited. signatures prove who wrote it.
import { generateKeyPairSync } from 'node:crypto'
const { privateKey, publicKey } = generateKeyPairSync('ed25519')
const log = new HashChainLog({ signWith: privateKey })
await log.append({ action: 'release funds' })
await log.verify(publicKey) // { ok: true, ... }ed25519, ed448, rsa and ec all work — the digest is picked from the key type.
verify anywhere
verify() is a plain function over plain json. no log instance, no storage, no network.
import { verify } from 'hashchain-log'
const entries = JSON.parse(await readFile('audit.json', 'utf8'))
verify(entries, publicKey)hand someone the file and the public key and they can check it themselves. that is the whole point.
storage
in memory by default. anything with three methods works:
const log = new HashChainLog({
storage: {
async append (entry) { await db.insert(entry) },
async * read () { yield * db.stream() },
async last () { return db.findLast() }
}
})canonical bodies
key order does not change the hash:
canonical({ b: 1, a: 2 }) === canonical({ a: 2, b: 1 }) // trueso a body that round trips through a database or another json serializer still verifies.
api
| | |
|---|---|
| new HashChainLog({ storage?, signWith?, now? }) | |
| .append(body) | returns the entry |
| .entries() | |
| .head() | |
| .verify(publicKey?) | |
| verify(entries, publicKey?) | standalone, offline |
| canonical(value) | stable json |
| hashEntry({ index, at, body, prev }) | |
entry shape: { index, at, body, prev, hash, sig? }.
zero dependencies. node crypto only. types included.
licence
MIT
