Version: 1.0
Last Updated: November 24, 2025
Classification: Public
- Security Principles
- Threat Model
- Cryptographic Architecture
- Key Management
- Wallet Security
- Network Security
- Data Protection
- Incident Response
- Security Checklist
CompoundChat is built on these core security principles:
Multiple layers of security controls protect user assets:
- Application layer: Input validation, rate limiting
- Transport layer: TLS 1.3+, certificate pinning
- Data layer: Encryption at rest, encrypted backups
- Access layer: Authentication, authorization, audit logging
Every component has minimal access required:
- Webhook server: Read-only database access
- Background workers: Specific table permissions
- Admins: No access to encrypted private keys
Never trust, always verify:
- Validate all inputs (including from database)
- Verify webhook signatures on every request
- Check transaction status on-chain, not just RPC response
- Assume any component can be compromised
All error conditions default to safe states:
- Failed decryption → Abort operation, alert admin
- Invalid signature → Reject request, log incident
- RPC timeout → Queue retry, notify user
Users always control their funds:
- Non-custodial wallets (users own private keys)
- Recovery via mnemonic phrase (no backdoors)
- Transparent operations (all transactions on-chain)
-
User private keys (Critical)
- Loss = permanent loss of user funds
- Compromise = theft of user funds
-
User mnemonic phrases (Critical)
- Loss = inability to recover wallet
- Compromise = theft of current + future funds
-
User funds on Compound (Critical)
- Loss = direct financial harm
- Theft = reputational damage, legal liability
-
User personal data (High)
- Phone numbers, transaction history
- Breach = privacy violation, regulatory issues
-
Service availability (Medium)
- Downtime = poor UX, lost growth
- DDoS = service disruption
Motivation: Financial gain
Capabilities: Moderate to high technical skills
Attack Vectors:
- Webhook exploitation
- Database breach
- Man-in-the-middle attacks
- Social engineering users
- DDoS attacks
Motivation: Financial gain, sabotage
Capabilities: High (system access)
Attack Vectors:
- Database access to encrypted keys
- Code modifications (backdoors)
- Credential theft
Motivation: Surveillance, disruption
Capabilities: Very high
Attack Vectors:
- Network surveillance
- Supply chain attacks
- Zero-day exploits
Motivation: Financial gain (low effort)
Capabilities: Low
Attack Vectors:
- Phishing users
- Automated scanning for vulnerabilities
- Credential stuffing
Attack: Attacker gains read access to PostgreSQL database
Impact WITHOUT mitigation:
- Access to all encrypted private keys
- Access to user phone numbers
- Transaction history
Mitigation:
✅ Private keys encrypted with AES-256-GCM
✅ Master key stored in AWS KMS (not in database)
✅ Per-user salt prevents rainbow tables
✅ Phone numbers can be hashed (future enhancement)
✅ Database access requires VPN + MFA
Result: Attacker gets encrypted data only, cannot decrypt without KMS access
Attack: Attacker sends malicious messages to webhook endpoint
Impact WITHOUT mitigation:
- Trigger unauthorized transactions
- Drain user funds
- Crash service (DoS)
Mitigation:
✅ Signature verification (HMAC-SHA256)
✅ Timestamp validation (replay prevention)
✅ Rate limiting (10 req/min per user)
✅ Input validation (Joi schemas)
✅ Transaction requires user confirmation
Result: Malicious messages rejected at webhook layer
Attack: Attacker intercepts WhatsApp messages
Impact WITHOUT mitigation:
- Read sensitive data (mnemonics)
- Modify transaction parameters
- Steal session tokens
Mitigation:
✅ WhatsApp uses E2EE (TLS for API calls)
✅ Sensitive data (mnemonics) sent only once, then deleted
✅ No session tokens (stateless where possible)
✅ Transaction parameters verified on-chain
Result: Limited exposure; mnemonic at risk only during initial send
Attack: Attacker tricks user into revealing mnemonic
Impact:
- Full wallet compromise
- Irreversible fund theft
Mitigation:
✅ User education: "Never share your recovery phrase"
✅ Bot never asks for mnemonic
✅ Warning messages on wallet creation
Result: Reduced risk through education, but not eliminated
Attack: Bug in Compound V3 contracts exploited
Impact:
- Loss of user funds on Compound
- Reputational damage
Mitigation:
✅ Use only audited Compound V3 contracts
✅ No custom smart contracts (use standard patterns)
✅ Monitor Compound governance for critical updates
✅ Emergency withdrawal feature (future)
✅ Insurance fund for losses (future)
Result: Risk minimized by using battle-tested protocols
Algorithm: AES-256-GCM (Authenticated Encryption)
Key Size: 256 bits
IV Size: 96 bits (random, unique per encryption)
Tag Size: 128 bits (authentication tag)
Key Derivation: HKDF-SHA256
Inputs:
- plaintext: User's private key (32 bytes)
- key: Derived from user phone + master key
- iv: Random 96-bit value
- aad: Additional authenticated data (user ID)
Outputs:
- ciphertext: Encrypted private key
- tag: Authentication tag (prevents tampering)Why AES-256-GCM?
- NIST-approved, FIPS 140-2 compliant
- Authenticated encryption (integrity + confidentiality)
- Fast on modern hardware (AES-NI support)
- Resistant to timing attacks
Function: HKDF (HMAC-based KDF)
Hash: SHA-256
Input Key Material (IKM): Master key from KMS
Salt: Random 32-byte value (unique per user)
Info: User ID + "compoundchat-wallet-v1"
Output Length: 32 bytes (256 bits)
Derived Key = HKDF-SHA256(
ikm = masterKey,
salt = userSalt,
info = userId + version,
length = 32
)Why HKDF?
- Cryptographically sound key derivation
- Prevents key reuse across users
- Salt ensures unique keys even with same master
- Info parameter binds key to specific use case
Entropy: 256 bits (cryptographically secure random)
Checksum: 8 bits (SHA-256 of entropy)
Mnemonic: 24 words (from BIP39 wordlist)
Language: English (standardized)
Security Properties:
- 2^256 possible mnemonics (practically unbreakable)
- Checksum detects typos/corruption
- Standardized across wallets (MetaMask compatible)Derivation Path: m/44'/60'/0'/0/0
m = master key
44' = BIP44 standard
60' = Ethereum (coin type)
0' = Account 0
0 = External chain
0 = First address
Algorithm: HMAC-SHA512 for child key derivation
Hardened Keys: Yes (apostrophe indicates hardened)Why BIP44?
- Industry standard (compatible with all major wallets)
- Hierarchical structure allows multiple accounts
- Hardened derivation prevents parent key extraction
// Prove: "I have at least X tokens" without revealing actual balance
Proof System: Groth16
Curve: BN254 (Ethereum-friendly)
Circuit Language: Circom 2.0
Public Inputs:
- requiredAmount (what user wants to supply)
- balanceCommitment (Poseidon hash of actual balance)
Private Inputs:
- actualBalance (secret)
Constraints:
1. actualBalance >= requiredAmount
2. Poseidon(actualBalance) == balanceCommitment
Proof Size: ~200 bytes
Generation Time: <500ms (on mobile)
Verification Time: <50ms (on server)Why Groth16?
- Smallest proof size (good for WhatsApp bandwidth)
- Fast verification (constant time)
- Ethereum-compatible (for future on-chain use)
Storage: AWS Key Management Service (KMS)
Access: IAM role with strict permissions
Rotation: Quarterly (automated)
Backup: Multi-region replication
Security Controls:
- ✅ Never stored in code or environment files
- ✅ Never logged (even in encrypted form)
- ✅ Accessed only via AWS SDK with IAM auth
- ✅ Audit logging (CloudTrail)
- ✅ Automatic rotation with version tracking
Derivation: HKDF from master key + user salt
Storage: Salt stored in database, key derived on-demand
Lifecycle: Generated on wallet creation, never changed
Properties:
- Unique per user (even with same master key)
- Reproducible (same inputs → same output)
- Cannot reverse-engineer master key from derived key
AWS KMS Master Key
↓
Master Encryption Key (MEK)
↓
HKDF(MEK, userSalt, userId)
↓
User-Specific Key (USK)
↓
AES-256-GCM(privateKey, USK)
↓
Encrypted Private Key (stored in DB)
When to Rotate:
- Quarterly (scheduled)
- After suspected compromise
- Major security update
- Team member departure (if they had KMS access)
Rotation Steps:
- Generate new master key in KMS
- Re-encrypt all private keys with new key
- Update key version in database
- Verify all wallets still accessible
- Archive old key (for disaster recovery)
- Monitor for decryption errors
Downtime: None (rolling update)
1. User sends "create wallet"
↓
2. Generate 256-bit entropy (crypto.randomBytes)
↓
3. Create BIP39 mnemonic (24 words)
↓
4. Derive HD wallet (BIP44 path)
↓
5. Extract private key + address
↓
6. Generate random salt (32 bytes)
↓
7. Derive user key: HKDF(masterKey, salt, userId)
↓
8. Encrypt private key: AES-GCM(privateKey, userKey)
↓
9. Store: address, encryptedKey, salt, iv (NO mnemonic!)
↓
10. Send mnemonic to user (E2EE via WhatsApp)
↓
11. Zero out mnemonic from memory (overwrite buffer)
↓
12. User confirms backup
↓
13. Wallet readySecurity Critical Steps:
- Entropy: MUST use
crypto.randomBytes(notMath.random) - Mnemonic: NEVER stored in database (not even encrypted)
- Memory: Zero out buffers after encryption
- Transmission: Mnemonic sent exactly once, then deleted
1. User sends "supply 100 USDC"
↓
2. Parse and validate command
↓
3. Fetch encrypted private key from DB
↓
4. Derive user key (same HKDF as creation)
↓
5. Decrypt private key (AES-GCM)
↓
6. Verify decryption (check auth tag)
↓
7. Create ethers.Wallet(privateKey) IN MEMORY
↓
8. Build transaction (supply call to Comet)
↓
9. Sign transaction (in-memory only)
↓
10. Zero out private key from memory
↓
11. Broadcast signed transaction
↓
12. Monitor transaction status
↓
13. Confirm to userSecurity Critical Steps:
- Decryption: Verify GCM auth tag (prevents tampering)
- Memory: Private key exists in memory only during signing
- Cleanup: Overwrite memory after signing (prevent leaks)
- Validation: Check transaction params match user intent
// BAD: Private key lingers in memory
const privateKey = decryptPrivateKey(encrypted);
const wallet = new ethers.Wallet(privateKey);
const tx = await wallet.sendTransaction(...);
// privateKey still in memory!
// GOOD: Explicit cleanup
const privateKey = decryptPrivateKey(encrypted);
try {
const wallet = new ethers.Wallet(privateKey);
const tx = await wallet.sendTransaction(...);
} finally {
// Zero out private key (overwrite memory)
if (typeof privateKey === 'string') {
privateKey = '0'.repeat(privateKey.length);
}
// Let GC clean up
}Protocol: TLS 1.3 (minimum TLS 1.2)
Cipher Suites:
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
Certificate: Let's Encrypt (auto-renewal)
HSTS: Enabled (max-age=31536000)
Certificate Pinning: Future enhancement
// WhatsApp webhook signature format
Header: X-Hub-Signature-256
Value: sha256=<hex_signature>
Verification:
const signature = hmac_sha256(
key: WEBHOOK_SECRET,
data: request.body (raw bytes)
);
if (signature !== header_signature) {
throw new WebhookError('INVALID_SIGNATURE');
}Per-User Limits:
- 10 requests / minute (normal operations)
- 1 wallet creation / hour (prevent abuse)
- 5 transactions / minute (prevent spam)
Global Limits:
- 1,000 requests / minute (total)
- Circuit breaker at 5,000 req/min (DDoS protection)
Implementation: Redis (sliding window)
Layers:
- CloudFlare (L3/L4 DDoS protection)
- AWS Shield (infrastructure protection)
- Application rate limiting (L7)
- Circuit breakers (automatic shutdown on attack)
| Data Type | Classification | Encryption | Retention |
|---|---|---|---|
| Private keys | Critical | AES-256-GCM | Indefinite |
| Mnemonics | Critical | Never stored | N/A |
| User phone | Sensitive | Plaintext* | 36 months |
| Tx history | Sensitive | Plaintext | 36 months |
| Session data | Transient | N/A | 24 hours |
*Future: Hash phone numbers for privacy
Database: PostgreSQL with encrypted volumes (AWS RDS)
Backups: Encrypted with separate KMS key
Logs: Sensitive fields redacted before writing
Active Users:
- Wallet data: Retained while account active
- Transaction history: 36 months (regulatory)
- Session data: 24 hours (Redis TTL)
Deleted Accounts:
- Private keys: Deleted immediately
- Transaction history: Anonymized after 90 days
- Phone numbers: Deleted after 90 days
P0 - Critical (Response: Immediate)
- Private key leak
- Master key compromise
- Active exploitation of vulnerability
- Data breach (PII exposure)
P1 - High (Response: <1 hour)
- Vulnerability discovered (unpatched)
- Suspicious admin activity
- Failed decryption spike (possible attack)
P2 - Medium (Response: <24 hours)
- DoS attack (service degraded)
- Unauthorized access attempt (blocked)
P3 - Low (Response: <1 week)
- Security misconfiguration (no exploit)
- Deprecated dependency
-
Immediate (0-5 minutes):
- Shut down affected service
- Rotate master encryption key
- Alert all team members
-
Short-term (5-30 minutes):
- Identify affected users
- Assess if funds are at risk
- Prepare user notification
-
Recovery (30 min - 24 hours):
- Help users create new wallets
- Guide fund transfers to new wallets
- Root cause analysis
-
Post-incident (1-7 days):
- Security audit of entire system
- Implement additional controls
- Public disclosure (if appropriate)
-
Immediate:
- Isolate database
- Verify encryption (keys still secure?)
- Change all passwords
-
Assessment:
- What data was accessed?
- Encrypted keys = low risk (master key in KMS)
- User phone numbers = privacy breach
-
Notification:
- Inform affected users within 72 hours (GDPR)
- Explain what data was exposed
- Steps users should take (if any)
Code Security:
- No private keys in code (scan with
git secrets) - No secrets in .env (use .env.example template)
- All user inputs validated (Joi schemas)
- SQL queries parameterized (no string concat)
- Crypto libraries up-to-date (
npm audit) - Dependencies scanned (Snyk)
Cryptography:
- AES-256-GCM implemented correctly
- Master key in KMS (not env variables)
- Random values use
crypto.randomBytes - Private keys zeroed after use
- Mnemonic never stored in database
Network:
- HTTPS enforced (no HTTP)
- Webhook signature verification enabled
- Rate limiting active
- CORS configured properly
- Helmet.js security headers
Database:
- Private keys encrypted at rest
- Database backups encrypted
- Access requires VPN (production)
- Least privilege permissions
Testing:
- 85%+ coverage on security code
- Penetration testing completed
- Fuzzing on input validation
- Load testing (100+ concurrent users)
Operations:
- Monitoring configured (Sentry)
- Alerts set up (PagerDuty)
- Incident response plan documented
- Team security training completed
Monthly:
- Review access logs
- Check for unusual patterns
- Update dependencies
- Review rate limit logs
Quarterly:
- Rotate master encryption key
- Security audit
- Penetration testing
- Review and update threat model
Annually:
- Third-party security audit
- Disaster recovery drill
- Update security documentation
- Team security refresher training
Security Issues: security@compoundchat.xyz
PGP Key: [To be published]
Bug Bounty: [Future: HackerOne program]
Document Owner: Beacher (Security Lead)
Review Cadence: Quarterly
Next Review: February 24, 2026