use arkhe_kernel::abi::{EntityId, Tick};
use serde::{Deserialize, Serialize};
use crate::action::ActionCompute;
use crate::context::{ActionContext, ActionError};
use crate::event::UserErasureScheduled;
use crate::ArkheAction;
use crate::ArkheComponent;
use crate::arkhe_pure;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UserId(EntityId);
impl UserId {
#[inline]
#[must_use]
pub fn new(id: EntityId) -> Self {
Self(id)
}
#[inline]
#[must_use]
pub fn get(self) -> EntityId {
self.0
}
}
#[non_exhaustive]
#[repr(u8)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub enum AuthKind {
Passkey = 0,
Email = 1,
Handle = 2,
Address = 3,
}
#[non_exhaustive]
#[repr(u8)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub enum GdprStatus {
Active = 0,
ErasurePending = 1,
Erased = 2,
}
#[non_exhaustive]
#[repr(u8)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub enum KdfKind {
Argon2id = 0,
Scrypt = 1,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct KdfParams {
pub m_cost: u32,
pub t_cost: u32,
pub p_cost: u32,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
#[arkhe(type_code = 0x0003_0001, schema_version = 1)]
pub struct UserProfile {
pub schema_version: u16,
pub created_tick: Tick,
pub primary_auth_kind: AuthKind,
pub gdpr_status: GdprStatus,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
#[arkhe(type_code = 0x0003_0002, schema_version = 1)]
pub struct AuthCredential {
pub schema_version: u16,
pub kind: AuthKind,
pub kdf: KdfKind,
pub salt: [u8; 16],
pub credential_hash: [u8; 32],
pub kdf_params: KdfParams,
pub expires_tick: Option<Tick>,
pub bound_tick: Tick,
}
impl AuthCredential {
pub const DEFAULT_KDF: KdfKind = KdfKind::Argon2id;
pub const MIN_ARGON2ID_M_COST: u32 = 19_456;
pub const MIN_ARGON2ID_T_COST: u32 = 2;
pub const MIN_ARGON2ID_P_COST: u32 = 1;
pub const MIN_SCRYPT_N_COST: u32 = 1 << 15;
pub const MIN_SCRYPT_R_COST: u32 = 8;
#[must_use]
pub fn validate_kdf_params(kdf: KdfKind, p: &KdfParams) -> bool {
match kdf {
KdfKind::Argon2id => {
p.m_cost >= Self::MIN_ARGON2ID_M_COST
&& p.t_cost >= Self::MIN_ARGON2ID_T_COST
&& p.p_cost >= Self::MIN_ARGON2ID_P_COST
}
KdfKind::Scrypt => {
p.m_cost >= Self::MIN_SCRYPT_N_COST
&& p.t_cost >= Self::MIN_SCRYPT_R_COST
&& p.p_cost >= 1
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheAction)]
#[arkhe(type_code = 0x0001_0001, schema_version = 1, band = 1)]
pub struct RegisterUser {
pub schema_version: u16,
pub profile: UserProfile,
pub credential: AuthCredential,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheAction)]
#[arkhe(type_code = 0x0001_0003, schema_version = 1, band = 1)]
pub struct GdprEraseUser {
pub schema_version: u16,
pub user: UserId,
}
impl ActionCompute for RegisterUser {
#[arkhe_pure]
fn compute<'i>(&self, ctx: &mut ActionContext<'i>) -> Result<(), ActionError> {
if !AuthCredential::validate_kdf_params(self.credential.kdf, &self.credential.kdf_params) {
return Err(ActionError::InvalidInput("KDF params below minimum"));
}
let user_entity = ctx.spawn_entity_for::<UserProfile>()?;
ctx.set_component(user_entity, &self.profile)?;
ctx.set_component(user_entity, &self.credential)?;
Ok(())
}
}
impl ActionCompute for GdprEraseUser {
#[arkhe_pure]
fn compute<'i>(&self, ctx: &mut ActionContext<'i>) -> Result<(), ActionError> {
let event = UserErasureScheduled {
schema_version: 1,
user: self.user,
scheduled_tick: ctx.tick(),
};
ctx.emit_event(&event)?;
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn make_uid(v: u64) -> UserId {
UserId::new(EntityId::new(v).unwrap())
}
#[test]
fn user_id_preserves_underlying_entity() {
let uid = make_uid(42);
assert_eq!(uid.get().get(), 42);
}
#[test]
fn auth_credential_validates_default_argon2id_params() {
let params = KdfParams {
m_cost: AuthCredential::MIN_ARGON2ID_M_COST,
t_cost: AuthCredential::MIN_ARGON2ID_T_COST,
p_cost: AuthCredential::MIN_ARGON2ID_P_COST,
};
assert!(AuthCredential::validate_kdf_params(
KdfKind::Argon2id,
¶ms
));
}
#[test]
fn auth_credential_rejects_under_cost_argon2id() {
let params = KdfParams {
m_cost: 1024,
t_cost: 1,
p_cost: 1,
};
assert!(!AuthCredential::validate_kdf_params(
KdfKind::Argon2id,
¶ms
));
}
#[test]
fn gdpr_status_roundtrip_via_postcard() {
let s = GdprStatus::ErasurePending;
let b = postcard::to_stdvec(&s).unwrap();
let back: GdprStatus = postcard::from_bytes(&b).unwrap();
assert_eq!(s, back);
}
}