Skip to main content

bitwarden_pam_sealed/
rotation_config.rs

1use bitwarden_crypto::{
2    EncString, KeySlotIds, KeyStoreContext, generate_versioned_sealable,
3    safe::{
4        DataEnvelope, DataEnvelopeError, DataEnvelopeNamespace, SealableData, SealableVersionedData,
5    },
6};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use crate::Sealed;
11
12/// Security-relevant fields of a PAM rotation config. Sealed by the admin client so a hostile
13/// server can't choose the account, cipher, or session-termination behavior.
14#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
15pub struct RotationConfigSealedV1 {
16    /// Cross-checked against the claim and the other blob to stop swaps.
17    pub target_system_id: Uuid,
18    /// Checked against the claim's `cipher_id`.
19    pub cipher_id: Uuid,
20    /// The account to rotate. Never interpolate unsanitized into a shell command or query.
21    pub account_identity: String,
22    /// ANDed with the target's `supports_session_termination`.
23    pub terminate_sessions: bool,
24}
25impl SealableData for RotationConfigSealedV1 {}
26
27generate_versioned_sealable!(
28    RotationConfigSealed,
29    DataEnvelopeNamespace::PamRotationConfig,
30    [
31        RotationConfigSealedV1 => "1",
32    ]
33);
34
35/// Seals `data` under `org_key`.
36pub fn seal_rotation_config<Ids: KeySlotIds>(
37    data: RotationConfigSealedV1,
38    org_key: &Ids::Symmetric,
39    ctx: &mut KeyStoreContext<Ids>,
40) -> Result<Sealed, DataEnvelopeError> {
41    let versioned: RotationConfigSealed = data.into();
42    let (envelope, wrapped_key) = DataEnvelope::seal_with_wrapping_key(versioned, org_key, ctx)?;
43    Ok(Sealed {
44        envelope,
45        wrapped_key,
46    })
47}
48
49/// Unseals a [`RotationConfigSealedV1`] from the output of [`seal_rotation_config`].
50pub fn unseal_rotation_config<Ids: KeySlotIds>(
51    envelope: &DataEnvelope,
52    wrapped_key: &EncString,
53    org_key: &Ids::Symmetric,
54    ctx: &mut KeyStoreContext<Ids>,
55) -> Result<RotationConfigSealedV1, DataEnvelopeError> {
56    let versioned: RotationConfigSealed =
57        envelope.unseal_with_wrapping_key(org_key, wrapped_key, ctx)?;
58    let RotationConfigSealed::RotationConfigSealedV1(data) = versioned;
59    Ok(data)
60}
61
62#[cfg(test)]
63mod tests {
64    use std::str::FromStr;
65
66    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
67    use bitwarden_encoding::B64;
68
69    use super::*;
70    use crate::test_support::TestIds;
71
72    fn sample() -> RotationConfigSealedV1 {
73        RotationConfigSealedV1 {
74            target_system_id: Uuid::parse_str("8c6e7d1a-3e3f-4a5b-9c1d-2f3e4d5c6b7a").unwrap(),
75            cipher_id: Uuid::parse_str("1a2b3c4d-5e6f-4789-9abc-def012345678").unwrap(),
76            account_identity: "[email protected]".to_string(),
77            terminate_sessions: true,
78        }
79    }
80
81    #[test]
82    fn round_trip() {
83        let store = KeyStore::<TestIds>::default();
84        let mut ctx = store.context_mut();
85        let org_key = ctx.generate_symmetric_key();
86
87        let data = sample();
88        let Sealed {
89            envelope,
90            wrapped_key,
91        } = seal_rotation_config(data.clone(), &org_key, &mut ctx).unwrap();
92        let unsealed = unseal_rotation_config(&envelope, &wrapped_key, &org_key, &mut ctx).unwrap();
93
94        assert_eq!(unsealed, data);
95    }
96
97    #[test]
98    fn wire_string_round_trip() {
99        let store = KeyStore::<TestIds>::default();
100        let mut ctx = store.context_mut();
101        let org_key = ctx.generate_symmetric_key();
102
103        let data = sample();
104        let Sealed {
105            envelope,
106            wrapped_key,
107        } = seal_rotation_config(data.clone(), &org_key, &mut ctx).unwrap();
108        let envelope: DataEnvelope = envelope.to_string().parse().unwrap();
109        let wrapped_key: EncString = wrapped_key.to_string().parse().unwrap();
110
111        let unsealed = unseal_rotation_config(&envelope, &wrapped_key, &org_key, &mut ctx).unwrap();
112        assert_eq!(unsealed, data);
113    }
114
115    #[test]
116    fn wrong_key_fails() {
117        let store = KeyStore::<TestIds>::default();
118        let mut ctx = store.context_mut();
119        let org_key = ctx.generate_symmetric_key();
120        let wrong_key = ctx.generate_symmetric_key();
121
122        let Sealed {
123            envelope,
124            wrapped_key,
125        } = seal_rotation_config(sample(), &org_key, &mut ctx).unwrap();
126        let result =
127            unseal_rotation_config::<TestIds>(&envelope, &wrapped_key, &wrong_key, &mut ctx);
128
129        assert!(result.is_err());
130    }
131
132    #[test]
133    #[ignore = "Manual test to generate test vectors"]
134    fn generate_test_vectors() {
135        let store = KeyStore::<TestIds>::default();
136        let mut ctx = store.context_mut();
137        let org_key = ctx.generate_symmetric_key();
138        let key_b64 = {
139            #[allow(deprecated)]
140            let key = ctx.dangerous_get_symmetric_key(org_key).unwrap();
141            key.to_base64()
142        };
143
144        let Sealed {
145            envelope,
146            wrapped_key,
147        } = seal_rotation_config(sample(), &org_key, &mut ctx).unwrap();
148
149        let (envelope, wrapped_key) = (envelope.to_string(), wrapped_key.to_string());
150        println!("const TEST_VECTOR_ORG_KEY: &str = \"{key_b64}\";");
151        println!("const TEST_VECTOR_ROTATION_CONFIG_SEALED_DATA: &str = \"{envelope}\";");
152        println!("const TEST_VECTOR_ROTATION_CONFIG_WRAPPED_KEY: &str = \"{wrapped_key}\";");
153    }
154
155    const TEST_VECTOR_ORG_KEY: &str =
156        "vA1+lDkWyNN0ZVTRLJMs64LXMbwW+h0x1nN2nPJ5clYtuXLL3ek3bKW/uDb1Kmye7vz7o7hADuZBDeMuQfoCiw==";
157    const TEST_VECTOR_ROTATION_CONFIG_SEALED_DATA: &str = "g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUOwuU91Xcz4pzHy3rmrPeP06AAE4gQI6AAE4gAWhBUyORP6msVGzzmXPo29YoSD80pwt83q+nnI+6B519Ur9T+t0PHxX50GRwzeJTE5zmD/fG5D5a32OrKkbeePwL/BDUjL4YPu44XC9Fp3T2vEUunJp4G4pm8gHWZdAmp6Pz2zNsq3nRSBBKUfkpQVRPf0+oCUVIRJF8UYIY+QkZmHimmNZi/DrwiXbNusjO7DjM5s2X5cnhxhLwpBaBwnyqMnCNLuptbBZYYKluNkebXFK";
158    const TEST_VECTOR_ROTATION_CONFIG_WRAPPED_KEY: &str = "2.n0nzXEnaK2oxw+szuM3OZA==|DoiILS1QEEt7VbD5VyBmVuhhlkdoqkXNeAhxvNHCJQbq5/9Ut89nUZ45uMNfqf0kaM0ttRAHly30433dJ8PwplyKnZqU5OoaoxBxWY7C1SA=|ZP7wSGPyte0dkkjjxQxZiJ/gBaK8iy/1MEuw+O9UZGM=";
159
160    #[test]
161    fn rotation_config_test_vector() {
162        let org_key =
163            SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_ORG_KEY).unwrap()).unwrap();
164
165        let store = KeyStore::<TestIds>::default();
166        let mut ctx = store.context_mut();
167        let org_key_id = ctx.add_local_symmetric_key(org_key);
168
169        let envelope = DataEnvelope::from_str(TEST_VECTOR_ROTATION_CONFIG_SEALED_DATA).unwrap();
170        let wrapped_key = EncString::from_str(TEST_VECTOR_ROTATION_CONFIG_WRAPPED_KEY).unwrap();
171        let unsealed = unseal_rotation_config(&envelope, &wrapped_key, &org_key_id, &mut ctx)
172            .expect(
173                "RotationConfigSealed wire format has changed in a backwards-incompatible \
174                     way. Existing sealed data must remain deserializable.",
175            );
176
177        assert_eq!(unsealed, sample());
178    }
179}