bitwarden_importers/importers/onepassword/access/
mac.rs1use std::sync::atomic::{AtomicU32, Ordering};
4
5use data_encoding::BASE64URL_NOPAD;
6use hmac::{Hmac, KeyInit, Mac};
7use rand::Rng;
8use sha2::Sha256;
9use url::Url;
10
11use super::{error::OnePasswordError, opdata::AesKey};
12
13const SESSION_HMAC_SECRET: &str = "He never wears a Mac, in the pouring rain. Very strange.";
14
15pub(super) struct MacSigner {
17 session_id: String,
18 salt: [u8; 32],
19 request_id: AtomicU32,
20}
21
22impl MacSigner {
23 pub(super) fn new(session_key: &AesKey) -> MacSigner {
25 Self::with_request_id(session_key, bitwarden_random::rng().next_u32())
26 }
27
28 pub(super) fn with_request_id(session_key: &AesKey, request_id: u32) -> MacSigner {
30 MacSigner {
31 session_id: session_key.id.clone(),
32 salt: calculate_session_hmac_salt(&session_key.key),
33 request_id: AtomicU32::new(request_id),
34 }
35 }
36
37 pub(super) fn sign(&self, url: &str, method: &str) -> Result<String, OnePasswordError> {
41 let id = self.request_id.fetch_add(1, Ordering::Relaxed);
42
43 let message = self.calculate_auth_message(url, method, id)?;
44 Ok(calculate_auth_signature(&self.salt, &message, id))
45 }
46
47 fn calculate_auth_message(
49 &self,
50 url: &str,
51 method: &str,
52 request_id: u32,
53 ) -> Result<String, OnePasswordError> {
54 let parsed = Url::parse(url)
55 .map_err(|_| OnePasswordError::Internal(format!("invalid url '{url}'")))?;
56 let host = parsed.host_str().unwrap_or("");
57 let path = parsed.path().trim_start_matches('/');
58 let query = parsed.query().unwrap_or("");
59
60 Ok(format!(
61 "{}|{}|{}/{}?{}|v1|{}",
62 self.session_id,
63 method.to_uppercase(),
64 host,
65 path,
66 query,
67 request_id
68 ))
69 }
70}
71
72fn calculate_auth_signature(salt: &[u8], auth_message: &str, request_id: u32) -> String {
74 let hash = hmac_sha256(salt, auth_message.as_bytes());
75 let hash12 = BASE64URL_NOPAD.encode(&hash[..12]);
76 format!("v1|{request_id}|{hash12}")
77}
78
79fn calculate_session_hmac_salt(session_key: &[u8]) -> [u8; 32] {
81 hmac_sha256(session_key, SESSION_HMAC_SECRET.as_bytes())
82}
83
84fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
85 let mut mac =
86 <Hmac<Sha256> as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
87 mac.update(message);
88 mac.finalize().into_bytes().into()
89}
90
91#[cfg(test)]
92mod tests {
93 use data_encoding::HEXLOWER;
94
95 use super::{super::opdata::decode64_loose, *};
96
97 const SESSION_KEY: &str = "WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM";
98
99 fn signer() -> MacSigner {
100 let key = AesKey::new(
101 "PBXONDZUWVCJFAV25C7XR7IYDQ",
102 decode64_loose(SESSION_KEY).expect("valid key"),
103 );
104 MacSigner::with_request_id(&key, 842346063)
105 }
106
107 #[test]
108 fn sign_returns_headers_with_signature() {
109 let signature = signer()
110 .sign("https://my.1password.com/api/v1/auth/verify", "POST")
111 .expect("signs");
112 assert_eq!(signature, "v1|842346063|xv-fEAYowunpH4V-");
113 }
114
115 #[test]
116 fn sign_returns_signature_for_url_with_query() {
117 let signature = signer()
118 .sign(
119 "https://my.1password.com/api/v1/account?attrs=billing,counts,groups,invite,me,settings,tier,user-flags,users,vaults",
120 "GET",
121 )
122 .expect("signs");
123 assert_eq!(signature, "v1|842346063|UyjKq0HAmjB5j7kF");
124 }
125
126 #[test]
127 fn sign_increments_the_counter() {
128 let signer = signer();
129 let first = signer
130 .sign("https://my.1password.com/api/v1/auth/verify", "POST")
131 .expect("signs");
132 let second = signer
133 .sign("https://my.1password.com/api/v1/auth/verify", "POST")
134 .expect("signs");
135
136 assert_ne!(first, second);
137
138 let seed = |s: &str| -> u32 {
139 s.split('|')
140 .nth(1)
141 .expect("seed field")
142 .parse()
143 .expect("u32")
144 };
145 assert_eq!(seed(&first) + 1, seed(&second));
146 }
147
148 #[test]
149 fn calculates_session_hmac_salt() {
150 let key = decode64_loose(SESSION_KEY).expect("valid key");
151 assert_eq!(
152 HEXLOWER.encode(&calculate_session_hmac_salt(&key)),
153 "cce080cc9b3eaeaa9b6e621e1b4c4d2048babe16e40b0576fc2520c26473b9ac"
154 );
155 }
156
157 #[test]
158 fn new_starts_from_a_random_request_id() {
159 let key = AesKey::new(
160 "PBXONDZUWVCJFAV25C7XR7IYDQ",
161 decode64_loose(SESSION_KEY).expect("valid key"),
162 );
163 let ids: Vec<u32> = (0..4)
164 .map(|_| MacSigner::new(&key).request_id.into_inner())
165 .collect();
166 assert!(ids.iter().any(|id| *id != ids[0]));
167 }
168
169 #[test]
170 fn with_request_id_signs_from_the_given_id() {
171 let key = AesKey::new(
172 "PBXONDZUWVCJFAV25C7XR7IYDQ",
173 decode64_loose(SESSION_KEY).expect("valid key"),
174 );
175 let signer = MacSigner::with_request_id(&key, 1);
176
177 let first = signer
178 .sign("https://my.1password.com/api/v1/auth/verify", "POST")
179 .expect("signs");
180 let second = signer
181 .sign("https://my.1password.com/api/v1/auth/verify", "POST")
182 .expect("signs");
183
184 assert!(first.starts_with("v1|1|"));
185 assert!(second.starts_with("v1|2|"));
186 }
187}