1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
pub use crate::internal::user_api::{
    EncryptedPrivateKey, UserCreateResult, UserDevice, UserDeviceListResult, UserId, UserResult,
    UserUpdatePrivateKeyResult,
};
use crate::{
    internal::{
        add_optional_timeout,
        user_api::{self, DeviceId, DeviceName},
        DeviceAddResult, PublicKey, OUR_REQUEST,
    },
    IronOxide, Result, SdkOperation,
};
use recrypt::api::Recrypt;
use std::{collections::HashMap, convert::TryInto};

/// Optional parameters for creating a new device instance.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DeviceCreateOpts {
    device_name: Option<DeviceName>,
}
impl DeviceCreateOpts {
    /// Create a new device with an optional readable name for the device.
    pub fn new(device_name: Option<DeviceName>) -> DeviceCreateOpts {
        DeviceCreateOpts { device_name }
    }
}
impl Default for DeviceCreateOpts {
    fn default() -> Self {
        DeviceCreateOpts::new(None)
    }
}

/// Options that can be specified at when calling [`user_create`](trait.UserOps.html#tymethod.user_create).
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct UserCreateOpts {
    // see docs on `new`
    needs_rotation: bool,
}

impl UserCreateOpts {
    /// Constructor. Also see [`default`](#method.default).
    ///
    /// # Arguments
    /// - `needs_rotation` - Set to true if the private key for this user should be rotated when the user "takes control" of their keys.
    ///
    /// The main use case for this is a workflow that requires that users and groups to be generated
    /// prior to the user logging in for the first time. In this situation, a user's cryptographic identity
    /// can be generated by a third party, like a server process, and then the user can take control of their
    /// keys by rotating the private key using [`rotate_private_key`](https://github.com/IronCoreLabs/ironoxide/issues/44).
    pub fn new(needs_rotation: bool) -> UserCreateOpts {
        UserCreateOpts { needs_rotation }
    }
}

impl Default for UserCreateOpts {
    fn default() -> Self {
        UserCreateOpts::new(false)
    }
}

#[async_trait]
pub trait UserOps {
    /// Sync a new user within the IronCore system.
    ///
    /// # Arguments
    /// - `jwt` - Valid IronCore or Auth0 JWT
    /// - `password` - Password used to encrypt and escrow the user's private master key
    /// - `user_create_opts` - see [`UserCreateOpts`](struct.UserCreateOpts.html)
    /// - `timeout` - timeout for this operation or None for no timeout. Timed out operations return IronOxideErr::OperationTimedOut
    /// # Returns
    /// Newly generated `UserCreateResult` or Err. For most use cases, this public key can
    /// be discarded as IronCore escrows your user's keys. The escrowed keys are unlocked
    /// by the provided password.
    async fn user_create(
        jwt: &str,
        password: &str,
        user_create_opts: &UserCreateOpts,
        timeout: Option<std::time::Duration>,
    ) -> Result<UserCreateResult>;

    /// Get all the devices for the current user
    ///
    /// # Returns
    /// All devices for the current user, sorted by the device id.
    async fn user_list_devices(&self) -> Result<UserDeviceListResult>;

    /// Generates a new device for the user specified in the signed JWT.
    ///
    /// This will result in a new transform key (from the user's master private key to the new device's public key)
    /// being generated and stored with the IronCore Service.
    ///
    /// # Arguments
    /// - `jwt`                   - Valid IronCore JWT
    /// - `password`              - Password used to encrypt and escrow the user's private key
    /// - `device_create_options` - Optional device create arguments, like device name
    /// - `timeout` - timeout for this operation or None for no timeout. Timed out operations return IronOxideErr::OperationTimedOut
    ///
    /// # Returns
    /// Details about the newly created device.
    async fn generate_new_device(
        jwt: &str,
        password: &str,
        device_create_options: &DeviceCreateOpts,
        timeout: Option<std::time::Duration>,
    ) -> Result<DeviceAddResult>;

    /// Delete a user device.
    ///
    /// If deleting the currently signed in device (None for `device_id`), the sdk will need to be
    /// reinitialized with `IronOxide.initialize()` before further use.
    ///
    /// # Arguments
    /// - `device_id` - ID of the device to delete. Get from `user_list_devices`. If None, deletes the currently SDK contexts device which
    ///                 once deleted will cause this SDK instance to no longer function.
    ///
    /// # Returns
    /// Id of deleted device or IronOxideErr
    async fn user_delete_device(&self, device_id: Option<&DeviceId>) -> Result<DeviceId>;

    /// Verify a user given a JWT for their user record.
    ///
    /// # Arguments
    /// - `jwt` - Valid IronCore JWT
    /// - `timeout` - timeout for this operation or None for no timeout. Timed out operations return IronOxideErr::OperationTimedOut
    ///
    /// # Returns
    /// Option of whether the user's account record exists in the IronCore system or not. Err if the request couldn't be made.
    async fn user_verify(
        jwt: &str,
        timeout: Option<std::time::Duration>,
    ) -> Result<Option<UserResult>>;

    /// Get a list of user public keys given their IDs. Allows discovery of which user IDs have keys in the
    /// IronCore system to determine of they can be added to groups or have documents shared with them.
    ///
    /// # Arguments
    /// - users - List of user IDs to check
    ///
    /// # Returns
    /// Map from user ID to users public key. Only users who have public keys will be returned in the map.
    async fn user_get_public_key(&self, users: &[UserId]) -> Result<HashMap<UserId, PublicKey>>;

    /// Rotate the current user's private key, but leave the public key the same.
    /// There's no black magic here! This is accomplished via multi-party computation with the
    /// IronCore webservice.
    ///
    /// # Arguments
    /// `password` - Password to unlock the current user's user master key
    ///
    /// # Returns
    /// The (encrypted) updated private key and associated metadata
    async fn user_rotate_private_key(&self, password: &str) -> Result<UserUpdatePrivateKeyResult>;
}

#[async_trait]
impl UserOps for IronOxide {
    async fn user_create(
        jwt: &str,
        password: &str,
        user_create_opts: &UserCreateOpts,
        timeout: Option<std::time::Duration>,
    ) -> Result<UserCreateResult> {
        let recrypt = Recrypt::new();
        add_optional_timeout(
            user_api::user_create(
                &recrypt,
                jwt.try_into()?,
                password.try_into()?,
                user_create_opts.needs_rotation,
                *OUR_REQUEST,
            ),
            timeout,
            SdkOperation::UserCreate,
        )
        .await?
    }

    async fn user_list_devices(&self) -> Result<UserDeviceListResult> {
        add_optional_timeout(
            user_api::device_list(self.device.auth()),
            self.config.sdk_operation_timeout,
            SdkOperation::UserListDevices,
        )
        .await?
    }

    async fn generate_new_device(
        jwt: &str,
        password: &str,
        device_create_options: &DeviceCreateOpts,
        timeout: Option<std::time::Duration>,
    ) -> Result<DeviceAddResult> {
        let recrypt = Recrypt::new();

        let device_create_options = device_create_options.clone();

        add_optional_timeout(
            user_api::generate_device_key(
                &recrypt,
                &jwt.try_into()?,
                password.try_into()?,
                device_create_options.device_name,
                &std::time::SystemTime::now().into(),
                &OUR_REQUEST,
            ),
            timeout,
            SdkOperation::GenerateNewDevice,
        )
        .await?
    }

    async fn user_delete_device(&self, device_id: Option<&DeviceId>) -> Result<DeviceId> {
        add_optional_timeout(
            user_api::device_delete(self.device.auth(), device_id),
            self.config.sdk_operation_timeout,
            SdkOperation::UserDeleteDevice,
        )
        .await?
    }

    async fn user_verify(
        jwt: &str,
        timeout: Option<std::time::Duration>,
    ) -> Result<Option<UserResult>> {
        add_optional_timeout(
            user_api::user_verify(jwt.try_into()?, *OUR_REQUEST),
            timeout,
            SdkOperation::UserVerify,
        )
        .await?
    }

    async fn user_get_public_key(&self, users: &[UserId]) -> Result<HashMap<UserId, PublicKey>> {
        add_optional_timeout(
            user_api::user_key_list(self.device.auth(), &users.to_vec()),
            self.config.sdk_operation_timeout,
            SdkOperation::UserGetPublicKey,
        )
        .await?
    }

    async fn user_rotate_private_key(&self, password: &str) -> Result<UserUpdatePrivateKeyResult> {
        add_optional_timeout(
            user_api::user_rotate_private_key(
                &self.recrypt,
                password.try_into()?,
                self.device().auth(),
            ),
            self.config.sdk_operation_timeout,
            SdkOperation::UserRotatePrivateKey,
        )
        .await?
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use galvanic_assert::matchers::*;

    #[test]
    fn user_create_opts_defaults() {
        let opts = UserCreateOpts::default();
        assert_that!(
            &opts,
            has_structure!(UserCreateOpts {
                needs_rotation: eq(false)
            })
        )
    }
    #[test]
    fn user_create_opts_new() {
        let opts = UserCreateOpts::new(true);
        assert_that!(
            &opts,
            has_structure!(UserCreateOpts {
                needs_rotation: eq(true)
            })
        )
    }
}