Skip to main content

bitwarden_access_connector/
crypto.rs

1//! Cryptographic helpers used by the access connector.
2//!
3//! The org slot is keyed by the [`OrganizationId`] from the access token's `organization` claim.
4//! The cipher half of a rotation lives in `bitwarden_pam::rotation`.
5
6use bitwarden_access_token::{AccessToken, AccessTokenError};
7use bitwarden_core::{
8    OrganizationId,
9    key_management::{KeySlotIds, SymmetricKeySlotId},
10};
11use bitwarden_crypto::KeyStore;
12use thiserror::Error;
13
14/// Errors produced by the cryptographic helpers in this module.
15#[derive(Debug, Error)]
16pub(crate) enum CryptoModuleError {
17    /// The encrypted payload could not be decoded or decrypted.
18    #[error("org-key payload is invalid")]
19    InvalidPayload,
20
21    /// The decrypted payload does not contain a valid org key.
22    #[error("org-key payload does not contain a valid encryption key")]
23    InvalidOrgKey,
24
25    /// A generic cryptographic operation failed.
26    #[error("cryptographic operation failed")]
27    Crypto(#[from] bitwarden_crypto::CryptoError),
28}
29
30/// Decrypt the identity server's `encrypted_payload` with `token`'s derived key and install the org
31/// key into `store`.
32pub(crate) fn unwrap_org_key(
33    store: &KeyStore<KeySlotIds>,
34    organization_id: OrganizationId,
35    token: &AccessToken,
36    encrypted_payload: &str,
37) -> Result<(), CryptoModuleError> {
38    let mut ctx = store.context_mut();
39
40    token
41        .open_payload(
42            &mut ctx,
43            encrypted_payload,
44            SymmetricKeySlotId::Organization(organization_id),
45        )
46        .map_err(|e| match e {
47            AccessTokenError::InvalidPayload => CryptoModuleError::InvalidPayload,
48            AccessTokenError::InvalidOrgKey => CryptoModuleError::InvalidOrgKey,
49            AccessTokenError::Crypto(c) => CryptoModuleError::Crypto(c),
50        })
51}
52
53#[cfg(test)]
54mod tests {
55    use bitwarden_access_token::{AccessTokenKind, make_access_token_secrets};
56    use bitwarden_crypto::{
57        KeyDecryptable, PrimitiveEncryptable, SymmetricCryptoKey, SymmetricKeyAlgorithm,
58    };
59    use uuid::uuid;
60
61    use super::*;
62    use crate::test_support::test_org_id as org_id;
63
64    fn org_slot() -> SymmetricKeySlotId {
65        SymmetricKeySlotId::Organization(org_id())
66    }
67
68    fn make_store_with_org_key() -> (KeyStore<KeySlotIds>, SymmetricCryptoKey) {
69        let store: KeyStore<KeySlotIds> = KeyStore::default();
70        let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
71
72        #[allow(deprecated)]
73        store
74            .context_mut()
75            .set_symmetric_key(org_slot(), org_key.clone())
76            .expect("set_symmetric_key");
77
78        (store, org_key)
79    }
80
81    /// Mints a token the way `bitwarden-pam` does, keyed to the organization key in
82    /// `issuer_store`, plus the `encrypted_payload` that goes with it.
83    fn mint_token_and_payload(issuer_store: &KeyStore<KeySlotIds>) -> (AccessToken, String) {
84        let secrets = {
85            let mut ctx = issuer_store.context_mut();
86            make_access_token_secrets(&mut ctx, org_slot(), AccessTokenKind::AccessConnector)
87                .expect("mint secrets")
88        };
89        let encrypted_payload = secrets.encrypted_payload.to_string();
90        let token_str = secrets.into_token(uuid!("22222222-2222-2222-2222-222222222222"), "secret");
91        let token = AccessToken::parse(&token_str, AccessTokenKind::AccessConnector)
92            .expect("the token parses");
93        (token, encrypted_payload)
94    }
95
96    #[test]
97    fn unwrap_org_key_round_trip() {
98        let (issuer_store, org_key) = make_store_with_org_key();
99        let (token, encrypted_payload) = mint_token_and_payload(&issuer_store);
100
101        let store: KeyStore<KeySlotIds> = KeyStore::default();
102        assert!(!store.context().has_symmetric_key(org_slot()));
103
104        unwrap_org_key(&store, org_id(), &token, &encrypted_payload).expect("unwrap_org_key");
105
106        // Probe that the org key is installed.
107        let probe = "probe value";
108        let encrypted_probe = {
109            let mut ctx = store.context();
110            probe.encrypt(&mut ctx, org_slot()).expect("encrypt probe")
111        };
112
113        let decrypted: String = encrypted_probe
114            .decrypt_with_key(&org_key)
115            .expect("decrypt probe");
116        assert_eq!(decrypted, probe);
117    }
118
119    #[test]
120    fn unwrap_org_key_bad_payload_returns_error() {
121        let (issuer_store, _org_key) = make_store_with_org_key();
122        let (token, _payload) = mint_token_and_payload(&issuer_store);
123        let store: KeyStore<KeySlotIds> = KeyStore::default();
124
125        let result = unwrap_org_key(&store, org_id(), &token, "not-an-enc-string");
126        assert!(
127            matches!(result, Err(CryptoModuleError::InvalidPayload)),
128            "expected InvalidPayload, got {result:?}",
129        );
130    }
131}