ironoxide/internal/user_api/
requests.rs

1//! User operation requests.
2//! Types and functions defined here should remain private to `user_api`
3
4use crate::{
5    crypto::aes::EncryptedMasterKey,
6    internal::{
7        self,
8        rest::{
9            self,
10            json::{Base64Standard, PublicKey},
11            Authorization, IronCoreRequest,
12        },
13        user_api::{DeviceName, Jwt, UserId},
14        IronOxideErr, RequestAuth, RequestErrorCode,
15    },
16};
17use serde::{Deserialize, Serialize};
18use std::convert::TryFrom;
19use time::OffsetDateTime;
20
21use crate::internal::auth_v2::AuthV2Builder;
22#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
23pub struct EncryptedPrivateKey(#[serde(with = "Base64Standard")] pub Vec<u8>);
24
25impl From<EncryptedMasterKey> for EncryptedPrivateKey {
26    fn from(enc_master_key: EncryptedMasterKey) -> Self {
27        EncryptedPrivateKey(enc_master_key.bytes().to_vec())
28    }
29}
30
31impl From<EncryptedPrivateKey> for internal::user_api::EncryptedPrivateKey {
32    fn from(resp_encrypt_priv_key: EncryptedPrivateKey) -> Self {
33        internal::user_api::EncryptedPrivateKey(resp_encrypt_priv_key.0)
34    }
35}
36
37impl TryFrom<EncryptedPrivateKey> for EncryptedMasterKey {
38    type Error = IronOxideErr;
39
40    fn try_from(value: EncryptedPrivateKey) -> Result<Self, Self::Error> {
41        EncryptedMasterKey::new_from_slice(&value.0)
42    }
43}
44
45pub mod user_verify {
46    use crate::internal::user_api::UserResult;
47    use std::convert::TryInto;
48
49    use super::*;
50
51    #[derive(Debug, PartialEq, Eq, Deserialize)]
52    #[serde(rename_all = "camelCase")]
53    pub struct UserVerifyResponse {
54        pub(crate) id: String,
55        status: usize,
56        pub(crate) segment_id: usize,
57        pub(crate) user_private_key: EncryptedPrivateKey,
58        pub(crate) user_master_public_key: PublicKey,
59        pub(crate) needs_rotation: bool,
60    }
61
62    pub async fn user_verify(
63        jwt: &Jwt,
64        request: &IronCoreRequest,
65    ) -> Result<Option<UserVerifyResponse>, IronOxideErr> {
66        request
67            .get_with_empty_result_jwt_auth(
68                "users/verify?returnKeys=true",
69                RequestErrorCode::UserVerify,
70                &Authorization::JwtAuth(jwt),
71            )
72            .await
73    }
74
75    impl TryFrom<UserVerifyResponse> for UserResult {
76        type Error = IronOxideErr;
77
78        fn try_from(body: UserVerifyResponse) -> Result<Self, Self::Error> {
79            Ok(UserResult {
80                account_id: body.id.try_into()?,
81                segment_id: body.segment_id,
82                user_public_key: body.user_master_public_key.try_into()?,
83                needs_rotation: body.needs_rotation,
84            })
85        }
86    }
87
88    #[cfg(test)]
89    mod tests {
90        use super::*;
91        use crate::internal;
92        use galvanic_assert::{matchers::*, *};
93        use recrypt::prelude::*;
94
95        #[test]
96        fn user_verify_resp_to_result() -> Result<(), IronOxideErr> {
97            let r = recrypt::api::Recrypt::new();
98            let (_, r_pub) = r.generate_key_pair()?;
99
100            // private key doesn't go through any validation as we don't return it in the Result
101            let priv_key: EncryptedPrivateKey = EncryptedPrivateKey(vec![1u8; 60]);
102            let pub_key: PublicKey = r_pub.into();
103
104            let t_account_id: UserId = UserId::unsafe_from_string("valid_user_id".to_string());
105            let t_segment_id: usize = 200;
106            let t_user_public_key: internal::PublicKey = r_pub.into();
107            let t_needs_rotation = true;
108
109            let resp = UserVerifyResponse {
110                id: t_account_id.id().to_string(),
111                status: 100,
112                segment_id: t_segment_id,
113                user_private_key: priv_key,
114                user_master_public_key: pub_key,
115                needs_rotation: t_needs_rotation,
116            };
117            let result: UserResult = resp.try_into().unwrap();
118
119            assert_that!(
120                &result,
121                has_structure!(UserResult {
122                    account_id: eq(t_account_id.clone()),
123                    segment_id: eq(t_segment_id),
124                    user_public_key: eq(t_user_public_key.clone()),
125                    needs_rotation: eq(t_needs_rotation)
126                })
127            );
128            Ok(())
129        }
130    }
131}
132
133pub mod user_get {
134    use super::*;
135    use crate::internal::group_api::GroupId;
136
137    #[derive(Debug, PartialEq, Eq, Deserialize)]
138    #[serde(rename_all = "camelCase")]
139    pub struct CurrentUserResponse {
140        pub(in crate::internal) current_key_id: u64,
141        pub(in crate::internal) id: UserId,
142        pub(in crate::internal) status: usize,
143        pub(in crate::internal) segment_id: usize,
144        pub(in crate::internal) user_master_public_key: PublicKey,
145        pub(in crate::internal) user_private_key: EncryptedPrivateKey,
146        pub(in crate::internal) needs_rotation: bool,
147        pub(in crate::internal) groups_needing_rotation: Vec<GroupId>,
148    }
149
150    pub async fn get_curr_user(auth: &RequestAuth) -> Result<CurrentUserResponse, IronOxideErr> {
151        auth.request
152            .get(
153                "users/current",
154                RequestErrorCode::UserGetCurrent,
155                AuthV2Builder::new(auth, OffsetDateTime::now_utc()),
156            )
157            .await
158    }
159}
160
161/// PUT /users/{userId}/keys/{userKeyId}
162pub mod user_update_private_key {
163    use super::*;
164    use internal::{rest::json::AugmentationFactor, user_api::UserUpdatePrivateKeyResult};
165
166    #[derive(Debug, Serialize)]
167    #[serde(rename_all = "camelCase")]
168    pub struct UserUpdatePrivateKey {
169        user_private_key: EncryptedPrivateKey,
170        augmentation_factor: AugmentationFactor,
171    }
172
173    #[derive(Debug, PartialEq, Eq, Deserialize)]
174    #[serde(rename_all = "camelCase")]
175    pub struct UserUpdatePrivateKeyResponse {
176        current_key_id: u64,
177        user_private_key: EncryptedPrivateKey,
178        needs_rotation: bool,
179    }
180
181    impl From<UserUpdatePrivateKeyResponse> for UserUpdatePrivateKeyResult {
182        fn from(resp: UserUpdatePrivateKeyResponse) -> Self {
183            // don't expose the current_key_id to the outside world until we need to
184            UserUpdatePrivateKeyResult {
185                user_master_private_key: resp.user_private_key.into(),
186                needs_rotation: resp.needs_rotation,
187            }
188        }
189    }
190
191    pub async fn update_private_key(
192        auth: &RequestAuth,
193        user_id: UserId,
194        user_key_id: u64,
195        new_encrypted_private_key: EncryptedPrivateKey,
196        augmenting_key: AugmentationFactor,
197    ) -> Result<UserUpdatePrivateKeyResponse, IronOxideErr> {
198        auth.request
199            .put(
200                &format!(
201                    "users/{}/keys/{}",
202                    rest::url_encode(user_id.id()),
203                    user_key_id
204                ),
205                &UserUpdatePrivateKey {
206                    user_private_key: new_encrypted_private_key,
207                    augmentation_factor: augmenting_key,
208                },
209                RequestErrorCode::UserKeyUpdate,
210                AuthV2Builder::new(auth, OffsetDateTime::now_utc()),
211            )
212            .await
213    }
214}
215
216pub mod user_create {
217    use crate::internal::{user_api::UserCreateResult, TryInto};
218
219    use super::*;
220
221    #[derive(Debug, PartialEq, Eq, Deserialize)]
222    #[serde(rename_all = "camelCase")]
223    pub struct UserCreateResponse {
224        id: String,
225        status: usize,
226        segment_id: usize,
227        pub user_private_key: EncryptedPrivateKey,
228        pub user_master_public_key: PublicKey,
229        needs_rotation: bool,
230    }
231
232    #[derive(Debug, Serialize)]
233    #[serde(rename_all = "camelCase")]
234    struct UserCreateReq {
235        user_public_key: PublicKey,
236        user_private_key: EncryptedPrivateKey,
237        needs_rotation: bool,
238    }
239
240    pub async fn user_create(
241        jwt: &Jwt,
242        user_public_key: PublicKey,
243        encrypted_user_private_key: EncryptedPrivateKey,
244        needs_rotation: bool,
245        request: IronCoreRequest,
246    ) -> Result<UserCreateResponse, IronOxideErr> {
247        let req_body = UserCreateReq {
248            user_private_key: encrypted_user_private_key,
249            user_public_key,
250            needs_rotation,
251        };
252        request
253            .post_jwt_auth(
254                "users",
255                &req_body,
256                RequestErrorCode::UserCreate,
257                &Authorization::JwtAuth(jwt),
258            )
259            .await
260    }
261    impl TryFrom<UserCreateResponse> for UserCreateResult {
262        type Error = IronOxideErr;
263
264        fn try_from(resp: UserCreateResponse) -> Result<Self, Self::Error> {
265            Ok(UserCreateResult {
266                id: resp.id,
267                user_public_key: resp.user_master_public_key.try_into()?,
268                needs_rotation: resp.needs_rotation,
269            })
270        }
271    }
272}
273
274pub mod user_update {
275    use crate::internal::{user_api::UserCreateResult, TryInto};
276
277    use super::*;
278
279    #[derive(Debug, PartialEq, Eq, Deserialize)]
280    #[serde(rename_all = "camelCase")]
281    pub struct UserUpdateResponse {
282        id: String,
283        status: usize,
284        segment_id: usize,
285        pub user_private_key: EncryptedPrivateKey,
286        pub user_master_public_key: PublicKey,
287        needs_rotation: bool,
288    }
289
290    #[derive(Debug, Serialize)]
291    #[serde(rename_all = "camelCase")]
292    struct UserUpdateReq {
293        user_private_key: Option<EncryptedPrivateKey>,
294    }
295
296    pub async fn user_update(
297        auth: &RequestAuth,
298        user_id: &UserId,
299        encrypted_user_private_key: Option<EncryptedPrivateKey>,
300    ) -> Result<UserUpdateResponse, IronOxideErr> {
301        let req_body = UserUpdateReq {
302            user_private_key: encrypted_user_private_key,
303        };
304        auth.request
305            .put(
306                &format!("users/{}", rest::url_encode(user_id.id())),
307                &req_body,
308                RequestErrorCode::UserUpdate,
309                AuthV2Builder::new(auth, OffsetDateTime::now_utc()),
310            )
311            .await
312    }
313
314    impl TryFrom<UserUpdateResponse> for UserCreateResult {
315        type Error = IronOxideErr;
316
317        fn try_from(resp: UserUpdateResponse) -> Result<Self, Self::Error> {
318            Ok(UserCreateResult {
319                id: resp.id,
320                user_public_key: resp.user_master_public_key.try_into()?,
321                needs_rotation: resp.needs_rotation,
322            })
323        }
324    }
325}
326
327pub mod user_key_list {
328    use super::*;
329
330    #[derive(Deserialize)]
331    #[serde(rename_all = "camelCase")]
332    pub struct UserPublicKey {
333        pub id: String,
334        pub user_master_public_key: crate::internal::rest::json::PublicKey,
335    }
336    #[derive(Deserialize)]
337    pub struct UserKeyListResponse {
338        pub(crate) result: Vec<UserPublicKey>,
339    }
340
341    pub async fn user_key_list_request(
342        auth: &RequestAuth,
343        users: &[UserId],
344    ) -> Result<UserKeyListResponse, IronOxideErr> {
345        let user_ids: Vec<&str> = users.iter().map(UserId::id).collect();
346        if !user_ids.is_empty() {
347            auth.request
348                .get_with_query_params(
349                    "users",
350                    &[("id".into(), rest::url_encode(&user_ids.join(",")))],
351                    RequestErrorCode::UserKeyList,
352                    AuthV2Builder::new(auth, OffsetDateTime::now_utc()),
353                )
354                .await
355        } else {
356            Ok(UserKeyListResponse { result: vec![] })
357        }
358    }
359}
360
361pub mod device_add {
362    use crate::internal::{
363        rest::json::TransformKey,
364        user_api::{requests::PublicKey, DeviceAdd, DeviceId, Jwt},
365    };
366
367    use super::*;
368
369    #[derive(Debug, Serialize)]
370    #[serde(rename_all = "camelCase")]
371    pub struct DeviceAddReq {
372        pub timestamp: u64,
373        pub device: Device,
374        #[serde(with = "Base64Standard")]
375        pub signature: Vec<u8>,
376        pub user_public_key: PublicKey,
377    }
378
379    #[derive(Debug, Serialize)]
380    #[serde(rename_all = "camelCase")]
381    pub struct Device {
382        pub transform_key: TransformKey,
383        pub name: Option<DeviceName>,
384    }
385
386    #[derive(Debug, Deserialize)]
387    #[serde(rename_all = "camelCase")]
388    pub struct DeviceAddResponse {
389        #[serde(rename = "id")]
390        pub device_id: DeviceId,
391        pub name: Option<DeviceName>,
392        #[serde(with = "time::serde::rfc3339")]
393        pub created: OffsetDateTime,
394        #[serde(with = "time::serde::rfc3339")]
395        pub updated: OffsetDateTime,
396    }
397
398    pub(crate) async fn user_device_add(
399        jwt: &Jwt,
400        device_add: &DeviceAdd,
401        name: &Option<DeviceName>,
402        request: &IronCoreRequest,
403    ) -> Result<DeviceAddResponse, IronOxideErr> {
404        let req_body: DeviceAddReq = DeviceAddReq {
405            timestamp: rest::as_unix_timestamp_millis(device_add.signature_ts) as u64,
406            user_public_key: device_add.user_public_key.clone().into(),
407            signature: device_add.signature.clone().into(),
408            device: Device {
409                transform_key: device_add.transform_key.clone().into(),
410                name: name.clone(),
411            },
412        };
413        request
414            .post_jwt_auth(
415                "users/devices",
416                &req_body,
417                RequestErrorCode::UserDeviceAdd,
418                &Authorization::JwtAuth(jwt),
419            )
420            .await
421    }
422}
423
424pub mod device_list {
425    use time::OffsetDateTime;
426
427    use crate::internal::user_api::{DeviceId, DeviceName, UserDevice};
428
429    use super::*;
430
431    #[derive(Debug, PartialEq, Eq, Deserialize)]
432    #[serde(rename_all = "camelCase")]
433    pub struct DeviceListItem {
434        #[serde(rename = "id")]
435        device_id: DeviceId,
436        name: Option<DeviceName>,
437        #[serde(with = "time::serde::rfc3339")]
438        created: OffsetDateTime,
439        #[serde(with = "time::serde::rfc3339")]
440        updated: OffsetDateTime,
441        is_current_device: bool,
442    }
443
444    #[derive(Debug, PartialEq, Eq, Deserialize)]
445    pub struct DeviceListResponse {
446        pub(in crate::internal) result: Vec<DeviceListItem>,
447    }
448
449    pub async fn device_list(auth: &RequestAuth) -> Result<DeviceListResponse, IronOxideErr> {
450        auth.request
451            .get(
452                &format!("users/{}/devices", rest::url_encode(&auth.account_id().0)),
453                RequestErrorCode::UserDeviceList,
454                AuthV2Builder::new(auth, OffsetDateTime::now_utc()),
455            )
456            .await
457    }
458
459    impl From<DeviceListItem> for UserDevice {
460        fn from(resp: DeviceListItem) -> Self {
461            UserDevice {
462                id: resp.device_id,
463                name: resp.name,
464                created: resp.created,
465                last_updated: resp.updated,
466                is_current_device: resp.is_current_device,
467            }
468        }
469    }
470}
471
472pub mod device_delete {
473    use super::*;
474    use crate::{internal::user_api::DeviceId, IronOxideErr};
475
476    #[derive(Deserialize)]
477    pub struct DeviceDeleteResponse {
478        pub(crate) id: DeviceId,
479    }
480
481    pub async fn device_delete(
482        auth: &RequestAuth,
483        device_id: &DeviceId,
484    ) -> Result<DeviceDeleteResponse, IronOxideErr> {
485        auth.request
486            .delete_with_no_body(
487                &format!(
488                    "users/{}/devices/{}",
489                    rest::url_encode(&auth.account_id().0),
490                    device_id.0
491                ),
492                RequestErrorCode::UserDeviceDelete,
493                AuthV2Builder::new(auth, OffsetDateTime::now_utc()),
494            )
495            .await
496    }
497
498    pub async fn device_delete_current(
499        auth: &RequestAuth,
500    ) -> Result<DeviceDeleteResponse, IronOxideErr> {
501        auth.request
502            .delete_with_no_body(
503                &format!(
504                    "users/{}/devices/current",
505                    rest::url_encode(&auth.account_id().0)
506                ),
507                RequestErrorCode::UserDeviceDelete,
508                AuthV2Builder::new(auth, OffsetDateTime::now_utc()),
509            )
510            .await
511    }
512}