///|
/// API key 工具:生成、哈希(sha256)、校验。
/// 库中只存哈希,明文 key 仅在创建时返回一次。
///|
/// 计算 API key 的 sha256 哈希(hex 编码)
pub fn hash_api_key(key : String) -> String {
@crypto.bytes_to_hex_string(@crypto.sha256(@utf8.encode(key)))
}
///|
pub(all) suberror EntropyUnavailable {
EntropyUnavailable
}
///|
/// 生成随机 API key:`mk_` 前缀 + 平台熵源生成的 32 字节 hex。
/// 熵源不可用时失败,绝不回退到可预测的伪随机种子。
pub fn generate_api_key() -> String raise EntropyUnavailable {
match @env.rand(32) {
Some(bytes) => "mk_" + @crypto.bytes_to_hex_string(bytes)
None => raise EntropyUnavailable::EntropyUnavailable
}
}
///|
pub struct AdminTokenProvider {
mut username : String
mut key_hash : String
}
///|
pub fn AdminTokenProvider::new(
username : String,
token : String,
) -> AdminTokenProvider {
{
username,
key_hash: if token.is_empty() {
""
} else {
hash_api_key(token)
},
}
}
///|
pub fn AdminTokenProvider::from_hash(
username : String,
key_hash : String,
) -> AdminTokenProvider {
{ username, key_hash, }
}
///|
/// 当前本地账户 provider 的公开入口;未来 provider 可替换这一层。
pub fn AdminTokenProvider::verify(
self : AdminTokenProvider,
username : String,
password : String,
) -> Bool {
username == self.username &&
!self.key_hash.is_empty() &&
hash_api_key(password) == self.key_hash
}
///|
pub fn AdminTokenProvider::username(self : AdminTokenProvider) -> String {
self.username
}
///|
pub fn AdminTokenProvider::configured(self : AdminTokenProvider) -> Bool {
!self.key_hash.is_empty()
}
///|
pub fn AdminTokenProvider::set_credentials(
self : AdminTokenProvider,
username : String,
key : String,
) -> Unit {
self.username = username
self.key_hash = hash_api_key(key)
}
///|
/// 生成仅用于当前进程的随机签名密钥。
pub fn generate_session_secret() -> String raise EntropyUnavailable {
match @env.rand(32) {
Some(bytes) => @crypto.bytes_to_hex_string(bytes)
None => raise EntropyUnavailable::EntropyUnavailable
}
}
///|
/// 使用 HS256 生成 JWT。token 的有效期由进程内 session registry 控制。
pub fn issue_jwt(secret : String, subject : String) -> String {
let header = base64url("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
let payload = base64url("{\"sub\":\"" + subject + "\"}")
let signing_input = header + "." + payload
let signature = @crypto.hmac(
@crypto.SHA256::new(),
@utf8.encode(secret).view(),
@utf8.encode(signing_input).view(),
)
signing_input + "." + base64url_bytes(signature)
}
///|
pub fn verify_jwt(secret : String, token : String) -> String? {
let parts = token.split(".").to_array()
if parts.length() != 3 {
return None
}
let signing_input = parts[0].to_owned() + "." + parts[1].to_owned()
let expected = @crypto.hmac(
@crypto.SHA256::new(),
@utf8.encode(secret).view(),
@utf8.encode(signing_input).view(),
)
if base64url_bytes(expected) != parts[2].to_owned() {
return None
}
let padded = parts[1].to_owned()
let payload = @utf8.decode_lossy(
@base64.decode_lossy(
padded.replace_all(old="-", new="+").replace_all(old="_", new="/"),
),
)
match payload.find("\"sub\":\"") {
Some(start) => {
let value = payload[start + 8:]
match value.find("\"") {
Some(end) => Some(value[:end].to_owned())
None => None
}
}
None => None
}
}
///|
fn base64url(value : String) -> String {
@base64.encode(@utf8.encode(value).view(), padding=false)
.replace_all(old="+", new="-")
.replace_all(old="/", new="_")
}
///|
fn base64url_bytes(value : FixedArray[Byte]) -> String {
let bytes = Bytes::from_array(value[:])
@base64.encode(bytes.view(), padding=false)
.replace_all(old="+", new="-")
.replace_all(old="/", new="_")
}