Split out of #103. The other two findings in that issue are fixed in #154 and #156; this one cannot be fixed on main, which is why it needs its own tracking issue rather than blocking #103.
Why it can't be fixed yet
chisel-ffi/ does not exist on origin/main. The Swift/UniFFI binding lives only on the local design/swift-binding branch, which sits on the orphaned pre-v1.0.0 lineage — git merge-base origin/main design/swift-binding reports no common ancestor. There is no file on the published tree to patch.
The defect (verbatim from the 2026-07-29 review)
chisel-ffi/src/types.rs:71 and :110:
#[derive(uniffi::Enum, Clone, Debug)]
pub enum ChiselKey { Raw { bytes: Vec<u8> }, Passphrase { phrase: String } }
Options embeds pub encryption_key: Option<ChiselKey> and is likewise #[derive(uniffi::Record, Clone, Debug)]. Both derive Debug on the plaintext secret.
This sits directly against the crate's own key-handling discipline two lines below: into_engine wraps everything in Zeroizing, and the root Cargo.toml pins zeroize specifically to wipe key material on drop. The Clone derive has the mirror problem — a cloned ChiselKey holds a plain Vec<u8>/String dropped without wiping.
Note the engine crate already does this correctly: crypto::Key hand-writes Debug to print Key::Raw(<redacted>) (src/crypto/mod.rs:42-49). The FFI layer just didn't inherit the discipline.
Why it matters
No call site formats these today, so nothing leaks right now — the derives are a standing trap. Any future tracing::debug!(?options), assert_eq! failure message, #[derive(Debug)] on an enclosing struct, or unwrap() on a Result<_, Options> prints the user's passphrase verbatim. On iOS those destinations (os_log, crash reporters) are routinely uploaded off-device.
Direction of a fix
Hand-write Debug for ChiselKey (and therefore for Options) to redact the payload — mirroring crypto::Key's existing impl — and drop Clone from ChiselKey unless a call site needs it.
Sequencing
This should be applied as part of porting the Swift binding onto the published lineage, not before — a fix committed to the orphaned branch cannot be PR'd and would likely be lost in the port. Whoever does that port should treat this as a checklist item.
Split out of #103. The other two findings in that issue are fixed in #154 and #156; this one cannot be fixed on
main, which is why it needs its own tracking issue rather than blocking #103.Why it can't be fixed yet
chisel-ffi/does not exist onorigin/main. The Swift/UniFFI binding lives only on the localdesign/swift-bindingbranch, which sits on the orphaned pre-v1.0.0 lineage —git merge-base origin/main design/swift-bindingreports no common ancestor. There is no file on the published tree to patch.The defect (verbatim from the 2026-07-29 review)
chisel-ffi/src/types.rs:71and:110:Optionsembedspub encryption_key: Option<ChiselKey>and is likewise#[derive(uniffi::Record, Clone, Debug)]. Both deriveDebugon the plaintext secret.This sits directly against the crate's own key-handling discipline two lines below:
into_enginewraps everything inZeroizing, and the rootCargo.tomlpinszeroizespecifically to wipe key material on drop. TheClonederive has the mirror problem — a clonedChiselKeyholds a plainVec<u8>/Stringdropped without wiping.Note the engine crate already does this correctly:
crypto::Keyhand-writesDebugto printKey::Raw(<redacted>)(src/crypto/mod.rs:42-49). The FFI layer just didn't inherit the discipline.Why it matters
No call site formats these today, so nothing leaks right now — the derives are a standing trap. Any future
tracing::debug!(?options),assert_eq!failure message,#[derive(Debug)]on an enclosing struct, orunwrap()on aResult<_, Options>prints the user's passphrase verbatim. On iOS those destinations (os_log, crash reporters) are routinely uploaded off-device.Direction of a fix
Hand-write
DebugforChiselKey(and therefore forOptions) to redact the payload — mirroringcrypto::Key's existing impl — and dropClonefromChiselKeyunless a call site needs it.Sequencing
This should be applied as part of porting the Swift binding onto the published lineage, not before — a fix committed to the orphaned branch cannot be PR'd and would likely be lost in the port. Whoever does that port should treat this as a checklist item.