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"))]
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum TimePrecision {
126 Hour,
131
132 Quarter,
139
140 Minute,
146}
147
148#[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 Hour24,
154
155 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"))]
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum Weekday {
169 Monday,
170 Tuesday,
171 Wednesday,
172 Thursday,
173 Friday,
174 Saturday,
175 Sunday,
176}
177
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"))]
179impl Weekday {
180 pub fn name(self) -> &'static str {
182 match self {
183 Self::Monday => "Monday",
184 Self::Tuesday => "Tuesday",
185 Self::Wednesday => "Wednesday",
186 Self::Thursday => "Thursday",
187 Self::Friday => "Friday",
188 Self::Saturday => "Saturday",
189 Self::Sunday => "Sunday",
190 }
191 }
192
193 pub fn number(self) -> u8 {
195 match self {
196 Self::Monday => 0,
197 Self::Tuesday => 1,
198 Self::Wednesday => 2,
199 Self::Thursday => 3,
200 Self::Friday => 4,
201 Self::Saturday => 5,
202 Self::Sunday => 6,
203 }
204 }
205
206 #[cfg(feature = "enc-timelock-keygen-now")]
208 pub(crate) fn from_chrono(w: chrono::Weekday) -> Self {
209 match w {
210 chrono::Weekday::Mon => Self::Monday,
211 chrono::Weekday::Tue => Self::Tuesday,
212 chrono::Weekday::Wed => Self::Wednesday,
213 chrono::Weekday::Thu => Self::Thursday,
214 chrono::Weekday::Fri => Self::Friday,
215 chrono::Weekday::Sat => Self::Saturday,
216 chrono::Weekday::Sun => Self::Sunday,
217 }
218 }
219}
220
221#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum Month {
228 January,
229 February,
230 March,
231 April,
232 May,
233 June,
234 July,
235 August,
236 September,
237 October,
238 November,
239 December,
240}
241
242#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
243impl Month {
244 pub fn name(self) -> &'static str {
246 match self {
247 Self::January => "January",
248 Self::February => "February",
249 Self::March => "March",
250 Self::April => "April",
251 Self::May => "May",
252 Self::June => "June",
253 Self::July => "July",
254 Self::August => "August",
255 Self::September => "September",
256 Self::October => "October",
257 Self::November => "November",
258 Self::December => "December",
259 }
260 }
261
262 pub fn number(self) -> u8 {
264 match self {
265 Self::January => 1,
266 Self::February => 2,
267 Self::March => 3,
268 Self::April => 4,
269 Self::May => 5,
270 Self::June => 6,
271 Self::July => 7,
272 Self::August => 8,
273 Self::September => 9,
274 Self::October => 10,
275 Self::November => 11,
276 Self::December => 12,
277 }
278 }
279
280 pub fn max_days(self) -> u8 {
285 match self {
286 Self::February => 28,
287 Self::April | Self::June | Self::September | Self::November => 30,
288 _ => 31,
289 }
290 }
291
292 #[cfg(feature = "enc-timelock-keygen-now")]
298 pub(crate) fn from_number(n: u8) -> Self {
299 match n {
300 1 => Self::January,
301 2 => Self::February,
302 3 => Self::March,
303 4 => Self::April,
304 5 => Self::May,
305 6 => Self::June,
306 7 => Self::July,
307 8 => Self::August,
308 9 => Self::September,
309 10 => Self::October,
310 11 => Self::November,
311 12 => Self::December,
312 _ => panic!("Month::from_number: invalid month number {}", n),
313 }
314 }
315}
316
317#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub enum TimeLockCadence {
342 None,
346
347 DayOfWeek(Weekday),
351
352 DayOfMonth(u8),
358
359 MonthOfYear(Month),
363
364 DayOfWeekInMonth(Weekday, Month),
368
369 DayOfMonthInMonth(u8, Month),
375
376 DayOfWeekAndDayOfMonth(Weekday, u8),
382}
383
384#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
385impl TimeLockCadence {
386 pub fn variant_id(self) -> u8 {
389 match self {
390 Self::None => 0,
391 Self::DayOfWeek(_) => 1,
392 Self::DayOfMonth(_) => 2,
393 Self::MonthOfYear(_) => 3,
394 Self::DayOfWeekInMonth(_, _) => 4,
395 Self::DayOfMonthInMonth(_, _) => 5,
396 Self::DayOfWeekAndDayOfMonth(_, _) => 6,
397 }
398 }
399
400 pub(crate) fn bake_string(self) -> String {
410 match self {
411 Self::None => String::new(),
412 Self::DayOfWeek(w) => format!("{}|", w.name()),
413 Self::DayOfMonth(d) => format!("{}|", d),
414 Self::MonthOfYear(m) => format!("{}|", m.name()),
415 Self::DayOfWeekInMonth(w, m) => format!("{}+{}|", w.name(), m.name()),
416 Self::DayOfMonthInMonth(d, m) => {
417 let max = m.max_days();
418 if d < 1 || d > max {
419 panic!(
420 "TimeLockCadence::DayOfMonthInMonth: day {} is out of range \
421 1–{} for {}",
422 d, max, m.name()
423 );
424 }
425 format!("{}+{}|", d, m.name())
426 }
427 Self::DayOfWeekAndDayOfMonth(w, d) => format!("{}+{}|", w.name(), d),
428 }
429 }
430}
431
432#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
450pub struct TimeLockTime {
451 hour: u32, minute: u32, }
454
455#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
456impl TimeLockTime {
457 pub fn new(hour: u32, minute: u32) -> Option<Self> {
462 if hour > 23 || minute > 59 {
463 return None;
464 }
465 Some(Self { hour, minute })
466 }
467
468 #[inline]
470 pub fn hour(self) -> u32 { self.hour }
471
472 #[inline]
474 pub fn minute(self) -> u32 { self.minute }
475}
476
477#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub struct Argon2PassParams {
483 pub m_cost: u32,
485 pub t_cost: u32,
487 pub p_cost: u32,
489}
490
491#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
494pub struct ScryptPassParams {
495 pub log_n: u8,
497 pub r: u32,
499 pub p: u32,
501}
502
503#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub struct KdfParams {
512 pub pass1: Argon2PassParams,
514 pub pass2: ScryptPassParams,
516 pub pass3: Argon2PassParams,
518}
519
520#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583pub enum KdfPreset {
584 Fast,
588 Balanced,
590 Paranoid,
592
593 FastMac,
597 BalancedMac,
599 ParanoidMac,
602
603 FastX86,
607 BalancedX86,
609 ParanoidX86,
611
612 FastArm,
616 BalancedArm,
618 ParanoidArm,
620
621 Custom(KdfParams),
640}
641
642#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
643impl KdfPreset {
644 pub fn params(self) -> KdfParams {
646 let fast = KdfParams {
648 pass1: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
649 pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
650 pass3: Argon2PassParams { m_cost: 65_536, t_cost: 3, p_cost: 1 },
651 };
652 let balanced = KdfParams {
654 pass1: Argon2PassParams { m_cost: 524_288, t_cost: 4, p_cost: 1 },
655 pass2: ScryptPassParams { log_n: 17, r: 8, p: 1 },
656 pass3: Argon2PassParams { m_cost: 262_144, t_cost: 4, p_cost: 1 },
657 };
658 let paranoid = KdfParams {
660 pass1: Argon2PassParams { m_cost: 786_432, t_cost: 5, p_cost: 1 },
661 pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
662 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 },
663 };
664
665 match self {
666 KdfPreset::Fast | KdfPreset::FastX86 => fast,
668 KdfPreset::Balanced | KdfPreset::BalancedX86 => balanced,
669 KdfPreset::Paranoid | KdfPreset::ParanoidX86 => paranoid,
670 KdfPreset::FastMac => balanced, KdfPreset::BalancedMac => KdfParams {
673 pass1: Argon2PassParams { m_cost: 1_048_576, t_cost: 4, p_cost: 1 }, pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
675 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 4, p_cost: 1 },
676 },
677 KdfPreset::ParanoidMac => KdfParams {
678 pass1: Argon2PassParams { m_cost: 3_145_728, t_cost: 4, p_cost: 1 }, pass2: ScryptPassParams { log_n: 20, r: 8, p: 1 },
680 pass3: Argon2PassParams { m_cost: 1_048_576, t_cost: 4, p_cost: 1 }, },
682 KdfPreset::FastArm => KdfParams {
684 pass1: Argon2PassParams { m_cost: 262_144, t_cost: 3, p_cost: 1 }, pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
686 pass3: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
687 },
688 KdfPreset::BalancedArm => KdfParams {
689 pass1: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 }, pass2: ScryptPassParams { log_n: 17, r: 8, p: 1 },
691 pass3: Argon2PassParams { m_cost: 262_144, t_cost: 5, p_cost: 1 },
692 },
693 KdfPreset::ParanoidArm => KdfParams {
694 pass1: Argon2PassParams { m_cost: 786_432, t_cost: 5, p_cost: 1 }, pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
696 pass3: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 },
697 },
698 KdfPreset::Custom(p) => p,
700 }
701 }
702}
703
704#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
715#[derive(Debug, Clone)]
716pub struct TimeLockSalts {
717 pub s1: [u8; 32],
719 pub s2: [u8; 32],
721 pub s3: [u8; 32],
723}
724
725#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
726impl TimeLockSalts {
727 pub fn generate() -> Self {
729 use rand::RngCore as _;
730 let mut rng = rand::rng();
731 let mut s = Self { s1: [0u8; 32], s2: [0u8; 32], s3: [0u8; 32] };
732 rng.fill_bytes(&mut s.s1);
733 rng.fill_bytes(&mut s.s2);
734 rng.fill_bytes(&mut s.s3);
735 s
736 }
737
738 pub fn from_bytes(s1: [u8; 32], s2: [u8; 32], s3: [u8; 32]) -> Self {
740 Self { s1, s2, s3 }
741 }
742
743 pub fn to_bytes(&self) -> [u8; 96] {
745 let mut out = [0u8; 96];
746 out[..32].copy_from_slice(&self.s1);
747 out[32..64].copy_from_slice(&self.s2);
748 out[64..].copy_from_slice(&self.s3);
749 out
750 }
751
752 pub fn from_slice(b: &[u8; 96]) -> Self {
754 let mut s1 = [0u8; 32]; s1.copy_from_slice(&b[..32]);
755 let mut s2 = [0u8; 32]; s2.copy_from_slice(&b[32..64]);
756 let mut s3 = [0u8; 32]; s3.copy_from_slice(&b[64..]);
757 Self { s1, s2, s3 }
758 }
759}
760
761#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
762impl Zeroize for TimeLockSalts {
763 fn zeroize(&mut self) {
764 self.s1.zeroize();
765 self.s2.zeroize();
766 self.s3.zeroize();
767 }
768}
769
770#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
771impl Drop for TimeLockSalts {
772 fn drop(&mut self) { self.zeroize(); }
773}
774
775#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
782pub struct TimeLockKey([u8; 32]);
783
784#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
785impl TimeLockKey {
786 #[inline]
791 pub fn as_bytes(&self) -> &[u8; 32] { &self.0 }
792}
793
794#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
795impl Zeroize for TimeLockKey {
796 #[inline]
797 fn zeroize(&mut self) { self.0.zeroize(); }
798}
799
800#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
801impl ZeroizeOnDrop for TimeLockKey {}
802
803#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
804impl Drop for TimeLockKey {
805 fn drop(&mut self) { self.zeroize(); }
806}
807
808#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
812#[derive(Debug)]
813pub enum TimeLockError {
814 Argon2(String),
816 Scrypt(String),
818 #[cfg(feature = "enc-timelock-keygen-now")]
820 ClockUnavailable,
821 #[cfg(feature = "enc-timelock-keygen-input")]
823 InvalidTime(String),
824 #[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
826 TaskPanic(String),
827 ForbiddenAction(&'static str),
830}
831
832#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
833impl std::fmt::Display for TimeLockError {
834 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
835 match self {
836 Self::Argon2(s) => write!(f, "Argon2id error: {s}"),
837 Self::Scrypt(s) => write!(f, "scrypt error: {s}"),
838 #[cfg(feature = "enc-timelock-keygen-now")]
839 Self::ClockUnavailable => write!(f, "system clock unavailable"),
840 #[cfg(feature = "enc-timelock-keygen-input")]
841 Self::InvalidTime(s) => write!(f, "invalid time input: {s}"),
842 #[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
843 Self::TaskPanic(s) => write!(f, "KDF task panicked: {s}"),
844 Self::ForbiddenAction(s) => write!(f, "action not permitted: {s}"),
845 }
846 }
847}
848
849#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
850impl std::error::Error for TimeLockError {}
851
852#[allow(dead_code)]
883#[cfg(feature = "enc-timelock-keygen-now")]
884fn derive_key_now(
885 precision: TimePrecision,
886 format: TimeFormat,
887 salts: &TimeLockSalts,
888 params: &KdfParams,
889) -> Result<TimeLockKey, TimeLockError> {
890 let time_str = helper::format_time_now(precision, format)?;
891 helper::run_kdf_chain(time_str.into_bytes(), salts, params)
892}
893
894#[allow(dead_code)]
922#[cfg(feature = "enc-timelock-keygen-input")]
923fn derive_key_at(
924 time: TimeLockTime,
925 precision: TimePrecision,
926 format: TimeFormat,
927 salts: &TimeLockSalts,
928 params: &KdfParams,
929) -> Result<TimeLockKey, TimeLockError> {
930 let time_str = helper::format_time_at(time, precision, format)?;
931 helper::run_kdf_chain(time_str.into_bytes(), salts, params)
932}
933
934#[allow(dead_code)]
951#[cfg(feature = "enc-timelock-async-keygen-now")]
952async fn derive_key_now_async(
953 precision: TimePrecision,
954 format: TimeFormat,
955 salts: TimeLockSalts,
956 params: KdfParams,
957) -> Result<TimeLockKey, TimeLockError> {
958 tokio::task::spawn_blocking(move || derive_key_now(precision, format, &salts, ¶ms))
959 .await
960 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
961}
962
963#[allow(dead_code)]
970#[cfg(feature = "enc-timelock-async-keygen-input")]
971async fn derive_key_at_async(
972 time: TimeLockTime,
973 precision: TimePrecision,
974 format: TimeFormat,
975 salts: TimeLockSalts,
976 params: KdfParams,
977) -> Result<TimeLockKey, TimeLockError> {
978 tokio::task::spawn_blocking(move || derive_key_at(time, precision, format, &salts, ¶ms))
979 .await
980 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
981}
982
983#[cfg(feature = "enc-timelock-keygen-input")]
1026fn derive_key_scheduled_at(
1027 cadence: TimeLockCadence,
1028 time: TimeLockTime,
1029 precision: TimePrecision,
1030 format: TimeFormat,
1031 salts: &TimeLockSalts,
1032 params: &KdfParams,
1033) -> Result<TimeLockKey, TimeLockError> {
1034 let cadence_part = cadence.bake_string();
1035 let time_part = helper::format_time_at(time, precision, format)?;
1036 let full = format!("{}{}", cadence_part, time_part);
1037 helper::run_kdf_chain(full.into_bytes(), salts, params)
1038}
1039
1040#[cfg(feature = "enc-timelock-keygen-now")]
1064fn derive_key_scheduled_now(
1065 timelock_params: &TimeLockParams,
1066) -> Result<TimeLockKey, TimeLockError> {
1067 let (precision, format, cadence_variant) = utility::unpack(timelock_params);
1068 let cadence_part = helper::bake_cadence_now(cadence_variant)?;
1069 let time_part = helper::format_time_now(precision, format)?;
1070 let full = format!("{}{}", cadence_part, time_part);
1071 helper::run_kdf_chain(full.into_bytes(), &timelock_params.salts, &timelock_params.kdf_params)
1072}
1073
1074#[cfg(feature = "enc-timelock-async-keygen-input")]
1085async fn derive_key_scheduled_at_async(
1086 cadence: TimeLockCadence,
1087 time: TimeLockTime,
1088 precision: TimePrecision,
1089 format: TimeFormat,
1090 salts: TimeLockSalts,
1091 params: KdfParams,
1092) -> Result<TimeLockKey, TimeLockError> {
1093 tokio::task::spawn_blocking(move || {
1094 derive_key_scheduled_at(cadence, time, precision, format, &salts, ¶ms)
1095 })
1096 .await
1097 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
1098}
1099
1100#[cfg(feature = "enc-timelock-async-keygen-now")]
1108async fn derive_key_scheduled_now_async(
1109 timelock_params: TimeLockParams,
1110) -> Result<TimeLockKey, TimeLockError> {
1111 tokio::task::spawn_blocking(move || {
1112 derive_key_scheduled_now(&timelock_params)
1113 })
1114 .await
1115 .map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
1116}
1117
1118#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1172pub fn timelock(
1173 cadence: Option<TimeLockCadence>,
1174 time: Option<TimeLockTime>,
1175 precision: Option<TimePrecision>,
1176 format: Option<TimeFormat>,
1177 salts: Option<TimeLockSalts>,
1178 kdf: Option<KdfParams>,
1179 params: Option<TimeLockParams>,
1180) -> Result<TimeLockKey, TimeLockError> {
1181 if let Some(p) = params {
1182 let _ = (cadence, time, precision, format, salts, kdf); #[cfg(not(feature = "enc-timelock-keygen-now"))]
1185 return Err(TimeLockError::ForbiddenAction(
1186 "enc-timelock-keygen-now feature is required for the _now (decryption) path"
1187 ));
1188 #[cfg(feature = "enc-timelock-keygen-now")]
1189 return derive_key_scheduled_now(&p);
1190 } else {
1191 #[cfg(not(feature = "enc-timelock-keygen-input"))]
1193 return Err(TimeLockError::ForbiddenAction(
1194 "enc-timelock-keygen-input feature is required for the _at (encryption) path; \
1195 pass Some(TimeLockParams) for the decryption path (requires enc-timelock-keygen-now)"
1196 ));
1197 #[cfg(feature = "enc-timelock-keygen-input")]
1198 {
1199 let c = cadence.ok_or(TimeLockError::ForbiddenAction("_at path: cadence must be Some"))?;
1200 let t = time.ok_or(TimeLockError::ForbiddenAction("_at path: time must be Some"))?;
1201 let pr = precision.ok_or(TimeLockError::ForbiddenAction("_at path: precision must be Some"))?;
1202 let fm = format.ok_or(TimeLockError::ForbiddenAction("_at path: format must be Some"))?;
1203 let sl = salts.ok_or(TimeLockError::ForbiddenAction("_at path: salts must be Some"))?;
1204 let kd = kdf.ok_or(TimeLockError::ForbiddenAction("_at path: kdf must be Some"))?;
1205 return derive_key_scheduled_at(c, t, pr, fm, &sl, &kd);
1206 }
1207 }
1208}
1209
1210#[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
1223pub async fn timelock_async(
1224 cadence: Option<TimeLockCadence>,
1225 time: Option<TimeLockTime>,
1226 precision: Option<TimePrecision>,
1227 format: Option<TimeFormat>,
1228 salts: Option<TimeLockSalts>,
1229 kdf: Option<KdfParams>,
1230 params: Option<TimeLockParams>,
1231) -> Result<TimeLockKey, TimeLockError> {
1232 if let Some(p) = params {
1233 let _ = (cadence, time, precision, format, salts, kdf);
1234 #[cfg(not(feature = "enc-timelock-async-keygen-now"))]
1235 return Err(TimeLockError::ForbiddenAction(
1236 "enc-timelock-async-keygen-now feature is required for the async _now (decryption) path"
1237 ));
1238 #[cfg(feature = "enc-timelock-async-keygen-now")]
1239 return derive_key_scheduled_now_async(p).await;
1240 } else {
1241 #[cfg(not(feature = "enc-timelock-async-keygen-input"))]
1242 return Err(TimeLockError::ForbiddenAction(
1243 "enc-timelock-async-keygen-input feature is required for the async _at (encryption) path"
1244 ));
1245 #[cfg(feature = "enc-timelock-async-keygen-input")]
1246 {
1247 let c = cadence.ok_or(TimeLockError::ForbiddenAction("_at path: cadence must be Some"))?;
1248 let t = time.ok_or(TimeLockError::ForbiddenAction("_at path: time must be Some"))?;
1249 let pr = precision.ok_or(TimeLockError::ForbiddenAction("_at path: precision must be Some"))?;
1250 let fm = format.ok_or(TimeLockError::ForbiddenAction("_at path: format must be Some"))?;
1251 let sl = salts.ok_or(TimeLockError::ForbiddenAction("_at path: salts must be Some"))?;
1252 let kd = kdf.ok_or(TimeLockError::ForbiddenAction("_at path: kdf must be Some"))?;
1253 return derive_key_scheduled_at_async(c, t, pr, fm, sl, kd).await;
1254 }
1255 }
1256}
1257
1258#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1261#[cfg(test)]
1262mod tests {
1263 use super::*;
1264 #[cfg(all(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1265 use chrono::Timelike as _;
1266
1267 fn fast() -> KdfParams {
1268 KdfParams {
1270 pass1: Argon2PassParams { m_cost: 32_768, t_cost: 1, p_cost: 1 },
1271 pass2: ScryptPassParams { log_n: 13, r: 8, p: 1 },
1272 pass3: Argon2PassParams { m_cost: 16_384, t_cost: 1, p_cost: 1 },
1273 }
1274 }
1275 fn salts() -> TimeLockSalts { TimeLockSalts::generate() }
1276
1277 #[cfg(feature = "enc-timelock-keygen-input")]
1280 #[test]
1281 fn timelocktime_valid_range() {
1282 assert!(TimeLockTime::new(0, 0).is_some());
1283 assert!(TimeLockTime::new(23, 59).is_some());
1284 assert!(TimeLockTime::new(14, 37).is_some());
1285 }
1286
1287 #[cfg(feature = "enc-timelock-keygen-input")]
1288 #[test]
1289 fn timelocktime_invalid_range() {
1290 assert!(TimeLockTime::new(24, 0).is_none(), "hour=24 should fail");
1291 assert!(TimeLockTime::new( 0, 60).is_none(), "minute=60 should fail");
1292 assert!(TimeLockTime::new(99, 99).is_none());
1293 }
1294
1295 #[test]
1298 fn format_hour_24h() {
1299 let s = helper::format_components(14, 37, TimePrecision::Hour, TimeFormat::Hour24);
1300 assert_eq!(s, "14");
1301 }
1302
1303 #[test]
1304 fn format_hour_12h() {
1305 let s_pm = helper::format_components(14, 0, TimePrecision::Hour, TimeFormat::Hour12);
1306 let s_am = helper::format_components( 2, 0, TimePrecision::Hour, TimeFormat::Hour12);
1307 assert_eq!(s_pm, "02PM");
1308 assert_eq!(s_am, "02AM");
1309 }
1310
1311 #[test]
1312 fn format_quarter_snaps_correctly() {
1313 assert_eq!(helper::format_components(14, 37, TimePrecision::Quarter, TimeFormat::Hour24), "14:30");
1315 assert_eq!(helper::format_components(14, 15, TimePrecision::Quarter, TimeFormat::Hour24), "14:15");
1316 assert_eq!(helper::format_components(14, 0, TimePrecision::Quarter, TimeFormat::Hour24), "14:00");
1317 assert_eq!(helper::format_components(14, 59, TimePrecision::Quarter, TimeFormat::Hour24), "14:45");
1318 }
1319
1320 #[test]
1321 fn format_minute_exact() {
1322 let s = helper::format_components(9, 5, TimePrecision::Minute, TimeFormat::Hour24);
1323 assert_eq!(s, "09:05");
1324 }
1325
1326 #[cfg(feature = "enc-timelock-keygen-input")]
1329 #[test]
1330 fn at_same_inputs_same_key() {
1331 let s = salts();
1332 let t = TimeLockTime::new(14, 37).unwrap();
1333 let k1 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1334 let s2 = TimeLockSalts::from_slice(&s.to_bytes());
1336 let k2 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s2, &fast()).unwrap();
1337 assert_eq!(k1.as_bytes(), k2.as_bytes());
1338 }
1339
1340 #[cfg(feature = "enc-timelock-keygen-input")]
1341 #[test]
1342 fn at_different_salts_different_key() {
1343 let t = TimeLockTime::new(14, 37).unwrap();
1344 let k1 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1345 let k2 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1346 assert_ne!(k1.as_bytes(), k2.as_bytes());
1347 }
1348
1349 #[cfg(feature = "enc-timelock-keygen-input")]
1350 #[test]
1351 fn at_different_time_different_key() {
1352 let s = salts();
1353 let t1 = TimeLockTime::new(14, 37).unwrap();
1354 let t2 = TimeLockTime::new(14, 38).unwrap();
1355 let k1 = derive_key_at(t1, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1356 let k2 = derive_key_at(t2, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1357 assert_ne!(k1.as_bytes(), k2.as_bytes());
1358 }
1359
1360 #[cfg(feature = "enc-timelock-keygen-now")]
1363 #[test]
1364 fn now_returns_nonzero_key() {
1365 let k = derive_key_now(TimePrecision::Hour, TimeFormat::Hour24, &salts(), &fast()).unwrap();
1366 assert_ne!(k.as_bytes(), &[0u8; 32]);
1367 }
1368
1369 #[cfg(all(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
1370 #[test]
1371 fn now_and_at_same_minute_match() {
1372 let now = chrono::Local::now();
1375 let t = TimeLockTime::new(now.hour(), now.minute()).unwrap();
1376 let s = salts();
1377 let kn = derive_key_now(TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1378 let ka = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
1379 assert_eq!(
1380 kn.as_bytes(), ka.as_bytes(),
1381 "now and explicit current time must produce the same key"
1382 );
1383 }
1384
1385 #[test]
1388 fn salt_round_trip() {
1389 let s = salts();
1390 let b = s.to_bytes();
1391 let s2 = TimeLockSalts::from_slice(&b);
1392 assert_eq!(s.s1, s2.s1);
1393 assert_eq!(s.s2, s2.s2);
1394 assert_eq!(s.s3, s2.s3);
1395 }
1396
1397 #[cfg(feature = "enc-timelock-keygen-input")]
1400 #[test]
1401 fn custom_params_works() {
1402 let preset = KdfPreset::Custom(KdfPreset::Fast.params());
1404 let t = TimeLockTime::new(10, 0).unwrap();
1405 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &preset.params())
1406 .expect("Custom params should succeed");
1407 }
1408
1409 #[cfg(feature = "enc-timelock-keygen-input")]
1410 #[test]
1411 fn custom_params_roundtrip_eq() {
1412 let p = KdfPreset::Fast.params();
1413 assert_eq!(KdfPreset::Custom(p).params(), p);
1414 }
1415
1416 #[cfg(feature = "enc-timelock-keygen-input")]
1419 #[test]
1420 #[ignore = "slow (~400–600 ms) — run with `cargo test -- --ignored`"]
1421 fn balanced_preset_completes() {
1422 let t = TimeLockTime::new(10, 0).unwrap();
1423 let start = std::time::Instant::now();
1424 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::Balanced.params())
1425 .expect("Balanced should succeed");
1426 println!("Balanced (generic): {:?}", start.elapsed());
1427 }
1428
1429 #[cfg(feature = "enc-timelock-keygen-input")]
1430 #[test]
1431 #[ignore = "slow (~2 s on Mac, ~8–15 s on x86) — run with `cargo test -- --ignored`"]
1432 fn paranoid_preset_completes() {
1433 let t = TimeLockTime::new(10, 0).unwrap();
1434 let start = std::time::Instant::now();
1435 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::Paranoid.params())
1436 .expect("Paranoid should succeed");
1437 println!("Paranoid (generic): {:?}", start.elapsed());
1438 }
1439
1440 #[cfg(all(feature = "enc-timelock-keygen-input", target_os = "macos"))]
1443 #[test]
1444 #[ignore = "slow (~2 s on M2) — run with `cargo test -- --ignored`"]
1445 fn balanced_mac_completes() {
1446 let t = TimeLockTime::new(10, 0).unwrap();
1447 let start = std::time::Instant::now();
1448 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedMac.params())
1449 .expect("BalancedMac should succeed");
1450 println!("BalancedMac: {:?}", start.elapsed());
1451 }
1452
1453 #[cfg(all(feature = "enc-timelock-keygen-input", target_os = "macos"))]
1454 #[test]
1455 #[ignore = "slow (~5–12 s on M2, faster on M3/M4) — run with `cargo test -- --ignored`"]
1456 fn paranoid_mac_completes() {
1457 let t = TimeLockTime::new(10, 0).unwrap();
1458 let start = std::time::Instant::now();
1459 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidMac.params())
1460 .expect("ParanoidMac should succeed");
1461 println!("ParanoidMac: {:?}", start.elapsed());
1462 }
1463
1464 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "x86_64"))]
1467 #[test]
1468 #[ignore = "slow (~1.5 s on typical x86-64) — run with `cargo test -- --ignored`"]
1469 fn balanced_x86_completes() {
1470 let t = TimeLockTime::new(10, 0).unwrap();
1471 let start = std::time::Instant::now();
1472 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedX86.params())
1473 .expect("BalancedX86 should succeed");
1474 println!("BalancedX86: {:?}", start.elapsed());
1475 }
1476
1477 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "x86_64"))]
1478 #[test]
1479 #[ignore = "slow (~8–15 s on typical x86-64) — run with `cargo test -- --ignored`"]
1480 fn paranoid_x86_completes() {
1481 let t = TimeLockTime::new(10, 0).unwrap();
1482 let start = std::time::Instant::now();
1483 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidX86.params())
1484 .expect("ParanoidX86 should succeed");
1485 println!("ParanoidX86: {:?}", start.elapsed());
1486 }
1487
1488 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "aarch64", not(target_os = "macos")))]
1491 #[test]
1492 #[ignore = "slow (~3 s on Graviton3) — run with `cargo test -- --ignored`"]
1493 fn balanced_arm_completes() {
1494 let t = TimeLockTime::new(10, 0).unwrap();
1495 let start = std::time::Instant::now();
1496 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedArm.params())
1497 .expect("BalancedArm should succeed");
1498 println!("BalancedArm: {:?}", start.elapsed());
1499 }
1500
1501 #[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "aarch64", not(target_os = "macos")))]
1502 #[test]
1503 #[ignore = "slow (~10–20 s on Graviton3) — run with `cargo test -- --ignored`"]
1504 fn paranoid_arm_completes() {
1505 let t = TimeLockTime::new(10, 0).unwrap();
1506 let start = std::time::Instant::now();
1507 derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidArm.params())
1508 .expect("ParanoidArm should succeed");
1509 println!("ParanoidArm: {:?}", start.elapsed());
1510 }
1511
1512 #[cfg(feature = "enc-timelock-keygen-input")]
1515 #[test]
1516 fn scheduled_none_same_as_regular_at() {
1517 let s = salts();
1519 let t = TimeLockTime::new(14, 0).unwrap();
1520 let regular = derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast()).unwrap();
1521 let scheduled = derive_key_scheduled_at(
1522 TimeLockCadence::None, t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1523 ).unwrap();
1524 assert_eq!(regular.as_bytes(), scheduled.as_bytes());
1525 }
1526
1527 #[cfg(feature = "enc-timelock-keygen-input")]
1528 #[test]
1529 fn scheduled_different_weekdays_different_keys() {
1530 let s = salts();
1531 let t = TimeLockTime::new(18, 0).unwrap();
1532 let k_mon = derive_key_scheduled_at(
1533 TimeLockCadence::DayOfWeek(Weekday::Monday),
1534 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1535 ).unwrap();
1536 let k_tue = derive_key_scheduled_at(
1537 TimeLockCadence::DayOfWeek(Weekday::Tuesday),
1538 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1539 ).unwrap();
1540 assert_ne!(k_mon.as_bytes(), k_tue.as_bytes());
1541 }
1542
1543 #[cfg(feature = "enc-timelock-keygen-input")]
1544 #[test]
1545 fn scheduled_different_months_different_keys() {
1546 let s = salts();
1547 let t = TimeLockTime::new(0, 0).unwrap();
1548 let k_jan = derive_key_scheduled_at(
1549 TimeLockCadence::MonthOfYear(Month::January),
1550 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1551 ).unwrap();
1552 let k_feb = derive_key_scheduled_at(
1553 TimeLockCadence::MonthOfYear(Month::February),
1554 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1555 ).unwrap();
1556 assert_ne!(k_jan.as_bytes(), k_feb.as_bytes());
1557 }
1558
1559 #[cfg(feature = "enc-timelock-keygen-input")]
1560 #[test]
1561 fn scheduled_at_deterministic() {
1562 let s = salts();
1564 let t = TimeLockTime::new(6, 0).unwrap();
1565 let c = TimeLockCadence::DayOfWeekInMonth(Weekday::Friday, Month::March);
1566 let k1 = derive_key_scheduled_at(c, t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast()).unwrap();
1567 let s2 = TimeLockSalts::from_slice(&s.to_bytes());
1568 let k2 = derive_key_scheduled_at(c, t, TimePrecision::Hour, TimeFormat::Hour24, &s2, &fast()).unwrap();
1569 assert_eq!(k1.as_bytes(), k2.as_bytes());
1570 }
1571
1572 #[cfg(feature = "enc-timelock-keygen-now")]
1573 #[test]
1574 fn scheduled_now_none_matches_derive_now() {
1575 let s = salts();
1578 let f = fast();
1579 let stored = pack(
1580 TimePrecision::Hour, TimeFormat::Hour24,
1581 &TimeLockCadence::None,
1582 s.clone(),
1583 f,
1584 );
1585 let k1 = derive_key_now(TimePrecision::Hour, TimeFormat::Hour24, &s, &f).unwrap();
1586 let k2 = derive_key_scheduled_now(&stored).unwrap();
1587 assert_eq!(k1.as_bytes(), k2.as_bytes());
1588 }
1589
1590 #[cfg(any(feature = "enc-timelock-keygen-input", feature = "enc-timelock-keygen-now"))]
1591 #[test]
1592 fn pack_unpack_roundtrip() {
1593 let params = pack(
1594 TimePrecision::Minute,
1595 TimeFormat::Hour24,
1596 &TimeLockCadence::DayOfWeekInMonth(Weekday::Tuesday, Month::February),
1597 salts(),
1598 fast(),
1599 );
1600 assert_eq!(params.time_precision, 2); assert_eq!(params.time_format, 1); assert_eq!(params.cadence_variant, 4); let (p, f, v) = unpack(¶ms);
1604 assert!(matches!(p, TimePrecision::Minute));
1605 assert!(matches!(f, TimeFormat::Hour24));
1606 assert_eq!(v, 4);
1607 }
1608
1609 #[cfg(feature = "enc-timelock-keygen-input")]
1610 #[test]
1611 #[should_panic(expected = "DayOfMonthInMonth")]
1612 fn day_of_month_in_month_panics_on_invalid_day() {
1613 let s = salts();
1615 let t = TimeLockTime::new(0, 0).unwrap();
1616 let _ = derive_key_scheduled_at(
1617 TimeLockCadence::DayOfMonthInMonth(29, Month::February),
1618 t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
1619 );
1620 }
1621}