KeiSeiKit-1.0/_primitives/_rust/kei-auth-magiclink/src/env.rs
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

92 lines
2.8 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 <author org>
//! Environment + key-decoding helpers for `MagicLinkProvider::from_env`.
//!
//! Kept in its own cube so [`provider`](crate::provider) stays under the
//! Constructor-Pattern 200-LOC limit. Pure functions, no trait surface.
use crate::error::{Error, Result};
use base64::engine::general_purpose::STANDARD as B64_STD;
use base64::Engine;
pub const ENV_KEY: &str = "MAGICLINK_HMAC_KEY";
pub const ENV_TTL: &str = "MAGICLINK_TTL_SECS";
pub const DEFAULT_TTL_SECS: i64 = 900; // 15 minutes
pub const MIN_KEY_LEN: usize = 32;
/// Read `MAGICLINK_HMAC_KEY` and `MAGICLINK_TTL_SECS` from the environment.
pub fn read_env() -> Result<(Vec<u8>, i64)> {
let raw = std::env::var(ENV_KEY)
.map_err(|_| Error::KeyMissing(format!("{ENV_KEY} unset")))?;
let key = decode_key(&raw)?;
let ttl = match std::env::var(ENV_TTL) {
Ok(v) => v
.parse::<i64>()
.map_err(|e| Error::KeyMissing(format!("{ENV_TTL} parse: {e}")))?,
Err(_) => DEFAULT_TTL_SECS,
};
Ok((key, ttl))
}
/// Decode a key string. 64 ASCII hex chars → hex; else standard base64.
pub fn decode_key(raw: &str) -> Result<Vec<u8>> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(Error::KeyMissing("empty value".into()));
}
if trimmed.len() == 64 && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
return decode_hex(trimmed);
}
B64_STD
.decode(trimmed)
.map_err(|e| Error::KeyMissing(format!("base64 decode: {e}")))
}
fn decode_hex(s: &str) -> Result<Vec<u8>> {
let mut out = Vec::with_capacity(s.len() / 2);
let bytes = s.as_bytes();
for i in (0..bytes.len()).step_by(2) {
let hi = hex_digit(bytes[i])?;
let lo = hex_digit(bytes[i + 1])?;
out.push((hi << 4) | lo);
}
Ok(out)
}
fn hex_digit(b: u8) -> Result<u8> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(Error::KeyMissing(format!("non-hex digit: 0x{b:02x}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_hex_64chars() {
let key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let bytes = decode_key(key).expect("ok");
assert_eq!(bytes.len(), 32);
assert_eq!(bytes[0], 0x01);
assert_eq!(bytes[31], 0xef);
}
#[test]
fn decode_base64() {
// 32 zero bytes → 44-char base64 standard.
let raw = B64_STD.encode([0u8; 32]);
let bytes = decode_key(&raw).expect("ok");
assert_eq!(bytes.len(), 32);
}
#[test]
fn decode_empty_rejected() {
let err = decode_key("").expect_err("must reject");
assert!(matches!(err, Error::KeyMissing(_)));
}
}