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 fn with_request_id(session_key: &AesKey, request_id: u32) -> MacSigner {
29 MacSigner {
30 session_id: session_key.id.clone(),
31 salt: calculate_session_hmac_salt(&session_key.key),
32 request_id: AtomicU32::new(request_id),
33 }
34 }
35
36 pub(super) fn sign(&self, url: &str, method: &str) -> Result<String, OnePasswordError> {
40 let id = self.request_id.fetch_add(1, Ordering::Relaxed);
41
42 let message = self.calculate_auth_message(url, method, id)?;
43 Ok(calculate_auth_signature(&self.salt, &message, id))
44 }
45
46 fn calculate_auth_message(
48 &self,
49 url: &str,
50 method: &str,
51 request_id: u32,
52 ) -> Result<String, OnePasswordError> {
53 let parsed = Url::parse(url)
54 .map_err(|_| OnePasswordError::Internal(format!("invalid url '{url}'")))?;
55 let host = parsed.host_str().unwrap_or("");
56 let path = parsed.path().trim_start_matches('/');
57 let query = parsed.query().unwrap_or("");
58
59 Ok(format!(
60 "{}|{}|{}/{}?{}|v1|{}",
61 self.session_id,
62 method.to_uppercase(),
63 host,
64 path,
65 query,
66 request_id
67 ))
68 }
69}
70
71fn calculate_auth_signature(salt: &[u8], auth_message: &str, request_id: u32) -> String {
73 let hash = hmac_sha256(salt, auth_message.as_bytes());
74 let hash12 = BASE64URL_NOPAD.encode(&hash[..12]);
75 format!("v1|{request_id}|{hash12}")
76}
77
78fn calculate_session_hmac_salt(session_key: &[u8]) -> [u8; 32] {
80 hmac_sha256(session_key, SESSION_HMAC_SECRET.as_bytes())
81}
82
83fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
84 let mut mac =
85 <Hmac<Sha256> as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
86 mac.update(message);
87 mac.finalize().into_bytes().into()
88}
89
90#[cfg(test)]
91mod tests {
92 use data_encoding::HEXLOWER;
93
94 use super::{super::opdata::decode64_loose, *};
95
96 const SESSION_KEY: &str = "WyICHHlP5lPigZUGZYoivbJMqgHjSti86UKwdjCryYM";
97
98 fn signer() -> MacSigner {
99 let key = AesKey::new(
100 "PBXONDZUWVCJFAV25C7XR7IYDQ",
101 decode64_loose(SESSION_KEY).expect("valid key"),
102 );
103 MacSigner::with_request_id(&key, 842346063)
104 }
105
106 #[test]
107 fn sign_returns_headers_with_signature() {
108 let signature = signer()
109 .sign("https://my.1password.com/api/v1/auth/verify", "POST")
110 .expect("signs");
111 assert_eq!(signature, "v1|842346063|xv-fEAYowunpH4V-");
112 }
113
114 #[test]
115 fn sign_returns_signature_for_url_with_query() {
116 let signature = signer()
117 .sign(
118 "https://my.1password.com/api/v1/account?attrs=billing,counts,groups,invite,me,settings,tier,user-flags,users,vaults",
119 "GET",
120 )
121 .expect("signs");
122 assert_eq!(signature, "v1|842346063|UyjKq0HAmjB5j7kF");
123 }
124
125 #[test]
126 fn sign_increments_the_counter() {
127 let signer = signer();
128 let first = signer
129 .sign("https://my.1password.com/api/v1/auth/verify", "POST")
130 .expect("signs");
131 let second = signer
132 .sign("https://my.1password.com/api/v1/auth/verify", "POST")
133 .expect("signs");
134
135 assert_ne!(first, second);
136
137 let seed = |s: &str| -> u32 {
138 s.split('|')
139 .nth(1)
140 .expect("seed field")
141 .parse()
142 .expect("u32")
143 };
144 assert_eq!(seed(&first) + 1, seed(&second));
145 }
146
147 #[test]
148 fn calculates_session_hmac_salt() {
149 let key = decode64_loose(SESSION_KEY).expect("valid key");
150 assert_eq!(
151 HEXLOWER.encode(&calculate_session_hmac_salt(&key)),
152 "cce080cc9b3eaeaa9b6e621e1b4c4d2048babe16e40b0576fc2520c26473b9ac"
153 );
154 }
155
156 #[test]
157 fn new_starts_from_a_random_request_id() {
158 let key = AesKey::new(
159 "PBXONDZUWVCJFAV25C7XR7IYDQ",
160 decode64_loose(SESSION_KEY).expect("valid key"),
161 );
162 let ids: Vec<u32> = (0..4)
163 .map(|_| MacSigner::new(&key).request_id.into_inner())
164 .collect();
165 assert!(ids.iter().any(|id| *id != ids[0]));
166 }
167}