From 267d8f6edd84ba294c8187df8f83a71893fd2f26 Mon Sep 17 00:00:00 2001 From: MoCipher Date: Thu, 12 Feb 2026 00:02:00 +0100 Subject: [PATCH] Initial CrypteCipher scaffold and features --- .github/workflows/ci.yml | 42 ++++ .gitignore | 5 + README.md | 19 ++ bindings/Cargo.toml | 16 ++ bindings/package.json | 9 + bindings/src/lib.rs | 272 +++++++++++++++++++++++++ bindings/test/test.js | 27 +++ core/Cargo.toml | 49 +++++ core/src/bip39.rs | 53 +++++ core/src/btc.rs | 60 ++++++ core/src/btc_htlc.rs | 74 +++++++ core/src/hardware.rs | 41 ++++ core/src/keystore.rs | 335 ++++++++++++++++++++++++++++++ core/src/lib.rs | 104 ++++++++++ core/src/lightning.rs | 51 +++++ core/src/lnd.rs | 44 ++++ core/src/monero.rs | 99 +++++++++ core/src/monero_swap.rs | 57 ++++++ core/src/swap_manager.rs | 58 ++++++ core/src/swap_sim.rs | 74 +++++++ core/src/wallet_bdk.rs | 403 +++++++++++++++++++++++++++++++++++++ core/src/wallet_manager.rs | 85 ++++++++ docs/ARCHITECTURE.md | 29 +++ electron/README.md | 8 + mobile/README.md | 9 + tools/cli/Cargo.toml | 8 + tools/cli/src/main.rs | 32 +++ 27 files changed, 2063 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bindings/Cargo.toml create mode 100644 bindings/package.json create mode 100644 bindings/src/lib.rs create mode 100644 bindings/test/test.js create mode 100644 core/Cargo.toml create mode 100644 core/src/bip39.rs create mode 100644 core/src/btc.rs create mode 100644 core/src/btc_htlc.rs create mode 100644 core/src/hardware.rs create mode 100644 core/src/keystore.rs create mode 100644 core/src/lib.rs create mode 100644 core/src/lightning.rs create mode 100644 core/src/lnd.rs create mode 100644 core/src/monero.rs create mode 100644 core/src/monero_swap.rs create mode 100644 core/src/swap_manager.rs create mode 100644 core/src/swap_sim.rs create mode 100644 core/src/wallet_bdk.rs create mode 100644 core/src/wallet_manager.rs create mode 100644 docs/ARCHITECTURE.md create mode 100644 electron/README.md create mode 100644 mobile/README.md create mode 100644 tools/cli/Cargo.toml create mode 100644 tools/cli/src/main.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b7f99b4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + rust: [stable] + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + - name: Cache cargo + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ matrix.rust }} + - name: Build + run: cargo build --workspace --verbose + - name: Run Rust tests + run: cargo test --workspace --verbose + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Run Node integration tests (bindings) + working-directory: ./bindings + run: | + npm ci || true + npm test + shell: bash diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76cd984 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +node_modules/ +**/*.log +.env +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..93d3e1f --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# CrypteCipher + +Secure privacy-first multi-currency wallet (scaffold) + +Overview +- Core: Rust library implementing key management, Bitcoin (on-chain + Lightning), Monero (RPC integration), and atomic-swap scaffolding. +- Bindings: Node/N-API bindings to use core from Electron/Node.js. +- Mobile: React Native app (TypeScript) that will use platform-native bindings to call the Rust core. +- Electron: Desktop app skeleton that uses N-API bindings. + +Security notes +- Core crypto and key management is implemented in Rust for memory safety. +- Do not ship private keys to servers — default is non-custodial. +- All cryptographic code must be audited before production. + +Next steps +1. Review ARCHITECTURE.md in `/docs`. +2. Wire up real libraries and node/native module integration. +3. Add automated tests and security audit. diff --git a/bindings/Cargo.toml b/bindings/Cargo.toml new file mode 100644 index 0000000..7cf83be --- /dev/null +++ b/bindings/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cryptec_bindings" +version = "0.1.0" +edition = "2021" + +[lib] +name = "cryptec_bindings" +crate-type = ["cdylib"] + +[dependencies] +# napi-rs provides Node.js native ABI bindings (useful for Electron desktop) +napi = { version = "2", features = ["compat-mode"] } +napi-derive = "2" + +# Depend on the core crate (path dependency) +cryptec_core = { path = "../core" } diff --git a/bindings/package.json b/bindings/package.json new file mode 100644 index 0000000..e863a8f --- /dev/null +++ b/bindings/package.json @@ -0,0 +1,9 @@ +{ + "name": "cryptec-bindings-tests", + "version": "0.1.0", + "scripts": { + "build-cli": "cargo build -p cryptec_cli --manifest-path ../tools/cli/Cargo.toml --release", + "test": "node test/test.js" + }, + "devDependencies": {} +} \ No newline at end of file diff --git a/bindings/src/lib.rs b/bindings/src/lib.rs new file mode 100644 index 0000000..dfa7a04 --- /dev/null +++ b/bindings/src/lib.rs @@ -0,0 +1,272 @@ +use napi::bindgen_prelude::*; +use napi_derive::napi; + +use cryptec_core::{WalletCore, Mnemonic, BdkWallet, LightningClient, encrypt_seed_file, decrypt_seed_file, LndClient, LedgerHw}; + +#[napi] +fn core_fingerprint() -> String { + let w = WalletCore::new(); + w.fingerprint() +} + +#[napi] +fn core_generate_seed_hex() -> String { + let s = WalletCore::generate_seed(); + hex::encode(s) +} + +#[napi] +fn core_example_btc_privkey_hex() -> String { + let w = WalletCore::new(); + w.example_btc_privkey_hex() +} + +#[napi] +fn generate_mnemonic(strength: u32) -> String { + // strength should be 128, 160, 192, 224, 256 + let m = Mnemonic::generate(strength as usize); + m.phrase +} + +#[napi] +fn bdk_first_receive_address(phrase: String) -> String { + // For simplicity use Testnet; in real binding allow network selection + let w = BdkWallet::new_from_mnemonic(&phrase, "", bitcoin::Network::Testnet).expect("create wallet"); + w.get_new_address().expect("get address") +} + +#[napi] +fn bdk_sync_electrum(phrase: String, electrum_url: String) -> String { + let w = BdkWallet::new_from_mnemonic(&phrase, "", bitcoin::Network::Testnet).expect("create wallet"); + w.sync_with_electrum(&electrum_url).expect("sync"); + "ok".to_string() +} + +#[napi] +fn bdk_create_psbt(phrase: String, to_address: String, satoshis: u64) -> String { + let w = BdkWallet::new_from_mnemonic(&phrase, "", bitcoin::Network::Testnet).expect("create wallet"); + w.create_psbt(&to_address, satoshis).expect("create psbt") +} + +#[napi] +fn bdk_sign_psbt(phrase: String, psbt_b64: String) -> String { + let w = BdkWallet::new_from_mnemonic(&phrase, "", bitcoin::Network::Testnet).expect("create wallet"); + w.sign_psbt_base64(&psbt_b64).expect("sign psbt") +} + +#[napi] +fn bdk_create_persistent_wallet(phrase: String, db_path: String) -> String { + let w = PersistentBdkWallet::new_from_mnemonic_sqlite(&phrase, "", bitcoin::Network::Testnet, &db_path).expect("create persistent"); + w.get_new_address().expect("get address") +} + +#[napi] +fn bdk_persistent_sync_electrum(db_path: String, electrum_url: String) -> String { + // open the wallet using the sqlite DB file path + // Note: in a full implementation we'd store wallet metadata/config; here we recreate wallet from DB + descriptors + // For demo purposes require that DB was created with same mnemonic via create_persistent_wallet + let client = PersistentBdkWallet::new_from_mnemonic_sqlite("", "", bitcoin::Network::Testnet, &db_path); + if client.is_err() { + return format!("error: {}", client.err().unwrap()); + } + let w = client.unwrap(); + w.sync_with_electrum(&electrum_url).expect("sync"); + "ok".to_string() +} + +#[napi] +fn encrypt_seed(path: String, password: String) -> String { + let seed = WalletCore::generate_seed(); + encrypt_seed_file(&seed, &password, &path).expect("encrypt"); + "ok".to_string() +} + +#[napi] +fn decrypt_seed(path: String, password: String) -> String { + let seed = decrypt_seed_file(&path, &password).expect("decrypt"); + hex::encode(seed) +} + +#[napi] +fn keystore_generate_db_key(service: String, user: String) -> String { + crate::keystore::generate_db_master_key(&service, &user).expect("generate key"); + "ok".to_string() +} + +#[napi] +fn bdk_create_encrypted_persistent_wallet(phrase: String, db_path_encrypted: String, service: String, user: String) -> String { + // Uses a temp plaintext DB which is removed after encryption + let network = bitcoin::Network::Testnet; + PersistentBdkWallet::create_encrypted_persistent_wallet(&phrase, "", network, &db_path_encrypted, &service, &user).expect("create encrypted wallet"); + "ok".to_string() +} + +#[napi] +fn bdk_encrypt_db(plain_path: String, encrypted_path: String, service: String, user: String) -> String { + // ensures master key exists and encrypts a plain DB file + if let Err(_) = crate::keystore::get_db_master_key(&service, &user) { + crate::keystore::generate_db_master_key(&service, &user).expect("generate key"); + } + PersistentBdkWallet::encrypt_db_file(&plain_path, &encrypted_path, &service, &user).expect("encrypt db"); + "ok".to_string() +} + +#[napi] +fn bdk_decrypt_db_to_temp(encrypted_path: String, service: String, user: String) -> String { + let tmp = PersistentBdkWallet::decrypt_db_to_tempfile(&encrypted_path, &service, &user).expect("decrypt tmp"); + tmp.path().to_str().unwrap().to_string() +} + +#[napi] +fn bdk_decrypted_db_close(path: String) -> String { + // attempt to securely delete the decrypted DB file + let _ = crate::keystore::secure_delete(&path); + "ok".to_string() +} + +#[napi] +fn bdk_open_inmemory_db(encrypted_path: String, service: String, user: String) -> String { + let im = InMemoryDb::from_encrypted_file_in_memory(&encrypted_path, &service, &user).expect("open in-memory db"); + let id = crate::wallet_manager::register_inmemory_db(im); + id +} + +#[napi] +fn bdk_inmemory_table_list(handle_id: String) -> String { + // convenience wrapper using the run query helper to get tables + let q = "SELECT name FROM sqlite_master WHERE type='table'".to_string(); + crate::wallet_manager::inmemory_run_query(&handle_id, &q).unwrap_or_else(|_| "[]".to_string()) +} + +#[napi] +fn bdk_inmemory_run_query(handle_id: String, sql: String) -> String { + crate::wallet_manager::inmemory_run_query(&handle_id, &sql).unwrap_or_else(|_| "[]".to_string()) +} + +#[napi] +fn bdk_inmemory_export_snapshot(handle_id: String) -> String { + crate::wallet_manager::inmemory_export_snapshot_base64(&handle_id).unwrap_or_else(|_| "".to_string()) +} + +#[napi] +fn bdk_close_inmemory_handle(handle_id: String) -> String { + let _ = crate::wallet_manager::close_inmemory_db(&handle_id); + "ok".to_string() +} + +#[napi] +fn bdk_list_handles() -> String { + let list = crate::wallet_manager::list_handles(); + serde_json::to_string(&list).unwrap_or_else(|_| "[]".to_string()) +} + +#[napi] +fn bdk_close_all_handles() -> String { + let _ = crate::wallet_manager::close_all(); + "ok".to_string() +} + +#[napi] +fn monero_get_balance(rpc_url: String, user: Option, pass: Option) -> String { + let client = MoneroClient::new(&rpc_url, user.as_deref(), pass.as_deref()); + match client.get_balance() { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn monero_get_address(rpc_url: String, user: Option, pass: Option) -> String { + let client = MoneroClient::new(&rpc_url, user.as_deref(), pass.as_deref()); + match client.get_address() { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn monero_get_tx_proof(rpc_url: String, user: Option, pass: Option, txid: String, address: Option) -> String { + let client = MoneroClient::new(&rpc_url, user.as_deref(), pass.as_deref()); + match client.get_tx_proof(&txid, address.as_deref()) { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn monero_check_tx_proof(rpc_url: String, user: Option, pass: Option, txid: String, address: String, signature: String) -> String { + let client = MoneroClient::new(&rpc_url, user.as_deref(), pass.as_deref()); + match client.check_tx_proof(&txid, &address, &signature) { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn monero_transfer(rpc_url: String, user: Option, pass: Option, amount: u64, address: String) -> String { + let client = MoneroClient::new(&rpc_url, user.as_deref(), pass.as_deref()); + match client.transfer(amount, &address) { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn monero_create_swap(btc_sats: u64, xmr_piconero: u64, role: String) -> String { + let r = if role == "maker" { SwapRole::Maker } else { SwapRole::Taker }; + let s = AtomicSwap::new(r, btc_sats, xmr_piconero); + let js = s.create_swap_contract(); + match js { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn monero_make_integrated_address(rpc_url: String, user: Option, pass: Option, payment_id: Option) -> String { + let client = MoneroClient::new(&rpc_url, user.as_deref(), pass.as_deref()); + match client.make_integrated_address(payment_id.as_deref()) { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn monero_simulate_swap(btc_sats: u64, xmr_piconero: u64) -> String { + let s = SwapSimulation::new(btc_sats, xmr_piconero); + let offer = s.maker_create_offer(); + let proof = s.taker_provide_proof(); + let reveal = s.maker_validate_and_reveal(proof.clone()); + let prehex = reveal.get("preimage").unwrap().as_str().unwrap().to_string(); + let redeem = s.taker_redeem_with_preimage(&prehex); + let out = serde_json::json!({"offer": offer, "proof": proof, "reveal": reveal, "redeem": redeem}); + serde_json::to_string(&out).unwrap() +} + +#[napi] +fn monero_swap_step(proof: String) -> String { + let v: serde_json::Value = serde_json::from_str(&proof).unwrap_or_else(|_| serde_json::json!(null)); + let s = AtomicSwap::new(SwapRole::Taker, 0, 0); + let res = s.validate_and_step(v); + match res { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn lnd_getinfo(host: String, macaroon: Option) -> String { + let client = LndClient::new(&host, macaroon.as_deref()); + let v = client.get_info(); + match v { + Ok(val) => serde_json::to_string(&val).unwrap_or_else(|_| "{}".to_string()), + Err(e) => format!("error: {}", e) + } +} + +#[napi] +fn hardware_ledger_status() -> String { + let l = LedgerHw::new(); + if l.connected { "connected".to_string() } else { "disconnected".to_string() } +} + diff --git a/bindings/test/test.js b/bindings/test/test.js new file mode 100644 index 0000000..05acea5 --- /dev/null +++ b/bindings/test/test.js @@ -0,0 +1,27 @@ +const { execSync } = require('child_process'); +const path = require('path'); +try { + // Build the CLI + console.log('building CLI...'); + execSync('npm run build-cli', { cwd: path.join(__dirname, '..'), stdio: 'inherit' }); + + // Locate the built binary + const binName = process.platform === 'win32' ? 'cryptec_cli.exe' : 'cryptec_cli'; + const targetDir = path.join(__dirname, '..', '..', 'target', 'release'); + const bin = path.join(targetDir, binName); + + // Run version + const ver = execSync(`"${bin}" version`).toString().trim(); + console.log('cli version output:', ver); + + // Generate mnemonic and get an address + const mnemonic = execSync(`"${bin}" gen-mnemonic`).toString().trim(); + console.log('mnemonic:', mnemonic.split(' ').slice(0,3).join(' ')+ '...'); + const addr = execSync(`"${bin}" bdk-new-addr "${mnemonic}"`).toString().trim(); + console.log('receive address:', addr); + console.log('Node integration tests passed'); + process.exit(0); +} catch (e) { + console.error('Node integration test failed', e.message); + process.exit(1); +} diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..0544c77 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "cryptec_core" +version = "0.1.0" +edition = "2021" + +[lib] +name = "cryptec_core" +crate-type = ["cdylib", "rlib"] + +[dependencies] +rand = "0.8" +hex = "0.4" +secp256k1 = { version = "0.26", default-features = false, features = ["rand"] } + +# Bitcoin on-chain utilities +bitcoin = "0.29" +bech32 = "0.8" +sha2 = "0.10" +hmac = "0.12" + +# BIP39 / HD derivation +bip39 = "1.1" +bip32 = "0.3" + +# BDK for wallet abstractions (UTXO management, PSBT, descriptors) +bdk = { version = "0.27", features = ["keys-bip39", "electrum", "sqlite"] } + +# Rust-Lightning (lightning protocol) - use as optional or for in-process nodes +lightning = "0.0.124" + +# Crypto / storage / tools +argon2 = "0.4" +aes-gcm = "0.10" +base64 = "0.21" +keyring = "1.0" +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +hidapi = "1.3" + +# Monero (RPC client) +reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] } +monero = "0.18" + +# Windows-specific secure-delete dependency +winapi = { version = "0.3", optional = true, features = ["winbase"] } + +# Note: Additional features and platform-specific flags will be required for full implementations + diff --git a/core/src/bip39.rs b/core/src/bip39.rs new file mode 100644 index 0000000..32483f5 --- /dev/null +++ b/core/src/bip39.rs @@ -0,0 +1,53 @@ +use bip39::{Language, Mnemonic as Bip39Mnemonic, Seed as Bip39Seed}; +use bitcoin::util::bip32::ExtendedPrivKey; +use bitcoin::network::constants::Network; + +/// High-level mnemonic wrapper +pub struct Mnemonic { + pub phrase: String, +} + +impl Mnemonic { + /// Generate a new BIP39 mnemonic with the given strength (entropy bits) + pub fn generate(strength: usize) -> Self { + let m = Bip39Mnemonic::new(bip39::MnemonicType::for_word_count((strength/32)*3).unwrap(), Language::English); + Self { phrase: m.phrase().to_string() } + } + + /// Create a mnemonic from an existing phrase (validation) + pub fn from_phrase(phrase: &str) -> Result { + let _ = Bip39Mnemonic::from_phrase(phrase, Language::English)?; + Ok(Self { phrase: phrase.to_string() }) + } + + /// Convert mnemonic (+optional passphrase) to BIP39 seed bytes + pub fn to_seed_bytes(&self, passphrase: &str) -> [u8; 64] { + let m = Bip39Mnemonic::from_phrase(&self.phrase, Language::English).expect("valid mnemonic"); + let seed = Bip39Seed::new(&m, passphrase); + let bytes = seed.as_bytes(); + let mut out = [0u8; 64]; + out.copy_from_slice(&bytes[0..64]); + out + } + + /// Derive master ExtendedPrivKey (BIP32) for the given network + pub fn to_master_xprv(&self, passphrase: &str, network: Network) -> ExtendedPrivKey { + let seed = self.to_seed_bytes(passphrase); + ExtendedPrivKey::new_master(network, &seed).expect("master xprv") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::network::constants::Network; + + #[test] + fn mnemonic_and_master() { + let m = Mnemonic::generate(128); + let seed = m.to_seed_bytes(""); + assert_eq!(seed.len(), 64); + let xprv = m.to_master_xprv("", Network::Testnet); + assert!(xprv.to_string().starts_with("tprv") || xprv.to_string().starts_with("xprv")); + } +} diff --git a/core/src/btc.rs b/core/src/btc.rs new file mode 100644 index 0000000..629fc54 --- /dev/null +++ b/core/src/btc.rs @@ -0,0 +1,60 @@ +use bitcoin::network::constants::Network; +use bitcoin::util::address::Address; +use bitcoin::util::key::PrivateKey; +use bitcoin::secp256k1::{Secp256k1, SecretKey, PublicKey}; +use sha2::{Digest, Sha256}; + +/// Simple Bitcoin on-chain wallet helper (minimal, safe-by-default) +/// +/// This implements a deterministic derivation of a single private key from the core seed +/// purely for demonstration and testing. In production use **BIP32/BIP39** HD derivation +/// and robust UTXO management (e.g., `bdk`). +pub struct BtcWallet { + secret_key: SecretKey, + network: Network, +} + +impl BtcWallet { + /// Derive a secret key deterministically from a seed and domain tag + pub fn derive_from_seed(seed: &[u8; 32], network: Network, tag: &[u8]) -> Self { + // Use SHA256(seed || tag) as a simple DRBG to get 32 bytes -> SecretKey + let mut hasher = Sha256::new(); + hasher.update(seed); + hasher.update(tag); + let hash = hasher.finalize(); + let sk = SecretKey::from_slice(&hash).expect("hash must be a valid secret key"); + Self { secret_key: sk, network } + } + + /// Return the P2WPKH bech32 address for this key + pub fn get_address(&self) -> Address { + let secp = Secp256k1::new(); + let pk = PublicKey::from_secret_key(&secp, &self.secret_key); + let compressed = PublicKey::new(pk.key); + let pk_hash = bitcoin::hashes::hash160::Hash::hash(&compressed.serialize()); + Address::p2wpkh(&pk, self.network).expect("address creation") + } + + /// Return the private key in WIF format (useful for tests/examples only) + pub fn wif(&self) -> String { + let pk = PrivateKey { key: self.secret_key, network: self.network, compressed: true }; + pk.to_wif() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::WalletCore; + + #[test] + fn derive_and_address() { + let core = WalletCore::new(); + let seed = core.seed; // NOTE: demo access to seed (in real code seed would be private) + let wallet = BtcWallet::derive_from_seed(&seed, Network::Testnet, b"cryptec-btc"); + let addr = wallet.get_address(); + assert!(addr.to_string().starts_with("tb1") || addr.to_string().starts_with("2") ); + let wif = wallet.wif(); + assert!(!wif.is_empty()); + } +} diff --git a/core/src/btc_htlc.rs b/core/src/btc_htlc.rs new file mode 100644 index 0000000..0c62af2 --- /dev/null +++ b/core/src/btc_htlc.rs @@ -0,0 +1,74 @@ +use bitcoin::blockdata::script::Script; +use bitcoin::util::key::PublicKey; +use bitcoin::hashes::{hash160, sha256, Hash}; +use bitcoin::blockdata::opcodes::all::*; +use bitcoin::network::constants::Network; +use bitcoin::util::address::Address; + +/// Build a simple HTLC redeem script (timelock refund + hashlock) +/// script: +/// OP_IF +/// OP_SIZE <32> OP_EQUALVERIFY OP_SHA256 OP_EQUALVERIFY OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG +/// OP_ELSE +/// OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG +/// OP_ENDIF + +pub fn build_htlc_redeem_script(hash: &[u8;32], recipient_pub: &PublicKey, refund_pub: &PublicKey, locktime: u32) -> Script { + let recipient_hash = hash160::Hash::hash(&recipient_pub.to_bytes()); + let refund_hash = hash160::Hash::hash(&refund_pub.to_bytes()); + + let mut builder = Script::builder(); + + builder = builder + .push_opcode(OP_IF) + .push_opcode(OP_SIZE) + .push_int(32) + .push_opcode(OP_EQUALVERIFY) + .push_opcode(OP_SHA256) + .push_slice(hash) + .push_opcode(OP_EQUALVERIFY) + .push_opcode(OP_DUP) + .push_opcode(OP_HASH160) + .push_slice(&recipient_hash.into_inner()) + .push_opcode(OP_EQUALVERIFY) + .push_opcode(OP_CHECKSIG) + .push_opcode(OP_ELSE) + .push_int(locktime as i64) + .push_opcode(OP_CHECKLOCKTIMEVERIFY) + .push_opcode(OP_DROP) + .push_opcode(OP_DUP) + .push_opcode(OP_HASH160) + .push_slice(&refund_hash.into_inner()) + .push_opcode(OP_EQUALVERIFY) + .push_opcode(OP_CHECKSIG) + .push_opcode(OP_ENDIF); + + builder.into_script() +} + +pub fn htlc_p2wsh_address(redeem: &Script, network: Network) -> Address { + Address::p2wsh(&redeem.to_v0_p2wsh(), network).expect("p2wsh address") +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::secp256k1::Secp256k1; + use bitcoin::secp256k1::PublicKey as SecpPub; + use secp256k1::SecretKey; + use rand::rngs::OsRng; + + #[test] + fn build_htlc_script_and_address() { + let secp = Secp256k1::new(); + let mut rng = OsRng::default(); + let sk1 = SecretKey::new(&mut rng); + let sk2 = SecretKey::new(&mut rng); + let pk1 = SecpPub::from_secret_key(&secp, &sk1); + let pk2 = SecpPub::from_secret_key(&secp, &sk2); + let mut hash = [0u8;32]; + OsRng.fill_bytes(&mut hash); + let script = build_htlc_redeem_script(&hash, &PublicKey{ key: pk1, compressed: true }, &PublicKey{ key: pk2, compressed: true }, 500); + assert!(script.len() > 0); + } +} diff --git a/core/src/hardware.rs b/core/src/hardware.rs new file mode 100644 index 0000000..2e64942 --- /dev/null +++ b/core/src/hardware.rs @@ -0,0 +1,41 @@ +use anyhow::Result; + +/// Hardware wallet trait - minimal abstraction +pub trait HardwareWallet { + fn get_xpub(&self, account: u32) -> Result; + fn sign_psbt(&self, psbt_b64: &str) -> Result; +} + +/// Ledger hardware wallet stub (placeholder) +pub struct LedgerHw { + pub connected: bool, +} + +impl LedgerHw { + pub fn new() -> Self { + // real implementation would use `hidapi` and APDU commands + Self { connected: false } + } +} + +impl HardwareWallet for LedgerHw { + fn get_xpub(&self, _account: u32) -> Result { + Err(anyhow::anyhow!("Ledger integration not implemented - stub")) + } + + fn sign_psbt(&self, _psbt_b64: &str) -> Result { + Err(anyhow::anyhow!("Ledger sign_psbt not implemented - stub")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ledger_stub_errors() { + let l = LedgerHw::new(); + assert!(l.get_xpub(0).is_err()); + assert!(l.sign_psbt("").is_err()); + } +} diff --git a/core/src/keystore.rs b/core/src/keystore.rs new file mode 100644 index 0000000..cf20e5e --- /dev/null +++ b/core/src/keystore.rs @@ -0,0 +1,335 @@ +use aes_gcm::{Aes256Gcm, Key, Nonce}; // Or `AesGcm` +use aes_gcm::aead::{Aead, NewAead}; +use argon2::{Argon2, password_hash::{SaltString, PasswordHasher, PasswordVerifier}, PasswordHash, PasswordHasher as _}; +use rand::rngs::OsRng; +use rand::RngCore; +use base64::{encode as b64encode, decode as b64decode}; +use serde::{Serialize, Deserialize}; +use std::fs; +use std::path::Path; +use anyhow::Result; + +use keyring::Keyring; + +#[derive(Serialize, Deserialize)] +struct EncryptedSeedFile { + salt: String, + nonce: String, + ciphertext: String, +} + +/// Derive 32-byte key using Argon2 from password + salt +fn derive_key(password: &str, salt: &[u8]) -> [u8; 32] { + let argon2 = Argon2::default(); + // Use password-hash crate to produce a derived key via Argon2: we will directly hash and then extract bytes + // Simpler: use Argon2::hash_password_simple to produce a string and then hash it again - but here create PHC and use raw hashing + use argon2::password_hash::Salt; + let salt = Salt::new(b64encode(salt).as_str()).expect("salt create"); + let mut out = [0u8; 32]; + // Use Argon2's hash_password_into + argon2.hash_password_into(password.as_bytes(), salt.as_ref().as_bytes(), &mut out).expect("argon2 derive"); + out +} + +/// Encrypt and write seed to file (JSON containing salt, nonce, ciphertext) +pub fn encrypt_seed_file(seed: &[u8; 32], password: &str, path: &str) -> Result<()> { + // generate salt + nonce + let mut salt = [0u8; 16]; + OsRng.fill_bytes(&mut salt); + let keybytes = derive_key(password, &salt); + let key = Key::from_slice(&keybytes); + let cipher = Aes256Gcm::new(key); + + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ct = cipher.encrypt(nonce, seed.as_ref()).expect("encryption"); + + let file = EncryptedSeedFile { + salt: b64encode(&salt), + nonce: b64encode(&nonce_bytes), + ciphertext: b64encode(&ct), + }; + + let s = serde_json::to_string_pretty(&file)?; + fs::write(path, s)?; + Ok(()) +} + +/// Decrypt a seed file given a password +pub fn decrypt_seed_file(path: &str, password: &str) -> Result<[u8; 32]> { + let content = fs::read_to_string(path)?; + let file: EncryptedSeedFile = serde_json::from_str(&content)?; + let salt = b64decode(&file.salt)?; + let nonce_bytes = b64decode(&file.nonce)?; + let ct = b64decode(&file.ciphertext)?; + + let keybytes = derive_key(password, &salt); + let key = Key::from_slice(&keybytes); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&nonce_bytes); + + let pt = cipher.decrypt(nonce, ct.as_ref())?; + let mut out = [0u8; 32]; + out.copy_from_slice(&pt[..32]); + Ok(out) +} + +/// Store a password in the OS keyring (optional convenience) +pub fn store_password_in_keyring(service: &str, user: &str, password: &str) -> Result<()> { + let kr = Keyring::new(service, user); + kr.set_password(password)?; + Ok(()) +} + +/// Retrieve a password from the OS keyring +pub fn retrieve_password_from_keyring(service: &str, user: &str) -> Result { + let kr = Keyring::new(service, user); + let pwd = kr.get_password()?; + Ok(pwd) +} + +/// Generate and store a random 32-byte master key in the OS keyring for DB encryption +pub fn generate_db_master_key(service: &str, user: &str) -> Result<()> { + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); + let s = b64encode(&key); + store_password_in_keyring(service, user, &s)?; + Ok(()) +} + +/// Retrieve the 32-byte master key from the OS keyring +pub fn get_db_master_key(service: &str, user: &str) -> Result<[u8; 32]> { + let s = retrieve_password_from_keyring(service, user)?; + let bytes = b64decode(&s)?; + let mut out = [0u8; 32]; + out.copy_from_slice(&bytes[0..32]); + Ok(out) +} + +/// Encrypt an arbitrary file using the master key stored in the OS keyring +pub fn encrypt_file_with_master_key(in_path: &str, out_path: &str, service: &str, user: &str) -> Result<()> { + let keybytes = get_db_master_key(service, user)?; + let key = Key::from_slice(&keybytes); + let cipher = Aes256Gcm::new(key); + + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let data = fs::read(in_path)?; + let ct = cipher.encrypt(nonce, data.as_ref())?; + + let file = EncryptedSeedFile { + salt: "".to_string(), + nonce: b64encode(&nonce_bytes), + ciphertext: b64encode(&ct), + }; + let s = serde_json::to_string_pretty(&file)?; + fs::write(out_path, s)?; + Ok(()) +} + +/// Decrypt an arbitrary file previously encrypted with `encrypt_file_with_master_key` +pub fn decrypt_file_with_master_key(in_path: &str, out_path: &str, service: &str, user: &str) -> Result<()> { + let content = fs::read_to_string(in_path)?; + let file: EncryptedSeedFile = serde_json::from_str(&content)?; + let nonce_bytes = b64decode(&file.nonce)?; + let ct = b64decode(&file.ciphertext)?; + + let keybytes = get_db_master_key(service, user)?; + let key = Key::from_slice(&keybytes); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&nonce_bytes); + + let pt = cipher.decrypt(nonce, ct.as_ref())?; + + // write with restricted permissions + fs::write(out_path, &pt)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(out_path, fs::Permissions::from_mode(0o600))?; + } + Ok(()) +} + +/// Decrypt an encrypted file and return the plaintext bytes in memory (avoid writing to disk) +pub fn decrypt_file_to_memory(in_path: &str, service: &str, user: &str) -> Result> { + let content = fs::read_to_string(in_path)?; + let file: EncryptedSeedFile = serde_json::from_str(&content)?; + let nonce_bytes = b64decode(&file.nonce)?; + let ct = b64decode(&file.ciphertext)?; + + let keybytes = get_db_master_key(service, user)?; + let key = Key::from_slice(&keybytes); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&nonce_bytes); + + let pt = cipher.decrypt(nonce, ct.as_ref())?; + Ok(pt) +} +/// Overwrite the file content with zeros and then remove the file. Best-effort secure delete. +pub fn secure_delete(path: &str) -> Result<()> { + if !Path::new(path).exists() { + return Ok(()); + } + + // Platform specific helpers + #[cfg(target_os = "macos")] + { + use std::process::Command; + // Prefer `srm -s` (secure rm) if available + if Command::new("srm").arg("-s").arg(path).status().map(|s| s.success()).unwrap_or(false) { + return Ok(()); + } + // Fallback to `shred -u` if available + if Command::new("shred").arg("-u").arg(path).status().map(|s| s.success()).unwrap_or(false) { + return Ok(()); + } + // Fallback to `rm -P` which attempts to overwrite + if Command::new("rm").arg("-P").arg(path).status().map(|s| s.success()).unwrap_or(false) { + return Ok(()); + } + // else fall through to generic overwrite + } + + #[cfg(target_os = "windows")] + { + // Use Windows API `ReplaceFile` as a best-effort removal after overwrite + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + use winapi::um::winbase::FILE_FLAG_WRITE_THROUGH; + use winapi::um::winbase::ReplaceFileW; + use std::os::windows::prelude::OsStrExt; + + let metadata = fs::metadata(path)?; + let len = metadata.len(); + let mut f = OpenOptions::new().write(true).custom_flags(FILE_FLAG_WRITE_THROUGH).open(path)?; + // overwrite multiple patterns (zeros, 0xFF, random) + let mut remaining = len; + let mut zeros = vec![0u8; 4096]; + let mut ff = vec![0xFFu8; 4096]; + while remaining > 0 { + let write_len = std::cmp::min(remaining, zeros.len() as u64) as usize; + f.write_all(&zeros[0..write_len])?; + f.flush()?; + f.write_all(&ff[0..write_len])?; + f.flush()?; + let mut rnd = vec![0u8; write_len]; + OsRng.fill_bytes(&mut rnd); + f.write_all(&rnd)?; + f.flush()?; + remaining -= write_len as u64; + } + f.sync_all()?; + drop(f); + + // Attempt to replace the file with an empty temporary file + let tmp = tempfile::NamedTempFile::new()?; + let tmp_wstr: Vec = tmp.path().as_os_str().encode_wide().chain(std::iter::once(0)).collect(); + let path_wstr: Vec = std::path::Path::new(path).as_os_str().encode_wide().chain(std::iter::once(0)).collect(); + unsafe { + let r = ReplaceFileW(path_wstr.as_ptr(), tmp_wstr.as_ptr(), std::ptr::null(), 0, std::ptr::null_mut(), std::ptr::null_mut()); + if r == 0 { + // fallback to delete + let _ = fs::remove_file(path); + } else { + let _ = fs::remove_file(tmp.path()); + } + } + return Ok(()); + } + + // Generic Unix-like overwrite (including Linux) + { + let metadata = fs::metadata(path)?; + let len = metadata.len(); + let mut f = fs::OpenOptions::new().write(true).open(path)?; + // overwrite with zeros + let zeros = vec![0u8; 4096]; + let mut remaining = len; + while remaining > 0 { + let write_len = std::cmp::min(remaining, zeros.len() as u64) as usize; + f.write_all(&zeros[0..write_len])?; + remaining -= write_len as u64; + } + f.sync_all()?; + drop(f); + fs::remove_file(path)?; + Ok(()) + } +} +#[cfg(test)] +mod tests { + use super::*; + use tempfile::NamedTempFile; + + #[test] + fn roundtrip_encrypt_decrypt() { + let mut seed = [0u8; 32]; + OsRng.fill_bytes(&mut seed); + let tmp = NamedTempFile::new().unwrap(); + let path = tmp.path().to_str().unwrap(); + encrypt_seed_file(&seed, "password123", path).expect("encrypt"); + let out = decrypt_seed_file(path, "password123").expect("decrypt"); + assert_eq!(seed, out); + } + + #[test] + fn db_master_key_encrypt_decrypt_file() { + // prepare sample file + let tmp_in = NamedTempFile::new().unwrap(); + let in_path = tmp_in.path().to_str().unwrap(); + fs::write(in_path, b"hello-db") .unwrap(); + + // generate master key + let _ = generate_db_master_key("cryptec-db", "test-user"); + let tmp_out = NamedTempFile::new().unwrap(); + let out_path = tmp_out.path().to_str().unwrap(); + + encrypt_file_with_master_key(in_path, out_path, "cryptec-db", "test-user").expect("encrypt file"); + + // decrypt to another temp path + let tmp_dec = NamedTempFile::new().unwrap(); + let dec_path = tmp_dec.path().to_str().unwrap(); + decrypt_file_with_master_key(out_path, dec_path, "cryptec-db", "test-user").expect("decrypt file"); + + let content = fs::read(dec_path).expect("read"); + assert_eq!(content, b"hello-db"); + } + + #[test] + fn secure_delete_removes_file() { + let tmp = NamedTempFile::new().unwrap(); + let p = tmp.path().to_str().unwrap().to_string(); + // write some data + fs::write(&p, b"sensitive").unwrap(); + assert!(Path::new(&p).exists()); + secure_delete(&p).expect("secure delete"); + assert!(!Path::new(&p).exists()); + } + + // Windows-specific secure delete test will only run on Windows + #[cfg(target_os = "windows")] + #[test] + fn secure_delete_windows_api() { + let tmp = NamedTempFile::new().unwrap(); + let p = tmp.path().to_str().unwrap().to_string(); + fs::write(&p, b"sensitive").unwrap(); + secure_delete(&p).expect("secure delete windows"); + assert!(!Path::new(&p).exists()); + } + + // macOS-specific test + #[cfg(target_os = "macos")] + #[test] + fn secure_delete_macos_cmd() { + let tmp = NamedTempFile::new().unwrap(); + let p = tmp.path().to_str().unwrap().to_string(); + fs::write(&p, b"sensitive").unwrap(); + secure_delete(&p).expect("secure delete macos"); + assert!(!Path::new(&p).exists()); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..beb1683 --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,104 @@ +//! CrypteCipher core (skeleton) +//! +//! This crate contains the core primitives (seed generation, key derivation, network interfaces). +//! It's a minimal skeleton; do NOT consider this production-ready. Use as a starting point. + +use rand::rngs::OsRng; +use rand::RngCore; +use secp256k1::SecretKey; + +mod btc; +mod bip39; +mod wallet_bdk; +mod lightning; +mod keystore; +mod hardware; +mod lnd; +mod monero; +mod wallet_manager; + +pub use btc::BtcWallet; +pub use bip39::Mnemonic; +pub use wallet_bdk::{BdkWallet, PersistentBdkWallet, DecryptedDb, InMemoryDb}; +pub use lightning::LightningClient; +pub use keystore::{encrypt_seed_file, decrypt_seed_file, decrypt_file_to_memory, store_password_in_keyring, retrieve_password_from_keyring}; +pub use hardware::{HardwareWallet, LedgerHw}; +pub use lnd::LndClient; +pub use monero::MoneroClient; +pub use monero_swap::{AtomicSwap, SwapRole}; +pub use swap_manager::SwapOffer; +pub use swap_sim::SwapSimulation; +pub use wallet_manager::{register_decrypted_db, close_decrypted_db, register_inmemory_db, close_inmemory_db, close_all, list_handles}; + +/// WalletCore: minimal demonstrative API +pub struct WalletCore { + // In a real implementation, secrets should be stored in a secure enclave or encrypted storage + pub(crate) seed: [u8; 32], +} + +impl WalletCore { + /// Generate a new random seed using system CSPRNG + pub fn generate_seed() -> [u8; 32] { + let mut seed = [0u8; 32]; + OsRng.fill_bytes(&mut seed); + seed + } + + /// Create a new core instance with a fresh seed + pub fn new() -> Self { + let s = Self::generate_seed(); + Self { seed: s } + } + + /// Example: derive an example secp256k1 private key from the seed (NOT a full HD derivation) + pub fn example_btc_privkey_hex(&self) -> String { + // This is ONLY an example; implement BIP32/BIP39 derivation in production + let mut buf = [0u8; 32]; + buf.copy_from_slice(&self.seed); + // Use SecretKey::from_slice to show how to validate + let sk = SecretKey::from_slice(&buf).expect("seed must be valid sk"); + hex::encode(sk.secret_bytes()) + } + + /// Return a public-facing fingerprint (for demo/testing) + pub fn fingerprint(&self) -> String { + // Return first 8 hex chars of seed as fingerprint + hex::encode(&self.seed[0..8]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_seed_len() { + let s = WalletCore::generate_seed(); + assert_eq!(s.len(), 32); + } + + #[test] + fn example_privkey() { + let w = WalletCore::new(); + let hex = w.example_btc_privkey_hex(); + assert_eq!(hex.len(), 64); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_seed_len() { + let s = WalletCore::generate_seed(); + assert_eq!(s.len(), 32); + } + + #[test] + fn example_privkey() { + let w = WalletCore::new(); + let hex = w.example_btc_privkey_hex(); + assert_eq!(hex.len(), 64); + } +} diff --git a/core/src/lightning.rs b/core/src/lightning.rs new file mode 100644 index 0000000..f52bf78 --- /dev/null +++ b/core/src/lightning.rs @@ -0,0 +1,51 @@ +/// Skeleton Lightning client abstraction +/// Supports external-node connection (e.g., LND/gRPC) and a placeholder for in-process rust-lightning + +pub enum LightningBackend { + ExternalNode { host: String, macaroon_path: Option, tls_cert_path: Option }, + InProcessPlaceholder, +} + +pub struct LightningClient { + backend: LightningBackend, +} + +impl LightningClient { + /// Create client that will connect to an external node (LND/c-lightning adapters can be added) + pub fn connect_external(host: &str, macaroon_path: Option<&str>, tls_cert_path: Option<&str>) -> Self { + Self { + backend: LightningBackend::ExternalNode { + host: host.to_string(), + macaroon_path: macaroon_path.map(|s| s.to_string()), + tls_cert_path: tls_cert_path.map(|s| s.to_string()), + } + } + } + + /// Create an in-process placeholder (use rust-lightning integration later) + pub fn placeholder() -> Self { + Self { backend: LightningBackend::InProcessPlaceholder } + } + + /// Example: return a status string (shows backend mode) - replace with real RPC calls later + pub fn status(&self) -> String { + match &self.backend { + LightningBackend::ExternalNode { host, .. } => format!("connected to external node at {}", host), + LightningBackend::InProcessPlaceholder => "in-process rust-lightning placeholder".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lightning_placeholder_status() { + let c = LightningClient::placeholder(); + assert!(c.status().contains("placeholder")); + + let e = LightningClient::connect_external("127.0.0.1:10009", None, None); + assert!(e.status().contains("127.0.0.1")); + } +} diff --git a/core/src/lnd.rs b/core/src/lnd.rs new file mode 100644 index 0000000..0743b1c --- /dev/null +++ b/core/src/lnd.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use std::process::Command; +use serde_json::Value; + +/// Lightweight wrapper that calls `lncli` if available. This is pragmatic for early integration +/// without pulling in gRPC/proto dependencies. For production, use direct gRPC (tonic + protos). +pub struct LndClient { + pub host: String, + pub macaroon_path: Option, +} + +impl LndClient { + pub fn new(host: &str, macaroon_path: Option<&str>) -> Self { + Self { host: host.to_string(), macaroon_path: macaroon_path.map(|s| s.to_string()) } + } + + /// Call `lncli --rpcserver getinfo` and parse JSON output (requires lncli in PATH) + pub fn get_info(&self) -> Result { + let mut cmd = Command::new("lncli"); + cmd.arg("--rpcserver").arg(&self.host).arg("getinfo"); + if let Some(m) = &self.macaroon_path { + cmd.arg("--macaroonpath").arg(m); + } + let out = cmd.output()?; + if !out.status.success() { + return Err(anyhow::anyhow!("lncli call failed: {}", String::from_utf8_lossy(&out.stderr))); + } + let s = String::from_utf8_lossy(&out.stdout); + let v: Value = serde_json::from_str(&s)?; + Ok(v) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lncli_not_present_returns_err() { + let c = LndClient::new("127.0.0.1:10009", None); + let r = c.get_info(); + assert!(r.is_err()); + } +} diff --git a/core/src/monero.rs b/core/src/monero.rs new file mode 100644 index 0000000..8d63659 --- /dev/null +++ b/core/src/monero.rs @@ -0,0 +1,99 @@ +use anyhow::Result; +use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +/// Minimal Monero Wallet RPC client wrapper (blocking) +/// Note: This talks to `monero-wallet-rpc` JSON-RPC interface. +pub struct MoneroClient { + pub rpc_url: String, + pub client: Client, + pub auth: Option<(String, String)>, +} + +#[derive(Serialize, Deserialize, Debug)] +struct RpcResponse { + jsonrpc: Option, + id: Option, + result: Option, + error: Option, +} + +impl MoneroClient { + pub fn new(rpc_url: &str, username: Option<&str>, password: Option<&str>) -> Self { + let client = Client::builder().build().unwrap(); + let auth = match (username, password) { + (Some(u), Some(p)) => Some((u.to_string(), p.to_string())), + _ => None, + }; + Self { rpc_url: rpc_url.to_string(), client, auth } + } + + fn rpc_call Deserialize<'de>>(&self, method: &str, params: serde_json::Value) -> Result { + let req = json!({"jsonrpc":"2.0","id":"0","method":method,"params":params}); + let mut r = self.client.post(&self.rpc_url); + if let Some((u,p)) = &self.auth { + r = r.basic_auth(u, Some(p)); + } + let resp = r.json(&req).send()?; + let v: RpcResponse = resp.json()?; + if let Some(err) = v.error { + return Err(anyhow::anyhow!("monero rpc error: {}", err)); + } + if let Some(res) = v.result { + Ok(res) + } else { + Err(anyhow::anyhow!("empty monero rpc response")) + } + } + + pub fn get_balance(&self) -> Result { + // get_balance params: { "account_index": 0 } + let r: serde_json::Value = self.rpc_call("get_balance", json!({}))?; + Ok(r) + } + + pub fn get_address(&self) -> Result { + let r: serde_json::Value = self.rpc_call("get_address", json!({}))?; + Ok(r) + } + + /// Create an integrated address (monero-wallet-rpc: make_integrated_address) + pub fn make_integrated_address(&self, payment_id: Option<&str>) -> Result { + let params = if let Some(id) = payment_id { json!({"payment_id": id}) } else { json!({}) }; + let r: serde_json::Value = self.rpc_call("make_integrated_address", params)?; + Ok(r) + } + + pub fn transfer(&self, amount: u64, address: &str) -> Result { + // amount param is in atomic units; convert depending on monero units expectation + let r: serde_json::Value = self.rpc_call("transfer", json!({"destinations":[{"amount":amount,"address":address}]}))?; + Ok(r) + } + + /// Request a tx proof (get_tx_proof) for an outgoing transaction id (or address/proof options) + pub fn get_tx_proof(&self, txid: &str, address: Option<&str>) -> Result { + let params = if let Some(a) = address { json!({"txid": txid, "address": a}) } else { json!({"txid": txid}) }; + let r: serde_json::Value = self.rpc_call("get_tx_proof", params)?; + Ok(r) + } + + /// Check a tx proof (check_tx_proof) + pub fn check_tx_proof(&self, txid: &str, address: &str, signature: &str) -> Result { + let r: serde_json::Value = self.rpc_call("check_tx_proof", json!({"txid": txid, "address": address, "signature": signature}))?; + Ok(r) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn monero_client_unreachable() { + // Connect to a likely-unavailable endpoint; expect an error + let c = MoneroClient::new("http://127.0.0.1:29999/json_rpc", None, None); + let r = c.get_balance(); + assert!(r.is_err()); + } +} diff --git a/core/src/monero_swap.rs b/core/src/monero_swap.rs new file mode 100644 index 0000000..145c2b0 --- /dev/null +++ b/core/src/monero_swap.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use serde_json::json; + +/// Atomic swap scaffold for BTC <-> XMR (research-level scaffolding) +/// This module provides high-level steps and helpers, not a production-ready implementation. + +pub enum SwapRole { + Maker, + Taker, +} + +pub struct AtomicSwap { + pub role: SwapRole, + pub btc_amount_sats: u64, + pub xmr_amount_piconero: u64, +} + +impl AtomicSwap { + pub fn new(role: SwapRole, btc_amt: u64, xmr_amt: u64) -> Self { + Self { role, btc_amount_sats: btc_amt, xmr_amount_piconero: xmr_amt } + } + + /// Generate the initial swap contract (HTLC / script) for Bitcoin and corresponding XMR output info + /// Returns JSON object with steps and parameters required by counterparties. + pub fn create_swap_contract(&self) -> Result { + // High level: for BTC create a script with hashlock + timelock, for XMR create integrated address and tx id + // This is a research scaffold: we only return placeholder fields here. + Ok(json!({ + "btc": { + "htlc_script_hex": "", + "refund_timelock": 1440 + }, + "xmr": { + "integrated_address": "", + "tx_extra": "" + } + })) + } + + /// Validate proof of funding and provide next steps (placeholder) + pub fn validate_and_step(&self, proof: serde_json::Value) -> Result { + Ok(json!({"status":"ok","proof":proof})) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_swap_scaffold() { + let s = AtomicSwap::new(SwapRole::Maker, 1000, 1000); + let c = s.create_swap_contract().expect("create"); + assert!(c.get("btc").is_some()); + assert!(c.get("xmr").is_some()); + } +} diff --git a/core/src/swap_manager.rs b/core/src/swap_manager.rs new file mode 100644 index 0000000..9b2e2a8 --- /dev/null +++ b/core/src/swap_manager.rs @@ -0,0 +1,58 @@ +use anyhow::Result; +use serde_json::json; +use crate::monero::MoneroClient; +use crate::btc_htlc::{build_htlc_redeem_script, htlc_p2wsh_address}; +use bitcoin::util::key::PublicKey; +use bitcoin::network::constants::Network; + +/// High-level swap manager that ties BTC HTLC creation and Monero proofs together. +/// This is an orchestrator stub for automating swap steps. + +pub struct SwapOffer { + pub btc_sats: u64, + pub xmr_piconero: u64, + pub btc_hash: [u8;32], + pub recipient_pub: PublicKey, + pub refund_pub: PublicKey, + pub locktime: u32, +} + +impl SwapOffer { + pub fn new(btc_sats: u64, xmr_piconero: u64, btc_hash: [u8;32], recipient_pub: PublicKey, refund_pub: PublicKey, locktime: u32) -> Self { + Self { btc_sats, xmr_piconero, btc_hash, recipient_pub, refund_pub, locktime } + } + + pub fn prepare_btc_htlc(&self, network: Network) -> serde_json::Value { + let redeem = build_htlc_redeem_script(&self.btc_hash, &self.recipient_pub, &self.refund_pub, self.locktime); + let addr = htlc_p2wsh_address(&redeem, network); + json!({"address": addr.to_string(), "redeem_hex": hex::encode(redeem.to_bytes())}) + } + + pub fn verify_xmr_payment(&self, rpc_url: &str, username: Option<&str>, password: Option<&str>) -> Result { + let client = MoneroClient::new(rpc_url, username, password); + // in real flow we'd watch for payment to integrated address and then request proof; here we just call get_balance as placeholder + let bal = client.get_balance()?; + Ok(json!({"balance": bal})) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::secp256k1::{Secp256k1, SecretKey}; + use rand::rngs::OsRng; + + #[test] + fn prepare_swap_offer_btc_htlc() { + let secp = Secp256k1::new(); + let sk1 = SecretKey::new(&mut OsRng::default()); + let sk2 = SecretKey::new(&mut OsRng::default()); + let pk1 = PublicKey { key: bitcoin::secp256k1::PublicKey::from_secret_key(&secp, &sk1), compressed: true }; + let pk2 = PublicKey { key: bitcoin::secp256k1::PublicKey::from_secret_key(&secp, &sk2), compressed: true }; + let mut h = [0u8;32]; + OsRng.fill_bytes(&mut h); + let s = SwapOffer::new(1000, 1000000, h, pk1, pk2, 500); + let out = s.prepare_btc_htlc(Network::Testnet); + assert!(out.get("address").is_some()); + } +} diff --git a/core/src/swap_sim.rs b/core/src/swap_sim.rs new file mode 100644 index 0000000..64da1d9 --- /dev/null +++ b/core/src/swap_sim.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use serde_json::json; +use rand::rngs::OsRng; +use rand::RngCore; +use sha2::{Sha256, Digest}; + +/// Simple in-process BTC<->XMR swap simulation (for testing and demonstration only) +/// This does not interact with real networks — it simulates the protocol flow using generated preimages. + +pub struct SwapSimulation { + pub preimage: Vec, + pub hash: Vec, + pub btc_sats: u64, + pub xmr_piconero: u64, +} + +impl SwapSimulation { + pub fn new(btc_sats: u64, xmr_piconero: u64) -> Self { + let mut pre = vec![0u8; 32]; + OsRng.fill_bytes(&mut pre); + let mut hasher = Sha256::new(); + hasher.update(&pre); + let h = hasher.finalize().to_vec(); + Self { preimage: pre, hash: h, btc_sats, xmr_piconero } + } + + /// Maker creates BTC HTLC (simulated) and XMR integrated address + pub fn maker_create_offer(&self) -> serde_json::Value { + json!({ + "btc_htlc_hash": hex::encode(&self.hash), + "xmr_amount": self.xmr_piconero, + }) + } + + /// Taker observes XMR payment (simulated) and provides a proof object (simulated) + pub fn taker_provide_proof(&self) -> serde_json::Value { + // Simulate proof containing txid placeholder and amount + json!({"txid":"simulated-xmr-txid","amount": self.xmr_piconero }) + } + + /// Maker validates proof (simulated) and provides preimage to redeem BTC HTLC + pub fn maker_validate_and_reveal(&self, proof: serde_json::Value) -> serde_json::Value { + // In real flow verify proof on Monero node; here assume proof valid + json!({"status":"proof_valid","preimage": hex::encode(&self.preimage)}) + } + + /// Taker uses revealed preimage to redeem BTC HTLC (simulated) + pub fn taker_redeem_with_preimage(&self, preimage_hex: &str) -> serde_json::Value { + let pre = hex::decode(preimage_hex).unwrap_or_default(); + let mut hasher = Sha256::new(); + hasher.update(&pre); + let h = hasher.finalize().to_vec(); + let ok = h == self.hash; + json!({"redeem_ok": ok}) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simulate_swap_flow() { + let sim = SwapSimulation::new(1000, 1000000); + let offer = sim.maker_create_offer(); + assert!(offer.get("btc_htlc_hash").is_some()); + let proof = sim.taker_provide_proof(); + let reveal = sim.maker_validate_and_reveal(proof); + assert!(reveal.get("preimage").is_some()); + let prehex = reveal.get("preimage").unwrap().as_str().unwrap(); + let redeem = sim.taker_redeem_with_preimage(prehex); + assert_eq!(redeem.get("redeem_ok").unwrap().as_bool().unwrap(), true); + } +} \ No newline at end of file diff --git a/core/src/wallet_bdk.rs b/core/src/wallet_bdk.rs new file mode 100644 index 0000000..b268d34 --- /dev/null +++ b/core/src/wallet_bdk.rs @@ -0,0 +1,403 @@ +use bdk::Wallet; +use bdk::database::{MemoryDatabase, SqliteDatabase}; +use bdk::wallet::AddressIndex; +use bdk::blockchain::{noop_progress::NoopProgress, ElectrumBlockchain, ElectrumBlockchainConfig}; +use bdk::keys::bip39::{Mnemonic as BdkMnemonic}; +use bdk::descriptor::DescriptorTemplateOut; +use bitcoin::Network; +use anyhow::Result; +use std::path::Path; + +/// Minimal BDK wallet scaffold with Electrum support (in-memory) +pub struct BdkWallet { + wallet: Wallet, +} + +impl BdkWallet { + /// Create a new BDK wallet from a BIP39 mnemonic (and passphrase) for the specified network + pub fn new_from_mnemonic(phrase: &str, passphrase: &str, network: Network) -> Result { + // Use BDK's bip39 helper to create descriptor templates + let mnemonic = BdkMnemonic::from_str(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?; + let xkey: DescriptorTemplateOut = bdk::keys::bip39::translate_mnemonic(&mnemonic, passphrase, network) + .map_err(|e| anyhow::anyhow!(e.to_string()))?; + + // For demo, use the first "external" descriptor for a single-key wallet + let descriptor = xkey.external.clone(); + let change_descriptor = xkey.internal.clone(); + + let wallet = Wallet::new_offline(&descriptor, Some(&change_descriptor), network, MemoryDatabase::default())?; + Ok(Self { wallet }) + } + + /// Create and sync with Electrum server (blocking) + pub fn sync_with_electrum(&self, electrum_url: &str) -> Result<()> { + let config = ElectrumBlockchainConfig::from(electrum_url); + let blockchain = ElectrumBlockchain::from_config(&config)?; + self.wallet.sync(&blockchain, NoopProgress)?; + Ok(()) + } + + /// Return the next receiving address + pub fn get_new_address(&self) -> Result { + let addr = self.wallet.get_address(AddressIndex::New)?; + Ok(addr.to_string()) + } + + /// Build a PSBT for a single recipient amount (satoshi) + pub fn create_psbt(&self, to_address: &str, satoshis: u64) -> Result { + let addr = to_address.parse::()?; + let mut builder = bdk::TxBuilder::new(); + builder = builder.add_recipient(addr.script_pubkey(), satoshis); + let (psbt, _details) = self.wallet.build_tx(builder)?; + let bs = base64::encode(&psbt.serialize()); + Ok(bs) + } + + /// Sign PSBT (NOTE: using the internal keys of the wallet) + pub fn sign_psbt_base64(&self, psbt_b64: &str) -> Result { + let raw = base64::decode(psbt_b64)?; + let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = bitcoin::consensus::deserialize(&raw)?; + self.wallet.sign(&mut psbt, bdk::SignOptions::default())?; + let out = base64::encode(&psbt.serialize()); + Ok(out) + } + + /// List UTXOs (simple) + pub fn list_utxos(&self) -> Result> { + let utxos = self.wallet.list_unspent()?; + Ok(utxos) + } +} + +/// Persistent wallet backed by SQLite file +pub struct PersistentBdkWallet { + wallet: Wallet, +} + +impl PersistentBdkWallet { + /// Create or open a persistent wallet using a SQLite DB file at `db_path`. + /// `db_path` should be a filesystem path like `/path/to/wallet.db`. + pub fn new_from_mnemonic_sqlite(phrase: &str, passphrase: &str, network: Network, db_path: &str) -> Result { + let mnemonic = BdkMnemonic::from_str(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?; + let xkey: DescriptorTemplateOut = bdk::keys::bip39::translate_mnemonic(&mnemonic, passphrase, network) + .map_err(|e| anyhow::anyhow!(e.to_string()))?; + + let descriptor = xkey.external.clone(); + let change_descriptor = xkey.internal.clone(); + + // Ensure directory exists + let p = Path::new(db_path); + if let Some(parent) = p.parent() { + std::fs::create_dir_all(parent)?; + } + + let db = SqliteDatabase::new(db_path)?; + let wallet = Wallet::new(&descriptor, Some(&change_descriptor), network, db)?; + Ok(Self { wallet }) + } + + /// Create and sync with Electrum server (blocking) + /// `electrum_url` can be `ssl://electrum.example:50002` or `tcp://host:50001` + /// TLS verification is handled by underlying Electrum client; for custom CA/TLS options change configuration here. + pub fn sync_with_electrum(&self, electrum_url: &str) -> Result<()> { + let config = ElectrumBlockchainConfig::from(electrum_url); + let blockchain = ElectrumBlockchain::from_config(&config)?; + self.wallet.sync(&blockchain, NoopProgress)?; + Ok(()) + } + + pub fn get_new_address(&self) -> Result { + let addr = self.wallet.get_address(AddressIndex::New)?; + Ok(addr.to_string()) + } + + pub fn create_psbt(&self, to_address: &str, satoshis: u64) -> Result { + let addr = to_address.parse::()?; + let mut builder = bdk::TxBuilder::new(); + builder = builder.add_recipient(addr.script_pubkey(), satoshis); + let (psbt, _details) = self.wallet.build_tx(builder)?; + let bs = base64::encode(&psbt.serialize()); + Ok(bs) + } + + pub fn sign_psbt_base64(&self, psbt_b64: &str) -> Result { + let raw = base64::decode(psbt_b64)?; + let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = bitcoin::consensus::deserialize(&raw)?; + self.wallet.sign(&mut psbt, bdk::SignOptions::default())?; + let out = base64::encode(&psbt.serialize()); + Ok(out) + } + + pub fn list_utxos(&self) -> Result> { + let utxos = self.wallet.list_unspent()?; + Ok(utxos) + } + + /// Encrypt an existing SQLite DB file using the OS keyring-managed master key + pub fn encrypt_db_file(db_path_plain: &str, db_path_encrypted: &str, service: &str, user: &str) -> Result<()> { + // Ensure master key exists + if let Err(_) = crate::keystore::get_db_master_key(service, user) { + crate::keystore::generate_db_master_key(service, user)?; + } + crate::keystore::encrypt_file_with_master_key(db_path_plain, db_path_encrypted, service, user)?; + Ok(()) + } + + /// Decrypt an encrypted DB file to a temporary file and return its path + pub fn decrypt_db_to_tempfile(db_path_encrypted: &str, service: &str, user: &str) -> Result { + // Create a secure temporary file with restricted permissions and write decrypted DB to it + let mut tmp = tempfile::NamedTempFile::new()?; + let tmp_path = tmp.path().to_path_buf(); + + // Decrypt into the temp file path + crate::keystore::decrypt_file_with_master_key(db_path_encrypted, tmp_path.to_str().unwrap(), service, user)?; + + // Restrict permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o600))?; + } + + // Keep the NamedTempFile alive by converting to persist it and manage deletion ourselves + let named = tmp.into_temp_path(); + let pathbuf = named.to_path_buf(); + // Note: into_temp_path keeps the file on disk but removes auto-delete; we'll delete securely in Drop + let dd = DecryptedDb { path: pathbuf }; + Ok(dd) + } + + /// Create a persistent wallet, encrypt the DB file with the OS keyring master key, and remove the plaintext DB + pub fn create_encrypted_persistent_wallet(phrase: &str, passphrase: &str, network: Network, db_path_encrypted: &str, service: &str, user: &str) -> Result<()> { + // create plaintext DB in a securely-created temp file with restricted permissions + let mut tmp = tempfile::NamedTempFile::new()?; + let tmp_path = tmp.path().to_str().unwrap().to_string(); + + // set restrictive permissions on temp file + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o600))?; + } + + // create wallet and DB at tmp_path (DB exists only briefly) + let _w = PersistentBdkWallet::new_from_mnemonic_sqlite(phrase, passphrase, network, &tmp_path)?; + + // ensure master key and encrypt + if let Err(_) = crate::keystore::get_db_master_key(service, user) { + crate::keystore::generate_db_master_key(service, user)?; + } + crate::keystore::encrypt_file_with_master_key(&tmp_path, db_path_encrypted, service, user)?; + + // Best-effort: overwrite plaintext file contents and remove file immediately + // ensure file is closed before secure delete + drop(tmp); + let _ = crate::keystore::secure_delete(&tmp_path); + Ok(()) + } + +/// Temporary decrypted DB handle. When dropped, it securely deletes the underlying file. +pub struct DecryptedDb { + pub path: std::path::PathBuf, +} + +impl DecryptedDb { + pub fn path(&self) -> &std::path::Path { + &self.path + } + + /// Consume and securely delete the underlying file immediately + pub fn close_and_delete(self) -> Result<()> { + let p = self.path.to_str().unwrap().to_string(); + crate::keystore::secure_delete(&p)?; + Ok(()) + } +} + +impl Drop for DecryptedDb { + fn drop(&mut self) { + let _ = crate::keystore::secure_delete(self.path.to_str().unwrap()); + } +} + +/// In-memory SQLite DB wrapper using sqlite3_deserialize to avoid persistent plaintext DB files. +pub struct InMemoryDb { + pub conn: rusqlite::Connection, +} + +impl InMemoryDb { + /// Create an in-memory DB by decrypting an encrypted DB file into memory and deserializing it into an in-memory sqlite DB. + pub fn from_encrypted_file_in_memory(encrypted_path: &str, service: &str, user: &str) -> Result { + // Decrypt bytes into memory + let bytes = crate::keystore::decrypt_file_to_memory(encrypted_path, service, user)?; + + // Open an in-memory SQLite connection + let conn = rusqlite::Connection::open_in_memory()?; + + // Try SQLite deserialize to load bytes into the in-memory DB (unsafe C API call) + let deserialize_result: Result<(), anyhow::Error> = unsafe { + use rusqlite::ffi; + use std::ffi::CString; + let db_handle = conn.handle(); + let dbname = CString::new("main").unwrap(); + // Allocate a buffer that SQLite will take ownership of (SQLITE_DESERIALIZE_FREEONCLOSE) + let mut buf = bytes.clone(); + let p = buf.as_mut_ptr(); + let len = buf.len() as i64; + let rc = ffi::sqlite3_deserialize(db_handle, dbname.as_ptr(), p as *mut _, len, len, ffi::SQLITE_DESERIALIZE_FREEONCLOSE); + if rc != ffi::SQLITE_OK { + Err(anyhow::anyhow!("sqlite3_deserialize failed: {}", rc)) + } else { + // buf ownership transferred to SQLite; avoid dropping it here + std::mem::forget(buf); + Ok(()) + } + }; + + if let Err(_e) = deserialize_result { + // Fallback: write bytes to a secure temp file and use SQLite backup API to load into memory + let mut tmp = tempfile::NamedTempFile::new()?; + let tmp_path = tmp.path().to_str().unwrap().to_string(); + // restrict permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o600))?; + } + std::fs::write(&tmp_path, &bytes)?; + + // Open file-backed DB and backup into in-memory conn + let file_conn = rusqlite::Connection::open(&tmp_path)?; + let mut backup = rusqlite::backup::Backup::new(&file_conn, &conn)?; + backup.step(-1)?; // copy entire DB + backup.finish()?; + + // secure delete temp plaintext file + let _ = crate::keystore::secure_delete(&tmp_path); + } + + Ok(Self { conn }) + } + + /// Run an arbitrary SQL query and return results as JSON array of rows + pub fn run_query_json(&self, sql: &str) -> Result { + let mut stmt = self.conn.prepare(sql)?; + let column_count = stmt.column_count(); + let column_names: Vec = (0..column_count).map(|i| stmt.column_name(i).unwrap_or("").to_string()).collect(); + let mut rows = stmt.query([])?; + let mut out_rows = Vec::new(); + while let Some(r) = rows.next()? { + let mut map = serde_json::Map::new(); + for (i, name) in column_names.iter().enumerate() { + let val: rusqlite::types::Value = r.get(i)?; + let j = match val { + rusqlite::types::Value::Null => serde_json::Value::Null, + rusqlite::types::Value::Integer(i) => serde_json::json!(i), + rusqlite::types::Value::Real(f) => serde_json::json!(f), + rusqlite::types::Value::Text(s) => serde_json::json!(s), + rusqlite::types::Value::Blob(b) => serde_json::json!(base64::encode(&b)), + }; + map.insert(name.clone(), j); + } + out_rows.push(serde_json::Value::Object(map)); + } + Ok(serde_json::Value::Array(out_rows)) + } + + /// Export an on-disk snapshot of the in-memory DB as byte vector. + /// First try sqlite3_serialize; if unavailable, fallback to backup-to-temp-file. + pub fn export_snapshot_bytes(&self) -> Result> { + // Try sqlite3_serialize + unsafe { + use rusqlite::ffi; + use std::ffi::CString; + let db_handle = self.conn.handle(); + let name = CString::new("main").unwrap(); + let mut len: rusqlite::ffi::sqlite3_int64 = 0; + let p = ffi::sqlite3_serialize(db_handle, name.as_ptr(), &mut len as *mut _, ffi::SQLITE_SERIALIZE_NOCOPY); + if !p.is_null() { + let slice = std::slice::from_raw_parts(p as *const u8, len as usize); + let vec = slice.to_vec(); + // free buffer allocated by SQLite + ffi::sqlite3_free(p as *mut _); + return Ok(vec); + } + } + + // Fallback: backup in-memory DB to a temp file then read bytes + let tmp = tempfile::NamedTempFile::new()?; + let tmp_path = tmp.path().to_str().unwrap().to_string(); + let file_conn = rusqlite::Connection::open(&tmp_path)?; + let mut backup = rusqlite::backup::Backup::new(&self.conn, &file_conn)?; + backup.step(-1)?; + backup.finish()?; + let bytes = std::fs::read(&tmp_path)?; + let _ = crate::keystore::secure_delete(&tmp_path); + Ok(bytes) + } +} + + +impl Drop for InMemoryDb { + fn drop(&mut self) { + // conn will be closed automatically; any memory backed pages freed + } +} +#[cfg(test)] +mod tests { + use super::*; + use crate::bip39::Mnemonic; + use bitcoin::Network; + use tempfile::tempdir; + + #[test] + fn bdk_wallet_create_and_addr() { + let m = Mnemonic::generate(128); + let wallet = BdkWallet::new_from_mnemonic(&m.phrase, "", Network::Testnet).expect("create wallet"); + let addr = wallet.get_new_address().expect("get address"); + assert!(!addr.is_empty()); + } + + #[test] + fn persistent_wallet_sqlite_roundtrip() { + let m = Mnemonic::generate(128); + let dir = tempdir().unwrap(); + let dbpath = dir.path().join("wallet.db"); + let dbs = dbpath.to_str().unwrap(); + let w = PersistentBdkWallet::new_from_mnemonic_sqlite(&m.phrase, "", Network::Testnet, dbs).expect("create persistent"); + let a = w.get_new_address().expect("addr"); + assert!(!a.is_empty()); + } + + #[test] + fn encrypt_and_decrypt_db_roundtrip() { + let m = Mnemonic::generate(128); + let dir = tempdir().unwrap(); + let dbpath = dir.path().join("wallet.db"); + let dbs = dbpath.to_str().unwrap(); + + // create plaintext DB + let _w = PersistentBdkWallet::new_from_mnemonic_sqlite(&m.phrase, "", Network::Testnet, dbs).expect("create persistent"); + + let encrypted = dir.path().join("wallet.db.enc"); + let encs = encrypted.to_str().unwrap(); + + // generate master key and encrypt + let _ = crate::keystore::generate_db_master_key("cryptec-db", "test-user"); + PersistentBdkWallet::encrypt_db_file(dbs, encs, "cryptec-db", "test-user").expect("encrypt db"); + + // remove plaintext DB and decrypt to temp + let _ = std::fs::remove_file(dbs); + let tmp_dec = PersistentBdkWallet::decrypt_db_to_tempfile(encs, "cryptec-db", "test-user").expect("decrypt db"); + + // try to open decrypted DB + let db = SqliteDatabase::new(tmp_dec.path().to_str().unwrap()).expect("open db"); + // if DB opens, we assume successful decrypt + assert!(db.path().is_some()); + + // drop handle and ensure file removal + let p = tmp_dec.path().to_str().unwrap().to_string(); + drop(tmp_dec); + assert!(!std::path::Path::new(&p).exists()); + } +} diff --git a/core/src/wallet_manager.rs b/core/src/wallet_manager.rs new file mode 100644 index 0000000..3c2b8e5 --- /dev/null +++ b/core/src/wallet_manager.rs @@ -0,0 +1,85 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use uuid::Uuid; +use anyhow::Result; + +use crate::wallet_bdk::{DecryptedDb, InMemoryDb}; + +/// Global wallet manager to track decrypted DB handles (temp files) and in-memory DBs. +/// Keys are UUID strings returned to consumers and used to close/inspect handles. + +lazy_static::lazy_static! { + static ref DECRYPTED_DB_HANDLES: Mutex>> = Mutex::new(HashMap::new()); + static ref INMEMORY_DB_HANDLES: Mutex>> = Mutex::new(HashMap::new()); +} + +/// Create and register a DecryptedDb and return its handle id +pub fn register_decrypted_db(dd: DecryptedDb) -> String { + let id = Uuid::new_v4().to_string(); + DECRYPTED_DB_HANDLES.lock().unwrap().insert(id.clone(), Arc::new(dd)); + id +} + +/// Close and remove a decrypted db handle by id +pub fn close_decrypted_db(id: &str) -> Result<()> { + if let Some(dd) = DECRYPTED_DB_HANDLES.lock().unwrap().remove(id) { + dd.close_and_delete()?; + } + Ok(()) +} + +/// Create and register an InMemoryDb and return its handle id +pub fn register_inmemory_db(im: InMemoryDb) -> String { + let id = Uuid::new_v4().to_string(); + INMEMORY_DB_HANDLES.lock().unwrap().insert(id.clone(), Arc::new(im)); + id +} + +/// Run a SQL query on an in-memory DB handle and return JSON string +pub fn inmemory_run_query(handle_id: &str, sql: &str) -> Result { + let map = INMEMORY_DB_HANDLES.lock().unwrap(); + if let Some(im) = map.get(handle_id) { + let r = im.run_query_json(sql)?; + Ok(serde_json::to_string(&r)?) + } else { + Ok("[]".to_string()) + } +} + +/// Export snapshot bytes base64-encoded for a handle +pub fn inmemory_export_snapshot_base64(handle_id: &str) -> Result { + let map = INMEMORY_DB_HANDLES.lock().unwrap(); + if let Some(im) = map.get(handle_id) { + let b = im.export_snapshot_bytes()?; + Ok(base64::encode(&b)) + } else { + Ok("".to_string()) + } +} + +/// Close and remove an in-memory db handle +pub fn close_inmemory_db(id: &str) -> Result<()> { + if let Some(_im) = INMEMORY_DB_HANDLES.lock().unwrap().remove(id) { + // Drop will free memory + } + Ok(()) +} + +/// Close all handles and attempt secure cleanup +pub fn close_all() -> Result<()> { + let mut dd = DECRYPTED_DB_HANDLES.lock().unwrap(); + for (_, d) in dd.drain() { + let _ = d.close_and_delete(); + } + let mut im = INMEMORY_DB_HANDLES.lock().unwrap(); + im.clear(); + Ok(()) +} + +/// List handle IDs (for debugging) +pub fn list_handles() -> Vec { + let mut vec = Vec::new(); + for k in DECRYPTED_DB_HANDLES.lock().unwrap().keys() { vec.push(k.clone()); } + for k in INMEMORY_DB_HANDLES.lock().unwrap().keys() { vec.push(k.clone()); } + vec +} \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..4b4c809 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,29 @@ +# Architecture (High level) + +## Goals +- **Non-custodial**: Users control seeds and private keys. +- **Privacy-first**: No KYC by default; Monero and coin selection for privacy. +- **Multi-currency**: Bitcoin (on-chain & Lightning) and Monero. +- **Secure**: Core crypto in Rust, audited libs, hardware wallet support optional. + +## Components +- Core (Rust): seed management, key derivation (BIP32/BIP39), Bitcoin on-chain interactions (rust-bitcoin), Lightning (rust-lightning or external node), Monero interactions (via RPC to `monerod`/`monero-wallet-rpc`), atomic swap scaffolding. +- Bindings: N-API (napi-rs) for desktop & Electron, native wrappers for iOS/Android to call Rust. +- Mobile: React Native (TypeScript) calling native module. +- Desktop: Electron + React using Node bindings. + +## Libraries & tools (recommended) +- Bitcoin: `rust-bitcoin`, `bdk` (for wallet abstractions), `rust-lightning` (for Lightning protocol) +- Monero: `monero` crate for primitives and RPC clients OR use JSON-RPC to a `monerod`/`monero-wallet-rpc` +- Node bindings: `napi-rs` or `neon` (napi-rs recommended) +- HD & Mnemonics: `bip39`, `bip32` crates (implement BIP39 with a well-reviewed crate) +- Key storage: integrate with platform KMS / secure enclave and support hardware wallets (Ledger/Trezor) + +## Security notes +- Never store cleartext seeds unencrypted on disk. +- Use OS-provided secure storage for seed encryption keys. +- Consider optional passphrase (BIP39 passphrase) and multi-factor unlocking. +- All crypto operations need a formal audit before production. + +## Atomic swaps +- BTC↔XMR atomic swaps are possible but complex — require HTLC-like constructs and cooperating protocols or third-party protocols. Start with a research phase and an off-chain exchange mediator if needed. diff --git a/electron/README.md b/electron/README.md new file mode 100644 index 0000000..b81b21c --- /dev/null +++ b/electron/README.md @@ -0,0 +1,8 @@ +# Electron (Desktop) - CrypteCipher + +This folder will contain the Electron-based desktop app. + +Notes: +- Use the `cryptec_bindings` Node addon (N-API) to call into the Rust core for cryptographic operations. +- For Bitcoin Lightning, you can either embed a rust-lightning node or connect to an external Lightning node (e.g., `lnd`/`c-lightning`). +- For Monero, consider running a full node or allowing users to configure a remote RPC node. diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000..9316749 --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,9 @@ +# Mobile (React Native) - CrypteCipher + +This folder will contain the React Native app. + +Recommendations: +- Use React Native + TypeScript (or Expo for rapid prototyping). +- For secure key operations on device, implement a native module that calls the Rust core via platform-native linking (Android: JNI/NDK; iOS: static/dynamic lib + Objective-C/Swift wrapper). +- Use platform secure storage (iOS Keychain / Android Keystore) for encrypted seed storage. +- For Monero, prefer using a remote `monerod`/`monero-wallet-rpc` for mobile to save resources, or use a light-weight daemon if available. diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml new file mode 100644 index 0000000..350a388 --- /dev/null +++ b/tools/cli/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cryptec_cli" +version = "0.1.0" +edition = "2021" + +[dependencies] +cryptec_core = { path = "../../core" } +serde_json = "1.0" diff --git a/tools/cli/src/main.rs b/tools/cli/src/main.rs new file mode 100644 index 0000000..446b656 --- /dev/null +++ b/tools/cli/src/main.rs @@ -0,0 +1,32 @@ +use cryptec_core::{Mnemonic, BdkWallet}; +use std::env; +use serde_json::json; + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("usage: cryptec_cli [args]"); + std::process::exit(1); + } + let cmd = &args[1]; + match cmd.as_str() { + "gen-mnemonic" => { + let m = Mnemonic::generate(128); + println!("{}", m.phrase); + } + "bdk-new-addr" => { + let phrase = if args.len() > 2 { args[2].clone() } else { Mnemonic::generate(128).phrase }; + let w = BdkWallet::new_from_mnemonic(&phrase, "", bitcoin::Network::Testnet).expect("create wallet"); + let addr = w.get_new_address().expect("addr"); + println!("{}", addr); + } + "version" => { + let v = json!({"name":"cryptec_cli","version":"0.1.0"}); + println!("{}", v.to_string()); + } + _ => { + eprintln!("unknown command"); + std::process::exit(2); + } + } +}