fix(gateway): mask catalog secrets on char boundaries

This commit is contained in:
fawney19
2026-05-05 12:48:19 +08:00
parent 627eb4f335
commit f7071967c0
4 changed files with 74 additions and 12 deletions

View File

@@ -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()
}

View File

@@ -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,
};

View File

@@ -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();

View File

@@ -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,