diff --git a/crates/shared/src/crypto.rs b/crates/shared/src/crypto.rs index 8ede3dd..cb03cce 100644 --- a/crates/shared/src/crypto.rs +++ b/crates/shared/src/crypto.rs @@ -144,6 +144,15 @@ pub enum MasterKeyError { /// Decoded to the wrong number of bytes. #[error("PICLOUD_SECRET_KEY must decode to exactly {KEY_LEN} bytes, got {0}")] WrongLength(usize), + + /// F-S-009: PICLOUD_DEV_MODE=true was set without PICLOUD_SECRET_KEY, + /// AND without the explicit PICLOUD_DEV_INSECURE_KEY acknowledgement. + #[error( + "PICLOUD_DEV_MODE=true without PICLOUD_SECRET_KEY requires an explicit acknowledgement. \ + Set PICLOUD_DEV_INSECURE_KEY=i-understand-this-is-insecure to confirm — but never in \ + production: the dev master key is a fully public deterministic value." + )] + DevModeUnacknowledged, } impl MasterKey { @@ -189,6 +198,18 @@ impl MasterKey { let dev_mode = std::env::var("PICLOUD_DEV_MODE") .map(|v| is_truthy(&v)) .unwrap_or(false); + // F-S-009: PICLOUD_DEV_MODE alone falls through to a fully + // public deterministic key (the warning is correct but the gate + // is a single env var — copying a dev docker-compose file into + // prod silently encrypts everything with a world-known key). + // Require an explicit second knob acknowledging the risk before + // we accept dev mode without a real secret. + let dev_ack = std::env::var("PICLOUD_DEV_INSECURE_KEY") + .map(|v| v == "i-understand-this-is-insecure") + .unwrap_or(false); + if dev_mode && secret.as_deref().map(str::trim).unwrap_or("").is_empty() && !dev_ack { + return Err(MasterKeyError::DevModeUnacknowledged); + } Self::resolve(secret.as_deref(), dev_mode) }