Skip to content

fix(trezor): redact secrets in log_debug transport output (#125) - #144

Open
coreyphillips wants to merge 3 commits into
masterfrom
issue-125
Open

fix(trezor): redact secrets in log_debug transport output (#125)#144
coreyphillips wants to merge 3 commits into
masterfrom
issue-125

Conversation

@coreyphillips

@coreyphillips coreyphillips commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Closes #125

Redact secrets from Trezor transport debug output in core, before the log_debug callback crosses the FFI boundary.

CallbackAdapter::log_debug forwarded the trezor-connect-rs diagnostic stream verbatim to the native callback. That stream is owned by the dependency, so its contents are only ever known for the pinned version; in 0.4.0 it is mostly state names and byte counts, but nothing stops a bump from adding credential blobs or frame dumps. The only control was consumer-side regex scrubbing, which bitkit-android#1067 showed is not a security boundary: unexpected labels, bare values and JSON arrays all slip past it.

What changed

  • New src/modules/trezor/log_sanitizer.rs: sanitize_debug_log(tag, message) redacts labeled key=value / "key": value pairs whose key names a secret, replacing the value with a shape-preserving <redacted> placeholder.
  • A second pass in redact_bare_secrets catches unlabeled material: extended keys (xpub/xprv and variants), base64 PSBTs, hex runs of 16+ bytes, and long base64 blobs.
  • is_harmless_value lets booleans, counts, byte lengths and None through under a sensitive label, so has_credentials=true and payload: 48 bytes still read as before; ALWAYS_SENSITIVE_KEY_FRAGMENTS (pin, passphrase, mnemonic, ...) is exempt from that carve-out because those secrets can themselves be short integers.
  • Both strings are length-capped (32 chars for the tag, 512 for the message), which also bounds the per-chunk BLE spam.
  • CallbackAdapter::log_debug in implementation.rs now sanitizes both arguments before invoking the native callback; the signature is unchanged.
  • The log_debug trait doc in callbacks.rs states the redaction guarantee so consumers can drop their own scrubbing.

How to test

  • cargo test modules::trezor, 75 pass, 15 of them new under tests.rs::log_sanitizer.
  • test_no_fixture_secret_survives_sanitization is the regression guard: it pushes each fixture (credential=, psbt=, bare xpub, frame hex, thp_credential/master_key, passphrase/pin/mnemonic) through as both tag and message and asserts no known secret appears in the output.
  • test_connection_state_passes_through, test_error_codes_pass_through and test_byte_lengths_and_booleans_pass_through pin the diagnostics that must survive.
  • cargo clippy --all-targets, no new warnings (the trezor warnings shown are pre-existing).
  • rustfmt --edition 2021 --check src/modules/trezor/*.rs, clean.
  • cargo check --target aarch64-apple-ios --lib, compiles the mobile-only callback adapter, which is the actual call site.
  • Full cargo test is 508 pass / 11 fail; all 11 failures are blocktank tests calling api.stag.blocktank.to and are network-dependent, unrelated to this change.

The log_debug stream handed to native apps is produced by trezor-connect-rs,
so its contents change with every dependency bump and cannot be assumed free
of key material. Consumer-side regex scrubbing was shown not to be a security
boundary (synonymdev/bitkit-android#1067).

Sanitize at the source instead: the callback adapter now runs both tag and
message through a redaction pass that blanks labeled secrets, bare extended
keys, PSBTs and long hex/base64 runs, and caps each string's length. Counts,
flags and error strings still pass through so the diagnostics stay useful.

No FFI signature change.
Multi-word values (mnemonics, unquoted passphrases) kept every word after
the first, since the value pattern stops at whitespace; a redaction now
absorbs trailing words up to the next delimiter or key=value pair.

Bare integers passed through under sensitive labels, so token=1234567890
was forwarded intact. A number is now only harmless when it carries a unit
or its label names a count or length.

Byte dumps a debug formatter split into groups ([04, 20, 00, ff, ...],
04:20:00:ff:..., [4, 32, 0, 255, ...]) bypassed the contiguous-hex pass
and are now redacted too.
Sensitive labels forwarded their value whenever it happened to look like
another labeled pair, so `token=user:hunter2` crossed log_debug intact:
only descend into a value that is itself a sensitive pair.

Four more shapes the flat parser let through:

- a secret nested under an innocuous label (`context={"token":"hunter2"}`)
  was forwarded whole; non-sensitive labels now recurse into their value
- a two-word label (`seed phrase:`) could not be spanned by the key
  pattern, so the up-to-two words preceding a label now count toward it
- an escaped quote ended a quoted value early, leaking the tail
- a spaced-out separator (`pin = 1234`) was absorbed as a trailing word of
  the previous secret, leaving its own value unredacted
@coreyphillips

Copy link
Copy Markdown
Collaborator Author

Not signed off: the last round of fixes was pushed but has not been reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(trezor): redact secrets in log_debug transport output

1 participant