Initial CrypteCipher scaffold and features
This commit is contained in:
commit
267d8f6edd
27 changed files with 2063 additions and 0 deletions
42
.github/workflows/ci.yml
vendored
Normal file
42
.github/workflows/ci.yml
vendored
Normal 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
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
target/
|
||||
node_modules/
|
||||
**/*.log
|
||||
.env
|
||||
.DS_Store
|
||||
19
README.md
Normal file
19
README.md
Normal 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
16
bindings/Cargo.toml
Normal 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
9
bindings/package.json
Normal 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
272
bindings/src/lib.rs
Normal 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
27
bindings/test/test.js
Normal 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
49
core/Cargo.toml
Normal 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
53
core/src/bip39.rs
Normal 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
60
core/src/btc.rs
Normal 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
74
core/src/btc_htlc.rs
Normal 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
41
core/src/hardware.rs
Normal 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
335
core/src/keystore.rs
Normal 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
104
core/src/lib.rs
Normal 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
51
core/src/lightning.rs
Normal 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
44
core/src/lnd.rs
Normal 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
99
core/src/monero.rs
Normal 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
57
core/src/monero_swap.rs
Normal 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
58
core/src/swap_manager.rs
Normal 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
74
core/src/swap_sim.rs
Normal 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
403
core/src/wallet_bdk.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
85
core/src/wallet_manager.rs
Normal file
85
core/src/wallet_manager.rs
Normal 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
29
docs/ARCHITECTURE.md
Normal 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
8
electron/README.md
Normal 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
9
mobile/README.md
Normal 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
8
tools/cli/Cargo.toml
Normal 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
32
tools/cli/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue