feat: 扩展 Rust gateway 全功能模块,新增 billing/crypto/wallet crate 及完整数据层

- 新增 aether-billing、aether-crypto、aether-wallet 独立 crate
- aether-data 扩展 repository 层:announcements、auth_modules、billing、
  candidate_selection、gemini_file_mappings、global_models、management_tokens、
  oauth_providers、proxy_nodes、quota、users、wallet 等模块
- aether-gateway 新增 api/auth/billing/control/middleware/scheduler/usage/
  video_tasks/hooks/maintenance/model_fetch/provider_transport 等功能模块
- 重构 executor decision 和 gateway state 为模块目录结构
- 新增 gateway router、frontdoor 路由层及对应测试
- Python 侧 API 路由重构,新增 compat/support 模块
- 前端 Logo 组件更新及 Provider 管理页面调整
This commit is contained in:
fawney19
2026-03-31 19:19:04 +08:00
parent b5a0070023
commit ddf18fed9a
690 changed files with 235087 additions and 16301 deletions

View File

@@ -0,0 +1,17 @@
[package]
name = "aether-crypto"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Shared crypto compatibility helpers for Rust migration"
[dependencies]
aes.workspace = true
base64.workspace = true
cbc.workspace = true
hmac.workspace = true
pbkdf2.workspace = true
sha2.workspace = true
thiserror.workspace = true
uuid.workspace = true

View File

@@ -0,0 +1,7 @@
mod python_fernet;
pub use python_fernet::{
decrypt_python_fernet_ciphertext, derive_python_fernet_key, encrypt_python_fernet_plaintext,
looks_like_python_fernet_ciphertext, PythonFernetCompat, PythonFernetError, APP_SALT_HEX,
APP_SALT_SEED, DEVELOPMENT_ENCRYPTION_KEY,
};

View File

