forge_workspace/store/
biometric.rs1use anyhow::Context;
8use redb::{ReadableTable, TableDefinition};
9use uuid::Uuid;
10
11use super::Db;
12use crate::biometric::Enrolment;
13
14const ENROLMENTS: TableDefinition<[u8; 16], &[u8]> = TableDefinition::new("biometric_enrolments");
15
16pub(crate) fn put(db: &Db, enrolment: &Enrolment) -> anyhow::Result<()> {
18 let value = serde_json::to_vec(enrolment).context("serialize biometric enrolment")?;
19 let txn = db.database().begin_write()?;
20 {
21 let mut table = txn.open_table(ENROLMENTS)?;
22 table.insert(enrolment.id.as_bytes(), value.as_slice())?;
23 }
24 txn.commit()?;
25 Ok(())
26}
27
28pub(crate) fn get(db: &Db, id: Uuid) -> anyhow::Result<Option<Enrolment>> {
34 let txn = db.database().begin_read()?;
35 let table = match txn.open_table(ENROLMENTS) {
36 Ok(table) => table,
37 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
40 Err(error) => return Err(error.into()),
41 };
42 let Some(value) = table.get(id.as_bytes())? else { return Ok(None) };
43 let enrolment = serde_json::from_slice(value.value())
44 .with_context(|| format!("decode biometric enrolment {id}"))?;
45 Ok(Some(enrolment))
46}
47
48pub(crate) fn list(db: &Db) -> anyhow::Result<Vec<Enrolment>> {
54 let txn = db.database().begin_read()?;
55 let table = match txn.open_table(ENROLMENTS) {
56 Ok(table) => table,
57 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
58 Err(error) => return Err(error.into()),
59 };
60 let mut out = Vec::new();
61 for entry in table.iter()? {
62 let (id, value) = entry?;
63 match serde_json::from_slice::<Enrolment>(value.value()) {
64 Ok(enrolment) => out.push(enrolment),
65 Err(error) => tracing::warn!(
66 target: "forge_workspace::store::biometric",
67 id = %Uuid::from_bytes(id.value()),
68 %error,
69 "skipping biometric enrolment record that failed to decode",
70 ),
71 }
72 }
73 Ok(out)
74}
75
76pub(crate) fn remove(db: &Db, id: Uuid) -> anyhow::Result<bool> {
78 let txn = db.database().begin_write()?;
79 let existed = match txn.open_table(ENROLMENTS) {
80 Ok(mut table) => table.remove(id.as_bytes())?.is_some(),
81 Err(redb::TableError::TableDoesNotExist(_)) => false,
82 Err(error) => return Err(error.into()),
83 };
84 txn.commit()?;
85 Ok(existed)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::biometric::StoredPurpose;
92 use std::time::{Duration, SystemTime};
93 use tempfile::tempdir;
94
95 fn enrolment(id: Uuid, purposes: &[(&str, &[u8])]) -> Enrolment {
96 let at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_790_000_000);
97 Enrolment {
98 id,
99 device: "Ved's MacBook Pro".to_owned(),
100 created_at: at,
101 purposes: purposes
102 .iter()
103 .map(|(purpose, blob)| StoredPurpose {
104 purpose: (*purpose).to_owned(),
105 blob: blob.to_vec(),
106 created_at: at,
107 last_used_at: None,
108 })
109 .collect(),
110 }
111 }
112
113 #[test]
114 fn an_enrolment_round_trips_through_the_table() {
115 let dir = tempdir().expect("tempdir");
116 let db = Db::open(&dir.path().join("db.redb")).expect("open db");
117 let id = Uuid::new_v4();
118 let written = enrolment(id, &[("vault", b"wrapped")]);
119
120 put(&db, &written).expect("put");
121 let read = get(&db, id).expect("get").expect("the row");
122 assert_eq!(read, written, "the stored row is the row that was written");
123 assert_eq!(list(&db).expect("list"), vec![written]);
124 }
125
126 #[test]
129 fn one_row_carries_every_purpose_with_its_own_shape() {
130 let dir = tempdir().expect("tempdir");
131 let db = Db::open(&dir.path().join("db.redb")).expect("open db");
132 let id = Uuid::new_v4();
133 let second = [9u8; 512];
134
135 put(&db, &enrolment(id, &[("vault", b"{\"token\":\"t\"}"), ("sso", &second)]))
136 .expect("put");
137
138 let read = get(&db, id).expect("get").expect("the row");
139 assert_eq!(read.purposes[0].blob, b"{\"token\":\"t\"}");
140 assert_eq!(read.purposes[1].blob, second);
141 }
142
143 #[test]
144 fn an_enrolment_survives_reopen() {
145 let dir = tempdir().expect("tempdir");
146 let path = dir.path().join("db.redb");
147 let id = Uuid::new_v4();
148 {
149 let db = Db::open(&path).expect("open db");
150 put(&db, &enrolment(id, &[("vault", b"kept")])).expect("put");
151 }
152 let db = Db::open(&path).expect("reopen db");
153 let read = get(&db, id).expect("get").expect("the row");
154 assert_eq!(read.purposes[0].blob, b"kept");
155 }
156
157 #[test]
160 fn a_corrupt_row_is_skipped_by_the_list_and_refused_by_get() {
161 let dir = tempdir().expect("tempdir");
162 let db = Db::open(&dir.path().join("db.redb")).expect("open db");
163 let good = Uuid::new_v4();
164 put(&db, &enrolment(good, &[("vault", b"good")])).expect("put the good row");
165
166 let corrupt = Uuid::new_v4();
167 let txn = db.database().begin_write().expect("begin");
168 {
169 let mut table = txn.open_table(ENROLMENTS).expect("table");
170 table.insert(corrupt.as_bytes(), "not an enrolment".as_bytes()).expect("insert");
171 }
172 txn.commit().expect("commit");
173
174 assert_eq!(list(&db).expect("list tolerates the corrupt row").len(), 1);
175 assert!(get(&db, corrupt).is_err(), "a named corrupt row is an error, not an absence");
176 }
177
178 #[test]
179 fn removing_an_enrolment_reports_whether_it_was_there() {
180 let dir = tempdir().expect("tempdir");
181 let db = Db::open(&dir.path().join("db.redb")).expect("open db");
182 let id = Uuid::new_v4();
183 put(&db, &enrolment(id, &[("vault", b"x")])).expect("put");
184
185 assert!(remove(&db, id).expect("remove"), "an existing row removes to true");
186 assert!(!remove(&db, id).expect("remove again"), "an absent row removes to false");
187 assert!(get(&db, id).expect("get").is_none());
188 }
189}