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