Skip to content

Commit f006bd6

Browse files
committed
Add PIN and reset service helpers to pc-hid-runner
1 parent 198d917 commit f006bd6

2 files changed

Lines changed: 139 additions & 1 deletion

File tree

pc-hid-runner/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ trussed = { version = "0.1", default-features = false, features = ["log-all", "v
2121
authenticator = { path = "../authenticator" }
2222
daemonize = "0.4"
2323
rand = { version = "0.8", features = ["std"] }
24+
sha2 = "0.10"
2425

2526
[features]
2627
default = ["uhid-backend"]

pc-hid-runner/src/service.rs

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use std::{
44
};
55

66
use authenticator::ctap::CtapApp;
7-
use transport_core::state::{IdentityConfig, PersistentStore};
7+
use sha2::{Digest, Sha256};
8+
use transport_core::state::{
9+
reset_state_dir, IdentityConfig, PersistentStore, StoredPinState, DEFAULT_PIN_RETRIES,
10+
};
811
use transport_core::{set_waiting, Apps as TrussedApps, Builder, Options, Platform, Syscall};
912
use trussed::{
1013
backend::{CoreOnly, NoId},
@@ -163,3 +166,137 @@ pub fn default_identity() -> IdentityStrings {
163166
pub fn ensure_state_dir(path: &Path) -> io::Result<()> {
164167
transport_core::state::ensure_state_dir(path)
165168
}
169+
170+
/// Minimum PIN length enforced by the CTAP layer.
171+
const MIN_PIN_LENGTH: usize = 4;
172+
/// Maximum PIN length permitted by CTAP (63 byte UTF-8 string max).
173+
const MAX_PIN_LENGTH: usize = 63;
174+
175+
fn validate_pin(pin: &str) -> io::Result<()> {
176+
let bytes = pin.as_bytes();
177+
if bytes.len() < MIN_PIN_LENGTH {
178+
return Err(io::Error::new(
179+
io::ErrorKind::InvalidInput,
180+
format!("PIN must be at least {MIN_PIN_LENGTH} bytes"),
181+
));
182+
}
183+
if bytes.len() > MAX_PIN_LENGTH {
184+
return Err(io::Error::new(
185+
io::ErrorKind::InvalidInput,
186+
format!("PIN must be at most {MAX_PIN_LENGTH} bytes"),
187+
));
188+
}
189+
Ok(())
190+
}
191+
192+
fn hash_pin(pin: &str) -> [u8; 16] {
193+
let digest = Sha256::digest(pin.as_bytes());
194+
let mut out = [0u8; 16];
195+
out.copy_from_slice(&digest[..16]);
196+
out
197+
}
198+
199+
/// Summary view of the persistent PIN state for the `pin status` CLI.
200+
pub struct PinInfo {
201+
pub is_set: bool,
202+
pub retries: u8,
203+
pub blocked: bool,
204+
}
205+
206+
/// Read the persistent PIN summary for the CLI.
207+
pub fn pin_info(state_dir: &Path) -> io::Result<PinInfo> {
208+
let store = PersistentStore::new(state_dir)?;
209+
let state = store.read_pin_state()?;
210+
Ok(PinInfo {
211+
is_set: state.pin_hash.is_some(),
212+
retries: state.pin_retries,
213+
blocked: state.pin_auth_blocked,
214+
})
215+
}
216+
217+
/// Persist a brand-new PIN. Fails if a PIN is already set.
218+
pub fn pin_set(state_dir: &Path, new_pin: &str) -> io::Result<()> {
219+
validate_pin(new_pin)?;
220+
let store = PersistentStore::new(state_dir)?;
221+
let existing = store.read_pin_state()?;
222+
if existing.pin_hash.is_some() {
223+
return Err(io::Error::new(
224+
io::ErrorKind::AlreadyExists,
225+
"a PIN is already set; use 'pin change' instead",
226+
));
227+
}
228+
let updated = StoredPinState {
229+
pin_hash: Some(hash_pin(new_pin)),
230+
pin_retries: DEFAULT_PIN_RETRIES,
231+
consecutive_failures: 0,
232+
pin_auth_blocked: false,
233+
};
234+
store.write_pin_state(&updated)
235+
}
236+
237+
/// Persist a replacement PIN after verifying the current one.
238+
pub fn pin_change(state_dir: &Path, current_pin: &str, new_pin: &str) -> io::Result<()> {
239+
validate_pin(new_pin)?;
240+
let store = PersistentStore::new(state_dir)?;
241+
let mut state = store.read_pin_state()?;
242+
verify_current_pin(&mut state, current_pin)?;
243+
state.pin_hash = Some(hash_pin(new_pin));
244+
state.pin_retries = DEFAULT_PIN_RETRIES;
245+
state.consecutive_failures = 0;
246+
state.pin_auth_blocked = false;
247+
store.write_pin_state(&state)
248+
}
249+
250+
/// Clear the PIN after verifying the current one.
251+
pub fn pin_remove(state_dir: &Path, current_pin: &str) -> io::Result<()> {
252+
let store = PersistentStore::new(state_dir)?;
253+
let mut state = store.read_pin_state()?;
254+
verify_current_pin(&mut state, current_pin)?;
255+
state.pin_hash = None;
256+
state.pin_retries = DEFAULT_PIN_RETRIES;
257+
state.consecutive_failures = 0;
258+
state.pin_auth_blocked = false;
259+
store.write_pin_state(&state)
260+
}
261+
262+
/// Wipe credentials and reset PIN state. Equivalent to a factory reset.
263+
pub fn reset_state(state_dir: &Path) -> io::Result<()> {
264+
reset_state_dir(state_dir)
265+
}
266+
267+
fn verify_current_pin(state: &mut StoredPinState, candidate: &str) -> io::Result<()> {
268+
let stored = state.pin_hash.ok_or_else(|| {
269+
io::Error::new(io::ErrorKind::PermissionDenied, "no PIN is currently set")
270+
})?;
271+
if state.pin_auth_blocked {
272+
return Err(io::Error::new(
273+
io::ErrorKind::PermissionDenied,
274+
"PIN is blocked until the authenticator is reset",
275+
));
276+
}
277+
let provided = hash_pin(candidate);
278+
let mut diff = 0u8;
279+
for (a, b) in stored.iter().zip(provided.iter()) {
280+
diff |= a ^ b;
281+
}
282+
if diff == 0 {
283+
state.pin_retries = DEFAULT_PIN_RETRIES;
284+
state.consecutive_failures = 0;
285+
Ok(())
286+
} else {
287+
if state.pin_retries > 0 {
288+
state.pin_retries -= 1;
289+
}
290+
state.consecutive_failures = state.consecutive_failures.saturating_add(1);
291+
if state.pin_retries == 0 {
292+
state.pin_auth_blocked = true;
293+
}
294+
Err(io::Error::new(
295+
io::ErrorKind::PermissionDenied,
296+
format!(
297+
"PIN is incorrect ({} retries remaining)",
298+
state.pin_retries
299+
),
300+
))
301+
}
302+
}

0 commit comments

Comments
 (0)