mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 22:23:01 +00:00
* 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.
178 lines
6.5 KiB
Rust
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)
|
|
}
|