Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 263 additions & 0 deletions AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Sage Wallet - Security Audit & Application Report

**Date:** February 10, 2026
**Version Audited:** 0.12.8 (commit 0b6a05fb)
**Repository:** https://github.com/xch-dev/sage
**Scope:** Full codebase review — Rust backend, TypeScript frontend, build configuration, dependencies

---

## Threat Model

Sage is a **native desktop/mobile application** built with Tauri v2. It runs on the user's own trusted device, not on a remote server. The security model relies on:

- **OS-level device protection:** Login passwords, biometric unlock, screen lock
- **Disk encryption:** FileVault (macOS), BitLocker (Windows), LUKS (Linux), iOS/Android device encryption
- **App sandboxing:** iOS and Android enforce per-app data isolation
- **Local-only IPC:** Tauri commands are only callable from the bundled frontend — not exposed to the network
- **Biometric auth on mobile:** WalletConnect signing operations prompt for fingerprint/face

This is consistent with how most desktop wallets operate (official Chia wallet, Electrum, MetaMask, etc.) — they rely on OS protection rather than application-level passwords. The findings below are assessed within this context.

---

## Executive Summary

Sage has strong security foundations: memory-safe Rust backend with `unsafe` denied, type-safe IPC via Specta, compile-time checked SQL queries, localhost-only RPC, and robust cryptographic library choices. The application follows industry-standard practices for a native wallet — OS-level protection is the primary security boundary, which is appropriate for a trusted-device application.

The main areas for improvement are defense-in-depth measures: adding a CSP policy (to guard against NFT metadata injection), sanitizing externally-sourced theme URLs, and adding `zeroize` to key material. These are hardening recommendations, not critical vulnerabilities in the context of a native app.

---

## 1. Findings Requiring Attention

### 1.1 Content Security Policy Disabled
**Severity:** MEDIUM
**Impact:** Reduced defense-in-depth against content injection

**Details:** In `src-tauri/tauri.conf.json:23`:
```json
"security": {
"csp": null
}
```

While the Tauri frontend loads only bundled local files (not remote content), a CSP would provide defense-in-depth against:
- Malicious NFT metadata containing script payloads rendered in the webview
- External resource loading from theme system (background images from NFT sources)
- Any future features that render user-supplied or blockchain-sourced content

**Recommendation:** Add a restrictive CSP that allows the bundled frontend resources and explicitly whitelists needed external domains (e.g., for NFT images, price APIs). This is low effort and follows Tauri's own security recommendations.

---

### 1.2 Theme Background Image URLs Not Sanitized
**Severity:** MEDIUM
**Files:** `src/components/Header.tsx`, `src/components/Layout.tsx`

Theme background images are applied directly as CSS:
```tsx
backgroundImage: `url(${currentTheme.backgroundImage})`
```

Since themes can be loaded from NFT metadata (external sources), a malicious theme could:
- Track users via a remote image URL (reveals IP address on theme load)
- Attempt CSS injection via crafted URL values

**Recommendation:** Validate and sanitize theme image URLs. Consider proxying external images through local storage or restricting to `data:` URIs and local files.

---

### 1.3 Debug Logging May Include Transaction Details
**Severity:** LOW
**File:** `crates/sage/src/endpoints/wallet_connect.rs:254`

```rust
debug!("{spend_bundle:?}");
```

Full spend bundle data is logged at debug level. In production builds, debug logging is disabled by default, making this a low risk. However, users running with `RUST_LOG=debug` could inadvertently write transaction details to log files.

**Recommendation:** Redact or summarize spend bundle data in debug logs, or use a separate `trace!` level.

---

### 1.4 No Memo Size Limits
**Severity:** LOW
**File:** `crates/sage/src/utils/parse.rs`

```rust
pub fn parse_memos(input: Vec<String>) -> Result<Vec<Bytes>> {
let mut memos = Vec::new();
for memo in input {
memos.push(Bytes::from(hex::decode(memo)?)); // No size limit
}
Ok(memos)
}
```

Extremely large memos could cause memory pressure. In practice, the blockchain itself limits transaction size, so this is defense-in-depth.

**Recommendation:** Add a reasonable memo size limit (e.g., 1MB per memo, 10MB total).

---

## 2. Design Observations (Not Vulnerabilities)

These are architectural observations that are **standard practice for native wallets** but worth documenting for completeness.

### 2.1 Keychain Encryption Uses Empty Password
**Status:** By design — optional user password is a planned feature

