bitwarden_core/auth/
access_token.rs

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