bitwarden_importers/importers/onepassword/access/
account_key.rs1use zeroize::Zeroize;
4
5use super::{error::OnePasswordError, kdf};
6
7const ALPHABET: &str = "23456789ABCDEFGHJKLMNPQRSTVWXYZ";
10
11pub(super) struct AccountKey {
14 pub format: String,
15 pub uuid: String,
16 pub key: String,
17}
18
19impl Drop for AccountKey {
20 fn drop(&mut self) {
21 self.key.zeroize();
22 }
23}
24
25impl AccountKey {
26 pub(super) fn parse(input: &str) -> Result<AccountKey, OnePasswordError> {
32 let s: String = input
33 .to_uppercase()
34 .chars()
35 .filter(|c| ALPHABET.contains(*c))
36 .collect();
37
38 let Some(format) = s.get(..2) else {
39 return Err(OnePasswordError::Internal(format!(
40 "invalid account key: too short, got {}",
41 s.len()
42 )));
43 };
44
45 match format {
48 "A2" if s.len() == 33 => {}
49 "A3" if s.len() == 34 => {}
50 "A2" => {
51 return Err(OnePasswordError::Internal(format!(
52 "invalid account key: 'A2' needs 33 characters without dashes, got {}",
53 s.len()
54 )));
55 }
56 "A3" => {
57 return Err(OnePasswordError::Internal(format!(
58 "invalid account key: 'A3' needs 34 characters without dashes, got {}",
59 s.len()
60 )));
61 }
62 _ => {
63 return Err(OnePasswordError::Internal(format!(
64 "invalid account key: unknown format '{format}'"
65 )));
66 }
67 }
68
69 let invalid = || OnePasswordError::Internal("invalid account key".into());
70 Ok(AccountKey {
71 format: format.to_string(),
72 uuid: s.get(2..8).ok_or_else(invalid)?.to_string(),
73 key: s.get(8..).ok_or_else(invalid)?.to_string(),
74 })
75 }
76
77 pub(super) fn hash(&self) -> [u8; 32] {
79 kdf::hkdf_sha256(&self.format, self.key.as_bytes(), self.uuid.as_bytes())
80 }
81
82 pub(super) fn combine_with(&self, bytes: &[u8]) -> Result<[u8; 32], OnePasswordError> {
84 let mut h = self.hash();
85 if h.len() != bytes.len() {
86 return Err(OnePasswordError::Internal(
87 "size doesn't match hash function".into(),
88 ));
89 }
90
91 for (byte, other) in h.iter_mut().zip(bytes) {
92 *byte ^= other;
93 }
94
95 Ok(h)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use data_encoding::BASE64URL_NOPAD;
102
103 use super::*;
104
105 fn key() -> AccountKey {
106 AccountKey {
107 format: "A3".into(),
108 uuid: "RTN9SA".into(),
109 key: "DY9445Y5FF96X6E7B5GPFA95R9".into(),
110 }
111 }
112
113 #[test]
114 fn parse_returns_parsed_format_a3_key() {
115 let key = AccountKey::parse("A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9").expect("valid key");
116 assert_eq!(key.format, "A3");
117 assert_eq!(key.uuid, "RTN9SA");
118 assert_eq!(key.key, "DY9445Y5FF96X6E7B5GPFA95R9");
119 }
120
121 #[test]
124 fn parse_ignores_everything_outside_the_alphabet() {
125 let cases = [
126 " A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9\n",
127 "a3-rtn9sa-dy9445y5ff96x6e7b5gpfa95r9",
128 "A3 RTN9SA DY9445Y 5FF96X6 E7B5GPF A95R9",
129 "A3_RTN9SA_DY9445Y5FF96X6E7B5GPFA95R9",
130 ];
131 for case in cases {
132 let key = AccountKey::parse(case).unwrap_or_else(|e| panic!("{case:?}: {e}"));
133 assert_eq!(key.format, "A3");
134 assert_eq!(key.uuid, "RTN9SA");
135 assert_eq!(key.key, "DY9445Y5FF96X6E7B5GPFA95R9");
136 }
137 }
138
139 #[test]
141 fn parse_returns_parsed_format_a2_key() {
142 let key = AccountKey::parse("A2-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R").expect("valid key");
143 assert_eq!(key.format, "A2");
144 assert_eq!(key.uuid, "RTN9SA");
145 assert_eq!(key.key, "DY9445Y5FF96X6E7B5GPFA95R");
146 }
147
148 #[test]
149 fn parse_throws_on_invalid_key_format() {
150 let cases = [
151 "",
152 "A",
153 "A2",
154 "A3",
155 "A2-RTN9SA-DY9445Y5FF96X6E7B5GPFA95",
156 "A2-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9",
157 "A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R",
158 "A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R99",
159 "A3-RTN9SA-DY9445Y-FF96X6E7B-GPFA95R9",
160 ];
161 for case in cases {
162 match AccountKey::parse(case) {
163 Ok(_) => panic!("expected {case:?} to be invalid"),
164 Err(err) => assert!(
165 err.to_string().contains("invalid account key"),
166 "unexpected error for {case:?}: {err}"
167 ),
168 }
169 }
170 }
171
172 #[test]
173 fn hash_returns_hashed_key() {
174 assert_eq!(
175 BASE64URL_NOPAD.encode(&key().hash()),
176 "ZlI2kRote1dv7uflTenyIp5jBE0u-7Fl4aIiE0D9L-g"
177 );
178 }
179
180 #[test]
181 fn combine_with_returns_hashed_key() {
182 let combined = key()
183 .combine_with(b"All your base are belong to us!!")
184 .expect("32 byte input");
185 assert_eq!(
186 BASE64URL_NOPAD.encode(&combined),
187 "Jz5asWNCDiVPjIaWKMmTUPtDZihClN8CwdZNMzWODsk"
188 );
189 }
190
191 #[test]
192 fn combine_with_throws_on_incorrect_length() {
193 let cases: [&[u8]; 5] = [
194 b"",
195 b"A",
196 b"All your base are belong to us",
197 b"All your base are belong to us!",
198 b"All your base are belong to us!!!",
199 ];
200 for case in cases {
201 let err = key().combine_with(case).expect_err("wrong length");
202 assert!(
203 err.to_string().contains("hash function"),
204 "unexpected error: {err}"
205 );
206 }
207 }
208}