@@ -0,0 +1,309 @@
use std::time::{SystemTime, UNIX_EPOCH};
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use base64::engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD};
use base64::Engine as _;
use cbc::{Decryptor, Encryptor};
use hmac::{Hmac, Mac};
use pbkdf2::pbkdf2_hmac;
use sha2::{Digest, Sha256};
use uuid::Uuid;
const FERNET_VERSION: u8 = 0x80;
const HMAC_SIZE: usize = 32;
const IV_SIZE: usize = 16;
const SIGNING_KEY_SIZE: usize = 16;
const ENCRYPTION_KEY_SIZE: usize = 16;
const MIN_TOKEN_SIZE: usize = 1 + 8 + IV_SIZE + HMAC_SIZE;
const PBKDF2_ITERATIONS: u32 = 100_000;
pub const APP_SALT_SEED: &[u8] = b"aether-v1";
pub const APP_SALT_HEX: &str = "8797080a7a4b45b4810e934d1af36261";
pub const DEVELOPMENT_ENCRYPTION_KEY: &str = "dev-encryption-key-do-not-use-in-production";
type Aes128CbcDec = Decryptor<aes::Aes128>;
type Aes128CbcEnc = Encryptor<aes::Aes128>;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, thiserror::Error)]
pub enum PythonFernetError {
#[error("invalid Python Fernet outer base64 payload")]
InvalidOuterBase64,
#[error("invalid Python Fernet inner base64 payload")]
InvalidInnerBase64,
#[error("invalid Python Fernet token structure")]
InvalidTokenStructure,
#[error("unsupported Python Fernet token version: {0:#x}")]
UnsupportedTokenVersion(u8),
#[error("invalid Python Fernet token signature")]
InvalidTokenSignature,
#[error("invalid Python Fernet token padding")]
InvalidPadding,
#[error("invalid Python Fernet plaintext utf-8")]
InvalidUtf8(#[from] std::string::FromUtf8Error),
}
#[derive(Debug, Clone)]
pub struct PythonFernetCompat {
signing_key: [u8; SIGNING_KEY_SIZE],
encryption_key: [u8; ENCRYPTION_KEY_SIZE],
}
impl PythonFernetCompat {
pub fn from_secret(secret: &str) -> Self {
let raw_key = raw_fernet_key(secret);
Self::from_raw_key(raw_key)
}
pub fn decrypt_ciphertext(&self, ciphertext: &str) -> Result<String, PythonFernetError> {
if ciphertext.is_empty() {
return Ok(String::new());
}
let outer =
decode_urlsafe(ciphertext).map_err(|_| PythonFernetError::InvalidOuterBase64)?;
let inner =
decode_urlsafe_bytes(&outer).map_err(|_| PythonFernetError::InvalidInnerBase64)?;
let plaintext = self.decrypt_token_bytes(&inner)?;
String::from_utf8(plaintext).map_err(PythonFernetError::InvalidUtf8)
}
pub fn encrypt_plaintext(&self, plaintext: &str) -> Result<String, PythonFernetError> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.encrypt_token(plaintext, timestamp, *Uuid::new_v4().as_bytes())
}
fn from_raw_key(raw_key: [u8; 32]) -> Self {
let mut signing_key = [0u8; SIGNING_KEY_SIZE];
let mut encryption_key = [0u8; ENCRYPTION_KEY_SIZE];
signing_key.copy_from_slice(&raw_key[..SIGNING_KEY_SIZE]);
encryption_key.copy_from_slice(&raw_key[SIGNING_KEY_SIZE..]);
Self {
signing_key,
encryption_key,
}
}
fn decrypt_token_bytes(&self, token: &[u8]) -> Result<Vec<u8>, PythonFernetError> {
if token.len() < MIN_TOKEN_SIZE {
return Err(PythonFernetError::InvalidTokenStructure);
}
if token[0] != FERNET_VERSION {
return Err(PythonFernetError::UnsupportedTokenVersion(token[0]));
}
let signed_len = token.len() - HMAC_SIZE;
let (signed, signature) = token.split_at(signed_len);
let mut mac = HmacSha256::new_from_slice(&self.signing_key)
.map_err(|_| PythonFernetError::InvalidTokenSignature)?;
mac.update(signed);
mac.verify_slice(signature)
.map_err(|_| PythonFernetError::InvalidTokenSignature)?;
let iv_offset = 1 + 8;
let ciphertext_offset = iv_offset + IV_SIZE;
let iv = &token[iv_offset..ciphertext_offset];
let mut ciphertext = token[ciphertext_offset..signed_len].to_vec();
let plaintext = Aes128CbcDec::new((&self.encryption_key).into(), iv.into())
.decrypt_padded_mut::<Pkcs7>(&mut ciphertext)
.map_err(|_| PythonFernetError::InvalidPadding)?;
Ok(plaintext.to_vec())
}
fn encrypt_token(
&self,
plaintext: &str,
timestamp: u64,
iv: [u8; IV_SIZE],
) -> Result<String, PythonFernetError> {
let plaintext = plaintext.as_bytes();
let mut padded = vec![0u8; plaintext.len() + IV_SIZE];
padded[..plaintext.len()].copy_from_slice(plaintext);
let ciphertext = Aes128CbcEnc::new((&self.encryption_key).into(), (&iv).into())
.encrypt_padded_mut::<Pkcs7>(&mut padded, plaintext.len())
.map_err(|_| PythonFernetError::InvalidPadding)?
.to_vec();
let mut signed = Vec::with_capacity(1 + 8 + IV_SIZE + ciphertext.len() + HMAC_SIZE);
signed.push(FERNET_VERSION);
signed.extend_from_slice(&timestamp.to_be_bytes());
signed.extend_from_slice(&iv);
signed.extend_from_slice(&ciphertext);
let mut mac = HmacSha256::new_from_slice(&self.signing_key)
.map_err(|_| PythonFernetError::InvalidTokenSignature)?;
mac.update(&signed);
let signature = mac.finalize().into_bytes();
signed.extend_from_slice(&signature);
let inner = URL_SAFE.encode(signed);
Ok(URL_SAFE.encode(inner.as_bytes()))
}
}
pub fn derive_python_fernet_key(secret: &str) -> String {
URL_SAFE.encode(raw_fernet_key(secret))
}
pub fn decrypt_python_fernet_ciphertext(
secret: &str,
ciphertext: &str,
) -> Result<String, PythonFernetError> {
PythonFernetCompat::from_secret(secret).decrypt_ciphertext(ciphertext)
}
pub fn looks_like_python_fernet_ciphertext(ciphertext: &str) -> bool {
let ciphertext = ciphertext.trim();
if ciphertext.is_empty() {
return false;
}
let Ok(outer) = decode_urlsafe(ciphertext) else {
return false;
};
let Ok(inner) = decode_urlsafe_bytes(&outer) else {
return false;
};
inner.len() >= MIN_TOKEN_SIZE && inner.first().copied() == Some(FERNET_VERSION)
}
pub fn encrypt_python_fernet_plaintext(
secret: &str,
plaintext: &str,
) -> Result<String, PythonFernetError> {
PythonFernetCompat::from_secret(secret).encrypt_plaintext(plaintext)
}
fn raw_fernet_key(secret: &str) -> [u8; 32] {
if let Ok(raw_key) = decode_direct_fernet_key(secret) {
return raw_key;
}
let mut salt = [0u8; 16];
salt.copy_from_slice(&Sha256::digest(APP_SALT_SEED)[..16]);
let mut raw_key = [0u8; 32];
pbkdf2_hmac::<Sha256>(secret.as_bytes(), &salt, PBKDF2_ITERATIONS, &mut raw_key);
raw_key
}
fn decode_direct_fernet_key(secret: &str) -> Result<[u8; 32], PythonFernetError> {
let decoded = URL_SAFE
.decode(secret)
.map_err(|_| PythonFernetError::InvalidInnerBase64)?;
let raw_key: [u8; 32] = decoded
.as_slice()
.try_into()
.map_err(|_| PythonFernetError::InvalidTokenStructure)?;
Ok(raw_key)
}
fn decode_urlsafe(value: &str) -> Result<Vec<u8>, base64::DecodeError> {
URL_SAFE
.decode(value)
.or_else(|_| URL_SAFE_NO_PAD.decode(value))
}
fn decode_urlsafe_bytes(value: &[u8]) -> Result<Vec<u8>, base64::DecodeError> {
URL_SAFE
.decode(value)
.or_else(|_| URL_SAFE_NO_PAD.decode(value))
}
#[cfg(test)]
mod tests {
use super::{
decrypt_python_fernet_ciphertext, derive_python_fernet_key,
encrypt_python_fernet_plaintext, looks_like_python_fernet_ciphertext, PythonFernetCompat,
PythonFernetError, APP_SALT_HEX, DEVELOPMENT_ENCRYPTION_KEY,
};
#[test]
fn derives_python_pbkdf2_key_for_development_secret() {
assert_eq!(APP_SALT_HEX, "8797080a7a4b45b4810e934d1af36261");
assert_eq!(
derive_python_fernet_key(DEVELOPMENT_ENCRYPTION_KEY),
"qGVbbzTSey8Hi1DRtS6wkb2jL33pRBHXTQW-GO6qne0="
);
}
#[test]
fn passes_through_existing_fernet_key_secret() {
let direct_key = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=";
assert_eq!(derive_python_fernet_key(direct_key), direct_key);
}
#[test]
fn treats_unpadded_direct_key_like_python_pbkdf2_secret() {
let unpadded_direct_key = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY";
assert_eq!(
derive_python_fernet_key(unpadded_direct_key),
"cI8mUtZz6AfpTnBy9xP48Wcp7k_r9h6jJ8jtUoc30cY="
);
}
#[test]
fn decrypts_python_compatible_outer_wrapped_ciphertext() {
let crypto = PythonFernetCompat::from_secret(DEVELOPMENT_ENCRYPTION_KEY);
let ciphertext = crypto
.encrypt_token(
"{\"api_key\":\"sk-test\",\"provider\":\"openai\"}",
1_710_000_000,
*b"fixed-fernet-iv!",
)
.expect("ciphertext should build");
let plaintext = decrypt_python_fernet_ciphertext(DEVELOPMENT_ENCRYPTION_KEY, &ciphertext)
.expect("ciphertext should decrypt");
assert_eq!(
plaintext,
"{\"api_key\":\"sk-test\",\"provider\":\"openai\"}"
);
}
#[test]
fn detects_python_fernet_ciphertext_shape() {
let ciphertext = encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "sk-test")
.expect("ciphertext should build");
assert!(looks_like_python_fernet_ciphertext(&ciphertext));
assert!(!looks_like_python_fernet_ciphertext("sk-plaintext-openai"));
assert!(!looks_like_python_fernet_ciphertext(
r#"{"headers":{"x-account-id":"acc-1"}}"#
));
}
#[test]
fn rejects_tampered_signature() {
let crypto = PythonFernetCompat::from_secret(DEVELOPMENT_ENCRYPTION_KEY);
let mut ciphertext = crypto
.encrypt_token("secret", 1_710_000_000, *b"fixed-fernet-iv!")
.expect("ciphertext should build");
ciphertext.replace_range(ciphertext.len() - 2.., "AA");
let err = decrypt_python_fernet_ciphertext(DEVELOPMENT_ENCRYPTION_KEY, &ciphertext)
.expect_err("tampered ciphertext should fail");
assert!(matches!(
err,
PythonFernetError::InvalidInnerBase64
| PythonFernetError::InvalidTokenSignature
| PythonFernetError::InvalidPadding
));
}
#[test]
fn encrypt_and_decrypt_round_trip() {
let ciphertext =
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "sk-live-openai")
.expect("ciphertext should build");
let plaintext = decrypt_python_fernet_ciphertext(DEVELOPMENT_ENCRYPTION_KEY, &ciphertext)
.expect("ciphertext should decrypt");
assert_eq!(plaintext, "sk-live-openai");
}
}