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#[cfg_attr(
21    feature = "wasm",
22    derive(tsify::Tsify),
23    tsify(into_wasm_abi, from_wasm_abi)
24)]
25#[derive(Serialize, Deserialize)]
26pub struct TrustDeviceResponse {
27    /// Base64 encoded device key
28    pub device_key: B64,
29    /// UserKey encrypted with DevicePublicKey
30    pub protected_user_key: UnsignedSharedKey,
31    /// DevicePrivateKey encrypted with [DeviceKey]
32    pub protected_device_private_key: EncString,
33    /// DevicePublicKey encrypted with [UserKey](super::UserKey)
34    pub protected_device_public_key: EncString,
35}
36
37impl DeviceKey {
38    /// Generate a new device key
39    ///
40    /// Note: Input has to be a SymmetricCryptoKey instead of UserKey because that's what we get
41    /// from EncSettings.
42    pub fn trust_device(user_key: &SymmetricCryptoKey) -> Result<TrustDeviceResponse> {
43        let device_key = DeviceKey(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
44
45        let device_private_key = PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
46
47        #[expect(deprecated)]
48        let protected_user_key = UnsignedSharedKey::encapsulate_key_unsigned(
49            user_key,
50            &device_private_key.to_public_key(),
51        )?;
52
53        let protected_device_public_key = device_private_key
54            .to_public_key()
55            .to_der()?
56            .encrypt_with_key(user_key)?;
57
58        let protected_device_private_key = device_private_key
59            .to_der()?
60            .encrypt_with_key(&device_key.0)?;
61
62        Ok(TrustDeviceResponse {
63            device_key: device_key.to_base64(),
64            protected_user_key,
65            protected_device_private_key,
66            protected_device_public_key,
67        })
68    }
69
70    /// Decrypt the user key using the device key
71    pub fn decrypt_user_key(
72        &self,
73        protected_device_private_key: EncString,
74        protected_user_key: UnsignedSharedKey,
75    ) -> Result<SymmetricCryptoKey> {
76        let device_private_key: Vec<u8> = protected_device_private_key.decrypt_with_key(&self.0)?;
77        let device_private_key = Pkcs8PrivateKeyBytes::from(device_private_key);
78        let device_private_key = PrivateKey::from_der(&device_private_key)?;
79
80        #[expect(deprecated)]
81        let user_key: SymmetricCryptoKey =
82            protected_user_key.decapsulate_key_unsigned(&device_private_key)?;
83        Ok(user_key)
84    }
85
86    fn to_base64(&self) -> B64 {
87        self.0.to_base64()
88    }
89}
90
91impl TryFrom<String> for DeviceKey {
92    type Error = CryptoError;
93
94    fn try_from(value: String) -> Result<Self, Self::Error> {
95        SymmetricCryptoKey::try_from(value).map(DeviceKey)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::{BitwardenLegacyKeyBytes, derive_symmetric_key};
103
104    #[test]
105    fn test_trust_device() {
106        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
107
108        let result = DeviceKey::trust_device(&key).unwrap();
109
110        let device_key = DeviceKey::try_from(result.device_key.to_string()).unwrap();
111        let decrypted = device_key
112            .decrypt_user_key(
113                result.protected_device_private_key,
114                result.protected_user_key,
115            )
116            .unwrap();
117
118        assert_eq!(key, decrypted);
119        assert_eq!(key, decrypted);
120    }
121
122    #[test]
123    fn test_decrypt_user_key() {
124        // Example keys from desktop app
125        let user_key: &[u8] = &[
126            109, 128, 172, 147, 206, 123, 134, 95, 16, 36, 155, 113, 201, 18, 186, 230, 216, 212,
127            173, 188, 74, 11, 134, 131, 137, 242, 105, 178, 105, 126, 52, 139, 248, 91, 215, 21,
128            128, 91, 226, 222, 165, 67, 251, 34, 83, 81, 77, 147, 225, 76, 13, 41, 102, 45, 183,
129            218, 106, 89, 254, 208, 251, 101, 130, 10,
130        ];
131        let user_key =
132            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(user_key)).unwrap();
133
134        let key_data: &[u8] = &[
135            114, 235, 60, 115, 172, 156, 203, 145, 195, 130, 215, 250, 88, 146, 215, 230, 12, 109,
136            245, 222, 54, 217, 255, 211, 221, 105, 230, 236, 65, 52, 209, 133, 76, 208, 113, 254,
137            194, 216, 156, 19, 230, 62, 32, 93, 87, 7, 144, 156, 117, 142, 250, 32, 182, 118, 187,
138            8, 247, 7, 203, 201, 65, 147, 206, 247,
139        ];
140        let device_key = DeviceKey(
141            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_data)).unwrap(),
142        );
143
144        let protected_user_key: UnsignedSharedKey = "4.f+VbbacRhO2q4MOUSdt1AIjQ2FuLAvg4aDxJMXAh3VxvbmUADj8Ct/R7XEpPUqApmbRS566jS0eRVy8Sk08ogoCdj1IFN9VsIky2i2X1WHK1fUnr3UBmXE3tl2NPBbx56U+h73S2jNTSyet2W18Jg2q7/w8KIhR3J41QrG9aGoOTN93to3hb5W4z6rdrSI0e7GkizbwcIA0NH7Z1JyAhrjPm9+tjRjg060YbEbGaWTAOkZWfgbLjr8bY455DteO2xxG139cOx7EBo66N+YhjsLi0ozkeUyPQkoWBdKMcQllS7jCfB4fDyJA05ALTbk74syKkvqFxqwmQbg+aVn+dcw==".parse().unwrap();
145
146        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();
147
148        let decrypted = device_key
149            .decrypt_user_key(protected_device_private_key, protected_user_key)
150            .unwrap();
151
152        assert_eq!(decrypted, user_key);
153    }
154}