Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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-log

why

"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 })   // true

so 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

About

tamper evident append only log. each entry hashes the previous, verify offline

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages