-
Notifications
You must be signed in to change notification settings - Fork 0
Security and Privacy
Being precise about scope matters more than the crypto primitives themselves:
-
What it protects against: someone who gains access to your disk — a stolen laptop, a leaked backup, a snapshot of
~/.config/aurisuploaded somewhere it shouldn't be. Without your passphrase, your API keys, portfolios, and chat sessions (when storage encryption is on) are AES-256-GCM ciphertext, not readable JSON. - What it does not protect against: a compromised running Auris process (if malware is reading process memory while you're using the app, the derived key is in RAM), or whatever your configured LLM/market providers themselves see — a message you send to Claude, Gemini, or FMP is visible to that provider under their own privacy policy, same as using their API directly. Local-first means your storage is private by default; it doesn't make third-party API calls private.
- Ollama is the one path that avoids the third-party-visibility question entirely — fully local inference, nothing leaves your machine for the LLM side of the conversation.
API keys are encrypted at rest with AES-256-GCM. The key is derived from your passphrase via Argon2id — the exact tuning (pkg/config/crypto.go):
| Parameter | Value |
|---|---|
| Time (iterations) | 1 |
| Memory | 64 MiB |
| Threads | 4 |
| Derived key length | 32 bytes (AES-256) |
| Salt | 16 bytes, cryptographically random |
| GCM nonce | 12 bytes |
A new random salt is generated on every Save for credential encryption — so the derived key material changes each time you save, even with the same passphrase. This is intentional and specific to credentials; it's not how portfolio/session encryption works (see below), and mixing the two up would be a bug, not a feature — which is exactly why they're two separate salt fields in the config.
Since FEAT-16, portfolios and chat sessions can optionally get the same AES-256-GCM treatment — on by default for new setups (opt-out, not opt-in; toggle it any time from the menu, /encryption). This reuses the exact same Argon2id tuning as credentials, but deliberately does not reuse the credential salt or its rotate-on-every-save behaviour:
-
AurisConfig.StorageSaltis generated once, when storage encryption is first enabled, and stays stable across every subsequent save. Regenerating it the way the credential salt regenerates would silently change the derived key underneath every already-encrypted portfolio/session file on the very next unrelated config save — a self-inflicted, silent data-loss bug. Keeping it stable is the whole point. - The active storage key lives in package-level in-memory state (
config.SetStorageKey/config.StorageKey), set on unlock and cleared on lock, rather than threaded as a parameter through every one of the many call sites that read/write portfolios and sessions across the TUI and agent tools.nilmeans "encryption off" — the previous, pre-FEAT-16 behaviour, unchanged. - Toggling encryption on/off, or changing your passphrase while it's enabled, synchronously re-encrypts every existing file immediately (
portfolio.ReencryptAllPortfolios/config.ReencryptAllSessions) rather than waiting for each file's next natural save. This is deliberately safe to retry: for each file, the routine tries the old key first, then the new key if that fails — so a batch interrupted partway through (crash, disk full) leaves some files already migrated and some not, and simply running it again picks up exactly where it left off, without corrupting anything already migrated.
Detecting whether a given file on disk is encrypted or legacy plaintext doesn't rely on "try to parse JSON and assume encryption if that fails" — it uses an explicit envelope format:
{"encrypted": true, "data": "..."}A legacy plaintext file simply doesn't have those keys, so it's detected as unencrypted and parsed directly — no separate migration step is needed, and encrypted and plaintext files can coexist on disk indefinitely (e.g., mid-toggle) without either code path getting confused.
Every write of config, session, portfolio, and export data goes through config.WriteFileAtomic — write to a temp file in the same directory, then os.Rename over the real path. os.Rename within the same filesystem is an atomic operation at the OS level, so a crash or full disk mid-write can only ever leave an orphaned .tmp file behind; the real target path is always either the old complete content or the new complete content, never a half-written, unrecoverable AES-GCM blob (a partial GCM ciphertext can't be authenticated — it's not "mostly readable," it's just gone).
Your financial profile (the 10-question risk/goals questionnaire from Using the Agent) is always stored as plaintext JSON, never encrypted, by explicit design — it isn't a credential, and keeping it plaintext means the app can read it (to build system prompts) without needing your passphrase unlocked first. This is a documented boundary, not an oversight: if you wouldn't want your risk tolerance and investment goals readable on disk, that's worth knowing going in.
| Data | Encrypted at rest? | Default |
|---|---|---|
| API keys / credentials | Always | — |
| Portfolios | Optional (AES-256-GCM) | On for new setups |
| Chat sessions | Optional (AES-256-GCM) | On for new setups |
| Financial profile | Never (plaintext JSON) | — |
See FAQ & Troubleshooting for what happens if you forget your passphrase.
For new users
For contributors
Under the hood