Skip to main content

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, &params))
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, &params))
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, &params)
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(&params);
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;