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