# Bad Wallet Security Protocol (Revised)

**Client-Server Cryptographic Authentication Architecture**

*TECHNICAL WHITEPAPER — rev. 2*

---

## Changes from rev. 1

- **§2** Time window widened from 60s to 120s; clock skew handled via optional `/api/time` offset endpoint.
- **§3** PoW difficulty is now per-route and server-advertised, not hardcoded.
- **§4** Signing scheme pinned explicitly (raw ECDSA, DER-encoded, low-S, over `SHA256(payload)`). The same library (`acinq:bitcoin-kmp`) is used on client and server to eliminate math divergence.
- **§5** Gatekeeper gains an intra-window **replay cache** (step 3) keyed by `pubkey:timestamp:nonce`, preventing replay during the validity window that the timestamp alone cannot cover.

---

## Abstract

Securing backend API infrastructure for open-source, non-custodial cryptocurrency wallets presents a unique paradox. Because the client source code is public and the application allows anonymous usage without standard accounts, traditional security mechanisms like secret API keys, session tokens, or App Attestation are either vulnerable or misaligned with the Cypherpunk ethos.

This document outlines the security architecture for **Bad Wallet**. By leveraging the client's inherent mathematical capabilities — specifically `secp256k1` elliptic curve cryptography and Hashcash-style Proof of Work (PoW) — the architecture ensures zero-knowledge authentication, prevents network replay attacks, and provides Sybil resistance against DDoS attacks, all while remaining entirely open-source.

## 1. The Identity Layer: Cryptographic Derivation

In a non-custodial wallet, the user's master seed phrase is the ultimate source of truth. However, mixing the keys that secure Bitcoin funds with the keys that authenticate API requests is a critical security vulnerability. If an API identity key is compromised, funds must remain perfectly safe.

> **Hardened Derivation Path**
>
> Bad Wallet utilizes a strict, application-specific BIP-32 derivation path to generate the API Identity Key. This path is entirely isolated from standard BIP-44, BIP-84, or BIP-86 paths used for holding funds.

Upon wallet creation or restoration, the client derives a unique key pair at path `m/2026'/0'/0'`. The resulting public key acts as the user's pseudo-anonymous username for the backend. The private key is **not persisted separately** — it is derived on demand from the already-protected mnemonic at signing time and zeroed from memory immediately after use. This minimizes the at-rest attack surface: the API key inherits the security of the mnemonic rather than introducing a second storage location.

## 2. Request Authentication: The Anti-Replay Payload

To prevent malicious actors from intercepting a valid network request and re-submitting it to drain backend resources (e.g., Vertex AI tokens), every request is bound to a strict, time-sensitive payload.

Before initiating a connection (such as opening a Server-Sent Events stream for the AI Chat), the client constructs a standardized string containing the current epoch timestamp, the HTTP method, the endpoint path, and the derived public key.

### The Base Payload Structure

```
"{timestamp_in_millis}:{http_method}:{endpoint_path}:{pub_key_hex}"
```

The backend enforces a **120-second validity window**. If the server receives a request where `|server_time - timestamp|` exceeds 120 seconds, it instantly rejects the connection with a `401 Unauthorized`. A 5-second negative skew (future timestamps) is permitted to tolerate small forward drift.

### Clock Skew Tolerance

Mobile device clocks can drift or be user-manipulated. The client SHOULD, at app startup, call the unauthenticated `GET /api/time` endpoint once to measure the server-client offset:

```
offset_ms = server_time_ms - local_time_ms
```

All subsequent request timestamps are computed as `local_time_ms + offset_ms`. The `/api/time` endpoint is rate-limited by source IP (not by pubkey) and carries no other authority.

## 3. Sybil Resistance: Proof of Work (Hashcash)

Because the authentication protocol is open-source, an attacker could easily write a script to generate thousands of random key pairs, sign valid timestamp payloads, and launch a Distributed Denial of Service (DDoS) attack against the backend infrastructure.

To mitigate this without forcing users to pay Bitcoin network fees, Bad Wallet implements a localized Proof of Work mechanism. Before the payload is signed, the client device must "mine" a valid nonce.

### The Mining Protocol

The client appends a `nonce` (an incrementing integer) to the base payload and calculates the SHA-256 hash. The client continues to increment the nonce until the resulting hash begins with a predetermined target prefix of zeroes.

