Skip to content
Merged
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
40 changes: 40 additions & 0 deletions docs/security/keychain-secretstore-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Secret Key Keychain Migration

## Goal

Move the `SecretStore` master encryption key out of `.secret_key` files and into the OS keychain for real app environments.

Accepted exception:
- unit tests and explicit debug overrides may keep using file-backed storage.

## Current State

- Auth/provider credentials already prefer the keychain when available.
- `SecretStore` still keeps its master key in `{openhuman_dir}/.secret_key`.
- `keyring` currently falls back to a file backend in some non-test environments.

## Target State

- `dev`, `staging`, and `prod` use the OS keychain for the `SecretStore` master key.
- Existing ciphertext stays on disk unchanged.
- Existing `.secret_key` files are migrated into the keychain and then deleted only after verification.
- Unit tests continue to work without depending on the host OS keychain.

## Migration Plan

1. Change keyring backend selection so `cfg(test)` keeps the file backend, but normal app environments default to the OS backend unless explicitly overridden.
2. Teach `SecretStore` to:
- derive a stable keychain namespace from the user/openhuman directory
- migrate an existing `.secret_key` file into the keychain
- create the key in keychain when no legacy file exists
- fall back safely when keychain is unavailable
3. Add tests for:
- legacy `.secret_key` migration
- post-migration decrypt compatibility
- unit-test file-backed behavior

## Constraints

- Never re-encrypt existing payloads unless the ciphertext format itself changes.
- Never delete `.secret_key` until the keychain write is verified.
- Keep an explicit override path for debugging and recovery.
1 change: 1 addition & 0 deletions gitbooks/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* [System & Utilities](features/native-tools/system-and-utilities.md)
* [Subconscious Loop](features/subconscious.md)
* [Privacy & Security](features/privacy-and-security.md)
* [OS Keyring & Secret Storage](features/os-keyring-and-secret-storage.md)
* [Platform & Availability](features/platform.md)
* [Cloud Deploy](features/cloud-deploy.md)

Expand Down
116 changes: 116 additions & 0 deletions gitbooks/features/os-keyring-and-secret-storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
icon: key
---

# OS Keyring & Secret Storage

OpenHuman uses the **operating system's secure credential store** to protect the secrets that must live on your device.

On desktop builds, that means:

* **macOS:** Keychain
* **Windows:** Credential Manager
* **Linux:** Secret Service / libsecret

This is the root of trust for local secret material. OpenHuman does not rely on a plaintext `.env` file or a plaintext local config file for user credentials.

***

## What goes into the OS keyring

OpenHuman uses the OS keyring for two kinds of local secret material:

### 1. Credential entries

When a feature needs a local credential slot, OpenHuman stores it in the platform keyring rather than writing the raw secret into a normal config file.

Examples include:

* locally stored provider API keys
* session and bearer tokens that must remain on-device
* wallet secret material where applicable

These entries are scoped under OpenHuman's own key namespace so they do not collide with unrelated apps.

### 2. The master encryption key

Some sensitive values still need to live **inside local files** because the application configuration itself is file-based.

OpenHuman handles that by splitting storage in two:

* the **secret value on disk** is stored as encrypted ciphertext
* the **master key used to decrypt it** lives in the OS keyring

This means your local config and state files can contain encrypted values without the decryption key sitting beside them in plaintext.

***

## What stays encrypted on disk

When OpenHuman needs to persist sensitive application settings locally, it writes the **ciphertext** to disk and keeps the key in the OS keyring.

That covers local secrets such as:

* BYO API keys for supported providers
* channel and webhook secrets stored in local config
* other locally persisted secret settings required for desktop features

The encryption format is authenticated, so OpenHuman can detect tampering instead of silently accepting modified ciphertext.

In practice, the security model is:

* **key in keyring**
* **ciphertext in file**
* **plaintext only in memory when needed**

***

## Why this is better than plaintext config

If your machine has a local workspace backup, sync folder, or support bundle, plaintext secrets in config files are a liability.

Using the OS keyring as the root secret store gives OpenHuman a safer split:

* config files can be copied without exposing raw credentials
* accidental log or file inspection is less likely to reveal secrets
* the decryption key is delegated to the platform's credential system rather than to an app-managed plaintext file

