fix(sdk): limit txMetadata decrypt plaintext lifetime - #4243
Conversation
…e + decrypt-on-fetch Adds the wallet-contract encrypted-document surface the Android wallet needs to retire the legacy org.dashj.platform stack (closes #4086 create/update, closes The encryption ENVELOPE is byte-for-byte wire-compatible with the legacy BlockchainIdentity.publishTxMetaData / getTxMetaData so documents written by either stack decrypt with the other (migrated users keep their history): - AES-256 key = the raw 32-byte secp256k1 private scalar of a hardened HD child (mirrors KeyCrypterAESCBC.deriveKey(ECKey); no ECDH, no HKDF). - Path: identity-auth path of the identity's encryption key (its id = the document's keyIndex) extended by / 32769' / encryptionKeyIndex'. - Cipher: AES-256-CBC / PKCS7, random 16-byte IV. - encryptedMetadata blob = version(1) ‖ IV(16) ‖ AES-256-CBC(payload); the version byte is OUTSIDE the ciphertext (0 = CBOR, 1 = protobuf). The plaintext payload stays OPAQUE to the SDK — the app owns the protobuf TxMetadataBatch item schema and the batching policy, exactly as on the legacy stack. The SDK owns only the crypto envelope + the {keyIndex, encryptionKeyIndex, encryptedMetadata} document fields. Layers: - rs-platform-wallet: crypto/tx_metadata.rs (derive/seal/open + tests); network/encrypted_document.rs (create_encrypted_document_with_signer reusing the tested create path; fetch_encrypted_documents mirroring the contactInfo paginated decrypt loop). - rs-platform-wallet-ffi: platform_wallet_create_encrypted_document_with_signer, platform_wallet_fetch_encrypted_documents (JSON-out; payload as base64). - rs-unified-sdk-jni: documentCreateEncrypted / documentFetchEncrypted. - kotlin-sdk: DocumentTransactions.createEncryptedDocument / fetchEncryptedDocuments (additive; no existing signatures change). Tests: seal↔open round-trip, key-derivation determinism + index separation, wrong-key/ malformed-blob fail cleanly, and a NIST SP 800-38A CBC-AES256 cross-stack vector pinning the cipher core + blob framing. cc @QuantumExplorer Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d vector
The encrypted-txMetadata scheme claims byte-for-byte wire compatibility with
the legacy org.dashj.platform stack, but the mnemonic->AES-key HD derivation
prefix was previously only "sample-confirmed" (the module's NIST test pinned
the AES-CBC envelope but explicitly could NOT pin the derivation-path account
prefix). That gap is now closed by reconstructing the exact legacy recipe from
the shipped jars and running it under a JVM.
Recovered legacy derivation (the wire-compat reference this crate mirrors):
AES-256-CBC key = raw private-key bytes of the ECKey at absolute HD path
m / 9' / coinType' / 5' / 0' / 0' / 0' / keyId' / 32769' / encryptionKeyIndex'
- account path 9'/coinType'/5'/0'/0'/0' is
DerivationPathFactory.blockchainIdentityECDSADerivationPath() (dashj-core
22.0.3), the path the BLOCKCHAIN_IDENTITY AuthenticationKeyChain is built
with (AuthenticationGroupExtension.getDefaultPath).
- keyId' / 32769' / encryptionKeyIndex' are appended by
BlockchainIdentity.privateKeyAtPath; keyId is the id of the identity's
ENCRYPTION/MEDIUM ECDSA key (id 2 in createIdentityPublicKeys), 32769' is
TxMetadataDocument.childNumber, encryptionKeyIndex is the app's per-document
counter (dash-sdk-kotlin 4.0.0-RC2).
- AES key bytes = KeyCrypterAESCBC.deriveKey(ecKey) = new KeyParameter(
ecKey.getPrivKeyBytes()) (raw scalar, no ECDH / no KDF); framing is
version(1) ‖ IV(16) ‖ AES-256-CBC/PKCS7(payload).
This is an exact match to Rust's identity_auth_derivation_path_for_type(ECDSA,
identity_index=0, key_index=keyId) extended by /32769'/encryptionKeyIndex': the
three legacy zeros correspond to [subfeature-auth, keytype=ECDSA=0,
identity_index=0]. No derivation-logic change is needed — verified by generating
the key AND a full encryptedMetadata blob with the real dashj stack for the
BIP-39 "abandon … about" mnemonic and asserting Rust reproduces the key and
decrypts the blob to the original plaintext.
- add legacy_dashj_wire_compat_vector: dashj-generated (key, blob, plaintext)
vector with full provenance, so the derivation prefix + envelope are now
CI-enforced rather than device-sample-confirmed.
- retarget the NIST test's doc comment as the narrower cipher-conformance leg
and drop the now-obsolete "cannot be reconstructed from the jars" caveat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tnet docs + query diagnostics The on-device decrypt-proof reported `sdkFetched=0` with ZERO decrypt-skip warnings, i.e. the fetch query returned nothing while the legacy stack sees 2 encrypted `txMetadata` documents for the same owner. This isolates the FETCH half of `IdentityWallet::fetch_encrypted_documents` and pins it against the real documents so a wire-query regression is caught in CI, and adds an on-device breadcrumb to localize any future empty result to the query vs the decrypt stage. What this proves (executable evidence): the exact production query — `$ownerId == owner AND $updatedAt >= since_ms`, ordered `$updatedAt asc`, paginated — returns BOTH real testnet documents (owner 532rVHxLD6Z3MNiu5LZyNqn55Ybz4bydZozXU4cqqp1L, wallet-utils contract 7CSFGeF4WNzgDmx94zwvHkYaG3Dx4XEe5LFsFgJswLbm, type `txMetadata`, since_ms 0) fully materialized, with `keyIndex=2`, `encryptionKeyIndex=1`, `encryptedMetadata` 3585 bytes. This holds for the exact production shape (`&Identifier` owner value, `U64` since bound), with and without the range clause, across pinned platform versions 1..=12 and the default, and including the production `register_data_contract` step. The query construction, value encoding, contract resolution (7CSFGeF4… is the built-in wallet-utils system contract), and JNI param marshalling (`read_id32`, `sinceMs` jlong→u64) are therefore all wire-correct; the on-device empty result is not reproducible from the query and points outside it (e.g. a stale native lib). Changes: - extract the paginated wire query into `query_owned_encrypted_documents` (takes the `Sdk` + fetched contract, no resident wallet/identity), re-exported so the new testnet integration test drives the SAME code the FFI path runs. Query logic unchanged. - add `tests/txmetadata_fetch.rs` (`#[ignore]`, testnet): asserts the query returns the 2 documents and that each decodes `keyIndex`/`encryptionKeyIndex` (u32) + `encryptedMetadata` (bytes) — i.e. the pipeline reaches decrypt for both, without needing the owner mnemonic. - log `raw_count`/`materialized` at INFO before the decrypt loop, so the next `adb logcat` run during the probe pins an empty result to the query (raw_count=0), a proof-materialization gap (raw_count>0, materialized=0), or the decrypt/JSON stage (materialized>0) — no guessing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-document fetch path The dash-wallet decrypt probe reported sdkFetched=0 on-device with NO Rust breadcrumb in logcat — either the JNI call never reached Rust or platform-wallet INFO tracing is filtered from logcat. Make every stage of the fetch path provably visible at WARN: - rs-platform-wallet fetch_encrypted_documents: entry log (owner/contract b58, type, since_ms), warn on every early-return (contract fetch failure, contract-not-found, encryption-context resolution failure, query failure) and a final raw/decrypted count; query_owned_encrypted_documents gets an entry log and a fetch_many error log, and the raw_count/materialized breadcrumb is raised from info! to warn!. - rs-unified-sdk-jni documentFetchEncrypted: entry log (wallet_handle nonzero?, since_ms), parsed-args log (owner/contract hex, doc type), per-early-return warns, and a success log with the returned JSON size. - take_pwffi_error / throw_sdk_exception warn-log every native->Kotlin error conversion (raw + offset code, full message), so a contained exception still leaves a logcat trail. Diagnostic only — no behavior change; cargo check -p rs-unified-sdk-jni and clippy on both touched crates are clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h log AND tracing so they reach Android logcat
The 2026-07 on-device forensic tap proved the two logging facades diverge on
Android: JNI_OnLoad installs android_logger as the global `log` logger
(logcat tag DashSDK), so the JNI layer's log::warn! lines were visible —
while the only tracing subscriber the Kotlin SDK installs
(dash_sdk_enable_logging, a tracing_subscriber::fmt layer) writes to STDOUT,
which Android discards. Every tracing::warn! breadcrumb in
fetch_encrypted_documents therefore never reached logcat, even at WARN.
Fix: a `breadcrumb()` helper in encrypted_document.rs emits each diagnostic
line through BOTH facades — `tracing` for host tests / desktop, `log` for
logcat — and every stage of the fetch path now uses it (entry, contract
fetch/not-found, encryption-context resolution, query entry, fetch_many
error, raw_count/materialized, per-document skips, final raw/decrypted
counts). The previously SILENT skip of a raw-but-unmaterialized document
(`let Some(doc) = maybe_doc else { continue }`) now leaves a trail too:
under proofs that shape is exactly what turns "2 documents exist" into an
empty result with no error.
Root-cause status of the on-device sdkFetched=0: NOT locally reproducible.
The device path was config-identical to the Mac repro (SdkBuilder::
new_testnet + TrustedHttpContextProvider::new(Testnet, None, 100), proofs on
by default, platform version auto, since_ms=0, contract registered with the
provider — the register step is now mirrored in tests/txmetadata_fetch.rs),
and that repro still returns raw_count=2 materialized=2 from this Mac. A
stale device lib is ruled out: the JNI warns visible in the tap were added
in d29d523, so the device ran current code. The next on-device tap will
pin the failing stage: query-empty (raw_count=0) vs materialization drop
(raw>0, NOT-materialized lines) vs decrypt skip (per-doc skip lines).
cargo test -p platform-wallet --lib: 427 passed; testnet integration test
txmetadata_fetch passes with the production-parity register step; clippy
clean on platform-wallet + rs-unified-sdk-jni.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…olver for external-signable wallets
The on-device decrypt-proof breadcrumbs delivered the verdict: the query was
never broken (raw=3 materialized=3), but every document skipped with
"txMetadata key derivation failed ... External signable wallet has no private
key". The app's SDK wallet is EXTERNAL-SIGNABLE — no private keys in the Rust
wallet; every key derives on demand through the registered mnemonic resolver
(the app's security architecture) — while derive_tx_metadata_key derived from
in-wallet private keys, which exist in test fixtures but never on-device. The
CREATE path had the identical flaw.
Fix, following the identity_key_preview / discovery capability convention:
- rs-platform-wallet: new derive_tx_metadata_key_from_master (same path,
caller-supplied master xprv; path construction shared via
tx_metadata_derivation_path so the two sources can never drift) and a
TxMetadataKeySource {ResidentWallet, Master} selector on both
create_encrypted_document_with_signer and fetch_encrypted_documents;
key-derivation breadcrumbs now name the active source.
- rs-platform-wallet-ffi: both encrypted-document entry points take a
nullable mnemonic_resolver_handle. Capability check under a short guard
(never held across the host resolver callback), resolver consulted ONLY
for external-signable / watch-only wallets (resident wallets keep the
historical in-process derive and skip the Keystore read), master scalar
wiped (non_secure_erase) before the result crosses back — atomic
derive + use + zeroize.
- rs-unified-sdk-jni + kotlin-sdk: documentCreateEncrypted /
documentFetchEncrypted and DocumentTransactions.createEncryptedDocument /
fetchEncryptedDocuments thread mnemonicResolverHandle through (the app
passes PlatformWalletManager.mnemonicResolverHandle, as discoverIdentities
already does).
Regression tests (network-free):
- master_derivation_matches_resident_wallet_derivation — both key sources
agree at every probed (identity, key, encryptionKey) slot;
- external_signable_wallet_derives_via_resolver_master — the device shape:
in-wallet derive fails with the exact no-private-key error, the
resolver-master path (stubbed with the test mnemonic) round-trips
seal/open against a resident wallet in both directions;
- legacy_dashj_wire_compat_vector now pins the resolver-master path to the
dashj-generated vector too (both sources hit the legacy key
byte-for-byte).
cargo test -p platform-wallet --lib: 429 passed; -p platform-wallet-ffi
--lib: 145 passed; clippy clean on all three crates; kotlin-sdk
:sdk:compileDebugKotlin green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…off the await; redact plaintext Debug; quiet breadcrumbs Addresses the latest review on #4091. Wire-compat vector (BLOCKING): the existing legacy_dashj_wire_compat_vector pinned identity_index=0, indistinguishable from KeyDerivationType::ECDSA=0 at the adjacent path slot, so it could not prove identity_index is wired correctly. Add legacy_dashj_wire_compat_vector_nonzero_identity_index, generated by the REAL legacy stack (dashj-core 22.0.3 + blockchainIdentityECDSADerivationPath / KeyCrypterAESCBC, run under a JVM) at identity_index=1 (m/9'/1'/5'/0'/0'/1'/2'/32769'/1'). Its key (8cda…5196) is provably distinct from the index-0 key (4a2e…84d7); both the resident-wallet and resolver-master derivations are asserted to hit it. The reproducible generator (LegacyKeyN.java) and a README are checked in under tests/legacy_wire_compat/ so the vector's provenance is independently verifiable. Master-key exposure across await (document.rs create+fetch): the resolved master xprv previously lived across the network broadcast/pagination awaits and was wiped only afterwards (skipped on panic/early-return). Create now derives the AES key + seals the wire blob SYNCHRONOUSLY (new IdentityWallet:: prepare_encrypted_txmetadata_properties) and wipes the master before the async broadcast, so no key material crosses the await. Fetch cannot pre-derive (per-doc keyIndex/encryptionKeyIndex are discovered during pagination), so the master is wrapped in a WipingMaster Drop guard that scrubs on every exit path, with the tradeoff documented. FFI decision tests (decide_key_source) cover null-handle, external-signable dispatch, and resolver-required. Hygiene: manual Debug impls redacting the decrypted payload on DecryptedEncryptedDocument and OpenedTxMetadata; transactions.rs no longer logs the raw mnemonic_resolver_handle pointer (nonzero=bool only); per-poll informational breadcrumbs downgraded warn!->debug! (genuine error/skip paths stay warn!), with the android_logger Info-level visibility implication noted so identity-correlated data stops reaching logcat on every successful fetch now that the sdkFetched=0 root cause is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…deep-cloning it Addresses review nitpick bbb24591b025 on #4091. fetch_encrypted_documents wrapped the fetched DataContract in one Arc for the context provider (Arc::new(contract.clone())) and then moved the original into a SECOND Arc — a redundant deep clone of the whole contract (document-type/index metadata) on every fetch. Wrap once and hand the provider a cheap Arc::clone of the same handle. Verified: cargo test -p platform-wallet green (430 lib + 9 integration). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…'t decode Addresses review findings 0dd9fc55de07 / CodeRabbit a783199f on #4091. createEncryptedDocument bounded `version` to 0..255, but only 0 (CBOR) and 1 (protobuf) are wire-meaningful: seal_tx_metadata writes the byte verbatim into the envelope and the legacy dashj decryptTxMetadata switches on exactly those two values. Accepting 2..255 would silently seal a document the legacy stack cannot decode, breaking the bidirectional wire-compat guarantee this PR exists to establish. Tighten to `require(version == 0 || version == 1)` with a message that names both wire versions. Adds DocumentTransactionsVersionValidationTest pinning the rejection of 2..255 and negative bytes (the `require` runs before the native call, so the rejection paths are exercised on the JVM). Full :sdk unit suite green (113). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ore + JNI, not just Kotlin The version-byte wire-compat guard previously landed only as a Kotlin `require` (DocumentTransactions.kt); the JNI entry point still accepted the stale 0..=255 range and `seal_tx_metadata` wrote the byte verbatim, so a caller reaching the FFI/JNI directly could still seal a document with a version (2..=255) the legacy dashj `decryptTxMetadata` can't decode — silently breaking wire-compat (#4091, findings 9c0ce58c3bb7 and 79595960d201). - seal_tx_metadata now returns Result and rejects any version != 0 (CBOR) / 1 (protobuf) at the one choke point every layer (JNI, FFI, resident wallet) funnels through; the FFI create path propagates the error. Added seal_rejects_non_wire_versions (asserts 0/1 seal, 2..=255 rejected). - The JNI create entry point replaces the `0..=255` check with `0..=1` and a message naming both wire versions, failing fast before the native call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…el nonzero vector as internal slot check (#4091) The reviewer was right on both threads. The legacy dashj createTxMetadata flow has NO identity-index component — it always derives against the primary identity via blockchainIdentityECDSADerivationPath() (index 0) — so legacy wire-compat is only defined at identity_index=0, and no legacy wallet ever wrote a document keyed at a nonzero index. Apply option (a) — vector VALUES unchanged, provenance corrected: 1. legacy_dashj_wire_compat_vector (identity_index=0, 4a2e…84d7): document that the account prefix was verified against the REAL dashj DerivationPathFactory.blockchainIdentityECDSADerivationPath() (path m/9'/1'/5'/0'/0'/0'/keyId'/32769'/encryptionKeyIndex'), not mirrored back from Rust's own tx_metadata_derivation_path (finding dd246b5e17d0). 2. Rename legacy_dashj_wire_compat_vector_nonzero_identity_index -> nonzero_identity_index_derivation_slot_is_internally_consistent and rewrite its docs/asserts: the 8cda…5196 value is SELF-REFERENTIAL (LegacyKeyN.java hand-builds the same path Rust constructs; it does not call the real DerivationPathFactory), so it pins internal slot placement + resident/master agreement only — explicitly NOT a legacy wire-compat claim (finding 4c0754158cc6). 3. Add a doc note on derive_tx_metadata_key and the module header stating wire-compat holds only at identity_index=0. Correct LegacyKeyN.java's header/inline comments and the README to state the generator hand-builds the account path and that the nonzero vector is an internal consistency cross-check, not a legacy sample. cargo test -p platform-wallet --lib: 431 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rifier Addresses blocker 989be307db0f on #4091 (its still-open core ask: "check in the actual JVM repro script/tool for independent verification"). b7319b5 corrected the vectors' provenance in prose (scoped wire-compat to identity_index=0; relabeled the nonzero vector as an internal slot check), but the "confirmed against the REAL dashj DerivationPathFactory" claim was still only asserted — LegacyKeyN.java hand-builds its account path and never drives the factory, so a maintainer could not reproduce the equality from checked-in code. Adds LegacyDerivationPathCheck.java: it drives the real org.bitcoinj.wallet.DerivationPathFactory (dashj-core 22.0.3, Testnet) and asserts its primary-identity path — blockchainIdentityECDSADerivationPath() (no-arg) = m/9'/1'/5'/0'/0'/0' — equals LegacyKeyN's hand-built account path at identity_index 0, printing WIRE_COMPAT_ANCHOR_OK = true (verified: true). It also prints the factory's INDEXED overload m/9'/1'/5'/0'/0'/0'/i' beside the hand-built nonzero path m/9'/1'/5'/0'/0'/i', making the shape difference visible so the nonzero vector is self-evidently NOT a factory-produced legacy sample (cross-refs dd246b5e17d0 / 4c0754158cc6). README: document the verifier + run command, and add the missing de.sfuhrm/saphir-hash-core/3.0.10 jar (TestNet3Params.get() needs X11 genesis-block hashing — the factory path fails with NoClassDefFoundError without it; LegacyKeyN alone never touches network params so it was omitted before). Verified end to end: LegacyDerivationPathCheck 0 -> WIRE_COMPAT_ANCHOR_OK=true; LegacyKeyN 0 2 1 -> 4a2e…84d7 and LegacyKeyN 1 2 1 -> 8cda…5196, both matching the hard-coded Rust vectors exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rdownGate
createEncryptedDocument and fetchEncryptedDocuments borrow the wallet /
mnemonic-resolver / signer handles but opened with plain
withContext(Dispatchers.IO), bypassing the TeardownGate: a concurrent wallet
shutdown could free the borrowed native handles mid-call (freed-handle UB) and
the source-scanning GateCoverageLintTest.everyHandleBorrowingSuspendFunIsGated
failed on both.
Both now open with gate.op { } like their six sibling document methods (gate.op
already runs the body on Dispatchers.IO, so the bodies are unchanged). Dropped
the now-unused Dispatchers/withContext imports. GateCoverageLintTest green; full
:sdk:testDebugUnitTest suite green (174 tests, 0 failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion/network
seal_tx_metadata had no client-side size check: a payload too large for the
encryptedMetadata field (maxItems 4096) derived the key and sealed only to be
rejected at broadcast with an opaque DPP schema error.
Add a typed PlatformWalletError::TxMetadataPayloadTooLarge { len, max } and a
shared ensure_tx_metadata_payload_fits() precheck. It runs FIRST in
prepare_encrypted_txmetadata_properties (before resolve/derive/broadcast) so an
over-large batch fails fast with the length + accepted max, and again inside
seal_tx_metadata as the choke-point last line of defense. The FFI maps the new
variant to ErrorInvalidParameter (already mirrored in Swift/Kotlin — no new
numeric code), so it surfaces sensibly across JNI/FFI with the typed Display.
True envelope math, derived from the code (not hardcoded): the blob is
version(1) + IV(16) + AES-256-CBC/PKCS7(plaintext). PKCS7 always adds a full
block when the plaintext is block-aligned, so ciphertext = 16*(L/16 + 1) and
blob = 17 + that. The largest ciphertext that fits 4096 is ((4096-17)/16)*16 =
254*16 = 4064, and since PKCS7 spends >=1 byte on padding the max plaintext is
one less -> MAX_TX_METADATA_PLAINTEXT_LEN = 4063. That plaintext frames to a
4081-byte blob (NOT 4096 as the review's arithmetic stated); 4064 jumps to 4097
and is the first rejected length. The 4063/4064 boundary itself matches the
reviewer; the "4063 -> 4096" envelope size does not (see the new boundary test,
which pins the real 4081-byte blob).
Boundary test seal_rejects_payload_above_size_limit: 4063 seals (blob == 4081,
round-trips), 4064 rejected as TxMetadataPayloadTooLarge, and the standalone
precheck agrees at the boundary.
Also strips the co-located "finding <hex>" tracker tokens from the comments in
these two files (rationale text kept); they move to the PR description.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…broadcast await platform_wallet_create_encrypted_document_with_signer copied the caller payload into a plain Vec<u8> that stayed live in the outer scope across the network broadcast .await — the native plaintext lingered in memory the whole time the document was being broadcast. Wrap the copy in Zeroizing<Vec<u8>> and make the with_item closure `move` so it OWNS the buffer, then drop(payload_vec) the instant the encrypted properties are prepared (right beside the existing master-key drop), before block_on_worker. The plaintext is now scrubbed and gone before any .await; only the sealed ciphertext properties cross into the async block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gated check The txmetadata_fetch doc claimed the wire-query regression is "caught in CI (against testnet)", but the test is #[ignore = "hits testnet"] and nothing runs `--ignored`, so CI never executes it. Soften the wording: it is a MANUAL, testnet-gated check, run explicitly with `--ignored`, NOT part of the default `cargo test`/CI run and with no scheduled job running `--ignored` today — a local/pre-release regression gate. (Wiring a scheduled `--ignored` job is out of scope for this PR.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the "finding <hex>" / "blocker <hex>" internal tracking tokens from the remaining code comments and docs (the co-located tokens in tx_metadata.rs and encrypted_document.rs were stripped in the size-precheck commit). Rationale text and the #4091 issue reference are kept; the tracker refs belong in the PR description, not the source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tract schema Review round 2: the 4096 field limit was a local const silently duplicating the wallet-utils contract's encryptedMetadata maxItems; pin them together so a contract-side limit change fails a test instead of drifting past the size precheck. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the reviewer-requested independent check (#4186): decrypt a txMetadata blob produced by a REAL legacy dash-wallet 11.9 install, not one this repo generated. A designated-throwaway testnet wallet (DPNS name `yabba2`, identity ESR1nfF3bj4TR2ZkLmDuSeu6r7VzpTurYi47BV6XwsoP) running stock dash-wallet 11.9 registered a username, did a send + receive, saved metadata, and published one encrypted `txMetadata` document to Platform. It was fetched back off testnet and decrypted with the NEW Rust crypto. - `legacy_install_yabba2_wire_compat_vector` (network-free fixture in tx_metadata.rs): hard-codes the recovery phrase, the real captured blob hex (version 1/protobuf, keyIndex 2, encryptionKeyIndex 1), and the expected protobuf `TxMetadataBatch` plaintext (two items, memos "username"/"faucet", USD exchange rates), and asserts the new `derive_tx_metadata_key` + `open_tx_metadata` path decrypts it byte-for-byte via both the resident and resolver-master key sources. Doc-commented as the independent legacy-install vector, distinct from the self-generated dashj-core scratch vectors. - `capture_legacy_yabba2_txmetadata_blobs` (testnet-gated helper in tests/txmetadata_fetch.rs): resolves the DPNS name, runs the exact production query, derives from the phrase, and prints the capture used to build the fixture. - README: documents the new real-install vector alongside the JVM-generated ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The JNI-owned `payload_bytes` in `documentCreateEncrypted` was a plain `Vec<u8>` held across the entire synchronous FFI call — including the network broadcast that runs inside it — and freed unscrubbed at end of scope. The inner FFI copy (`payload_vec` in rs-platform-wallet-ffi's document.rs) already got `Zeroizing` + an explicit pre-broadcast drop; mirror that discipline for the JNI copy so the plaintext-lifetime guarantee holds end-to-end. - Wrap `payload_bytes` in `zeroize::Zeroizing` so it is scrubbed on drop. - Drop it explicitly the instant the FFI call returns (the earliest point reachable from JNI, since the broadcast completes inside that call), before result/JSON handling. It is the only plaintext copy in the fn. Also strip the two surviving `#4091` tracker tokens from the version-guard and fetch-breadcrumb comments (rationale text kept). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, not the host Follow-up to #4186 (shumkov review on the encrypted-documents port): the encryptionKeyIndex allocation policy was left in the Kotlin host, which told callers to supply the legacy `1 + countAllRequests()` counter. Concurrent callers/devices could pick the same index, and a caller-side key-index policy loop violates the Kotlin SDK host-thin rule. Move the policy into Rust; hosts now provide only the opaque payload. Rust core (rs-platform-wallet): - IdentityWallet::allocate_encryption_key_index counts the identity's existing txMetadata documents on Platform and returns `1 + count`, matching dash-wallet's retired `1 + countAllRequests()` (SELECT COUNT(*) FROM transaction_metadata_platform) semantics EXACTLY (count+1, not max+1; empty state -> 1). - Allocation is serialized through a shared per-wallet allocator mutex (EncryptionKeyIndexAllocator on IdentityWallet, an Arc<Mutex<HashMap>> shared across handle clones): two concurrent creates through the SAME process seed the in-process high-water once from Platform and then hand out monotonically increasing indices, so they can never pick the same index. - Cross-device uniqueness is best-effort only and is NOT data-loss: every document stores its own keyIndex/encryptionKeyIndex and the reader derives each document's key from its own stored indices, so two documents sharing an index each carry a fresh IV and both decrypt independently. - Unit tests: legacy 1+count math (incl. saturation), empty-state seed + increment, per-owner isolation, and a concurrent no-collision test. FFI (rs-platform-wallet-ffi): - Add ABI-additive sibling platform_wallet_create_encrypted_document_with_signer_auto_index (identical params minus encryption_key_index); the existing explicit-index export is unchanged and both share one impl taking Option<u32>. When None, the index is allocated from Platform state before any key material is resolved. JNI (rs-unified-sdk-jni): - documentCreateEncrypted treats encryptionKeyIndex == -1 as the "let Rust allocate" sentinel (routes to the auto-index export); a non-negative value routes to the explicit export; < -1 is rejected. Kotlin SDK: - DocumentTransactions.createEncryptedDocument takes encryptionKeyIndex: Int? = null (null -> allocate in Rust); removed the `1 + countAllRequests()` guidance and deprecated the caller-supplied counter in KDoc. Null maps to the -1 JNI sentinel. - Tests: explicit-negative rejection and no-index-path acceptance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round 2 (doc-only): the per-wallet mutex serializes cross-owner allocations during a first-time seed fetch, and a create that fails after allocating leaves an index gap, never a collision — both now stated at the allocator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing encryptionKeyIndex Addresses shumkov's #4195 review: on the auto-index create path the network count / high-water reservation (`allocate_encryption_key_index` -> `reserve_next_index`) ran BEFORE the deterministic 4063-byte payload gate, so an oversized payload — which must fail — still consumed an index and left a gap. Run the size check first. It is a pure, network-free bound (`ensure_tx_metadata_payload_fits` / `MAX_TX_METADATA_PLAINTEXT_LEN`) and needs no key material: - new `reserve_next_index_checked(allocator, owner, payload_len, seed)` runs `ensure_tx_metadata_payload_fits(payload_len)?` before `reserve_next_index`, so an oversized payload returns the typed `TxMetadataPayloadTooLarge` without polling the seed — the high-water is never seeded or advanced (no consumed index, no gap). - `IdentityWallet::allocate_encryption_key_index` gains a `payload_len` param and routes through the checked variant; the FFI auto-index create path passes `payload_vec.len()`. Explicit-index path is unchanged (it never allocates). - new unit test `oversized_payload_does_not_advance_highwater`: asserts the typed error, that the owner is absent from the allocator map, and that the next well-sized reservation still seeds at 1 (no gap). cargo test (platform-wallet allocator 5/5, platform-wallet-ffi 204/204) + clippy green; Kotlin :sdk:compileDebugKotlin + :sdk:testDebugUnitTest green (JNI/Kotlin ABI unchanged — payload_len plumbing is internal to Rust). Note (source-breaking, positional Kotlin callers): the #4186 stack moved `SDK.Documents.createEncryptedDocument`'s `encryptionKeyIndex` to the last positional slot (`Int? = null`). Positional callers must drop the argument (let Rust allocate) or switch to a named argument; named callers are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mechanical Swift wrappers for the encrypted-document C-ABI exports added by #4091 (port/v4.1/encrypted-documents). Re-stacked onto #4195 (followup/v4.1/keyindex-rust-allocation) per the reviewer sequencing decision so the Rust-side auto-index export is available: - platform_wallet_create_encrypted_document_with_signer_auto_index - platform_wallet_fetch_encrypted_documents Adds ManagedPlatformWallet.createEncryptedDocument(...) and .fetchEncryptedDocuments(...) to Sources/SwiftDashSDK/PlatformWallet, mirroring the existing createDocument (signer + byte-buffer marshalling, withExtendedLifetime pinning, result-code .check(), string_free) and previewIdentityRegistrationKeys (internal MnemonicResolver construction + pinning) patterns. The plaintext payload is handed straight to Rust's Zeroizing buffer with no extra Swift-side copy, as the neighboring seed path does. createEncryptedDocument now calls the AUTO-INDEX export and DROPS the host-supplied encryptionKeyIndex parameter: Rust allocates the per-document encryptionKeyIndex from authoritative Platform state (#4195), so hosts no longer assign it (host-side assignment risked cross-device collisions). This matches the Android auto-index path, where Kotlin's createEncryptedDocument omits the index (encryptionKeyIndex = null). The version-byte {0,1} guard and argument-order/nullability parity with the Kotlin counterpart are preserved; fetchEncryptedDocuments is unchanged. Updates EncryptedDocumentVersionValidationTests (the Swift mirror of the Kotlin DocumentTransactionsVersionValidationTest) to the new signature: the wire-meaningless version bytes (2/3/127/255) are rejected before any FFI dispatch. Verified: cbindgen regenerates the platform-wallet-ffi header with the auto-index export at the expected signature (no encryption_key_index arg); `swift build` of SwiftDashSDK type-checks the reworked call site against that header. `swift test` currently fails only at link because the checked-in prebuilt DashSDKFFI.xcframework static archive predates #4195 and lacks the auto-index symbol; a framework rebuild against #4195 resolves it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…crypt-plaintext-lifetime
Keep decrypted txMetadata payloads in zeroizing owners through AES, wallet, and FFI serialization, then release the shared C result through a dedicated zeroizing free on Kotlin/JNI and Swift. Preserve the existing JSON and host String APIs while documenting that runtime-managed host strings and parsed copies cannot be reliably scrubbed. Test would have caught this in CI: ✖ baseline lifetime assertions did not compile because decrypted payloads were plain Vec values and no sensitive decrypt primitive existed; ✔ the unchanged assertions and focused encryption, wallet, FFI, and JNI suites pass with zeroizing owners and sensitive release coverage.
…crypt-plaintext-lifetime
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughAdds legacy-compatible encrypted ChangesEncrypted txMetadata flow
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant KotlinOrSwift
participant JNIOrSwiftBridge
participant WalletFFI
participant IdentityWallet
participant Platform
KotlinOrSwift->>JNIOrSwiftBridge: request encrypted document fetch
JNIOrSwiftBridge->>WalletFFI: call fetch FFI
WalletFFI->>Platform: query owned encrypted documents
Platform-->>IdentityWallet: encrypted document records
IdentityWallet->>IdentityWallet: derive keys and decrypt metadata
IdentityWallet-->>WalletFFI: decrypted records
WalletFFI-->>JNIOrSwiftBridge: sensitive JSON pointer
JNIOrSwiftBridge->>WalletFFI: zeroize and free pointer
JNIOrSwiftBridge-->>KotlinOrSwift: JSON string
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (6)
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt (1)
189-194: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMirror the fetch-side plaintext-residual caveat here.
documentFetchEncrypted's KDoc (Lines 223-227) tells callers the hostStringis plaintext-equivalent and unscrubuable. The create side takes a caller-owned plaintextByteArraywith the same property and no equivalent note — worth documenting symmetrically since the JNI copy isZeroizingbut the Kotlin-owned array is not.📝 Proposed KDoc addition
* `@param` payload the already-serialized opaque plaintext (a protobuf * `TxMetadataBatch`); the SDK does not parse it. + * The native copy is zeroized after the broadcast completes, but this + * caller-owned `ByteArray` is not — overwrite it yourself once the call + * returns if the plaintext must not linger on the JVM heap. * `@return` the confirmed document's canonical JSON (its 32-byte id is the * base58 `$id` field).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt` around lines 189 - 194, Update the KDoc for the create-side transaction method near the payload parameter to state that the caller-owned plaintext ByteArray is plaintext-equivalent and cannot be scrubbed by the SDK; clarify that JNI zeroization does not erase the Kotlin-owned array. Keep the existing parameter and return documentation unchanged.packages/rs-unified-sdk-jni/src/transactions.rs (1)
945-956: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueOptional: reuse a pointer guard for the create path's JSON too.
out_jsonis freed manually here, so a panic between the FFI return and line 956 leaks the allocation (ciphertext-only, so no plaintext exposure). A small non-sensitive analogue ofSensitivePlatformWalletStringwould make both bridges structurally identical.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/rs-unified-sdk-jni/src/transactions.rs` around lines 945 - 956, Add a non-sensitive RAII pointer guard for the encrypted document create path and use it for out_json in place of the manual platform_wallet_string_free call. Ensure the guard frees the FFI allocation on normal completion and during unwinding, while preserving the existing null handling and JSON conversion in the surrounding create flow.packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift (1)
3482-3489: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSwift-side wire-version constant duplicates a Rust-owned protocol invariant.
seal_tx_metadataalready rejects non-0/1 versions, so this guard hard-codes a wire constant in the Swift bridge purely for a faster error. If you keep it (the existingEncryptedDocumentVersionValidationTestsrely on it), consider surfacing the accepted set from Rust rather than literals so the two can't drift.As per coding guidelines: "In Swift, do not perform ... any reimplementation of protocol constants such as gap limits, key indices, or path shapes."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift` around lines 3482 - 3489, The version guard in seal_tx_metadata duplicates Rust-owned protocol constants by hard-coding 0 and 1 in Swift. Remove this Swift-side validation and let the Rust FFI enforce accepted versions, updating EncryptedDocumentVersionValidationTests to assert the resulting propagated error behavior without relying on the duplicated Swift guard.Source: Coding guidelines
packages/rs-platform-wallet/src/wallet/identity/network/encrypted_document.rs (1)
226-234: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winWARN breadcrumbs contradict the stated "no identity-correlated data in logcat" rule.
breadcrumb's doc (Lines 216-220) justifies DEBUG precisely so identity/contract/document ids never reach logcat, yet everybreadcrumb_errorcall site embedsowner=anddoc=and lands at WARN (logcat-visible). The skip paths (un-materialized entry, missing fields, decrypt failure) are not exceptional enough to treat as one-off diagnostics. Consider truncating the ids (e.g. first 8 hex chars) in the WARN lines so on-device logs stay non-correlatable while remaining actionable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/rs-platform-wallet/src/wallet/identity/network/encrypted_document.rs` around lines 226 - 234, Update breadcrumb_error and its call sites to prevent full identity, contract, and document identifiers from reaching WARN-level logcat output. Truncate each embedded identifier to a non-correlatable short prefix, such as the first eight hexadecimal characters, while retaining enough context to distinguish failure or skip paths; leave normal breadcrumb handling unchanged.packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/documents/DocumentTransactions.kt (1)
313-320: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe
{0, 1}version set is a protocol constant now duplicated in the host.
seal_tx_metadataalready rejects any other byte, so this guard restates wire knowledge that the Kotlin SDK is supposed to leave in Rust — and it will silently reject a future version 2 that Rust accepts. If the fail-fast is worth keeping, consider exposing the allowed set from Rust (or a small FFI validation entry point) rather than hardcoding the literals here.As per coding guidelines, "Do not implement derivation-path construction, policy-loop orchestration, mnemonic/seed processing across JNI, protocol constants... implement these in Rust instead."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/documents/DocumentTransactions.kt` around lines 313 - 320, The version validation in the document transaction flow duplicates Rust-owned protocol knowledge. Remove the hardcoded require check for version values 0 and 1 from the surrounding transaction-sealing method, leaving validation to seal_tx_metadata; do not introduce another Kotlin-side constant or validation path.Source: Coding guidelines
packages/rs-platform-wallet/tests/txmetadata_fetch.rs (1)
241-258: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueCapture scaffolding permanently prints decrypted plaintext and a recovery phrase.
The hardcoded mnemonic and the
PLAINTEXT_HEX/PLAINTEXT_UTF8_LOSSYprints are justified for a designated-throwaway fixture wallet, and the doc comment says so explicitly. Once the vectors are captured intolegacy_install_yabba2_wire_compat_vector, this helper's job is done — worth deleting it (or moving it toexamples/) rather than leaving a permanent plaintext-dumping code path in the test tree that a future maintainer might re-point at a real wallet.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/rs-platform-wallet/tests/txmetadata_fetch.rs` around lines 241 - 258, Remove the temporary plaintext-dumping capture helper and its associated hardcoded mnemonic/scaffolding from the test tree now that legacy_install_yabba2_wire_compat_vector contains the vectors. Delete the PLAINTEXT_HEX and PLAINTEXT_UTF8_LOSSY output along with the helper’s other diagnostic prints, or move the complete capture utility to examples/ without leaving a reusable decrypted-data path in tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/sdk/TXMETADATA_DECRYPT_PLAINTEXT_LIFETIME_SPEC.md`:
- Line 66: Correct the spelling of “unsrubbable” to “unscrubbable” in both the
prose description and the Mermaid node, preserving all other wording and
behavior.
In `@packages/rs-platform-wallet/src/wallet/identity/crypto/tx_metadata.rs`:
- Around line 254-265: Update the derivation flow around derived.private_key in
the transaction metadata key function to explicitly call non_secure_erase()
after copying secret_bytes() and before returning the Zeroizing-wrapped scalar.
Revise the adjacent hygiene comment to remove the incorrect claim that SecretKey
is memzeroed on drop.
In `@packages/rs-platform-wallet/tests/txmetadata_fetch.rs`:
- Around line 57-59: Remove live testnet dependencies from the ignored tests,
including fetch_returns_both_legacy_txmetadata_documents and the second related
test. Mock the SDK/DAPI interactions so the wire query’s where-clause and
order-by shape are asserted offline; alternatively relocate the live probes
outside tests/ and keep hermetic coverage for the query behavior.
In `@packages/rs-unified-sdk-jni/src/support.rs`:
- Around line 78-86: In packages/rs-unified-sdk-jni/src/support.rs lines 78-86,
update take_pwffi_error to keep only the raw and offset codes in the warn! log,
and emit message separately with log::debug!. Apply the same change at lines
104-107: keep only code at warn! and move message to log::debug!, preserving the
existing code values and removing duplicate message emission.
---
Nitpick comments:
In
`@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/documents/DocumentTransactions.kt`:
- Around line 313-320: The version validation in the document transaction flow
duplicates Rust-owned protocol knowledge. Remove the hardcoded require check for
version values 0 and 1 from the surrounding transaction-sealing method, leaving
validation to seal_tx_metadata; do not introduce another Kotlin-side constant or
validation path.
In
`@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.kt`:
- Around line 189-194: Update the KDoc for the create-side transaction method
near the payload parameter to state that the caller-owned plaintext ByteArray is
plaintext-equivalent and cannot be scrubbed by the SDK; clarify that JNI
zeroization does not erase the Kotlin-owned array. Keep the existing parameter
and return documentation unchanged.
In
`@packages/rs-platform-wallet/src/wallet/identity/network/encrypted_document.rs`:
- Around line 226-234: Update breadcrumb_error and its call sites to prevent
full identity, contract, and document identifiers from reaching WARN-level
logcat output. Truncate each embedded identifier to a non-correlatable short
prefix, such as the first eight hexadecimal characters, while retaining enough
context to distinguish failure or skip paths; leave normal breadcrumb handling
unchanged.
In `@packages/rs-platform-wallet/tests/txmetadata_fetch.rs`:
- Around line 241-258: Remove the temporary plaintext-dumping capture helper and
its associated hardcoded mnemonic/scaffolding from the test tree now that
legacy_install_yabba2_wire_compat_vector contains the vectors. Delete the
PLAINTEXT_HEX and PLAINTEXT_UTF8_LOSSY output along with the helper’s other
diagnostic prints, or move the complete capture utility to examples/ without
leaving a reusable decrypted-data path in tests.
In `@packages/rs-unified-sdk-jni/src/transactions.rs`:
- Around line 945-956: Add a non-sensitive RAII pointer guard for the encrypted
document create path and use it for out_json in place of the manual
platform_wallet_string_free call. Ensure the guard frees the FFI allocation on
normal completion and during unwinding, while preserving the existing null
handling and JSON conversion in the surrounding create flow.
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift`:
- Around line 3482-3489: The version guard in seal_tx_metadata duplicates
Rust-owned protocol constants by hard-coding 0 and 1 in Swift. Remove this
Swift-side validation and let the Rust FFI enforce accepted versions, updating
EncryptedDocumentVersionValidationTests to assert the resulting propagated error
behavior without relying on the duplicated Swift guard.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d8bf60e2-0712-41c6-8571-fd95bd90e173
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (31)
docs/sdk/TXMETADATA_DECRYPT_PLAINTEXT_LIFETIME_SPEC.mdpackages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/documents/DocumentTransactions.ktpackages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/TransactionsNative.ktpackages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/documents/DocumentTransactionsVersionValidationTest.ktpackages/rs-platform-encryption/Cargo.tomlpackages/rs-platform-encryption/src/aes.rspackages/rs-platform-encryption/src/lib.rspackages/rs-platform-wallet-ffi/Cargo.tomlpackages/rs-platform-wallet-ffi/src/document.rspackages/rs-platform-wallet-ffi/src/error.rspackages/rs-platform-wallet-ffi/src/lib.rspackages/rs-platform-wallet-ffi/src/tx_metadata_json.rspackages/rs-platform-wallet-ffi/src/types.rspackages/rs-platform-wallet/Cargo.tomlpackages/rs-platform-wallet/src/error.rspackages/rs-platform-wallet/src/lib.rspackages/rs-platform-wallet/src/wallet/identity/crypto/mod.rspackages/rs-platform-wallet/src/wallet/identity/crypto/tx_metadata.rspackages/rs-platform-wallet/src/wallet/identity/network/encrypted_document.rspackages/rs-platform-wallet/src/wallet/identity/network/identity_handle.rspackages/rs-platform-wallet/src/wallet/identity/network/mod.rspackages/rs-platform-wallet/src/wallet/identity/network/payments.rspackages/rs-platform-wallet/src/wallet/platform_wallet.rspackages/rs-platform-wallet/tests/legacy_wire_compat/LegacyDerivationPathCheck.javapackages/rs-platform-wallet/tests/legacy_wire_compat/LegacyKeyN.javapackages/rs-platform-wallet/tests/legacy_wire_compat/README.mdpackages/rs-platform-wallet/tests/txmetadata_fetch.rspackages/rs-unified-sdk-jni/src/support.rspackages/rs-unified-sdk-jni/src/transactions.rspackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swiftpackages/swift-sdk/SwiftTests/SwiftDashSDKTests/EncryptedDocumentVersionValidationTests.swift
Declined on this U7 branch: all six suggestions target inherited dependency-stack code rather than the decrypt plaintext-lifetime change—#4186 owns the Kotlin create KDoc/version guard, JNI create JSON cleanup, wallet breadcrumbs, and capture helper; #4194 owns the Swift version guard. U7 is intentionally limited to the shared decrypt path and host residual documentation, so applying these here would duplicate parent/host fixes. They should land in their originating PRs and flow forward into this branch. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## v4.2-dev #4243 +/- ##
==========================================
Coverage 87.54% 87.55%
==========================================
Files 2670 2671 +1
Lines 338763 338901 +138
==========================================
+ Hits 296583 296718 +135
- Misses 42180 42183 +3
🚀 New features to boost your workflow:
|
Rust 1.92 formatting rejected dependency-stack code before the macOS workspace job could reach its tests. CI transition: cargo fmt --check --all failed before formatting and passes afterward.
|
🕓 Ready for review — 7 ahead in queue (commit 9870f4f) |
Issue being fixed or feature implemented
Decrypting encrypted txMetadata could leave plaintext in SDK-owned AES, wallet, JSON/base64, and C-string allocations after those allocations were released. This closes the native lifetime gaps for both Kotlin and Swift while preserving the existing JSON and host
StringAPIs; the terminal runtime-managed host strings remain unsrubbable and are documented symmetrically.This is the final stacked parity leg. Land the encrypted-document core, Rust allocator, and Swift-wrapper dependencies before this PR.
Related: #4186, #4195, #4194
What was done?
Zeroizing<Vec<u8>>, including padding-error and unwind paths.platform_wallet_sensitive_string_freecontract that wipes the complete C allocation before deallocation.NewStringUTF; make Swift release the same result through the sensitive free even when result validation throws.Stringresidual with identical retention and logging guidance.How Has This Been Tested?
Vec<u8>; the unchanged assertions now compile and pass.cargo test -p platform-encryption— 18 passed.cargo test -p platform-wallet --lib tx_metadataplus the decrypted-document owner regression — 14 passed.cargo test -p platform-wallet-ffi --lib— 225 passed.cargo test -p rs-unified-sdk-jni --lib— 38 passed.--verify— 227 JNI exports, C ABI anchor present, and no LOAD segment below 16 KB alignment../gradlew :sdk:assembleDebug :sdk:testDebugUnitTest— successful.swift test— 282 passed, including the encrypted-document version test; 8 explicitly gated integration tests skipped becauseRUN_INTEGRATION_TESTSwas unset.cargo fmtpasses for the changed encryption, FFI, and JNI crates;git diff --checkpasses. Full-workspace formatting remains red on inherited formatting in the stacked allocator/wallet changes and was not swept into this fix.New concepts
Fixed-size sensitive serialization
Sensitive output is counted before plaintext writing begins, then written into one exact-size zeroizing allocation:
This avoids the ordinary
Value → String → CStringchain, where every growth or conversion can strand another plaintext allocation. It is appropriate for small, security-sensitive outputs whose exact encoded length is computable; ordinary non-sensitive JSON should keep using Serde.Breaking Changes
Rust source compatibility only:
OpenedTxMetadata.payloadandDecryptedEncryptedDocument.payloadchange fromVec<u8>toZeroizing<Vec<u8>>. The C fetch signature, JNI descriptor, Kotlin/Swift signatures, and JSON shape are unchanged; the sensitive free symbol is ABI-additive.Checklist:
For repository code-owners and collaborators only
Summary by CodeRabbit