authenticator/backup/
bitwarden.rs1use anyhow::Result;
2use gettextrs::gettext;
3use serde::Deserialize;
4use zeroize::{Zeroize, ZeroizeOnDrop};
5
6use super::{Restorable, RestorableItem};
7use crate::models::{Algorithm, Method, OTP, OTPUri};
8
9#[derive(Deserialize)]
10pub struct Bitwarden {
11 items: Vec<BitwardenItem>,
12}
13
14#[derive(Deserialize)]
15pub struct BitwardenItem {
16 #[serde(rename = "name")]
17 issuer: Option<String>,
18 login: Option<BitwardenDetails>,
19 #[serde(skip)]
20 algorithm: Algorithm,
21 #[serde(skip)]
22 method: Method,
23 #[serde(skip)]
24 digits: Option<u32>,
25 #[serde(skip)]
26 period: Option<u32>,
27 #[serde(skip)]
28 counter: Option<u32>,
29}
30
31#[derive(Deserialize, ZeroizeOnDrop, Zeroize)]
32struct BitwardenDetails {
33 #[zeroize(skip)]
34 username: Option<String>,
35 totp: Option<String>,
36}
37
38impl RestorableItem for BitwardenItem {
39 fn account(&self) -> String {
40 if let Some(account) = self
41 .login
42 .as_ref()
43 .and_then(|login| login.username.as_ref())
44 {
45 account.clone()
46 } else {
47 gettext("Unknown account")
48 }
49 }
50
51 fn issuer(&self) -> String {
52 if let Some(issuer) = self.issuer.clone() {
53 issuer
54 } else {
55 gettext("Unknown issuer")
56 }
57 }
58
59 fn secret(&self) -> String {
60 self.login
61 .as_ref()
62 .unwrap()
63 .totp
64 .as_ref()
65 .unwrap()
66 .to_owned()
67 }
68
69 fn period(&self) -> Option<u32> {
70 self.period
71 }
72
73 fn method(&self) -> Method {
74 self.method
75 }
76
77 fn algorithm(&self) -> Algorithm {
78 self.algorithm
79 }
80
81 fn digits(&self) -> Option<u32> {
82 self.digits
83 }
84
85 fn counter(&self) -> Option<u32> {
86 self.counter
87 }
88}
89
90impl BitwardenItem {
91 fn overwrite_with(&mut self, uri: OTPUri) {
92 self.issuer = Some(uri.issuer());
93
94 if let Some(ref mut login) = self.login {
95 login.totp = Some(uri.secret());
96 login.username = Some(uri.account());
97 } else {
98 self.login = Some(BitwardenDetails {
99 username: Some(uri.account()),
100 totp: Some(uri.secret()),
101 });
102 }
103
104 self.algorithm = uri.algorithm();
105 self.method = uri.method();
106 self.digits = uri.digits();
107 self.period = uri.period();
108 self.counter = uri.counter();
109 }
110}
111
112impl Restorable for Bitwarden {
113 const ENCRYPTABLE: bool = false;
114 const SCANNABLE: bool = false;
115 const IDENTIFIER: &'static str = "bitwarden";
116 type Item = BitwardenItem;
117
118 fn title() -> String {
119 gettext("_Bitwarden")
121 }
122
123 fn subtitle() -> String {
124 gettext("From a plain-text JSON file")
125 }
126
127 fn restore_from_data(from: &[u8], _key: Option<&str>) -> Result<Vec<Self::Item>> {
128 let bitwarden_root: Bitwarden = serde_json::de::from_slice(from)?;
129
130 let mut items = Vec::new();
131
132 for mut item in bitwarden_root.items {
133 if let Some(ref mut login) = item.login
134 && let Some(ref totp) = login.totp
135 {
136 if totp.starts_with("steam://") {
137 login.totp = Some(totp.trim_start_matches("steam://").to_owned());
138 item.algorithm = Algorithm::SHA1;
139 item.method = Method::Steam;
140 item.period = Some(OTP::STEAM_DEFAULT_PERIOD);
141 item.digits = Some(OTP::STEAM_DEFAULT_DIGITS);
142 items.push(item);
143 } else if let Ok(uri) = totp.parse::<OTPUri>() {
144 item.overwrite_with(uri);
145 items.push(item);
146 }
147 }
148 }
149
150 Ok(items)
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 #[test]
158 fn parse() {
159 let data = std::fs::read_to_string("./src/backup/tests/bitwarden.json").unwrap();
160 let items = Bitwarden::restore_from_data(data.as_bytes(), None).unwrap();
161
162 assert_eq!(items[0].account(), "Mason");
163 assert_eq!(items[0].issuer(), "Deno");
164 assert_eq!(items[0].secret(), "4SJHB4GSD43FZBAI7C2HLRJGPQ");
165 assert_eq!(items[0].period(), Some(30));
166 assert_eq!(items[0].method(), Method::TOTP);
167 assert_eq!(items[0].algorithm(), Algorithm::SHA1);
168 assert_eq!(items[0].digits(), Some(6));
169 assert_eq!(items[0].counter(), None);
170
171 assert_eq!(items[1].account(), "James");
172 assert_eq!(items[1].issuer(), "SPDX");
173 assert_eq!(items[1].secret(), "5OM4WOOGPLQEF6UGN3CPEOOLWU");
174 assert_eq!(items[1].period(), Some(20));
175 assert_eq!(items[1].method(), Method::TOTP);
176 assert_eq!(items[1].algorithm(), Algorithm::SHA256);
177 assert_eq!(items[1].digits(), Some(7));
178 assert_eq!(items[1].counter(), None);
179
180 assert_eq!(items[2].account(), "Elijah");
181 assert_eq!(items[2].issuer(), "Airbnb");
182 assert_eq!(items[2].secret(), "7ELGJSGXNCCTV3O6LKJWYFV2RA");
183 assert_eq!(items[2].period(), Some(50));
184 assert_eq!(items[2].method(), Method::TOTP);
185 assert_eq!(items[2].algorithm(), Algorithm::SHA512);
186 assert_eq!(items[2].digits(), Some(8));
187 assert_eq!(items[2].counter(), None);
188
189 assert_eq!(items[3].account(), "Unknown account");
190 assert_eq!(items[3].issuer(), "Test Steam");
191 assert_eq!(items[3].secret(), "JRZCL47CMXVOQMNPZR2F7J4RGI");
192 assert_eq!(items[3].period(), Some(30));
193 assert_eq!(items[3].method(), Method::Steam);
194 assert_eq!(items[3].algorithm(), Algorithm::SHA1);
195 assert_eq!(items[3].digits(), Some(5));
196 assert_eq!(items[3].counter(), None);
197 }
198}