authenticator/backup/
bitwarden.rs

1use 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        // Translators: This is for restoring a backup from Bitwarden.
120        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}