bitwarden_core/auth/
access_token.rs

1use std::{fmt::Debug, str::FromStr};
2
3use bitwarden_crypto::{SymmetricCryptoKey, derive_shareable_key};
4use bitwarden_encoding::{B64, NotB64EncodedError};
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] NotB64EncodedError),
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!(
105            token.encryption_key.to_base64().to_string(),
106            "H9/oIRLtL9nGCQOVDjSMoEbJsjWXSOCb3qeyDt6ckzS3FhyboEDWyTP/CQfbIszNmAVg2ExFganG1FVFGXO/Jg=="
107        );
108    }
109
110    #[test]
111    fn malformed_tokens() {
112        use std::str::FromStr;
113
114        // Encryption key without base64 padding, we generate it with padding but ignore it when
115        // decoding
116        let t = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ";
117        assert!(AccessToken::from_str(t).is_ok());
118
119        // Invalid version
120        let t = "1.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==";
121        assert!(AccessToken::from_str(t).is_err());
122
123        // Invalid splits
124        let t = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw.X8vbvA0bduihIDe/qrzIQQ==";
125        assert!(AccessToken::from_str(t).is_err());
126
127        let t = "ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==";
128        assert!(AccessToken::from_str(t).is_err());
129
130        // Invalid base64
131        let t = "1.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe9qrzIQQ==";
132        assert!(AccessToken::from_str(t).is_err());
133    }
134}