Skip to content
Clone

feat(auth): accept altcha base64 pow payload and no-pad salt encoding

PowProof challenge/solution now deserialize from both object form and the base64 payload string produced by the altcha JS (btoa(JSON.stringify({challenge, solution}))). Use STANDARD_NO_PAD for salt encoding.
zhenyi committed
parent f27e717 commit 2aef8d4 Browse files

2 files changed

+71 −4

File tree 2
Modified lib/auth/features/password_hash/usecase.rs View lib/auth/features/password_hash/usecase.rs
@@ -13,7 +13,7 @@
1313 let _timer = PasswordOpTimer::start(PasswordOp::Hash);
1414 let mut bytes = [0u8; 16];
1515 rand::rng().fill_bytes(&mut bytes);
16 let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
16 let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes);
1717 let salt = SaltString::from_b64(&b64)
1818 .map_err(|e| AuthError::Internal(format!("salt encoding: {e}")))?;
1919 let argon2 = Argon2::default();
Modified lib/auth/features/register/types.rs View lib/auth/features/register/types.rs
@@ -1,14 +1,14 @@
1use serde::{Deserialize, Serialize};
1use base64::Engine as _;
2use serde::{Deserialize, Serialize, de::DeserializeOwned, de::Error as DeError};
23use utoipa::ToSchema;
34use uuid::Uuid;
45
56use db::user::user_email_verification_codes::EmailVerificationPurpose;
6
77use crate::features::TempKey;
88use crate::pow::Challenge;
99use crate::pow::Solution;
1010
11#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
11#[derive(Debug, Clone, Serialize, ToSchema)]
1212pub struct PowProof {
1313 #[schema(value_type = String)]
1414 pub challenge: Challenge,
@@ -16,6 +16,73 @@
1616 pub solution: Solution,
1717}
1818
19impl<'de> Deserialize<'de> for PowProof {
20 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
21 where
22 D: serde::Deserializer<'de>,
23 {
24 #[derive(Deserialize)]
25 struct Repr {
26 #[serde(deserialize_with = "deserialize_challenge")]
27 challenge: Challenge,
28 #[serde(deserialize_with = "deserialize_solution")]
29 solution: Solution,
30 }
31 let Repr { challenge, solution } = Repr::deserialize(deserializer)?;
32 Ok(Self { challenge, solution })
33 }
34}
35
36/// challenge 兼容「对象」与「base64 payload 字符串」两种形态
37fn deserialize_challenge<'de, D>(deserializer: D) -> Result<Challenge, D::Error>
38where
39 D: serde::Deserializer<'de>,
40{
41 #[derive(Deserialize)]
42 #[serde(untagged)]
43 enum Repr {
44 Object(Challenge),
45 String(String),
46 }
47 match Repr::deserialize(deserializer)? {
48 Repr::Object(challenge) => Ok(challenge),
49 Repr::String(raw) => {
50 parse_payload_field::<Challenge>(&raw, "challenge").map_err(DeError::custom)
51 }
52 }
53}
54
55/// solution 兼容「对象」与「base64 payload(altcha JS btoa 输出)」两种形态
56fn deserialize_solution<'de, D>(deserializer: D) -> Result<Solution, D::Error>
57where
58 D: serde::Deserializer<'de>,
59{
60 #[derive(Deserialize)]
61 #[serde(untagged)]
62 enum Repr {
63 Object(Solution),
64 String(String),
65 }
66 match Repr::deserialize(deserializer)? {
67 Repr::Object(solution) => Ok(solution),
68 Repr::String(raw) => parse_payload_field::<Solution>(&raw, "solution").map_err(DeError::custom),
69 }
70}
71
72/// 解析 altcha base64 payload:btoa(JSON.stringify({challenge, solution})),取指定字段
73fn parse_payload_field<T: DeserializeOwned>(raw: &str, field: &str) -> Result<T, String> {
74 let bytes = base64::engine::general_purpose::STANDARD
75 .decode(raw)
76 .map_err(|err| format!("invalid base64: {err}"))?;
77 let value: serde_json::Value =
78 serde_json::from_slice(&bytes).map_err(|err| format!("invalid json: {err}"))?;
79 let target = value
80 .get(field)
81 .cloned()
82 .ok_or_else(|| format!("missing field '{field}' in pow payload"))?;
83 serde_json::from_value(target).map_err(|err| format!("invalid {field}: {err}"))
84}
85
1986#[derive(Debug, Clone, Deserialize, ToSchema)]
2087pub struct RegisterCmd {
2188 pub username: Option<String>,