Skip to main content

hdm_am/operations/
session.rs

1use crate::wire::OperationCode;
2use serde::{Deserialize, Serialize};
3
4use super::{EmptyResponse, Operation};
5
6/// Op 2 request. Encrypted with the password key. The response contains the session key for
7/// subsequent operations.
8///
9/// `Debug` is hand-written to redact `password` and `pin` so they never leak through `{:?}`.
10#[derive(Clone, Serialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12pub struct OperatorLoginRequest {
13    /// HDM access password.
14    pub password: String,
15    /// Operator (cashier) numeric ID — value from `OperatorInfo::id`.
16    pub cashier: u32,
17    /// Operator's PIN code.
18    pub pin: String,
19}
20
21impl std::fmt::Debug for OperatorLoginRequest {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("OperatorLoginRequest")
24            .field("password", &"[redacted]")
25            .field("cashier", &self.cashier)
26            .field("pin", &"[redacted]")
27            .finish()
28    }
29}
30
31impl Operation for OperatorLoginRequest {
32    const CODE: OperationCode = OperationCode::OperatorLogin;
33    const USES_PASSWORD_KEY: bool = true;
34    const RESPONSE_IS_SECRET: bool = true;
35    type Response = OperatorLoginResponse;
36}
37
38/// Op 2 response: the Base64-encoded session key.
39///
40/// Deliberately does **not** derive `Serialize` (so it can't leak via `--json`), and its `Debug` is
41/// hand-written to redact `key`: the session secret must never be casually written to logs or
42/// output. The client consumes it internally and never exposes it.
43#[derive(Clone, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[non_exhaustive]
46pub struct OperatorLoginResponse {
47    /// 24-byte session key, Base64-encoded.
48    pub key: String,
49}
50
51impl std::fmt::Debug for OperatorLoginResponse {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("OperatorLoginResponse")
54            .field("key", &"[redacted]")
55            .finish()
56    }
57}
58
59/// Op 3 request. Encrypted with the session key. No meaningful response.
60#[derive(Debug, Clone, Serialize)]
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
62pub struct OperatorLogoutRequest {}
63
64impl Operation for OperatorLogoutRequest {
65    const CODE: OperationCode = OperationCode::OperatorLogout;
66    type Response = EmptyResponse;
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    /// `Debug` must redact the password, PIN, and session key — never print them.
74    #[test]
75    fn debug_redacts_secrets() {
76        let request = OperatorLoginRequest {
77            password: "super-secret-pw".to_owned(),
78            cashier: 3,
79            pin: "9999".to_owned(),
80        };
81        let rendered = format!("{request:?}");
82        assert!(rendered.contains("[redacted]"), "{rendered}");
83        assert!(
84            !rendered.contains("super-secret-pw"),
85            "password leaked: {rendered}"
86        );
87        assert!(!rendered.contains("9999"), "pin leaked: {rendered}");
88        assert!(
89            rendered.contains("cashier: 3"),
90            "non-secret field should show: {rendered}"
91        );
92
93        let response = OperatorLoginResponse {
94            key: "SECRET-SESSION-KEY".to_owned(),
95        };
96        let rendered = format!("{response:?}");
97        assert!(rendered.contains("[redacted]"), "{rendered}");
98        assert!(
99            !rendered.contains("SECRET-SESSION-KEY"),
100            "session key leaked: {rendered}"
101        );
102    }
103}