1use ring::rand::{SecureRandom, SystemRandom};
2use serde_json::Value;
3use sqlx::Row;
4use sqlx::sqlite::SqliteRow;
5use tracing::{debug, info};
6use uuid::Uuid;
7
8use crate::sqlite::db::Database;
9use crate::sqlite::nonce::now_secs;
10use crate::sqlite::order::rfc3339;
11
12#[derive(Debug)]
28pub struct Eab {
29 pub kid: String,
30 pub secret: Vec<u8>,
31 pub label: Option<String>,
32 pub profile: Option<String>,
35 pub status: String,
36 pub created_at: i64,
37}
38
39const SECRET_LEN: usize = 32;
42
43fn generate_secret() -> Vec<u8> {
47 let rng = SystemRandom::new();
48 let mut bytes = vec![0u8; SECRET_LEN];
49 rng.fill(&mut bytes).expect("system RNG unavailable");
50 bytes
51}
52
53impl Eab {
54 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
55 Ok(Eab {
56 kid: row.try_get("kid")?,
57 secret: row.try_get("secret")?,
58 label: row.try_get("label")?,
59 profile: row.try_get("profile")?,
60 status: row.try_get("status")?,
61 created_at: row.try_get("created_at")?,
62 })
63 }
64
65 pub async fn create(
70 label: Option<String>,
71 profile: Option<String>,
72 database: &Database,
73 ) -> Result<Eab, sqlx::Error> {
74 let eab = Eab {
75 kid: Uuid::new_v4().to_string(),
76 secret: generate_secret(),
77 label,
78 profile,
79 status: "active".to_string(),
80 created_at: now_secs(),
81 };
82
83 debug!(event = "db_eab_create_started", outcome = "progress", kid = ?eab.kid, profile = ?eab.profile);
84 sqlx::query(
85 "INSERT INTO eab_keys (kid, secret, label, profile, status, created_at) \
86 VALUES (?, ?, ?, ?, ?, ?);",
87 )
88 .bind(&eab.kid)
89 .bind(&eab.secret)
90 .bind(&eab.label)
91 .bind(&eab.profile)
92 .bind(&eab.status)
93 .bind(eab.created_at)
94 .execute(&database.pool)
95 .await?;
96
97 info!(event = "db_eab_created", outcome = "success", kid = ?eab.kid);
98 Ok(eab)
99 }
100
101 pub async fn find_by_kid(
105 kid: &str,
106 profile: &str,
107 database: &Database,
108 ) -> Result<Option<Eab>, sqlx::Error> {
109 debug!(event = "db_eab_find_by_kid_started", outcome = "progress", kid = ?kid, profile = %profile);
110 let row = sqlx::query(
111 "SELECT kid, secret, label, profile, status, created_at FROM eab_keys \
112 WHERE kid = ? AND (profile IS NULL OR profile = ?);",
113 )
114 .bind(kid)
115 .bind(profile)
116 .fetch_optional(&database.pool)
117 .await?;
118
119 row.map(Eab::from_row).transpose()
120 }
121
122 pub async fn find_any_by_kid(
127 kid: &str,
128 database: &Database,
129 ) -> Result<Option<Eab>, sqlx::Error> {
130 debug!(event = "db_eab_find_any_by_kid_started", outcome = "progress", kid = ?kid);
131 let row = sqlx::query(
132 "SELECT kid, secret, label, profile, status, created_at FROM eab_keys WHERE kid = ?;",
133 )
134 .bind(kid)
135 .fetch_optional(&database.pool)
136 .await?;
137
138 row.map(Eab::from_row).transpose()
139 }
140
141 pub async fn list_all(database: &Database) -> Result<Vec<Eab>, sqlx::Error> {
143 debug!(event = "db_eab_list_all_started", outcome = "progress");
144 let rows = sqlx::query(
145 "SELECT kid, secret, label, profile, status, created_at FROM eab_keys ORDER BY created_at ASC;",
146 )
147 .fetch_all(&database.pool)
148 .await?;
149
150 rows.into_iter().map(Eab::from_row).collect()
151 }
152
153 pub async fn revoke(kid: &str, database: &Database) -> Result<bool, sqlx::Error> {
158 debug!(event = "db_eab_revoke_started", outcome = "progress", kid = ?kid);
159 let result = sqlx::query("UPDATE eab_keys SET status = 'revoked' WHERE kid = ?;")
160 .bind(kid)
161 .execute(&database.pool)
162 .await?;
163
164 let updated = result.rows_affected() > 0;
165 if updated {
166 info!(event = "db_eab_revoked", outcome = "success", kid = ?kid);
167 } else {
168 debug!(event = "db_eab_revoke_missing", outcome = "success", kid = ?kid);
169 }
170 Ok(updated)
171 }
172
173 #[must_use]
175 pub fn is_active(&self) -> bool {
176 self.status == "active"
177 }
178
179 #[must_use]
185 pub fn to_json(&self) -> Value {
186 serde_json::json!({
187 "kid": self.kid,
188 "label": self.label,
189 "profile": self.profile,
190 "status": self.status,
191 "createdAt": rfc3339(self.created_at),
192 })
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use std::sync::Arc;
200
201 #[tokio::test]
202 async fn create_persists_an_active_key_with_a_32_byte_secret() {
203 let db = Arc::new(Database::connect_in_memory().await.unwrap());
204 let eab = Eab::create(Some("team-a".to_string()), None, &db)
205 .await
206 .unwrap();
207 assert_eq!(eab.status, "active");
208 assert_eq!(eab.secret.len(), 32);
209 assert_eq!(eab.label.as_deref(), Some("team-a"));
210 }
211
212 #[tokio::test]
213 async fn find_by_kid_round_trip() {
214 let db = Arc::new(Database::connect_in_memory().await.unwrap());
215 let created = Eab::create(None, None, &db).await.unwrap();
216 let found = Eab::find_by_kid(&created.kid, "default", &db)
217 .await
218 .unwrap()
219 .unwrap();
220 assert_eq!(found.secret, created.secret);
221 assert!(found.label.is_none());
222 }
223
224 #[tokio::test]
225 async fn find_by_kid_of_unknown_returns_none() {
226 let db = Arc::new(Database::connect_in_memory().await.unwrap());
227 assert!(
228 Eab::find_by_kid("nope", "default", &db)
229 .await
230 .unwrap()
231 .is_none()
232 );
233 }
234
235 #[tokio::test]
236 async fn list_all_orders_oldest_first_and_empty_is_empty() {
237 let db = Arc::new(Database::connect_in_memory().await.unwrap());
238 assert!(Eab::list_all(&db).await.unwrap().is_empty());
239
240 let first = Eab::create(None, None, &db).await.unwrap();
241 let second = Eab::create(None, None, &db).await.unwrap();
242 let all = Eab::list_all(&db).await.unwrap();
243 assert_eq!(all.len(), 2);
244 assert_eq!(all[0].kid, first.kid);
245 assert_eq!(all[1].kid, second.kid);
246 }
247
248 #[tokio::test]
249 async fn revoke_marks_revoked_reports_true_and_is_idempotent() {
250 let db = Arc::new(Database::connect_in_memory().await.unwrap());
251 let eab = Eab::create(None, None, &db).await.unwrap();
252 assert!(Eab::revoke(&eab.kid, &db).await.unwrap());
253 assert!(
254 !Eab::find_by_kid(&eab.kid, "default", &db)
255 .await
256 .unwrap()
257 .unwrap()
258 .is_active()
259 );
260 assert!(Eab::revoke(&eab.kid, &db).await.unwrap());
262 }
263
264 #[tokio::test]
265 async fn revoke_of_unknown_kid_reports_false() {
266 let db = Arc::new(Database::connect_in_memory().await.unwrap());
267 assert!(!Eab::revoke("nope", &db).await.unwrap());
268 }
269
270 #[tokio::test]
271 async fn to_json_never_includes_the_secret() {
272 let db = Arc::new(Database::connect_in_memory().await.unwrap());
273 let eab = Eab::create(Some("x".to_string()), None, &db).await.unwrap();
274 let json = eab.to_json();
275 assert!(json.get("secret").is_none());
276 assert!(json.get("hmacKey").is_none());
277 assert_eq!(json["kid"], eab.kid);
278 assert_eq!(json["status"], "active");
279 assert_eq!(json["label"], "x");
280 }
281}