Skip to main content

bitwarden_importers/importers/onepassword/convert/
login.rs

1//! The Login category, and the pieces every category that lands on a login shares.
2
3use bitwarden_exporters::{Login, LoginUri};
4use itertools::Itertools;
5
6use super::{
7    claimed::{Claimed, designation, section_fields},
8    value::non_blank,
9};
10use crate::importers::onepassword::access::wire::{VaultItemDetails, VaultItemOverview};
11
12/// 1Password stores a one-time password in a section field whose stable id carries this prefix.
13const TOTP_PREFIX: &str = "TOTP_";
14
15pub(super) fn login(overview: &VaultItemOverview, details: &VaultItemDetails) -> (Login, Claimed) {
16    let totp = first_totp(details);
17
18    let mut login = Login {
19        username: designation(details, "username"),
20        password: designation(details, "password"),
21        login_uris: login_uris(website_addresses(overview)),
22        totp: totp.as_ref().map(|(_, secret)| secret.clone()),
23        fido2_credentials: None,
24    };
25    login.sanitize_uris();
26
27    let claimed = Claimed {
28        designations: &["username", "password"],
29        fields: totp.map(|(position, _)| position).into_iter().collect(),
30    };
31
32    (login, claimed)
33}
34
35/// Reads the item's first one-time password, with the position of the field it came from. A
36/// `TOTP_` field can arrive without a secret, so the first one carrying a value wins; any further
37/// one stays a custom field, since a cipher has room for only one.
38pub(super) fn first_totp(details: &VaultItemDetails) -> Option<(usize, String)> {
39    section_fields(details)
40        .enumerate()
41        .find_map(|(position, field)| {
42            if !field
43                .id
44                .as_deref()
45                .is_some_and(|id| id.starts_with(TOTP_PREFIX))
46            {
47                return None;
48            }
49
50            let secret = field.value.as_ref()?.as_str().and_then(non_blank)?;
51            Some((position, secret.to_string()))
52        })
53}
54
55/// The item's website addresses. 1Password keeps the primary one in `url` and repeats it in
56/// `URLs`.
57pub(super) fn website_addresses(overview: &VaultItemOverview) -> impl Iterator<Item = &str> {
58    overview.url.as_deref().into_iter().chain(
59        overview
60            .urls
61            .iter()
62            .flatten()
63            .filter_map(|url| url.url.as_deref()),
64    )
65}
66
67/// Turns addresses into login URIs, collapsing an address given twice into a single URI.
68pub(super) fn login_uris<'a>(addresses: impl Iterator<Item = &'a str>) -> Vec<LoginUri> {
69    addresses
70        .filter_map(non_blank)
71        .unique()
72        .map(|uri| LoginUri {
73            uri: Some(uri.to_string()),
74            r#match: None,
75        })
76        .collect()
77}