squigit_auth/security/
api_keys.rs1use std::str::FromStr;
5
6use crate::{ProfileError, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ApiKeyProvider {
10 GoogleAiStudio,
11 ImgBb,
12}
13
14impl ApiKeyProvider {
15 pub fn display_name(self) -> &'static str {
16 match self {
17 Self::GoogleAiStudio => "Google AI Studio",
18 Self::ImgBb => "ImgBB",
19 }
20 }
21
22 pub fn storage_key_name(self) -> &'static str {
23 match self {
24 Self::GoogleAiStudio => "google-ai-studio",
25 Self::ImgBb => "imgbb",
26 }
27 }
28
29 pub fn is_valid_key(self, key: &str) -> bool {
30 match self {
31 Self::GoogleAiStudio => {
32 let is_standard = key.starts_with("AIzaSy") && key.len() == 39;
33 let is_prefixed = key.starts_with("AQ.") && key.len() >= 50 && key.len() <= 60;
34 is_standard || is_prefixed
35 }
36 Self::ImgBb => key.len() == 32,
37 }
38 }
39
40 pub fn validation_hint(self) -> &'static str {
41 match self {
42 Self::GoogleAiStudio => {
43 "Expected a key that starts with 'AIzaSy' (39 chars) or 'AQ.' (50-60 chars)."
44 }
45 Self::ImgBb => "Expected a 32-character API key.",
46 }
47 }
48}
49
50impl FromStr for ApiKeyProvider {
51 type Err = ProfileError;
52
53 fn from_str(value: &str) -> Result<Self> {
54 match value {
55 "google-ai-studio" => Ok(Self::GoogleAiStudio),
56 "imgbb" => Ok(Self::ImgBb),
57 other => Err(ProfileError::InvalidProvider(other.to_owned())),
58 }
59 }
60}
61
62pub fn validate_api_key(provider: ApiKeyProvider, plaintext: &str) -> Result<()> {
63 let trimmed = plaintext.trim();
64 if provider.is_valid_key(trimmed) {
65 return Ok(());
66 }
67
68 Err(ProfileError::byok(
69 crate::ByokErrorCode::InvalidCredential,
70 format!(
71 "Invalid {} API key format. {}",
72 provider.display_name(),
73 provider.validation_hint()
74 ),
75 ))
76}