A secure, offline-to-online payment simulation system involving three actors: Sender, Receiver, and Bank. It demonstrates hash-chained ledgers, ECDSA signatures, AES-GCM encryption, and ECDH key exchange for secure offline payments.
- Project Overview
- Quick Start
- Architecture & Concepts
- Setup & Configuration
- Implementation Details
- Testing & Debugging
- Future Roadmap
Offline Payment Gateway simulates a scenario where a Sender (Buyer) and Receiver (Merchant) exchange payments without an internet connection. The Receiver later syncs with a Bank for settlement validation.
- Offline-First: Sender and Receiver operate without active internet.
- Secure:
- Confidentiality: AES-256-GCM encryption for all transactions.
- Integrity: SHA-256 hash chains prevent ledger tampering.
- Authentication: ECDSA P-256 signatures prove identity.
- Key Exchange: ECDH P-256 for secure shared secrets.
- Auditability: Bank maintains a complete audit log of all settlements.
- Sender (Device 1): Creates and signs transactions (offline). Usage: Vite + JS.
- Receiver (Device 2): Verifies signatures and maintains a local ledger (offline). Usage: Vite + JS.
- Bank (Device 3): Validates ledgers and settles funds (online). Usage: Python (FastAPI) + PostgreSQL.
- Python 3.8+
- Node.js (for frontend development server)
- PostgreSQL (running on port 5432)
# Create database
createdb -U postgres payment_gateway
# Apply schema
psql -U postgres -d payment_gateway -f schema.sqlcd bank
python -m venv venv
# Windows:
venv\Scripts\activate
# Linux/Mac:
# source venv/bin/activate
pip install -r requirements.txt
cp env.sample .env
# Edit .env to set your DATABASE_URL
python run.py
# Server running at http://localhost:4000Terminal 1 (Sender):
cd sender
npm install
npm run dev
# Sender running at http://localhost:5173Terminal 2 (Receiver):
cd receiver
npm install
npm run dev
# Receiver running at http://localhost:5174- Get Bank Key:
curl http://localhost:4000/bank-public-key - Configure Receiver (
http://localhost:5174):- Import Bank Public Key.
- Export "Receiver Public Key" (save as JSON).
- Configure Sender (
http://localhost:5173):- Import "Receiver Public Key" (from previous step).
- Create Transaction (Sender):
- Enter Receiver ID & Amount -> "Create + Encrypt + Sign".
- Export encrypted JSON.
- Process Transaction (Receiver):
- Import encrypted JSON.
- Verify success message.
- Settle (Receiver -> Bank):
- Export "Encrypted Ledger".
- Send to Bank via API:
curl -X POST http://localhost:4000/settle-ledger -H "Content-Type: application/json" -d "@encrypted-ledger.json"
The user sees a simple "Pay" and "Receive" flow, but complex cryptography happens in the background.
- Sender calculates a hash of the transaction and signs it (ECDSA).
- Sender encrypts the transaction with a random AES key.
- Sender encrypts the AES key using the Receiver's Public Key (ECDH).
- Receiver decrypts the AES key using their Private Key.
- Receiver decrypts the transaction and verifies the Sender's signature.
- Receiver adds the transaction to a hash-chained ledger (like a blockchain).
- Bank verifies the entire chain and signatures before moving money.
- Sender -> Receiver: File Transfer or QR Code (Offline).
- Receiver -> Bank: API over Internet (Online).
- Digital Signature (ECDSA): Proves origin.
- Symmetric Encryption (AES-GCM): Protects data privacy.
- Asymmetric Key Exchange (ECDH): Securely shares encryption keys.
- Hash Chain: Ensures ledger immutability.
If the quick start failed, try these options for PostgreSQL:
Option 1: Using psql
psql -U postgres -c "CREATE DATABASE payment_gateway;"
psql -U postgres -d payment_gateway -f schema.sqlOption 2: Connection String
Modify bank/.env with your credentials:
DATABASE_URL=postgresql://username:password@localhost:5432/payment_gateway
The system relies on exchanging public keys before transactions can happen.
- Bank Identity:
- The Bank generates an ECDH keypair on startup (
bank/bank_keys.json). - Public key available at
/bank-public-key.
- The Bank generates an ECDH keypair on startup (
- Receiver Setup:
- Generates keys on first load.
- Must import Bank's Public Key to encrypt ledgers for the Bank.
- Must export their Public Key for the Sender.
- Sender Setup:
- Generates keys on first load.
- Must import Receiver's Public Key to encrypt transactions for the Receiver.
- Frontend: Vite, Vanilla JavaScript, Web Crypto API.
- Backend: Python 3.9+, FastAPI,
cryptographylibrary. - Database: PostgreSQL/TimescaleDB.
- Hashing: SHA-256
- Signing: ECDSA P-256
- Encryption: AES-256-GCM
- Key Exchange: ECDH P-256 with HKDF key derivation
Transaction JSON (Encrypted):
{
"encrypted_payload": "base64...",
"encrypted_aes_key": "base64...",
"iv": "base64...",
"sender_public_key": { "kty": "EC", ... },
"sender_ecdh_public_key": { "kty": "EC", ... }
}Ledger Entry:
{
"ledger_index": 0,
"transaction": { ...decrypted_txn... },
"hash": "sha256(prev_hash + txn_hash)",
"status": "verified"
}Cause: Old transaction format created before ECDH implementation. Fix:
- Go to Sender -> "Regenerate keypair".
- Import Receiver Public Key again.
- Create a NEW transaction.
Cause: Key mismatch (e.g., Receiver regenerated keys after Sender imported the old public key). Fix: Reshare keys.
- Receiver: Regenerate & Export Public Key.
- Sender: Import new Public Key.
- Sender: Create fresh transaction.
Cause: Incorrect password or DB name in .env.
Fix:
- Verify credentials with
psql -U <user> -d payment_gateway. - Ensure PostgreSQL service is running (
Get-Service postgresql*).
- Frontend: Check Browser Console (F12) and the in-app "Logs" tab.
- Backend: Check the terminal running
python run.py. - Database: Query the
audit_logstable:SELECT * FROM audit_logs ORDER BY created_at DESC;
- Real-world Network Test: Deploy Bank to cloud (e.g., Render/Heroku) and test with mobile devices on 4G.
- QR Code Scanning: Integrate a JS library to scan QR codes directly in the browser instead of file upload.
- User Accounts: Implement proper login/auth instead of purely local identity.
- P2P Sync: Allow offline peer-to-peer sync between Senders (e.g., splitting a bill).