This is not a replacement for full-disk encryption or OS account security. It is a narrower, stronger way to handle application secrets.

***

## Managed integrations vs local secrets

Not every secret follows the same path.

### Managed integrations

For the default managed integration flow, third-party OAuth tokens are handled by the OpenHuman backend. Your local app does **not** need to persist those provider tokens in plaintext on your machine.

### Local BYO credentials

When you choose a bring-your-own-key or direct-mode path, OpenHuman treats those credentials as **local secrets** and protects them using the OS keyring plus encrypted-at-rest local storage where needed.

***

## Migration from older installs

Older versions could keep local encryption material in a file-based form.

Current desktop builds migrate that material into the OS keyring and keep the encrypted payloads on disk. The goal is to move the root secret out of ordinary files and into the platform credential store, without requiring users to re-enter every secret by hand.

***

## Platform note

This page describes **desktop** OpenHuman: the Tauri app on macOS, Windows, and Linux.

In development and test environments, the repository may use test-specific overrides so automated runs do not depend on an interactive OS keychain. That is a developer convenience, not the end-user desktop security model.

***

## See also

* [Privacy & Security](privacy-and-security.md)
* [Third-party Integrations](integrations/README.md)
* [Local AI (optional)](model-routing/local-ai.md)
4 changes: 3 additions & 1 deletion gitbooks/features/privacy-and-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ OpenHuman is designed so that the **memory of your life lives on your machine**.

**Integration tokens are held by the backend, not on your laptop.** OAuth tokens are never written to disk in plaintext on your device. The OpenHuman backend brokers each integration request, the core never speaks any third-party API directly.

**OS-level credential storage.** Sensitive tokens are stored in your platform's secure keychain, macOS Keychain, Windows Credential Manager, Linux Secret Service.
**OS-level credential storage.** Sensitive local secrets are rooted in your platform's secure keychain, macOS Keychain, Windows Credential Manager, Linux Secret Service. See [OS Keyring & Secret Storage](os-keyring-and-secret-storage.md).

**No training on your data.** Your conversations, your Memory Tree, and your personal information are never used to train AI models or improve systems.

Expand Down Expand Up @@ -70,6 +70,8 @@ Compression and locality together become the privacy architecture.

**Encrypted in transit.** All communication between the application and the OpenHuman backend uses TLS. No data travels in plain text.

**Key in keyring, ciphertext on disk.** For local secrets that must be persisted in app files, OpenHuman stores encrypted ciphertext on disk and keeps the master decryption key in the OS keyring. See [OS Keyring & Secret Storage](os-keyring-and-secret-storage.md).

**Sandboxed skills.** Each skill runs in its own isolated execution environment with enforced memory and resource limits. Skills cannot access each other's data, the host system's file system, or your credentials.

