feat(types): introduce SecretString and use it for LoginParams password#56
Open
devin-ai-integration[bot] wants to merge 1 commit intomainfrom
Open
feat(types): introduce SecretString and use it for LoginParams password#56devin-ai-integration[bot] wants to merge 1 commit intomainfrom
devin-ai-integration[bot] wants to merge 1 commit intomainfrom
Conversation
Passwords passed through LoginParams used to live in a plain
String. Rust's String does not zeroise its buffer on drop, so
compromised heap dumps or post-free memory reads could surface
the cleartext password long after login finished. LoginParams
also derived Debug, so ad-hoc println!/tracing of it leaked the
full credential.
This PR introduces a minimal, crate-local zeroising secret
wrapper and adopts it on LoginParams.
New API (teamtalk::types):
* SecretString — UTF-8 secret with:
- #[derive(Clone, ZeroizeOnDrop)] so the backing buffer is
wiped when the value (or a clone) goes out of scope.
- Custom Debug / Display that render as '<redacted>' so the
secret never hits logs.
- Content-based, branch-less PartialEq / Eq over equal-length
byte strings (a.len() != b.len() still short-circuits).
- expose_secret() -> &str as the single read-out method; doc
block explicitly warns to keep the borrow narrow.
- zeroize_in_place() to wipe the buffer while keeping capacity.
- From<String>, From<&str>, From<&String>, From<Cow<'_, str>>
so callers do not need to think about the wrapper unless
they want to.
* Re-exported as teamtalk::types::SecretString (and via
teamtalk::types::secret::SecretString for the focused path).
LoginParams migration:
* LoginParams.password: String -> SecretString.
* LoginParams::new(.., password: impl Into<SecretString>, ..) so
callers keep passing &str/String unchanged.
* LoginParams::from_env() continues to read TT_PASS into a
SecretString.
* Debug derive on LoginParams now auto-redacts the password
because SecretString's Debug returns '<redacted>'; nickname
and username remain visible.
* All internal consumers updated to call
params.password.expose_secret() at the FFI boundary only:
- client/users/auth.rs: login_with_params, login_from_env.
- client/core/recovery.rs: auto-login path after reconnect.
Dependency: adds zeroize = { version = "1.8", features =
["zeroize_derive"] } to teamtalk's Cargo.toml. The secrecy
crate was intentionally not pulled in to keep the dependency
surface small; the local wrapper has no external users.
Breaking change note:
LoginParams.password: String -> SecretString.
LoginParams is #[non_exhaustive] so field-init syntax already
required .. rest patterns, but direct field reads now need
.expose_secret() to get &str. SecretString implements
From<String>/From<&str>, so constructing LoginParams with new()
continues to work with the same argument types as before.
Tests: crates/teamtalk/tests/secret_string_tests.rs adds 12
integration cases covering:
* expose_secret round-trip, default / new / is_empty / len.
* zeroize_in_place empties the buffer.
* Debug and Display do not contain the plaintext and do mention
'redacted'.
* Content-based equality and inequality including
different-length secrets.
* From<String>, From<&str>, From<Cow<'_, str>>, From<&String>.
* Clone preserves the value.
* LoginParams.password is a SecretString and is accessed via
expose_secret().
* LoginParams Debug does not leak the password but keeps nickname
and username visible.
* LoginParams::new() still accepts owned String, &str, and a
pre-built SecretString for the password argument.
* Empty secret still renders redacted in Debug.
Local verification:
* cargo fmt --all clean.
* cargo clippy --workspace --all-targets --all-features -- -D
warnings clean.
* cargo test --workspace --all-features: all existing tests pass
plus 12 new SecretString cases.
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
This was referenced Apr 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Passwords passed through
LoginParamsused to live in a plainString.Stringdoes not zeroise its buffer on drop, so compromised heap dumps or post-free memory reads could surface the cleartext password long after login finished.LoginParamsalso derivedDebug, so ad-hocprintln!/tracingof it leaked the full credential.This PR introduces a minimal, crate-local zeroising secret wrapper and adopts it on
LoginParams.New API (
teamtalk::types):SecretString— UTF-8 secret with:#[derive(Clone, ZeroizeOnDrop)]so the backing buffer is wiped when the value (or a clone) goes out of scope.Debug/Displaythat render as<redacted>so the secret never hits logs.PartialEq/Eqover equal-length byte strings (lengths still short-circuit).expose_secret() -> &stras the single read-out method; doc block explicitly warns to keep the borrow narrow.zeroize_in_place()to wipe the buffer while keeping capacity.From<String>,From<&str>,From<&String>,From<Cow<'_, str>>so callers do not need to think about the wrapper unless they want to.teamtalk::types::SecretString(and viateamtalk::types::secret::SecretStringfor the focused path).LoginParamsmigration:LoginParams.password: String→SecretString.LoginParams::new(.., password: impl Into<SecretString>, ..)so callers keep passing&str/Stringunchanged.LoginParams::from_env()continues to readTT_PASSinto aSecretString.Debugderive onLoginParamsnow auto-redacts the password;nicknameandusernameremain visible.params.password.expose_secret()at the FFI boundary only (client/users/auth.rs::login_with_params,login_from_env,client/core/recovery.rsauto-login path).Dependency: adds
zeroize = { version = "1.8", features = ["zeroize_derive"] }toteamtalk's Cargo.toml. Thesecrecycrate was intentionally not pulled in to keep the dependency surface small; the local wrapper has no external users.Breaking change note:
LoginParams.password: String→SecretString.LoginParamsis already#[non_exhaustive], andSecretStringimplementsFrom<String>/From<&str>, so constructingLoginParamsvianew()continues to work with the same argument types as before. Direct field reads now need.expose_secret()to recover&str.Tests:
crates/teamtalk/tests/secret_string_tests.rsadds 12 integration cases —expose_secretround-trip,default/new/is_empty/len,zeroize_in_place,Debug/Displayredaction, content-based equality across different-length inputs,From<{String, &str, &String, Cow}>,Clone,LoginParams.passwordis aSecretString,LoginParamsDebugdoes not leak the password but keeps nickname/username visible,LoginParams::new()accepts ownedString/&str/pre-builtSecretString, empty secret still renders<redacted>.Local verification:
cargo fmt --all --checkclean;cargo clippy --workspace --all-targets --all-features -- -D warningsclean;cargo test --workspace --all-features— all existing tests pass plus 12 newSecretStringcases.Review & Testing Checklist for Human
LoginParams.passwordonly —UserAccount.password(intypes/entities/accounts.rs) is still a plainString. It is a stored admin data model rather than an in-flight client credential, so the trade-off is different; a follow-up PR can liftSecretStringthere too if desired.SecretStringinstead of adopting thesecrecycrate. Rationale: zero new transitive deps, noExposeSecrettrait surface to document, andsecrecy0.10 is still in the Rust ecosystem churn.LoginParams.passwordthat now go throughexpose_secret()keep the borrow narrow (client/users/auth.rsandclient/core/recovery.rs). The testlogin_params_debug_does_not_leak_passwordpins theDebugbehaviour; please still skimauth.rs/recovery.rsand confirm you do not want additional redaction hooks (e.g. intracingevents).Notes
Fifth P1 API improvement after
ExponentialBackoffjitter (#51),StreamTypes(#52),SdkErrorCode(#54), andTimeoutKind(#55). Branches from cleanmain, independent of the other open PRs, so can be merged in any order.Next up: indexed
EventBus::dispatch/Router::dispatchviaHashMap(keyed onEventdiscriminant for O(subscribers-on-event) dispatch), then a coverage-gap test fill-in driven byscripts/audit_teamtalk_coverage.py.The pre-existing
semverCI gate is expected to remain red;release-plzhandles the eventual version bump.Link to Devin session: https://app.devin.ai/sessions/71fdd6196cb74723a2e277bb81993a9c
Requested by: @BlindMaster24