Files
openhuman/rust-core/src/ai/encryption.rs
T
Steven EnamakelandGitHub 37991fc567 refactor: split tauri host from openhuman_core runtime (#43)
* chore: add Cargo.toml and Cargo.lock for rust-core workspace

- Introduced a new workspace for the rust-core module with its own Cargo.toml.
- Added Cargo.lock to manage dependencies for the rust-core module.
- Updated .gitignore to exclude the target directory generated by Cargo.
- Modified GitHub Actions workflow to build from the new rust-core path instead of src-tauri.

* feat: enhance memory and authentication models in rust-core

- Added new `memory` module to handle persistent memory operations for skills, including methods for storing and querying skill data.
- Introduced `models` module with `auth` and `socket` submodules to manage user session and socket connection states.
- Updated `lib.rs` to include new modules and ensure proper integration within the rust-core workspace.
- Modified `eslint.config.js` to ignore target directories in linting processes.

* feat: integrate Tauri and QuickJS support in rust-core

- Added `tauri` and `rquickjs` as optional dependencies in `Cargo.toml` to enable Tauri integration and JavaScript execution.
- Introduced `CronScheduler` and `PingScheduler` modules for managing scheduled tasks and health checks for skills.
- Implemented `MemoryState` struct for shared app-state management in the memory client.
- Updated the runtime module to include new schedulers and ensure proper integration with Tauri features.
- Enhanced the skill registry to support new functionalities related to skill management and communication.

* chore: update ESLint configuration and refactor Rust module imports

- Added 'rust-core/**' to ESLint ignore list to streamline linting processes.
- Refactored import paths in Rust modules to directly reference the `memory` module, enhancing clarity and maintainability.
- Improved formatting in JavaScript files for better readability and consistency.

* refactor: rename rust-core to openhuman-core and update dependencies

- Renamed the `rust-core` module to `openhuman-core` across all files for consistency.
- Updated `Cargo.lock` to include `openhuman-core` as a new dependency and removed `rust-core`.
- Adjusted import paths in the codebase to reflect the new module name, ensuring all references are updated.
- Enhanced the Tauri integration by modifying dependencies in `src-tauri/Cargo.toml` to point to `openhuman-core`.

* refactor: simplify platform detection using match statements

- Replaced multiple if-else statements with match expressions in `current_platform`, `get_platform`, and `register` functions for improved readability and maintainability.
- Removed unnecessary conditional compilation for Android and iOS in the `SocketManager` and `tauri_bridge` modules, streamlining the codebase.
- Enhanced the `send_notification` function to handle platform checks more efficiently.

* refactor: update platform detection to use std::env::consts

- Replaced `cfg!(target_os = "os_name")` checks with `std::env::consts::OS` for improved clarity and consistency across the codebase.
- Added `rppal` as an optional dependency in `Cargo.toml` for Raspberry Pi support.
- Cleaned up platform-specific code in various modules, enhancing maintainability.

* refactor: streamline platform-specific code and improve readability

- Replaced conditional compilation with `std::env::consts::OS` checks in various modules to enhance clarity and maintainability.
- Simplified platform detection logic in `available_disk_space_mb`, `ensure_arduino_cli`, and `open_in_brave` functions.
- Updated `screenshot_command_exists` test to conditionally skip based on the operating system.
- Cleaned up unnecessary `#[cfg]` attributes, focusing on a more consistent approach across the codebase.

* feat: introduce comprehensive AI configuration and memory management

- Added multiple configuration files for OpenHuman AI, including `AGENTS.md`, `BOOTSTRAP.md`, `CONSCIOUS_LOOP.md`, `IDENTITY.md`, `MEMORY.md`, `README.md`, `SOUL.md`, `TOOLS.md`, and `USER.md` to define agent roles, onboarding processes, identity, memory management, and tool capabilities.
- Implemented an encryption layer for AI memory storage in `encryption.rs`, utilizing AES-256-GCM for secure data handling.
- Updated `lib.rs` to include the new AI module structure, enhancing the overall architecture and maintainability of the codebase.

* refactor: update dependencies and configuration for improved structure

- Removed `android_logger` and related packages from `Cargo.lock` and `Cargo.toml`, streamlining the dependency list.
- Adjusted the `APP_IDENTIFIER` constant in `config.rs` to reflect the new application identifier.
- Updated resource paths in `tauri.conf.json` for better organization.
- Enhanced platform-specific dependency management in `Cargo.toml` for clarity and maintainability.

* fix(core): gate ai module behind tauri-host feature

* refactor: update AI loading mechanisms and improve platform handling

- Refactored the AI configuration loading in `loader.ts` and `tools/loader.ts` to utilize Tauri commands for desktop environments, enhancing performance and reliability.
- Updated paths for AI markdown files to reflect the new `rust-core` structure.
- Introduced a new end-to-end test for Tauri command interactions in `tauriCoreBridge.e2e.test.ts`.
- Cleaned up platform-specific code across various modules, ensuring a more consistent approach to handling desktop and web contexts.

* feat: add sidecar core binary build and staging for Tauri bundler

- Implemented build steps for the sidecar core binary in multiple workflows, targeting both x86_64-unknown-linux-gnu and aarch64-apple-darwin architectures.
- Added staging steps to copy the built binaries into the Tauri resources directory, ensuring proper integration for application bundling.
- Updated relevant workflows to enhance the build process and streamline artifact management.

* refactor: enhance AI loading logic for Tauri integration

- Updated `loader.ts` and `tools/loader.ts` to prioritize Tauri commands for loading configurations in desktop environments, with a fallback to bundled markdown files for web contexts.
- Adjusted test mocks to reflect the new file paths for tools markdown.
- Improved test setup to mock Tauri API behavior accurately.

* docs: update CLAUDE.md and remove Android build scripts from package.json

- Added a new section in CLAUDE.md detailing the runtime scope, clarifying that Tauri is desktop-only and should not include mobile or web branches.
- Removed Android development and build scripts from package.json to streamline the project for desktop platforms only.

* refactor: streamline import statements and enhance test mock structure

- Reordered import statements in `loader.ts` and `tools/loader.ts` for consistency.
- Simplified mock implementation in `tauriCoreBridge.e2e.test.ts` to improve readability and maintainability.
- Added external binary configuration in `build.rs` to support resource management during local builds.

* test: stabilize core/unit test paths and tauri build-test config

* chore: update subproject commit reference in skills

* chore: update Tauri build configuration to include custom environment variable

- Modified the Tauri build command in the GitHub Actions workflow to set a custom configuration for updater artifacts, enhancing the build process for the x86_64-unknown-linux-gnu target.
2026-03-27 13:30:32 -07:00

178 lines
6.5 KiB
Rust

//! AES-256-GCM encryption layer for AI memory storage.
//!
//! All memory data (SQLite content, embeddings, session transcripts) is
//! encrypted at rest using AES-256-GCM. Keys are derived from a user
//! password via Argon2id.
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::{
aead::{Aead, KeyInit, OsRng},
Aes256Gcm, Nonce,
};
use argon2::{self, Algorithm, Argon2, Params, Version};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Salt length for Argon2id key derivation
const SALT_LENGTH: usize = 16;
/// Nonce length for AES-256-GCM (96 bits)
const NONCE_LENGTH: usize = 12;
/// Derived key length (256 bits for AES-256)
const KEY_LENGTH: usize = 32;
/// Encrypted payload with metadata for decryption
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EncryptedPayload {
/// AES-256-GCM ciphertext
pub ciphertext: Vec<u8>,
/// Random nonce used for this encryption
pub nonce: Vec<u8>,
/// Argon2id salt used for key derivation
pub salt: Vec<u8>,
}
/// Encryption key material
#[derive(Clone)]
pub struct EncryptionKey {
key_bytes: [u8; KEY_LENGTH],
}
impl EncryptionKey {
/// Derive an encryption key from a password and salt using Argon2id.
pub fn derive(password: &str, salt: &[u8]) -> Result<Self, String> {
let params = Params::new(65536, 3, 1, Some(KEY_LENGTH))
.map_err(|e| format!("Argon2 params error: {e}"))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut key_bytes = [0u8; KEY_LENGTH];
argon2
.hash_password_into(password.as_bytes(), salt, &mut key_bytes)
.map_err(|e| format!("Key derivation failed: {e}"))?;
Ok(Self { key_bytes })
}
/// Generate a new random salt for key derivation.
pub fn generate_salt() -> Vec<u8> {
let mut salt = vec![0u8; SALT_LENGTH];
OsRng.fill_bytes(&mut salt);
salt
}
/// Encrypt plaintext bytes.
pub fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptedPayload, String> {
let cipher =
Aes256Gcm::new_from_slice(&self.key_bytes).map_err(|e| format!("Cipher init: {e}"))?;
let mut nonce_bytes = [0u8; NONCE_LENGTH];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(|e| format!("Encryption failed: {e}"))?;
Ok(EncryptedPayload {
ciphertext,
nonce: nonce_bytes.to_vec(),
salt: Vec::new(), // Salt is stored separately in the key file
})
}
/// Decrypt an encrypted payload.
pub fn decrypt(&self, payload: &EncryptedPayload) -> Result<Vec<u8>, String> {
let cipher =
Aes256Gcm::new_from_slice(&self.key_bytes).map_err(|e| format!("Cipher init: {e}"))?;
let nonce = Nonce::from_slice(&payload.nonce);
cipher
.decrypt(nonce, payload.ciphertext.as_ref())
.map_err(|e| format!("Decryption failed: {e}"))
}
/// Encrypt a string and return base64-encoded JSON payload.
pub fn encrypt_string(&self, plaintext: &str) -> Result<String, String> {
let payload = self.encrypt(plaintext.as_bytes())?;
serde_json::to_string(&payload).map_err(|e| format!("Serialization failed: {e}"))
}
/// Decrypt a base64-encoded JSON payload back to a string.
pub fn decrypt_string(&self, encrypted_json: &str) -> Result<String, String> {
let payload: EncryptedPayload =
serde_json::from_str(encrypted_json).map_err(|e| format!("Deserialization: {e}"))?;
let plaintext = self.decrypt(&payload)?;
String::from_utf8(plaintext).map_err(|e| format!("UTF-8 decode: {e}"))
}
}
/// Get the path to the OpenHuman data directory (~/.openhuman/).
pub fn get_data_dir() -> Result<PathBuf, String> {
let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?;
let data_dir = home.join(".openhuman");
std::fs::create_dir_all(&data_dir)
.map_err(|e| format!("Failed to create data directory: {e}"))?;
Ok(data_dir)
}
/// Get the path to the encryption key file (~/.openhuman/encryption.key).
fn get_key_file_path() -> Result<PathBuf, String> {
Ok(get_data_dir()?.join("encryption.key"))
}
/// Key file stores the salt; the actual key is derived at runtime from password.
#[derive(Serialize, Deserialize)]
struct KeyFile {
salt: Vec<u8>,
/// Version for future key rotation
version: u32,
}
// --- Tauri Commands ---
/// Initialize encryption with a password. Creates key file if needed.
#[tauri::command]
pub async fn ai_init_encryption(password: String) -> Result<bool, String> {
let key_path = get_key_file_path()?;
if key_path.exists() {
// Key file exists, verify password works by loading it
let content =
std::fs::read_to_string(&key_path).map_err(|e| format!("Read key file: {e}"))?;
let key_file: KeyFile =
serde_json::from_str(&content).map_err(|e| format!("Parse key file: {e}"))?;
let _key = EncryptionKey::derive(&password, &key_file.salt)?;
Ok(true)
} else {
// Create new key file with random salt
let salt = EncryptionKey::generate_salt();
let key_file = KeyFile { salt, version: 1 };
let content =
serde_json::to_string_pretty(&key_file).map_err(|e| format!("Serialize: {e}"))?;
std::fs::write(&key_path, content).map_err(|e| format!("Write key file: {e}"))?;
Ok(true)
}
}
/// Encrypt a string value using the password-derived key.
#[tauri::command]
pub async fn ai_encrypt(password: String, plaintext: String) -> Result<String, String> {
let key_path = get_key_file_path()?;
let content = std::fs::read_to_string(&key_path).map_err(|e| format!("Read key: {e}"))?;
let key_file: KeyFile =
serde_json::from_str(&content).map_err(|e| format!("Parse key: {e}"))?;
let key = EncryptionKey::derive(&password, &key_file.salt)?;
key.encrypt_string(&plaintext)
}
/// Decrypt a string value using the password-derived key.
#[tauri::command]
pub async fn ai_decrypt(password: String, encrypted: String) -> Result<String, String> {
let key_path = get_key_file_path()?;
let content = std::fs::read_to_string(&key_path).map_err(|e| format!("Read key: {e}"))?;
let key_file: KeyFile =
serde_json::from_str(&content).map_err(|e| format!("Parse key: {e}"))?;
let key = EncryptionKey::derive(&password, &key_file.salt)?;
key.decrypt_string(&encrypted)
}