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