diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 3d2844f..5fe5477 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -33,6 +33,7 @@ jobs: run: | cargo update -p openssl --precise "0.10.78" cargo update -p openssl-sys --precise "0.9.114" + cargo update -p zeroize --precise "1.8.2" - name: Test run: cargo test --verbose --all-features @@ -72,6 +73,7 @@ jobs: run: | cargo update -p openssl --precise "0.10.78" cargo update -p openssl-sys --precise "0.9.114" + cargo update -p zeroize --precise "1.8.2" - name: Check features run: cargo check --verbose ${{ matrix.features }} diff --git a/src/raw_client.rs b/src/raw_client.rs index 0a7a3ff..70bee28 100644 --- a/src/raw_client.rs +++ b/src/raw_client.rs @@ -88,6 +88,45 @@ macro_rules! impl_batch_call { }}; } +const REDACTED_AUTHORIZATION: &str = ""; +const REDACTION_FAILED: &[u8] = b""; + +/// Redacts authorization values from serialized, newline-delimited JSON-RPC requests. +/// +/// The input is produced by serializing [`Request`] values, so malformed JSON is not expected. +/// Nevertheless, redaction fails closed to ensure a request containing credentials is never +/// written to the log if its format changes unexpectedly. +fn redact_authorization(raw: &[u8]) -> Vec { + let mut redacted = Vec::with_capacity(raw.len()); + + for (index, raw_request) in raw.split(|byte| *byte == b'\n').enumerate() { + if index > 0 { + redacted.push(b'\n'); + } + if raw_request.is_empty() { + continue; + } + + let mut request: serde_json::Value = match serde_json::from_slice(raw_request) { + Ok(request) => request, + Err(_) => return REDACTION_FAILED.to_vec(), + }; + + match request.get_mut("authorization") { + Some(authorization) => { + *authorization = serde_json::Value::String(REDACTED_AUTHORIZATION.to_owned()); + match serde_json::to_vec(&request) { + Ok(request) => redacted.extend(request), + Err(_) => return REDACTION_FAILED.to_vec(), + } + } + None => redacted.extend(raw_request), + } + } + + redacted +} + /// A trait for [`ToSocketAddrs`](https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html) that /// can also be turned into a domain. Used when an SSL client needs to validate the server's /// certificate. @@ -795,7 +834,10 @@ impl RawClient { let req = req.with_auth(authorization); let mut raw = serde_json::to_vec(&req)?; - trace!("==> {}", String::from_utf8_lossy(&raw)); + trace!( + "==> {}", + String::from_utf8_lossy(&redact_authorization(&raw)) + ); raw.extend_from_slice(b"\n"); let mut stream = self.stream.lock()?; @@ -951,7 +993,10 @@ impl ElectrumApi for RawClient { return Ok(vec![]); } - trace!("==> {}", String::from_utf8_lossy(&raw)); + trace!( + "==> {}", + String::from_utf8_lossy(&redact_authorization(&raw)) + ); let mut stream = self.stream.lock()?; stream.write_all(&raw)?; @@ -1411,7 +1456,7 @@ mod test { use crate::utils; - use super::{ElectrumSslStream, RawClient}; + use super::{redact_authorization, ElectrumSslStream, RawClient, REDACTION_FAILED}; use crate::api::ElectrumApi; use crate::config::AuthProvider; @@ -1439,6 +1484,46 @@ mod test { .expect("should build the `RawClient` successfully!") } + #[test] + fn test_redact_authorization() { + let raw = br#"{"jsonrpc":"2.0","id":1,"method":"server.version","params":[],"authorization":"Bearer secret-token"}"#; + let redacted = redact_authorization(raw); + let request: serde_json::Value = serde_json::from_slice(&redacted).unwrap(); + + assert_eq!(request["authorization"], ""); + assert!(!String::from_utf8_lossy(&redacted).contains("secret-token")); + } + + #[test] + fn test_redact_authorization_preserves_unauthenticated_request() { + let raw = br#"{"jsonrpc":"2.0","id":1,"method":"server.version","params":[]}"#; + + assert_eq!(redact_authorization(raw), raw); + } + + #[test] + fn test_redact_authorization_from_batch() { + let raw = b"{\"id\":1,\"authorization\":\"Bearer batch-secret\"}\n\ + {\"id\":2}\n"; + let redacted = redact_authorization(raw); + let requests: Vec = redacted + .split(|byte| *byte == b'\n') + .filter(|request| !request.is_empty()) + .map(|request| serde_json::from_slice(request).unwrap()) + .collect(); + + assert_eq!(requests[0]["authorization"], ""); + assert!(requests[1].get("authorization").is_none()); + assert!(!String::from_utf8_lossy(&redacted).contains("batch-secret")); + } + + #[test] + fn test_redact_authorization_fails_closed() { + let raw = b"{\"authorization\":\"Bearer secret-token\""; + + assert_eq!(redact_authorization(raw), REDACTION_FAILED); + } + #[test] fn test_server_features_simple() { let client = get_test_client();