pb_mapper_auth/persistence/
admin_key.rs1use super::super::*;
3use super::{
4 atomic_write, auth_snapshot_path, auth_wal_path, create_new_write, encrypted_auth_state_exists,
5 open_blob, truncate_auth_wal,
6};
7
8pub(crate) fn load_or_create_instance_id(
9 path: &Path,
10) -> Result<[u8; INSTANCE_ID_LEN], AuthFailure> {
11 let instance_path = path.join("server-instance-id");
12 if let Some(instance_id) = read_instance_id_file(&instance_path)? {
13 return Ok(instance_id);
14 }
15 let instance_id = random_instance_id();
16 atomic_write(&instance_path, &instance_id, 0o600)?;
17 Ok(instance_id)
18}
19
20pub(crate) fn read_instance_id_file(
21 path: &Path,
22) -> Result<Option<[u8; INSTANCE_ID_LEN]>, AuthFailure> {
23 if !path.exists() {
24 return Ok(None);
25 }
26 let bytes = std::fs::read(path).map_err(|error| {
27 AuthFailure::new(
28 "auth_state_unavailable",
29 format!("failed to read `{}`: {error}", path.display()),
30 false,
31 )
32 })?;
33 bytes.try_into().map(Some).map_err(|_| {
34 AuthFailure::new(
35 "auth_state_unavailable",
36 "server instance id must be exactly 16 bytes",
37 false,
38 )
39 })
40}
41
42pub(crate) fn recover_instance_id_after_reset(
48 state_dir: &Path,
49 admin_key: &AesKeyType,
50 current: [u8; INSTANCE_ID_LEN],
51) -> Result<[u8; INSTANCE_ID_LEN], AuthFailure> {
52 let next_path = state_dir.join("server-instance-id.next");
53 let Some(next) = read_instance_id_file(&next_path)? else {
54 return Ok(current);
55 };
56 let snapshot_path = auth_snapshot_path(state_dir);
57 if !snapshot_path.exists() {
58 let _ = std::fs::remove_file(&next_path);
59 return Ok(current);
60 }
61 let bytes = std::fs::read(&snapshot_path).map_err(|error| {
62 AuthFailure::new(
63 "temporary_key_store_unavailable",
64 format!("failed to read `{}`: {error}", snapshot_path.display()),
65 false,
66 )
67 })?;
68 let Ok(plain) = open_blob(admin_key, &bytes) else {
69 return Ok(current);
70 };
71 let Ok(snapshot) = serde_json::from_slice::<PersistedSnapshot>(&plain) else {
72 return Ok(current);
73 };
74 if snapshot.instance_id == current {
75 let _ = std::fs::remove_file(&next_path);
76 return Ok(current);
77 }
78 if snapshot.instance_id != next {
79 return Ok(current);
80 }
81 truncate_auth_wal(state_dir)?;
84 atomic_write(&state_dir.join("server-instance-id"), &next, 0o600)?;
85 let _ = std::fs::remove_file(&next_path);
86 Ok(next)
87}
88
89pub(crate) fn random_instance_id() -> [u8; INSTANCE_ID_LEN] {
90 let mut instance_id = [0_u8; INSTANCE_ID_LEN];
91 let mut rng = rand::rng();
92 for byte in &mut instance_id {
93 *byte = rng.random();
94 }
95 instance_id
96}
97
98pub(crate) fn write_admin_key(state_dir: &Path, key: &str) -> Result<(), AuthFailure> {
99 atomic_write(
100 &state_dir.join("admin.key"),
101 format!("{key}\n").as_bytes(),
102 0o600,
103 )
104}
105
106pub fn generate_admin_key() -> String {
107 const CHARSET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
108 let mut rng = rand::rng();
109 (0..32)
110 .map(|_| CHARSET[rng.random_range(0..CHARSET.len())] as char)
111 .collect()
112}
113
114pub fn initialize_admin_key(path: &Path, force: bool) -> Result<String, AuthFailure> {
115 if path.exists() && !force {
116 return Err(AuthFailure::new(
117 "administrator_key_exists",
118 format!("administrator key file `{}` already exists", path.display()),
119 false,
120 ));
121 }
122 refuse_write_if_encrypted_state(path, force)?;
123 let key = generate_admin_key();
124 atomic_write(path, format!("{key}\n").as_bytes(), 0o600)?;
125 Ok(key)
126}
127
128fn validate_admin_key(key: &str) -> Result<(), AuthFailure> {
130 let Credential::Admin(_) = parse_credential(key)
131 .map_err(|error| AuthFailure::new("administrator_key_invalid", error, false))?
132 else {
133 return Err(AuthFailure::new(
134 "administrator_key_invalid",
135 "administrator key file requires a 32-byte administrator key",
136 false,
137 ));
138 };
139 Ok(())
140}
141
142pub fn write_admin_key_file(path: &Path, key: &str, force: bool) -> Result<(), AuthFailure> {
143 validate_admin_key(key)?;
144 if path.exists() && !force {
145 return Err(AuthFailure::new(
146 "administrator_key_exists",
147 format!(
148 "administrator key file `{}` already exists; pass --force to replace it",
149 path.display()
150 ),
151 false,
152 ));
153 }
154 if path.file_name() == Some(std::ffi::OsStr::new("admin.key"))
155 && !key_matches_existing_state(path.parent(), key)
156 {
157 refuse_write_if_encrypted_state(path, force)?;
158 }
159 atomic_write(path, format!("{key}\n").as_bytes(), 0o600)
160}
161
162pub fn staged_admin_key_path(path: &Path) -> PathBuf {
168 let name = path
169 .file_name()
170 .and_then(|name| name.to_str())
171 .unwrap_or("admin.key");
172 path.with_file_name(format!(".{name}.next"))
173}
174
175pub fn stage_admin_key_candidate(path: &Path, key: &str) -> Result<PathBuf, AuthFailure> {
185 let staged_path = staged_admin_key_path(path);
186 if let Some(staged) = read_admin_key_file(&staged_path)? {
187 return accept_staged_candidate(path, staged_path, &staged, key);
188 }
189 validate_admin_key(key)?;
190 if !create_new_write(&staged_path, format!("{key}\n").as_bytes(), 0o600)? {
196 let staged = read_admin_key_file(&staged_path)?.unwrap_or_default();
197 return accept_staged_candidate(path, staged_path, &staged, key);
198 }
199 Ok(staged_path)
200}
201
202fn accept_staged_candidate(
209 path: &Path,
210 staged_path: PathBuf,
211 staged: &str,
212 key: &str,
213) -> Result<PathBuf, AuthFailure> {
214 if staged.trim() == key.trim() {
215 return Ok(staged_path);
216 }
217 Err(AuthFailure::new(
218 "administrator_key_staged",
219 format!(
220 "a previous root rotation left an unresolved candidate at `{}`, so the relay's active \
221 administrator key is either `{}` or that candidate; determine which key the relay \
222 accepts, install it at `{}`, and delete the staged file before rotating again",
223 staged_path.display(),
224 path.display(),
225 path.display()
226 ),
227 false,
228 ))
229}
230
231pub fn discard_staged_admin_key(staged_path: &Path) {
237 if let Err(error) = std::fs::remove_file(staged_path) {
238 tracing::warn!(
239 path = %staged_path.display(),
240 %error,
241 "administrator key was rotated, but the staged candidate could not be removed"
242 );
243 }
244}
245
246fn read_admin_key_file(path: &Path) -> Result<Option<String>, AuthFailure> {
248 match std::fs::read_to_string(path) {
249 Ok(contents) => Ok(Some(contents)),
250 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
251 Err(error) => Err(AuthFailure::new(
252 "auth_state_unavailable",
253 format!("failed to read `{}`: {error}", path.display()),
254 false,
255 )),
256 }
257}
258
259pub(crate) fn reset_already_installed(
260 state_dir: &Path,
261 admin_key: &AesKeyType,
262 new_instance_id: &[u8; INSTANCE_ID_LEN],
263) -> bool {
264 let Ok(Some(live)) = read_instance_id_file(&state_dir.join("server-instance-id")) else {
265 return false;
266 };
267 if live != *new_instance_id {
268 return false;
269 }
270 let Ok(bytes) = std::fs::read(auth_snapshot_path(state_dir)) else {
271 return false;
272 };
273 let Ok(plain) = open_blob(admin_key, &bytes) else {
274 return false;
275 };
276 let Ok(snapshot) = serde_json::from_slice::<PersistedSnapshot>(&plain) else {
277 return false;
278 };
279 snapshot.instance_id == *new_instance_id
280}
281
282pub(crate) fn rotation_already_installed(state_dir: &Path, new_key: &str) -> bool {
283 key_matches_existing_snapshot(Some(state_dir), new_key)
284 && live_admin_key_matches(state_dir, new_key)
285}
286
287fn live_admin_key_matches(state_dir: &Path, new_key: &str) -> bool {
288 let Ok(raw) = std::fs::read(state_dir.join("admin.key")) else {
289 return false;
290 };
291 let Ok(text) = std::str::from_utf8(&raw) else {
292 return false;
293 };
294 text.trim().as_bytes() == new_key.trim().as_bytes()
295}
296
297pub(crate) fn key_matches_existing_snapshot(state_dir: Option<&Path>, key: &str) -> bool {
298 let Some(state_dir) = state_dir else {
299 return false;
300 };
301 let snapshot_path = auth_snapshot_path(state_dir);
302 if !snapshot_path.exists() {
303 return false;
304 }
305 let Ok(Credential::Admin(admin_key)) = parse_credential(key) else {
306 return false;
307 };
308 let Ok(bytes) = std::fs::read(&snapshot_path) else {
309 return false;
310 };
311 open_blob(&admin_key, &bytes).is_ok()
312}
313
314pub(crate) fn key_matches_existing_state(state_dir: Option<&Path>, key: &str) -> bool {
315 if key_matches_existing_snapshot(state_dir, key) {
316 return true;
317 }
318 let Some(state_dir) = state_dir else {
319 return false;
320 };
321 if auth_snapshot_path(state_dir).exists() {
322 return false;
323 }
324 let wal_path = auth_wal_path(state_dir);
325 if !wal_path.exists() {
326 return false;
327 }
328 let Ok(Credential::Admin(admin_key)) = parse_credential(key) else {
329 return false;
330 };
331 wal_decrypts_with_key(&wal_path, &admin_key)
332}
333
334fn wal_decrypts_with_key(path: &Path, admin_key: &AesKeyType) -> bool {
335 let Ok(mut file) = File::open(path) else {
336 return false;
337 };
338 let Ok(metadata) = file.metadata() else {
339 return false;
340 };
341 if metadata.len() == 0 {
342 return true;
343 }
344 let mut length = [0_u8; 4];
345 if file.read_exact(&mut length).is_err() {
346 return false;
347 }
348 let length = u32::from_be_bytes(length) as usize;
349 if length == 0 || length > 1024 * 1024 {
350 return false;
351 }
352 let mut sealed = vec![0_u8; length];
353 if file.read_exact(&mut sealed).is_err() {
354 return false;
355 }
356 open_blob(admin_key, &sealed).is_ok()
357}
358
359fn refuse_write_if_encrypted_state(path: &Path, force: bool) -> Result<(), AuthFailure> {
360 let Some(state_dir) = path.parent() else {
364 return Ok(());
365 };
366 if !encrypted_auth_state_exists(state_dir) {
367 return Ok(());
368 }
369 Err(AuthFailure::new(
370 "administrator_key_state_exists",
371 format!(
372 "refusing to {} `{}` while encrypted auth state exists; use `pb-mapper admin root-key rotate` or `pb-mapper admin auth-state reset --confirm`",
373 if force { "replace" } else { "create" },
374 path.display()
375 ),
376 false,
377 ))
378}