#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
mod helper;
#[cfg(feature = "backend-deps")]
pub mod backend_deps;
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
pub mod utility;
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
pub use utility::{TimeLockParams, pack, unpack};
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
use zeroize::{Zeroize, ZeroizeOnDrop};
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimePrecision {
Hour,
Quarter,
Minute,
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimeFormat {
Hour24,
Hour12,
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Default for TimeFormat {
fn default() -> Self { Self::Hour24 }
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Default for TimePrecision {
fn default() -> Self { Self::Minute }
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Weekday {
pub fn name(self) -> &'static str {
match self {
Self::Monday => "Monday",
Self::Tuesday => "Tuesday",
Self::Wednesday => "Wednesday",
Self::Thursday => "Thursday",
Self::Friday => "Friday",
Self::Saturday => "Saturday",
Self::Sunday => "Sunday",
}
}
pub fn number(self) -> u8 {
match self {
Self::Monday => 0,
Self::Tuesday => 1,
Self::Wednesday => 2,
Self::Thursday => 3,
Self::Friday => 4,
Self::Saturday => 5,
Self::Sunday => 6,
}
}
#[cfg(feature = "enc-timelock-keygen-now")]
pub(crate) fn from_chrono(w: chrono::Weekday) -> Self {
match w {
chrono::Weekday::Mon => Self::Monday,
chrono::Weekday::Tue => Self::Tuesday,
chrono::Weekday::Wed => Self::Wednesday,
chrono::Weekday::Thu => Self::Thursday,
chrono::Weekday::Fri => Self::Friday,
chrono::Weekday::Sat => Self::Saturday,
chrono::Weekday::Sun => Self::Sunday,
}
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Month {
January,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December,
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Month {
pub fn name(self) -> &'static str {
match self {
Self::January => "January",
Self::February => "February",
Self::March => "March",
Self::April => "April",
Self::May => "May",
Self::June => "June",
Self::July => "July",
Self::August => "August",
Self::September => "September",
Self::October => "October",
Self::November => "November",
Self::December => "December",
}
}
pub fn number(self) -> u8 {
match self {
Self::January => 1,
Self::February => 2,
Self::March => 3,
Self::April => 4,
Self::May => 5,
Self::June => 6,
Self::July => 7,
Self::August => 8,
Self::September => 9,
Self::October => 10,
Self::November => 11,
Self::December => 12,
}
}
pub fn max_days(self) -> u8 {
match self {
Self::February => 28,
Self::April | Self::June | Self::September | Self::November => 30,
_ => 31,
}
}
#[cfg(feature = "enc-timelock-keygen-now")]
pub(crate) fn from_number(n: u8) -> Self {
match n {
1 => Self::January,
2 => Self::February,
3 => Self::March,
4 => Self::April,
5 => Self::May,
6 => Self::June,
7 => Self::July,
8 => Self::August,
9 => Self::September,
10 => Self::October,
11 => Self::November,
12 => Self::December,
_ => panic!("Month::from_number: invalid month number {}", n),
}
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimeLockCadence {
None,
DayOfWeek(Weekday),
DayOfMonth(u8),
MonthOfYear(Month),
DayOfWeekInMonth(Weekday, Month),
DayOfMonthInMonth(u8, Month),
DayOfWeekAndDayOfMonth(Weekday, u8),
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Default for TimeLockCadence {
fn default() -> Self { Self::None }
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl TimeLockCadence {
pub fn variant_id(self) -> u8 {
match self {
Self::None => 0,
Self::DayOfWeek(_) => 1,
Self::DayOfMonth(_) => 2,
Self::MonthOfYear(_) => 3,
Self::DayOfWeekInMonth(_, _) => 4,
Self::DayOfMonthInMonth(_, _) => 5,
Self::DayOfWeekAndDayOfMonth(_, _) => 6,
}
}
pub(crate) fn bake_string(self) -> Result<String, TimeLockError> {
match self {
Self::None => Ok(String::new()),
Self::DayOfWeek(w) => Ok(format!("{}|", w.name())),
Self::DayOfMonth(d) => Ok(format!("{}|", d)),
Self::MonthOfYear(m) => Ok(format!("{}|", m.name())),
Self::DayOfWeekInMonth(w, m) => Ok(format!("{}+{}|", w.name(), m.name())),
Self::DayOfMonthInMonth(d, m) => {
let max = m.max_days();
if d < 1 || d > max {
return Err(TimeLockError::ForbiddenAction(
"DayOfMonthInMonth: day is out of range for the specified month",
));
}
Ok(format!("{}+{}|", d, m.name()))
}
Self::DayOfWeekAndDayOfMonth(w, d) => Ok(format!("{}+{}|", w.name(), d)),
}
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeLockTime {
hour: u32, minute: u32, }
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl TimeLockTime {
pub fn new(hour: u32, minute: u32) -> Option<Self> {
if hour > 23 || minute > 59 {
return None;
}
Some(Self { hour, minute })
}
#[inline]
pub fn hour(self) -> u32 { self.hour }
#[inline]
pub fn minute(self) -> u32 { self.minute }
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Argon2PassParams {
pub m_cost: u32,
pub t_cost: u32,
pub p_cost: u32,
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScryptPassParams {
pub log_n: u8,
pub r: u32,
pub p: u32,
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KdfParams {
pub pass1: Argon2PassParams,
pub pass2: ScryptPassParams,
pub pass3: Argon2PassParams,
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum KdfPreset {
Fast,
Balanced,
Paranoid,
FastMac,
BalancedMac,
ParanoidMac,
FastX86,
BalancedX86,
ParanoidX86,
FastArm,
BalancedArm,
ParanoidArm,
Custom(KdfParams),
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl KdfPreset {
pub fn params(self) -> KdfParams {
let fast = KdfParams {
pass1: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 65_536, t_cost: 3, p_cost: 1 },
};
let balanced = KdfParams {
pass1: Argon2PassParams { m_cost: 524_288, t_cost: 4, p_cost: 1 },
pass2: ScryptPassParams { log_n: 17, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 262_144, t_cost: 4, p_cost: 1 },
};
let paranoid = KdfParams {
pass1: Argon2PassParams { m_cost: 786_432, t_cost: 5, p_cost: 1 },
pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 },
};
match self {
KdfPreset::Fast | KdfPreset::FastX86 => fast,
KdfPreset::Balanced | KdfPreset::BalancedX86 => balanced,
KdfPreset::Paranoid | KdfPreset::ParanoidX86 => paranoid,
KdfPreset::FastMac => balanced, KdfPreset::BalancedMac => KdfParams {
pass1: Argon2PassParams { m_cost: 1_048_576, t_cost: 4, p_cost: 1 }, pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 524_288, t_cost: 4, p_cost: 1 },
},
KdfPreset::ParanoidMac => KdfParams {
pass1: Argon2PassParams { m_cost: 3_145_728, t_cost: 4, p_cost: 1 }, pass2: ScryptPassParams { log_n: 20, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 1_048_576, t_cost: 4, p_cost: 1 }, },
KdfPreset::FastArm => KdfParams {
pass1: Argon2PassParams { m_cost: 262_144, t_cost: 3, p_cost: 1 }, pass2: ScryptPassParams { log_n: 16, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 131_072, t_cost: 3, p_cost: 1 },
},
KdfPreset::BalancedArm => KdfParams {
pass1: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 }, pass2: ScryptPassParams { log_n: 17, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 262_144, t_cost: 5, p_cost: 1 },
},
KdfPreset::ParanoidArm => KdfParams {
pass1: Argon2PassParams { m_cost: 786_432, t_cost: 5, p_cost: 1 }, pass2: ScryptPassParams { log_n: 18, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 524_288, t_cost: 5, p_cost: 1 },
},
KdfPreset::Custom(p) => p,
}
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug, Clone)]
pub struct TimeLockSalts {
pub s1: [u8; 32],
pub s2: [u8; 32],
pub s3: [u8; 32],
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl TimeLockSalts {
pub fn generate() -> Self {
use rand::RngCore as _;
let mut rng = rand::rng();
let mut s = Self { s1: [0u8; 32], s2: [0u8; 32], s3: [0u8; 32] };
rng.fill_bytes(&mut s.s1);
rng.fill_bytes(&mut s.s2);
rng.fill_bytes(&mut s.s3);
s
}
pub fn from_bytes(s1: [u8; 32], s2: [u8; 32], s3: [u8; 32]) -> Self {
Self { s1, s2, s3 }
}
pub fn to_bytes(&self) -> [u8; 96] {
let mut out = [0u8; 96];
out[..32].copy_from_slice(&self.s1);
out[32..64].copy_from_slice(&self.s2);
out[64..].copy_from_slice(&self.s3);
out
}
pub fn from_slice(b: &[u8; 96]) -> Self {
let mut s1 = [0u8; 32]; s1.copy_from_slice(&b[..32]);
let mut s2 = [0u8; 32]; s2.copy_from_slice(&b[32..64]);
let mut s3 = [0u8; 32]; s3.copy_from_slice(&b[64..]);
Self { s1, s2, s3 }
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Zeroize for TimeLockSalts {
fn zeroize(&mut self) {
self.s1.zeroize();
self.s2.zeroize();
self.s3.zeroize();
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Drop for TimeLockSalts {
fn drop(&mut self) { self.zeroize(); }
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
pub struct TimeLockKey([u8; 32]);
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl TimeLockKey {
#[inline]
pub fn as_bytes(&self) -> &[u8; 32] { &self.0 }
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Zeroize for TimeLockKey {
#[inline]
fn zeroize(&mut self) { self.0.zeroize(); }
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl ZeroizeOnDrop for TimeLockKey {}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl Drop for TimeLockKey {
fn drop(&mut self) { self.zeroize(); }
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
#[derive(Debug)]
#[non_exhaustive]
pub enum TimeLockError {
Argon2(String),
Scrypt(String),
#[cfg(feature = "enc-timelock-keygen-now")]
ClockUnavailable,
#[cfg(feature = "enc-timelock-keygen-input")]
InvalidTime(String),
#[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
TaskPanic(String),
ForbiddenAction(&'static str),
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl std::fmt::Display for TimeLockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Argon2(s) => write!(f, "Argon2id error: {s}"),
Self::Scrypt(s) => write!(f, "scrypt error: {s}"),
#[cfg(feature = "enc-timelock-keygen-now")]
Self::ClockUnavailable => write!(f, "system clock unavailable"),
#[cfg(feature = "enc-timelock-keygen-input")]
Self::InvalidTime(s) => write!(f, "invalid time input: {s}"),
#[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
Self::TaskPanic(s) => write!(f, "KDF task panicked: {s}"),
Self::ForbiddenAction(s) => write!(f, "action not permitted: {s}"),
}
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl std::error::Error for TimeLockError {}
#[allow(dead_code)]
#[cfg(feature = "enc-timelock-keygen-now")]
fn derive_key_now(
precision: TimePrecision,
format: TimeFormat,
salts: &TimeLockSalts,
params: &KdfParams,
) -> Result<TimeLockKey, TimeLockError> {
let time_str = helper::format_time_now(precision, format)?;
helper::run_kdf_chain(time_str.into_bytes(), salts, params)
}
#[allow(dead_code)]
#[cfg(feature = "enc-timelock-keygen-input")]
fn derive_key_at(
time: TimeLockTime,
precision: TimePrecision,
format: TimeFormat,
salts: &TimeLockSalts,
params: &KdfParams,
) -> Result<TimeLockKey, TimeLockError> {
let time_str = helper::format_time_at(time, precision, format)?;
helper::run_kdf_chain(time_str.into_bytes(), salts, params)
}
#[allow(dead_code)]
#[cfg(feature = "enc-timelock-async-keygen-now")]
async fn derive_key_now_async(
precision: TimePrecision,
format: TimeFormat,
salts: TimeLockSalts,
params: KdfParams,
) -> Result<TimeLockKey, TimeLockError> {
tokio::task::spawn_blocking(move || derive_key_now(precision, format, &salts, ¶ms))
.await
.map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
}
#[allow(dead_code)]
#[cfg(feature = "enc-timelock-async-keygen-input")]
async fn derive_key_at_async(
time: TimeLockTime,
precision: TimePrecision,
format: TimeFormat,
salts: TimeLockSalts,
params: KdfParams,
) -> Result<TimeLockKey, TimeLockError> {
tokio::task::spawn_blocking(move || derive_key_at(time, precision, format, &salts, ¶ms))
.await
.map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
}
#[cfg(feature = "enc-timelock-keygen-input")]
fn derive_key_scheduled_at(
cadence: TimeLockCadence,
time: TimeLockTime,
precision: TimePrecision,
format: TimeFormat,
salts: &TimeLockSalts,
params: &KdfParams,
) -> Result<TimeLockKey, TimeLockError> {
let cadence_part = cadence.bake_string()?;
let time_part = helper::format_time_at(time, precision, format)?;
let full = format!("{}{}", cadence_part, time_part);
helper::run_kdf_chain(full.into_bytes(), salts, params)
}
#[cfg(feature = "enc-timelock-keygen-now")]
fn derive_key_scheduled_now(
timelock_params: &TimeLockParams,
) -> Result<TimeLockKey, TimeLockError> {
let (precision, format, cadence_variant) = utility::unpack(timelock_params);
let cadence_part = helper::bake_cadence_now(cadence_variant)?;
let time_part = helper::format_time_now(precision, format)?;
let full = format!("{}{}", cadence_part, time_part);
helper::run_kdf_chain(full.into_bytes(), &timelock_params.salts, &timelock_params.kdf_params)
}
#[cfg(feature = "enc-timelock-async-keygen-input")]
async fn derive_key_scheduled_at_async(
cadence: TimeLockCadence,
time: TimeLockTime,
precision: TimePrecision,
format: TimeFormat,
salts: TimeLockSalts,
params: KdfParams,
) -> Result<TimeLockKey, TimeLockError> {
tokio::task::spawn_blocking(move || {
derive_key_scheduled_at(cadence, time, precision, format, &salts, ¶ms)
})
.await
.map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
}
#[cfg(feature = "enc-timelock-async-keygen-now")]
async fn derive_key_scheduled_now_async(
timelock_params: TimeLockParams,
) -> Result<TimeLockKey, TimeLockError> {
tokio::task::spawn_blocking(move || {
derive_key_scheduled_now(&timelock_params)
})
.await
.map_err(|e| TimeLockError::TaskPanic(e.to_string()))?
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
pub fn timelock(
cadence: Option<TimeLockCadence>,
time: Option<TimeLockTime>,
precision: Option<TimePrecision>,
format: Option<TimeFormat>,
salts: Option<TimeLockSalts>,
kdf: Option<KdfParams>,
params: Option<TimeLockParams>,
) -> Result<TimeLockKey, TimeLockError> {
if let Some(p) = params {
let _ = (cadence, time, precision, format, salts, kdf); #[cfg(not(feature = "enc-timelock-keygen-now"))]
return Err(TimeLockError::ForbiddenAction(
"enc-timelock-keygen-now feature is required for the _now (decryption) path"
));
#[cfg(feature = "enc-timelock-keygen-now")]
return derive_key_scheduled_now(&p);
} else {
#[cfg(not(feature = "enc-timelock-keygen-input"))]
return Err(TimeLockError::ForbiddenAction(
"enc-timelock-keygen-input feature is required for the _at (encryption) path; \
pass Some(TimeLockParams) for the decryption path (requires enc-timelock-keygen-now)"
));
#[cfg(feature = "enc-timelock-keygen-input")]
{
let c = cadence.ok_or(TimeLockError::ForbiddenAction("_at path: cadence must be Some"))?;
let t = time.ok_or(TimeLockError::ForbiddenAction("_at path: time must be Some"))?;
let pr = precision.ok_or(TimeLockError::ForbiddenAction("_at path: precision must be Some"))?;
let fm = format.ok_or(TimeLockError::ForbiddenAction("_at path: format must be Some"))?;
let sl = salts.ok_or(TimeLockError::ForbiddenAction("_at path: salts must be Some"))?;
let kd = kdf.ok_or(TimeLockError::ForbiddenAction("_at path: kdf must be Some"))?;
return derive_key_scheduled_at(c, t, pr, fm, &sl, &kd);
}
}
}
#[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
pub async fn timelock_async(
cadence: Option<TimeLockCadence>,
time: Option<TimeLockTime>,
precision: Option<TimePrecision>,
format: Option<TimeFormat>,
salts: Option<TimeLockSalts>,
kdf: Option<KdfParams>,
params: Option<TimeLockParams>,
) -> Result<TimeLockKey, TimeLockError> {
if let Some(p) = params {
let _ = (cadence, time, precision, format, salts, kdf);
#[cfg(not(feature = "enc-timelock-async-keygen-now"))]
return Err(TimeLockError::ForbiddenAction(
"enc-timelock-async-keygen-now feature is required for the async _now (decryption) path"
));
#[cfg(feature = "enc-timelock-async-keygen-now")]
return derive_key_scheduled_now_async(p).await;
} else {
#[cfg(not(feature = "enc-timelock-async-keygen-input"))]
return Err(TimeLockError::ForbiddenAction(
"enc-timelock-async-keygen-input feature is required for the async _at (encryption) path"
));
#[cfg(feature = "enc-timelock-async-keygen-input")]
{
let c = cadence.ok_or(TimeLockError::ForbiddenAction("_at path: cadence must be Some"))?;
let t = time.ok_or(TimeLockError::ForbiddenAction("_at path: time must be Some"))?;
let pr = precision.ok_or(TimeLockError::ForbiddenAction("_at path: precision must be Some"))?;
let fm = format.ok_or(TimeLockError::ForbiddenAction("_at path: format must be Some"))?;
let sl = salts.ok_or(TimeLockError::ForbiddenAction("_at path: salts must be Some"))?;
let kd = kdf.ok_or(TimeLockError::ForbiddenAction("_at path: kdf must be Some"))?;
return derive_key_scheduled_at_async(c, t, pr, fm, sl, kd).await;
}
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
pub struct TimelockBuilder {
cadence: Option<TimeLockCadence>,
time: Option<TimeLockTime>,
precision: Option<TimePrecision>,
format: Option<TimeFormat>,
salts: Option<TimeLockSalts>,
kdf: Option<KdfParams>,
params: Option<TimeLockParams>,
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
impl TimelockBuilder {
#[cfg(any(feature = "enc-timelock-keygen-input", feature = "enc-timelock-async-keygen-input"))]
pub fn encrypt() -> Self {
Self {
cadence: Some(TimeLockCadence::None),
time: None,
precision: Some(TimePrecision::Minute),
format: Some(TimeFormat::Hour24),
salts: None,
kdf: None,
params: None,
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-async-keygen-now"))]
pub fn decrypt(params: TimeLockParams) -> Self {
Self {
cadence: None,
time: None,
precision: None,
format: None,
salts: None,
kdf: None,
params: Some(params),
}
}
pub fn cadence(mut self, cadence: TimeLockCadence) -> Self {
self.cadence = Some(cadence);
self
}
pub fn time(mut self, time: TimeLockTime) -> Self {
self.time = Some(time);
self
}
pub fn precision(mut self, precision: TimePrecision) -> Self {
self.precision = Some(precision);
self
}
pub fn format(mut self, format: TimeFormat) -> Self {
self.format = Some(format);
self
}
pub fn salts(mut self, salts: TimeLockSalts) -> Self {
self.salts = Some(salts);
self
}
pub fn kdf(mut self, kdf: KdfParams) -> Self {
self.kdf = Some(kdf);
self
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
pub fn derive(self) -> Result<TimeLockKey, TimeLockError> {
timelock(
self.cadence,
self.time,
self.precision,
self.format,
self.salts,
self.kdf,
self.params,
)
}
#[cfg(any(feature = "enc-timelock-async-keygen-now", feature = "enc-timelock-async-keygen-input"))]
pub async fn derive_async(self) -> Result<TimeLockKey, TimeLockError> {
timelock_async(
self.cadence,
self.time,
self.precision,
self.format,
self.salts,
self.kdf,
self.params,
).await
}
}
#[cfg(any(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
use chrono::Timelike as _;
fn fast() -> KdfParams {
KdfParams {
pass1: Argon2PassParams { m_cost: 32_768, t_cost: 1, p_cost: 1 },
pass2: ScryptPassParams { log_n: 13, r: 8, p: 1 },
pass3: Argon2PassParams { m_cost: 16_384, t_cost: 1, p_cost: 1 },
}
}
fn salts() -> TimeLockSalts { TimeLockSalts::generate() }
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn timelocktime_valid_range() {
assert!(TimeLockTime::new(0, 0).is_some());
assert!(TimeLockTime::new(23, 59).is_some());
assert!(TimeLockTime::new(14, 37).is_some());
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn timelocktime_invalid_range() {
assert!(TimeLockTime::new(24, 0).is_none(), "hour=24 should fail");
assert!(TimeLockTime::new( 0, 60).is_none(), "minute=60 should fail");
assert!(TimeLockTime::new(99, 99).is_none());
}
#[test]
fn format_hour_24h() {
let s = helper::format_components(14, 37, TimePrecision::Hour, TimeFormat::Hour24);
assert_eq!(s, "14");
}
#[test]
fn format_hour_12h() {
let s_pm = helper::format_components(14, 0, TimePrecision::Hour, TimeFormat::Hour12);
let s_am = helper::format_components( 2, 0, TimePrecision::Hour, TimeFormat::Hour12);
assert_eq!(s_pm, "02PM");
assert_eq!(s_am, "02AM");
}
#[test]
fn format_quarter_snaps_correctly() {
assert_eq!(helper::format_components(14, 37, TimePrecision::Quarter, TimeFormat::Hour24), "14:30");
assert_eq!(helper::format_components(14, 15, TimePrecision::Quarter, TimeFormat::Hour24), "14:15");
assert_eq!(helper::format_components(14, 0, TimePrecision::Quarter, TimeFormat::Hour24), "14:00");
assert_eq!(helper::format_components(14, 59, TimePrecision::Quarter, TimeFormat::Hour24), "14:45");
}
#[test]
fn format_minute_exact() {
let s = helper::format_components(9, 5, TimePrecision::Minute, TimeFormat::Hour24);
assert_eq!(s, "09:05");
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn at_same_inputs_same_key() {
let s = salts();
let t = TimeLockTime::new(14, 37).unwrap();
let k1 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
let s2 = TimeLockSalts::from_slice(&s.to_bytes());
let k2 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s2, &fast()).unwrap();
assert_eq!(k1.as_bytes(), k2.as_bytes());
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn at_different_salts_different_key() {
let t = TimeLockTime::new(14, 37).unwrap();
let k1 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &salts(), &fast()).unwrap();
let k2 = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &salts(), &fast()).unwrap();
assert_ne!(k1.as_bytes(), k2.as_bytes());
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn at_different_time_different_key() {
let s = salts();
let t1 = TimeLockTime::new(14, 37).unwrap();
let t2 = TimeLockTime::new(14, 38).unwrap();
let k1 = derive_key_at(t1, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
let k2 = derive_key_at(t2, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
assert_ne!(k1.as_bytes(), k2.as_bytes());
}
#[cfg(feature = "enc-timelock-keygen-now")]
#[test]
fn now_returns_nonzero_key() {
let k = derive_key_now(TimePrecision::Hour, TimeFormat::Hour24, &salts(), &fast()).unwrap();
assert_ne!(k.as_bytes(), &[0u8; 32]);
}
#[cfg(all(feature = "enc-timelock-keygen-now", feature = "enc-timelock-keygen-input"))]
#[test]
fn now_and_at_same_minute_match() {
let now = chrono::Local::now();
let t = TimeLockTime::new(now.hour(), now.minute()).unwrap();
let s = salts();
let kn = derive_key_now(TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
let ka = derive_key_at(t, TimePrecision::Minute, TimeFormat::Hour24, &s, &fast()).unwrap();
assert_eq!(
kn.as_bytes(), ka.as_bytes(),
"now and explicit current time must produce the same key"
);
}
#[test]
fn salt_round_trip() {
let s = salts();
let b = s.to_bytes();
let s2 = TimeLockSalts::from_slice(&b);
assert_eq!(s.s1, s2.s1);
assert_eq!(s.s2, s2.s2);
assert_eq!(s.s3, s2.s3);
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn custom_params_works() {
let preset = KdfPreset::Custom(KdfPreset::Fast.params());
let t = TimeLockTime::new(10, 0).unwrap();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &preset.params())
.expect("Custom params should succeed");
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn custom_params_roundtrip_eq() {
let p = KdfPreset::Fast.params();
assert_eq!(KdfPreset::Custom(p).params(), p);
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
#[ignore = "slow (~400–600 ms) — run with `cargo test -- --ignored`"]
fn balanced_preset_completes() {
let t = TimeLockTime::new(10, 0).unwrap();
let start = std::time::Instant::now();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::Balanced.params())
.expect("Balanced should succeed");
println!("Balanced (generic): {:?}", start.elapsed());
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
#[ignore = "slow (~2 s on Mac, ~8–15 s on x86) — run with `cargo test -- --ignored`"]
fn paranoid_preset_completes() {
let t = TimeLockTime::new(10, 0).unwrap();
let start = std::time::Instant::now();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::Paranoid.params())
.expect("Paranoid should succeed");
println!("Paranoid (generic): {:?}", start.elapsed());
}
#[cfg(all(feature = "enc-timelock-keygen-input", target_os = "macos"))]
#[test]
#[ignore = "slow (~2 s on M2) — run with `cargo test -- --ignored`"]
fn balanced_mac_completes() {
let t = TimeLockTime::new(10, 0).unwrap();
let start = std::time::Instant::now();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedMac.params())
.expect("BalancedMac should succeed");
println!("BalancedMac: {:?}", start.elapsed());
}
#[cfg(all(feature = "enc-timelock-keygen-input", target_os = "macos"))]
#[test]
#[ignore = "slow (~5–12 s on M2, faster on M3/M4) — run with `cargo test -- --ignored`"]
fn paranoid_mac_completes() {
let t = TimeLockTime::new(10, 0).unwrap();
let start = std::time::Instant::now();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidMac.params())
.expect("ParanoidMac should succeed");
println!("ParanoidMac: {:?}", start.elapsed());
}
#[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "x86_64"))]
#[test]
#[ignore = "slow (~1.5 s on typical x86-64) — run with `cargo test -- --ignored`"]
fn balanced_x86_completes() {
let t = TimeLockTime::new(10, 0).unwrap();
let start = std::time::Instant::now();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedX86.params())
.expect("BalancedX86 should succeed");
println!("BalancedX86: {:?}", start.elapsed());
}
#[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "x86_64"))]
#[test]
#[ignore = "slow (~8–15 s on typical x86-64) — run with `cargo test -- --ignored`"]
fn paranoid_x86_completes() {
let t = TimeLockTime::new(10, 0).unwrap();
let start = std::time::Instant::now();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidX86.params())
.expect("ParanoidX86 should succeed");
println!("ParanoidX86: {:?}", start.elapsed());
}
#[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "aarch64", not(target_os = "macos")))]
#[test]
#[ignore = "slow (~3 s on Graviton3) — run with `cargo test -- --ignored`"]
fn balanced_arm_completes() {
let t = TimeLockTime::new(10, 0).unwrap();
let start = std::time::Instant::now();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::BalancedArm.params())
.expect("BalancedArm should succeed");
println!("BalancedArm: {:?}", start.elapsed());
}
#[cfg(all(feature = "enc-timelock-keygen-input", target_arch = "aarch64", not(target_os = "macos")))]
#[test]
#[ignore = "slow (~10–20 s on Graviton3) — run with `cargo test -- --ignored`"]
fn paranoid_arm_completes() {
let t = TimeLockTime::new(10, 0).unwrap();
let start = std::time::Instant::now();
derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &salts(), &KdfPreset::ParanoidArm.params())
.expect("ParanoidArm should succeed");
println!("ParanoidArm: {:?}", start.elapsed());
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn scheduled_none_same_as_regular_at() {
let s = salts();
let t = TimeLockTime::new(14, 0).unwrap();
let regular = derive_key_at(t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast()).unwrap();
let scheduled = derive_key_scheduled_at(
TimeLockCadence::None, t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
).unwrap();
assert_eq!(regular.as_bytes(), scheduled.as_bytes());
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn scheduled_different_weekdays_different_keys() {
let s = salts();
let t = TimeLockTime::new(18, 0).unwrap();
let k_mon = derive_key_scheduled_at(
TimeLockCadence::DayOfWeek(Weekday::Monday),
t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
).unwrap();
let k_tue = derive_key_scheduled_at(
TimeLockCadence::DayOfWeek(Weekday::Tuesday),
t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
).unwrap();
assert_ne!(k_mon.as_bytes(), k_tue.as_bytes());
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn scheduled_different_months_different_keys() {
let s = salts();
let t = TimeLockTime::new(0, 0).unwrap();
let k_jan = derive_key_scheduled_at(
TimeLockCadence::MonthOfYear(Month::January),
t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
).unwrap();
let k_feb = derive_key_scheduled_at(
TimeLockCadence::MonthOfYear(Month::February),
t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
).unwrap();
assert_ne!(k_jan.as_bytes(), k_feb.as_bytes());
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
fn scheduled_at_deterministic() {
let s = salts();
let t = TimeLockTime::new(6, 0).unwrap();
let c = TimeLockCadence::DayOfWeekInMonth(Weekday::Friday, Month::March);
let k1 = derive_key_scheduled_at(c, t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast()).unwrap();
let s2 = TimeLockSalts::from_slice(&s.to_bytes());
let k2 = derive_key_scheduled_at(c, t, TimePrecision::Hour, TimeFormat::Hour24, &s2, &fast()).unwrap();
assert_eq!(k1.as_bytes(), k2.as_bytes());
}
#[cfg(feature = "enc-timelock-keygen-now")]
#[test]
fn scheduled_now_none_matches_derive_now() {
let s = salts();
let f = fast();
let stored = pack(
TimePrecision::Hour, TimeFormat::Hour24,
&TimeLockCadence::None,
s.clone(),
f,
);
let k1 = derive_key_now(TimePrecision::Hour, TimeFormat::Hour24, &s, &f).unwrap();
let k2 = derive_key_scheduled_now(&stored).unwrap();
assert_eq!(k1.as_bytes(), k2.as_bytes());
}
#[cfg(any(feature = "enc-timelock-keygen-input", feature = "enc-timelock-keygen-now"))]
#[test]
fn pack_unpack_roundtrip() {
let params = pack(
TimePrecision::Minute,
TimeFormat::Hour24,
&TimeLockCadence::DayOfWeekInMonth(Weekday::Tuesday, Month::February),
salts(),
fast(),
);
assert_eq!(params.time_precision, 2); assert_eq!(params.time_format, 1); assert_eq!(params.cadence_variant, 4); let (p, f, v) = unpack(¶ms);
assert!(matches!(p, TimePrecision::Minute));
assert!(matches!(f, TimeFormat::Hour24));
assert_eq!(v, 4);
}
#[cfg(feature = "enc-timelock-keygen-input")]
#[test]
#[should_panic(expected = "DayOfMonthInMonth")]
fn day_of_month_in_month_panics_on_invalid_day() {
let s = salts();
let t = TimeLockTime::new(0, 0).unwrap();
let _ = derive_key_scheduled_at(
TimeLockCadence::DayOfMonthInMonth(29, Month::February),
t, TimePrecision::Hour, TimeFormat::Hour24, &s, &fast(),
);
}
}
#[cfg(any(
feature = "enc-timelock-keygen-now",
feature = "enc-timelock-keygen-input",
feature = "enc-timelock-async-keygen-now",
feature = "enc-timelock-async-keygen-input",
))]
pub use toolkit_zero_macros::timelock;