diff --git a/apps/aether-gateway/src/handlers/admin/provider/delete_task.rs b/apps/aether-gateway/src/handlers/admin/provider/delete_task.rs index f8969540e..83967be34 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/delete_task.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/delete_task.rs @@ -3,7 +3,10 @@ use crate::handlers::admin::provider::shared::support::{ ADMIN_PROVIDER_MAPPING_PREVIEW_MAX_KEYS, ADMIN_PROVIDER_MAPPING_PREVIEW_MAX_MODELS, }; use crate::handlers::admin::request::AdminAppState; -use crate::handlers::admin::shared::{decrypt_catalog_secret_with_fallbacks, json_string_list}; +use crate::handlers::admin::shared::{ + decrypt_catalog_secret_with_fallbacks, json_string_list, take_secret_prefix, + take_secret_suffix, +}; use crate::handlers::public::matches_model_mapping_for_models; use crate::{GatewayError, LocalProviderDeleteTaskState}; use aether_data_contracts::repository::global_models::{ @@ -175,14 +178,15 @@ pub(crate) fn mapping_preview_masked_catalog_api_key( decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext) .map(|value| { - if value.len() > 8 { + let char_count = value.chars().count(); + if char_count > 8 { format!( "{}***{}", - &value[..4], - &value[value.len().saturating_sub(4)..] + take_secret_prefix(&value, 4), + take_secret_suffix(&value, 4) ) - } else if value.len() >= 2 { - format!("{}***", &value[..2]) + } else if char_count >= 2 { + format!("{}***", take_secret_prefix(&value, 2)) } else { "***".to_string() } diff --git a/apps/aether-gateway/src/handlers/admin/shared/mod.rs b/apps/aether-gateway/src/handlers/admin/shared/mod.rs index c64bfdbe0..ad2b19ea0 100644 --- a/apps/aether-gateway/src/handlers/admin/shared/mod.rs +++ b/apps/aether-gateway/src/handlers/admin/shared/mod.rs @@ -12,6 +12,6 @@ pub(crate) use crate::handlers::shared::{ masked_catalog_api_key, normalize_json_array, normalize_json_object, normalize_string_list, parse_catalog_auth_config_json, provider_catalog_key_supports_format, provider_key_health_summary, provider_key_status_snapshot_payload, query_param_bool, - query_param_optional_bool, query_param_value, unix_secs_to_rfc3339, - OFFICIAL_EXTERNAL_MODEL_PROVIDERS, + query_param_optional_bool, query_param_value, take_secret_prefix, take_secret_suffix, + unix_secs_to_rfc3339, OFFICIAL_EXTERNAL_MODEL_PROVIDERS, }; diff --git a/apps/aether-gateway/src/handlers/shared/catalog.rs b/apps/aether-gateway/src/handlers/shared/catalog.rs index ff6cd741a..ca5b6cd5f 100644 --- a/apps/aether-gateway/src/handlers/shared/catalog.rs +++ b/apps/aether-gateway/src/handlers/shared/catalog.rs @@ -102,6 +102,29 @@ pub(crate) fn encrypt_catalog_secret_with_fallbacks( encrypt_python_fernet_plaintext(encryption_key.as_ref(), plaintext).ok() } +pub(crate) fn take_secret_prefix(value: &str, prefix_chars: usize) -> &str { + let end = value + .char_indices() + .nth(prefix_chars) + .map(|(index, _)| index) + .unwrap_or(value.len()); + &value[..end] +} + +pub(crate) fn take_secret_suffix(value: &str, suffix_chars: usize) -> &str { + if suffix_chars == 0 { + return &value[value.len()..]; + } + + let start = value + .char_indices() + .rev() + .nth(suffix_chars - 1) + .map(|(index, _)| index) + .unwrap_or(0); + &value[start..] +} + pub(crate) fn masked_catalog_api_key(state: &AppState, key: &StoredProviderCatalogKey) -> String { match key.auth_type.trim() { "service_account" | "vertex_ai" => "[Service Account]".to_string(), @@ -117,13 +140,13 @@ pub(crate) fn masked_catalog_api_key(state: &AppState, key: &StoredProviderCatal }; decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext) .map(|value| { - if value.len() <= 12 { + if value.chars().count() <= 12 { format!("{value}***") } else { format!( "{}***{}", - &value[..8], - &value[value.len().saturating_sub(4)..] + take_secret_prefix(&value, 8), + take_secret_suffix(&value, 4) ) } }) @@ -1621,6 +1644,41 @@ mod tests { .expect("key transport should build") } + #[test] + fn masked_catalog_api_key_handles_unicode_plaintext_without_panicking() { + let state = AppState::new().expect("gateway should build"); + let encrypted_api_key = encrypt_python_fernet_plaintext( + DEVELOPMENT_ENCRYPTION_KEY, + "测试-密钥-1234567890", + ) + .expect("api key ciphertext should build"); + let key = StoredProviderCatalogKey::new( + "key-unicode".to_string(), + "provider-test".to_string(), + "default".to_string(), + "api_key".to_string(), + None, + true, + ) + .expect("key should build") + .with_transport_fields( + Some(json!(["openai:chat"])), + encrypted_api_key, + None, + None, + None, + None, + None, + None, + None, + ) + .expect("key transport should build"); + + let masked = masked_catalog_api_key(&state, &key); + assert!(masked.contains("***")); + assert_ne!(masked, "***ERROR***"); + } + #[test] fn provider_key_status_snapshot_payload_backfills_missing_quota_from_upstream_metadata() { let mut key = sample_catalog_key(); diff --git a/apps/aether-gateway/src/handlers/shared/mod.rs b/apps/aether-gateway/src/handlers/shared/mod.rs index 3bf090e4d..5c072ecc2 100644 --- a/apps/aether-gateway/src/handlers/shared/mod.rs +++ b/apps/aether-gateway/src/handlers/shared/mod.rs @@ -25,7 +25,7 @@ pub(crate) use self::catalog::{ encrypt_catalog_secret_with_fallbacks, masked_catalog_api_key, parse_catalog_auth_config_json, provider_catalog_key_supports_format, provider_key_health_summary, provider_key_status_snapshot_payload, sync_provider_key_oauth_status_snapshot, - sync_provider_key_quota_status_snapshot, + sync_provider_key_quota_status_snapshot, take_secret_prefix, take_secret_suffix, }; pub(crate) use self::email_templates::{ admin_email_template_definition, admin_email_template_html_key,