How OpenHuman Implements Local-First Encryption to Ensure User Data Privacy
OpenHuman protects sensitive user data through a two-layer local-first encryption strategy that uses ChaCha20-Poly1305 for secrets and AES-256-GCM for AI memory, ensuring plaintext never touches the filesystem.
OpenHuman is an open-source AI framework designed to keep user data private by processing and storing information locally before any remote synchronization. According to the tinyhumansai/openhuman source code, the system achieves this through a robust local-first encryption architecture that protects credentials and AI memory at rest. By combining modern authenticated encryption standards with strict key management, the system guarantees that user privacy is maintained even if the underlying storage is compromised.
Layer 1: Secret Store for Configuration and API Tokens
The Secret Store, implemented in src/openhuman/security/secrets.rs, handles encryption for API keys, tokens, and configuration values using a transparent encryption layer that operates automatically during read and write operations.
ChaCha20-Poly1305 AEAD Protocol
Each secret is encrypted using the ChaCha20-Poly1305 authenticated encryption algorithm. When SecretStore::encrypt is called, the system generates a fresh 12-byte nonce for every encryption operation to prevent replay attacks and ensure semantic security. The resulting ciphertext is prefixed with the marker enc2: and hex-encoded for safe storage in JSON configuration files.
Key Management and File Permissions
The encryption key is a random 256-bit value generated by a cryptographically secure pseudo-random number generator (CSPRNG). This key is stored in {data_dir}/.secret_key with strict Unix permissions 0600 (read/write owner only). To minimize I/O overhead and survive transient Windows sharing violations, the key file contents are cached process-wide after the initial read, preventing repeated filesystem access during intensive operations.
Automatic Legacy Migration
The system maintains backward compatibility while upgrading security postures. The SecretStore::decrypt_and_migrate method automatically detects legacy enc: prefixed values (which used insecure XOR encryption) and transparently re-encrypts them using the modern enc2: ChaCha20-Poly1305 format. This ensures older configurations are gradually upgraded without user intervention or risk of accidental exposure.
// Encrypt a secret (e.g., API token)
use openhuman::security::SecretStore;
use std::path::Path;
let store = SecretStore::new(Path::new("/home/alice/.openhuman"), true);
let encrypted = store.encrypt("my-super-secret-token")?; // → "enc2:ab12…"
println!("Stored value: {}", encrypted);
// Decrypt (and migrate) a secret
let (plain, maybe_new) = store.decrypt_and_migrate(&encrypted)?;
println!("Plaintext: {}", plain);
if let Some(new_val) = maybe_new {
// Persist the upgraded `enc2:` value back to config
// write_config("apiToken", &new_val);
}
Layer 2: User Data Encryption for AI Memory
For protecting AI memory and arbitrary user content, OpenHuman uses a password-based encryption system located in src/openhuman/encryption/core.rs with a public API exposed through src/openhuman/encryption/ops.rs and JSON-RPC schema registration in src/openhuman/encryption/schemas.rs.
Argon2id Key Derivation
When a user sets a password, the system derives a 256-bit encryption key using the Argon2id algorithm. Rather than storing the key directly, OpenHuman persists only a random salt in {data_dir}/encryption.key. The actual encryption key is deterministically derived from the user's password and this salt at runtime, ensuring the key material never exists on disk in plaintext, even when the application is not running.
AES-256-GCM Implementation
User data encryption uses AES-256-GCM via the EncryptionKey::encrypt method. Each encryption operation generates a unique 12-byte nonce to guarantee that identical plaintexts produce distinct ciphertexts. The system returns an EncryptedPayload containing the ciphertext, nonce, and authentication tag, while the salt remains managed separately in the key file to facilitate key regeneration on demand.
RPC Interface and Operations
The high-level JSON-RPC methods ai_encrypt and ai_decrypt defined in src/openhuman/encryption/ops.rs provide the primary interface for the application. These helpers handle loading the salt from encryption.key, deriving the Argon2id key, and executing the cryptographic operations via encrypt_string and decrypt_string functions respectively.
// Initialize user-level encryption
use openhuman::encryption::core::{ai_init_encryption, ai_encrypt, ai_decrypt};
// First run – creates a random salt and writes `encryption.key`
ai_init_encryption("my-strong-password".to_string()).await?;
// Encrypt arbitrary user data
let cipher = ai_encrypt("my-strong-password".to_string(),
"some private note".to_string()).await?;
println!("Encrypted note: {}", cipher);
// Decrypt later
let plain = ai_decrypt("my-strong-password".to_string(), cipher).await?;
assert_eq!(plain, "some private note");
Security Guarantees
This dual-layer local-first approach provides several critical security properties:
- Data never leaves the device unencrypted: The core side-car stores only ciphertext, ensuring plaintext exists only in memory during active operations.
- Strong cryptographic primitives: ChaCha20-Poly1305 and AES-256-GCM provide authenticated encryption resistant to tampering and unauthorized access.
- Key protection: Secret store keys use filesystem permissions
0600and process caching; user data keys rely on Argon2id derivation without persistent key storage. - Graceful migration: Legacy XOR-encrypted values are automatically upgraded to prevent accidental exposure of outdated secrets.
Summary
- OpenHuman uses ChaCha20-Poly1305 in
src/openhuman/security/secrets.rsto encrypt API keys and configuration values with per-message nonces, hex-encodedenc2:prefixes, and automatic legacy migration from insecure XOR formats. - AES-256-GCM protects AI memory and user data in
src/openhuman/encryption/core.rs, using keys derived via Argon2id from user passwords and random salts stored separately inencryption.key. - Encryption keys are never stored in plaintext; the secret store uses permission-restricted
0600files with process-wide caching, while user data relies entirely on password derivation. - The RPC interface in
src/openhuman/encryption/ops.rsexposesai_encryptandai_decryptfor seamless application-level integration, with schema definitions insrc/openhuman/encryption/schemas.rs. - All ciphertext is prefixed with version markers (
enc2:) to distinguish modern AEAD encryption from legacy formats, enabling transparent security upgrades.
Frequently Asked Questions
What encryption algorithms does OpenHuman use for local data?
OpenHuman uses ChaCha20-Poly1305 for configuration secrets and API tokens in the Secret Store, and AES-256-GCM for AI memory and user data. Both are authenticated encryption with associated data (AEAD) algorithms that provide confidentiality and integrity protection against tampering.
How are encryption keys stored without compromising security?
For the Secret Store, a random 256-bit key is stored in .secret_key with 0600 filesystem permissions and cached process-wide to prevent leakage. For user data, only a random salt is stored in encryption.key; the actual encryption key is derived at runtime using Argon2id from the user's password and the stored salt, ensuring the key never persists on disk.
Can OpenHuman decrypt data from older versions?
Yes. The SecretStore::decrypt_and_migrate function in src/openhuman/security/secrets.rs automatically detects legacy enc: prefixed values (using XOR encryption) and re-encrypts them using the modern enc2: ChaCha20-Poly1305 format during the decryption process, preventing accidental use of weak cryptography.
Is the encryption truly local-first?
Yes. According to the tinyhumansai/openhuman source code, all encryption occurs on the local device before any data reaches the filesystem. The architecture ensures that user data remains private and never exists in plaintext outside the secure encryption boundaries of the local application, with ciphertext being the only form stored persistently.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →