|  | 
|  | 1 | +use serde::{Deserialize, Serialize}; | 
|  | 2 | + | 
|  | 3 | +use crate::{ | 
|  | 4 | +    AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CryptoError, EncString, KeyDecryptable, | 
|  | 5 | +    KeyEncryptable, KeyIds, KeyStoreContext, Pkcs8PrivateKeyBytes, SpkiPublicKeyBytes, | 
|  | 6 | +    SymmetricCryptoKey, UnsignedSharedKey, | 
|  | 7 | +}; | 
|  | 8 | + | 
|  | 9 | +/// A set of keys where a given `DownstreamKey` is protected by an encrypted public/private | 
|  | 10 | +/// key-pair. The `DownstreamKey` is used to encrypt/decrypt data, while the public/private key-pair | 
|  | 11 | +/// is used to rotate the `DownstreamKey`. | 
|  | 12 | +/// | 
|  | 13 | +/// The `PrivateKey` is protected by an `UpstreamKey`, such as a `DeviceKey`, or `PrfKey`, | 
|  | 14 | +/// and the `PublicKey` is protected by the `DownstreamKey`. This setup allows: | 
|  | 15 | +/// | 
|  | 16 | +///   - Access to `DownstreamKey` by knowing the `UpstreamKey` | 
|  | 17 | +///   - Rotation to a `NewDownstreamKey` by knowing the current `DownstreamKey`, without needing | 
|  | 18 | +///     access to the `UpstreamKey` | 
|  | 19 | +#[derive(Serialize, Deserialize, Debug)] | 
|  | 20 | +#[serde(rename_all = "camelCase", deny_unknown_fields)] | 
|  | 21 | +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] | 
|  | 22 | +#[cfg_attr( | 
|  | 23 | +    feature = "wasm", | 
|  | 24 | +    derive(tsify::Tsify), | 
|  | 25 | +    tsify(into_wasm_abi, from_wasm_abi) | 
|  | 26 | +)] | 
|  | 27 | +pub struct RotateableKeySet { | 
|  | 28 | +    /// `DownstreamKey` protected by encapsulation key | 
|  | 29 | +    encapsulated_downstream_key: UnsignedSharedKey, | 
|  | 30 | +    /// Encapsulation key protected by `DownstreamKey` | 
|  | 31 | +    encrypted_encapsulation_key: EncString, | 
|  | 32 | +    /// Decapsulation key protected by `UpstreamKey` | 
|  | 33 | +    encrypted_decapsulation_key: EncString, | 
|  | 34 | +} | 
|  | 35 | + | 
|  | 36 | +impl RotateableKeySet { | 
|  | 37 | +    /// Create a set of keys to allow access to the downstream key via the provided | 
|  | 38 | +    /// upstream key while allowing the downstream key to be rotated. | 
|  | 39 | +    pub fn new<Ids: KeyIds>( | 
|  | 40 | +        ctx: &KeyStoreContext<Ids>, | 
|  | 41 | +        upstream_key: &SymmetricCryptoKey, | 
|  | 42 | +        downstream_key_id: Ids::Symmetric, | 
|  | 43 | +    ) -> Result<Self, CryptoError> { | 
|  | 44 | +        let key_pair = AsymmetricCryptoKey::make(crate::PublicKeyEncryptionAlgorithm::RsaOaepSha1); | 
|  | 45 | + | 
|  | 46 | +        // This uses this deprecated method and other methods directly on the other keys | 
|  | 47 | +        // rather than the key store context because we don't want the keys to | 
|  | 48 | +        // wind up being stored in the borrowed context. | 
|  | 49 | +        #[allow(deprecated)] | 
|  | 50 | +        let downstream_key = ctx.dangerous_get_symmetric_key(downstream_key_id)?; | 
|  | 51 | +        // encapsulate downstream key | 
|  | 52 | +        let encapsulated_downstream_key = | 
|  | 53 | +            UnsignedSharedKey::encapsulate_key_unsigned(downstream_key, &key_pair.to_public_key())?; | 
|  | 54 | + | 
|  | 55 | +        // wrap decapsulation key with upstream key | 
|  | 56 | +        let encrypted_decapsulation_key = key_pair.to_der()?.encrypt_with_key(upstream_key)?; | 
|  | 57 | + | 
|  | 58 | +        // wrap encapsulation key with downstream key | 
|  | 59 | +        // Note: Usually, a public key is - by definition - public, so this should not be necessary. | 
|  | 60 | +        // The specific use-case for this function is to enable rotateable key sets, where | 
|  | 61 | +        // the "public key" is not public, with the intent of preventing the server from being able | 
|  | 62 | +        // to overwrite the downstream key unlocked by the rotateable keyset. | 
|  | 63 | +        let encrypted_encapsulation_key = key_pair | 
|  | 64 | +            .to_public_key() | 
|  | 65 | +            .to_der()? | 
|  | 66 | +            .encrypt_with_key(downstream_key)?; | 
|  | 67 | + | 
|  | 68 | +        Ok(RotateableKeySet { | 
|  | 69 | +            encapsulated_downstream_key, | 
|  | 70 | +            encrypted_encapsulation_key, | 
|  | 71 | +            encrypted_decapsulation_key, | 
|  | 72 | +        }) | 
|  | 73 | +    } | 
|  | 74 | + | 
|  | 75 | +    // TODO: Eventually, the webauthn-login-strategy service should be migrated | 
|  | 76 | +    // to use this method, and we can remove the #[allow(dead_code)] attribute. | 
|  | 77 | +    #[allow(dead_code)] | 
|  | 78 | +    fn unlock<Ids: KeyIds>( | 
|  | 79 | +        &self, | 
|  | 80 | +        ctx: &mut KeyStoreContext<Ids>, | 
|  | 81 | +        upstream_key: &SymmetricCryptoKey, | 
|  | 82 | +        downstream_key_id: Ids::Symmetric, | 
|  | 83 | +    ) -> Result<(), CryptoError> { | 
|  | 84 | +        let priv_key_bytes: Vec<u8> = self | 
|  | 85 | +            .encrypted_decapsulation_key | 
|  | 86 | +            .decrypt_with_key(upstream_key)?; | 
|  | 87 | +        let decapsulation_key = | 
|  | 88 | +            AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?; | 
|  | 89 | +        let downstream_key = self | 
|  | 90 | +            .encapsulated_downstream_key | 
|  | 91 | +            .decapsulate_key_unsigned(&decapsulation_key)?; | 
|  | 92 | +        #[allow(deprecated)] | 
|  | 93 | +        ctx.set_symmetric_key(downstream_key_id, downstream_key)?; | 
|  | 94 | +        Ok(()) | 
|  | 95 | +    } | 
|  | 96 | +} | 
|  | 97 | + | 
|  | 98 | +#[allow(dead_code)] | 
|  | 99 | +fn rotate_key_set<Ids: KeyIds>( | 
|  | 100 | +    ctx: &KeyStoreContext<Ids>, | 
|  | 101 | +    key_set: RotateableKeySet, | 
|  | 102 | +    old_downstream_key_id: Ids::Symmetric, | 
|  | 103 | +    new_downstream_key_id: Ids::Symmetric, | 
|  | 104 | +) -> Result<RotateableKeySet, CryptoError> { | 
|  | 105 | +    let pub_key_bytes = ctx.decrypt_data_with_symmetric_key( | 
|  | 106 | +        old_downstream_key_id, | 
|  | 107 | +        &key_set.encrypted_encapsulation_key, | 
|  | 108 | +    )?; | 
|  | 109 | +    let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes); | 
|  | 110 | +    let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&pub_key)?; | 
|  | 111 | +    // TODO: There is no method to store only the public key in the store, so we | 
|  | 112 | +    // have pull out the downstream key to encapsulate it manually. | 
|  | 113 | +    #[allow(deprecated)] | 
|  | 114 | +    let new_downstream_key = ctx.dangerous_get_symmetric_key(new_downstream_key_id)?; | 
|  | 115 | +    let new_encapsulated_key = | 
|  | 116 | +        UnsignedSharedKey::encapsulate_key_unsigned(new_downstream_key, &encapsulation_key)?; | 
|  | 117 | +    let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_downstream_key)?; | 
|  | 118 | +    Ok(RotateableKeySet { | 
|  | 119 | +        encapsulated_downstream_key: new_encapsulated_key, | 
|  | 120 | +        encrypted_encapsulation_key: new_encrypted_encapsulation_key, | 
|  | 121 | +        encrypted_decapsulation_key: key_set.encrypted_decapsulation_key, | 
|  | 122 | +    }) | 
|  | 123 | +} | 
|  | 124 | + | 
|  | 125 | +#[cfg(test)] | 
|  | 126 | +mod tests { | 
|  | 127 | +    use super::*; | 
|  | 128 | +    use crate::{ | 
|  | 129 | +        KeyStore, | 
|  | 130 | +        traits::tests::{TestIds, TestSymmKey}, | 
|  | 131 | +    }; | 
|  | 132 | + | 
|  | 133 | +    #[test] | 
|  | 134 | +    fn test_rotateable_key_set_can_unlock() { | 
|  | 135 | +        // generate initial keys | 
|  | 136 | +        let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); | 
|  | 137 | +        // set up store | 
|  | 138 | +        let store: KeyStore<TestIds> = KeyStore::default(); | 
|  | 139 | +        let mut ctx = store.context_mut(); | 
|  | 140 | +        let original_downstream_key_id = TestSymmKey::A(0); | 
|  | 141 | +        ctx.generate_symmetric_key(original_downstream_key_id) | 
|  | 142 | +            .unwrap(); | 
|  | 143 | + | 
|  | 144 | +        // create key set | 
|  | 145 | +        let key_set = | 
|  | 146 | +            RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap(); | 
|  | 147 | + | 
|  | 148 | +        // unlock key set | 
|  | 149 | +        let unwrapped_downstream_key_id = TestSymmKey::A(1); | 
|  | 150 | +        key_set | 
|  | 151 | +            .unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id) | 
|  | 152 | +            .unwrap(); | 
|  | 153 | + | 
|  | 154 | +        #[allow(deprecated)] | 
|  | 155 | +        let original_downstream_key = ctx | 
|  | 156 | +            .dangerous_get_symmetric_key(original_downstream_key_id) | 
|  | 157 | +            .unwrap(); | 
|  | 158 | +        #[allow(deprecated)] | 
|  | 159 | +        let unwrapped_downstream_key = ctx | 
|  | 160 | +            .dangerous_get_symmetric_key(unwrapped_downstream_key_id) | 
|  | 161 | +            .unwrap(); | 
|  | 162 | +        assert_eq!(original_downstream_key, unwrapped_downstream_key); | 
|  | 163 | +    } | 
|  | 164 | + | 
|  | 165 | +    #[test] | 
|  | 166 | +    fn test_rotateable_key_set_rotation() { | 
|  | 167 | +        // generate initial keys | 
|  | 168 | +        let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); | 
|  | 169 | +        // set up store | 
|  | 170 | +        let store: KeyStore<TestIds> = KeyStore::default(); | 
|  | 171 | +        let mut ctx = store.context_mut(); | 
|  | 172 | +        let original_downstream_key_id = TestSymmKey::A(1); | 
|  | 173 | +        ctx.generate_symmetric_key(original_downstream_key_id) | 
|  | 174 | +            .unwrap(); | 
|  | 175 | + | 
|  | 176 | +        // create key set | 
|  | 177 | +        let key_set = | 
|  | 178 | +            RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap(); | 
|  | 179 | + | 
|  | 180 | +        // rotate | 
|  | 181 | +        let new_downstream_key_id = TestSymmKey::A(2_1); | 
|  | 182 | +        ctx.generate_symmetric_key(new_downstream_key_id).unwrap(); | 
|  | 183 | +        let new_key_set = rotate_key_set( | 
|  | 184 | +            &ctx, | 
|  | 185 | +            key_set, | 
|  | 186 | +            original_downstream_key_id, | 
|  | 187 | +            new_downstream_key_id, | 
|  | 188 | +        ) | 
|  | 189 | +        .unwrap(); | 
|  | 190 | + | 
|  | 191 | +        // After rotation, the new key set should be unlocked by the same | 
|  | 192 | +        // upstream key and return the new downstream key. | 
|  | 193 | +        let unwrapped_downstream_key_id = TestSymmKey::A(2_2); | 
|  | 194 | +        new_key_set | 
|  | 195 | +            .unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id) | 
|  | 196 | +            .unwrap(); | 
|  | 197 | +        #[allow(deprecated)] | 
|  | 198 | +        let new_downstream_key = ctx | 
|  | 199 | +            .dangerous_get_symmetric_key(new_downstream_key_id) | 
|  | 200 | +            .unwrap(); | 
|  | 201 | +        #[allow(deprecated)] | 
|  | 202 | +        let unwrapped_downstream_key = ctx | 
|  | 203 | +            .dangerous_get_symmetric_key(unwrapped_downstream_key_id) | 
|  | 204 | +            .unwrap(); | 
|  | 205 | +        assert_eq!(new_downstream_key, unwrapped_downstream_key); | 
|  | 206 | +    } | 
|  | 207 | +} | 
0 commit comments