Skip to content

Commit deeda55

Browse files
bfoss765claude
andcommitted
fix(shielded-invites): zeroize the one-time bearer spending key end-to-end (dashpay#4204)
Reviewer (thepastaclaw) key-hygiene blocker: the one-time Orchard bearer spending key was copied into several plain, unsanitized buffers on both the claim and generate paths. Claim path — carry the key through `Zeroizing` from the FFI copy down through the wallet layers instead of leaking a plain `[u8; 32]` at each hop: - rs-platform-wallet-ffi: the `[u8; 32]` claim-key copy is now `Zeroizing<[u8; 32]>` and moves (not copies) into the wallet layer. - platform-wallet `identity_create_from_one_time_key` (both the PlatformWallet method and the operations fn) now take `Zeroizing<[u8; 32]>`; the key is scrubbed on drop, dereferenced only at the single `SpendingKey::from_bytes` consumption point. Generate path — wipe the transient native and JVM copies after handoff: - rs-platform-wallet-ffi: explicitly zeroize the native `sk` after copying it into the caller's `out_sk_32`. - rs-unified-sdk-jni: hold the JNI `sk` and the combined 75-byte `out` blob in `Zeroizing` buffers so both scrub on drop, including early returns. - kotlin-sdk `generateOneTimeOrchardKey`: wipe the 75-byte JVM blob in a `finally` once the two owned arrays have been sliced out. Validated: cargo build + cargo test -p platform-wallet (493 pass) + cargo fmt; :sdk:compileDebugKotlin succeeds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 58bc609 commit deeda55

5 files changed

Lines changed: 33 additions & 14 deletions

File tree

packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/wallet/PlatformWalletManager.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2278,11 +2278,17 @@ data class OneTimeOrchardKey(
22782278
*/
22792279
fun generateOneTimeOrchardKey(): OneTimeOrchardKey {
22802280
val blob = mapNativeErrors { FundingNative.generateOneTimeOrchardKey() }
2281-
require(blob.size == 75) { "expected a 75-byte sk||address blob, got ${blob.size}" }
2282-
return OneTimeOrchardKey(
2283-
spendingKey = blob.copyOfRange(0, 32),
2284-
address = blob.copyOfRange(32, 75),
2285-
)
2281+
// The blob's first 32 bytes are bearer spend authority; wipe the transient
2282+
// JVM copy once the two owned arrays have been sliced out (#4204 key-hygiene).
2283+
try {
2284+
require(blob.size == 75) { "expected a 75-byte sk||address blob, got ${blob.size}" }
2285+
return OneTimeOrchardKey(
2286+
spendingKey = blob.copyOfRange(0, 32),
2287+
address = blob.copyOfRange(32, 75),
2288+
)
2289+
} finally {
2290+
blob.fill(0)
2291+
}
22862292
}
22872293

22882294
/**

packages/rs-platform-wallet-ffi/src/shielded_send.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,10 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_identity_create_from_o
895895

896896
// Copy the one-time spending key (32 bytes; the caller's safety contract
897897
// guarantees the length — no companion length arg crosses the C ABI).
898-
let mut one_time_sk = [0u8; 32];
898+
// Bearer spend authority: hold this FFI-layer copy in a `Zeroizing` buffer so
899+
// it is scrubbed on drop. It is moved into the wallet layer, which likewise
900+
// carries it in `Zeroizing` (#4204 key-hygiene).
901+
let mut one_time_sk = zeroize::Zeroizing::new([0u8; 32]);
899902
std::ptr::copy_nonoverlapping(one_time_sk_bytes, one_time_sk.as_mut_ptr(), 32);
900903

901904
// Decode the claimer's own 43-byte default Orchard change address.
@@ -1589,7 +1592,7 @@ pub unsafe extern "C" fn platform_wallet_generate_one_time_orchard_key(
15891592
// this is a `#[no_mangle] extern "C"` export, so a panic would abort the
15901593
// process across the C ABI before any JNI panic guard could convert it —
15911594
// an OS RNG failure must surface as a normal error, never a hard abort.
1592-
let (sk, address) = match generate_one_time_orchard_key() {
1595+
let (mut sk, address) = match generate_one_time_orchard_key() {
15931596
Ok(pair) => pair,
15941597
Err(e) => {
15951598
return PlatformWalletFFIResult::err(
@@ -1600,6 +1603,9 @@ pub unsafe extern "C" fn platform_wallet_generate_one_time_orchard_key(
16001603
};
16011604
std::ptr::copy_nonoverlapping(sk.as_ptr(), out_sk_32, 32);
16021605
std::ptr::copy_nonoverlapping(address.as_ptr(), out_address_43, 43);
1606+
// Wipe this native copy of the one-time spending key now that it has been
1607+
// handed to the caller's `out_sk_32` buffer (#4204 key-hygiene).
1608+
zeroize::Zeroize::zeroize(&mut sk);
16031609
PlatformWalletFFIResult::ok()
16041610
}
16051611

packages/rs-platform-wallet/src/wallet/platform_wallet.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1129,7 +1129,9 @@ impl PlatformWallet {
11291129
pub async fn identity_create_from_one_time_key<P, IS>(
11301130
&self,
11311131
coordinator: &Arc<crate::wallet::shielded::NetworkShieldedCoordinator>,
1132-
one_time_sk: [u8; 32],
1132+
// Bearer spend authority carried in a `Zeroizing` buffer so this layer's copy
1133+
// of the one-time spending key is scrubbed on drop (#4204 key-hygiene).
1134+
one_time_sk: zeroize::Zeroizing<[u8; 32]>,
11331135
funding_birth_height: Option<u32>,
11341136
change_address: dpp::address_funds::OrchardAddress,
11351137
identity_index: u32,

packages/rs-platform-wallet/src/wallet/shielded/operations.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,7 +1578,9 @@ where
15781578
pub async fn identity_create_from_one_time_key<S, P, IS>(
15791579
sdk: &Arc<dash_sdk::Sdk>,
15801580
store: &Arc<RwLock<S>>,
1581-
one_time_sk: [u8; 32],
1581+
// Bearer spend authority: carried in a `Zeroizing` buffer so every wallet-layer
1582+
// copy of the one-time spending key is scrubbed on drop (#4204 key-hygiene).
1583+
one_time_sk: zeroize::Zeroizing<[u8; 32]>,
15821584
funding_birth_height: Option<u32>,
15831585
change_address: &OrchardAddress,
15841586
public_keys: Vec<(IdentityPublicKey, IdentityPublicKeyInCreation)>,
@@ -1603,7 +1605,7 @@ where
16031605
// Derive the Orchard key material from the one-time spending key. `from_bytes`
16041606
// returns a `CtOption`; an invalid scalar means the caller handed us a
16051607
// non-key, which is a hard input error.
1606-
let sk: SpendingKey = Option::from(SpendingKey::from_bytes(one_time_sk)).ok_or_else(|| {
1608+
let sk: SpendingKey = Option::from(SpendingKey::from_bytes(*one_time_sk)).ok_or_else(|| {
16071609
PlatformWalletError::ShieldedKeyDerivation(
16081610
"one-time spending key is not a valid Orchard SpendingKey".to_string(),
16091611
)

packages/rs-unified-sdk-jni/src/funding.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,10 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_FundingNative_generat
986986
_class: JClass,
987987
) -> jni::sys::jbyteArray {
988988
guard(&mut env, ptr::null_mut(), |env| {
989-
let mut sk = [0u8; 32];
989+
// Bearer spend authority: hold the native `sk` and the combined `out`
990+
// blob (its first 32 bytes are the spending key) in `Zeroizing` buffers so
991+
// both are scrubbed on drop, including any early return (#4204 key-hygiene).
992+
let mut sk = zeroize::Zeroizing::new([0u8; 32]);
990993
let mut addr = [0u8; 43];
991994
let result = unsafe {
992995
platform_wallet_ffi::platform_wallet_generate_one_time_orchard_key(
@@ -998,10 +1001,10 @@ pub extern "system" fn Java_org_dashfoundation_dashsdk_ffi_FundingNative_generat
9981001
return ptr::null_mut();
9991002
}
10001003
// sk ‖ addr — a 75-byte blob the Kotlin side slices into (sk32, addr43).
1001-
let mut out = [0u8; 75];
1002-
out[..32].copy_from_slice(&sk);
1004+
let mut out = zeroize::Zeroizing::new([0u8; 75]);
1005+
out[..32].copy_from_slice(&sk[..]);
10031006
out[32..].copy_from_slice(&addr);
1004-
env.byte_array_from_slice(&out)
1007+
env.byte_array_from_slice(&out[..])
10051008
.map(|a| a.into_raw())
10061009
.unwrap_or(ptr::null_mut())
10071010
})

0 commit comments

Comments
 (0)