Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

NoInternetUpi

A simulation of an offline UPI payment. Three processes stand in for the three parties:

Process Role Internet
cmd/bank The bank. Holds the only copy of the RSA private key and the ledger. n/a — it is the server
cmd/node (Alice) Payer with a dead data connection. no
cmd/node (Bob) A stranger's phone in radio range. Relays packets. yes

Alice cannot reach the bank. She builds an encrypted payment packet and broadcasts it over UDP (standing in for Bluetooth/NFC). Bob picks it up and POSTs it to the bank. Bob is a courier, not a trusted party — he cannot read who is paying whom, or how much.

Everything below is runnable as written; no other document is needed.

Contents

How the packet keeps Bob out

Hybrid encryption, in crypto/hybrid.go:

  1. Alice generates a fresh 32-byte AES key, unique to this transaction.
  2. The payment details (sender_upi, receiver_upi, amount) are sealed with AES-GCM. GCM also authenticates, so flipping a single byte makes decryption fail rather than silently produce different numbers.
  3. That AES key is encrypted with the bank's RSA-2048 public key. Only the bank's private key can recover it.

What travels in the clear is only what a relay needs to do its job: an opaque transaction ID (for de-duplication) and a TTL (so stale packets die). See models/packet.go.

Prerequisites

Go 1.22 or newer. The SQLite driver is the pure-Go modernc.org/sqlite, so no C compiler is needed.

go version

If that fails, install Go and then open a new terminal so the PATH change takes effect:

winget install GoLang.Go

Then, from the repository root:

go mod download

The repo ships bank_public.pem / bank_private.pem, so there is nothing to generate. To make a fresh pair anyway:

go run ./cmd/keygen

Regenerating requires restarting the bank, and any packet built with the old public key will fail to decrypt.

Fast check: the automated tests

This verifies the crypto and every ledger rule (replay, double-spend, insufficient funds, unknown accounts) with no servers running. Good smoke test before a demo:

go test ./...
ok  github.com/Amitesh-exp/NoInternetUpi/bank
ok  github.com/Amitesh-exp/NoInternetUpi/crypto

Running the demo

Before an audience arrives

Compile everything once so nothing stalls mid-demo:

go build ./...

Four terminals

Run all four from the repository root — the bank reads bank_private.pem and the nodes read bank_public.pem from the working directory. Arrange the windows so all four are visible at once; the point lands visually.

Terminal 1 — the bank. -reset restores the demo balances and clears the replay log, so the demo is repeatable.

go run ./cmd/bank -reset
Demo balances reset to starting values
Bank server running on :8080
  alice@upi      1000.00
  bob@upi        500.00
  charlie@upi    750.00

Terminal 2 — Bob, the stranger who has internet.

go run ./cmd/node Bob true 9997

Terminal 3 — Charlie, another phone with no internet. Optional, but he shows that a relay without internet correctly declines to forward.

go run ./cmd/node Charlie false 9996

Terminal 4 — Alice, who wants to pay and has no signal.

go run ./cmd/node Alice false 9998

Each node needs its own terminal with a live keyboard. A node launched with its input piped or redirected drops the menu and stays a pure listener — useful for scripting, but not what you want for Alice.

Sending the payment

In Alice's terminal, choose 1 and answer:

Your UPI ID:      alice@upi
Receiver UPI ID:  bob@upi
Amount:           100

What to point at, in order

1. Alice — she broadcasts, and never talks to the bank:

Node has no internet — broadcasting to nearby nodes
Packet broadcast to 2 node(s): 69d43c75b6798841e5a74fbfd6814c53

2. Charlie — received it, and is a dead end:

[Charlie] Received packet: 69d43c75b6798841e5a74fbfd6814c53
[Charlie] No internet — cannot forward

3. Bob — carried a payment he cannot read:

[Bob] Received packet: 69d43c75b6798841e5a74fbfd6814c53
[Bob] Has internet — forwarding to bank
Payment forwarded successfully: payment successful
[Bob] Forward successful

4. The bank — settles it:

SETTLED 69d43c75b6798841e5a74fbfd6814c53: alice@upi -> bob@upi  100.00

Confirming the money actually moved

In Bob's terminal press 2 and enter alice@upi (expect 900), then again with bob@upi (expect 600).

Or query the bank directly:

curl.exe "http://localhost:8080/balance?upi=alice@upi"
curl.exe "http://localhost:8080/balance?upi=bob@upi"

