diff --git a/src/models.rs b/src/models.rs index 35ce341..ccd857b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -14,7 +14,7 @@ where // --- DB models --- -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct Account { @@ -28,6 +28,22 @@ pub struct Account { pub software: String, } +// token をログ・panic メッセージに漏らさないため Debug は手書き +impl std::fmt::Debug for Account { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Account") + .field("id", &self.id) + .field("host", &self.host) + .field("token", &"") + .field("user_id", &self.user_id) + .field("username", &self.username) + .field("display_name", &self.display_name) + .field("avatar_url", &self.avatar_url) + .field("software", &self.software) + .finish() + } +} + impl Drop for Account { fn drop(&mut self) { self.token.zeroize(); @@ -1042,7 +1058,7 @@ pub struct AuthSession { pub host: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct AuthResult { @@ -1050,6 +1066,16 @@ pub struct AuthResult { pub user: NormalizedUser, } +// token をログ・panic メッセージに漏らさないため Debug は手書き +impl std::fmt::Debug for AuthResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AuthResult") + .field("token", &"") + .field("user", &self.user) + .finish() + } +} + // --- Raw Misskey API response types (for deserialization) --- #[derive(Debug, Deserialize)] @@ -1291,13 +1317,24 @@ pub struct MutedWordsResult { pub muted_instances: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Deserialize)] pub struct RawMiAuthResponse { pub ok: bool, pub token: Option, pub user: Option, } +// token をログ・panic メッセージに漏らさないため Debug は手書き +impl std::fmt::Debug for RawMiAuthResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RawMiAuthResponse") + .field("ok", &self.ok) + .field("token", &self.token.as_ref().map(|_| "")) + .field("user", &self.user) + .finish() + } +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RawCreateNoteResponse { @@ -1688,6 +1725,51 @@ mod tests { assert_eq!(json.get("hasToken").unwrap(), true); } + // ---- Debug redaction ---- + + #[test] + fn account_debug_redacts_token() { + let account = Account { + id: "acc1".into(), + host: "misskey.io".into(), + token: "secret-token".into(), + user_id: "uid1".into(), + username: "taka".into(), + display_name: None, + avatar_url: None, + software: "misskey".into(), + }; + let debug = format!("{account:?}"); + assert!(!debug.contains("secret-token"), "token leaked: {debug}"); + assert!(debug.contains("")); + assert!(debug.contains("misskey.io")); + } + + #[test] + fn auth_result_debug_redacts_token() { + let user: NormalizedUser = + serde_json::from_value(json!({"id": "u1", "username": "taka"})).unwrap(); + let result = AuthResult { + token: "secret-token".into(), + user, + }; + let debug = format!("{result:?}"); + assert!(!debug.contains("secret-token"), "token leaked: {debug}"); + assert!(debug.contains("")); + } + + #[test] + fn raw_miauth_response_debug_redacts_token() { + let resp = RawMiAuthResponse { + ok: true, + token: Some("secret-token".into()), + user: None, + }; + let debug = format!("{resp:?}"); + assert!(!debug.contains("secret-token"), "token leaked: {debug}"); + assert!(debug.contains("")); + } + #[test] fn account_public_without_token() { let account = Account {