|
6 | 6 |
|
7 | 7 | use std::borrow::Cow;
|
8 | 8 |
|
9 |
| -use anyhow::{Context, bail}; |
| 9 | +use anyhow::{Context, anyhow, bail}; |
10 | 10 | use camino::Utf8PathBuf;
|
11 | 11 | use mas_jose::jwk::{JsonWebKey, JsonWebKeySet};
|
12 | 12 | use mas_keystore::{Encrypter, Keystore, PrivateKey};
|
@@ -46,18 +46,68 @@ pub struct KeyConfig {
|
46 | 46 | key_file: Option<Utf8PathBuf>,
|
47 | 47 | }
|
48 | 48 |
|
49 |
| -/// Application secrets |
| 49 | +/// Encryption config option. |
| 50 | +#[derive(Debug, Clone)] |
| 51 | +pub enum Encryption { |
| 52 | + File(Utf8PathBuf), |
| 53 | + Value([u8; 32]), |
| 54 | +} |
| 55 | + |
| 56 | +/// Encryption fields as serialized in JSON. |
50 | 57 | #[serde_as]
|
51 |
| -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] |
52 |
| -pub struct SecretsConfig { |
53 |
| - /// Encryption key for secure cookies |
| 58 | +#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)] |
| 59 | +struct EncryptionRaw { |
| 60 | + /// File containing the encryption key for secure cookies. |
| 61 | + #[schemars(with = "Option<String>")] |
| 62 | + encryption_file: Option<Utf8PathBuf>, |
| 63 | + |
| 64 | + /// Encryption key for secure cookies. |
54 | 65 | #[schemars(
|
55 |
| - with = "String", |
| 66 | + with = "Option<String>", |
56 | 67 | regex(pattern = r"[0-9a-fA-F]{64}"),
|
57 | 68 | example = "example_secret"
|
58 | 69 | )]
|
59 |
| - #[serde_as(as = "serde_with::hex::Hex")] |
60 |
| - pub encryption: [u8; 32], |
| 70 | + #[serde_as(as = "Option<serde_with::hex::Hex>")] |
| 71 | + encryption: Option<[u8; 32]>, |
| 72 | +} |
| 73 | + |
| 74 | +impl TryFrom<EncryptionRaw> for Encryption { |
| 75 | + type Error = anyhow::Error; |
| 76 | + |
| 77 | + fn try_from(value: EncryptionRaw) -> Result<Encryption, Self::Error> { |
| 78 | + match (value.encryption, value.encryption_file) { |
| 79 | + (None, None) => bail!("Missing `encryption` or `encryption_file`"), |
| 80 | + (None, Some(path)) => Ok(Encryption::File(path)), |
| 81 | + (Some(encryption), None) => Ok(Encryption::Value(encryption)), |
| 82 | + (Some(_), Some(_)) => bail!("Cannot specify both `encryption` and `encryption_file`"), |
| 83 | + } |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +impl From<Encryption> for EncryptionRaw { |
| 88 | + fn from(value: Encryption) -> Self { |
| 89 | + match value { |
| 90 | + Encryption::File(path) => EncryptionRaw { |
| 91 | + encryption_file: Some(path), |
| 92 | + encryption: None, |
| 93 | + }, |
| 94 | + Encryption::Value(encryption) => EncryptionRaw { |
| 95 | + encryption_file: None, |
| 96 | + encryption: Some(encryption), |
| 97 | + }, |
| 98 | + } |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +/// Application secrets |
| 103 | +#[serde_as] |
| 104 | +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] |
| 105 | +pub struct SecretsConfig { |
| 106 | + /// Encryption key for secure cookies |
| 107 | + #[schemars(with = "EncryptionRaw")] |
| 108 | + #[serde_as(as = "serde_with::TryFromInto<EncryptionRaw>")] |
| 109 | + #[serde(flatten)] |
| 110 | + encryption: Encryption, |
61 | 111 |
|
62 | 112 | /// List of private keys to use for signing and encrypting payloads
|
63 | 113 | #[serde(default)]
|
@@ -118,9 +168,27 @@ impl SecretsConfig {
|
118 | 168 | }
|
119 | 169 |
|
120 | 170 | /// Derive an [`Encrypter`] out of the config
|
121 |
| - #[must_use] |
122 |
| - pub fn encrypter(&self) -> Encrypter { |
123 |
| - Encrypter::new(&self.encryption) |
| 171 | + /// |
| 172 | + /// # Errors |
| 173 | + /// |
| 174 | + /// Returns an error when the Encryptor can not be created. |
| 175 | + pub async fn encrypter(&self) -> anyhow::Result<Encrypter> { |
| 176 | + Ok(Encrypter::new(&self.encryption().await?)) |
| 177 | + } |
| 178 | + |
| 179 | + /// Returns the encryption secret. |
| 180 | + /// |
| 181 | + /// # Errors |
| 182 | + /// |
| 183 | + /// Returns an error when the encryption secret could not be read from file. |
| 184 | + pub async fn encryption(&self) -> anyhow::Result<[u8; 32]> { |
| 185 | + // Read the encryption secret either embedded in the config file or on disk |
| 186 | + match self.encryption { |
| 187 | + Encryption::Value(encryption) => Ok(encryption), |
| 188 | + Encryption::File(ref path) => tokio::fs::read(path).await?.try_into().map_err(|_| { |
| 189 | + anyhow!("Content of `encryption_file` must be exactly 32 bytes long.") |
| 190 | + }), |
| 191 | + } |
124 | 192 | }
|
125 | 193 | }
|
126 | 194 |
|
@@ -246,7 +314,7 @@ impl SecretsConfig {
|
246 | 314 | };
|
247 | 315 |
|
248 | 316 | Ok(Self {
|
249 |
| - encryption: Standard.sample(&mut rng), |
| 317 | + encryption: Encryption::Value(Standard.sample(&mut rng)), |
250 | 318 | keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
|
251 | 319 | })
|
252 | 320 | }
|
@@ -291,7 +359,7 @@ impl SecretsConfig {
|
291 | 359 | };
|
292 | 360 |
|
293 | 361 | Self {
|
294 |
| - encryption: [0xEA; 32], |
| 362 | + encryption: Encryption::Value([0xEA; 32]), |
295 | 363 | keys: vec![rsa_key, ecdsa_key],
|
296 | 364 | }
|
297 | 365 | }
|
|
0 commit comments