Skip to content
Open
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
13 changes: 9 additions & 4 deletions walletkit-core/src/storage/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,19 @@ impl StorageKeys {

// Trait-object bridge from walletkit-core's uniffi-annotated traits onto
// walletkit-db's plain-Rust trait surface. Required because Rust's orphan
// rule prevents a blanket impl across crates; the wrappers are pure
// delegation since both trait shapes already use `Vec<u8>` / `String`.
// rule prevents a blanket impl across crates. `Keystore::seal` borrows its
// plaintext (see walletkit-db/src/traits.rs); `Ks::seal` is the single
// point where the secret is copied into an owned `Vec<u8>`, because
// `DeviceKeystore` is a uniffi callback interface and those only support
// pass-by-value parameters (no `&[u8]`). That copy — and any further copy
// the foreign (Swift/Kotlin/etc.) implementation makes on its own side — is
// outside Rust's control; this is an accepted uniffi limitation, not a bug.

struct Ks<'a>(&'a dyn DeviceKeystore);
impl walletkit_db::Keystore for Ks<'_> {
fn seal(&self, aad: Vec<u8>, pt: Vec<u8>) -> walletkit_db::StoreResult<Vec<u8>> {
fn seal(&self, aad: &[u8], pt: &[u8]) -> walletkit_db::StoreResult<Vec<u8>> {
self.0
.seal(aad, pt)
.seal(aad.to_vec(), pt.to_vec())
.map_err(|e| walletkit_db::StoreError::Keystore(e.to_string()))
}
fn open_sealed(
Expand Down
19 changes: 9 additions & 10 deletions walletkit-db/src/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,14 @@ pub fn init_or_open_envelope_key(
let mut k_intermediate = Zeroizing::new([0u8; 32]);
getrandom::fill(k_intermediate.as_mut())
.map_err(|err| StoreError::Crypto(format!("rng failure: {err}")))?;
// TODO: `keystore.seal(_, Vec<u8>)` requires the plaintext as an
// owned heap allocation because the trait shape matches
// walletkit-core's uniffi `DeviceKeystore` so the adapter stays
// zero-copy. That `Vec<u8>` is NOT zeroized on drop — key bytes
// can linger in the allocator's freelist. Improve by either
// (a) changing the trait to take a stack reference and updating
// the host bridges, or (b) wrapping the `to_vec()` result in
// `Zeroizing` and ensuring `Keystore` impls don't clone it.
let wrapped = keystore.seal(ad.to_vec(), k_intermediate.to_vec())?;
// `keystore.seal` borrows the plaintext, so `k_intermediate` is
// never copied into an un-zeroized `Vec<u8>` at this layer. A
// `Keystore` bridging to an owned-only interface (e.g. a uniffi
// callback like walletkit-core's `DeviceKeystore`) still needs one
// owned copy to cross that boundary; that is an accepted,
// uniffi-imposed limitation (callback interfaces only support
// pass-by-value), not something fixable from this layer.
let wrapped = keystore.seal(ad, k_intermediate.as_slice())?;
let envelope = KeyEnvelope::new(wrapped, now);
let bytes = envelope.serialize()?;
blob_store.write_atomic(filename.to_string(), bytes)?;
Expand Down Expand Up @@ -193,7 +192,7 @@ mod tests {
}

impl Keystore for XorKeystore {
fn seal(&self, _ad: Vec<u8>, plaintext: Vec<u8>) -> StoreResult<Vec<u8>> {
fn seal(&self, _ad: &[u8], plaintext: &[u8]) -> StoreResult<Vec<u8>> {
Ok(plaintext
.iter()
.enumerate()
Expand Down
18 changes: 13 additions & 5 deletions walletkit-db/src/traits.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
//! Interfaces for consumer-supplied platform integrations.
//!
//! Argument shapes (`Vec<u8>`, owned `String`) mirror `WalletKit`'s existing
//! uniffi-annotated traits so consumers can bridge with a thin newtype that
//! just delegates and maps errors. (A blanket impl across crates is blocked
//! by Rust's orphan rule, so consumers do need a small wrapper.)
//! Argument shapes (`Vec<u8>`, owned `String`) mostly mirror `WalletKit`'s
//! existing uniffi-annotated traits so consumers can bridge with a thin
//! newtype that just delegates and maps errors. (A blanket impl across
//! crates is blocked by Rust's orphan rule, so consumers do need a small
//! wrapper.) `Keystore::seal` is the one exception: it borrows its
//! plaintext so the secret is never owned by this crate longer than
//! necessary; a bridge to an owned-only interface (e.g. a uniffi callback)
//! still needs one copy at that boundary.

use crate::error::StoreResult;

Expand All @@ -16,11 +20,15 @@ pub trait Keystore: Send + Sync {
/// Seals plaintext under the device-bound key, authenticating `aad`
/// (additional authenticated data).
///
/// `plaintext` is borrowed rather than owned so that callers holding it
/// in a zeroizing buffer (e.g. `Zeroizing<[u8; 32]>`) never have to
/// hand ownership of an un-zeroized copy to this trait.
///
/// # Errors
///
/// Returns an error if the keystore refuses the operation or the seal
/// fails.
fn seal(&self, aad: Vec<u8>, plaintext: Vec<u8>) -> StoreResult<Vec<u8>>;
fn seal(&self, aad: &[u8], plaintext: &[u8]) -> StoreResult<Vec<u8>>;

/// Opens ciphertext under the device-bound key, verifying `aad`. The
/// same `aad` supplied at seal time must be supplied here or the open
Expand Down
Loading