mas_config/sections/
secrets.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::borrow::Cow;
8
9use anyhow::{Context, bail};
10use camino::Utf8PathBuf;
11use futures_util::future::{try_join, try_join_all};
12use mas_jose::jwk::{JsonWebKey, JsonWebKeySet};
13use mas_keystore::{Encrypter, Keystore, PrivateKey};
14use rand::{
15    Rng, SeedableRng,
16    distributions::{Alphanumeric, DistString, Standard},
17    prelude::Distribution as _,
18};
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21use serde_with::serde_as;
22use tokio::task;
23use tracing::info;
24
25use super::ConfigurationSection;
26
27/// Password config option.
28///
29/// It either holds the password value directly or references a file where the
30/// password is stored.
31#[derive(Clone, Debug)]
32pub enum Password {
33    File(Utf8PathBuf),
34    Value(String),
35}
36
37/// Password fields as serialized in JSON.
38#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
39struct PasswordRaw {
40    #[schemars(with = "Option<String>")]
41    #[serde(skip_serializing_if = "Option::is_none")]
42    password_file: Option<Utf8PathBuf>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    password: Option<String>,
45}
46
47impl TryFrom<PasswordRaw> for Option<Password> {
48    type Error = anyhow::Error;
49
50    fn try_from(value: PasswordRaw) -> Result<Self, Self::Error> {
51        match (value.password, value.password_file) {
52            (None, None) => Ok(None),
53            (None, Some(path)) => Ok(Some(Password::File(path))),
54            (Some(password), None) => Ok(Some(Password::Value(password))),
55            (Some(_), Some(_)) => bail!("Cannot specify both `password` and `password_file`"),
56        }
57    }
58}
59
60impl From<Option<Password>> for PasswordRaw {
61    fn from(value: Option<Password>) -> Self {
62        match value {
63            Some(Password::File(path)) => PasswordRaw {
64                password_file: Some(path),
65                password: None,
66            },
67            Some(Password::Value(password)) => PasswordRaw {
68                password_file: None,
69                password: Some(password),
70            },
71            None => PasswordRaw {
72                password_file: None,
73                password: None,
74            },
75        }
76    }
77}
78
79/// Key config option.
80///
81/// It either holds the key value directly or references a file where the key is
82/// stored.
83#[derive(Clone, Debug)]
84pub enum Key {
85    File(Utf8PathBuf),
86    Value(String),
87}
88
89/// Key fields as serialized in JSON.
90#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
91struct KeyRaw {
92    #[schemars(with = "Option<String>")]
93    #[serde(skip_serializing_if = "Option::is_none")]
94    key_file: Option<Utf8PathBuf>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    key: Option<String>,
97}
98
99impl TryFrom<KeyRaw> for Key {
100    type Error = anyhow::Error;
101
102    fn try_from(value: KeyRaw) -> Result<Key, Self::Error> {
103        match (value.key, value.key_file) {
104            (None, None) => bail!("Missing `key` or `key_file`"),
105            (None, Some(path)) => Ok(Key::File(path)),
106            (Some(key), None) => Ok(Key::Value(key)),
107            (Some(_), Some(_)) => bail!("Cannot specify both `key` and `key_file`"),
108        }
109    }
110}
111
112impl From<Key> for KeyRaw {
113    fn from(value: Key) -> Self {
114        match value {
115            Key::File(path) => KeyRaw {
116                key_file: Some(path),
117                key: None,
118            },
119            Key::Value(key) => KeyRaw {
120                key_file: None,
121                key: Some(key),
122            },
123        }
124    }
125}
126
127/// A single key with its key ID and optional password.
128#[serde_as]
129#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
130pub struct KeyConfig {
131    kid: String,
132
133    #[schemars(with = "PasswordRaw")]
134    #[serde_as(as = "serde_with::TryFromInto<PasswordRaw>")]
135    #[serde(flatten)]
136    password: Option<Password>,
137
138    #[schemars(with = "KeyRaw")]
139    #[serde_as(as = "serde_with::TryFromInto<KeyRaw>")]
140    #[serde(flatten)]
141    key: Key,
142}
143
144impl KeyConfig {
145    /// Returns the password in case any is provided.
146    ///
147    /// If `password_file` was given, the password is read from that file.
148    async fn password(&self) -> anyhow::Result<Option<Cow<String>>> {
149        Ok(match &self.password {
150            Some(Password::File(path)) => Some(Cow::Owned(tokio::fs::read_to_string(path).await?)),
151            Some(Password::Value(password)) => Some(Cow::Borrowed(password)),
152            None => None,
153        })
154    }
155
156    /// Returns the key.
157    ///
158    /// If `key_file` was given, the key is read from that file.
159    async fn key(&self) -> anyhow::Result<Cow<String>> {
160        Ok(match &self.key {
161            Key::File(path) => Cow::Owned(tokio::fs::read_to_string(path).await?),
162            Key::Value(key) => Cow::Borrowed(key),
163        })
164    }
165
166    /// Returns the JSON Web Key derived from this key config.
167    ///
168    /// Password and/or key are read from file if they’re given as path.
169    async fn json_web_key(&self) -> anyhow::Result<JsonWebKey<mas_keystore::PrivateKey>> {
170        let (key, password) = try_join(self.key(), self.password()).await?;
171
172        let private_key = match password {
173            Some(password) => PrivateKey::load_encrypted(key.as_bytes(), password.as_bytes())?,
174            None => PrivateKey::load(key.as_bytes())?,
175        };
176
177        Ok(JsonWebKey::new(private_key)
178            .with_kid(self.kid.clone())
179            .with_use(mas_iana::jose::JsonWebKeyUse::Sig))
180    }
181}
182
183/// Encryption config option.
184#[derive(Debug, Clone)]
185pub enum Encryption {
186    File(Utf8PathBuf),
187    Value([u8; 32]),
188}
189
190/// Encryption fields as serialized in JSON.
191#[serde_as]
192#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)]
193struct EncryptionRaw {
194    /// File containing the encryption key for secure cookies.
195    #[schemars(with = "Option<String>")]
196    #[serde(skip_serializing_if = "Option::is_none")]
197    encryption_file: Option<Utf8PathBuf>,
198
199    /// Encryption key for secure cookies.
200    #[schemars(
201        with = "Option<String>",
202        regex(pattern = r"[0-9a-fA-F]{64}"),
203        example = &"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
204    )]
205    #[serde_as(as = "Option<serde_with::hex::Hex>")]
206    #[serde(skip_serializing_if = "Option::is_none")]
207    encryption: Option<[u8; 32]>,
208}
209
210impl TryFrom<EncryptionRaw> for Encryption {
211    type Error = anyhow::Error;
212
213    fn try_from(value: EncryptionRaw) -> Result<Encryption, Self::Error> {
214        match (value.encryption, value.encryption_file) {
215            (None, None) => bail!("Missing `encryption` or `encryption_file`"),
216            (None, Some(path)) => Ok(Encryption::File(path)),
217            (Some(encryption), None) => Ok(Encryption::Value(encryption)),
218            (Some(_), Some(_)) => bail!("Cannot specify both `encryption` and `encryption_file`"),
219        }
220    }
221}
222
223impl From<Encryption> for EncryptionRaw {
224    fn from(value: Encryption) -> Self {
225        match value {
226            Encryption::File(path) => EncryptionRaw {
227                encryption_file: Some(path),
228                encryption: None,
229            },
230            Encryption::Value(encryption) => EncryptionRaw {
231                encryption_file: None,
232                encryption: Some(encryption),
233            },
234        }
235    }
236}
237
238/// Application secrets
239#[serde_as]
240#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
241pub struct SecretsConfig {
242    /// Encryption key for secure cookies
243    #[schemars(with = "EncryptionRaw")]
244    #[serde_as(as = "serde_with::TryFromInto<EncryptionRaw>")]
245    #[serde(flatten)]
246    encryption: Encryption,
247
248    /// List of private keys to use for signing and encrypting payloads
249    #[serde(default)]
250    keys: Vec<KeyConfig>,
251}
252
253impl SecretsConfig {
254    /// Derive a signing and verifying keystore out of the config
255    ///
256    /// # Errors
257    ///
258    /// Returns an error when a key could not be imported
259    #[tracing::instrument(name = "secrets.load", skip_all)]
260    pub async fn key_store(&self) -> anyhow::Result<Keystore> {
261        let web_keys = try_join_all(self.keys.iter().map(KeyConfig::json_web_key)).await?;
262
263        Ok(Keystore::new(JsonWebKeySet::new(web_keys)))
264    }
265
266    /// Derive an [`Encrypter`] out of the config
267    ///
268    /// # Errors
269    ///
270    /// Returns an error when the Encryptor can not be created.
271    pub async fn encrypter(&self) -> anyhow::Result<Encrypter> {
272        Ok(Encrypter::new(&self.encryption().await?))
273    }
274
275    /// Returns the encryption secret.
276    ///
277    /// # Errors
278    ///
279    /// Returns an error when the encryption secret could not be read from file.
280    pub async fn encryption(&self) -> anyhow::Result<[u8; 32]> {
281        // Read the encryption secret either embedded in the config file or on disk
282        match self.encryption {
283            Encryption::Value(encryption) => Ok(encryption),
284            Encryption::File(ref path) => {
285                let mut bytes = [0; 32];
286                let content = tokio::fs::read(path).await?;
287                hex::decode_to_slice(content, &mut bytes).context(
288                    "Content of `encryption_file` must contain hex characters \
289                    encoding exactly 32 bytes",
290                )?;
291                Ok(bytes)
292            }
293        }
294    }
295}
296
297impl ConfigurationSection for SecretsConfig {
298    const PATH: Option<&'static str> = Some("secrets");
299}
300
301impl SecretsConfig {
302    #[tracing::instrument(skip_all)]
303    pub(crate) async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
304    where
305        R: Rng + Send,
306    {
307        info!("Generating keys...");
308
309        let span = tracing::info_span!("rsa");
310        let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
311        let rsa_key = task::spawn_blocking(move || {
312            let _entered = span.enter();
313            let ret = PrivateKey::generate_rsa(key_rng).unwrap();
314            info!("Done generating RSA key");
315            ret
316        })
317        .await
318        .context("could not join blocking task")?;
319        let rsa_key = KeyConfig {
320            kid: Alphanumeric.sample_string(&mut rng, 10),
321            password: None,
322            key: Key::Value(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
323        };
324
325        let span = tracing::info_span!("ec_p256");
326        let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
327        let ec_p256_key = task::spawn_blocking(move || {
328            let _entered = span.enter();
329            let ret = PrivateKey::generate_ec_p256(key_rng);
330            info!("Done generating EC P-256 key");
331            ret
332        })
333        .await
334        .context("could not join blocking task")?;
335        let ec_p256_key = KeyConfig {
336            kid: Alphanumeric.sample_string(&mut rng, 10),
337            password: None,
338            key: Key::Value(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
339        };
340
341        let span = tracing::info_span!("ec_p384");
342        let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
343        let ec_p384_key = task::spawn_blocking(move || {
344            let _entered = span.enter();
345            let ret = PrivateKey::generate_ec_p384(key_rng);
346            info!("Done generating EC P-256 key");
347            ret
348        })
349        .await
350        .context("could not join blocking task")?;
351        let ec_p384_key = KeyConfig {
352            kid: Alphanumeric.sample_string(&mut rng, 10),
353            password: None,
354            key: Key::Value(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
355        };
356
357        let span = tracing::info_span!("ec_k256");
358        let key_rng = rand_chacha::ChaChaRng::from_rng(&mut rng)?;
359        let ec_k256_key = task::spawn_blocking(move || {
360            let _entered = span.enter();
361            let ret = PrivateKey::generate_ec_k256(key_rng);
362            info!("Done generating EC secp256k1 key");
363            ret
364        })
365        .await
366        .context("could not join blocking task")?;
367        let ec_k256_key = KeyConfig {
368            kid: Alphanumeric.sample_string(&mut rng, 10),
369            password: None,
370            key: Key::Value(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()),
371        };
372
373        Ok(Self {
374            encryption: Encryption::Value(Standard.sample(&mut rng)),
375            keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
376        })
377    }
378
379    pub(crate) fn test() -> Self {
380        let rsa_key = KeyConfig {
381            kid: "abcdef".to_owned(),
382            password: None,
383            key: Key::Value(
384                indoc::indoc! {r"
385                  -----BEGIN PRIVATE KEY-----
386                  MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN
387                  QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU
388                  scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh
389                  3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE
390                  vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw
391                  N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622
392                  tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl
393                  Gh7BNzCeN+D6
394                  -----END PRIVATE KEY-----
395                "}
396                .to_owned(),
397            ),
398        };
399        let ecdsa_key = KeyConfig {
400            kid: "ghijkl".to_owned(),
401            password: None,
402            key: Key::Value(
403                indoc::indoc! {r"
404                  -----BEGIN PRIVATE KEY-----
405                  MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA
406                  NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn
407                  OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC
408                  -----END PRIVATE KEY-----
409                "}
410                .to_owned(),
411            ),
412        };
413
414        Self {
415            encryption: Encryption::Value([0xEA; 32]),
416            keys: vec![rsa_key, ecdsa_key],
417        }
418    }
419}