The keychain (`crates/sage-keychain/src/encrypt.rs`) encrypts private keys with AES-256-GCM + Argon2, but all call sites currently pass `b""` as the password. This means the encryption provides format-level protection (keys aren't stored in plaintext) but relies on OS-level protection for actual security.

This is the same model used by the official Chia wallet, Electrum, and most desktop cryptocurrency wallets. The password parameter in the API exists because the developer plans to add an option during wallet setup for users to set an encryption password for the key file. The infrastructure already supports this — only the call sites need to be updated to accept a user-provided password. See also PR #720 for secure-element integration.

---

### 2.2 Fingerprint-Only Login
**Status:** By design — appropriate for trusted device

The login mechanism requires only a wallet fingerprint (uint32) to select the active wallet. On a personal device, the user has already authenticated to the OS. Mobile platforms additionally have biometric auth for WalletConnect signing.

This is standard for desktop wallets — opening the Chia GUI or Electrum also doesn't require a separate password beyond OS login.

---

### 2.3 Default File Permissions
**Status:** Low risk on single-user systems

Files are created with default OS permissions. On typical single-user desktop/laptop systems, the default umask (0022) creates files readable only by the owner. Mobile platforms enforce app sandboxing. This is a concern only on shared multi-user systems with permissive umask settings.

**Optional enhancement:** On Unix, set `0o600` permissions on `keys.bin` for defense-in-depth. Low effort.

---

### 2.4 SQLite Not Encrypted at Rest
**Status:** By design — stores public blockchain data

The SQLite database stores transaction history, coin data, and addresses without application-level encryption. This data is public blockchain data, not secrets — encrypting it at the application level would not provide meaningful security benefit. Users concerned about data at rest can use OS-level disk encryption (FileVault, BitLocker, LUKS, iOS/Android device encryption).

Neither the official Chia reference wallet nor Goby (another Chia wallet) encrypts their local database. This is standard practice across the ecosystem.

---

### 2.5 Local IPC Without Rate Limiting
**Status:** Not exploitable in normal operation

Tauri IPC commands are only callable from the bundled frontend process. An attacker would need code execution on the device to inject calls, at which point rate limiting provides no meaningful protection (they could directly read `keys.bin` instead).

---

### 2.6 Self-Signed TLS for RPC
**Status:** Appropriate for localhost

The RPC server uses self-signed certificates and is bound exclusively to `127.0.0.1`. Self-signed certs are standard for localhost-only services where the identity of the server is not in question.

---

### 2.7 WalletConnect Project ID in Source
**Status:** Standard practice

The WalletConnect project ID is hardcoded, which is the normal pattern for WalletConnect integrations. Every WC-enabled wallet has a visible project ID. This is not a secret.

---

## 3. Positive Findings

### 3.1 Unsafe Code Denied
The workspace-level lint configuration denies all unsafe code:
```toml
unsafe_code = "deny"
```
No `unsafe` blocks were found in any crate. This is excellent practice for a financial application.

### 3.2 Strong Cryptographic Library Choices
- **AES-256-GCM** (aes-gcm 0.10.3) — authenticated encryption
- **Argon2** (argon2 0.5.3) — memory-hard KDF
- **Rustls with AWS LC RS** — FIPS-validated TLS
- **ChaCha20Rng** — cryptographically secure RNG for key operations
- **BIP39 2.0.0** — standard mnemonic implementation

### 3.3 RPC Server Bound to Localhost Only
```rust
let addr: SocketAddr = ([127, 0, 0, 1], app.config.rpc.port).into();
```

### 3.4 Type-Safe IPC via Specta
The Tauri IPC bridge uses Specta for compile-time TypeScript type generation, eliminating type mismatches between Rust and TypeScript. This prevents a class of serialization bugs.

### 3.5 SQL Injection Prevention
All database queries use SQLx's compile-time checked prepared statements. No string interpolation in SQL queries was found.

### 3.6 Comprehensive Error Type System
The `Error` enum in `crates/sage/src/error.rs` has 40+ specific variants, mapped to API-level `ErrorKind` categories. This prevents information leakage while providing useful error context.

### 3.7 Mobile Biometric Authentication
WalletConnect signing operations on mobile prompt for biometric authentication (fingerprint/face), providing an additional authorization layer for dApp interactions.

### 3.8 Encryption Infrastructure Ready for Passwords
The keychain module accepts a password parameter at every level — the infrastructure is already built for optional user-password support. The developer plans to add an option during wallet setup for users to set an encryption password, requiring only call-site changes to enable.

---

## 4. Application Information

### Version & Release Cadence
| Version | Date | Key Changes |
|---------|------|-------------|
| 0.12.8 | Feb 8, 2026 | Tab order fix, RPC actions, QR sharing, wallet switching |
| 0.12.7 | Dec 3, 2025 | Clawback finalization, balance bug fixes |
| 0.12.6 | Nov 8, 2025 | Theme updates, offers page, Hong Kong DID |
| 0.12.5 | Oct 16, 2025 | Arbor compatibility, NFT minter hashes, dialog backgrounds |
| 0.12.4 | Sep 30, 2025 | Derivation improvements, revocable CATs, key propagation |
| 0.12.3 | Sep 22, 2025 | Edition options, tooltips, theme loading, coin totals |
| 0.12.2 | Sep 19, 2025 | Major theming overhaul, Dexie swap, glass themes |

### Open Issues (as of Feb 10, 2026)
- **#737** Show spendable balance
- **#735** App upgrade overwrites user theme selection
- **#729** Reflections about synchronization
- **#727** Deep links as alternative to WalletConnect
- **#726** Import error messages need more specificity
- **#723** Wallet error on single-sided request-only offer
- **#704** Android keyboard closes during token search
- **#642** Upload offer short codes / QR support (bug)
- **#575** Try from int error (bug)

### Open Pull Requests
- **#720** Secure-element integration for vault support (since Dec 15, 2025)
- **#728** Scheme handler (since Jan 16, 2026)
- **#694** Webhooks (since Oct 5, 2025)

### Known Bug History
34 total bug-labeled issues tracked. Notable patterns:
- UI state synchronization issues (checkboxes, balances, theme loading)
- Offer system edge cases (options in offers, single-sided offers)
- Syncing reliability (timeouts, data loss during batch sync, double-spend on retry)
- Mobile-specific issues (keyboard auto-close, safe area handling)

### Dependency Summary
- **Rust workspace dependencies:** 60+ crates
- **npm dependencies:** 110+ packages
- **Key blockchain deps:** chia-wallet-sdk 0.33.0, chia_rs, clvm_rs
- **No known CVEs** found in current dependency versions (as of audit date)

---

## 5. Recommendations Summary

| Priority | Finding | Effort | Category |
|----------|---------|--------|----------|
| P1 | Add CSP to tauri.conf.json | Low | Defense-in-depth |
| P1 | Sanitize theme image URLs | Low | Defense-in-depth |
| P2 | Add zeroize to SecretKeyData | Low | Defense-in-depth |
| P2 | Guard debug logging of spend bundles | Low | Defense-in-depth |
| P2 | Add memo size limits | Low | Input validation |
| P3 | Optional user password for keychain | Medium | Planned feature |
| P3 | Set 0o600 on keys.bin (Unix) | Low | Defense-in-depth |
| P3 | Add integrity checking to keys.bin | Low | Defense-in-depth |

---

*This audit was performed through static code analysis only. Dynamic testing, fuzzing, and penetration testing were not in scope. Findings are assessed against a trusted-device threat model appropriate for native desktop/mobile wallet applications.*
106 changes: 106 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Sage Wallet - Project Reference

## Overview
Sage is a high-performance Chia blockchain light wallet (beta). It connects directly to peers or a trusted full node. Built with **Tauri v2** (Rust backend + React/TypeScript frontend). Supports desktop (Win/Mac/Linux), iOS (TestFlight), and Android.

**Repo:** xch-dev/sage | **License:** Apache-2.0 | **Version:** 0.12.8 | **Stars:** 52 | **Commits:** 2,839
**Primary maintainers:** Rigidity (~1,629 commits), dkackman (~1,162 commits)

## Tech Stack
- **Frontend:** React 18.3, TypeScript 5.9, Vite 7.3, Tailwind CSS 3.4, shadcn/ui (Radix), Zustand, React Router v6 (hash), Lingui (i18n), Framer Motion
- **Backend:** Rust 2024 edition, Tauri 2.10, Tokio, SQLite/SQLx, Axum (RPC), chia-wallet-sdk 0.33
- **Crypto:** AES-256-GCM + Argon2 (keychain), BLS signatures (chia_rs), BIP39 mnemonics, Rustls (TLS)
- **Type bridge:** Specta (Rust→TS codegen) → `src/bindings.ts` (63KB auto-generated)

## Architecture

### Rust Crates (11)
| Crate | Purpose |
|-------|---------|
| `sage` | Core business logic, endpoints, config orchestration |
| `sage-api` | API types, records, request/response definitions (Specta + OpenAPI) |
| `sage-wallet` | Wallet driver, sync manager, peer communication, **main test suite** |
| `sage-database` | SQLite abstraction, queries (SQLx with compile-time checked SQL) |
| `sage-config` | TOML config management (wallet, network, migration) |
| `sage-keychain` | Password-based key encryption (AES-GCM + Argon2), BIP39 |
| `sage-rpc` | Axum HTTP/TLS RPC server with OpenAPI docs |
| `sage-client` | RPC client library |
| `sage-cli` | CLI binary (`sage` command) |
| `sage-assets` | External data fetching (NFT metadata, prices, icons) |
| `tauri-plugin-sage` | Custom Tauri plugin (NFC, biometric, platform-specific) |

### Frontend Structure
```
src/
├── App.tsx # Router + 9 nested context providers
├── bindings.ts # Auto-generated Tauri IPC types (Specta)
├── state.ts # Zustand stores (wallet, offer, navigation)
├── components/ # 113 files
│ ├── ui/ # 26 shadcn primitives
│ ├── confirmations/ # 11 tx confirmation components
│ ├── dialogs/ # 8 modal dialogs
│ └── selectors/ # 5 asset/token selectors
├── pages/ # 31 route pages
├── contexts/ # 8 React contexts
├── hooks/ # 26 custom hooks
├── lib/ # Utilities (utils, themes, forms, exports)
├── walletconnect/ # WC v2 integration (commands, handler)
├── themes/ # Built-in themes (circuit, glass, xch, win95...)
└── locales/ # en-US, de-DE, zh-CN, es-MX (.po files)
```

### Key Patterns
- **Command/Event IPC:** Frontend calls Rust via `commands.*()`, receives updates via `events.syncEvent.listen()`
- **Error flow:** Rust `Error` → `ErrorKind` enum → frontend `ErrorContext` → toast/modal
- **State:** Zustand for app state, React Context for services (wallet, peer, price, WC, biometric)
- **Forms:** react-hook-form + Zod validation with custom amount/address validators
- **Tables:** TanStack React Table + React Virtual for large lists

## Important File Paths
- **Workspace config:** `Cargo.toml` (60+ workspace deps)
- **Tauri config:** `src-tauri/tauri.conf.json` (CSP is null!)
- **Endpoints:** `crates/sage/src/endpoints/` (keys, actions, data, offers, transactions, wallet_connect, settings, themes)
- **Sync engine:** `crates/sage-wallet/src/sync_manager.rs` (22KB)
- **Database schema:** `migrations/0001_setup.sql` (15.5KB) + 4 migration files
- **Keychain:** `crates/sage-keychain/src/encrypt.rs` (AES-GCM + Argon2)
- **Main struct:** `crates/sage/src/sage.rs` (Sage struct, initialization, DB setup)

## Build & Dev Commands
```bash
pnpm install # Install frontend deps
pnpm tauri dev # Dev mode
pnpm tauri dev --release # Optimized dev
pnpm tauri build # Production build
pnpm tauri ios dev # iOS simulator
pnpm tauri android dev # Android simulator
pnpm extract # Extract i18n strings
pnpm prettier # Format code
cargo sqlx prepare --workspace # Regenerate SQLx query cache
RUST_LOG=debug,sqlx=off cargo t -p sage-wallet # Run tests
```

## Code Quality Config
- **Rust lints:** `unsafe_code = deny`, `unwrap_used = warn`, `clippy::all = deny`
- **TS:** strict mode, noUnusedParameters, noFallthroughCasesInSwitch
- **CI:** Prettier check, Clippy, Cargo tests, cargo-machete, rustfmt
- **Platforms built:** macOS universal, Linux x64/arm64, Windows x64/arm64, iOS, Android

## Database
SQLite with WAL mode, foreign keys enabled. Key tables: coins, assets, nfts, dids, cats, collections, derivations, offers, transactions, mempool_items, blocks, files.

## Security Model (Trusted Device)
Sage runs on the user's own device — security relies on OS-level protection (login, disk encryption, app sandboxing), consistent with Chia GUI, Electrum, MetaMask, etc.

- **Keychain encryption uses empty password `b""`** — by design, developer plans to add optional user password during wallet setup (infrastructure already supports it)
- **CSP is `null`** in tauri.conf.json — should be set for defense-in-depth against NFT metadata injection (MEDIUM)
- **Theme image URLs not sanitized** — could leak IP via remote image in NFT-sourced themes (MEDIUM)
- **No file permission controls** on keys.bin — low risk on single-user systems, optional 0o600 enhancement
- **Fingerprint-only login** — standard for desktop wallets on trusted devices; mobile adds biometric auth for WalletConnect
- **PR #720 (open):** secure-element integration for vault support

## Current State
- Beta status, actively developed (~10 releases in 6 months)
- 9 open issues, 3 open PRs (secure-element, scheme handler, webhooks)
- No frontend tests exist; backend tests only in sage-wallet crate
- 4 supported languages (en-US, de-DE, zh-CN, es-MX)
- Theme system via NFT metadata (theme-o-rama library)
Loading