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
56 if dpop_secret.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() {
57 panic!("DPOP_SECRET must be at least 32 characters");
58 }
59
60 let mut hasher = Sha256::new();
61 hasher.update(b"oauth-signing-key-derivation:");
62 hasher.update(jwt_secret.as_bytes());
63 let seed = hasher.finalize();
64
65 let signing_key = SigningKey::from_slice(&seed)
66 .unwrap_or_else(|e| panic!("Failed to create signing key from seed: {}. This is a bug.", e));
67
68 let verifying_key = signing_key.verifying_key();
69 let point = verifying_key.to_encoded_point(false);
70
71 let signing_key_x = URL_SAFE_NO_PAD.encode(
72 point.x().expect("EC point missing X coordinate - this should never happen")
73 );
74 let signing_key_y = URL_SAFE_NO_PAD.encode(
75 point.y().expect("EC point missing Y coordinate - this should never happen")
76 );
77
78 let mut kid_hasher = Sha256::new();
79 kid_hasher.update(signing_key_x.as_bytes());
80 kid_hasher.update(signing_key_y.as_bytes());
81 let kid_hash = kid_hasher.finalize();
82 let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]);
83
84 let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| {
85 if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() {
86 "test-master-key-not-for-production".to_string()
87 } else {
88 panic!(
89 "MASTER_KEY environment variable must be set in production. \
90 Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
91 );
92 }
93 });
94
95 if master_key.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() {
96 panic!("MASTER_KEY must be at least 32 characters");
97 }
98
99 let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes());
100 let mut key_encryption_key = [0u8; 32];
101 hk.expand(b"bspds-user-key-encryption", &mut key_encryption_key)
102 .expect("HKDF expansion failed");
103
104 AuthConfig {
105 jwt_secret,
106 dpop_secret,
107 signing_key,
108 signing_key_id,
109 signing_key_x,
110 signing_key_y,
111 key_encryption_key,
112 }
113 })
114 }
115
116 pub fn get() -> &'static Self {
117 CONFIG.get().expect("AuthConfig not initialized - call AuthConfig::init() first")
118 }
119
120 pub fn jwt_secret(&self) -> &str {
121 &self.jwt_secret
122 }
123
124 pub fn dpop_secret(&self) -> &str {
125 &self.dpop_secret
126 }
127
128 pub fn encrypt_user_key(&self, plaintext: &[u8]) -> Result<Vec<u8>, String> {
129 use rand::RngCore;
130
131 let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key)
132 .map_err(|e| format!("Failed to create cipher: {}", e))?;
133
134 let mut nonce_bytes = [0u8; 12];
135 rand::thread_rng().fill_bytes(&mut nonce_bytes);
136
137 #[allow(deprecated)]
138 let nonce = Nonce::from_slice(&nonce_bytes);
139
140 let ciphertext = cipher
141 .encrypt(nonce, plaintext)
142 .map_err(|e| format!("Encryption failed: {}", e))?;
143
144 let mut result = Vec::with_capacity(12 + ciphertext.len());
145 result.extend_from_slice(&nonce_bytes);
146 result.extend_from_slice(&ciphertext);
147
148 Ok(result)
149 }
150
151 pub fn decrypt_user_key(&self, encrypted: &[u8]) -> Result<Vec<u8>, String> {
152 if encrypted.len() < 12 {
153 return Err("Encrypted data too short".to_string());
154 }
155
156 let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key)
157 .map_err(|e| format!("Failed to create cipher: {}", e))?;
158
159 #[allow(deprecated)]
160 let nonce = Nonce::from_slice(&encrypted[..12]);
161 let ciphertext = &encrypted[12..];
162
163 cipher
164 .decrypt(nonce, ciphertext)
165 .map_err(|e| format!("Decryption failed: {}", e))
166 }
167}
168
169pub fn encrypt_key(plaintext: &[u8]) -> Result<Vec<u8>, String> {
170 AuthConfig::get().encrypt_user_key(plaintext)
171}
172
173pub fn decrypt_key(encrypted: &[u8], version: Option<i32>) -> Result<Vec<u8>, String> {
174 match version.unwrap_or(0) {
175 0 => Ok(encrypted.to_vec()),
176 1 => AuthConfig::get().decrypt_user_key(encrypted),
177 v => Err(format!("Unknown encryption version: {}", v)),
178 }
179}