technical ยท 6 min read
BOA QR Codes: Encrypted Receipts and How Decryption Works
Bank of Abyssinia QR codes use AES-256-CBC encryption, not URLs like CBE. Here's how the encryption works, what's inside the payload, and how cheki decrypts it for instant inter-bank verification.
Bank of Abyssinia takes a different approach to receipt QR codes than CBE. Instead of encoding a URL that points to a server-side receipt, BOA encrypts the full transaction data directly into the QR code. This means the QR code itself IS the receipt, not just a link to one.
Two approaches, same goal
CBE's QR codes encode a short URL (mbreciept.cbe.com.et/{id}) that redirects to server-side data. BOA's QR codes encode the transaction data itself, encrypted with AES-256-CBC. Both work, but they have different security trade-offs.
What the QR code contains
When you scan a BOA receipt QR code with a standard QR reader, you get a base64-encoded string like this:
3cHRaxVjn/pySpNXEHQE61JOQ2poZRMwnDHwMiX7YO9UVtJZT/ndmwHEzWkJoloEf4dIQIzJf5zmvbBo5qHTdm/23nc6NRzTSfxEjIHa7Ju4Ti+xydrVn8qF+9/OPAF5LIfMEvxFqZ6wlKMvSN/jrQ==This is not a URL. It's a base64-encoded ciphertext. When decoded and decrypted, it reveals a comma-separated string with 7 fields:
senderAccount,senderName,amount,reference,date,receiverAccount,receiverNameFor example, a real decrypted payload looks like:
1000370251685,EYOUEL ARAGAW HAILE,1107.59,FT252003JZPP,19/07/2025,1000370251685,MOHAMMED ABDULWASI RESHIDHow the encryption works
BOA uses the CryptoJS library for encryption. CryptoJS is a popular JavaScript encryption library. The encryption parameters are hardcoded in BOA's receipt web app, which is served from cs.bankofabyssinia.com/slip/assets/index-*.js. Here are the exact parameters:
| Parameter | Value | Purpose |
|---|---|---|
| Algorithm | AES-256-CBC | Symmetric encryption with 256-bit key and CBC mode |
| Password | ELqVy2g4pGWLUIKSa+1ijwpPy6eDxBFBLBPrJ24v/IA= | Base64-encoded passphrase for key derivation |
| Salt | salt | PBKDF2 salt (static string) |
| IV | 1234567890123456 | 16-byte initialization vector (static) |
| Iterations | 10000 | PBKDF2 key derivation iterations |
| Key length | 32 bytes (256 bits) | Derived key length for AES-256 |
| Hash function | SHA1 | PBKDF2 hash function for key derivation |
The decryption process works in 3 steps:
- Base64 decode the QR payload to get the raw ciphertext bytes
- Derive the AES key using PBKDF2 with the password, salt, 10000 iterations, SHA1, and 32-byte key length
- Decrypt the ciphertext using AES-256-CBC with the derived key and the static IV
The result is a UTF-8 string in CSV format with 7 comma-separated fields.
Why the key is public
BOA's receipt viewer is a client-side web app. When someone shares a receipt link, the recipient opens it in their browser and the JavaScript app decrypts the QR data locally to display the receipt. This means the decryption key must be embedded in the JavaScript that runs in the browser. Anyone who inspects the JS bundle can extract it.
Security implication
Because the key is public, anyone can create a valid-looking BOA QR payload from scratch. You could encrypt arbitrary transaction data with the same key and produce a QR code that cheki (or BOA's own app) would decrypt as 'verified.' This is a fundamental limitation of BOA's design, not a cheki issue. The same applies to any verification tool that decrypts BOA QR codes.
Forged QR risk for inter-bank
For normal BOA-to-BOA transfers, you can cross-check the QR data against BOA's JSON API. But for inter-bank transfers (BOA to CBE, Dashen, etc.), the API returns 'Invalid reference number,' so the QR code is the ONLY proof. Since the key is public, a forged QR code cannot be distinguished from a real one. Always verify inter-bank transfers with additional confirmation (bank statement, SMS alert, branch confirmation) for high-value transactions.
Inter-bank vs intra-bank: two verification paths
BOA has two distinct verification paths depending on whether the transfer stays within BOA or goes to another bank:
| Transfer type | API lookup | QR decryption | Recommended method |
|---|---|---|---|
| BOA to BOA (intra-bank) | Works (JSON API) | Works | API lookup (can cross-check) |
| BOA to CBE/Dashen/other (inter-bank) | Fails ('Invalid reference number') | Works | QR decryption (only option) |
When a customer sends money from BOA to CBE, the transaction reference starts with 'FT' (e.g., FT252003JZPP). BOA's online slip API at cs.bankofabyssinia.com does not recognize these references because the transfer left BOA's system. The QR code, however, was generated at the time of transfer and contains all the details.
How cheki handles BOA QR codes
cheki's unified scanner auto-detects BOA QR payloads. When you scan a QR code or upload a receipt image, cheki checks whether the decoded string matches BOA's encrypted format (base64 string with correct AES block alignment). If it does, cheki decrypts it server-side via the /api/verify endpoint and returns the parsed transaction data.
Scan or upload
Open cheki, tap the camera icon to scan the QR code on the receipt, or upload a screenshot/photo of the receipt. cheki's multi-scale QR detector finds QR codes even in full receipt screenshots.
Auto-detection
cheki checks if the decoded string is a BOA encrypted payload (base64, correct length, AES block-aligned). If yes, it routes to BOA QR decryption. If it's a URL, it routes to URL verification. If it's a CBE reference, it routes to CBE.
Server-side decryption
The encrypted payload is sent to cheki's /api/verify endpoint, which decrypts it server-side using Node.js crypto. This prevents client-side tampering where someone could modify the decryption logic in the browser.
Instant result
Decryption takes ~8ms server-side. The full transaction data (sender, receiver, amount, date, reference) appears in under 500ms including network round-trip.
No account number needed for QR
Unlike BOA's JSON API which requires the last 5 digits of the receiving account, QR-based verification needs no additional information. The QR code contains everything. Just scan and verify.
BOA QR vs CBE QR: a comparison
| Feature | BOA QR code | CBE QR code |
|---|---|---|
| Encoding | AES-256-CBC encrypted payload | Plain URL (mbreciept.cbe.com.et/{id}) |
| Contains transaction data | Yes (encrypted in QR) | No (data fetched from server) |
| Requires server call | No (can decrypt locally) | Yes (must call CBE API) |
| Account number needed | No | No (new system) / Yes (old system) |
| Inter-bank support | Yes (QR is the only option) | N/A (CBE receipts always work) |
| Forgery risk | High (key is public, can forge QR) | Low (server validates ID) |
| Speed | ~500ms (decrypt + network) | ~1-2s (API call + JSON parse) |
| Offline verification | Possible (decrypt locally) | Not possible (needs API) |
Verifying BOA QR codes via API
Send the QR payload in the qrData field. No reference or account number is needed:
curl -X POST https://chekiapp.vercel.app/api/verify \
-H "Content-Type: application/json" \
-d '{"bank":"boa","qrData":"3cHRaxVjn/pySpNXEHQE61JOQ2poZRMwnDHwMiX7YO9UVtJZT/ndmwHEzWkJoloEf4dIQIzJf5zmvbBo5qHTdm/23nc6NRzTSfxEjIHa7Ju4Ti+xydrVn8qF+9/OPAF5LIfMEvxFqZ6wlKMvSN/jrQ=="}'Response:
{
"verified": true,
"bank": "Bank of Abyssinia",
"senderName": "EYOUEL ARAGAW HAILE",
"senderAccount": "1000370251685",
"receiverName": "MOHAMMED ABDULWASI RESHID",
"receiverAccount": "1000370251685",
"amount": 1107.59,
"currency": "ETB",
"date": "19/07/2025",
"reference": "FT252003JZPP"
}Extracting the key from BOA's web app
The decryption key and parameters are in BOA's receipt viewer JavaScript bundle. Here's where to find them:
# The receipt viewer is at:
# https://cs.bankofabyssinia.com/slip/{receipt-id}
#
# The JavaScript bundle is loaded from:
# https://cs.bankofabyssinia.com/slip/assets/index-{hash}.js
#
# Search the bundle for CryptoJS.AES.decrypt to find:
# - The password string
# - The salt, IV, iterations, and hash function
# All are passed as literals to CryptoJS.AES.decrypt()Key may change
If BOA updates their receipt viewer JavaScript, the password or parameters could change. cheki monitors this and updates the parser when needed. If you're self-hosting, check for key rotation after BOA app updates.