Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions seedelf-platform/seedelf-gui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ tauri-build = { version = "2", features = [] }

[dependencies]
blstrs = "0.7.1"
libc = "0.2.175"
once_cell = "1.21.3"
pallas-addresses = "0.33.0"
parking_lot = "0.12.4"
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-fs = "2.4.0"
tokio = "1.47.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
windows-sys = { version = "0.60.2", features = ["Win32_System_Memory", "Win32_System_ErrorReporting"] }
zeroize = "1.8.1"
# seedelf stuff
seedelf-cli = { workspace = true }
Expand Down
6 changes: 4 additions & 2 deletions seedelf-platform/seedelf-gui/src-tauri/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ pub async fn create_seedelf(network_flag: bool, addr: String, label: String) ->
cpu_units,
mem_units,
..
} = match session::with_key(|sk| build_create_seedelf(config, network_flag, addr, label, *sk)) {
Ok(v) => v.await,
} = match session::with_key(|sk| build_create_seedelf(config, network_flag, addr, label, *sk))
.await
{
Ok(v) => v,
_ => return String::new(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ pub async fn extract_seedelf(
*sk,
send_all,
)
}) {
Ok(v) => v.await,
})
.await
{
Ok(v) => v,
_ => return String::new(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ pub async fn remove_seedelf(network_flag: bool, addr: String, seedelf: String) -
spend_mem_units,
..
} = match session::with_key(|sk| build_remove_seedelf(config, network_flag, addr, seedelf, *sk))
.await
{
Ok(v) => v.await,
Ok(v) => v,
_ => return String::new(),
};

Expand Down
6 changes: 4 additions & 2 deletions seedelf-platform/seedelf-gui/src-tauri/src/commands/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ pub async fn send_seedelf(network_flag: bool, seedelf: String, lovelace: u64) ->
None,
*sk,
)
}) {
Ok(v) => v.await,
})
.await
{
Ok(v) => v,
_ => return String::new(),
};
if usable_utxos.is_empty() {
Expand Down
191 changes: 156 additions & 35 deletions seedelf-platform/seedelf-gui/src-tauri/src/session.rs
Original file line number Diff line number Diff line change
@@ -1,66 +1,187 @@
use blstrs::Scalar;
use core::{fmt, ops::Deref};
use once_cell::sync::OnceCell;
use std::sync::RwLock;
use zeroize::Zeroize; // trait we’ll implement for the wrapper
use parking_lot::RwLock;
use zeroize::Zeroize;

/* ---------- SecretScalar: zeroes itself when dropped ---------- */
#[repr(transparent)]
pub struct SecretScalar(Scalar);

pub struct SecretScalar(pub Scalar);

impl Zeroize for SecretScalar {
fn zeroize(&mut self) {
unsafe {
// overwrite the entire struct with zeros (32 bytes)
core::ptr::write_bytes(
self as *mut _ as *mut u8,
0,
core::mem::size_of::<SecretScalar>(),
);
}
impl fmt::Debug for SecretScalar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("SecretScalar(**redacted**)")
}
}

impl Drop for SecretScalar {
fn drop(&mut self) {
self.zeroize(); // guaranteed wipe on drop
unsafe {
let p = self as *mut _ as *mut u8;
let n = core::mem::size_of::<SecretScalar>();
core::slice::from_raw_parts_mut(p, n).zeroize(); // volatile wipe
}
}
}

/* ---------- Global RAM slot ---------- */
struct LockedBox {
inner: Box<SecretScalar>,
}

static KEY: OnceCell<RwLock<Option<SecretScalar>>> = OnceCell::new();
impl LockedBox {
fn new(secret: SecretScalar) -> Self {
let mut inner = Box::new(secret);
unsafe {
let p = (&mut *inner) as *mut SecretScalar as *mut u8;
let n = core::mem::size_of::<SecretScalar>();
os_mem::page_lock(p, n);
}
Self { inner }
}
}

fn slot() -> &'static RwLock<Option<SecretScalar>> {
KEY.get_or_init(|| RwLock::new(None))
impl Drop for LockedBox {
fn drop(&mut self) {
unsafe {
let p = (&mut *self.inner) as *mut SecretScalar as *mut u8;
let n = core::mem::size_of::<SecretScalar>();
// scrub while still locked, then unlock
core::slice::from_raw_parts_mut(p, n).zeroize();
os_mem::page_unlock(p, n);
}
}
}

/* ---------- API ---------- */
// --- global slot ---
static KEY: OnceCell<RwLock<Option<LockedBox>>> = OnceCell::new();
fn slot() -> &'static RwLock<Option<LockedBox>> {
KEY.get_or_init(|| RwLock::new(None))
}

/// Put the decrypted key in RAM (replaces any existing one).
// --- public API ---
pub fn unlock(new_key: Scalar) {
*slot().write().expect("poisoned lock") = Some(SecretScalar(new_key));
*slot().write() = Some(LockedBox::new(SecretScalar(new_key)));
}

/// Remove and wipe the key.
pub fn lock() {
// `take()` drops the SecretScalar; its Drop impl zeroises the bytes.
let _ = slot().write().expect("poisoned lock").take();
let _ = slot().write().take();
}

/// Borrow the key immutably for one operation.
pub fn with_key<F, R>(f: F) -> Result<R, &'static str>
pub fn is_unlocked() -> bool {
slot().read().is_some()
}