**Workspace-scoped tools.** The native [filesystem tools](native-tools/coder.md) operate within the workspace the user opens; they do not have ambient access to the rest of the disk.
Expand Down
2 changes: 2 additions & 0 deletions scripts/test-rust-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ ALL_E2E_SUITES=(
autocomplete_memory_e2e
calendar_grounding_e2e
json_rpc_e2e
keyring_secretstore_fresh_e2e
keyring_secretstore_e2e
linux_cef_deb_runtime_e2e
live_routing_e2e
memory_graph_sync_e2e
Expand Down
12 changes: 6 additions & 6 deletions src/openhuman/config/schema/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,12 +370,12 @@ async fn resolve_config_dirs_ignoring_env(
}

fn decrypt_optional_secret(
store: &crate::openhuman::security::SecretStore,
store: &crate::openhuman::keyring::SecretStore,
value: &mut Option<String>,
field_name: &str,
) -> Result<()> {
if let Some(raw) = value.clone() {
if crate::openhuman::security::SecretStore::is_encrypted(&raw) {
if crate::openhuman::keyring::SecretStore::is_encrypted(&raw) {
*value = Some(
store
.decrypt(&raw)
Expand All @@ -387,12 +387,12 @@ fn decrypt_optional_secret(
}

fn encrypt_optional_secret(
store: &crate::openhuman::security::SecretStore,
store: &crate::openhuman::keyring::SecretStore,
value: &mut Option<String>,
field_name: &str,
) -> Result<()> {
if let Some(raw) = value.clone() {
if !crate::openhuman::security::SecretStore::is_encrypted(&raw) {
if !crate::openhuman::keyring::SecretStore::is_encrypted(&raw) {
*value = Some(
store
.encrypt(&raw)
Expand All @@ -412,7 +412,7 @@ fn decrypt_config_secrets(config: &mut Config, openhuman_dir: &Path) -> Result<(
if !config.secrets.encrypt {
return Ok(());
}
let store = crate::openhuman::security::SecretStore::new(openhuman_dir, true);
let store = crate::openhuman::keyring::SecretStore::new(openhuman_dir, true);

decrypt_optional_secret(&store, &mut config.api_key, "api_key")?;

Expand Down Expand Up @@ -521,7 +521,7 @@ fn encrypt_config_secrets(config: &mut Config) -> Result<()> {
.config_path
.parent()
.context("Config path must have a parent directory")?;
let store = crate::openhuman::security::SecretStore::new(parent_dir, true);
let store = crate::openhuman::keyring::SecretStore::new(parent_dir, true);

encrypt_optional_secret(&store, &mut config.api_key, "api_key")?;

Expand Down
2 changes: 1 addition & 1 deletion src/openhuman/credentials/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::openhuman::credentials::session_support::{
build_session_state, is_local_session_token, local_session_user_id, parse_fields_value,
profile_name_or_default, summarize_auth_profile, LOCAL_SESSION_USER_ID,
};
use crate::openhuman::security::SecretStore;
use crate::openhuman::keyring::SecretStore;
use crate::rpc::RpcOutcome;

use super::{AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME};
Expand Down
2 changes: 1 addition & 1 deletion src/openhuman/credentials/profiles.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::openhuman::security::SecretStore;
use crate::openhuman::keyring::SecretStore;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
Expand Down
2 changes: 1 addition & 1 deletion src/openhuman/devices/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::openhuman::devices::tunnel_client;
use crate::openhuman::devices::types::{
CreatePairingResponse, ListDevicesResponse, PairingSession, RevokeDeviceResponse,
};
use crate::openhuman::security::SecretStore;
use crate::openhuman::keyring::SecretStore;
use crate::rpc::RpcOutcome;

// ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/openhuman/encryption/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ AES-256-GCM at-rest crypto for AI memory storage and the encrypt/decrypt RPC sur

## Tests

- This domain has no `*_tests.rs` siblings; the underlying crypto round-trips are exercised by `src/openhuman/security/secrets_tests.rs` and the credentials tests, which both cover encrypt/decrypt happy paths and tampered-ciphertext rejection.
- This domain has no `*_tests.rs` siblings; the underlying crypto round-trips are exercised by `src/openhuman/keyring/encrypted_store_tests.rs` and the credentials tests, which both cover encrypt/decrypt happy paths and tampered-ciphertext rejection.
12 changes: 4 additions & 8 deletions src/openhuman/keyring/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
//!
//! - [`FileBackend`]: Stores secrets in a plain JSON file at
//! `{workspace}/dev-keychain.json`. **This file is NOT encrypted** — it is a
//! development artifact only and must never be used in production. It exists
//! solely to avoid the "different binary signature → macOS Keychain permission
//! prompt on every `cargo run`" problem that plagues dev workflows.
//! test/debug artifact only and must never be used in production.
//!
//! Backend selection happens once at first use (see [`super::selected_backend`]).

Expand Down Expand Up @@ -99,15 +97,13 @@ impl KeyringBackend for OsBackend {

// ── FileBackend ───────────────────────────────────────────────────────────────

/// Dev-only backend: plain JSON file at `{workspace}/dev-keychain.json`.
/// Test/debug backend: plain JSON file at `{workspace}/dev-keychain.json`.
///
/// # WARNING — NOT FOR PRODUCTION
///
/// Secrets stored here are **not encrypted**. This backend exists only to
/// eliminate macOS Keychain permission prompts during development (where the
/// binary signature changes on every `cargo build`). It is selected
/// automatically in debug builds and when `OPENHUMAN_APP_ENV=dev|staging`.
/// Never use it in a production deployment.
/// keep unit tests and explicit recovery/debug overrides independent from the
/// host OS keychain. Never use it in a production deployment.
///
/// # Thread safety
///
Expand Down
Loading
Loading