Skip to main content

hsm_crypto_service/
session.rs

1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! PIN session bookkeeping.
17//!
18//! A PIN session is opened by a successful [`crate::CryptoService::verify_pin`]
19//! call and expires after [`SESSION_TIMEOUT_MS`] of inactivity. Each
20//! authenticated operation (signing, key generation) refreshes the session.
21//!
22//! The session uses a [`Clock`] abstraction so that tests can drive time
23//! deterministically. In firmware the implementation is backed by
24//! `embassy_time::Instant`. In tests it is a hand-rolled `Cell<u64>`.
25
26/// Session inactivity timeout in milliseconds.
27pub const SESSION_TIMEOUT_MS: u64 = 30_000;
28
29/// Source of monotonic time used by the session.
30///
31/// The returned value is in milliseconds since some fixed epoch chosen by
32/// the implementation. The crypto service only ever compares values, so
33/// the epoch does not matter as long as it is monotonic.
34pub trait Clock
35{
36    /// Current monotonic time in milliseconds.
37    fn now_ms(&self) -> u64;
38}
39
40/// PIN session state.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[cfg_attr(feature = "defmt", derive(defmt::Format))]
43pub struct Session
44{
45    /// Timestamp (`Clock::now_ms`) of the last activity. `None` means no
46    /// session is open.
47    last_activity_ms: Option<u64>,
48}
49
50impl Session
51{
52    /// Build a fresh session, closed.
53    #[must_use]
54    pub(crate) const fn new() -> Self
55    {
56        Self { last_activity_ms: None }
57    }
58
59    /// Open the session at the given time.
60    pub(crate) fn open(&mut self, now_ms: u64)
61    {
62        self.last_activity_ms = Some(now_ms);
63    }
64
65    /// Close the session immediately.
66    pub(crate) fn close(&mut self)
67    {
68        self.last_activity_ms = None;
69    }
70
71    /// Refresh the activity timestamp. No-op if the session is closed.
72    pub(crate) fn touch(&mut self, now_ms: u64)
73    {
74        if self.last_activity_ms.is_some()
75        {
76            self.last_activity_ms = Some(now_ms);
77        }
78    }
79
80    /// Returns `true` if the session is open and not yet timed out at the
81    /// given current time.
82    #[must_use]
83    pub(crate) fn is_active(&self, now_ms: u64) -> bool
84    {
85        match self.last_activity_ms
86        {
87            None => false,
88            Some(last) => now_ms.saturating_sub(last) < SESSION_TIMEOUT_MS,
89        }
90    }
91}
92
93impl Default for Session
94{
95    fn default() -> Self
96    {
97        Self::new()
98    }
99}
100
101#[cfg(test)]
102mod tests
103{
104    use super::*;
105
106    #[test]
107    fn fresh_session_is_inactive()
108    {
109        let s = Session::new();
110        assert!(!s.is_active(0));
111        assert!(!s.is_active(1_000_000));
112    }
113
114    #[test]
115    fn opened_session_is_active_at_open_time()
116    {
117        let mut s = Session::new();
118        s.open(100);
119        assert!(s.is_active(100));
120    }
121
122    #[test]
123    fn session_active_within_timeout()
124    {
125        let mut s = Session::new();
126        s.open(0);
127        assert!(s.is_active(SESSION_TIMEOUT_MS - 1));
128    }
129
130    #[test]
131    fn session_expires_at_timeout()
132    {
133        let mut s = Session::new();
134        s.open(0);
135        assert!(!s.is_active(SESSION_TIMEOUT_MS));
136    }
137
138    #[test]
139    fn touch_extends_session()
140    {
141        let mut s = Session::new();
142        s.open(0);
143        // Halfway to expiry.
144        s.touch(15_000);
145        // 15 s after the touch, still less than the full timeout from the
146        // touch's own timestamp.
147        assert!(s.is_active(30_000));
148        assert!(!s.is_active(45_000));
149    }
150
151    #[test]
152    fn touch_on_closed_session_is_noop()
153    {
154        let mut s = Session::new();
155        s.touch(100);
156        assert!(!s.is_active(100));
157    }
158
159    #[test]
160    fn close_drops_active_session()
161    {
162        let mut s = Session::new();
163        s.open(0);
164        s.close();
165        assert!(!s.is_active(0));
166    }
167}