Milkiyat

A tamper-evident land registry built on a signed hash chain.

Milkiyat is a desktop land-records system where every approved registration or transfer becomes a block on a SHA-256 hash chain, signed by the approving surveyor and encrypted at rest. Any edit, even one made directly in the database, is detected.

Type
Security engineering project
My role
Design and implementation
Based on
A security framework proposed in an academic paper
Timeline
2026
Stack
  • Python
  • cryptography
  • Tkinter
  • SQLite
  • AES-256-GCM
  • RSA-PSS
  • PBKDF2
  • TOTP
Milkiyat

The problem

Land ownership records are a high-value target. In many systems a single person with database access can change an owner's name, and nothing records that it happened.

The goal was a registry where records can't be silently altered, every approval can be traced to a person who can't deny it, and personal data stays confidential, all on a single offline workstation, as a records office would use it.

What I built

  • Hash-chained ledger where each block's SHA-256 hash covers its plaintext data and the previous block's hash.
  • Field-level encryption of owner names and locations with AES-256-GCM and a fresh nonce per field.
  • Digital signatures: every approved block is signed with the surveyor's own RSA-2048 key (PSS, SHA-256).
  • Four roles (Citizen, Registrar, Surveyor, Admin) with separation of duties, two-factor login and a full audit log.
  • One-click integrity verification that reports exactly which block failed and why.

Architecture

Users Citizen own records only Registrar submits requests Surveyor approves & signs Admin users · audit · verify Access Login PBKDF2 · TOTP 2FA RBAC per-role dashboards Ledger core Hash SHA-256 over plaintext Encrypt AES-256-GCM fields Sign RSA-2048 PSS Storage SQLite blocks · users · requests Audit log every action
Milkiyat architecture

Key decisions

Hash the plaintext, not the ciphertext

Even an attacker holding the key who re-encrypts a forged owner name produces valid ciphertext, but the forged plaintext no longer matches the stored hash.

Per-user signing keys, encrypted with the user's password

Private keys are only decrypted in memory for a session, so approvals are bound to a specific surveyor and can't be made in their name.

Admins can't create or sign records

Separation of duties: the account that manages users and audits the chain has no code path to write to it.

Canonical JSON with sorted keys

The same data always hashes to the same value, and numeric fields are normalised so SQLite round-trips don't cause false alarms.

Security

  • PBKDF2-HMAC-SHA256 password hashing, 200,000 iterations, random salt per user
  • Constant-time comparisons with hmac.compare_digest
  • RFC 6238 TOTP implemented from scratch, compatible with Google Authenticator
  • Audit trail of logins, failed logins, requests, approvals and verification runs
  • Tested end to end, including a direct-database tampering attempt that verification caught

Results

3independent integrity checks per block
4roles with separation of duties
200kPBKDF2 iterations
1 clickfull-chain verification

What I'd improve next

  • Move from one node to a permissioned multi-node ledger (e.g. Hyperledger Fabric) with consensus
  • Store the master key in an HSM or managed KMS with rotation
  • Make two-factor mandatory for signing roles
  • Independent security audit before handling real ownership data