1use std::path::Path;
7
8use time::OffsetDateTime;
9
10use super::{TenantError, Tenants, commit, io};
11use crate::audit::{Action, Actor, Entry};
12use crate::backend::Under;
13use crate::clock::nanos;
14use crate::envelope::{self, Context, Key, WrapLimit};
15use crate::ids::TenantId;
16use crate::store::StoreError;
17
18pub(super) const REKEY_BATCH: usize = 1000;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub(crate) enum RekeyStep {
24 Pending,
26 Batch,
28 Rewrapped,
30 Reencrypted,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub(crate) enum MasterStep {
37 Copied,
39 Rewrapped,
41 Renamed,
43}
44
45pub fn rekey_master(
53 dir: &Path,
54 old: &Key,
55 new: &Key,
56 now: OffsetDateTime,
57) -> Result<(), TenantError> {
58 rekey_master_stopping(dir, old, new, now, None)
59}
60
61pub(crate) fn rekey_master_stopping(
62 dir: &Path,
63 old: &Key,
64 new: &Key,
65 now: OffsetDateTime,
66 stop: Option<MasterStep>,
67) -> Result<(), TenantError> {
68 crate::backend::local::rekey_master(dir, old, new, now, stop)
69}
70
71impl Tenants {
72 pub fn rekey_tenant(&self, who: Actor, id: TenantId) -> Result<(), TenantError> {
75 self.rekey_in_batches(who, id, REKEY_BATCH)
76 }
77
78 pub(super) fn rekey_in_batches(
79 &self,
80 who: Actor,
81 id: TenantId,
82 batch: usize,
83 ) -> Result<(), TenantError> {
84 let entry = |status| Entry {
85 actor: who,
86 action: Action::RekeyTenant,
87 target: Some(id.to_string()),
88 version: None,
89 status,
90 };
91 let ctx = Context::Tenant { tenant: id };
92
93 let (old, new) = {
96 let mut tx = self.registry.immediate()?;
97 let Some(row) = tx.tenant(id)? else {
98 self.record(&mut tx, &entry(404))?;
99 commit(tx)?;
100 return Err(TenantError::NotFound);
101 };
102 let (wrapped, pending) = (row.wrapped, row.pending);
103 let old = envelope::unwrap(&self.master, &ctx, &wrapped).map_err(io)?;
104 let new = match pending {
105 Some(pending) => envelope::unwrap(&self.master, &ctx, &pending).map_err(io)?,
106 None => {
107 let master_id = self.master.id();
108 if envelope::wrap_limit(tx.master_wraps(master_id)?) == WrapLimit::Refuse {
109 self.record(&mut tx, &entry(503))?;
110 commit(tx)?;
111 return Err(TenantError::LimitReached);
112 }
113 tx.add_master_wrap(master_id, nanos(self.clock.now()))?;
114 let new = Key::generate().map_err(io)?;
115 let pending = envelope::wrap(&self.master, &ctx, &new);
116 tx.set_pending(id, &pending)?;
117 new
118 }
119 };
120 commit(tx)?;
121 (old, new)
122 };
123 self.stop_after(RekeyStep::Pending)?;
124
125 drop(self.drain(id)?);
128 let moved = self.move_catalog(id, old, &new, batch);
129 self.stores().remove(&id);
130 moved?;
131
132 {
135 let mut tx = self.registry.immediate()?;
136 tx.promote_pending(id, new.id(), nanos(self.clock.now()))?;
137 self.record(&mut tx, &entry(200))?;
138 commit(tx)?;
139 }
140 self.copy_to_tenant(id, &entry(200))
141 }
142
143 fn move_catalog(
147 &self,
148 id: TenantId,
149 old: Key,
150 new: &Key,
151 batch: usize,
152 ) -> Result<(), TenantError> {
153 let old_catalog = envelope::catalog_key(&old).map_err(io)?;
154 let new_catalog = envelope::catalog_key(new).map_err(io)?;
155 if self.space.catalog_under(id, &old_catalog, &new_catalog)? == Under::New {
156 return Ok(());
157 }
158 let store = self
159 .space
160 .open(id, old, true, self.clock.clone())
161 .map_err(io)?;
162 loop {
163 let done = store.rewrap(new, batch).map_err(|e| match e {
164 StoreError::LimitReached => TenantError::LimitReached,
165 other => io(other),
166 })?;
167 if done == 0 {
168 break;
169 }
170 self.stop_after(RekeyStep::Batch)?;
171 }
172 drop(store);
173 self.stop_after(RekeyStep::Rewrapped)?;
174 self.space.reseal(id, &old_catalog, &new_catalog)?;
175 self.stop_after(RekeyStep::Reencrypted)
176 }
177
178 fn stop_after(&self, step: RekeyStep) -> Result<(), TenantError> {
180 #[cfg(test)]
181 {
182 let mut stop = self.hooks.rekey_stop.lock().expect("hook");
183 if *stop == Some(step) {
184 *stop = None;
185 return Err(TenantError::Io);
186 }
187 }
188 let _ = step;
189 Ok(())
190 }
191}
192
193#[cfg(test)]
194mod rekey_tests {
195 use uuid::Uuid;
196
197 use super::super::tests::{Fixture, fixture};
198 use super::*;
199 use crate::catalog::{self, CatalogError};
200 use crate::envelope::{self, Context, Key};
201 use crate::ids::VersionId;
202 use crate::store::testing::vcon;
203 use crate::store::{Caller, Created};
204
205 type RawVersion = (Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>);
207
208 const CALLER: Caller = Caller {
209 key: crate::ids::KeyId([7; 8]),
210 };
211
212 fn tenant_with_vcons(f: &Fixture) -> (TenantId, Vec<Uuid>) {
215 let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
216 let store = f.tenants.store(id).expect("store");
217 let mut uuids = Vec::new();
218 for n in 0..5 {
219 let (found, body) = vcon(&format!("01a07da8-c2bb-83e5-b9a2-279e0d16bc4{n}"), "kept");
220 assert!(matches!(
221 store.create(&CALLER, &found, &body),
222 Ok(Created::New(_))
223 ));
224 uuids.push(found.uuid);
225 }
226 store
227 .delete(&CALLER, uuids[4], crate::retention::Retention::Years(7))
228 .expect("deleted");
229 (id, uuids)
230 }
231
232 fn all_under_the_new_key_only(f: &Fixture, id: TenantId, uuids: &[Uuid], old: &Key) {
235 let store = f.tenants.store(id).expect("store");
236 for uuid in &uuids[..4] {
237 assert!(store.get(&CALLER, *uuid).is_ok(), "{uuid} reads");
238 }
239 assert!(
240 store.legal_get(&CALLER, uuids[4], None).is_ok(),
241 "the deleted vCon's kept version reads"
242 );
243 let path = f.tenants_dir().join(id.to_string()).join("catalog.sqlite");
244 let old_catalog = envelope::catalog_key(old).expect("catalog key");
245 assert!(matches!(
246 catalog::open(&path, &old_catalog, ""),
247 Err(CatalogError::WrongKey)
248 ));
249 let new = f.tenant_key(id);
250 assert_ne!(new.id(), old.id());
251 let conn = catalog::open(&path, &envelope::catalog_key(&new).expect("key"), "")
252 .expect("opens under the new key");
253 let rows: Vec<RawVersion> = conn
254 .prepare("SELECT version, uuid, tenant_key_id, wrapped_key FROM version")
255 .expect("prepare")
256 .query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)))
257 .expect("query")
258 .collect::<Result<_, _>>()
259 .expect("rows");
260 assert_eq!(rows.len(), 5);
261 for (version, uuid, key_id, wrapped) in rows {
262 let ctx = Context::Version {
263 tenant: id,
264 uuid: Uuid::from_slice(&uuid).expect("uuid"),
265 version: VersionId(version.try_into().expect("16 bytes")),
266 };
267 assert_eq!(key_id, new.id().bytes().to_vec());
268 assert!(envelope::unwrap(&new, &ctx, &wrapped).is_ok());
269 assert!(envelope::unwrap(old, &ctx, &wrapped).is_err());
270 }
271 let pending: Option<Vec<u8>> = f
272 .conn()
273 .query_row(
274 "SELECT pending_wrapped_tenant_key FROM tenant WHERE id = ?1",
275 [id.0],
276 |r| r.get(0),
277 )
278 .expect("row");
279 assert_eq!(pending, None, "no key is left pending");
280 }
281
282 #[test]
283 fn a_rekey_moves_everything_kept_to_a_new_key_and_records_it() {
284 let f = fixture();
285 let (id, uuids) = tenant_with_vcons(&f);
286 let old = f.tenant_key(id);
287 let since_before: i64 = f
288 .conn()
289 .query_row("SELECT key_since FROM tenant WHERE id = ?1", [id.0], |r| {
290 r.get(0)
291 })
292 .expect("row");
293 f.clock.set(time::macros::datetime!(2026-10-23 13:00 UTC));
294 f.tenants.rekey_tenant(Actor::Cli, id).expect("rekeyed");
295 all_under_the_new_key_only(&f, id, &uuids, &old);
296 let since_after: i64 = f
297 .conn()
298 .query_row("SELECT key_since FROM tenant WHERE id = ?1", [id.0], |r| {
299 r.get(0)
300 })
301 .expect("row");
302 assert!(
303 since_after > since_before,
304 "key_since moves to the rotation"
305 );
306 assert_eq!(
307 since_after,
308 crate::clock::nanos(time::macros::datetime!(2026-10-23 13:00 UTC)),
309 "key_since is the rotation's own time"
310 );
311 let records = f.admin_records("rekey_tenant");
312 assert_eq!(records.len(), 1);
313 assert_eq!(records[0]["status"], 200);
314 assert_eq!(records[0]["target"], id.to_string().as_str());
315 let own = crate::audit::testing::exported(&f.store(id).conn_for_test(), None);
316 let copies: Vec<_> = own
317 .iter()
318 .filter(|r| r["action"] == "rekey_tenant")
319 .collect();
320 assert_eq!(copies.len(), 1, "the tenant's log keeps one copy");
321 assert_eq!(copies[0]["status"], 200, "the copy records the success");
322 assert_eq!(
323 copies[0]["target"],
324 id.to_string().as_str(),
325 "the copy names the tenant"
326 );
327 assert_eq!(f.master_wraps(), 2, "the new tenant key's wrap is counted");
328 }
329
330 #[test]
331 fn a_rekey_stopped_at_any_step_is_finished_by_running_it_again() {
332 for step in [
333 RekeyStep::Pending,
334 RekeyStep::Batch,
335 RekeyStep::Rewrapped,
336 RekeyStep::Reencrypted,
337 ] {
338 let f = fixture();
339 let (id, uuids) = tenant_with_vcons(&f);
340 let old = f.tenant_key(id);
341 *f.tenants.hooks.rekey_stop.lock().expect("hook") = Some(step);
342 assert_eq!(
343 f.tenants.rekey_in_batches(Actor::Cli, id, 2),
344 Err(TenantError::Io),
345 "{step:?}"
346 );
347 f.tenants
348 .rekey_in_batches(Actor::Cli, id, 2)
349 .unwrap_or_else(|e| panic!("{step:?}: {e:?}"));
350 all_under_the_new_key_only(&f, id, &uuids, &old);
351 assert_eq!(f.master_wraps(), 2, "{step:?}: one new key, made once");
352 }
353 }
354
355 mod master {
356 use std::path::{Path, PathBuf};
357 use std::sync::Arc;
358
359 use super::*;
360 use crate::clock::ManualClock;
361 use crate::fs::OsFs;
362
363 const NOW: OffsetDateTime = time::macros::datetime!(2026-10-25 12:00 UTC);
364
365 struct Dir {
366 dir: tempfile::TempDir,
367 }
368
369 impl Dir {
370 fn data(&self) -> &Path {
371 self.dir.path()
372 }
373 fn key_file(&self, name: &str) -> PathBuf {
374 self.dir.path().join(name)
375 }
376 fn key(&self, name: &str) -> Key {
378 Key::load_master(&self.key_file(name)).expect("a key file")
379 }
380 fn open(&self, name: &str) -> Result<Tenants, crate::tenants::RegistryError> {
381 Tenants::open(
382 self.data(),
383 self.key(name),
384 Arc::new(OsFs),
385 Arc::new(ManualClock::at(NOW)),
386 )
387 }
388 fn server_file(&self) -> Vec<u8> {
389 std::fs::read(self.data().join("server.sqlite")).expect("server.sqlite")
390 }
391 }
392
393 fn setup() -> (Dir, Vec<(TenantId, Uuid)>, TenantId) {
398 let d = Dir {
399 dir: tempfile::tempdir().expect("temp dir"),
400 };
401 std::fs::write(d.key_file("old"), [0x31; 32]).expect("old key");
402 std::fs::write(d.key_file("new"), [0x32; 32]).expect("new key");
403 let tenants = d.open("old").expect("opens");
404 let mut made = Vec::new();
405 for (n, name) in ["acme", "beta"].into_iter().enumerate() {
406 let id = tenants.create(Actor::Cli, name, None).expect("made");
407 let (found, body) = vcon(&format!("01a07da8-c2bb-83e5-b9a2-279e0d16bc4{n}"), name);
408 tenants
409 .store(id)
410 .expect("store")
411 .create(&CALLER, &found, &body)
412 .expect("stored");
413 made.push((id, found.uuid));
414 }
415 let pending = made[1].0;
416 *tenants.hooks.rekey_stop.lock().expect("hook") = Some(RekeyStep::Reencrypted);
417 assert_eq!(
418 tenants.rekey_tenant(Actor::Cli, pending),
419 Err(TenantError::Io)
420 );
421 drop(tenants);
422 (d, made, pending)
423 }
424
425 fn rotated(d: &Dir, made: &[(TenantId, Uuid)], pending: TenantId) {
427 assert!(
428 matches!(d.open("old"), Err(crate::tenants::RegistryError::WrongKey)),
429 "after the rotation, server.sqlite is under the new master key only"
430 );
431 let tenants = d.open("new").expect("opens under the new master key");
432 tenants
434 .rekey_tenant(Actor::Cli, pending)
435 .expect("the pending tenant key unwraps under the new master");
436 for (id, uuid) in made {
437 let store = tenants.store(*id).expect("store");
438 assert!(store.get(&CALLER, *uuid).is_ok(), "{id} reads");
439 }
440 let records = tenants.admin_records();
441 let rotations: Vec<_> = records
442 .iter()
443 .filter(|r| r["action"] == "rekey_master")
444 .collect();
445 assert_eq!(rotations.len(), 1);
446 assert_eq!(rotations[0]["status"], 200);
447 assert_eq!(rotations[0]["actor"], "cli");
448 let uses: Vec<(Vec<u8>, i64)> = tenants
449 .conn_for_test()
450 .prepare("SELECT master_key_id, wraps FROM master_key_use")
451 .expect("prepare")
452 .query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
453 .expect("query")
454 .collect::<Result<_, _>>()
455 .expect("rows");
456 assert_eq!(
457 uses,
458 [(d.key("new").id().bytes().to_vec(), 3)],
459 "two tenant keys and one pending one, wrapped under the new key only"
460 );
461 }
462
463 #[test]
464 fn a_master_rotation_moves_every_tenant_key_to_the_new_master() {
465 let (d, made, pending) = setup();
466 rekey_master(d.data(), &d.key("old"), &d.key("new"), NOW).expect("rotated");
467 rotated(&d, &made, pending);
468 }
469
470 #[test]
471 fn stopped_before_the_rename_the_old_file_is_untouched_and_a_rerun_finishes() {
472 for step in [MasterStep::Copied, MasterStep::Rewrapped] {
473 let (d, made, pending) = setup();
474 let before = d.server_file();
475 assert_eq!(
476 rekey_master_stopping(d.data(), &d.key("old"), &d.key("new"), NOW, Some(step)),
477 Err(TenantError::Io),
478 "{step:?}"
479 );
480 assert_eq!(
481 d.server_file(),
482 before,
483 "{step:?}: the old file is untouched"
484 );
485 assert!(d.open("old").is_ok(), "{step:?}: still under the old key");
486 rekey_master(d.data(), &d.key("old"), &d.key("new"), NOW).expect("rotated");
487 rotated(&d, &made, pending);
488 }
489 }
490
491 #[test]
492 fn stopped_after_the_rename_a_rerun_has_nothing_left_to_do() {
493 let (d, made, pending) = setup();
494 assert_eq!(
495 rekey_master_stopping(
496 d.data(),
497 &d.key("old"),
498 &d.key("new"),
499 NOW,
500 Some(MasterStep::Renamed)
501 ),
502 Err(TenantError::Io)
503 );
504 let after = d.server_file();
505 rekey_master(d.data(), &d.key("old"), &d.key("new"), NOW).expect("done already");
506 assert_eq!(d.server_file(), after, "nothing is rewritten");
507 rotated(&d, &made, pending);
508 }
509
510 #[test]
511 fn a_key_that_opens_neither_way_is_refused() {
512 let (d, _, _) = setup();
513 std::fs::write(d.key_file("other"), [0x33; 32]).expect("other key");
514 let before = d.server_file();
515 assert_eq!(
516 rekey_master(d.data(), &d.key("other"), &d.key("new"), NOW),
517 Err(TenantError::WrongKey)
518 );
519 assert_eq!(d.server_file(), before);
520 }
521 }
522
523 #[test]
524 fn an_unknown_tenant_is_refused_and_recorded() {
525 let f = fixture();
526 let unknown = TenantId::generate();
527 assert_eq!(
528 f.tenants.rekey_tenant(Actor::Cli, unknown),
529 Err(TenantError::NotFound)
530 );
531 assert_eq!(f.admin_records("rekey_tenant")[0]["status"], 404);
532 }
533
534 #[test]
535 fn a_master_key_at_its_wrap_limit_makes_no_new_tenant_key() {
536 let f = fixture();
537 let (id, _) = tenant_with_vcons(&f);
538 f.conn()
539 .execute("UPDATE master_key_use SET wraps = ?1", [1_i64 << 32])
540 .expect("at the limit");
541 assert_eq!(
542 f.tenants.rekey_tenant(Actor::Cli, id),
543 Err(TenantError::LimitReached)
544 );
545 assert_eq!(f.admin_records("rekey_tenant")[0]["status"], 503);
546 let pending: Option<Vec<u8>> = f
547 .conn()
548 .query_row(
549 "SELECT pending_wrapped_tenant_key FROM tenant WHERE id = ?1",
550 [id.0],
551 |r| r.get(0),
552 )
553 .expect("row");
554 assert_eq!(pending, None);
555 assert!(
556 f.tenants.store(id).is_ok(),
557 "the tenant is served as before"
558 );
559 }
560}