bitwarden_core/auth/
access_token.rs1use 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
28pub struct AccessToken {
30 pub access_token_id: Uuid,
32 pub client_secret: String,
34 pub encryption_key: SymmetricCryptoKey,
36}
37
38impl 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 let t = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ";
114 assert!(AccessToken::from_str(t).is_ok());
115
116 let t = "1.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==";
118 assert!(AccessToken::from_str(t).is_err());
119
120 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 let t = "1.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe9qrzIQQ==";
129 assert!(AccessToken::from_str(t).is_err());
130 }
131}