/// Synchronous borrow: the closure must finish before returning.
pub fn with_key_sync<F, R>(f: F) -> Result<R, &'static str>
where
F: FnOnce(&Scalar) -> R,
{
slot()
.read()
.expect("poisoned lock")
let guard = slot().read();
guard
.as_ref()
.map(|s| f(&s.0)) // pass inner scalar to the closure
.map(|b| f(&b.inner.0))
.ok_or("Wallet is locked")
}

/// Check if a key is currently loaded.
pub fn is_unlocked() -> bool {
slot().read().expect("poisoned lock").is_some()
/// Async-friendly: hands you an owned, zeroizing wrapper you can move across .await.
/// The inner scalar is wiped when the future completes and the wrapper is dropped.
pub async fn with_key<F, Fut, R>(f: F) -> Result<R, &'static str>
where
F: FnOnce(EphemeralScalar) -> Fut,
Fut: core::future::Future<Output = R>,
{
// copy under the read lock, then drop the lock
let s = {
let g = slot().read();
let lb = g.as_ref().ok_or("Wallet is locked")?;
lb.inner.0 // by-value copy
};
let eph = EphemeralScalar(s);
let out = f(eph).await; // drops `eph` afterwards -> zeroized
Ok(out)
}

/// Owned, zeroizing scalar for async flows. Move it; don't clone it.
#[repr(transparent)]
pub struct EphemeralScalar(Scalar);

impl Deref for EphemeralScalar {
type Target = Scalar;
fn deref(&self) -> &Scalar {
&self.0
}
}

impl Drop for EphemeralScalar {
fn drop(&mut self) {
unsafe {
let p = &mut self.0 as *mut _ as *mut u8;
let n = core::mem::size_of::<Scalar>();
core::slice::from_raw_parts_mut(p, n).zeroize();
}
}
}

impl fmt::Debug for EphemeralScalar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("EphemeralScalar(**redacted**)")
}
}

/// Call once at process startup for extra hygiene (Unix).
pub fn harden_process_best_effort() {
os_mem::disable_core_dumps();
}

// ---------- platform glue ----------
mod os_mem {
#[allow(unused_variables)]
pub unsafe fn page_lock(p: *mut u8, n: usize) {
#[cfg(target_family = "unix")]
unsafe {
let _ = libc::mlock(p as *const _, n);
#[cfg(target_os = "linux")]
{
let _ = libc::madvise(p as *mut _, n, libc::MADV_DONTDUMP);
}
}
#[cfg(windows)]
{
use windows_sys::Win32::System::Memory::VirtualLock;
let _ = VirtualLock(p as *mut _, n);
// Optional: exclude from Windows Error Reporting heap dumps:
use windows_sys::Win32::System::ErrorReporting::WerAddExcludedMemoryBlock;
let _ = WerAddExcludedMemoryBlock(p as _, n as u32);
}
}

#[allow(unused_variables)]
pub unsafe fn page_unlock(p: *mut u8, n: usize) {
#[cfg(target_family = "unix")]
unsafe {
let _ = libc::munlock(p as *const _, n);
}
#[cfg(windows)]
{
use windows_sys::Win32::System::Memory::VirtualUnlock;
let _ = VirtualUnlock(p as *mut _, n);
}
}

pub fn disable_core_dumps() {
#[cfg(target_family = "unix")]
unsafe {
// hard-disable core files; macOS + Linux
let r = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let _ = libc::setrlimit(libc::RLIMIT_CORE, &r);

#[cfg(target_os = "linux")]
{
// make process undumpable (also blocks ptrace by non-root)
let _ = libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0);
}
}
}
}
15 changes: 9 additions & 6 deletions seedelf-platform/seedelf-gui/src-tauri/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub async fn get_wallet_history(network_flag: bool) -> Vec<TxResponseWithSide> {
}
};

session::with_key(|sk| {
session::with_key_sync(|sk| {
let filtered: Vec<TxResponseWithSide> = all_txs
.into_iter()
.filter_map(|tx| {
Expand Down Expand Up @@ -74,18 +74,21 @@ pub async fn get_every_utxo(network_flag: bool) -> Vec<UtxoResponse> {
}

#[tauri::command]
pub fn get_owned_utxo(network_flag: bool, every_utxo: Vec<UtxoResponse>) -> Vec<UtxoResponse> {
pub async fn get_owned_utxo(
network_flag: bool,
every_utxo: Vec<UtxoResponse>,
) -> Vec<UtxoResponse> {
let config: Config = match get_config(VARIANT, network_flag) {
Some(c) => c,
None => {
return Vec::new();
}
};

match session::with_key(|sk| {
match session::with_key_sync(|sk| {
utxos::collect_all_wallet_utxos(*sk, &config.contract.seedelf_policy_id, every_utxo)
}) {
Ok(Ok(v)) => v,
Ok(v) => v.unwrap_or_default(),
_ => Vec::new(),
}
}
Expand All @@ -99,15 +102,15 @@ pub fn get_lovelace_balance(owned_utxos: Vec<UtxoResponse>) -> u64 {
}

#[tauri::command]
pub fn get_owned_seedelfs(network_flag: bool, every_utxo: Vec<UtxoResponse>) -> Vec<String> {
pub async fn get_owned_seedelfs(network_flag: bool, every_utxo: Vec<UtxoResponse>) -> Vec<String> {
let config: Config = match get_config(VARIANT, network_flag) {
Some(c) => c,
None => {
return Vec::new();
}
};

session::with_key(|sk| {
session::with_key_sync(|sk| {
display::extract_all_owned_seedelfs(*sk, &config.contract.seedelf_policy_id, every_utxo)
})
.unwrap_or_default()
Expand Down