diff --git a/crates/cli/src/commands/config.rs b/crates/cli/src/commands/config.rs index 8416e5592..00600026c 100644 --- a/crates/cli/src/commands/config.rs +++ b/crates/cli/src/commands/config.rs @@ -123,7 +123,7 @@ impl Options { SC::Sync { prune, dry_run } => { let config = SyncConfig::extract(figment)?; let clock = SystemClock::default(); - let encrypter = config.secrets.encrypter(); + let encrypter = config.secrets.encrypter().await?; // Grab a connection to the database let mut conn = database_connection_from_config(&config.database).await?; diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 4f2fc6205..dcdbca0d3 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -94,7 +94,7 @@ impl Options { .context("could not run database migrations")?; } - let encrypter = config.secrets.encrypter(); + let encrypter = config.secrets.encrypter().await?; if self.no_sync { info!("Skipping configuration sync"); @@ -124,8 +124,10 @@ impl Options { .await .context("could not import keys from config")?; - let cookie_manager = - CookieManager::derive_from(config.http.public_base.clone(), &config.secrets.encryption); + let cookie_manager = CookieManager::derive_from( + config.http.public_base.clone(), + &config.secrets.encryption().await?, + ); // Load and compile the WASM policies (and fallback to the default embedded one) info!("Loading and compiling the policy module"); diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index ac009f5cf..22194953e 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -133,7 +133,7 @@ impl Options { // in the MAS database let config = SyncConfig::extract(figment)?; let clock = SystemClock::default(); - let encrypter = config.secrets.encrypter(); + let encrypter = config.secrets.encrypter().await?; crate::sync::config_sync( config.upstream_oauth2, diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index 10df52e02..1800ebc2e 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -6,7 +6,7 @@ use std::borrow::Cow; -use anyhow::{Context, bail}; +use anyhow::{Context, anyhow, bail}; use camino::Utf8PathBuf; use mas_jose::jwk::{JsonWebKey, JsonWebKeySet}; use mas_keystore::{Encrypter, Keystore, PrivateKey}; @@ -46,18 +46,68 @@ pub struct KeyConfig { key_file: Option, } +/// Encryption config option. +#[derive(Debug, Clone)] +pub enum Encryption { + File(Utf8PathBuf), + Value([u8; 32]), +} + +/// Encryption fields as serialized in JSON. +#[serde_as] +#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)] +struct EncryptionRaw { + /// File containing the encryption key for secure cookies. + #[schemars(with = "Option")] + encryption_file: Option, + + /// Encryption key for secure cookies. + #[schemars( + with = "Option", + regex(pattern = r"[0-9a-fA-F]{64}"), + example = "example_secret" + )] + #[serde_as(as = "Option")] + encryption: Option<[u8; 32]>, +} + +impl TryFrom for Encryption { + type Error = anyhow::Error; + + fn try_from(value: EncryptionRaw) -> Result { + match (value.encryption, value.encryption_file) { + (None, None) => bail!("Missing `encryption` or `encryption_file`"), + (None, Some(path)) => Ok(Encryption::File(path)), + (Some(encryption), None) => Ok(Encryption::Value(encryption)), + (Some(_), Some(_)) => bail!("Cannot specify both `encryption` and `encryption_file`"), + } + } +} + +impl From for EncryptionRaw { + fn from(value: Encryption) -> Self { + match value { + Encryption::File(path) => EncryptionRaw { + encryption_file: Some(path), + encryption: None, + }, + Encryption::Value(encryption) => EncryptionRaw { + encryption_file: None, + encryption: Some(encryption), + }, + } + } +} + /// Application secrets #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct SecretsConfig { /// Encryption key for secure cookies - #[schemars( - with = "String", - regex(pattern = r"[0-9a-fA-F]{64}"), - example = "example_secret" - )] - #[serde_as(as = "serde_with::hex::Hex")] - pub encryption: [u8; 32], + #[schemars(with = "EncryptionRaw")] + #[serde_as(as = "serde_with::TryFromInto")] + #[serde(flatten)] + encryption: Encryption, /// List of private keys to use for signing and encrypting payloads #[serde(default)] @@ -118,9 +168,27 @@ impl SecretsConfig { } /// Derive an [`Encrypter`] out of the config - #[must_use] - pub fn encrypter(&self) -> Encrypter { - Encrypter::new(&self.encryption) + /// + /// # Errors + /// + /// Returns an error when the Encryptor can not be created. + pub async fn encrypter(&self) -> anyhow::Result { + Ok(Encrypter::new(&self.encryption().await?)) + } + + /// Returns the encryption secret. + /// + /// # Errors + /// + /// Returns an error when the encryption secret could not be read from file. + pub async fn encryption(&self) -> anyhow::Result<[u8; 32]> { + // Read the encryption secret either embedded in the config file or on disk + match self.encryption { + Encryption::Value(encryption) => Ok(encryption), + Encryption::File(ref path) => tokio::fs::read(path).await?.try_into().map_err(|_| { + anyhow!("Content of `encryption_file` must be exactly 32 bytes long.") + }), + } } } @@ -246,7 +314,7 @@ impl SecretsConfig { }; Ok(Self { - encryption: Standard.sample(&mut rng), + encryption: Encryption::Value(Standard.sample(&mut rng)), keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key], }) } @@ -291,7 +359,7 @@ impl SecretsConfig { }; Self { - encryption: [0xEA; 32], + encryption: Encryption::Value([0xEA; 32]), keys: vec![rsa_key, ecdsa_key], } } diff --git a/docs/config.schema.json b/docs/config.schema.json index 3bc0f407d..102857999 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1515,18 +1515,7 @@ "SecretsConfig": { "description": "Application secrets", "type": "object", - "required": [ - "encryption" - ], "properties": { - "encryption": { - "description": "Encryption key for secure cookies", - "examples": [ - "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" - ], - "type": "string", - "pattern": "[0-9a-fA-F]{64}" - }, "keys": { "description": "List of private keys to use for signing and encrypting payloads", "default": [], @@ -1534,6 +1523,19 @@ "items": { "$ref": "#/definitions/KeyConfig" } + }, + "encryption_file": { + "description": "File containing the encryption key for secure cookies.", + "type": "string" + }, + "encryption": { + "description": "Encryption key for secure cookies.", + "default": null, + "examples": [ + "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" + ], + "type": "string", + "pattern": "[0-9a-fA-F]{64}" } } },