Skip to content

Commit 639b0a1

Browse files
johnsideserfclaude
andauthored
Redact PII in --debug logs, add --debug-full for unredacted (#170)
--debug now masks phone numbers ("+1***...567") and message bodies ("[msg: 42 chars]") in log output. --debug-full preserves the old behavior with full unredacted output. - Add REDACT flag, mask_phone(), mask_body() helpers to debug_log.rs - Add redacted_summary() on SignalEvent for structured event logging - Mask phone numbers in send/edit/receipt/read_sync log calls - Add --debug-full CLI flag Closes #163 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 763ad70 commit 639b0a1

File tree

5 files changed

+151
-12
lines changed

5 files changed

+151
-12
lines changed

src/app.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4311,7 +4311,8 @@ impl App {
43114311
}
43124312
if !found {
43134313
crate::debug_log::logf(format_args!(
4314-
"read_sync: no conversation found for sender={sender} ts={timestamp}"
4314+
"read_sync: no conversation found for sender={} ts={timestamp}",
4315+
crate::debug_log::mask_phone(sender)
43154316
));
43164317
}
43174318
}
@@ -4714,7 +4715,8 @@ impl App {
47144715
fn handle_send_timestamp(&mut self, rpc_id: &str, server_ts: i64) {
47154716
if let Some((conv_id, local_ts)) = self.pending_sends.remove(rpc_id) {
47164717
crate::debug_log::logf(format_args!(
4717-
"send confirmed: conv={conv_id} local_ts={local_ts} server_ts={server_ts}"
4718+
"send confirmed: conv={} local_ts={local_ts} server_ts={server_ts}",
4719+
crate::debug_log::mask_phone(&conv_id)
47184720
));
47194721
let effective_ts = if server_ts != 0 { server_ts } else { local_ts };
47204722
let mut found = false;
@@ -4827,7 +4829,8 @@ impl App {
48274829
// that assigns the server timestamp. Buffer it for replay.
48284830
if !matched_any && !timestamps.is_empty() {
48294831
crate::debug_log::logf(format_args!(
4830-
"receipt: buffering {receipt_type} from {sender} (no matching ts)"
4832+
"receipt: buffering {receipt_type} from {} (no matching ts)",
4833+
crate::debug_log::mask_phone(sender)
48314834
));
48324835
self.pending_receipts.push((
48334836
sender.to_string(),
@@ -4836,7 +4839,8 @@ impl App {
48364839
));
48374840
} else if matched_any {
48384841
crate::debug_log::logf(format_args!(
4839-
"receipt: {receipt_type} from {sender} -> {new_status:?}"
4842+
"receipt: {receipt_type} from {} -> {new_status:?}",
4843+
crate::debug_log::mask_phone(sender)
48404844
));
48414845
}
48424846
}

src/debug_log.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
//! Optional debug logger — writes to ~/.cache/siggy/debug.log when --debug is passed.
22
//! Rotates log file when it exceeds MAX_LOG_SIZE bytes.
3+
//!
4+
//! `--debug` enables logging with PII redaction (phone numbers masked, message bodies omitted).
5+
//! `--debug-full` enables logging with full unredacted output.
36
47
use std::fs::{File, OpenOptions};
58
use std::io::Write;
69
use std::sync::atomic::{AtomicBool, Ordering};
710
use std::sync::Mutex;
811

912
static ENABLED: AtomicBool = AtomicBool::new(false);
13+
static REDACT: AtomicBool = AtomicBool::new(true);
1014
static FILE: Mutex<Option<File>> = Mutex::new(None);
1115
static PATH: Mutex<Option<std::path::PathBuf>> = Mutex::new(None);
1216

@@ -19,8 +23,7 @@ fn log_path() -> std::path::PathBuf {
1923
.join("debug.log")
2024
}
2125

22-
pub fn enable() {
23-
ENABLED.store(true, Ordering::Relaxed);
26+
fn setup_file() {
2427
let path = log_path();
2528
if let Some(parent) = path.parent() {
2629
let _ = std::fs::create_dir_all(parent);
@@ -59,6 +62,49 @@ pub fn enable() {
5962
eprintln!("Debug logging enabled: {}", path.display());
6063
}
6164

65+
/// Enable debug logging with PII redaction (--debug).
66+
pub fn enable() {
67+
ENABLED.store(true, Ordering::Relaxed);
68+
REDACT.store(true, Ordering::Relaxed);
69+
setup_file();
70+
}
71+
72+
/// Enable debug logging without PII redaction (--debug-full).
73+
pub fn enable_full() {
74+
ENABLED.store(true, Ordering::Relaxed);
75+
REDACT.store(false, Ordering::Relaxed);
76+
setup_file();
77+
}
78+
79+
/// Whether PII should be redacted in log output.
80+
pub fn redact() -> bool {
81+
REDACT.load(Ordering::Relaxed)
82+
}
83+
84+
/// Mask a phone number for redacted logging: "+15551234567" → "+1***...567"
85+
pub fn mask_phone(phone: &str) -> String {
86+
if !redact() {
87+
return phone.to_string();
88+
}
89+
if phone.starts_with('+') && phone.len() > 5 {
90+
let suffix = &phone[phone.len() - 3..];
91+
format!("+{}***...{}", &phone[1..2], suffix)
92+
} else if phone.len() > 8 {
93+
// Group IDs or other long identifiers
94+
format!("{}...{}", &phone[..4], &phone[phone.len() - 4..])
95+
} else {
96+
"[redacted]".to_string()
97+
}
98+
}
99+
100+
/// Summarize a message body for redacted logging: "hello world" → "[msg: 11 chars]"
101+
pub fn mask_body(body: &str) -> String {
102+
if !redact() {
103+
return body.to_string();
104+
}
105+
format!("[msg: {} chars]", body.len())
106+
}
107+
62108
pub fn log(msg: &str) {
63109
if !ENABLED.load(Ordering::Relaxed) {
64110
return;

src/main.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ async fn main() -> Result<()> {
7979
let mut demo_mode = false;
8080
let mut incognito = false;
8181
let mut debug = false;
82+
let mut debug_full = false;
8283

8384
let mut i = 1;
8485
while i < args.len() {
@@ -117,6 +118,10 @@ async fn main() -> Result<()> {
117118
debug = true;
118119
i += 1;
119120
}
121+
"--debug-full" => {
122+
debug_full = true;
123+
i += 1;
124+
}
120125
"--help" => {
121126
eprintln!("siggy - Terminal Signal client");
122127
eprintln!();
@@ -128,7 +133,8 @@ async fn main() -> Result<()> {
128133
eprintln!(" --setup Run first-time setup wizard");
129134
eprintln!(" --demo Launch with dummy data (no signal-cli needed)");
130135
eprintln!(" --incognito No local message storage (in-memory only)");
131-
eprintln!(" --debug Write debug log to siggy-debug.log");
136+
eprintln!(" --debug Write debug log (PII redacted)");
137+
eprintln!(" --debug-full Write debug log (full, unredacted)");
132138
eprintln!(" --help Show this help");
133139
std::process::exit(0);
134140
}
@@ -139,9 +145,12 @@ async fn main() -> Result<()> {
139145
}
140146
}
141147

142-
if debug {
148+
if debug_full {
149+
debug_log::enable_full();
150+
debug_log::log("=== siggy debug session started (full/unredacted) ===");
151+
} else if debug {
143152
debug_log::enable();
144-
debug_log::log("=== siggy debug session started ===");
153+
debug_log::log("=== siggy debug session started (PII redacted) ===");
145154
}
146155

147156
// Load config
@@ -530,7 +539,7 @@ async fn dispatch_send(
530539
let att_refs: Vec<&std::path::Path> = attachments.iter().map(|p| p.as_path()).collect();
531540
match signal_client.send_message(&recipient, &body, is_group, &mentions, &att_refs, quote.as_ref().map(|(a, t, b)| (a.as_str(), *t, b.as_str()))).await {
532541
Ok(rpc_id) => {
533-
debug_log::logf(format_args!("send: to={recipient} ts={local_ts_ms}"));
542+
debug_log::logf(format_args!("send: to={} ts={local_ts_ms}", debug_log::mask_phone(&recipient)));
534543
app.pending_sends
535544
.insert(rpc_id, (recipient.to_string(), local_ts_ms));
536545
}
@@ -556,7 +565,7 @@ async fn dispatch_send(
556565
};
557566
match signal_client.send_edit_message(&recipient, &body, is_group, edit_timestamp, &mentions, quote.as_ref().map(|(a, t, b)| (a.as_str(), *t, b.as_str()))).await {
558567
Ok(rpc_id) => {
559-
debug_log::logf(format_args!("edit: to={recipient} ts={edit_timestamp}"));
568+
debug_log::logf(format_args!("edit: to={} ts={edit_timestamp}", debug_log::mask_phone(&recipient)));
560569
app.pending_sends.insert(rpc_id, (recipient.to_string(), local_ts_ms));
561570
}
562571
Err(e) => {

src/signal/client.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ impl SignalClient {
100100
};
101101

102102
if let Some(ref event) = event {
103-
crate::debug_log::logf(format_args!("event: {event:?}"));
103+
if crate::debug_log::redact() {
104+
crate::debug_log::logf(format_args!("event: {}", event.redacted_summary()));
105+
} else {
106+
crate::debug_log::logf(format_args!("event: {event:?}"));
107+
}
104108
}
105109

106110
if let Some(event) = event {

src/signal/types.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,82 @@ pub enum SignalEvent {
207207
Error(String),
208208
}
209209

210+
impl SignalEvent {
211+
/// Format this event for debug logging with PII redacted.
212+
pub fn redacted_summary(&self) -> String {
213+
use crate::debug_log::{mask_phone, mask_body};
214+
match self {
215+
Self::MessageReceived(msg) => format!(
216+
"MessageReceived(from={}, body={}, attachments={}, group={})",
217+
mask_phone(&msg.source),
218+
msg.body.as_deref().map_or("[none]".to_string(), mask_body),
219+
msg.attachments.len(),
220+
msg.group_id.is_some(),
221+
),
222+
Self::ReceiptReceived { sender, receipt_type, timestamps } => format!(
223+
"ReceiptReceived({receipt_type} from={}, count={})",
224+
mask_phone(sender), timestamps.len(),
225+
),
226+
Self::SendTimestamp { rpc_id, server_ts } => format!(
227+
"SendTimestamp(rpc={rpc_id}, ts={server_ts})",
228+
),
229+
Self::SendFailed { rpc_id } => format!("SendFailed(rpc={rpc_id})"),
230+
Self::TypingIndicator { sender, is_typing, .. } => format!(
231+
"TypingIndicator(from={}, typing={is_typing})",
232+
mask_phone(sender),
233+
),
234+
Self::ReactionReceived { conv_id, emoji, sender, target_timestamp, is_remove, .. } => format!(
235+
"ReactionReceived(conv={}, from={}, emoji={emoji}, target_ts={target_timestamp}, remove={is_remove})",
236+
mask_phone(conv_id), mask_phone(sender),
237+
),
238+
Self::EditReceived { conv_id, target_timestamp, new_body, .. } => format!(
239+
"EditReceived(conv={}, target_ts={target_timestamp}, body={})",
240+
mask_phone(conv_id), mask_body(new_body),
241+
),
242+
Self::RemoteDeleteReceived { conv_id, target_timestamp, .. } => format!(
243+
"RemoteDeleteReceived(conv={}, target_ts={target_timestamp})",
244+
mask_phone(conv_id),
245+
),
246+
Self::PinReceived { conv_id, target_timestamp, .. } => format!(
247+
"PinReceived(conv={}, target_ts={target_timestamp})",
248+
mask_phone(conv_id),
249+
),
250+
Self::UnpinReceived { conv_id, target_timestamp, .. } => format!(
251+
"UnpinReceived(conv={}, target_ts={target_timestamp})",
252+
mask_phone(conv_id),
253+
),
254+
Self::PollCreated { conv_id, timestamp, .. } => format!(
255+
"PollCreated(conv={}, ts={timestamp})",
256+
mask_phone(conv_id),
257+
),
258+
Self::PollVoteReceived { conv_id, target_timestamp, voter, .. } => format!(
259+
"PollVoteReceived(conv={}, target_ts={target_timestamp}, voter={})",
260+
mask_phone(conv_id), mask_phone(voter),
261+
),
262+
Self::PollTerminated { conv_id, target_timestamp } => format!(
263+
"PollTerminated(conv={}, target_ts={target_timestamp})",
264+
mask_phone(conv_id),
265+
),
266+
Self::SystemMessage { conv_id, body, .. } => format!(
267+
"SystemMessage(conv={}, body={})",
268+
mask_phone(conv_id), mask_body(body),
269+
),
270+
Self::ExpirationTimerChanged { conv_id, seconds, .. } => format!(
271+
"ExpirationTimerChanged(conv={}, seconds={seconds})",
272+
mask_phone(conv_id),
273+
),
274+
Self::ReadSyncReceived { read_messages } => format!(
275+
"ReadSyncReceived(count={})",
276+
read_messages.len(),
277+
),
278+
Self::ContactList(contacts) => format!("ContactList(count={})", contacts.len()),
279+
Self::GroupList(groups) => format!("GroupList(count={})", groups.len()),
280+
Self::IdentityList(ids) => format!("IdentityList(count={})", ids.len()),
281+
Self::Error(e) => format!("Error({e})"),
282+
}
283+
}
284+
}
285+
210286
/// A message from Signal
211287
#[derive(Debug, Clone)]
212288
pub struct SignalMessage {

0 commit comments

Comments
 (0)