this repo has no description
1#[allow(deprecated)]
2use aes_gcm::{
3 Aes256Gcm, KeyInit, Nonce,
4 aead::Aead,
5};
6use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
7use hkdf::Hkdf;
8use p256::ecdsa::SigningKey;
9use sha2::{Digest, Sha256};
10use std::sync::OnceLock;
11
12static CONFIG: OnceLock<AuthConfig> = OnceLock::new();
13
14pub const ENCRYPTION_VERSION: i32 = 1;
15
16pub struct AuthConfig {
17 jwt_secret: String,
18 dpop_secret: String,
19 #[allow(dead_code)]
20 signing_key: SigningKey,
21 pub signing_key_id: String,
22 pub signing_key_x: String,
23 pub signing_key_y: String,
24 key_encryption_key: [u8; 32],
25}
26
27impl AuthConfig {
28 pub fn init() -> &'static Self {
29 CONFIG.get_or_init(|| {
30 let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
31 if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() {
32 "test-jwt-secret-not-for-production".to_string()
33 } else {
34 panic!(
35 "JWT_SECRET environment variable must be set in production. \
36 Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
37 );
38 }
39 });
40
41 let dpop_secret = std::env::var("DPOP_SECRET").unwrap_or_else(|_| {
42 if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() {
43 "test-dpop-secret-not-for-production".to_string()
44 } else {
45 panic!(
46 "DPOP_SECRET environment variable must be set in production. \
47 Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
48 );
49 }
50 });
51
52 if jwt_secret.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() {
53 panic!("JWT_SECRET must be at least 32 characters");
54 }
55 if dpop_secret.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() {
56 panic!("DPOP_SECRET must be at least 32 characters");
57 }
58
59 let mut hasher = Sha256::new();
60 hasher.update(b"oauth-signing-key-derivation:");
61 hasher.update(jwt_secret.as_bytes());
62 let seed = hasher.finalize();
63
64 let signing_key = SigningKey::from_slice(&seed)
65 .expect("Failed to create signing key from seed");
66
67 let verifying_key = signing_key.verifying_key();
68 let point = verifying_key.to_encoded_point(false);
69
70 let signing_key_x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
71 let signing_key_y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
72
73 let mut kid_hasher = Sha256::new();
74 kid_hasher.update(signing_key_x.as_bytes());
75 kid_hasher.update(signing_key_y.as_bytes());
76 let kid_hash = kid_hasher.finalize();
77 let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]);
78
79 let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| {
80 if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() {
81 "test-master-key-not-for-production".to_string()
82 } else {
83 panic!(
84 "MASTER_KEY environment variable must be set in production. \
85 Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
86 );
87 }
88 });
89
90 if master_key.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() {
91 panic!("MASTER_KEY must be at least 32 characters");
92 }
93
94 let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes());
95 let mut key_encryption_key = [0u8; 32];
96 hk.expand(b"bspds-user-key-encryption", &mut key_encryption_key)
97 .expect("HKDF expansion failed");
98
99 AuthConfig {
100 jwt_secret,
101 dpop_secret,
102 signing_key,
103 signing_key_id,
104 signing_key_x,
105 signing_key_y,
106 key_encryption_key,
107 }
108 })
109 }
110
111 pub fn get() -> &'static Self {
112 CONFIG.get().expect("AuthConfig not initialized - call AuthConfig::init() first")
113 }
114
115 pub fn jwt_secret(&self) -> &str {
116 &self.jwt_secret
117 }
118
119 pub fn dpop_secret(&self) -> &str {
120 &self.dpop_secret
121 }
122
123 pub fn encrypt_user_key(&self, plaintext: &[u8]) -> Result<Vec<u8>, String> {
124 use rand::RngCore;
125
126 let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key)
127 .map_err(|e| format!("Failed to create cipher: {}", e))?;
128
129 let mut nonce_bytes = [0u8; 12];
130 rand::thread_rng().fill_bytes(&mut nonce_bytes);
131 #[allow(deprecated)]
132 let nonce = Nonce::from_slice(&nonce_bytes);
133
134 let ciphertext = cipher
135 .encrypt(nonce, plaintext)
136 .map_err(|e| format!("Encryption failed: {}", e))?;
137
138 let mut result = Vec::with_capacity(12 + ciphertext.len());
139 result.extend_from_slice(&nonce_bytes);
140 result.extend_from_slice(&ciphertext);
141
142 Ok(result)
143 }
144
145 pub fn decrypt_user_key(&self, encrypted: &[u8]) -> Result<Vec<u8>, String> {
146 if encrypted.len() < 12 {
147 return Err("Encrypted data too short".to_string());
148 }
149
150 let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key)
151 .map_err(|e| format!("Failed to create cipher: {}", e))?;
152
153 #[allow(deprecated)]
154 let nonce = Nonce::from_slice(&encrypted[..12]);
155 let ciphertext = &encrypted[12..];
156
157 cipher
158 .decrypt(nonce, ciphertext)
159 .map_err(|e| format!("Decryption failed: {}", e))
160 }
161}
162
163pub fn encrypt_key(plaintext: &[u8]) -> Result<Vec<u8>, String> {
164 AuthConfig::get().encrypt_user_key(plaintext)
165}
166
167pub fn decrypt_key(encrypted: &[u8], version: Option<i32>) -> Result<Vec<u8>, String> {
168 match version.unwrap_or(0) {
169 0 => Ok(encrypted.to_vec()),
170 1 => AuthConfig::get().decrypt_user_key(encrypted),
171 v => Err(format!("Unknown encryption version: {}", v)),
172 }
173}