Skip to main content

bitwarden_crypto/keys/
device_key.rs

1use bitwarden_encoding::B64;
2use serde::{Deserialize, Serialize};
3
4use super::{PrivateKey, PublicKeyEncryptionAlgorithm};
5use crate::{
6    CryptoError, EncString, KeyDecryptable, KeyEncryptable, Pkcs8PrivateKeyBytes,
7    SymmetricCryptoKey, UnsignedSharedKey, error::Result,
8};
9
10/// Device Key
11///
12/// Encrypts the DevicePrivateKey
13/// Allows the device to decrypt the UserKey, via the DevicePrivateKey.
14#[derive(Debug)]
15pub struct DeviceKey(SymmetricCryptoKey);
16
17#[allow(missing_docs)]
18#[derive(Debug, Clone)]
19#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
20#[bitwarden_ffi::wasm_record]
21#[derive(Serialize, Deserialize)]
22pub struct TrustDeviceResponse {
23    /// Base64 encoded device key
24    pub device_key: B64,
25    /// UserKey encrypted with DevicePublicKey
26    pub protected_user_key: UnsignedSharedKey,
27    /// DevicePrivateKey encrypted with [DeviceKey]
28    pub protected_device_private_key: EncString,
29    /// DevicePublicKey encrypted with [UserKey](super::UserKey)
30    pub protected_device_public_key: EncString,
31}
32
33impl DeviceKey {
34    /// Generate a new device key
35    ///
36    /// Note: Input has to be a SymmetricCryptoKey instead of UserKey because that's what we get
37    /// from EncSettings.
38    pub fn trust_device(user_key: &SymmetricCryptoKey) -> Result<TrustDeviceResponse> {
39        let device_key = DeviceKey(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
40
41        let device_private_key = PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
42
43        #[expect(deprecated)]
44        let protected_user_key = UnsignedSharedKey::encapsulate_key_unsigned(
45            user_key,
46            &device_private_key.to_public_key(),
47        )?;
48
49        let protected_device_public_key = device_private_key
50            .to_public_key()
51            .to_der()?
52            .encrypt_with_key(user_key)?;
53
54        let protected_device_private_key = device_private_key
55            .to_der()?
56            .encrypt_with_key(&device_key.0)?;
57
58        Ok(TrustDeviceResponse {
59            device_key: device_key.to_base64(),
60            protected_user_key,
61            protected_device_private_key,
62            protected_device_public_key,
63        })
64    }
65
66    /// Decrypt the user key using the device key
67    pub fn decrypt_user_key(
68        &self,
69        protected_device_private_key: EncString,
70        protected_user_key: UnsignedSharedKey,
71    ) -> Result<SymmetricCryptoKey> {
72        let device_private_key: Vec<u8> = protected_device_private_key.decrypt_with_key(&self.0)?;
73        let device_private_key = Pkcs8PrivateKeyBytes::from(device_private_key);
74        let device_private_key = PrivateKey::from_der(&device_private_key)?;
75
76        #[expect(deprecated)]
77        let user_key: SymmetricCryptoKey =
78            protected_user_key.decapsulate_key_unsigned(&device_private_key)?;
79        Ok(user_key)
80    }
81
82    fn to_base64(&self) -> B64 {
83        self.0.to_base64()
84    }
85}
86
87impl TryFrom<String> for DeviceKey {
88    type Error = CryptoError;
89
90    fn try_from(value: String) -> Result<Self, Self::Error> {
91        SymmetricCryptoKey::try_from(value).map(DeviceKey)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::{BitwardenLegacyKeyBytes, derive_symmetric_key};
99
100    #[test]
101    fn test_trust_device() {
102        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
103
104        let result = DeviceKey::trust_device(&key).unwrap();
105
106        let device_key = DeviceKey::try_from(result.device_key.to_string()).unwrap();
107        let decrypted = device_key
108            .decrypt_user_key(
109                result.protected_device_private_key,
110                result.protected_user_key,
111            )
112            .unwrap();
113
114        assert_eq!(key, decrypted);
115        assert_eq!(key, decrypted);
116    }
117
118    #[test]
119    fn test_decrypt_user_key() {
120        // Example keys from desktop app
121        let user_key: &[u8] = &[
122            109, 128, 172, 147, 206, 123, 134, 95, 16, 36, 155, 113, 201, 18, 186, 230, 216, 212,
123            173, 188, 74, 11, 134, 131, 137, 242, 105, 178, 105, 126, 52, 139, 248, 91, 215, 21,
124            128, 91, 226, 222, 165, 67, 251, 34, 83, 81, 77, 147, 225, 76, 13, 41, 102, 45, 183,
125            218, 106, 89, 254, 208, 251, 101, 130, 10,
126        ];
127        let user_key =
128            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(user_key)).unwrap();
129
130        let key_data: &[u8] = &[
131            114, 235, 60, 115, 172, 156, 203, 145, 195, 130, 215, 250, 88, 146, 215, 230, 12, 109,
132            245, 222, 54, 217, 255, 211, 221, 105, 230, 236, 65, 52, 209, 133, 76, 208, 113, 254,
133            194, 216, 156, 19, 230, 62, 32, 93, 87, 7, 144, 156, 117, 142, 250, 32, 182, 118, 187,
134            8, 247, 7, 203, 201, 65, 147, 206, 247,
135        ];
136        let device_key = DeviceKey(
137            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_data)).unwrap(),
138        );
139
140        let protected_user_key: UnsignedSharedKey = "4.f+VbbacRhO2q4MOUSdt1AIjQ2FuLAvg4aDxJMXAh3VxvbmUADj8Ct/R7XEpPUqApmbRS566jS0eRVy8Sk08ogoCdj1IFN9VsIky2i2X1WHK1fUnr3UBmXE3tl2NPBbx56U+h73S2jNTSyet2W18Jg2q7/w8KIhR3J41QrG9aGoOTN93to3hb5W4z6rdrSI0e7GkizbwcIA0NH7Z1JyAhrjPm9+tjRjg060YbEbGaWTAOkZWfgbLjr8bY455DteO2xxG139cOx7EBo66N+YhjsLi0ozkeUyPQkoWBdKMcQllS7jCfB4fDyJA05ALTbk74syKkvqFxqwmQbg+aVn+dcw==".parse().unwrap();
141
142        let protected_device_private_key: EncString = "2.GyQfUYWW6Byy4UV5icFLxg==|EMiU7OTF79N6tfv3+YUs5zJhBAgqv6sa5YCoPl6yAETh7Tfk+JmbeizxXFPj5Q1X/tcVpDZl/3fGcxtnIxg1YtvDFn7j8uPnoApOWhCKmwcvJSIkt+qvX3lELNBwZXozSiy7PbQ0JbCMe2d4MkimR5k8+lE9FB3208yYK7nOJhlrsUCnOekCYEU9/4NCMA8tz8SpITx/MN4JJ1TQ/KjPJYLt+3JNUxK47QlgREWQvyVzCRt7ZGtcgIJ/U1qycAWMpEg9NkuV8j5QRA1S7VBsA6qliJwys5+dmTuIOmOMwdKFZDc4ZvWoRkPp2TSJBu7L8sSAgU6mmDWac8iQ+9Ka/drdfwYLrH8GAZvURk79tSpRrT7+PAFe2QdUtliUIyiqkh8iJVjZube4hRnEsRuX9V9b+UdtAr6zAj7mugO/VAu5T9J38V79V2ohG3NtXysDeKLXpAlkhjllWXeq/wret2fD4WiwqEDj0G2A/PY3F3OziIgp0UKc00AfqrPq8OVK3A+aowwVqdYadgxyoVCKWJ8unJeAXG7MrMQ9tHpzF6COoaEy7Wwoc17qko33zazwLZbfAjB4oc8Ea26jRKnJZP56sVZAjOSQQMziAsA08MRaa/DQhgRea1+Ygba0gMft8Dww8anN2gQBveTZRBWyqXYgN3U0Ity5gNauT8RnFk9faqVFt2Qxnp0JgJ+PsqEt5Hn4avBRZQQ7o8VvPnxYLDKFe3I2m6HFYFWRhOGeDYxexIuaiF2iIAYFVUmnDuWpgnUiL4XJ3KHDsjkPzcV3z4D2Knr/El2VVXve8jhDjETfovmmN28+i2e29PXvKIymTskMFpFCQPc7wBY/Id7pmgb3SujKYNpkAS2sByDoRir0my49DDGfta0dENssJhFd3x+87fZbEj3cMiikg2pBwpTLgmfIUa5cVZU2s8JZ9wu7gaioYzvX+elHa3EHLcnEUoJTtSf9kjb+Nbq4ktMgYAO2wIC96t1LvmqK4Qn2cOdw5QNlRqALhqe5V31kyIcwRMK0AyIoOPhnSqtpYdFiR3LDTvZA8dU0vSsuchCwHNMeRUtKvdzN/tk+oeznyY/mpakUESN501lEKd/QFLtJZsDZTtNlcA8fU3kDtws4ZIMR0O5+PFmgQFSU8OMobf9ClUzy/wHTvYGyDuSwbOoPeS955QKkUKXCNMj33yrPr+ioHQ1BNwLX3VmMF4bNRBY/vr+CG0/EZi0Gwl0kyHGl0yWEtpQuu+/PaROJeOraWy5D1UoZZhY4n0zJZBt1eg3FZ2rhKv4gdUc50nZpeNWE8pIqZ6RQ7qPJuqfF1Z+G73iOSnLYCHDiiFmhD5ivf9IGkTAcWcBsQ/2wcSj9bFJr4DrKfsbQ4CkSWICWVn/W+InKkO6BTsBbYmvte5SvbaN+UOtiUSkHLBCCr8273VNgcB/hgtbUires3noxYZJxoczr+i7vdlEgQnWEKrpo0CifsFxGwYS3Yy2K79iwvDMaLPDf73zLSbuoUl6602F2Mzcjnals67f+gSpaDvWt7Kg9c/ZfGjq8oNxVaXJnX3gSDsO+fhwVAtnDApL+tL8cFfxGerW4KGi9/74woH+C3MMIViBtNnrpEuvxUW97Dg5nd40oGDeyi/q+8HdcxkneyFY=|JYdol19Yi+n1r7M+06EwK5JCi2s/CWqKui2Cy6hEb3k=".parse().unwrap();
143
144        let decrypted = device_key
145            .decrypt_user_key(protected_device_private_key, protected_user_key)
146            .unwrap();
147
148        assert_eq!(decrypted, user_key);
149    }
150}