#![allow(missing_docs)]
use alloc::string::String;
use alloc::vec::Vec;
use purecrypto::zeroize::Zeroize;
use crate::error::{Error, Result};
use crate::format::{Reader, Writer};
pub const SSH_MSG_SERVICE_REQUEST: u8 = 5;
pub const SSH_MSG_SERVICE_ACCEPT: u8 = 6;
pub const SSH_MSG_USERAUTH_REQUEST: u8 = 50;
pub const SSH_MSG_USERAUTH_FAILURE: u8 = 51;
pub const SSH_MSG_USERAUTH_SUCCESS: u8 = 52;
pub const SSH_MSG_USERAUTH_BANNER: u8 = 53;
pub const SSH_MSG_USERAUTH_PK_OK: u8 = 60;
pub const SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: u8 = 60;
pub const SSH_MSG_USERAUTH_INFO_REQUEST: u8 = 60;
pub const SSH_MSG_USERAUTH_INFO_RESPONSE: u8 = 61;
const MAX_USERAUTH_STRING: usize = crate::transport::MAX_PACKET_LEN as usize;
fn read_utf8(r: &mut Reader<'_>) -> Result<String> {
let bytes = read_bytes_capped(r)?;
core::str::from_utf8(bytes)
.map(|s| s.into())
.map_err(|_| Error::Format("auth: invalid utf-8"))
}
fn read_bytes_capped<'a>(r: &mut Reader<'a>) -> Result<&'a [u8]> {
let len = r.read_u32()? as usize;
if len > MAX_USERAUTH_STRING {
return Err(Error::Format("userauth string too large"));
}
r.take(len)
}
fn ensure_empty(r: &Reader<'_>) -> Result<()> {
if !r.is_empty() {
return Err(Error::Format("auth: trailing data"));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceRequest {
pub service: String,
}
impl ServiceRequest {
pub fn encode(&self) -> Vec<u8> {
let mut w = Writer::with_capacity(5 + self.service.len());
w.write_u8(SSH_MSG_SERVICE_REQUEST);
w.write_string(self.service.as_bytes());
w.into_vec()
}
pub fn decode(payload: &[u8]) -> Result<Self> {
let mut r = Reader::new(payload);
if r.read_u8()? != SSH_MSG_SERVICE_REQUEST {
return Err(Error::Format("auth: not SERVICE_REQUEST"));
}
let service = read_utf8(&mut r)?;
ensure_empty(&r)?;
Ok(Self { service })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceAccept {
pub service: String,
}
impl ServiceAccept {
pub fn encode(&self) -> Vec<u8> {
let mut w = Writer::with_capacity(5 + self.service.len());
w.write_u8(SSH_MSG_SERVICE_ACCEPT);
w.write_string(self.service.as_bytes());
w.into_vec()
}
pub fn decode(payload: &[u8]) -> Result<Self> {
let mut r = Reader::new(payload);
if r.read_u8()? != SSH_MSG_SERVICE_ACCEPT {
return Err(Error::Format("auth: not SERVICE_ACCEPT"));
}
let service = read_utf8(&mut r)?;
ensure_empty(&r)?;
Ok(Self { service })
}
}
#[derive(Clone)]
pub struct SecretString(purecrypto::zeroize::Zeroizing<String>);
impl SecretString {
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
fn ct_bytes_eq(a: &[u8], b: &[u8]) -> bool {
let n = a.len().max(b.len());
let mut diff: u8 = 0;
for i in 0..n {
let x = a.get(i).copied().unwrap_or(0);
let y = b.get(i).copied().unwrap_or(0);
diff |= x ^ y;
}
let len_diff = (a.len() ^ b.len()) as u64;
let len_bit = ((len_diff | len_diff.wrapping_neg()) >> 63) as u8;
diff |= len_bit;
core::hint::black_box(diff) == 0
}
impl PartialEq for SecretString {
fn eq(&self, other: &Self) -> bool {
ct_bytes_eq(self.as_bytes(), other.as_bytes())
}
}
impl Eq for SecretString {}
impl From<String> for SecretString {
fn from(s: String) -> Self {
SecretString(purecrypto::zeroize::Zeroizing::new(s))
}
}
impl From<&str> for SecretString {
fn from(s: &str) -> Self {
SecretString(purecrypto::zeroize::Zeroizing::new(s.into()))
}
}
impl core::ops::Deref for SecretString {
type Target = str;
fn deref(&self) -> &str {
self.0.as_str()
}
}
impl PartialEq<&str> for SecretString {
fn eq(&self, other: &&str) -> bool {
ct_bytes_eq(self.as_bytes(), other.as_bytes())
}
}
impl PartialEq<str> for SecretString {
fn eq(&self, other: &str) -> bool {
ct_bytes_eq(self.as_bytes(), other.as_bytes())
}
}
impl core::fmt::Debug for SecretString {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("<redacted>")
}
}
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AuthMethodPayload {
None,
Password {
new_password: Option<SecretString>,
password: SecretString,
},
PublicKey {
signature_present: bool,
algorithm: String,
public_blob: Vec<u8>,
signature: Option<Vec<u8>>,
host_key: Option<Vec<u8>>,
},
KeyboardInteractive {
language_tag: String,
submethods: String,
},
Other {
method: String,
tail: Vec<u8>,
},
}
impl core::fmt::Debug for AuthMethodPayload {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AuthMethodPayload::None => f.write_str("None"),
AuthMethodPayload::Password {
new_password,
password: _,
} => f
.debug_struct("Password")
.field("new_password", &new_password.as_ref().map(|_| "<redacted>"))
.field("password", &"<redacted>")
.finish(),
AuthMethodPayload::PublicKey {
signature_present,
algorithm,
public_blob,
signature,
host_key: _,
} => f
.debug_struct("PublicKey")
.field("signature_present", signature_present)
.field("algorithm", algorithm)
.field("public_blob", public_blob)
.field("signature", signature)
.finish(),
AuthMethodPayload::KeyboardInteractive {
language_tag,
submethods,
} => f
.debug_struct("KeyboardInteractive")
.field("language_tag", language_tag)
.field("submethods", submethods)
.finish(),
AuthMethodPayload::Other { method, tail } => f
.debug_struct("Other")
.field("method", method)
.field("tail", tail)
.finish(),
}
}
}
impl AuthMethodPayload {
pub fn method_name(&self) -> &str {
match self {
AuthMethodPayload::None => "none",
AuthMethodPayload::Password { .. } => "password",
AuthMethodPayload::PublicKey { host_key: None, .. } => "publickey",
AuthMethodPayload::PublicKey { .. } => PUBLICKEY_HOSTBOUND,
AuthMethodPayload::KeyboardInteractive { .. } => "keyboard-interactive",
AuthMethodPayload::Other { method, .. } => method,
}
}
fn write_tail(&self, w: &mut Writer) {
match self {
AuthMethodPayload::None => {}
AuthMethodPayload::Password {
new_password,
password,
} => {
w.write_bool(new_password.is_some());
w.write_string(password.as_bytes());
if let Some(np) = new_password {
w.write_string(np.as_bytes());
}
}
AuthMethodPayload::PublicKey {
signature_present,
algorithm,
public_blob,
signature,
host_key,
} => {
w.write_bool(*signature_present);
w.write_string(algorithm.as_bytes());
w.write_string(public_blob);
if let Some(hk) = host_key {
w.write_string(hk);
}
if *signature_present {
if let Some(sig) = signature {
w.write_string(sig);
} else {
w.write_string(&[]);
}
}
}
AuthMethodPayload::KeyboardInteractive {
language_tag,
submethods,
} => {
w.write_string(language_tag.as_bytes());
w.write_string(submethods.as_bytes());
}
AuthMethodPayload::Other { tail, .. } => {
w.write_raw(tail);
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserauthRequest {
pub user: String,
pub service: String,
pub method: AuthMethodPayload,
}
impl UserauthRequest {
pub fn encode(&self) -> Vec<u8> {
let mut w = Writer::with_capacity(64);
w.write_u8(SSH_MSG_USERAUTH_REQUEST);
w.write_string(self.user.as_bytes());
w.write_string(self.service.as_bytes());
w.write_string(self.method.method_name().as_bytes());
self.method.write_tail(&mut w);
w.into_vec()
}
pub fn decode(payload: &[u8]) -> Result<Self> {
let mut r = Reader::new(payload);
if r.read_u8()? != SSH_MSG_USERAUTH_REQUEST {
return Err(Error::Format("auth: not USERAUTH_REQUEST"));
}
let user = read_utf8(&mut r)?;
let service = read_utf8(&mut r)?;
let method_name = read_utf8(&mut r)?;
let method = match method_name.as_str() {
"none" => {
ensure_empty(&r)?;
AuthMethodPayload::None
}
"password" => {
let change = r.read_bool()?;
let password: SecretString = read_utf8(&mut r)?.into();
let new_password: Option<SecretString> = if change {
Some(read_utf8(&mut r)?.into())
} else {
None
};
ensure_empty(&r)?;
AuthMethodPayload::Password {
new_password,
password,
}
}
"publickey" | PUBLICKEY_HOSTBOUND => {
let signature_present = r.read_bool()?;
let algorithm = read_utf8(&mut r)?;
let public_blob = read_bytes_capped(&mut r)?.to_vec();
let host_key = if method_name == PUBLICKEY_HOSTBOUND {
Some(read_bytes_capped(&mut r)?.to_vec())
} else {
None
};
let signature = if signature_present {
Some(read_bytes_capped(&mut r)?.to_vec())
} else {
None
};
ensure_empty(&r)?;
AuthMethodPayload::PublicKey {
signature_present,
algorithm,
public_blob,
signature,
host_key,
}
}
"keyboard-interactive" => {
let language_tag = read_utf8(&mut r)?;
let submethods = read_utf8(&mut r)?;
ensure_empty(&r)?;
AuthMethodPayload::KeyboardInteractive {
language_tag,
submethods,
}
}
_ => {
let tail = r.take(r.remaining())?.to_vec();
AuthMethodPayload::Other {
method: method_name,
tail,
}
}
};
Ok(Self {
user,
service,
method,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserauthFailure {
pub continuations: Vec<String>,
pub partial_success: bool,
}
impl UserauthFailure {
pub fn encode(&self) -> Vec<u8> {
let mut w = Writer::with_capacity(16);
w.write_u8(SSH_MSG_USERAUTH_FAILURE);
let mut joined = String::new();
for (i, name) in self.continuations.iter().enumerate() {
if i > 0 {
joined.push(',');
}
joined.push_str(name);
}
w.write_string(joined.as_bytes());
w.write_bool(self.partial_success);
w.into_vec()
}
pub fn decode(payload: &[u8]) -> Result<Self> {
let mut r = Reader::new(payload);
if r.read_u8()? != SSH_MSG_USERAUTH_FAILURE {
return Err(Error::Format("auth: not USERAUTH_FAILURE"));
}
let raw = read_bytes_capped(&mut r)?;
let partial_success = r.read_bool()?;
ensure_empty(&r)?;
let mut continuations = Vec::new();
for part in raw.split(|&b| b == b',') {
if part.is_empty() {
continue;
}
let s = core::str::from_utf8(part).map_err(|_| Error::Format("auth: invalid utf-8"))?;
continuations.push(s.into());
}
Ok(Self {
continuations,
partial_success,
})
}
}
pub fn encode_success() -> Vec<u8> {
let mut w = Writer::with_capacity(1);
w.write_u8(SSH_MSG_USERAUTH_SUCCESS);
w.into_vec()
}
pub fn decode_success(payload: &[u8]) -> Result<()> {
if payload.len() != 1 || payload[0] != SSH_MSG_USERAUTH_SUCCESS {
return Err(Error::Format("auth: not USERAUTH_SUCCESS"));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserauthBanner {
pub message: String,
pub language: String,
}
impl UserauthBanner {
pub fn encode(&self) -> Vec<u8> {
let mut w = Writer::with_capacity(16 + self.message.len() + self.language.len());
w.write_u8(SSH_MSG_USERAUTH_BANNER);
w.write_string(self.message.as_bytes());
w.write_string(self.language.as_bytes());
w.into_vec()
}
pub fn decode(payload: &[u8]) -> Result<Self> {
let mut r = Reader::new(payload);
if r.read_u8()? != SSH_MSG_USERAUTH_BANNER {
return Err(Error::Format("auth: not USERAUTH_BANNER"));
}
let message = read_utf8(&mut r)?;
let language = read_utf8(&mut r)?;
ensure_empty(&r)?;
Ok(Self { message, language })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserauthPkOk {
pub algorithm: String,
pub public_blob: Vec<u8>,
}
impl UserauthPkOk {
pub fn encode(&self) -> Vec<u8> {
let mut w = Writer::with_capacity(16 + self.algorithm.len() + self.public_blob.len());
w.write_u8(SSH_MSG_USERAUTH_PK_OK);
w.write_string(self.algorithm.as_bytes());
w.write_string(&self.public_blob);
w.into_vec()
}
pub fn decode(payload: &[u8]) -> Result<Self> {
let mut r = Reader::new(payload);
if r.read_u8()? != SSH_MSG_USERAUTH_PK_OK {
return Err(Error::Format("auth: not USERAUTH_PK_OK"));
}
let algorithm = read_utf8(&mut r)?;
let public_blob = read_bytes_capped(&mut r)?.to_vec();
ensure_empty(&r)?;
Ok(Self {
algorithm,
public_blob,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserauthInfoRequest {
pub name: String,
pub instruction: String,
pub language: String,
pub prompts: Vec<(String, bool)>,
}
impl UserauthInfoRequest {
pub fn encode(&self) -> Vec<u8> {
let mut w = Writer::with_capacity(32);
w.write_u8(SSH_MSG_USERAUTH_INFO_REQUEST);
w.write_string(self.name.as_bytes());
w.write_string(self.instruction.as_bytes());
w.write_string(self.language.as_bytes());
w.write_u32(self.prompts.len() as u32);
for (prompt, echo) in &self.prompts {
w.write_string(prompt.as_bytes());
w.write_bool(*echo);
}
w.into_vec()
}
pub fn decode(payload: &[u8]) -> Result<Self> {
let mut r = Reader::new(payload);
if r.read_u8()? != SSH_MSG_USERAUTH_INFO_REQUEST {
return Err(Error::Format("auth: not USERAUTH_INFO_REQUEST"));
}
let name = read_utf8(&mut r)?;
let instruction = read_utf8(&mut r)?;
let language = read_utf8(&mut r)?;
let n = r.read_u32()? as usize;
if n > 1024 {
return Err(Error::Format("auth: too many prompts"));
}
let mut prompts = Vec::with_capacity(n);
for _ in 0..n {
let prompt = read_utf8(&mut r)?;
let echo = r.read_bool()?;
prompts.push((prompt, echo));
}
ensure_empty(&r)?;
Ok(Self {
name,
instruction,
language,
prompts,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserauthInfoResponse {
pub responses: Vec<String>,
}
impl Drop for UserauthInfoResponse {
fn drop(&mut self) {
for resp in &mut self.responses {
resp.zeroize();
}
}
}
impl UserauthInfoResponse {
pub fn encode(&self) -> Vec<u8> {
let mut w = Writer::with_capacity(8);
w.write_u8(SSH_MSG_USERAUTH_INFO_RESPONSE);
w.write_u32(self.responses.len() as u32);
for resp in &self.responses {
w.write_string(resp.as_bytes());
}
w.into_vec()
}
pub fn decode(payload: &[u8]) -> Result<Self> {
let mut r = Reader::new(payload);
if r.read_u8()? != SSH_MSG_USERAUTH_INFO_RESPONSE {
return Err(Error::Format("auth: not USERAUTH_INFO_RESPONSE"));
}
let n = r.read_u32()? as usize;
if n > 1024 {
return Err(Error::Format("auth: too many responses"));
}
let mut responses = Vec::with_capacity(n);
for _ in 0..n {
responses.push(read_utf8(&mut r)?);
}
ensure_empty(&r)?;
Ok(Self { responses })
}
}
pub fn publickey_signed_data(
session_id: &[u8],
user: &str,
service: &str,
algorithm: &str,
public_blob: &[u8],
) -> Vec<u8> {
publickey_signed_data_bound(session_id, user, service, algorithm, public_blob, None)
}
pub const PUBLICKEY_HOSTBOUND: &str = "publickey-hostbound-v00@openssh.com";
pub fn publickey_signed_data_bound(
session_id: &[u8],
user: &str,
service: &str,
algorithm: &str,
public_blob: &[u8],
host_key: Option<&[u8]>,
) -> Vec<u8> {
let mut w = Writer::with_capacity(64 + session_id.len() + public_blob.len());
w.write_string(session_id);
w.write_u8(SSH_MSG_USERAUTH_REQUEST);
w.write_string(user.as_bytes());
w.write_string(service.as_bytes());
w.write_string(match host_key {
Some(_) => PUBLICKEY_HOSTBOUND.as_bytes(),
None => b"publickey",
});
w.write_bool(true);
w.write_string(algorithm.as_bytes());
w.write_string(public_blob);
if let Some(hk) = host_key {
w.write_string(hk);
}
w.into_vec()
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn service_request_roundtrip() {
let msg = ServiceRequest {
service: "ssh-userauth".into(),
};
let enc = msg.encode();
assert_eq!(enc[0], SSH_MSG_SERVICE_REQUEST);
let dec = ServiceRequest::decode(&enc).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn service_accept_roundtrip() {
let msg = ServiceAccept {
service: "ssh-userauth".into(),
};
let dec = ServiceAccept::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn userauth_request_none_roundtrip() {
let msg = UserauthRequest {
user: "alice".into(),
service: "ssh-connection".into(),
method: AuthMethodPayload::None,
};
let dec = UserauthRequest::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn userauth_request_password_roundtrip() {
let msg = UserauthRequest {
user: "alice".into(),
service: "ssh-connection".into(),
method: AuthMethodPayload::Password {
new_password: None,
password: "hunter2".into(),
},
};
let dec = UserauthRequest::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn userauth_request_publickey_probe_roundtrip() {
let msg = UserauthRequest {
user: "alice".into(),
service: "ssh-connection".into(),
method: AuthMethodPayload::PublicKey {
signature_present: false,
algorithm: "ssh-ed25519".into(),
public_blob: vec![1, 2, 3, 4],
signature: None,
host_key: None,
},
};
let dec = UserauthRequest::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn userauth_request_publickey_signed_roundtrip() {
let msg = UserauthRequest {
user: "alice".into(),
service: "ssh-connection".into(),
method: AuthMethodPayload::PublicKey {
signature_present: true,
algorithm: "ssh-ed25519".into(),
public_blob: vec![1, 2, 3, 4],
signature: Some(vec![9, 8, 7, 6]),
host_key: None,
},
};
let dec = UserauthRequest::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn userauth_request_kbdint_roundtrip() {
let msg = UserauthRequest {
user: "alice".into(),
service: "ssh-connection".into(),
method: AuthMethodPayload::KeyboardInteractive {
language_tag: "".into(),
submethods: "".into(),
},
};
let dec = UserauthRequest::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn userauth_failure_roundtrip() {
let msg = UserauthFailure {
continuations: vec!["password".into(), "publickey".into()],
partial_success: false,
};
let dec = UserauthFailure::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn userauth_failure_empty_continuations() {
let msg = UserauthFailure {
continuations: vec![],
partial_success: true,
};
let dec = UserauthFailure::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn success_roundtrip() {
let enc = encode_success();
decode_success(&enc).unwrap();
assert!(decode_success(&[]).is_err());
assert!(decode_success(&[99]).is_err());
}
#[test]
fn banner_roundtrip() {
let msg = UserauthBanner {
message: "welcome\n".into(),
language: "en-US".into(),
};
let dec = UserauthBanner::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn pk_ok_roundtrip() {
let msg = UserauthPkOk {
algorithm: "ssh-ed25519".into(),
public_blob: vec![1, 2, 3],
};
let dec = UserauthPkOk::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn info_request_roundtrip() {
let msg = UserauthInfoRequest {
name: "Login".into(),
instruction: "Enter creds".into(),
language: "".into(),
prompts: vec![("Password: ".into(), false), ("Token: ".into(), true)],
};
let dec = UserauthInfoRequest::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn info_response_roundtrip() {
let msg = UserauthInfoResponse {
responses: vec!["secret".into(), "12345".into()],
};
let dec = UserauthInfoResponse::decode(&msg.encode()).unwrap();
assert_eq!(dec, msg);
}
#[test]
fn malformed_userauth_request() {
assert!(UserauthRequest::decode(&[]).is_err());
assert!(UserauthRequest::decode(&[50]).is_err());
assert!(UserauthRequest::decode(&[50, 0, 0, 0, 100]).is_err());
}
#[test]
fn password_debug_is_redacted() {
use alloc::format;
let m = AuthMethodPayload::Password {
new_password: Some("brand-new-secret".into()),
password: "super-secret-pw".into(),
};
let s = format!("{m:?}");
assert!(
!s.contains("super-secret-pw"),
"password leaked in Debug output: {s}"
);
assert!(
!s.contains("brand-new-secret"),
"new_password leaked in Debug output: {s}"
);
assert!(
s.contains("redacted"),
"redaction marker missing in Debug output: {s}"
);
}
#[test]
fn userauth_string_cap_rejects_oversize_length() {
let payload = [
SSH_MSG_USERAUTH_REQUEST,
0x40,
0x00,
0x00,
0x00,
];
let err = UserauthRequest::decode(&payload).expect_err("must reject oversize string");
match err {
Error::Format(msg) => assert!(
msg.contains("too large"),
"expected 'too large' error, got: {msg}"
),
other => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
#[allow(clippy::cmp_owned, clippy::comparison_to_empty)]
fn secret_string_equality_is_exact() {
let s = SecretString::from("hunter2");
assert!(s == "hunter2");
assert!(s == *"hunter2");
assert!(s == SecretString::from("hunter2"));
assert!(s != "hunter");
assert!(s != "hunter22");
assert!(s != "");
assert!(SecretString::from("") != "x");
assert!(SecretString::from("") == "");
assert!(s != "Hunter2");
assert!(s != "hunter3");
assert!(s != SecretString::from("hunter3"));
assert!(ct_bytes_eq(b"", b""));
assert!(!ct_bytes_eq(b"\x00", b""));
assert!(!ct_bytes_eq(b"", b"\x00"));
assert!(!ct_bytes_eq(b"ab\x00", b"ab"));
}
#[test]
fn userauth_request_debug_is_redacted() {
use alloc::format;
let req = UserauthRequest {
user: "alice".into(),
service: "ssh-connection".into(),
method: AuthMethodPayload::Password {
new_password: None,
password: "hunter2".into(),
},
};
let s = format!("{req:?}");
assert!(!s.contains("hunter2"), "password leaked: {s}");
}
}