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