bitwarden_core/platform/
generate_fingerprint.rs

1//! Fingerprint generation.
2//!
3//! This module contains the logic for generating fingerprints.
4
5use base64::{engine::general_purpose::STANDARD, Engine};
6use bitwarden_crypto::fingerprint;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::{key_management::AsymmetricKeyId, MissingPrivateKeyError, VaultLockedError};
11
12/// Request to generate a fingerprint.
13#[derive(Serialize, Deserialize, Debug)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
16pub struct FingerprintRequest {
17    /// The input material, used in the fingerprint generation process.
18    pub fingerprint_material: String,
19    /// The user's public key encoded with base64.
20    pub public_key: String,
21}
22
23/// Response containing a generated fingerprint.
24///
25/// TODO: We should attempt to remove this and just return a string.
26#[derive(Serialize, Deserialize, Debug)]
27#[serde(rename_all = "camelCase", deny_unknown_fields)]
28pub struct FingerprintResponse {
29    /// The generated fingerprint.
30    pub fingerprint: String,
31}
32
33/// Errors that can occur when computing a fingerprint.
34#[allow(missing_docs)]
35#[derive(Debug, Error)]
36pub enum FingerprintError {
37    #[error(transparent)]
38    CryptoError(#[from] bitwarden_crypto::CryptoError),
39    #[error(transparent)]
40    InvalidBase64(#[from] base64::DecodeError),
41}
42
43pub(crate) fn generate_fingerprint(input: &FingerprintRequest) -> Result<String, FingerprintError> {
44    let key = STANDARD.decode(&input.public_key)?;
45    Ok(fingerprint(&input.fingerprint_material, &key.into())?)
46}
47
48/// Errors that can occur when computing a fingerprint.
49#[allow(missing_docs)]
50#[derive(Debug, Error)]
51pub enum UserFingerprintError {
52    #[error(transparent)]
53    Crypto(#[from] bitwarden_crypto::CryptoError),
54    #[error(transparent)]
55    VaultLocked(#[from] VaultLockedError),
56    #[error(transparent)]
57    MissingPrivateKey(#[from] MissingPrivateKeyError),
58}
59
60pub(crate) fn generate_user_fingerprint(
61    client: &crate::Client,
62    fingerprint_material: String,
63) -> Result<String, UserFingerprintError> {
64    let key_store = client.internal.get_key_store();
65    let ctx = key_store.context();
66    // FIXME: [PM-18110] This should be removed once the key store can handle public keys and
67    // fingerprints
68    #[allow(deprecated)]
69    let private_key = ctx.dangerous_get_asymmetric_key(AsymmetricKeyId::UserPrivateKey)?;
70
71    let public_key = private_key.to_public_key().to_der()?;
72    let fingerprint = fingerprint(&fingerprint_material, &public_key)?;
73
74    Ok(fingerprint)
75}
76
77#[cfg(test)]
78mod tests {
79    use std::num::NonZeroU32;
80
81    use bitwarden_crypto::{Kdf, MasterKey};
82
83    use super::*;
84    use crate::{client::internal::UserKeyState, Client};
85
86    #[test]
87    fn test_generate_user_fingerprint() {
88        let user_key = "2.oZg5RYpU2HjUAKI1DUQCkg==|PyRzI9kZpt66P2OedH8CHOeU0/lgKLkhIJiKDijdyFqIemBSIBoslhfQh/P1TK9xgZp0smgD6+5+yNbZfOpBaCVrsT3WWAO78xOWizduRe4=|xfDLDZSJ+yZAdh388flVg7SMDBJuMs0+CHTjutKs4uQ=";
89        let private_key = "2.tY6WsWKUbBwNU8wROuipiQ==|DNFL1d19xVojUKTTy2gxT+9J1VXbMQLcbMnx1HSeA6U3yZhsLR6DPaGibb3Bp8doIHtrsxzL/JeLb4gLDZ8RnDhFfE4iLRaPakX14kbBXrKH9/uW/zc7TqIVciWhI1PaeFlu8wnVuGt3e5Ysx6Y7Uw7RS8pRT5aE3sX3aDPGZTAdTutLn1VUfkShS5OK5HJl9CdiwV2wOcrf4w/WqtaNUUqGdsJ8C4ELlpBzHxqs+lEm+8pGPYmuGQIjVc0eOR9Tza9GTk3ih1XGc1znOCoKUZbtA29RfbwfmJy/yGi/3RLWZFQGCCij4cLC5OpldiX4JWL5Dhox44p/5IVF3rfxTVz3GCyDOoHevRG/06sUBq6nhbdCQf3lJvxwcQJhoQg4rsapM3rgol+u+TbXRiwWPbfswuLkRlvGFKtKUWMa4S57gj0CFYgSBPdTyhZTB44D7JQ2bd901Ur1dYWcDe4Kn3ZawpxL0cX2ZPlE3v8FXFJf2s8DJytL8yu73GasDzVmaGHxueWWVz7EHjh+pmB4oaAHARcY8d3LActAyl/+bcFRPYQJ68ae6DJhYYJGHIBWMImf2BifGgUX8vUFfUAYjne3D82lRyZQHs3xbl+ZxEPgWiPYRWUtxGXLLP4f9mbl+LeJdehtHNjC8kOduBL0CsP4gmugzNNUXI+Izc/9svno6kFr6SU0LA3MGrOU8ao7UCQbf/Pj/RKnG1gRmBDQqf7IMm6jOyTwdde9NpfQb32iH11PkuAKBvEtUuq9BeAKWjoZku+ycsN2jZH0hzd/QrU2c+E4+yHwX3wSxxorNOXt5EZkJbEDBlpRyE1zWoyy0wIYfcChYLvFN8QFHchlw5wmHxL+OOgdgndAtV/2DCx+NB6caY31qLictME+1GPPlQ7QvicMLgmpSWq83rs4ex/My6p3hCRSrJJiLvjEDZLYWKHHLd5tsPRAjX8ADNWB1VeIeiJrj1wpOCc1PbWpbljbbTsBmVPo6iKm/UDGAHBdQ//0j3FQg8f5w/j+McsoaMpDNHNTiLvjWERR+RBmsEA0lEL00wZz/DHlzOAYHLYYqFMT7GBCQD+Wk/l1TL+X2agUy7Irlk7QbZ4ivfdNIpSW8Ct9MGE6o4wV+nIpXURojgBBTcP85RTBLXXGrIprnK1G/VE8ONag3+nkqIyChjYyk5QMsxqOqSHsbiOxhCdXypbCbY4g9yKJtBJ/ADjxmELj0X7pqsTFqC0eRT7rk9qTBcYBBu6rwlAfq8AKjDB7WjNjzLaMi6lBoe4petBn1xcLkXD5hHra0TULxcYrq8MIb+Vk4CBZZdwwyVm/28SwSjHBIBpRysPAonDDsp3KlahwXEFvRDQR/oFww172GI7cx8SoPn93Qh0JfpTAAowsO3meR8bzUSyd7v3rmtaBPsWHE9zUXye/6nloMU5joEcD6uJaxd0kdaWWIoKLH++zHW1R776wJrS6u+TIWZgHqiIJoCd9fV25BnQcbZRKd6mnfNQkchJ6c6ozXKrFaa8DLdERdfh84+isw5mzW2zMJwHEwtKt6LUTyieC2exzPAwPxJT1+IMjuzuwiLnvGKOq+kwE/LWBSB0ZfGuCP/3jMM8OCfe7Hbpt1TfXcUxUzj6sSjkjQB6qBt+TINRdOFA=|fppguME86utsAOKrBYn6XU95q7daVbZ+3dD9OVkQlAw=";
90        let fingerprint_material = "a09726a0-9590-49d1-a5f5-afe300b6a515";
91
92        let client = Client::new(None);
93
94        let master_key = MasterKey::derive(
95            "asdfasdfasdf",
96            "[email protected]",
97            &Kdf::PBKDF2 {
98                iterations: NonZeroU32::new(600_000).unwrap(),
99            },
100        )
101        .unwrap();
102
103        client
104            .internal
105            .initialize_user_crypto_master_key(
106                master_key,
107                user_key.parse().unwrap(),
108                UserKeyState {
109                    private_key: private_key.parse().unwrap(),
110                    signing_key: None,
111                    security_state: None,
112                },
113            )
114            .unwrap();
115
116        let fingerprint =
117            generate_user_fingerprint(&client, fingerprint_material.to_string()).unwrap();
118
119        assert_eq!(fingerprint, "turban-deftly-anime-chatroom-unselfish");
120    }
121}