toolkit_zero/encryption/timelock/mod.rs
1//! Time-locked key derivation.
2//!
3//! Derives a deterministic 32-byte key from a **time value** through a
4//! three-pass heterogeneous KDF chain:
5//!
6//! | Pass | Algorithm | Role |
7//! |------|-----------|------|
8//! | 1 | **Argon2id** | PHC winner; sequential- and random-access memory-hard; GPU/ASIC-resistant |
9//! | 2 | **scrypt** | Independently designed memory-hard function (ROMix); orthogonal to Argon2id |
10//! | 3 | **Argon2id** | Extends the chain depth with fresh parameters and a distinct salt |
11//!
12//! Using two *independently designed* memory-hard functions ensures the chain
13//! remains strong even if a weakness is discovered in either algorithm.
14//! Every intermediate KDF output is zeroized from memory before the subsequent
15//! pass begins.
16//!
17//! # Two entry points
18//!
19//! | `params` argument | Path | Intended use |
20//! |-------------------------------------|---------------------|----------------------------------------------------------------------|
21//! | `params: None` (+ all other `Some`) | `_at` — encryption | Caller supplies cadence, time, precision, format, salts, and KDF parameters |
22//! | `params: Some(p)` (rest `None`) | `_now` — decryption | All settings are read from [`TimeLockParams`]; no additional input required |
23//!
24//! Async counterparts ([`timelock_async`]) are provided under the
25//! `enc-timelock-async-keygen-now` and `enc-timelock-async-keygen-input` features;
26//! they offload blocking KDF work to a dedicated thread, ensuring the calling
27//! executor is never stalled.
28//!
29//! # Time input
30//!
31//! The KDF input is a short ASCII string derived from the time value at one
32//! of three selectable precision levels.
33//!
34//! | [`TimePrecision`] | [`TimeFormat`] | Example string | Window | Candidates/day |
35//! |-------------------|----------------|----------------|----------|----------------|
36//! | `Hour` | `Hour24` | `"14"` | 60 min | 24 |
37//! | `Hour` | `Hour12` | `"02PM"` | 60 min | 12 unique × 2 |
38//! | `Quarter` | `Hour24` | `"14:30"` | 15 min | 96 |
39//! | `Quarter` | `Hour12` | `"02:30PM"` | 15 min | 48 unique × 2 |
40//! | `Minute` | `Hour24` | `"14:37"` | 1 min | 1440 |
41//! | `Minute` | `Hour12` | `"02:37PM"` | 1 min | 720 unique × 2 |
42//!
43//! > **`Hour12` note**: the same time slot recurs twice daily (AM + PM),
44//! > making the derived key valid twice per day. Use `Hour24` for a key
45//! > that is uniquely valid once per day.
46//!
47//! > **Clock skew (`Minute` precision):** if both parties' clocks may diverge
48//! > by up to one minute, derive keys for `now() − 1 min`, `now()`, and
49//! > `now() + 1 min` and try each in turn. The additional cost is negligible
50//! > relative to a single full KDF pass.
51//!
52//! # Salts
53//!
54//! [`TimeLockSalts`] holds three independent 32-byte random values — one per
55//! KDF pass — generated at encryption time via [`TimeLockSalts::generate`].
56//! Salts are **not secret**; they prevent precomputation attacks and must be
57//! stored in plaintext alongside the ciphertext header. The identical salts
58//! must be provided to the decryption call.
59//!
60//! # Memory safety
61//!
62//! All intermediate KDF outputs are wrapped in [`Zeroizing`] and overwritten
63//! upon being dropped. [`TimeLockKey`] implements [`ZeroizeOnDrop`]; the final
64//! 32-byte key material is scrubbed from memory the moment it goes out of scope.
65//!
66//! # Quick start
67//!
68//! ```no_run
69//! use toolkit_zero::encryption::timelock::*;
70//!
71//! // ── Encryption side ───────────────────────────────────────────────────
72//! let salts = TimeLockSalts::generate();
73//! let kdf = KdfPreset::Balanced.params();
74//! let lock_time = TimeLockTime::new(14, 37).unwrap();
75//!
76//! // Derive the encryption key (params = None → _at path).
77//! let enc_key = timelock(
78//! Some(TimeLockCadence::None),
79//! Some(lock_time),
80//! Some(TimePrecision::Minute),
81//! Some(TimeFormat::Hour24),
82//! Some(salts.clone()),
83//! Some(kdf),
84//! None,
85//! ).unwrap();
86//!
87//! // Pack all settings into a header and store alongside the ciphertext.
88//! let header = pack(TimePrecision::Minute, TimeFormat::Hour24,
89//! &TimeLockCadence::None, salts, kdf);
90//!
91//! // ── Decryption side ───────────────────────────────────────────────────
92//! // Load header from ciphertext (params = Some → _now path).
93//! // Call at 14:37 local time:
94//! let dec_key = timelock(
95//! None, None, None, None, None, None,
96//! Some(header),
97//! ).unwrap();
98//! // enc_key.as_bytes() == dec_key.as_bytes() when called at 14:37 local time
99//! ```
100
101#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
102mod helper;
103
104#[cfg(feature = "backend-deps")]
105pub mod backend_deps;
106
107#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
108pub mod utility;
109#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
110pub use utility::{TimeLockParams, pack, unpack};
111
112#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
113use zeroize::{Zeroize, ZeroizeOnDrop};
114
115// ─── time precision / format ──────────────────────────────────────────────────
116
117/// The granularity at which the time value is quantised when constructing the
118/// KDF input string.
119///
120/// Coarser precision yields a longer validity window, making it easier for a
121/// legitimate user to produce the correct key; finer precision increases the
122/// cost of time-sweeping attacks.
123#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum TimePrecision {
126 /// Quantise to the current **hour**.
127 ///
128 /// Input example: `"14"` (24-hour) | `"02PM"` (12-hour).
129 /// The derived key is valid for the entire 60-minute block.
130 Hour,
131
132 /// Quantise to the current **15-minute block** (minute snapped to
133 /// 00, 15, 30, or 45).
134 ///
135 /// Input example: `"14:30"` (24-hour) | `"02:30PM"` (12-hour).
136 /// The derived key is valid for the 15-minute interval enclosing the
137 /// chosen minute.
138 Quarter,
139
140 /// Quantise to the current **minute** (1-minute validity window).
141 ///
142 /// Input example: `"14:37"` (24-hour) | `"02:37PM"` (12-hour).
143 /// The strongest temporal constraint available — both parties' clocks must
144 /// be NTP-synchronised to within ±30 seconds.
145 Minute,
146}
147
148/// Clock representation used when formatting the time input string.
149#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum TimeFormat {
152 /// 24-hour clock (`00`–`23`). Every time slot is unique within a day.
153 Hour24,
154
155 /// 12-hour clock (`01`–`12`) with an `AM`/`PM` suffix.
156 /// Each time slot recurs **twice daily**.
157 Hour12,
158}
159
160#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
161impl Default for TimeFormat {
162 /// Default clock representation is 24-hour (`Hour24`).
163 fn default() -> Self { Self::Hour24 }
164}
165
166#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
167impl Default for TimePrecision {
168 /// Default precision is per-minute (`Minute`).
169 fn default() -> Self { Self::Minute }
170}
171
172// ─── schedule cadence ────────────────────────────────────────────────────────
173
174/// Day of the week, Monday-indexed (Mon = 0 … Sun = 6).
175///
176/// Used as a cadence component in [`TimeLockCadence`] to constrain key
177/// derivation to a specific weekday.
178#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum Weekday {
181 Monday,
182 Tuesday,
183 Wednesday,
184 Thursday,
185 Friday,
186 Saturday,
187 Sunday,
188}
189
190#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
191impl Weekday {
192 /// The full English name of this weekday (e.g. `"Tuesday"`).
193 pub fn name(self) -> &'static str {
194 match self {
195 Self::Monday => "Monday",
196 Self::Tuesday => "Tuesday",
197 Self::Wednesday => "Wednesday",
198 Self::Thursday => "Thursday",
199 Self::Friday => "Friday",
200 Self::Saturday => "Saturday",
201 Self::Sunday => "Sunday",
202 }
203 }
204
205 /// Zero-based weekday number (Monday = 0, …, Sunday = 6).
206 pub fn number(self) -> u8 {
207 match self {
208 Self::Monday => 0,
209 Self::Tuesday => 1,
210 Self::Wednesday => 2,
211 Self::Thursday => 3,
212 Self::Friday => 4,
213 Self::Saturday => 5,
214 Self::Sunday => 6,
215 }
216 }
217
218 /// Convert from a `chrono::Weekday` value (used by the `_now` derivation path).
219 #[cfg(feature = "enc-timelock-keygen-now")]
220 pub(crate) fn from_chrono(w: chrono::Weekday) -> Self {
221 match w {
222 chrono::Weekday::Mon => Self::Monday,
223 chrono::Weekday::Tue => Self::Tuesday,
224 chrono::Weekday::Wed => Self::Wednesday,
225 chrono::Weekday::Thu => Self::Thursday,
226 chrono::Weekday::Fri => Self::Friday,
227 chrono::Weekday::Sat => Self::Saturday,
228 chrono::Weekday::Sun => Self::Sunday,
229 }
230 }
231}
232
233/// Calendar month (January = 1 … December = 12).
234///
235/// Used as a cadence component in [`TimeLockCadence`] to constrain key
236/// derivation to a specific month of the year.
237#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum Month {
240 January,
241 February,
242 March,
243 April,
244 May,
245 June,
246 July,
247 August,
248 September,
249 October,
250 November,
251 December,
252}
253
254#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
255impl Month {
256 /// The full English name of this month (e.g. `"February"`).
257 pub fn name(self) -> &'static str {
258 match self {
259 Self::January => "January",
260 Self::February => "February",
261 Self::March => "March",
262 Self::April => "April",
263 Self::May => "May",
264 Self::June => "June",
265 Self::July => "July",
266 Self::August => "August",
267 Self::September => "September",
268 Self::October => "October",
269 Self::November => "November",
270 Self::December => "December",
271 }
272 }
273
274 /// 1-based month number (January = 1, …, December = 12).
275 pub fn number(self) -> u8 {
276 match self {
277 Self::January => 1,
278 Self::February => 2,
279 Self::March => 3,
280 Self::April => 4,
281 Self::May => 5,
282 Self::June => 6,
283 Self::July => 7,
284 Self::August => 8,
285 Self::September => 9,
286 Self::October => 10,
287 Self::November => 11,
288 Self::December => 12,
289 }
290 }
291
292 /// Maximum day count for this month.
293 ///
294 /// February is defined as 28 days; leap years are intentionally not
295 /// accounted for to keep the cadence policy stable across years.
296 pub fn max_days(self) -> u8 {
297 match self {
298 Self::February => 28,
299 Self::April | Self::June | Self::September | Self::November => 30,
300 _ => 31,
301 }
302 }
303
304 /// Construct from a 1-based month number (1 = January … 12 = December).
305 ///
306 /// # Panics
307 ///
308 /// Panics if `n` is outside 1–12.
309 #[cfg(feature = "enc-timelock-keygen-now")]
310 pub(crate) fn from_number(n: u8) -> Self {
311 match n {
312 1 => Self::January,
313 2 => Self::February,
314 3 => Self::March,
315 4 => Self::April,
316 5 => Self::May,
317 6 => Self::June,
318 7 => Self::July,
319 8 => Self::August,
320 9 => Self::September,
321 10 => Self::October,
322 11 => Self::November,
323 12 => Self::December,
324 _ => panic!("Month::from_number: invalid month number {}", n),
325 }
326 }
327}
328
329/// Calendar cadence for a scheduled time-lock — constrains key derivation to
330/// a recurring calendar pattern **in addition to** the time-of-day window.
331///
332/// Combine with a [`TimeLockTime`] on the encryption path to express policies
333/// such as:
334///
335/// - *"valid only on Tuesdays at 18:00"* — `DayOfWeek(Weekday::Tuesday)` + 18 h
336/// - *"valid only on the 1st of each month at 00:00"* — `DayOfMonth(1)` + 0 h
337/// - *"valid only on Tuesdays in February at 06:00"* — `DayOfWeekInMonth(Weekday::Tuesday, Month::February)` + 6 h
338///
339/// On the decryption side, pass the cadence to [`pack`] (along with precision
340/// and format) to obtain a [`TimeLockParams`] for storage in the ciphertext
341/// header.
342///
343/// `TimeLockCadence::None` is equivalent to a call without any calendar
344/// constraint — no calendar dimension is incorporated into the KDF input.
345///
346/// # Panics
347///
348/// Constructing [`DayOfMonthInMonth`](TimeLockCadence::DayOfMonthInMonth) is
349/// always valid, but **key derivation panics** if the stored day exceeds the
350/// month's maximum (for example, day 29 for February or day 31 for April).
351#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum TimeLockCadence {
354 /// No calendar constraint — behaves like a plain time-lock.
355 ///
356 /// Compact discriminant: `0`.
357 None,
358
359 /// Valid only on the specified weekday.
360 ///
361 /// Compact discriminant: `1`.
362 DayOfWeek(Weekday),
363
364 /// Valid only on the specified day of any month (1–31).
365 ///
366 /// Days 29–31 simply never match in shorter months.
367 ///
368 /// Compact discriminant: `2`.
369 DayOfMonth(u8),
370
371 /// Valid only during the specified month of any year.
372 ///
373 /// Compact discriminant: `3`.
374 MonthOfYear(Month),
375
376 /// Valid only on the specified weekday **and** during the specified month.
377 ///
378 /// Compact discriminant: `4`.
379 DayOfWeekInMonth(Weekday, Month),
380
381 /// Valid only on the specified day of the specified month.
382 ///
383 /// Key derivation panics if the day exceeds the month's maximum.
384 ///
385 /// Compact discriminant: `5`.
386 DayOfMonthInMonth(u8, Month),
387
388 /// Valid only on the specified weekday **and** the specified day of month.
389 ///
390 /// Days 29–31 do not match in shorter months.
391 ///
392 /// Compact discriminant: `6`.
393 DayOfWeekAndDayOfMonth(Weekday, u8),
394}
395
396#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
397impl Default for TimeLockCadence {
398 /// Default cadence is `None` (no calendar constraint).
399 fn default() -> Self { Self::None }
400}
401
402#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
403impl TimeLockCadence {
404 /// Returns the compact variant discriminant stored in
405 /// [`TimeLockParams::cadence_variant`].
406 pub fn variant_id(self) -> u8 {
407 match self {
408 Self::None => 0,
409 Self::DayOfWeek(_) => 1,
410 Self::DayOfMonth(_) => 2,
411 Self::MonthOfYear(_) => 3,
412 Self::DayOfWeekInMonth(_, _) => 4,
413 Self::DayOfMonthInMonth(_, _) => 5,
414 Self::DayOfWeekAndDayOfMonth(_, _) => 6,
415 }
416 }
417
418 /// Produces the cadence prefix baked into the KDF input during the
419 /// encryption (`_at`) path.
420 ///
421 /// The prefix is empty for `None`; otherwise it is `"<component>|"` or
422 /// `"<a>+<b>|"` for composite variants.
423 ///
424 /// # Panics
425 ///
426 /// Panics if `DayOfMonthInMonth(day, month)` has `day > month.max_days()`.
427 pub(crate) fn bake_string(self) -> String {
428 match self {
429 Self::None => String::new(),
430 Self::DayOfWeek(w) => format!("{}|", w.name()),
431 Self::DayOfMonth(d) => format!("{}|", d),
432 Self::MonthOfYear(m) => format!("{}|", m.name()),
433 Self::DayOfWeekInMonth(w, m) => format!("{}+{}|", w.name(), m.name()),
434 Self::DayOfMonthInMonth(d, m) => {
435 let max = m.max_days();
436 if d < 1 || d > max {
437 panic!(
438 "TimeLockCadence::DayOfMonthInMonth: day {} is out of range \
439 1–{} for {}",
440 d, max, m.name()
441 );
442 }
443 format!("{}+{}|", d, m.name())
444 }
445 Self::DayOfWeekAndDayOfMonth(w, d) => format!("{}+{}|", w.name(), d),
446 }
447 }
448}
449
450// ─── explicit time input ──────────────────────────────────────────────────────
451
452/// An explicit time value supplied by the caller for encryption-time key
453/// derivation.
454///
455/// `hour` is always expressed in **24-hour notation** (0–23) regardless of
456/// the [`TimeFormat`] chosen for the KDF string — the format flag only
457/// controls how the string is rendered, not how you supply the input.
458///
459/// # Example
460///
461/// ```
462/// use toolkit_zero::encryption::timelock::TimeLockTime;
463///
464/// let t = TimeLockTime::new(14, 37).unwrap(); // 14:37 local (2:37 PM)
465/// ```
466#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub struct TimeLockTime {
469 hour: u32, // 0–23
470 minute: u32, // 0–59
471}
472
473#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
474impl TimeLockTime {
475 /// Construct a `TimeLockTime` from a 24-hour `hour` (0–23) and `minute`
476 /// (0–59).
477 ///
478 /// Returns `None` if either value is out of range.
479 pub fn new(hour: u32, minute: u32) -> Option<Self> {
480 if hour > 23 || minute > 59 {
481 return None;
482 }
483 Some(Self { hour, minute })
484 }
485
486 /// The hour component (0–23).
487 #[inline]
488 pub fn hour(self) -> u32 { self.hour }
489
490 /// The minute component (0–59).
491 #[inline]
492 pub fn minute(self) -> u32 { self.minute }
493}
494
495// ─── KDF parameters ───────────────────────────────────────────────────────────
496
497/// Argon2id parameters for one pass of the KDF chain.
498#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub struct Argon2PassParams {
501 /// Memory usage in **KiB** (e.g. `131_072` = 128 MiB).
502 pub m_cost: u32,
503 /// Number of passes over memory (time cost).
504 pub t_cost: u32,
505 /// Degree of parallelism (lanes). Keep at `1` for single-threaded use.
506 pub p_cost: u32,
507}
508
509/// scrypt parameters for the second pass of the KDF chain.
510#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
511#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512pub struct ScryptPassParams {
513 /// CPU/memory cost exponent: `N = 2^log_n`. Each increment doubles memory.
514 pub log_n: u8,
515 /// Block size (`r`). Standard value is `8`.
516 pub r: u32,
517 /// Parallelization factor (`p`). Keep at `1` for sequential derivation.
518 pub p: u32,
519}
520
521/// Combined parameters for the full three-pass
522/// Argon2id → scrypt → Argon2id KDF chain.
523///
524/// Prefer constructing via [`KdfPreset::params`] unless you have specific
525/// tuning requirements. All fields implement `Copy`, so this struct can be
526/// stored inline in [`KdfPreset::Custom`].
527#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub struct KdfParams {
530 /// First pass: Argon2id.
531 pub pass1: Argon2PassParams,
532 /// Second pass: scrypt.
533 pub pass2: ScryptPassParams,
534 /// Third pass: Argon2id (different parameters and a distinct salt).
535 pub pass3: Argon2PassParams,
536}
537
538// ─── presets ──────────────────────────────────────────────────────────────────
539
540/// Pre-tuned [`KdfParams`] sets.
541///
542/// Pick the variant that matches your **target platform** and security goal.
543/// Use [`Custom`](KdfPreset::Custom) to supply entirely your own parameters.
544///
545/// > **Why device-specific presets?** Apple Silicon has exceptional memory
546/// > bandwidth (unified memory, ~400 GB/s on M2). The same parameters that
547/// > take 2 seconds on an M2 may take 15+ seconds on a typical x86-64 server.
548/// > Device-specific variants let you choose a cost that is _consistent_ across
549/// > the hardware you actually deploy on.
550///
551/// ## Generic (cross-platform)
552///
553/// Suitable for any platform. Use these when you don't know or don't control
554/// the target hardware.
555///
556/// | Preset | Peak RAM | Est. Mac M2 | Est. x86-64 |
557/// |------------|-----------|-------------|--------------|
558/// | `Fast` | ~128 MiB | ~500 ms | ~1.5 s |
559/// | `Balanced` | ~512 MiB | ~2 s | ~8–15 s |
560/// | `Paranoid` | ~768 MiB | ~4–6 s | ~20–30 s |
561///
562/// ## Apple Silicon (`*Mac`)
563///
564/// Harder parameters calibrated for Apple Silicon's superior memory bandwidth.
565/// All three tiers assume at least 8 GiB unified memory (all M-series chips).
566///
567/// | Preset | Peak RAM | Est. Mac M2 | Est. Mac M3/M4 |
568/// |---------------|----------|--------------|----------------|
569/// | `FastMac` | ~512 MiB | ~2 s | faster |
570/// | `BalancedMac` | ~1 GiB | ~5–12 s | faster |
571/// | `ParanoidMac` | ~3 GiB | ~30–60 s | faster |
572///
573/// ## x86-64 (`*X86`)
574///
575/// Equivalent to Generic; provided as explicit named variants so code
576/// documents intent clearly.
577///
578/// | Preset | Peak RAM | Est. x86-64 |
579/// |---------------|-----------|------------------|
580/// | `FastX86` | ~128 MiB | ~1.5 s |
581/// | `BalancedX86` | ~512 MiB | ~8–15 s |
582/// | `ParanoidX86` | ~768 MiB | ~20–30 s |
583///
584/// ## Linux ARM64 (`*Arm`)
585///
586/// Tuned for AWS Graviton3 / similar high-end ARM servers. Raspberry Pi and
587/// lower-end ARM boards will be slower.
588///
589/// | Preset | Peak RAM | Est. Graviton3 |
590/// |---------------|-----------|----------------|
591/// | `FastArm` | ~256 MiB | ~3 s |
592/// | `BalancedArm` | ~512 MiB | ~10–20 s |
593/// | `ParanoidArm` | ~768 MiB | ~30–50 s |
594///
595/// ## Custom
596///
597/// `Custom(KdfParams)` lets you supply exactly the parameters you measured
598/// and tuned for your own hardware.
599#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
601pub enum KdfPreset {
602 // ── generic (cross-platform) ─────────────────────────────────────────────
603
604 /// ~128 MiB · scrypt 2¹⁶ · ~64 MiB, 3 iters each.
605 Fast,
606 /// ~512 MiB · scrypt 2¹⁷ · ~256 MiB, 4 iters each.
607 Balanced,
608 /// ~768 MiB · scrypt 2¹⁸ · ~512 MiB, 5 iters each.
609 Paranoid,
610
611 // ── Apple Silicon ─────────────────────────────────────────────────────────
612
613 /// Dev / CI on macOS. ~512 MiB · scrypt 2¹⁷ · ~256 MiB, 4 iters each.
614 FastMac,
615 /// Production on macOS (Apple Silicon). ~1 GiB · scrypt 2¹⁸ · ~512 MiB, 4 iters each.
616 BalancedMac,
617 /// Maximum security on macOS. ~3 GiB · scrypt 2²⁰ · ~1 GiB, 4 iters each.
618 /// Assumes 8+ GiB unified memory (all M-series chips).
619 ParanoidMac,
620
621 // ── x86-64 ───────────────────────────────────────────────────────────────
622
623 /// Dev / CI on x86-64. Same params as `Fast`.
624 FastX86,
625 /// Production on x86-64. Same params as `Balanced`.
626 BalancedX86,
627 /// Maximum security on x86-64. Same params as `Paranoid`.
628 ParanoidX86,
629
630 // ── Linux ARM64 ──────────────────────────────────────────────────────────
631
632 /// Dev / CI on Linux ARM64. ~256 MiB · scrypt 2¹⁶ · ~128 MiB, 3 iters each.
633 FastArm,
634 /// Production on Linux ARM64. ~512 MiB · scrypt 2¹⁷ · ~256 MiB, 5 iters each.
635 BalancedArm,
636 /// Maximum security on Linux ARM64. ~768 MiB · scrypt 2¹⁸ · ~512 MiB, 5 iters each.
637 ParanoidArm,
638
639 // ── custom ────────────────────────────────────────────────────────────────
640
641 /// Fully user-defined parameters. Use when you have measured and tuned
642 /// KDF cost on your own hardware.
643 ///
644 /// # Example
645 ///
646 /// ```no_run
647 /// # #[cfg(feature = "enc-timelock-keygen-input")]
648 /// # {
649 /// use toolkit_zero::encryption::timelock::*;
650 /// let p = KdfPreset::Custom(KdfParams {
651 /// pass1: Argon2PassParams { m_cost: 262_144, t_cost: 3, p_cost: 1 },
652 /// pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
653 /// pass3: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
654 /// });
655 /// # }
656 /// ```
657 Custom(KdfParams),
658}
659
660#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
661impl KdfPreset {
662 /// Return the [`KdfParams`] for this preset.
663 pub fn params(self) -> KdfParams {
664 // Fast = ~128 MiB · scrypt 2¹⁶ · ~64 MiB
665 let fast = KdfParams {
666 pass1: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
667 pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
668 pass3: Argon2PassParams { m_cost: 65_536, t_cost: 3, p_cost: 1 },
669 };
670 // Balanced = ~512 MiB · scrypt 2¹⁷ · ~256 MiB
671 let balanced = KdfParams {
672 pass1: Argon2PassParams { m_cost: 524_288, t_cost: 4, p_cost: 1 },
673 pass2: ScryptPassParams { log_n: 17, r: 8, p: 1 },
674 pass3: Argon2PassParams { m_cost: 262_144, t_cost: 4, p_cost: 1 },
675 };
676 // Paranoid = ~768 MiB · scrypt 2¹⁸ · ~512 MiB
677 let paranoid = KdfParams {
678 pass1: Argon2PassParams { m_cost: 786_432, t_cost: 5, p_cost: 1 },
679 pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
680 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 },
681 };
682
683 match self {
684 // Generic / x86-64 (identical params, named for code clarity)
685 KdfPreset::Fast | KdfPreset::FastX86 => fast,
686 KdfPreset::Balanced | KdfPreset::BalancedX86 => balanced,
687 KdfPreset::Paranoid | KdfPreset::ParanoidX86 => paranoid,
688 // Apple Silicon — calibrated for M-series memory bandwidth
689 KdfPreset::FastMac => balanced, // ~512 MiB
690 KdfPreset::BalancedMac => KdfParams {
691 pass1: Argon2PassParams { m_cost: 1_048_576, t_cost: 4, p_cost: 1 }, // 1 GiB
692 pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
693 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 4, p_cost: 1 },
694 },
695 KdfPreset::ParanoidMac => KdfParams {
696 pass1: Argon2PassParams { m_cost: 3_145_728, t_cost: 4, p_cost: 1 }, // 3 GiB
697 pass2: ScryptPassParams { log_n: 20, r: 8, p: 1 },
698 pass3: Argon2PassParams { m_cost: 1_048_576, t_cost: 4, p_cost: 1 }, // 1 GiB
699 },
700 // Linux ARM64
701 KdfPreset::FastArm => KdfParams {
702 pass1: Argon2PassParams { m_cost: 262_144, t_cost: 3, p_cost: 1 }, // 256 MiB
703 pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
704 pass3: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
705 },
706 KdfPreset::BalancedArm => KdfParams {
707 pass1: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 }, // 512 MiB
708 pass2: ScryptPassParams { log_n: 17, r: 8, p: 1 },
709 pass3: Argon2PassParams { m_cost: 262_144, t_cost: 5, p_cost: 1 },
710 },
711 KdfPreset::ParanoidArm => KdfParams {
712 pass1: Argon2PassParams { m_cost: 786_432, t_cost: 5, p_cost: 1 }, // 768 MiB
713 pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
714 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 },
715 },
716 // Custom
717 KdfPreset::Custom(p) => p,
718 }
719 }
720}
721
722// ─── salts ────────────────────────────────────────────────────────────────────
723
724/// Three independent 32-byte random salts — one per KDF pass.
725///
726/// Generate once at **encryption time** via [`TimeLockSalts::generate`] and
727/// store 96 bytes in the ciphertext header. The same `TimeLockSalts` **must**
728/// be supplied to [`derive_key_now`] / [`derive_key_at`] at decryption time.
729///
730/// Salts are **not secret** — they only prevent precomputation attacks.
731/// All three fields are zeroized when this value is dropped.
732#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
733#[derive(Debug, Clone)]
734pub struct TimeLockSalts {
735 /// Salt for the first Argon2id pass.
736 pub s1: [u8; 32],
737 /// Salt for the scrypt pass.
738 pub s2: [u8; 32],
739 /// Salt for the final Argon2id pass.
740 pub s3: [u8; 32],
741}
742
743#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
744impl TimeLockSalts {
745 /// Generate three independent 32-byte salts from the OS CSPRNG.
746 pub fn generate() -> Self {
747 use rand::RngCore as _;
748 let mut rng = rand::rng();
749 let mut s = Self { s1: [0u8; 32], s2: [0u8; 32], s3: [0u8; 32] };
750 rng.fill_bytes(&mut s.s1);
751 rng.fill_bytes(&mut s.s2);
752 rng.fill_bytes(&mut s.s3);
753 s
754 }
755
756 /// Construct from raw bytes (e.g. when loading from a ciphertext header).
757 pub fn from_bytes(s1: [u8; 32], s2: [u8; 32], s3: [u8; 32]) -> Self {
758 Self { s1, s2, s3 }
759 }
760
761 /// Serialize to 96 contiguous bytes (`s1 ∥ s2 ∥ s3`) for header storage.
762 pub fn to_bytes(&self) -> [u8; 96] {
763 let mut out = [0u8; 96];
764 out[..32].copy_from_slice(&self.s1);
765 out[32..64].copy_from_slice(&self.s2);
766 out[64..].copy_from_slice(&self.s3);
767 out
768 }
769
770 /// Deserialize from 96 contiguous bytes produced by [`to_bytes`].
771 pub fn from_slice(b: &[u8; 96]) -> Self {
772 let mut s1 = [0u8; 32]; s1.copy_from_slice(&b[..32]);
773 let mut s2 = [0u8; 32]; s2.copy_from_slice(&b[32..64]);
774 let mut s3 = [0u8; 32]; s3.copy_from_slice(&b[64..]);
775 Self { s1, s2, s3 }
776 }
777}
778
779#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
780impl Zeroize for TimeLockSalts {
781 fn zeroize(&mut self) {
782 self.s1.zeroize();
783 self.s2.zeroize();
784 self.s3.zeroize();
785 }
786}
787
788#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
789impl Drop for TimeLockSalts {
790 fn drop(&mut self) { self.zeroize(); }
791}
792
793// ─── output ───────────────────────────────────────────────────────────────────
794
795/// A derived 32-byte time-locked key.
796///
797/// The inner bytes are **automatically overwritten** (`ZeroizeOnDrop`) the
798/// moment this value is dropped. Access the key via [`as_bytes`](Self::as_bytes).
799#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
800pub struct TimeLockKey([u8; 32]);
801
802#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
803impl TimeLockKey {
804 /// Borrow the raw 32-byte key.
805 ///
806 /// The reference is valid only while this `TimeLockKey` is alive. If you
807 /// must copy the bytes into another buffer, protect it with [`Zeroize`] too.
808 #[inline]
809 pub fn as_bytes(&self) -> &[u8; 32] { &self.0 }
810}
811
812#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
813impl Zeroize for TimeLockKey {
814 #[inline]
815 fn zeroize(&mut self) { self.0.zeroize(); }
816}
817
818#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
819impl ZeroizeOnDrop for TimeLockKey {}
820
821#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
822impl Drop for TimeLockKey {
823 fn drop(&mut self) { self.zeroize(); }
824}
825
826// ─── error ────────────────────────────────────────────────────────────────────
827
828/// Errors returned by the `derive_key_*` functions.
829#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
830#[derive(Debug)]
831pub enum TimeLockError {
832 /// An Argon2id pass failed (invalid parameters or internal error).
833 Argon2(String),
834 /// The scrypt pass failed (invalid parameters or output length).
835 Scrypt(String),
836 /// The OS clock returned an unusable value.
837 #[cfg(feature = "enc-timelock-keygen-now")]
838 ClockUnavailable,
839 /// A [`TimeLockTime`] field was out of range.
840 #[cfg(feature = "enc-timelock-keygen-input")]
841 InvalidTime(String),
842 /// The async task panicked inside `spawn_blocking`.
843 #[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
844 TaskPanic(String),
845 /// The caller passed `Some(time)` but `enc-timelock-keygen-input` is not
846 /// active, or passed `None` but `enc-timelock-keygen-now` is not active.
847 ForbiddenAction(&'static str),
848}
849
850#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
851impl std::fmt::Display for TimeLockError {
852 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
853 match self {
854 Self::Argon2(s) => write!(f, "Argon2id error: {s}"),
855 Self::Scrypt(s) => write!(f, "scrypt error: {s}"),
856 #[cfg(feature = "enc-timelock-keygen-now")]
857 Self::ClockUnavailable => write!(f, "system clock unavailable"),
858 #[cfg(feature = "enc-timelock-keygen-input")]
859 Self::InvalidTime(s) => write!(f, "invalid time input: {s}"),
860 #[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
861 Self::TaskPanic(s) => write!(f, "KDF task panicked: {s}"),
862 Self::ForbiddenAction(s) => write!(f, "action not permitted: {s}"),
863 }
864 }
865}
866
867#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
868impl std::error::Error for TimeLockError {}
869
870// ─── internal sync API ───────────────────────────────────────────────────────
871
872/// Derive a 32-byte key from the **current system time** (decryption path).
873///
874/// The OS wall clock is read inside this call — the caller supplies no time
875/// value. Use this on the **decryption side** so the user never needs to
876/// re-enter the unlock time.
877///
878/// The `precision`, `format`, and `salts` must match exactly what was used
879/// during [`derive_key_at`] at encryption time (store them in the header).
880///
881/// # Errors
882///
883/// Returns [`TimeLockError`] if the system clock is unavailable or any KDF
884/// pass fails.
885///
886/// # Example
887///
888/// ```ignore
889/// # use toolkit_zero::encryption::timelock::*;
890/// let salts = TimeLockSalts::generate();
891/// let key = timelock(
892/// TimeLockCadence::None,
893/// None,
894/// TimePrecision::Minute,
895/// TimeFormat::Hour24,
896/// &salts,
897/// &KdfPreset::Balanced.params(),
898/// ).unwrap();
899/// ```
900#[allow(dead_code)]
901#[cfg(feature = "enc-timelock-keygen-now")]
902fn derive_key_now(
903 precision: TimePrecision,
904 format: TimeFormat,
905 salts: &TimeLockSalts,
906 params: &KdfParams,
907) -> Result<TimeLockKey, TimeLockError> {
908 let time_str = helper::format_time_now(precision, format)?;
909 helper::run_kdf_chain(time_str.into_bytes(), salts, params)
910}
911
912/// Derive a 32-byte key from an **explicit [`TimeLockTime`]** (encryption path).
913///
914/// The caller supplies the time at which decryption should be permitted.
915/// Use this on the **encryption side** — the user chooses `(hour, minute)` and
916/// the result is the key that will only be reproducible by [`derive_key_now`]
917/// called within the matching time window.
918///
919/// # Errors
920///
921/// Returns [`TimeLockError`] if the time value is invalid or any KDF pass
922/// fails.
923///
924/// # Example
925///
926/// ```ignore
927/// # use toolkit_zero::encryption::timelock::*;
928/// let salts = TimeLockSalts::generate();
929/// let at = TimeLockTime::new(14, 37).unwrap();
930/// let key = timelock(
931/// TimeLockCadence::None,
932/// Some(at),
933/// TimePrecision::Minute,
934/// TimeFormat::Hour24,
935/// &salts,
936/// &KdfPreset::Balanced.params(),
937/// ).unwrap();
938/// ```
939#[allow(dead_code)]
940#[cfg(feature = "enc-timelock-keygen-input")]
941fn derive_key_at(
942 time: TimeLockTime,
943 precision: TimePrecision,
944 format: TimeFormat,
945 salts: &TimeLockSalts,
946 params: &KdfParams,
947) -> Result<TimeLockKey, TimeLockError> {
948 let time_str = helper::format_time_at(time, precision, format)?;
949 helper::run_kdf_chain(time_str.into_bytes(), salts, params)
950}
951
952// ─── internal async API ──────────────────────────────────────────────────────
953
954/// Async variant of [`derive_key_now`].
955///
956/// Offloads the blocking Argon2id + scrypt work to a Tokio blocking thread
957/// so the calling future's executor is never stalled during derivation.
958///
959/// Takes `salts` and `params` by **value** (required for `'static` move into
960/// `spawn_blocking`); both are zeroized before the async task exits.
961///
962/// Requires the `enc-timelock-async` feature.
963///
964/// # Errors
965///
966/// Returns [`TimeLockError`] if the system clock is unavailable, any KDF
967/// pass fails, or the spawned task panics.
968#[allow(dead_code)]
969#[cfg(feature = "enc-timelock-async-keygen-now")]
970async fn derive_key_now_async(
971 precision: TimePrecision,
972 format: TimeFormat,
973 salts: TimeLockSalts,
974 params: KdfParams,
975) -> Result<TimeLockKey, TimeLockError> {
976 tokio::task::spawn_blocking(move || derive_key_now(precision, format, &salts, ¶ms))
977 .await
978 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
979}
980
981/// Async variant of [`derive_key_at`].
982///
983/// Offloads the blocking Argon2id + scrypt work to a Tokio blocking thread.
984/// Takes `salts` and `params` by **value**; both are zeroized on drop.
985///
986/// Requires the `enc-timelock-async-keygen-input` feature.
987#[allow(dead_code)]
988#[cfg(feature = "enc-timelock-async-keygen-input")]
989async fn derive_key_at_async(
990 time: TimeLockTime,
991 precision: TimePrecision,
992 format: TimeFormat,
993 salts: TimeLockSalts,
994 params: KdfParams,
995) -> Result<TimeLockKey, TimeLockError> {
996 tokio::task::spawn_blocking(move || derive_key_at(time, precision, format, &salts, ¶ms))
997 .await
998 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
999}
1000
1001// ─── internal scheduled sync API ─────────────────────────────────────────────
1002
1003/// Derive a 32-byte key from a [`TimeLockCadence`] anchor plus an **explicit
1004/// [`TimeLockTime`]** (encryption path).
1005///
1006/// Extends [`derive_key_at`] with a calendar constraint. The KDF input
1007/// is `"<cadence_prefix><time_string>"`. For [`TimeLockCadence::None`] the
1008/// prefix is empty, producing a result identical to [`derive_key_at`].
1009///
1010/// Store [`pack`]ed settings alongside the salts in the ciphertext header so
1011/// the decryption side can reconstruct the correct KDF input via
1012/// [`derive_key_scheduled_now`].
1013///
1014/// # Panics
1015///
1016/// Panics if `cadence` is [`TimeLockCadence::DayOfMonthInMonth`] with a day
1017/// that exceeds the month's maximum (e.g. day 29 for February).
1018///
1019/// # Errors
1020///
1021/// Returns [`TimeLockError`] if the time value is out of range or any KDF
1022/// pass fails.
1023///
1024/// # Example
1025///
1026/// ```ignore
1027/// # use toolkit_zero::encryption::timelock::*;
1028/// let salts = TimeLockSalts::generate();
1029/// let kdf = KdfPreset::Balanced.params();
1030/// let t = TimeLockTime::new(18, 0).unwrap();
1031/// // Use the public timelock() entry point (params = None → _at path):
1032/// let key = timelock(
1033/// Some(TimeLockCadence::DayOfWeek(Weekday::Tuesday)),
1034/// Some(t),
1035/// Some(TimePrecision::Hour),
1036/// Some(TimeFormat::Hour24),
1037/// Some(salts),
1038/// Some(kdf),
1039/// None,
1040/// ).unwrap();
1041/// // key is valid only at 18:xx on any Tuesday
1042/// ```
1043#[cfg(feature = "enc-timelock-keygen-input")]
1044fn derive_key_scheduled_at(
1045 cadence: TimeLockCadence,
1046 time: TimeLockTime,
1047 precision: TimePrecision,
1048 format: TimeFormat,
1049 salts: &TimeLockSalts,
1050 params: &KdfParams,
1051) -> Result<TimeLockKey, TimeLockError> {
1052 let cadence_part = cadence.bake_string();
1053 let time_part = helper::format_time_at(time, precision, format)?;
1054 let full = format!("{}{}", cadence_part, time_part);
1055 helper::run_kdf_chain(full.into_bytes(), salts, params)
1056}
1057
1058/// Derive a 32-byte key from the **current system time and calendar state**
1059/// using the settings stored in a [`TimeLockParams`] (decryption path).
1060///
1061/// Extends [`derive_key_now`] with calendar awareness. The `cadence_variant`
1062/// field in `timelock_params` determines which calendar dimension(s) are read
1063/// from the live clock, making the KDF input identical to what
1064/// [`derive_key_scheduled_at`] produced on the matching slot.
1065///
1066/// # Errors
1067///
1068/// Returns [`TimeLockError`] if the system clock is unavailable or any KDF
1069/// pass fails.
1070///
1071/// # Example
1072///
1073/// ```ignore
1074/// # use toolkit_zero::encryption::timelock::*;
1075/// // Load header from ciphertext then call with params = Some(header):
1076/// let dec_key = timelock(
1077/// None, None, None, None, None, None,
1078/// Some(header), // header: TimeLockParams loaded from ciphertext
1079/// ).unwrap();
1080/// ```
1081#[cfg(feature = "enc-timelock-keygen-now")]
1082fn derive_key_scheduled_now(
1083 timelock_params: &TimeLockParams,
1084) -> Result<TimeLockKey, TimeLockError> {
1085 let (precision, format, cadence_variant) = utility::unpack(timelock_params);
1086 let cadence_part = helper::bake_cadence_now(cadence_variant)?;
1087 let time_part = helper::format_time_now(precision, format)?;
1088 let full = format!("{}{}", cadence_part, time_part);
1089 helper::run_kdf_chain(full.into_bytes(), &timelock_params.salts, &timelock_params.kdf_params)
1090}
1091
1092// ─── internal scheduled async API ───────────────────────────────────────────
1093
1094/// Async variant of [`derive_key_scheduled_at`].
1095///
1096/// Offloads the blocking KDF work to a Tokio blocking thread. Takes `salts`
1097/// and `params` by **value** (required for `'static` move into
1098/// `spawn_blocking`); both are zeroized on drop. `cadence` and `time` are
1099/// `Copy`.
1100///
1101/// Requires the `enc-timelock-async-keygen-input` feature.
1102#[cfg(feature = "enc-timelock-async-keygen-input")]
1103async fn derive_key_scheduled_at_async(
1104 cadence: TimeLockCadence,
1105 time: TimeLockTime,
1106 precision: TimePrecision,
1107 format: TimeFormat,
1108 salts: TimeLockSalts,
1109 params: KdfParams,
1110) -> Result<TimeLockKey, TimeLockError> {
1111 tokio::task::spawn_blocking(move || {
1112 derive_key_scheduled_at(cadence, time, precision, format, &salts, ¶ms)
1113 })
1114 .await
1115 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
1116}
1117
1118/// Async variant of [`derive_key_scheduled_now`].
1119///
1120/// Offloads the blocking KDF work to a Tokio blocking thread. Takes
1121/// `timelock_params` by **value**; the [`TimeLockSalts`] inside are
1122/// zeroized on drop.
1123///
1124/// Requires the `enc-timelock-async-keygen-now` feature.
1125#[cfg(feature = "enc-timelock-async-keygen-now")]
1126async fn derive_key_scheduled_now_async(
1127 timelock_params: TimeLockParams,
1128) -> Result<TimeLockKey, TimeLockError> {
1129 tokio::task::spawn_blocking(move || {
1130 derive_key_scheduled_now(&timelock_params)
1131 })
1132 .await
1133 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
1134}
1135
1136// ─── public API ───────────────────────────────────────────────────────────────
1137
1138/// Derive a 32-byte time-locked key — unified sync entry point.
1139///
1140/// ## Encryption path (`params = None`)
1141///
1142/// Set `params` to `None` and supply all of `cadence`, `time`, `precision`,
1143/// `format`, `salts`, and `kdf` as `Some(...)`. Requires the
1144/// `enc-timelock-keygen-input` feature. After calling, use [`pack`] with the
1145/// same arguments to produce a [`TimeLockParams`] header for the ciphertext.
1146///
1147/// ## Decryption path (`params = Some(p)`)
1148///
1149/// Set `params` to `Some(header)` where `header` is the [`TimeLockParams`]
1150/// read from the ciphertext. All other arguments are ignored and may be
1151/// `None`. Requires the `enc-timelock-keygen-now` feature.
1152///
1153/// # Errors
1154///
1155/// - [`TimeLockError::ForbiddenAction`] if the required feature is not active,
1156/// or if the `_at` path is taken but any required `Option` argument is `None`.
1157/// - [`TimeLockError::Argon2`] / [`TimeLockError::Scrypt`] on KDF failure.
1158/// - [`TimeLockError::ClockUnavailable`] if the OS clock is unusable (`_now` path).
1159///
1160/// # Example
1161///
1162/// ```no_run
1163/// # use toolkit_zero::encryption::timelock::*;
1164/// let salts = TimeLockSalts::generate();
1165/// let kdf = KdfPreset::BalancedMac.params();
1166///
1167/// // Encryption side — lock to every Tuesday at 18:00
1168/// let enc_key = timelock(
1169/// Some(TimeLockCadence::DayOfWeek(Weekday::Tuesday)),
1170/// Some(TimeLockTime::new(18, 0).unwrap()),
1171/// Some(TimePrecision::Hour),
1172/// Some(TimeFormat::Hour24),
1173/// Some(salts.clone()),
1174/// Some(kdf),
1175/// None,
1176/// ).unwrap();
1177///
1178/// // Pack settings + salts + kdf into header; store in ciphertext.
1179/// let header = pack(TimePrecision::Hour, TimeFormat::Hour24,
1180/// &TimeLockCadence::DayOfWeek(Weekday::Tuesday), salts, kdf);
1181///
1182/// // Decryption side — call on a Tuesday at 18:xx:
1183/// let dec_key = timelock(
1184/// None, None, None, None, None, None,
1185/// Some(header),
1186/// ).unwrap();
1187/// // enc_key.as_bytes() == dec_key.as_bytes() when called at the right time
1188/// ```
1189#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1190pub fn timelock(
1191 cadence: Option<TimeLockCadence>,
1192 time: Option<TimeLockTime>,
1193 precision: Option<TimePrecision>,
1194 format: Option<TimeFormat>,
1195 salts: Option<TimeLockSalts>,
1196 kdf: Option<KdfParams>,
1197 params: Option<TimeLockParams>,
1198) -> Result<TimeLockKey, TimeLockError> {
1199 if let Some(p) = params {
1200 // _now (decryption) path: all settings come from TimeLockParams.
1201 let _ = (cadence, time, precision, format, salts, kdf); // unused on this path
1202 #[cfg(not(feature = "enc-timelock-keygen-now"))]
1203 return Err(TimeLockError::ForbiddenAction(
1204 "enc-timelock-keygen-now feature is required for the _now (decryption) path"
1205 ));
1206 #[cfg(feature = "enc-timelock-keygen-now")]
1207 return derive_key_scheduled_now(&p);
1208 } else {
1209 // _at (encryption) path: caller must supply all other arguments.
1210 #[cfg(not(feature = "enc-timelock-keygen-input"))]
1211 return Err(TimeLockError::ForbiddenAction(
1212 "enc-timelock-keygen-input feature is required for the _at (encryption) path; \
1213 pass Some(TimeLockParams) for the decryption path (requires enc-timelock-keygen-now)"
1214 ));
1215 #[cfg(feature = "enc-timelock-keygen-input")]
1216 {
1217 let c = cadence.ok_or(TimeLockError::ForbiddenAction("_at path: cadence must be Some"))?;
1218 let t = time.ok_or(TimeLockError::ForbiddenAction("_at path: time must be Some"))?;
1219 let pr = precision.ok_or(TimeLockError::ForbiddenAction("_at path: precision must be Some"))?;
1220 let fm = format.ok_or(TimeLockError::ForbiddenAction("_at path: format must be Some"))?;
1221 let sl = salts.ok_or(TimeLockError::ForbiddenAction("_at path: salts must be Some"))?;
1222 let kd = kdf.ok_or(TimeLockError::ForbiddenAction("_at path: kdf must be Some"))?;
1223 return derive_key_scheduled_at(c, t, pr, fm, &sl, &kd);
1224 }
1225 }
1226}
1227
1228/// Derive a 32-byte time-locked key — unified async entry point.
1229///
1230/// Async counterpart of [`timelock`]. Same `params`-based routing: set
1231/// `params = Some(header)` for the **decryption** path, or `params = None`
1232/// with all other arguments as `Some(...)` for the **encryption** path.
1233/// All arguments are taken by value; the blocking KDF work is offloaded to a
1234/// Tokio blocking thread.
1235///
1236/// # Errors
1237///
1238/// Same as [`timelock`], plus [`TimeLockError::TaskPanic`] if the spawned
1239/// task panics.
1240#[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
1241pub async fn timelock_async(
1242 cadence: Option<TimeLockCadence>,
1243 time: Option<TimeLockTime>,
1244 precision: Option<TimePrecision>,
1245 format: Option<TimeFormat>,
1246 salts: Option<TimeLockSalts>,
1247 kdf: Option<KdfParams>,
1248 params: Option<TimeLockParams>,
1249) -> Result<TimeLockKey, TimeLockError> {
1250 if let Some(p) = params {
1251 let _ = (cadence, time, precision, format, salts, kdf);
1252 #[cfg(not(feature = "enc-timelock-async-keygen-now"))]
1253 return Err(TimeLockError::ForbiddenAction(
1254 "enc-timelock-async-keygen-now feature is required for the async _now (decryption) path"
1255 ));
1256 #[cfg(feature = "enc-timelock-async-keygen-now")]
1257 return derive_key_scheduled_now_async(p).await;
1258 } else {
1259 #[cfg(not(feature = "enc-timelock-async-keygen-input"))]
1260 return Err(TimeLockError::ForbiddenAction(
1261 "enc-timelock-async-keygen-input feature is required for the async _at (encryption) path"
1262 ));
1263 #[cfg(feature = "enc-timelock-async-keygen-input")]
1264 {
1265 let c = cadence.ok_or(TimeLockError::ForbiddenAction("_at path: cadence must be Some"))?;
1266 let t = time.ok_or(TimeLockError::ForbiddenAction("_at path: time must be Some"))?;
1267 let pr = precision.ok_or(TimeLockError::ForbiddenAction("_at path: precision must be Some"))?;
1268 let fm = format.ok_or(TimeLockError::ForbiddenAction("_at path: format must be Some"))?;
1269 let sl = salts.ok_or(TimeLockError::ForbiddenAction("_at path: salts must be Some"))?;
1270 let kd = kdf.ok_or(TimeLockError::ForbiddenAction("_at path: kdf must be Some"))?;
1271 return derive_key_scheduled_at_async(c, t, pr, fm, sl, kd).await;
1272 }
1273 }
1274}
1275
1276// ─── builder ─────────────────────────────────────────────────────────────────
1277
1278/// Fluent builder for [`timelock`] / [`timelock_async`] key derivation.
1279///
1280/// Provides a readable alternative to the 7-positional-argument `timelock()` function.
1281/// Create a builder via [`TimelockBuilder::encrypt`] (encryption path, `_at`) or
1282/// [`TimelockBuilder::decrypt`] (decryption path, `_now`), optionally configure
1283/// it with setter methods, then call [`derive`](Self::derive) or
1284/// [`derive_async`](Self::derive_async).
1285///
1286/// ## Encryption (key-at path)
1287///
1288/// All of `time`, `salts`, and `kdf` are **required**. `cadence`, `precision`, and
1289/// `format` are optional and fall back to sensible defaults:
1290/// * `cadence` → [`TimeLockCadence::None`] (no calendar constraint)
1291/// * `precision` → [`TimePrecision::Minute`]
1292/// * `format` → [`TimeFormat::Hour24`]
1293///
1294/// ```no_run
1295/// # use toolkit_zero::encryption::timelock::*;
1296/// let salts = TimeLockSalts::generate();
1297/// let kdf = KdfPreset::Balanced.params();
1298///
1299/// let key = TimelockBuilder::encrypt()
1300/// .time(TimeLockTime::new(14, 37).unwrap())
1301/// .salts(salts)
1302/// .kdf(kdf)
1303/// .derive()
1304/// .unwrap();
1305/// ```
1306///
1307/// ## Decryption (key-now path)
1308///
1309/// Pass the [`TimeLockParams`] header stored in the ciphertext. No other
1310/// configuration is required; all settings are read from `params`.
1311///
1312/// ```no_run
1313/// # use toolkit_zero::encryption::timelock::*;
1314/// # let header: TimeLockParams = todo!();
1315/// let key = TimelockBuilder::decrypt(header).derive().unwrap();
1316/// ```
1317#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
1318pub struct TimelockBuilder {
1319 cadence: Option<TimeLockCadence>,
1320 time: Option<TimeLockTime>,
1321 precision: Option<TimePrecision>,
1322 format: Option<TimeFormat>,
1323 salts: Option<TimeLockSalts>,
1324 kdf: Option<KdfParams>,
1325 params: Option<TimeLockParams>,
1326}
1327
1328#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
1329impl TimelockBuilder {
1330 /// Begin configuring an **encryption** (key-at) derivation.
1331 ///
1332 /// Requires `enc-timelock-keygen-input` (or async variant) to call
1333 /// [`derive`](Self::derive) / [`derive_async`](Self::derive_async).
1334 #[cfg(any(feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-input"))]
1335 pub fn encrypt() -> Self {
1336 Self {
1337 cadence: Some(TimeLockCadence::None),
1338 time: None,
1339 precision: Some(TimePrecision::Minute),
1340 format: Some(TimeFormat::Hour24),
1341 salts: None,
1342 kdf: None,
1343 params: None,
1344 }
1345 }
1346
1347 /// Begin configuring a **decryption** (key-now) derivation from a stored header.
1348 ///
1349 /// Requires `enc-timelock-keygen-now` (or async variant) to call
1350 /// [`derive`](Self::derive) / [`derive_async`](Self::derive_async).
1351 #[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-async-keygen-now"))]
1352 pub fn decrypt(params: TimeLockParams) -> Self {
1353 Self {
1354 cadence: None,
1355 time: None,
1356 precision: None,
1357 format: None,
1358 salts: None,
1359 kdf: None,
1360 params: Some(params),
1361 }
1362 }
1363
1364 /// Set the calendar cadence (default: `TimeLockCadence::None`).
1365 pub fn cadence(mut self, cadence: TimeLockCadence) -> Self {
1366 self.cadence = Some(cadence);
1367 self
1368 }
1369
1370 /// Set the explicit lock time (required for the encryption path).
1371 pub fn time(mut self, time: TimeLockTime) -> Self {
1372 self.time = Some(time);
1373 self
1374 }
1375
1376 /// Set the time precision (default: `TimePrecision::Minute`).
1377 pub fn precision(mut self, precision: TimePrecision) -> Self {
1378 self.precision = Some(precision);
1379 self
1380 }
1381
1382 /// Set the clock format (default: `TimeFormat::Hour24`).
1383 pub fn format(mut self, format: TimeFormat) -> Self {
1384 self.format = Some(format);
1385 self
1386 }
1387
1388 /// Set the KDF salts (required for the encryption path).
1389 pub fn salts(mut self, salts: TimeLockSalts) -> Self {
1390 self.salts = Some(salts);
1391 self
1392 }
1393
1394 /// Set the KDF parameters (required for the encryption path).
1395 pub fn kdf(mut self, kdf: KdfParams) -> Self {
1396 self.kdf = Some(kdf);
1397 self
1398 }
1399
1400 /// Derive the key synchronously.
1401 ///
1402 /// Delegates directly to [`timelock`].
1403 #[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1404 pub fn derive(self) -> Result<TimeLockKey, TimeLockError> {
1405 timelock(
1406 self.cadence,
1407 self.time,
1408 self.precision,
1409 self.format,
1410 self.salts,
1411 self.kdf,
1412 self.params,
1413 )
1414 }
1415
1416 /// Derive the key asynchronously.
1417 ///
1418 /// Delegates directly to [`timelock_async`]. The blocking KDF work is
1419 /// offloaded to a Tokio blocking thread.
1420 #[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
1421 pub async fn derive_async(self) -> Result<TimeLockKey, TimeLockError> {
1422 timelock_async(
1423 self.cadence,
1424 self.time,
1425 self.precision,
1426 self.format,
1427 self.salts,
1428 self.kdf,
1429 self.params,
1430 ).await
1431 }
1432}
1433
1434// ─── tests ────────────────────────────────────────────────────────────────────
1435
1436#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1437#[cfg(test)]
1438mod tests {
1439 use super::*;
1440 #[cfg(all(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1441 use chrono::Timelike as _;
1442
1443 fn fast() -> KdfParams {
1444 // Minimal params for fast test execution — not a real security preset.
1445 KdfParams {
1446 pass1: Argon2PassParams { m_cost: 32_768, t_cost: 1, p_cost: 1 },
1447 pass2: ScryptPassParams { log_n: 13, r: 8, p: 1 },
1448 pass3: Argon2PassParams { m_cost: 16_384, t_cost: 1, p_cost: 1 },
1449 }
1450 }
1451 fn salts() -> TimeLockSalts { TimeLockSalts::generate() }
1452
1453 // ── TimeLockTime construction ─────────────────────────────────────────
1454
1455 #[cfg(feature = "enc-timelock-keygen-input")]
1456 #[test]
1457 fn timelocktime_valid_range() {
1458 assert!(TimeLockTime::new(0, 0).is_some());
1459 assert!(TimeLockTime::new(23, 59).is_some());
1460 assert!(TimeLockTime::new(14, 37).is_some());
1461 }
1462
1463 #[cfg(feature = "enc-timelock-keygen-input")]
1464 #[test]
1465 fn timelocktime_invalid_range() {
1466 assert!(TimeLockTime::new(24, 0).is_none(), "hour=24 should fail");
1467 assert!(TimeLockTime::new( 0, 60).is_none(), "minute=60 should fail");
1468 assert!(TimeLockTime::new(99, 99).is_none());
1469 }
1470
1471 // ── format_components ────────────────────────────────────────────────
1472
1473 #[test]
1474 fn format_hour_24h() {
1475 let s = helper::format_components(14, 37, TimePrecision::Hour, TimeFormat::Hour24);
1476 assert_eq!(s, "14");
1477 }
1478
1479 #[test]
1480 fn format_hour_12h() {
1481 let s_pm = helper::format_components(14, 0, TimePrecision::Hour, TimeFormat::Hour12);
1482 let s_am = helper::format_components( 2, 0, TimePrecision::Hour, TimeFormat::Hour12);
1483 assert_eq!(s_pm, "02PM");
1484 assert_eq!(s_am, "02AM");
1485 }
1486
1487 #[test]
1488 fn format_quarter_snaps_correctly() {
1489 // 37 should snap to 30; 15 → 15; 0 → 0; 59 → 45
1490 assert_eq!(helper::format_components(14, 37, TimePrecision::Quarter, TimeFormat::Hour24), "14:30");
1491 assert_eq!(helper::format_components(14, 15, TimePrecision::Quarter, TimeFormat::Hour24), "14:15");
1492 assert_eq!(helper::format_components(14, 0, TimePrecision::Quarter, TimeFormat::Hour24), "14:00");
1493 assert_eq!(helper::format_components(14, 59, TimePrecision::Quarter, TimeFormat::Hour24), "14:45");
1494 }
1495
1496 #[test]
1497 fn format_minute_exact() {
1498 let s = helper::format_components(9, 5, TimePrecision::Minute, TimeFormat::Hour24);
1499 assert_eq!(s, "09:05");
1500 }
1501
1502 // ── derive_key_at: determinism ────────────────────────────────────────
1503
1504 #[cfg(feature = "enc-timelock-keygen-input")]
1505 #[test]
1506 fn at_same_inputs_same_key() {
1507 let s = salts();
1508 let t = TimeLockTime::new(14, 37).unwrap();
1509 let k1 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1510 // Regenerate salts from their raw bytes to prove serialization round-trip too.
1511 let s2 = TimeLockSalts::from_slice(&s.to_bytes());
1512 let k2 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s2, &fast()).unwrap();
1513 assert_eq!(k1.as_bytes(), k2.as_bytes());
1514 }
1515
1516 #[cfg(feature = "enc-timelock-keygen-input")]
1517 #[test]
1518 fn at_different_salts_different_key() {
1519 let t = TimeLockTime::new(14, 37).unwrap();
1520 let k1 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1521 let k2 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1522 assert_ne!(k1.as_bytes(), k2.as_bytes());
1523 }
1524
1525 #[cfg(feature = "enc-timelock-keygen-input")]
1526 #[test]
1527 fn at_different_time_different_key() {
1528 let s = salts();
1529 let t1 = TimeLockTime::new(14, 37).unwrap();
1530 let t2 = TimeLockTime::new(14, 38).unwrap();
1531 let k1 = derive_key_at(t1, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1532 let k2 = derive_key_at(t2, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1533 assert_ne!(k1.as_bytes(), k2.as_bytes());
1534 }
1535
1536 // ── derive_key_now: liveness ──────────────────────────────────────────
1537
1538 #[cfg(feature = "enc-timelock-keygen-now")]
1539 #[test]
1540 fn now_returns_nonzero_key() {
1541 let k = derive_key_now(TimePrecision::Hour, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1542 assert_ne!(k.as_bytes(), &[0u8; 32]);
1543 }
1544
1545 #[cfg(all(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1546 #[test]
1547 fn now_and_at_same_minute_match() {
1548 // Build a TimeLockTime from the current clock and confirm it produces
1549 // the same key as derive_key_now with Minute precision.
1550 let now = chrono::Local::now();
1551 let t = TimeLockTime::new(now.hour(), now.minute()).unwrap();
1552 let s = salts();
1553 let kn = derive_key_now(TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1554 let ka = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1555 assert_eq!(
1556 kn.as_bytes(), ka.as_bytes(),
1557 "now and explicit current time must produce the same key"
1558 );
1559 }
1560
1561 // ── salt serialization round-trip ─────────────────────────────────────
1562
1563 #[test]
1564 fn salt_round_trip() {
1565 let s = salts();
1566 let b = s.to_bytes();
1567 let s2 = TimeLockSalts::from_slice(&b);
1568 assert_eq!(s.s1, s2.s1);
1569 assert_eq!(s.s2, s2.s2);
1570 assert_eq!(s.s3, s2.s3);
1571 }
1572
1573 // ── Custom variant ───────────────────────────────────────────────────
1574
1575 #[cfg(feature = "enc-timelock-keygen-input")]
1576 #[test]
1577 fn custom_params_works() {
1578 // Verify Custom(KdfParams) goes through the code path without failing.
1579 let preset = KdfPreset::Custom(KdfPreset::Fast.params());
1580 let t = TimeLockTime::new(10, 0).unwrap();
1581 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &preset.params())
1582 .expect("Custom params should succeed");
1583 }
1584
1585 #[cfg(feature = "enc-timelock-keygen-input")]
1586 #[test]
1587 fn custom_params_roundtrip_eq() {
1588 let p = KdfPreset::Fast.params();
1589 assert_eq!(KdfPreset::Custom(p).params(), p);
1590 }
1591
1592 // ── Generic preset smoke tests (slow) ────────────────────────────────
1593
1594 #[cfg(feature = "enc-timelock-keygen-input")]
1595 #[test]
1596 #[ignore = "slow (~400–600 ms) — run with `cargo test -- --ignored`"]
1597 fn balanced_preset_completes() {
1598 let t = TimeLockTime::new(10, 0).unwrap();
1599 let start = std::time::Instant::now();
1600 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::Balanced.params())
1601 .expect("Balanced should succeed");
1602 println!("Balanced (generic): {:?}", start.elapsed());
1603 }
1604
1605 #[cfg(feature = "enc-timelock-keygen-input")]
1606 #[test]
1607 #[ignore = "slow (~2 s on Mac, ~8–15 s on x86) — run with `cargo test -- --ignored`"]
1608 fn paranoid_preset_completes() {
1609 let t = TimeLockTime::new(10, 0).unwrap();
1610 let start = std::time::Instant::now();
1611 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::Paranoid.params())
1612 .expect("Paranoid should succeed");
1613 println!("Paranoid (generic): {:?}", start.elapsed());
1614 }
1615
1616 // ── Mac preset smoke tests ───────────────────────────────────────────
1617
1618 #[cfg(all(feature = "enc-timelock-keygen-input", target_os = "macos"))]
1619 #[test]
1620 #[ignore = "slow (~2 s on M2) — run with `cargo test -- --ignored`"]
1621 fn balanced_mac_completes() {
1622 let t = TimeLockTime::new(10, 0).unwrap();
1623 let start = std::time::Instant::now();
1624 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedMac.params())
1625 .expect("BalancedMac should succeed");
1626 println!("BalancedMac: {:?}", start.elapsed());
1627 }
1628
1629 #[cfg(all(feature = "enc-timelock-keygen-input", target_os = "macos"))]
1630 #[test]
1631 #[ignore = "slow (~5–12 s on M2, faster on M3/M4) — run with `cargo test -- --ignored`"]
1632 fn paranoid_mac_completes() {
1633 let t = TimeLockTime::new(10, 0).unwrap();
1634 let start = std::time::Instant::now();
1635 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidMac.params())
1636 .expect("ParanoidMac should succeed");
1637 println!("ParanoidMac: {:?}", start.elapsed());
1638 }
1639
1640 // ── x86-64 preset smoke tests ────────────────────────────────────────
1641
1642 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "x86_64"))]
1643 #[test]
1644 #[ignore = "slow (~1.5 s on typical x86-64) — run with `cargo test -- --ignored`"]
1645 fn balanced_x86_completes() {
1646 let t = TimeLockTime::new(10, 0).unwrap();
1647 let start = std::time::Instant::now();
1648 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedX86.params())
1649 .expect("BalancedX86 should succeed");
1650 println!("BalancedX86: {:?}", start.elapsed());
1651 }
1652
1653 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "x86_64"))]
1654 #[test]
1655 #[ignore = "slow (~8–15 s on typical x86-64) — run with `cargo test -- --ignored`"]
1656 fn paranoid_x86_completes() {
1657 let t = TimeLockTime::new(10, 0).unwrap();
1658 let start = std::time::Instant::now();
1659 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidX86.params())
1660 .expect("ParanoidX86 should succeed");
1661 println!("ParanoidX86: {:?}", start.elapsed());
1662 }
1663
1664 // ── Linux ARM64 preset smoke tests ───────────────────────────────────
1665
1666 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "aarch64", not(target_os = "macos")))]
1667 #[test]
1668 #[ignore = "slow (~3 s on Graviton3) — run with `cargo test -- --ignored`"]
1669 fn balanced_arm_completes() {
1670 let t = TimeLockTime::new(10, 0).unwrap();
1671 let start = std::time::Instant::now();
1672 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedArm.params())
1673 .expect("BalancedArm should succeed");
1674 println!("BalancedArm: {:?}", start.elapsed());
1675 }
1676
1677 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "aarch64", not(target_os = "macos")))]
1678 #[test]
1679 #[ignore = "slow (~10–20 s on Graviton3) — run with `cargo test -- --ignored`"]
1680 fn paranoid_arm_completes() {
1681 let t = TimeLockTime::new(10, 0).unwrap();
1682 let start = std::time::Instant::now();
1683 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidArm.params())
1684 .expect("ParanoidArm should succeed");
1685 println!("ParanoidArm: {:?}", start.elapsed());
1686 }
1687
1688 // ── Scheduled key derivation ─────────────────────────────────────────────
1689
1690 #[cfg(feature = "enc-timelock-keygen-input")]
1691 #[test]
1692 fn scheduled_none_same_as_regular_at() {
1693 // cadence=None adds no prefix — result must equal derive_key_at
1694 let s = salts();
1695 let t = TimeLockTime::new(14, 0).unwrap();
1696 let regular = derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast()).unwrap();
1697 let scheduled = derive_key_scheduled_at(
1698 TimeLockCadence::None, t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1699 ).unwrap();
1700 assert_eq!(regular.as_bytes(), scheduled.as_bytes());
1701 }
1702
1703 #[cfg(feature = "enc-timelock-keygen-input")]
1704 #[test]
1705 fn scheduled_different_weekdays_different_keys() {
1706 let s = salts();
1707 let t = TimeLockTime::new(18, 0).unwrap();
1708 let k_mon = derive_key_scheduled_at(
1709 TimeLockCadence::DayOfWeek(Weekday::Monday),
1710 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1711 ).unwrap();
1712 let k_tue = derive_key_scheduled_at(
1713 TimeLockCadence::DayOfWeek(Weekday::Tuesday),
1714 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1715 ).unwrap();
1716 assert_ne!(k_mon.as_bytes(), k_tue.as_bytes());
1717 }
1718
1719 #[cfg(feature = "enc-timelock-keygen-input")]
1720 #[test]
1721 fn scheduled_different_months_different_keys() {
1722 let s = salts();
1723 let t = TimeLockTime::new(0, 0).unwrap();
1724 let k_jan = derive_key_scheduled_at(
1725 TimeLockCadence::MonthOfYear(Month::January),
1726 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1727 ).unwrap();
1728 let k_feb = derive_key_scheduled_at(
1729 TimeLockCadence::MonthOfYear(Month::February),
1730 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1731 ).unwrap();
1732 assert_ne!(k_jan.as_bytes(), k_feb.as_bytes());
1733 }
1734
1735 #[cfg(feature = "enc-timelock-keygen-input")]
1736 #[test]
1737 fn scheduled_at_deterministic() {
1738 // Same inputs must always produce the same key
1739 let s = salts();
1740 let t = TimeLockTime::new(6, 0).unwrap();
1741 let c = TimeLockCadence::DayOfWeekInMonth(Weekday::Friday, Month::March);
1742 let k1 = derive_key_scheduled_at(c, t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast()).unwrap();
1743 let s2 = TimeLockSalts::from_slice(&s.to_bytes());
1744 let k2 = derive_key_scheduled_at(c, t, TimePrecision::Hour, TimeFormat::Hour24, &s2, &fast()).unwrap();
1745 assert_eq!(k1.as_bytes(), k2.as_bytes());
1746 }
1747
1748 #[cfg(feature = "enc-timelock-keygen-now")]
1749 #[test]
1750 fn scheduled_now_none_matches_derive_now() {
1751 // cadence_variant=0 (None) + Hour + Hour24 must match derive_key_now exactly.
1752 // TimeLockParams now carries salts+kdf; build via pack() and clone salts.
1753 let s = salts();
1754 let f = fast();
1755 let stored = pack(
1756 TimePrecision::Hour, TimeFormat::Hour24,
1757 &TimeLockCadence::None,
1758 s.clone(),
1759 f,
1760 );
1761 let k1 = derive_key_now(TimePrecision::Hour, TimeFormat::Hour24, &s, &f).unwrap();
1762 let k2 = derive_key_scheduled_now(&stored).unwrap();
1763 assert_eq!(k1.as_bytes(), k2.as_bytes());
1764 }
1765
1766 #[cfg(any(feature = "enc-timelock-keygen-input", feature = "enc-timelock-keygen-now"))]
1767 #[test]
1768 fn pack_unpack_roundtrip() {
1769 let params = pack(
1770 TimePrecision::Minute,
1771 TimeFormat::Hour24,
1772 &TimeLockCadence::DayOfWeekInMonth(Weekday::Tuesday, Month::February),
1773 salts(),
1774 fast(),
1775 );
1776 assert_eq!(params.time_precision, 2); // Minute
1777 assert_eq!(params.time_format, 1); // Hour24
1778 assert_eq!(params.cadence_variant, 4); // DayOfWeekInMonth
1779 let (p, f, v) = unpack(¶ms);
1780 assert!(matches!(p, TimePrecision::Minute));
1781 assert!(matches!(f, TimeFormat::Hour24));
1782 assert_eq!(v, 4);
1783 }
1784
1785 #[cfg(feature = "enc-timelock-keygen-input")]
1786 #[test]
1787 #[should_panic(expected = "DayOfMonthInMonth")]
1788 fn day_of_month_in_month_panics_on_invalid_day() {
1789 // February can have at most 28 days; day 29 must panic
1790 let s = salts();
1791 let t = TimeLockTime::new(0, 0).unwrap();
1792 let _ = derive_key_scheduled_at(
1793 TimeLockCadence::DayOfMonthInMonth(29, Month::February),
1794 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1795 );
1796 }
1797}
1798
1799// ─── attribute macro re-export ────────────────────────────────────────────────
1800
1801/// Concise attribute macro for deriving a time-locked key inline.
1802///
1803/// Replaces the decorated `fn` with a call to [`timelock`] (sync) or
1804/// [`timelock_async`] (async, add the `async` flag). See
1805/// [`toolkit_zero_macros::timelock`] for the full argument reference.
1806///
1807/// # Examples
1808///
1809/// ```rust,ignore
1810/// use toolkit_zero::encryption::timelock::*;
1811///
1812/// // Encryption — derive for 14:37 with Minute precision.
1813/// fn encrypt_key() -> Result<TimeLockKey, TimeLockError> {
1814/// let salts = TimeLockSalts::generate();
1815/// let kdf = KdfPreset::Balanced.params();
1816/// #[timelock(precision = Minute, format = Hour24, time(14, 37), salts = salts, kdf = kdf)]
1817/// fn key() {}
1818/// Ok(key)
1819/// }
1820///
1821/// // Decryption — re-derive from a stored header.
1822/// fn decrypt_key(header: TimeLockParams) -> Result<TimeLockKey, TimeLockError> {
1823/// #[timelock(params = header)]
1824/// fn key() {}
1825/// Ok(key)
1826/// }
1827/// ```
1828#[cfg(any(
1829 feature = "enc-timelock-keygen-now",
1830 feature = "enc-timelock-keygen-input",
1831 feature = "enc-timelock-async-keygen-now",
1832 feature = "enc-timelock-async-keygen-input",
1833))]
1834pub use toolkit_zero_macros::timelock;