In PowerShell use curl.exe, not curl — the bare name is an alias for Invoke-WebRequest. The PowerShell-native form is:

Invoke-RestMethod "http://localhost:8080/balance?upi=alice@upi"

Showing that Bob learns nothing

Add fmt.Printf("%+v\n", packet) to the handler in cmd/node/main.go. All Bob can print is the transaction ID, the TTL, and two opaque byte blobs — no UPI IDs, no amount.

Follow-ups an audience will ask for

"Could Bob read it, or change the amount?"

The node CLI only produces well-formed packets, so cmd/probe plays the part of a misbehaving client. With the bank running, in a fifth terminal:

go run ./cmd/probe all
valid        PASS   success=true message="payment successful"
tamper       PASS   success=false message="decryption failed — packet may be tampered"
expired      PASS   success=false message="packet expired — TTL exceeded"
replay/1st   PASS   success=true message="payment successful"
replay/2nd   PASS   success=true message="already processed"
  • tamper flips one bit of ciphertext, proving a relay cannot rewrite the amount without the bank noticing.
  • expired sends a packet whose TTL is already in the past.
  • replay sends the identical packet twice; the money moves once.

Individual cases and options: go run ./cmd/probe -amount 5 tamper, go run ./cmd/probe -to nobody@upi valid. It exits non-zero if any case misbehaves, so it also works in CI.

"What if two people relay the same payment?"

Stop Charlie (Ctrl+C) and restart him with internet:

go run ./cmd/node Dave true 9996

Send again from Alice. Both Bob and Dave forward the identical packet, but the bank prints SETTLED once and REPLAY for the other:

SETTLED f502b4d3bbd6b793fc27dd22b8730232: alice@upi -> bob@upi  25.00
REPLAY  f502b4d3bbd6b793fc27dd22b8730232: already processed

The balance drops by one payment, not two. The transaction ID is recorded in the same SQL transaction that moves the money, so two relays arriving at the same instant cannot both settle.

"What if she doesn't have the money?"

Send 99999 from alice@upi. Bob reports bank rejected payment: insufficient balance, and no balance changes.

"What if she typos the receiver?"

Send to nobody@upi. Bob reports bank rejected payment: receiver account not found, and Alice is not debited.

Resetting between runs

Ctrl+C the bank and restart it with -reset. Balances return to 1000 / 500 / 750 and the replay log clears, so the whole demo can be run again — including reusing transaction IDs.

go run ./cmd/bank -reset

Piping input instead of typing

The node CLI reads plain lines from stdin, so a run can be scripted. End with menu choice 3 to exit; if stdin simply runs out, the node keeps listening as a relay (which is what you want when starting Bob with no terminal attached).

In bash:

printf '1\nalice@upi\nbob@upi\n100\n3\n' | go run ./cmd/node Alice false 9998

In PowerShell, feed the lines as an array so no escape characters are needed:

@('1','alice@upi','bob@upi','100','3') | go run ./cmd/node Alice false 9998

Bank HTTP API

POST /pay — body {"packet": {...}}, replies {"success":bool,"message":string}.

GET /balance?upi=alice@upi — replies {"upi":string,"balance":number}.

Flags: -addr (default :8080), -db (default bank.db), -key (default bank_private.pem), -reset.

Node arguments

go run ./cmd/node <node-name> <has-internet> <port>

has-internet is true or false. port must be one of the ports in KnownNodes (9998, 9997, 9996) for other nodes to reach it.

Known limitations

Deliberate simplifications, not bugs — worth knowing before anyone mistakes this for a design. Better said out loud in a demo than waited on.

  • No sender authentication. The packet is encrypted to the bank but not signed by Alice. Anyone holding bank_public.pem — which every node has by design — can mint a packet that debits alice@upi. A real system would give each user a keypair, sign the payload, and have the bank verify against a registered public key. This is the largest gap.
  • bank_private.pem is committed to the repository. Fine for a demo, catastrophic anywhere else.
  • Balances are float64. Real money should be integer minor units to avoid rounding drift.
  • Single-hop relaying. A node that receives a packet and has no internet drops it rather than re-broadcasting. Multi-hop would need a hop count and loop suppression.
  • No offline balance check. Alice cannot know she has funds until the bank settles, so an offline payment can be rejected after the fact. Real designs use pre-authorised spending limits.
  • KnownNodes is a hardcoded port list in node/broadcast.go, standing in for "whoever is within radio range".

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages