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}