Skip to content

Commit f6c6da2

Browse files
authored
Merge pull request #1032 from Favourof/feat/issue-944-951-circuit-breaker-encryption
feat(backend): implement circuit breaker (#944) and data encryption a…
2 parents 3d5072e + 03dd224 commit f6c6da2

39 files changed

Lines changed: 2590 additions & 445 deletions

File tree

backend/CIRCUIT_BREAKER.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# External Service Circuit Breaker (Issue #944)
2+
3+
## Overview
4+
The External Service Circuit Breaker protects the ArenaX backend from cascading failures caused by outages, network timeouts, or degraded performance in third-party services (e.g. Stellar Horizon, Soroban RPC, external payment providers, webhooks).
5+
6+
## States & State Transitions
7+
8+
```mermaid
9+
stateDiagram-v2
10+
[*] --> Closed
11+
Closed --> Open: Failures >= threshold
12+
Open --> HalfOpen: Cooldown period elapsed
13+
HalfOpen --> Closed: Consecutive successes >= success_threshold
14+
HalfOpen --> Open: Trial request fails (exponential backoff scaled)
15+
```
16+
17+
1. **Closed**: Normal state. All requests are allowed through. Consecutive failures are tracked. When failures reach `failure_threshold` (default: 5), the circuit trips to `Open`.
18+
2. **Open**: Outage state. Incoming calls fail fast immediately with `503 Service Unavailable` and a `Retry-After` header without making any network calls to the external service.
19+
3. **HalfOpen**: Recovery state. Once the exponential backoff cooldown has elapsed, the circuit admits a limited number of trial probe requests (`half_open_probe_limit`, default: 1).
20+
- If the trial request succeeds and reaches `success_threshold` (default: 2), the circuit transitions back to `Closed` and resets the backoff exponent.
21+
- If the trial request fails, the circuit immediately returns to `Open` and scales the cooldown duration exponentially.
22+
23+
## Exponential Backoff Calculation
24+
25+
The cooldown period before transitioning from `Open` to `HalfOpen` grows exponentially with consecutive trip cycles:
26+
27+
$$\text{cooldown} = \min\left(\text{max\_backoff}, \; \text{initial\_backoff} \times \text{backoff\_multiplier}^{(\text{trip\_count} - 1)}\right)$$
28+
29+
### Default Settings
30+
- `failure_threshold`: 5 consecutive failures
31+
- `success_threshold`: 2 consecutive successes
32+
- `initial_backoff`: 5 seconds
33+
- `max_backoff`: 60 seconds
34+
- `backoff_multiplier`: 2.0
35+
- `half_open_probe_limit`: 1 concurrent probe
36+
- `request_timeout`: 10 seconds
37+
38+
## Usage
39+
40+
### 1. Service Registry Wrapper
41+
```rust
42+
use arenax_backend::middleware::circuit_breaker::CircuitBreakerRegistry;
43+
44+
let registry = CircuitBreakerRegistry::default();
45+
46+
// Execute outbound calls under the "stellar" circuit breaker
47+
let result = registry.call("stellar", || async {
48+
// Outbound HTTP request to Stellar Horizon
49+
reqwest::get("https://horizon.stellar.org/accounts/XYZ").await
50+
}).await;
51+
52+
match result {
53+
Ok(data) => println!("Success: {:?}", data),
54+
Err(CircuitBreakerError::CircuitOpen { service, retry_after_secs }) => {
55+
eprintln!("Fast failed: {} circuit is open, retry in {}s", service, retry_after_secs);
56+
}
57+
Err(CircuitBreakerError::Timeout(dur)) => {
58+
eprintln!("Call timed out after {:?}", dur);
59+
}
60+
Err(CircuitBreakerError::Inner(e)) => {
61+
eprintln!("External call failed: {:?}", e);
62+
}
63+
}
64+
```
65+
66+
### 2. Actix-Web Route Middleware
67+
```rust
68+
use actix_web::{web, App};
69+
use arenax_backend::middleware::circuit_breaker::{
70+
CircuitBreaker, CircuitBreakerConfig, ExternalCircuitBreakerMiddleware,
71+
};
72+
73+
let breaker = Arc::new(CircuitBreaker::new(
74+
CircuitBreakerConfig::new("proxy_service")
75+
.with_failure_threshold(3)
76+
.with_initial_backoff(Duration::from_secs(10)),
77+
));
78+
79+
App::new()
80+
.service(
81+
web::scope("/api/external")
82+
.wrap(ExternalCircuitBreakerMiddleware::new(breaker.clone()))
83+
.route("/proxy", web::get().to(proxy_handler))
84+
);
85+
```
86+
87+
## Metrics Export
88+
The circuit breaker publishes Prometheus metrics via the `/metrics` endpoint:
89+
90+
| Metric Name | Type | Labels | Description |
91+
|-------------|------|--------|-------------|
92+
| `circuit_breaker_state` | Gauge | `service` | Current state (0 = Closed, 1 = HalfOpen, 2 = Open) |
93+
| `circuit_breaker_requests_total` | Counter | `service`, `status` | Total requests (`success`, `failure`, `rejected`) |
94+
| `circuit_breaker_trips_total` | Counter | `service` | Total times circuit tripped Open |

backend/Cargo.lock

Lines changed: 100 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ futures = "0.3"
4141
futures-util = "0.3"
4242
hex = "0.4"
4343
fs2 = "0.4"
44+
aes-gcm = { version = "0.10", features = ["zeroize"] }
45+
zeroize = { version = "1.8", features = ["derive"] }
4446

4547
# Security: pin patched versions of transitive deps
4648
quinn-proto = "0.11.14"

backend/DATA_ENCRYPTION.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Data Encryption at Rest & Key Rotation (Issue #951)
2+
3+
## Overview
4+
ArenaX backend implements authenticated data encryption at rest for Personally Identifiable Information (PII) and sensitive records (phone numbers, emails, device fingerprints, secrets) using **AES-256-GCM** (NIST SP 800-38D).
5+
6+
## Key Features
7+
8+
1. **Authenticated Encryption (AEAD)**:
9+
- Uses AES-256-GCM providing both confidentiality and integrity/tamper detection.
10+
- Generates a unique, cryptographically random 96-bit nonce per encryption operation via `OsRng`.
11+
2. **Versioned Envelope Format**:
12+
- `enc:v<version>:<nonce_hex>$<ciphertext_and_tag_hex>`
13+
- Allows multi-version key rings to coexist without breaking older stored data.
14+
3. **Live Key Rotation**:
15+
- `KeyRing` supports multiple key versions (`v1`, `v2`, `v3`).
16+
- New encryptions always use the `active_version`.
17+
- Older ciphertext is transparently decrypted using the corresponding historical key in the key ring.
18+
- `reencrypt()` allows zero-downtime lazy or background batch migrations to the latest active key.
19+
4. **Transparent Decryption & PII Redaction**:
20+
- `EncryptedField<T>` wrapper implements `Serialize` and `Deserialize` with Serde.
21+
- `Debug` and `Display` implementations automatically redact plaintext (`"[REDACTED_PII]"`) to prevent accidental log leakage.
22+
- Explicit `.expose_secret()` or `.decrypt()` method ensures deliberate access.
23+
5. **Access Audit Trail**:
24+
- Every encryption, decryption, failed authentication, key rotation, and re-encryption emits a structured security audit event.
25+
- Captures timestamp, operation (`ENCRYPT`, `DECRYPT`, `KEY_ROTATION`, `REENCRYPT`, `DECRYPT_FAILED`), target field name, key version, actor ID, and operation duration in microseconds.
26+
6. **High Performance (<5% Overhead)**:
27+
- Microsecond-level AES-256-GCM execution (sub-10 µs per field).
28+
- Minimal allocations and hardware AES-NI acceleration where available.
29+
30+
## Architecture
31+
32+
```mermaid
33+
flowchart LR
34+
Plaintext[PII Plaintext] -->|AES-256-GCM + Nonce| Encrypt[PiiDataProtector.encrypt]
35+
Encrypt -->|enc:v1:nonce$ciphertext| Database[(PostgreSQL Storage)]
36+
Database -->|enc:v1:nonce$ciphertext| Decrypt[PiiDataProtector.decrypt]
37+
KeyRing[(KeyRing v1, v2)] -->|Lookup Key by Version| Decrypt
38+
Decrypt --> PlaintextOut[Plaintext PII]
39+
Decrypt --> Audit[EncryptionAuditLogger]
40+
Encrypt --> Audit
41+
```
42+
43+
## Key Rotation Workflow
44+
45+
1. Generate a new 256-bit encryption key.
46+
2. Register and promote the new key in the `KeyRing`:
47+
```rust
48+
let new_key = EncryptionKey::generate();
49+
key_ring.rotate_key("v2", new_key);
50+
```
51+
3. All new records will immediately be written under `v2`.
52+
4. Existing records stored under `v1` continue to be read transparently.
53+
5. (Optional) Run background re-encryption worker:
54+
```rust
55+
let updated_envelope = protector.reencrypt(&old_envelope, "user.email", Some("migration_worker"))?;
56+
```
57+
58+
## Configuration & Environment Variables
59+
60+
| Variable | Description |
61+
|----------|-------------|
62+
| `ENCRYPTION_KEY_V1` | 64-character hexadecimal representation of 256-bit key for version 1 |
63+
| `ENCRYPTION_KEY_V2` | 64-character hexadecimal representation of 256-bit key for version 2 |
64+
| `ENCRYPTION_ACTIVE_KEY_VERSION` | Version string of the active key (e.g. `v1`, `v2`) |

backend/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod metrics;
77
pub mod middleware;
88
pub mod models;
99
pub mod realtime;
10+
pub mod security;
1011
pub mod service;
1112
pub mod telemetry;
1213
pub mod transaction;

backend/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod models;
1414
mod realtime;
1515
mod service;
1616
mod orchestrator;
17+
mod security;
1718
mod telemetry;
1819
mod validators;
1920

0 commit comments

Comments
 (0)