max / synckit
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+31 insertions,
-2 deletions
| @@ -332,6 +332,15 @@ | |||
| 332 | 332 | pub fn as_str(&self) -> &str { | |
| 333 | 333 | &self.0 | |
| 334 | 334 | } | |
| 335 | + | ||
| 336 | + | /// Scrub the token bytes in place, leaving an empty string behind. | |
| 337 | + | /// | |
| 338 | + | /// [`Drop`] delegates here rather than zeroizing inline so the guarantee is | |
| 339 | + | /// reachable from a test. Nothing else calls it. | |
| 340 | + | fn zeroize_in_place(&mut self) { | |
| 341 | + | use zeroize::Zeroize; | |
| 342 | + | self.0.zeroize(); | |
| 343 | + | } | |
| 335 | 344 | } | |
| 336 | 345 | ||
| 337 | 346 | impl std::ops::Deref for SecretToken { | |
| @@ -355,8 +364,7 @@ | |||
| 355 | 364 | ||
| 356 | 365 | impl Drop for SecretToken { | |
| 357 | 366 | fn drop(&mut self) { | |
| 358 | - | use zeroize::Zeroize; | |
| 359 | - | self.0.zeroize(); | |
| 367 | + | self.zeroize_in_place(); | |
| 360 | 368 | } | |
| 361 | 369 | } | |
| 362 | 370 | ||
| @@ -723,6 +731,27 @@ | |||
| 723 | 731 | assert!(version.starts_with(char::is_numeric)); | |
| 724 | 732 | } | |
| 725 | 733 | ||
| 734 | + | #[test] | |
| 735 | + | fn secret_token_zeroize_scrubs_the_buffer() { | |
| 736 | + | // The type exists to keep a bearer token out of memory after use, and | |
| 737 | + | // nothing else in the suite exercises that. Drop delegates to | |
| 738 | + | // zeroize_in_place, so calling it directly checks the same code path. | |
| 739 | + | let mut token = SecretToken::new("super-secret-jwt-value".to_string()); | |
| 740 | + | let len = token.as_str().len(); | |
| 741 | + | assert!(len > 0); | |
| 742 | + | ||
| 743 | + | token.zeroize_in_place(); | |
| 744 | + | ||
| 745 | + | assert_eq!(token.as_str(), ""); | |
| 746 | + | // zeroize keeps the String's allocation, so the bytes the token occupied | |
| 747 | + | // are still readable and must all be zero. | |
| 748 | + | let bytes = unsafe { std::slice::from_raw_parts(token.0.as_ptr(), len) }; | |
| 749 | + | assert!( | |
| 750 | + | bytes.iter().all(|b| *b == 0), | |
| 751 | + | "token bytes survived the zeroize: {bytes:?}" | |
| 752 | + | ); | |
| 753 | + | } | |
| 754 | + | ||
| 726 | 755 | #[test] | |
| 727 | 756 | fn requires_https_enforces_tls_off_loopback() { | |
| 728 | 757 | // https or non-loopback http must enforce TLS. |