bitwarden_core/auth/
access_token.rs

1use std::{fmt::Debug, str::FromStr};
2
3use base64::Engine;
4use bitwarden_crypto::{derive_shareable_key, SymmetricCryptoKey};
5use thiserror::Error;
6use uuid::Uuid;
7use zeroize::Zeroizing;
8
9use crate::util::STANDARD_INDIFFERENT;
10
11#[allow(missing_docs)]
12#[derive(Debug, Error)]
13pub enum AccessTokenInvalidError {
14    #[error("Doesn't contain a decryption key")]
15    NoKey,
16    #[error("Has the wrong number of parts")]
17    WrongParts,
18    #[error("Is the wrong version")]
19    WrongVersion,
20    #[error("Has an invalid identifier")]
21    InvalidUuid,
22
23    #[error("Error decoding base64: {0}")]
24    InvalidBase64(#[from] base64::DecodeError),
25
26    #[error("Invalid base64 length: expected {expected}, got {got}")]
27    InvalidBase64Length { expected: usize, got: usize },
28}
29
30/// Access Token
31pub struct AccessToken {
32    /// The ID of the access token
33    pub access_token_id: Uuid,
34    /// The client secret
35    pub client_secret: String,
36    /// The encryption key used to decrypt a payload to retrieve the organization key.
37    pub encryption_key: SymmetricCryptoKey,
38}
39
40// We don't want to log the more sensitive fields from an AccessToken
41impl Debug for AccessToken {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("AccessToken")
44            .field("access_token_id", &self.access_token_id)
45            .finish()
46    }
47}
48
49impl FromStr for AccessToken {
50    type Err = AccessTokenInvalidError;
51
52    fn from_str(key: &str) -> std::result::Result<Self, Self::Err> {
53        let (first_part, encryption_key) =
54            key.split_once(':').ok_or(AccessTokenInvalidError::NoKey)?;
55
56        let [version, access_token_id, client_secret]: [&str; 3] = first_part
57            .split('.')
58            .collect::<Vec<_>>()
59            .try_into()
60            .map_err(|_| AccessTokenInvalidError::WrongParts)?;
61
62        if version != "0" {
63            return Err(AccessTokenInvalidError::WrongVersion);
64        }
65
66        let Ok(access_token_id) = access_token_id.parse() else {
67            return Err(AccessTokenInvalidError::InvalidUuid);
68        };
69
70        let encryption_key = STANDARD_INDIFFERENT.decode(encryption_key)?;
71        let encryption_key = Zeroizing::new(encryption_key.try_into().map_err(|e: Vec<_>| {
72            AccessTokenInvalidError::InvalidBase64Length {
73                expected: 16,
74                got: e.len(),
75            }
76        })?);
77        let encryption_key =
78            derive_shareable_key(encryption_key, "accesstoken", Some("sm-access-token"));
79
80        Ok(AccessToken {
81            access_token_id,
82            client_secret: client_secret.to_owned(),
83            encryption_key: SymmetricCryptoKey::Aes256CbcHmacKey(encryption_key),
84        })
85    }
86}
87
88#[cfg(test)]
89mod tests {
90
91    use super::AccessToken;
92
93    #[test]
94    fn can_decode_access_token() {
95        use std::str::FromStr;
96
97        let access_token = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==";
98        let token = AccessToken::from_str(access_token).unwrap();
99
100        assert_eq!(
101            &token.access_token_id.to_string(),
102            "ec2c1d46-6a4b-4751-a310-af9601317f2d"
103        );
104        assert_eq!(token.client_secret, "C2IgxjjLF7qSshsbwe8JGcbM075YXw");
105        assert_eq!(token.encryption_key.to_base64(), "H9/oIRLtL9nGCQOVDjSMoEbJsjWXSOCb3qeyDt6ckzS3FhyboEDWyTP/CQfbIszNmAVg2ExFganG1FVFGXO/Jg==");
106    }
107
108    #[test]
109    fn malformed_tokens() {
110        use std::str::FromStr;
111
112        // Encryption key without base64 padding, we generate it with padding but ignore it when
113        // decoding
114        let t = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ";
115        assert!(AccessToken::from_str(t).is_ok());
116
117        // Invalid version
118        let t = "1.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==";
119        assert!(AccessToken::from_str(t).is_err());
120
121        // Invalid splits
122        let t = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw.X8vbvA0bduihIDe/qrzIQQ==";
123        assert!(AccessToken::from_str(t).is_err());
124
125        let t = "ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==";
126        assert!(AccessToken::from_str(t).is_err());
127
128        // Invalid base64
129        let t = "1.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe9qrzIQQ==";
130        assert!(AccessToken::from_str(t).is_err());
131    }
132}