Initial CrypteCipher scaffold and features
Some checks failed
CI / build (macos-latest, stable) (push) Has been cancelled
CI / build (ubuntu-latest, stable) (push) Has been cancelled
CI / build (windows-latest, stable) (push) Has been cancelled

This commit is contained in:
MoCipher 2026-02-12 00:02:00 +01:00
commit 267d8f6edd
27 changed files with 2063 additions and 0 deletions

42
.github/workflows/ci.yml vendored Normal file
View file

@ -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

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
target/
node_modules/
**/*.log
.env
.DS_Store

19
README.md Normal file
View file

@ -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.

16
bindings/Cargo.toml Normal file
View file

@ -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" }

9
bindings/package.json Normal file
View file

@ -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": {}
}

272
bindings/src/lib.rs Normal file
View file

@ -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<String>, pass: Option<String>) -> 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<String>, pass: Option<String>) -> 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<String>, pass: Option<String>, txid: String, address: Option<String>) -> 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<String>, pass: Option<String>, 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<String>, pass: Option<String>, 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<String>, pass: Option<String>, payment_id: Option<String>) -> 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>) -> 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() }
}

27
bindings/test/test.js Normal file
View file

@ -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);
}

49
core/Cargo.toml Normal file
View file

@ -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

53
core/src/bip39.rs Normal file
View file

@ -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<Self, bip39::Error> {
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"));
}
}

60
core/src/btc.rs Normal file
View file

@ -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());
}
}

74
core/src/btc_htlc.rs Normal file
View file

@ -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 <hash> OP_EQUALVERIFY OP_DUP OP_HASH160 <recipient_hash160> OP_EQUALVERIFY OP_CHECKSIG
/// OP_ELSE
/// <locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 <refund_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);
}
}

41
core/src/hardware.rs Normal file
View file

@ -0,0 +1,41 @@
use anyhow::Result;
/// Hardware wallet trait - minimal abstraction
pub trait HardwareWallet {
fn get_xpub(&self, account: u32) -> Result<String>;
fn sign_psbt(&self, psbt_b64: &str) -> Result<String>;
}
/// 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<String> {
Err(anyhow::anyhow!("Ledger integration not implemented - stub"))
}
fn sign_psbt(&self, _psbt_b64: &str) -> Result<String> {
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());
}
}

335
core/src/keystore.rs Normal file
View file

@ -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<String> {
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<Vec<u8>> {
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<u16> = tmp.path().as_os_str().encode_wide().chain(std::iter::once(0)).collect();
let path_wstr: Vec<u16> = 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());
}
}

104
core/src/lib.rs Normal file
View file

@ -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);
}
}

51
core/src/lightning.rs Normal file
View file

@ -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<String>, tls_cert_path: Option<String> },
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"));
}
}

44
core/src/lnd.rs Normal file
View file

@ -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<String>,
}
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 <host> getinfo` and parse JSON output (requires lncli in PATH)
pub fn get_info(&self) -> Result<Value> {
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());
}
}

99
core/src/monero.rs Normal file
View file

@ -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<T> {
jsonrpc: Option<String>,
id: Option<String>,
result: Option<T>,
error: Option<serde_json::Value>,
}
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<T: for<'de> Deserialize<'de>>(&self, method: &str, params: serde_json::Value) -> Result<T> {
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<T> = 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<serde_json::Value> {
// 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<serde_json::Value> {
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<serde_json::Value> {
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<serde_json::Value> {
// 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<serde_json::Value> {
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<serde_json::Value> {
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());
}
}

57
core/src/monero_swap.rs Normal file
View file

@ -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<serde_json::Value> {
// 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<serde_json::Value> {
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());
}
}

58
core/src/swap_manager.rs Normal file
View file

@ -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<serde_json::Value> {
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());
}
}

74
core/src/swap_sim.rs Normal file
View file

@ -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<u8>,
pub hash: Vec<u8>,
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);
}
}

403
core/src/wallet_bdk.rs Normal file
View file

@ -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<MemoryDatabase>,
}
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<Self> {
// 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<String> {
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<String> {
let addr = to_address.parse::<bitcoin::Address>()?;
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<String> {
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<Vec<bdk::local_wallet::LocalUtxo>> {
let utxos = self.wallet.list_unspent()?;
Ok(utxos)
}
}
/// Persistent wallet backed by SQLite file
pub struct PersistentBdkWallet {
wallet: Wallet<SqliteDatabase>,
}
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<Self> {
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<String> {
let addr = self.wallet.get_address(AddressIndex::New)?;
Ok(addr.to_string())
}
pub fn create_psbt(&self, to_address: &str, satoshis: u64) -> Result<String> {
let addr = to_address.parse::<bitcoin::Address>()?;
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<String> {
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<Vec<bdk::local_wallet::LocalUtxo>> {
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<DecryptedDb> {
// 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<Self> {
// 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<serde_json::Value> {
let mut stmt = self.conn.prepare(sql)?;
let column_count = stmt.column_count();
let column_names: Vec<String> = (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<Vec<u8>> {
// 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());
}
}

View file

@ -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<HashMap<String, Arc<DecryptedDb>>> = Mutex::new(HashMap::new());
static ref INMEMORY_DB_HANDLES: Mutex<HashMap<String, Arc<InMemoryDb>>> = 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<String> {
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<String> {
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<String> {
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
}

29
docs/ARCHITECTURE.md Normal file
View file

@ -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.

8
electron/README.md Normal file
View file

@ -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.

9
mobile/README.md Normal file
View file

@ -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.

8
tools/cli/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "cryptec_cli"
version = "0.1.0"
edition = "2021"
[dependencies]
cryptec_core = { path = "../../core" }
serde_json = "1.0"

32
tools/cli/src/main.rs Normal file
View file

@ -0,0 +1,32 @@
use cryptec_core::{Mnemonic, BdkWallet};
use std::env;
use serde_json::json;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("usage: cryptec_cli <cmd> [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);
}
}
}