```
Attempt 1: SHA256("1713821388:GET:/api/chat/stream:...:0") -> e3b0c... (Fail)
Attempt 2: SHA256("1713821388:GET:/api/chat/stream:...:1") -> 8a2f1... (Fail)
...
Attempt N: SHA256("1713821388:GET:/api/chat/stream:...:N") -> 00007... (Success!)
```

### Per-Route Difficulty

Difficulty is **not** hardcoded. Each route declares its own PoW difficulty in the backend config, reflecting the cost of the downstream work:

| Route | Difficulty (leading hex zeroes) | Target avg attempts | Avg mobile time |
|---|---|---|---|
| `GET /api/time` | 0 (no PoW required) | — | — |
| `GET /api/chat/stream` | 4 (`0000`) | 2¹⁶ ≈ 65 k | 1–2 s |
| `POST /api/chat/feedback` | 3 (`000`) | 2¹² ≈ 4 k | <200 ms |

The client learns the current difficulty table at startup (bundled in the `/api/time` response). This allows difficulty to be tuned server-side without a client release, and prevents under-PoW'd Sybil floods on expensive routes.

For a modern mobile device, solving a 4-zero puzzle takes roughly 1 to 2 seconds — an acceptable UI delay masked by a "Securing connection..." animation. For a botnet attempting 10,000 requests per minute against `/api/chat/stream`, the computational cost becomes exponentially prohibitive.

## 4. The Cryptographic Signature

Once the valid nonce is found, the final, "solved" payload string is hashed and signed using the user's isolated `secp256k1` private key.

### Pinned Signing Scheme

To eliminate any ambiguity between client and server, the scheme is pinned:

- **Curve:** `secp256k1`
- **Hash:** `SHA-256` over the UTF-8 bytes of the solved payload string (single-hash, not double-hash).
- **Signature:** ECDSA, **DER-encoded**, with **low-S enforcement** (BIP-62 §5). Transmitted as hex.
- **Library:** `fr.acinq.bitcoin:bitcoin-kmp` on both client (KMP) and server (JVM). Using one library on both sides removes any risk of subtle divergence (e.g., Bitcoin-signed-message framing vs. raw ECDSA).

The client transmits the request to the Ktor backend, injecting the required parameters into the HTTP headers:

- `X-BadWallet-PubKey` — The public identifier (compressed 33-byte pubkey, hex).
- `X-BadWallet-Timestamp` — The request origin time (milliseconds since epoch).
- `X-BadWallet-Nonce` — The mined Proof of Work solution (decimal integer).
- `X-BadWallet-Signature` — The cryptographic proof of ownership (DER-encoded ECDSA sig, hex).

## 5. Backend Verification Pipeline

When the Ktor backend (hosted on Google Cloud Run) receives a request, it performs a lightweight, five-step verification process before any heavy logic or external API calls are executed. The pipeline drops invalid requests in fractions of a millisecond to preserve server capacity.

> **The Gatekeeper Logic**
>
> 1. **Time Check:** Is `|server_time − timestamp|` ≤ 120s (with ≤ 5s future skew allowed)?
> 2. **Proof Check:** Does `SHA256(payload_with_nonce)` start with the route's configured number of leading hex zeroes?
> 3. **Replay Check:** Has the tuple `(pubkey, timestamp, nonce)` been seen before? The backend maintains a short-TTL (≥ window + margin, e.g. 130s) set in Memorystore (Redis) keyed by a hash of that tuple. First-seen ⇒ claim the slot. Already-claimed ⇒ reject as replay.
> 4. **Signature Check:** Using `acinq:bitcoin-kmp`, is the DER-encoded, low-S ECDSA signature valid for `SHA256(payload)` under the supplied public key?
> 5. **Rate Limit:** Has this specific PubKey exceeded its token-bucket allowance? Bucket state lives in Redis so it is consistent across Cloud Run instances.

Failures at steps 1–4 return `401 Unauthorized`. Failures at step 5 return `429 Too Many Requests` with a `Retry-After` header.

## Conclusion

The Bad Wallet security architecture successfully defends the backend infrastructure without compromising on non-custodial principles or the transparency of open-source code. By combining hard-derived identity keys with a pinned signing scheme, computational friction (Hashcash), and shared replay/rate state, the system creates a resilient, Cypherpunk-aligned API layer capable of supporting advanced features like AI Chat and market data integration.
