From 38587404886e2bba9825a125378fb9b54fe4d7e7 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 5 Jun 2026 01:53:05 +0200 Subject: [PATCH] refactor: drop speculative prose markers, keep only backtick extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per operator review on the PR: the prose marker list ('new password is:', ' to:', 'changed to:', etc.) was speculative — built from a misread screenshot, not a real reply. The conduit admin bot always code-spans the password, so the backtick anchor is the verified, complete format. Removing the markers leaves a ~15-line function that's honest about what it parses. If a non-code-span format ever appears, extract_new_password returns None and the diagnostic logging in admin_room_send_and_poll records the raw body, so a real format change is visible — far better than a speculative marker silently mis-parsing it. Tests now cover the live format, symbol passwords, the code-spanned-user-id error guard, and the no-codespan / non-password cases. --- hive-c0re/src/matrix.rs | 235 ++++++++++------------------------------ 1 file changed, 59 insertions(+), 176 deletions(-) diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 7e1247fa..e041eb28 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -350,189 +350,50 @@ async fn discover_admin_room_id( }) } -/// Try to parse the new password from an admin-room bot response. -/// Conduwuit/tuwunel responds with a message like: -/// "Done: Password of user @user:server has been reset. The new password is: " +/// Extract the new password from a conduit/tuwunel admin-room reset reply. /// -/// Handles several format variants emitted by different tuwunel / conduwuit -/// versions — "new password is:", "password is:", "changed to:", etc. +/// The admin bot always renders the new password as a backtick code span. +/// The live reply observed in the `#admins` room is: +/// "Successfully reset the password for user @x:server: ``" +/// The surrounding prose varies between builds (the delimiter is `: ` after +/// the user id, not `" to:"`), so we anchor on the code span rather than +/// parsing the prose. Returns the content of the first backtick pair when the +/// message is a password-reset success. /// -/// Uses [`str::to_ascii_lowercase`] for case folding — unlike `to_lowercase`, -/// ASCII lowercasing is guaranteed to produce a same-byte-length string, so the -/// byte offset from `find` is always a valid index into the original `bot_message` -/// and we never slice at a non-char boundary. +/// Guard: an error reply can also code-span the *user id* ("@x:server"); a +/// real password has no whitespace and isn't a `@localpart:server` id, so we +/// reject that shape and return `None`. On `None` the caller surfaces the +/// timeout and `admin_room_send_and_poll` logs the unparsed body — so a +/// future format change is visible rather than silently mis-parsed. fn extract_new_password(bot_message: &str) -> Option { - // ASCII lowercase: same byte length as the original, so positions from - // `lower.find(marker)` are valid byte indices into `bot_message`. - let lower = bot_message.to_ascii_lowercase(); - - // Primary strategy: the conduit/tuwunel admin bot always renders the new - // password as a backtick code span. The live reply observed in the admin - // room is: - // "Successfully reset the password for user @x:server: ``" - // (note the delimiter is ": " after the user id, NOT " to:" — the prose - // wording varies between builds, so anchoring on the code span is the - // robust extraction). Take the content of the first backtick pair when the - // message is a password-reset success. Guard against grabbing a code-spanned - // matrix user id ("@x:server") from an error message: a real password has - // no whitespace and isn't a `@localpart:server` id. - if lower.contains("password") - && let Some(open) = bot_message.find('`') + // Only consider password-reset success replies. + if !bot_message.to_ascii_lowercase().contains("password") { + return None; + } + // Content of the first backtick code span. + let open = bot_message.find('`')?; + let after = &bot_message[open + 1..]; + let close = after.find('`')?; + let pw = &after[..close]; + // Reject a code-spanned matrix user id from an error reply, and any + // multi-token span — generated passwords are a single whitespace-free run. + if pw.is_empty() + || pw.contains(char::is_whitespace) + || (pw.starts_with('@') && pw.contains(':')) { - let after = &bot_message[open + 1..]; - if let Some(close) = after.find('`') { - let pw = &after[..close]; - if !pw.is_empty() - && !pw.contains(char::is_whitespace) - && !(pw.starts_with('@') && pw.contains(':')) - { - return Some(pw.to_owned()); - } - } + return None; } - - for marker in &[ - // Explicit "is:" variants (most common in conduwuit / tuwunel): - "new password is: ", - "new password is:", - "password is: ", - "password is:", - // "changed to:" / "reset to:" / "set to:" variants: - "changed to: ", - "changed to:", - "reset to: ", - "reset to:", - "set to: ", - "set to:", - // Generic "… to: " form. tuwunel's actual reset-password reply is - // "Successfully reset password for @user:server to: " — the - // password follows " to: " but no recognised verb sits adjacent to it, - // so the markers above miss it. Matrix user ids / server names can't - // contain " to: ", so this only ever anchors on the prose delimiter. - // Placed after the specific verb markers and before the bare - // "password:" last resort. - " to: ", - // Bare "new password:" without "is": - "new password: ", - "new password:", - // Bare "password:" as last resort (must come after more specific markers): - "password: ", - "password:", - ] { - if let Some(pos) = lower.find(marker) { - let rest = &bot_message[pos + marker.len()..]; - // Strip leading whitespace, then a leading code-span backtick: - // tuwunel renders the password as a code span, so the plain - // `body` carries literal backticks ("… to: ``"). - let rest = rest.trim_start().trim_start_matches('`'); - // The password ends at the first whitespace OR the closing - // backtick. Generated passwords contain neither, so this never - // truncates a real password mid-token. - let end = rest - .find(|c: char| c.is_whitespace() || c == '`') - .unwrap_or(rest.len()); - let pw = rest[..end].trim(); - if !pw.is_empty() { - return Some(pw.to_owned()); - } - } - } - None + Some(pw.to_owned()) } #[cfg(test)] mod extract_new_password_tests { use super::extract_new_password; - #[test] - fn tuwunel_style_response() { - let msg = "Done: Password of user @atlas:pr1ma.darkest.space has been reset. The new password is: abc123XYZ!"; - assert_eq!(extract_new_password(msg).as_deref(), Some("abc123XYZ!")); - } - - #[test] - fn case_insensitive_marker() { - let msg = "Password reset complete. New Password Is: S3cr3tP@ss"; - assert_eq!(extract_new_password(msg).as_deref(), Some("S3cr3tP@ss")); - } - - #[test] - fn marker_without_trailing_space() { - let msg = "new password is:hunter2"; - assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2")); - } - - #[test] - fn shorter_marker_variant() { - let msg = "Your password is: Tr0ub4dor&3"; - assert_eq!(extract_new_password(msg).as_deref(), Some("Tr0ub4dor&3")); - } - - #[test] - fn no_match_returns_none() { - let msg = "Command not recognised. Please try again."; - assert_eq!(extract_new_password(msg), None); - } - - #[test] - fn empty_after_marker_returns_none() { - let msg = "new password is: "; - assert_eq!(extract_new_password(msg), None); - } - - #[test] - fn password_stops_at_whitespace() { - let msg = "New password is: abc123 (save it now)"; - assert_eq!(extract_new_password(msg).as_deref(), Some("abc123")); - } - - #[test] - fn changed_to_variant() { - let msg = "Password of user @atlas:pr1ma.darkest.space has been changed to: Xyz987!"; - assert_eq!(extract_new_password(msg).as_deref(), Some("Xyz987!")); - } - - #[test] - fn reset_to_variant() { - let msg = "Password for user @foo:bar has been reset to: hunter2"; - assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2")); - } - - #[test] - fn tuwunel_reset_password_for_to_variant() { - // The actual tuwunel admin-room reply observed in production — the - // verb ("reset") is not adjacent to "to:", so only the generic - // " to: " marker catches it. - let msg = "Successfully reset password for @atlas:pr1ma.darkest.space to: N3wP@ssw0rd"; - assert_eq!(extract_new_password(msg).as_deref(), Some("N3wP@ssw0rd")); - } - - #[test] - fn to_marker_not_confused_by_user_id() { - // The user id contains no " to: " so the marker only fires on the - // real delimiter; the password is the token right after it. - let msg = "Successfully reset password for @sock:pr1ma.darkest.space to: abc123XYZ"; - assert_eq!(extract_new_password(msg).as_deref(), Some("abc123XYZ")); - } - - #[test] - fn backtick_wrapped_password() { - // tuwunel renders the password as a code span; the plain body - // carries literal backticks. Strip them, don't capture them. - let msg = "Successfully reset password for @atlas:pr1ma.darkest.space to: `N3wP@ssw0rd`"; - assert_eq!(extract_new_password(msg).as_deref(), Some("N3wP@ssw0rd")); - } - - #[test] - fn backtick_wrapped_with_trailing_text() { - let msg = "Done. New password is: `hunter2` (store it now)"; - assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2")); - } - #[test] fn conduit_live_admin_room_format() { - // The ACTUAL reply observed in the live #admins room — the delimiter - // is ": " after the user id (no " to:"), password in a code span. + // The exact reply observed in the live #admins room: ": " after the + // user id, password in a backtick code span. let msg = "Successfully reset the password for user @triage:pr1ma.darkest.space: `hVfa6TpvIKnADoEJNWn9saHoI`"; assert_eq!( extract_new_password(msg).as_deref(), @@ -540,6 +401,20 @@ mod extract_new_password_tests { ); } + #[test] + fn backtick_span_anywhere_in_prose() { + // Wording around the code span is irrelevant — we anchor on the span. + let msg = "Done. New password is: `hunter2` (store it now)"; + assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2")); + } + + #[test] + fn password_with_symbols_inside_span() { + // '@' mid-token is fine — only a leading "@…:…" user-id shape is rejected. + let msg = "Successfully reset the password for user @atlas:pr1ma.darkest.space: `N3wP@ss-w0rd!`"; + assert_eq!(extract_new_password(msg).as_deref(), Some("N3wP@ss-w0rd!")); + } + #[test] fn codespan_userid_in_error_not_mistaken_for_password() { // An error that code-spans the user id must not yield it as a password. @@ -548,15 +423,23 @@ mod extract_new_password_tests { } #[test] - fn bare_new_password_colon() { - let msg = "New password: P@ssword1"; - assert_eq!(extract_new_password(msg).as_deref(), Some("P@ssword1")); + fn no_codespan_returns_none() { + // No backtick span → unparseable here; the caller logs the raw body + // so a genuinely new format surfaces instead of being mis-parsed. + let msg = "Password reset complete. New password is: abc123XYZ"; + assert_eq!(extract_new_password(msg), None); } #[test] - fn bare_password_colon_last_resort() { - let msg = "Your account password: S3cr3t"; - assert_eq!(extract_new_password(msg).as_deref(), Some("S3cr3t")); + fn non_password_message_returns_none() { + let msg = "Command not recognised. Please try again."; + assert_eq!(extract_new_password(msg), None); + } + + #[test] + fn empty_codespan_returns_none() { + let msg = "Successfully reset the password for user @x:server: ``"; + assert_eq!(extract_new_password(msg), None); } }