1#[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#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum TimePrecision {
124 Hour,
129
130 Quarter,
136
137 Minute,
143}
144
145#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum TimeFormat {
149 Hour24,
151
152 Hour12,
155}
156
157#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Weekday {
166 Monday,
167 Tuesday,
168 Wednesday,
169 Thursday,
170 Friday,
171 Saturday,
172 Sunday,
173}
174
175#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
176impl Weekday {
177 pub fn name(self) -> &'static str {
179 match self {
180 Self::Monday => "Monday",
181 Self::Tuesday => "Tuesday",
182 Self::Wednesday => "Wednesday",
183 Self::Thursday => "Thursday",
184 Self::Friday => "Friday",
185 Self::Saturday => "Saturday",
186 Self::Sunday => "Sunday",
187 }
188 }
189
190 pub fn number(self) -> u8 {
192 match self {
193 Self::Monday => 0,
194 Self::Tuesday => 1,
195 Self::Wednesday => 2,
196 Self::Thursday => 3,
197 Self::Friday => 4,
198 Self::Saturday => 5,
199 Self::Sunday => 6,
200 }
201 }
202
203 #[cfg(feature = "enc-timelock-keygen-now")]
205 pub(crate) fn from_chrono(w: chrono::Weekday) -> Self {
206 match w {
207 chrono::Weekday::Mon => Self::Monday,
208 chrono::Weekday::Tue => Self::Tuesday,
209 chrono::Weekday::Wed => Self::Wednesday,
210 chrono::Weekday::Thu => Self::Thursday,
211 chrono::Weekday::Fri => Self::Friday,
212 chrono::Weekday::Sat => Self::Saturday,
213 chrono::Weekday::Sun => Self::Sunday,
214 }
215 }
216}
217
218#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum Month {
225 January,
226 February,
227 March,
228 April,
229 May,
230 June,
231 July,
232 August,
233 September,
234 October,
235 November,
236 December,
237}
238
239#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
240impl Month {
241 pub fn name(self) -> &'static str {
243 match self {
244 Self::January => "January",
245 Self::February => "February",
246 Self::March => "March",
247 Self::April => "April",
248 Self::May => "May",
249 Self::June => "June",
250 Self::July => "July",
251 Self::August => "August",
252 Self::September => "September",
253 Self::October => "October",
254 Self::November => "November",
255 Self::December => "December",
256 }
257 }
258
259 pub fn number(self) -> u8 {
261 match self {
262 Self::January => 1,
263 Self::February => 2,
264 Self::March => 3,
265 Self::April => 4,
266 Self::May => 5,
267 Self::June => 6,
268 Self::July => 7,
269 Self::August => 8,
270 Self::September => 9,
271 Self::October => 10,
272 Self::November => 11,
273 Self::December => 12,
274 }
275 }
276
277 pub fn max_days(self) -> u8 {
282 match self {
283 Self::February => 28,
284 Self::April | Self::June | Self::September | Self::November => 30,
285 _ => 31,
286 }
287 }
288
289 #[cfg(feature = "enc-timelock-keygen-now")]
295 pub(crate) fn from_number(n: u8) -> Self {
296 match n {
297 1 => Self::January,
298 2 => Self::February,
299 3 => Self::March,
300 4 => Self::April,
301 5 => Self::May,
302 6 => Self::June,
303 7 => Self::July,
304 8 => Self::August,
305 9 => Self::September,
306 10 => Self::October,
307 11 => Self::November,
308 12 => Self::December,
309 _ => panic!("Month::from_number: invalid month number {}", n),
310 }
311 }
312}
313
314#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337pub enum TimeLockCadence {
338 None,
342
343 DayOfWeek(Weekday),
347
348 DayOfMonth(u8),
354
355 MonthOfYear(Month),
359
360 DayOfWeekInMonth(Weekday, Month),
364
365 DayOfMonthInMonth(u8, Month),
371
372 DayOfWeekAndDayOfMonth(Weekday, u8),
378}
379
380#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
381impl TimeLockCadence {
382 pub fn variant_id(self) -> u8 {
385 match self {
386 Self::None => 0,
387 Self::DayOfWeek(_) => 1,
388 Self::DayOfMonth(_) => 2,
389 Self::MonthOfYear(_) => 3,
390 Self::DayOfWeekInMonth(_, _) => 4,
391 Self::DayOfMonthInMonth(_, _) => 5,
392 Self::DayOfWeekAndDayOfMonth(_, _) => 6,
393 }
394 }
395
396 pub(crate) fn bake_string(self) -> String {
406 match self {
407 Self::None => String::new(),
408 Self::DayOfWeek(w) => format!("{}|", w.name()),
409 Self::DayOfMonth(d) => format!("{}|", d),
410 Self::MonthOfYear(m) => format!("{}|", m.name()),
411 Self::DayOfWeekInMonth(w, m) => format!("{}+{}|", w.name(), m.name()),
412 Self::DayOfMonthInMonth(d, m) => {
413 let max = m.max_days();
414 if d < 1 || d > max {
415 panic!(
416 "TimeLockCadence::DayOfMonthInMonth: day {} is out of range \
417 1–{} for {}",
418 d, max, m.name()
419 );
420 }
421 format!("{}+{}|", d, m.name())
422 }
423 Self::DayOfWeekAndDayOfMonth(w, d) => format!("{}+{}|", w.name(), d),
424 }
425 }
426}
427
428#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub struct TimeLockTime {
447 hour: u32, minute: u32, }
450
451#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
452impl TimeLockTime {
453 pub fn new(hour: u32, minute: u32) -> Option<Self> {
458 if hour > 23 || minute > 59 {
459 return None;
460 }
461 Some(Self { hour, minute })
462 }
463
464 #[inline]
466 pub fn hour(self) -> u32 { self.hour }
467
468 #[inline]
470 pub fn minute(self) -> u32 { self.minute }
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"))]
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478pub struct Argon2PassParams {
479 pub m_cost: u32,
481 pub t_cost: u32,
483 pub p_cost: u32,
485}
486
487#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
489#[derive(Debug, Clone, Copy, PartialEq, Eq)]
490pub struct ScryptPassParams {
491 pub log_n: u8,
493 pub r: u32,
495 pub p: u32,
497}
498
499#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
506#[derive(Debug, Clone, Copy, PartialEq, Eq)]
507pub struct KdfParams {
508 pub pass1: Argon2PassParams,
510 pub pass2: ScryptPassParams,
512 pub pass3: Argon2PassParams,
514}
515
516#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub enum KdfPreset {
580 Fast,
584 Balanced,
586 Paranoid,
588
589 FastMac,
593 BalancedMac,
595 ParanoidMac,
598
599 FastX86,
603 BalancedX86,
605 ParanoidX86,
607
608 FastArm,
612 BalancedArm,
614 ParanoidArm,
616
617 Custom(KdfParams),
636}
637
638#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
639impl KdfPreset {
640 pub fn params(self) -> KdfParams {
642 let fast = KdfParams {
644 pass1: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
645 pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
646 pass3: Argon2PassParams { m_cost: 65_536, t_cost: 3, p_cost: 1 },
647 };
648 let balanced = KdfParams {
650 pass1: Argon2PassParams { m_cost: 524_288, t_cost: 4, p_cost: 1 },
651 pass2: ScryptPassParams { log_n: 17, r: 8, p: 1 },
652 pass3: Argon2PassParams { m_cost: 262_144, t_cost: 4, p_cost: 1 },
653 };
654 let paranoid = KdfParams {
656 pass1: Argon2PassParams { m_cost: 786_432, t_cost: 5, p_cost: 1 },
657 pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
658 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 },
659 };
660
661 match self {
662 KdfPreset::Fast | KdfPreset::FastX86 => fast,
664 KdfPreset::Balanced | KdfPreset::BalancedX86 => balanced,
665 KdfPreset::Paranoid | KdfPreset::ParanoidX86 => paranoid,
666 KdfPreset::FastMac => balanced, KdfPreset::BalancedMac => KdfParams {
669 pass1: Argon2PassParams { m_cost: 1_048_576, t_cost: 4, p_cost: 1 }, pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
671 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 4, p_cost: 1 },
672 },
673 KdfPreset::ParanoidMac => KdfParams {
674 pass1: Argon2PassParams { m_cost: 3_145_728, t_cost: 4, p_cost: 1 }, pass2: ScryptPassParams { log_n: 20, r: 8, p: 1 },
676 pass3: Argon2PassParams { m_cost: 1_048_576, t_cost: 4, p_cost: 1 }, },
678 KdfPreset::FastArm => KdfParams {
680 pass1: Argon2PassParams { m_cost: 262_144, t_cost: 3, p_cost: 1 }, pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
682 pass3: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
683 },
684 KdfPreset::BalancedArm => KdfParams {
685 pass1: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 }, pass2: ScryptPassParams { log_n: 17, r: 8, p: 1 },
687 pass3: Argon2PassParams { m_cost: 262_144, t_cost: 5, p_cost: 1 },
688 },
689 KdfPreset::ParanoidArm => KdfParams {
690 pass1: Argon2PassParams { m_cost: 786_432, t_cost: 5, p_cost: 1 }, pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
692 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 },
693 },
694 KdfPreset::Custom(p) => p,
696 }
697 }
698}
699
700#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
711#[derive(Debug, Clone)]
712pub struct TimeLockSalts {
713 pub s1: [u8; 32],
715 pub s2: [u8; 32],
717 pub s3: [u8; 32],
719}
720
721#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
722impl TimeLockSalts {
723 pub fn generate() -> Self {
725 use rand::RngCore as _;
726 let mut rng = rand::rng();
727 let mut s = Self { s1: [0u8; 32], s2: [0u8; 32], s3: [0u8; 32] };
728 rng.fill_bytes(&mut s.s1);
729 rng.fill_bytes(&mut s.s2);
730 rng.fill_bytes(&mut s.s3);
731 s
732 }
733
734 pub fn from_bytes(s1: [u8; 32], s2: [u8; 32], s3: [u8; 32]) -> Self {
736 Self { s1, s2, s3 }
737 }
738
739 pub fn to_bytes(&self) -> [u8; 96] {
741 let mut out = [0u8; 96];
742 out[..32].copy_from_slice(&self.s1);
743 out[32..64].copy_from_slice(&self.s2);
744 out[64..].copy_from_slice(&self.s3);
745 out
746 }
747
748 pub fn from_slice(b: &[u8; 96]) -> Self {
750 let mut s1 = [0u8; 32]; s1.copy_from_slice(&b[..32]);
751 let mut s2 = [0u8; 32]; s2.copy_from_slice(&b[32..64]);
752 let mut s3 = [0u8; 32]; s3.copy_from_slice(&b[64..]);
753 Self { s1, s2, s3 }
754 }
755}
756
757#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
758impl Zeroize for TimeLockSalts {
759 fn zeroize(&mut self) {
760 self.s1.zeroize();
761 self.s2.zeroize();
762 self.s3.zeroize();
763 }
764}
765
766#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
767impl Drop for TimeLockSalts {
768 fn drop(&mut self) { self.zeroize(); }
769}
770
771#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
778pub struct TimeLockKey([u8; 32]);
779
780#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
781impl TimeLockKey {
782 #[inline]
787 pub fn as_bytes(&self) -> &[u8; 32] { &self.0 }
788}
789
790#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
791impl Zeroize for TimeLockKey {
792 #[inline]
793 fn zeroize(&mut self) { self.0.zeroize(); }
794}
795
796#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
797impl ZeroizeOnDrop for TimeLockKey {}
798
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"))]
800impl Drop for TimeLockKey {
801 fn drop(&mut self) { self.zeroize(); }
802}
803
804#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
808#[derive(Debug)]
809pub enum TimeLockError {
810 Argon2(String),
812 Scrypt(String),
814 #[cfg(feature = "enc-timelock-keygen-now")]
816 ClockUnavailable,
817 #[cfg(feature = "enc-timelock-keygen-input")]
819 InvalidTime(String),
820 #[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
822 TaskPanic(String),
823 ForbiddenAction(&'static str),
826}
827
828#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
829impl std::fmt::Display for TimeLockError {
830 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
831 match self {
832 Self::Argon2(s) => write!(f, "Argon2id error: {s}"),
833 Self::Scrypt(s) => write!(f, "scrypt error: {s}"),
834 #[cfg(feature = "enc-timelock-keygen-now")]
835 Self::ClockUnavailable => write!(f, "system clock unavailable"),
836 #[cfg(feature = "enc-timelock-keygen-input")]
837 Self::InvalidTime(s) => write!(f, "invalid time input: {s}"),
838 #[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
839 Self::TaskPanic(s) => write!(f, "KDF task panicked: {s}"),
840 Self::ForbiddenAction(s) => write!(f, "action not permitted: {s}"),
841 }
842 }
843}
844
845#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
846impl std::error::Error for TimeLockError {}
847
848#[allow(dead_code)]
879#[cfg(feature = "enc-timelock-keygen-now")]
880fn derive_key_now(
881 precision: TimePrecision,
882 format: TimeFormat,
883 salts: &TimeLockSalts,
884 params: &KdfParams,
885) -> Result<TimeLockKey, TimeLockError> {
886 let time_str = helper::format_time_now(precision, format)?;
887 helper::run_kdf_chain(time_str.into_bytes(), salts, params)
888}
889
890#[allow(dead_code)]
918#[cfg(feature = "enc-timelock-keygen-input")]
919fn derive_key_at(
920 time: TimeLockTime,
921 precision: TimePrecision,
922 format: TimeFormat,
923 salts: &TimeLockSalts,
924 params: &KdfParams,
925) -> Result<TimeLockKey, TimeLockError> {
926 let time_str = helper::format_time_at(time, precision, format)?;
927 helper::run_kdf_chain(time_str.into_bytes(), salts, params)
928}
929
930#[allow(dead_code)]
947#[cfg(feature = "enc-timelock-async-keygen-now")]
948async fn derive_key_now_async(
949 precision: TimePrecision,
950 format: TimeFormat,
951 salts: TimeLockSalts,
952 params: KdfParams,
953) -> Result<TimeLockKey, TimeLockError> {
954 tokio::task::spawn_blocking(move || derive_key_now(precision, format, &salts, ¶ms))
955 .await
956 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
957}
958
959#[allow(dead_code)]
966#[cfg(feature = "enc-timelock-async-keygen-input")]
967async fn derive_key_at_async(
968 time: TimeLockTime,
969 precision: TimePrecision,
970 format: TimeFormat,
971 salts: TimeLockSalts,
972 params: KdfParams,
973) -> Result<TimeLockKey, TimeLockError> {
974 tokio::task::spawn_blocking(move || derive_key_at(time, precision, format, &salts, ¶ms))
975 .await
976 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
977}
978
979#[cfg(feature = "enc-timelock-keygen-input")]
1022fn derive_key_scheduled_at(
1023 cadence: TimeLockCadence,
1024 time: TimeLockTime,
1025 precision: TimePrecision,
1026 format: TimeFormat,
1027 salts: &TimeLockSalts,
1028 params: &KdfParams,
1029) -> Result<TimeLockKey, TimeLockError> {
1030 let cadence_part = cadence.bake_string();
1031 let time_part = helper::format_time_at(time, precision, format)?;
1032 let full = format!("{}{}", cadence_part, time_part);
1033 helper::run_kdf_chain(full.into_bytes(), salts, params)
1034}
1035
1036#[cfg(feature = "enc-timelock-keygen-now")]
1060fn derive_key_scheduled_now(
1061 timelock_params: &TimeLockParams,
1062) -> Result<TimeLockKey, TimeLockError> {
1063 let (precision, format, cadence_variant) = utility::unpack(timelock_params);
1064 let cadence_part = helper::bake_cadence_now(cadence_variant)?;
1065 let time_part = helper::format_time_now(precision, format)?;
1066 let full = format!("{}{}", cadence_part, time_part);
1067 helper::run_kdf_chain(full.into_bytes(), &timelock_params.salts, &timelock_params.kdf_params)
1068}
1069
1070#[cfg(feature = "enc-timelock-async-keygen-input")]
1081async fn derive_key_scheduled_at_async(
1082 cadence: TimeLockCadence,
1083 time: TimeLockTime,
1084 precision: TimePrecision,
1085 format: TimeFormat,
1086 salts: TimeLockSalts,
1087 params: KdfParams,
1088) -> Result<TimeLockKey, TimeLockError> {
1089 tokio::task::spawn_blocking(move || {
1090 derive_key_scheduled_at(cadence, time, precision, format, &salts, ¶ms)
1091 })
1092 .await
1093 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
1094}
1095
1096#[cfg(feature = "enc-timelock-async-keygen-now")]
1104async fn derive_key_scheduled_now_async(
1105 timelock_params: TimeLockParams,
1106) -> Result<TimeLockKey, TimeLockError> {
1107 tokio::task::spawn_blocking(move || {
1108 derive_key_scheduled_now(&timelock_params)
1109 })
1110 .await
1111 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
1112}
1113
1114#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1168pub fn timelock(
1169 cadence: Option<TimeLockCadence>,
1170 time: Option<TimeLockTime>,
1171 precision: Option<TimePrecision>,
1172 format: Option<TimeFormat>,
1173 salts: Option<TimeLockSalts>,
1174 kdf: Option<KdfParams>,
1175 params: Option<TimeLockParams>,
1176) -> Result<TimeLockKey, TimeLockError> {
1177 if let Some(p) = params {
1178 let _ = (cadence, time, precision, format, salts, kdf); #[cfg(not(feature = "enc-timelock-keygen-now"))]
1181 return Err(TimeLockError::ForbiddenAction(
1182 "enc-timelock-keygen-now feature is required for the _now (decryption) path"
1183 ));
1184 #[cfg(feature = "enc-timelock-keygen-now")]
1185 return derive_key_scheduled_now(&p);
1186 } else {
1187 #[cfg(not(feature = "enc-timelock-keygen-input"))]
1189 return Err(TimeLockError::ForbiddenAction(
1190 "enc-timelock-keygen-input feature is required for the _at (encryption) path; \
1191 pass Some(TimeLockParams) for the decryption path (requires enc-timelock-keygen-now)"
1192 ));
1193 #[cfg(feature = "enc-timelock-keygen-input")]
1194 {
1195 let c = cadence.ok_or(TimeLockError::ForbiddenAction("_at path: cadence must be Some"))?;
1196 let t = time.ok_or(TimeLockError::ForbiddenAction("_at path: time must be Some"))?;
1197 let pr = precision.ok_or(TimeLockError::ForbiddenAction("_at path: precision must be Some"))?;
1198 let fm = format.ok_or(TimeLockError::ForbiddenAction("_at path: format must be Some"))?;
1199 let sl = salts.ok_or(TimeLockError::ForbiddenAction("_at path: salts must be Some"))?;
1200 let kd = kdf.ok_or(TimeLockError::ForbiddenAction("_at path: kdf must be Some"))?;
1201 return derive_key_scheduled_at(c, t, pr, fm, &sl, &kd);
1202 }
1203 }
1204}
1205
1206#[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
1219pub async fn timelock_async(
1220 cadence: Option<TimeLockCadence>,
1221 time: Option<TimeLockTime>,
1222 precision: Option<TimePrecision>,
1223 format: Option<TimeFormat>,
1224 salts: Option<TimeLockSalts>,
1225 kdf: Option<KdfParams>,
1226 params: Option<TimeLockParams>,
1227) -> Result<TimeLockKey, TimeLockError> {
1228 if let Some(p) = params {
1229 let _ = (cadence, time, precision, format, salts, kdf);
1230 #[cfg(not(feature = "enc-timelock-async-keygen-now"))]
1231 return Err(TimeLockError::ForbiddenAction(
1232 "enc-timelock-async-keygen-now feature is required for the async _now (decryption) path"
1233 ));
1234 #[cfg(feature = "enc-timelock-async-keygen-now")]
1235 return derive_key_scheduled_now_async(p).await;
1236 } else {
1237 #[cfg(not(feature = "enc-timelock-async-keygen-input"))]
1238 return Err(TimeLockError::ForbiddenAction(
1239 "enc-timelock-async-keygen-input feature is required for the async _at (encryption) path"
1240 ));
1241 #[cfg(feature = "enc-timelock-async-keygen-input")]
1242 {
1243 let c = cadence.ok_or(TimeLockError::ForbiddenAction("_at path: cadence must be Some"))?;
1244 let t = time.ok_or(TimeLockError::ForbiddenAction("_at path: time must be Some"))?;
1245 let pr = precision.ok_or(TimeLockError::ForbiddenAction("_at path: precision must be Some"))?;
1246 let fm = format.ok_or(TimeLockError::ForbiddenAction("_at path: format must be Some"))?;
1247 let sl = salts.ok_or(TimeLockError::ForbiddenAction("_at path: salts must be Some"))?;
1248 let kd = kdf.ok_or(TimeLockError::ForbiddenAction("_at path: kdf must be Some"))?;
1249 return derive_key_scheduled_at_async(c, t, pr, fm, sl, kd).await;
1250 }
1251 }
1252}
1253
1254#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1257#[cfg(test)]
1258mod tests {
1259 use super::*;
1260 #[cfg(all(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1261 use chrono::Timelike as _;
1262
1263 fn fast() -> KdfParams {
1264 KdfParams {
1266 pass1: Argon2PassParams { m_cost: 32_768, t_cost: 1, p_cost: 1 },
1267 pass2: ScryptPassParams { log_n: 13, r: 8, p: 1 },
1268 pass3: Argon2PassParams { m_cost: 16_384, t_cost: 1, p_cost: 1 },
1269 }
1270 }
1271 fn salts() -> TimeLockSalts { TimeLockSalts::generate() }
1272
1273 #[cfg(feature = "enc-timelock-keygen-input")]
1276 #[test]
1277 fn timelocktime_valid_range() {
1278 assert!(TimeLockTime::new(0, 0).is_some());
1279 assert!(TimeLockTime::new(23, 59).is_some());
1280 assert!(TimeLockTime::new(14, 37).is_some());
1281 }
1282
1283 #[cfg(feature = "enc-timelock-keygen-input")]
1284 #[test]
1285 fn timelocktime_invalid_range() {
1286 assert!(TimeLockTime::new(24, 0).is_none(), "hour=24 should fail");
1287 assert!(TimeLockTime::new( 0, 60).is_none(), "minute=60 should fail");
1288 assert!(TimeLockTime::new(99, 99).is_none());
1289 }
1290
1291 #[test]
1294 fn format_hour_24h() {
1295 let s = helper::format_components(14, 37, TimePrecision::Hour, TimeFormat::Hour24);
1296 assert_eq!(s, "14");
1297 }
1298
1299 #[test]
1300 fn format_hour_12h() {
1301 let s_pm = helper::format_components(14, 0, TimePrecision::Hour, TimeFormat::Hour12);
1302 let s_am = helper::format_components( 2, 0, TimePrecision::Hour, TimeFormat::Hour12);
1303 assert_eq!(s_pm, "02PM");
1304 assert_eq!(s_am, "02AM");
1305 }
1306
1307 #[test]
1308 fn format_quarter_snaps_correctly() {
1309 assert_eq!(helper::format_components(14, 37, TimePrecision::Quarter, TimeFormat::Hour24), "14:30");
1311 assert_eq!(helper::format_components(14, 15, TimePrecision::Quarter, TimeFormat::Hour24), "14:15");
1312 assert_eq!(helper::format_components(14, 0, TimePrecision::Quarter, TimeFormat::Hour24), "14:00");
1313 assert_eq!(helper::format_components(14, 59, TimePrecision::Quarter, TimeFormat::Hour24), "14:45");
1314 }
1315
1316 #[test]
1317 fn format_minute_exact() {
1318 let s = helper::format_components(9, 5, TimePrecision::Minute, TimeFormat::Hour24);
1319 assert_eq!(s, "09:05");
1320 }
1321
1322 #[cfg(feature = "enc-timelock-keygen-input")]
1325 #[test]
1326 fn at_same_inputs_same_key() {
1327 let s = salts();
1328 let t = TimeLockTime::new(14, 37).unwrap();
1329 let k1 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1330 let s2 = TimeLockSalts::from_slice(&s.to_bytes());
1332 let k2 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s2, &fast()).unwrap();
1333 assert_eq!(k1.as_bytes(), k2.as_bytes());
1334 }
1335
1336 #[cfg(feature = "enc-timelock-keygen-input")]
1337 #[test]
1338 fn at_different_salts_different_key() {
1339 let t = TimeLockTime::new(14, 37).unwrap();
1340 let k1 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1341 let k2 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1342 assert_ne!(k1.as_bytes(), k2.as_bytes());
1343 }
1344
1345 #[cfg(feature = "enc-timelock-keygen-input")]
1346 #[test]
1347 fn at_different_time_different_key() {
1348 let s = salts();
1349 let t1 = TimeLockTime::new(14, 37).unwrap();
1350 let t2 = TimeLockTime::new(14, 38).unwrap();
1351 let k1 = derive_key_at(t1, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1352 let k2 = derive_key_at(t2, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1353 assert_ne!(k1.as_bytes(), k2.as_bytes());
1354 }
1355
1356 #[cfg(feature = "enc-timelock-keygen-now")]
1359 #[test]
1360 fn now_returns_nonzero_key() {
1361 let k = derive_key_now(TimePrecision::Hour, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1362 assert_ne!(k.as_bytes(), &[0u8; 32]);
1363 }
1364
1365 #[cfg(all(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1366 #[test]
1367 fn now_and_at_same_minute_match() {
1368 let now = chrono::Local::now();
1371 let t = TimeLockTime::new(now.hour(), now.minute()).unwrap();
1372 let s = salts();
1373 let kn = derive_key_now(TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1374 let ka = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1375 assert_eq!(
1376 kn.as_bytes(), ka.as_bytes(),
1377 "now and explicit current time must produce the same key"
1378 );
1379 }
1380
1381 #[test]
1384 fn salt_round_trip() {
1385 let s = salts();
1386 let b = s.to_bytes();
1387 let s2 = TimeLockSalts::from_slice(&b);
1388 assert_eq!(s.s1, s2.s1);
1389 assert_eq!(s.s2, s2.s2);
1390 assert_eq!(s.s3, s2.s3);
1391 }
1392
1393 #[cfg(feature = "enc-timelock-keygen-input")]
1396 #[test]
1397 fn custom_params_works() {
1398 let preset = KdfPreset::Custom(KdfPreset::Fast.params());
1400 let t = TimeLockTime::new(10, 0).unwrap();
1401 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &preset.params())
1402 .expect("Custom params should succeed");
1403 }
1404
1405 #[cfg(feature = "enc-timelock-keygen-input")]
1406 #[test]
1407 fn custom_params_roundtrip_eq() {
1408 let p = KdfPreset::Fast.params();
1409 assert_eq!(KdfPreset::Custom(p).params(), p);
1410 }
1411
1412 #[cfg(feature = "enc-timelock-keygen-input")]
1415 #[test]
1416 #[ignore = "slow (~400–600 ms) — run with `cargo test -- --ignored`"]
1417 fn balanced_preset_completes() {
1418 let t = TimeLockTime::new(10, 0).unwrap();
1419 let start = std::time::Instant::now();
1420 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::Balanced.params())
1421 .expect("Balanced should succeed");
1422 println!("Balanced (generic): {:?}", start.elapsed());
1423 }
1424
1425 #[cfg(feature = "enc-timelock-keygen-input")]
1426 #[test]
1427 #[ignore = "slow (~2 s on Mac, ~8–15 s on x86) — run with `cargo test -- --ignored`"]
1428 fn paranoid_preset_completes() {
1429 let t = TimeLockTime::new(10, 0).unwrap();
1430 let start = std::time::Instant::now();
1431 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::Paranoid.params())
1432 .expect("Paranoid should succeed");
1433 println!("Paranoid (generic): {:?}", start.elapsed());
1434 }
1435
1436 #[cfg(all(feature = "enc-timelock-keygen-input", target_os = "macos"))]
1439 #[test]
1440 #[ignore = "slow (~2 s on M2) — run with `cargo test -- --ignored`"]
1441 fn balanced_mac_completes() {
1442 let t = TimeLockTime::new(10, 0).unwrap();
1443 let start = std::time::Instant::now();
1444 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedMac.params())
1445 .expect("BalancedMac should succeed");
1446 println!("BalancedMac: {:?}", start.elapsed());
1447 }
1448
1449 #[cfg(all(feature = "enc-timelock-keygen-input", target_os = "macos"))]
1450 #[test]
1451 #[ignore = "slow (~5–12 s on M2, faster on M3/M4) — run with `cargo test -- --ignored`"]
1452 fn paranoid_mac_completes() {
1453 let t = TimeLockTime::new(10, 0).unwrap();
1454 let start = std::time::Instant::now();
1455 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidMac.params())
1456 .expect("ParanoidMac should succeed");
1457 println!("ParanoidMac: {:?}", start.elapsed());
1458 }
1459
1460 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "x86_64"))]
1463 #[test]
1464 #[ignore = "slow (~1.5 s on typical x86-64) — run with `cargo test -- --ignored`"]
1465 fn balanced_x86_completes() {
1466 let t = TimeLockTime::new(10, 0).unwrap();
1467 let start = std::time::Instant::now();
1468 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedX86.params())
1469 .expect("BalancedX86 should succeed");
1470 println!("BalancedX86: {:?}", start.elapsed());
1471 }
1472
1473 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "x86_64"))]
1474 #[test]
1475 #[ignore = "slow (~8–15 s on typical x86-64) — run with `cargo test -- --ignored`"]
1476 fn paranoid_x86_completes() {
1477 let t = TimeLockTime::new(10, 0).unwrap();
1478 let start = std::time::Instant::now();
1479 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidX86.params())
1480 .expect("ParanoidX86 should succeed");
1481 println!("ParanoidX86: {:?}", start.elapsed());
1482 }
1483
1484 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "aarch64", not(target_os = "macos")))]
1487 #[test]
1488 #[ignore = "slow (~3 s on Graviton3) — run with `cargo test -- --ignored`"]
1489 fn balanced_arm_completes() {
1490 let t = TimeLockTime::new(10, 0).unwrap();
1491 let start = std::time::Instant::now();
1492 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedArm.params())
1493 .expect("BalancedArm should succeed");
1494 println!("BalancedArm: {:?}", start.elapsed());
1495 }
1496
1497 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "aarch64", not(target_os = "macos")))]
1498 #[test]
1499 #[ignore = "slow (~10–20 s on Graviton3) — run with `cargo test -- --ignored`"]
1500 fn paranoid_arm_completes() {
1501 let t = TimeLockTime::new(10, 0).unwrap();
1502 let start = std::time::Instant::now();
1503 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidArm.params())
1504 .expect("ParanoidArm should succeed");
1505 println!("ParanoidArm: {:?}", start.elapsed());
1506 }
1507
1508 #[cfg(feature = "enc-timelock-keygen-input")]
1511 #[test]
1512 fn scheduled_none_same_as_regular_at() {
1513 let s = salts();
1515 let t = TimeLockTime::new(14, 0).unwrap();
1516 let regular = derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast()).unwrap();
1517 let scheduled = derive_key_scheduled_at(
1518 TimeLockCadence::None, t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1519 ).unwrap();
1520 assert_eq!(regular.as_bytes(), scheduled.as_bytes());
1521 }
1522
1523 #[cfg(feature = "enc-timelock-keygen-input")]
1524 #[test]
1525 fn scheduled_different_weekdays_different_keys() {
1526 let s = salts();
1527 let t = TimeLockTime::new(18, 0).unwrap();
1528 let k_mon = derive_key_scheduled_at(
1529 TimeLockCadence::DayOfWeek(Weekday::Monday),
1530 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1531 ).unwrap();
1532 let k_tue = derive_key_scheduled_at(
1533 TimeLockCadence::DayOfWeek(Weekday::Tuesday),
1534 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1535 ).unwrap();
1536 assert_ne!(k_mon.as_bytes(), k_tue.as_bytes());
1537 }
1538
1539 #[cfg(feature = "enc-timelock-keygen-input")]
1540 #[test]
1541 fn scheduled_different_months_different_keys() {
1542 let s = salts();
1543 let t = TimeLockTime::new(0, 0).unwrap();
1544 let k_jan = derive_key_scheduled_at(
1545 TimeLockCadence::MonthOfYear(Month::January),
1546 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1547 ).unwrap();
1548 let k_feb = derive_key_scheduled_at(
1549 TimeLockCadence::MonthOfYear(Month::February),
1550 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1551 ).unwrap();
1552 assert_ne!(k_jan.as_bytes(), k_feb.as_bytes());
1553 }
1554
1555 #[cfg(feature = "enc-timelock-keygen-input")]
1556 #[test]
1557 fn scheduled_at_deterministic() {
1558 let s = salts();
1560 let t = TimeLockTime::new(6, 0).unwrap();
1561 let c = TimeLockCadence::DayOfWeekInMonth(Weekday::Friday, Month::March);
1562 let k1 = derive_key_scheduled_at(c, t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast()).unwrap();
1563 let s2 = TimeLockSalts::from_slice(&s.to_bytes());
1564 let k2 = derive_key_scheduled_at(c, t, TimePrecision::Hour, TimeFormat::Hour24, &s2, &fast()).unwrap();
1565 assert_eq!(k1.as_bytes(), k2.as_bytes());
1566 }
1567
1568 #[cfg(feature = "enc-timelock-keygen-now")]
1569 #[test]
1570 fn scheduled_now_none_matches_derive_now() {
1571 let s = salts();
1574 let f = fast();
1575 let stored = pack(
1576 TimePrecision::Hour, TimeFormat::Hour24,
1577 &TimeLockCadence::None,
1578 s.clone(),
1579 f,
1580 );
1581 let k1 = derive_key_now(TimePrecision::Hour, TimeFormat::Hour24, &s, &f).unwrap();
1582 let k2 = derive_key_scheduled_now(&stored).unwrap();
1583 assert_eq!(k1.as_bytes(), k2.as_bytes());
1584 }
1585
1586 #[cfg(any(feature = "enc-timelock-keygen-input", feature = "enc-timelock-keygen-now"))]
1587 #[test]
1588 fn pack_unpack_roundtrip() {
1589 let params = pack(
1590 TimePrecision::Minute,
1591 TimeFormat::Hour24,
1592 &TimeLockCadence::DayOfWeekInMonth(Weekday::Tuesday, Month::February),
1593 salts(),
1594 fast(),
1595 );
1596 assert_eq!(params.time_precision, 2); assert_eq!(params.time_format, 1); assert_eq!(params.cadence_variant, 4); let (p, f, v) = unpack(¶ms);
1600 assert!(matches!(p, TimePrecision::Minute));
1601 assert!(matches!(f, TimeFormat::Hour24));
1602 assert_eq!(v, 4);
1603 }
1604
1605 #[cfg(feature = "enc-timelock-keygen-input")]
1606 #[test]
1607 #[should_panic(expected = "DayOfMonthInMonth")]
1608 fn day_of_month_in_month_panics_on_invalid_day() {
1609 let s = salts();
1611 let t = TimeLockTime::new(0, 0).unwrap();
1612 let _ = derive_key_scheduled_at(
1613 TimeLockCadence::DayOfMonthInMonth(29, Month::February),
1614 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1615 );
1616 }
1617}