use crate::{
crypto::aes::{self, EncryptedMasterKey},
internal::{rest::IronCoreRequest, *},
};
use chrono::{DateTime, Utc};
use futures::prelude::*;
use itertools::{Either, Itertools};
use rand::rngs::EntropyRng;
use recrypt::prelude::*;
use std::{
collections::HashMap,
convert::{TryFrom, TryInto},
result::Result,
sync::Mutex,
};
mod requests;
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Eq, Hash)]
pub struct UserId(pub(crate) String);
impl UserId {
pub fn id(&self) -> &str {
&self.0
}
pub fn unsafe_from_string(id: String) -> UserId {
UserId(id)
}
}
impl TryFrom<String> for UserId {
type Error = IronOxideErr;
fn try_from(user_id: String) -> Result<Self, Self::Error> {
user_id.as_str().try_into()
}
}
impl TryFrom<&str> for UserId {
type Error = IronOxideErr;
fn try_from(user_id: &str) -> Result<Self, Self::Error> {
validate_id(user_id, "user_id").map(UserId)
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct DeviceId(pub(crate) u64);
impl DeviceId {
pub fn id(&self) -> &u64 {
&self.0
}
}
impl TryFrom<u64> for DeviceId {
type Error = IronOxideErr;
fn try_from(device_id: u64) -> Result<Self, Self::Error> {
if device_id < 1 || device_id > (std::i64::MAX as u64) {
Err(IronOxideErr::ValidationError(
"device_id".to_string(),
format!("'{}' must be a number greater than 0", device_id),
))
} else {
Ok(DeviceId(device_id))
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeviceName(pub(crate) String);
impl DeviceName {
pub fn name(&self) -> &String {
&self.0
}
}
impl TryFrom<&str> for DeviceName {
type Error = IronOxideErr;
fn try_from(name: &str) -> Result<Self, Self::Error> {
validate_name(name, "device_name").map(DeviceName)
}
}
#[derive(Debug)]
pub struct UserCreateResult {
user_public_key: PublicKey,
needs_rotation: bool,
}
impl UserCreateResult {
pub fn user_public_key(&self) -> &PublicKey {
&self.user_public_key
}
pub fn needs_rotation(&self) -> bool {
self.needs_rotation
}
}
pub struct DeviceAdd {
user_public_key: PublicKey,
transform_key: TransformKey,
device_keys: KeyPair,
signing_keys: DeviceSigningKeyPair,
signature: SchnorrSignature,
signature_ts: DateTime<Utc>,
}
#[derive(Debug)]
pub struct UserResult {
account_id: UserId,
segment_id: usize,
user_public_key: PublicKey,
needs_rotation: bool,
}
impl UserResult {
pub fn user_public_key(&self) -> &PublicKey {
&self.user_public_key
}
pub fn account_id(&self) -> &UserId {
&self.account_id
}
pub fn segment_id(&self) -> usize {
self.segment_id
}
pub fn needs_rotation(&self) -> bool {
self.needs_rotation
}
}
#[derive(Debug)]
pub struct UserDeviceListResult {
result: Vec<UserDevice>,
}
impl UserDeviceListResult {
fn new(result: Vec<UserDevice>) -> UserDeviceListResult {
UserDeviceListResult { result }
}
pub fn result(&self) -> &Vec<UserDevice> {
&self.result
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct UserDevice {
id: DeviceId,
name: Option<DeviceName>,
created: DateTime<Utc>,
last_updated: DateTime<Utc>,
is_current_device: bool,
}
impl UserDevice {
pub fn id(&self) -> &DeviceId {
&self.id
}
pub fn name(&self) -> Option<&DeviceName> {
self.name.as_ref()
}
pub fn created(&self) -> &DateTime<Utc> {
&self.created
}
pub fn last_updated(&self) -> &DateTime<Utc> {
&self.last_updated
}
pub fn is_current_device(&self) -> bool {
self.is_current_device
}
}
pub fn user_verify(
jwt: Jwt,
request: IronCoreRequest,
) -> impl Future<Item = Option<UserResult>, Error = IronOxideErr> {
requests::user_verify::user_verify(&jwt, &request)
.and_then(|e| e.map(|resp| resp.try_into()).transpose())
}
pub fn user_create<CR: rand::CryptoRng + rand::RngCore>(
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
jwt: Jwt,
passphrase: Password,
needs_rotation: bool,
request: IronCoreRequest<'static>,
) -> impl Future<Item = UserCreateResult, Error = IronOxideErr> {
recrypt
.generate_key_pair()
.map_err(IronOxideErr::from)
.and_then(|(recrypt_priv, recrypt_pub)| {
Ok(aes::encrypt_user_master_key(
&Mutex::new(rand::thread_rng()),
passphrase.0.as_str(),
recrypt_priv.bytes(),
)
.map(|encrypted_private_key| (encrypted_private_key, recrypt_pub))?)
})
.into_future()
.and_then(move |(encrypted_priv_key, recrypt_pub)| {
requests::user_create::user_create(
&jwt,
recrypt_pub.into(),
encrypted_priv_key.into(),
needs_rotation,
request,
)
})
.and_then(|resp| resp.try_into())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptedPrivateKey(Vec<u8>);
impl EncryptedPrivateKey {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct UserUpdatePrivateKeyResult {
user_master_private_key: EncryptedPrivateKey,
needs_rotation: bool,
}
impl UserUpdatePrivateKeyResult {
pub fn user_master_private_key(&self) -> &EncryptedPrivateKey {
&self.user_master_private_key
}
pub fn needs_rotation(&self) -> bool {
self.needs_rotation
}
}
pub fn user_get_current(
auth: &RequestAuth,
) -> impl Future<Item = UserResult, Error = IronOxideErr> + '_ {
requests::user_get::get_curr_user(auth).and_then(|result| {
Ok(UserResult {
needs_rotation: result.needs_rotation,
user_public_key: result.user_master_public_key.try_into()?,
segment_id: result.segment_id,
account_id: UserId::unsafe_from_string(result.id),
})
})
}
pub fn user_rotate_private_key<'apicall, CR: rand::CryptoRng + rand::RngCore>(
recrypt: &'apicall Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
password: Password,
auth: &'apicall RequestAuth,
) -> impl Future<Item = UserUpdatePrivateKeyResult, Error = IronOxideErr> + 'apicall {
requests::user_get::get_curr_user(&auth)
.and_then(move |curr_user| {
Ok({
let encrypt_priv_key = curr_user.user_private_key;
let priv_key: PrivateKey = aes::decrypt_user_master_key(
&password.0,
&aes::EncryptedMasterKey::new_from_slice(&encrypt_priv_key.0)?,
)?
.into();
let (new_priv_key, aug_factor) =
augment_private_key_with_retry(recrypt, &priv_key)?;
let new_encrypted_priv_key = aes::encrypt_user_master_key(
&Mutex::new(EntropyRng::default()),
&password.0,
new_priv_key.as_bytes(),
)?;
(
UserId(curr_user.id),
curr_user.current_key_id,
new_encrypted_priv_key,
aug_factor,
)
})
})
.and_then(
move |(user_id, curr_key_id, new_encrypted_priv_key, aug_factor)| {
requests::user_update_private_key::update_private_key(
&auth,
user_id,
curr_key_id,
new_encrypted_priv_key.into(),
aug_factor.into(),
)
.map(|resp| resp.into())
},
)
}
pub fn generate_device_key<'a, CR: rand::CryptoRng + rand::RngCore>(
recrypt: &'a Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
jwt: &'a Jwt,
password: Password,
device_name: Option<DeviceName>,
signing_ts: &'a DateTime<Utc>,
request: IronCoreRequest<'static>,
) -> impl Future<Item = DeviceContext, Error = IronOxideErr> + 'a {
requests::user_verify::user_verify(&jwt, &request)
.and_then(|maybe_user| {
maybe_user.ok_or(IronOxideErr::UserDoesNotExist(
"Device cannot be added to a user that doesn't exist".to_string(),
))
})
.and_then(
move |requests::user_verify::UserVerifyResponse {
user_private_key,
user_master_public_key,
id: account_id,
segment_id,
..
}| {
Ok((
{
let user_public_key: RecryptPublicKey =
PublicKey::try_from(user_master_public_key)?.into();
let user_private_key =
EncryptedMasterKey::new_from_slice(&user_private_key.0)?;
let user_private_key =
aes::decrypt_user_master_key(&password.0, &user_private_key)?;
let user_keypair: KeyPair =
KeyPair::new(user_public_key, RecryptPrivateKey::new(user_private_key));
let device_add =
generate_device_add(recrypt, &jwt, &user_keypair, &signing_ts)?;
device_add
},
account_id.try_into()?,
segment_id,
))
},
)
.and_then(move |(device_add, account_id, segment_id)| {
requests::device_add::user_device_add(&jwt, &device_add, &device_name, &request)
.map(move |response| {
DeviceContext::new(
response.device_id,
account_id,
segment_id,
device_add.device_keys.private_key,
device_add.signing_keys,
)
})
})
}
pub fn device_list(
auth: &RequestAuth,
) -> impl Future<Item = UserDeviceListResult, Error = IronOxideErr> + '_ {
requests::device_list::device_list(auth).map(|resp| {
let mut vec: Vec<UserDevice> = resp.result.into_iter().map(UserDevice::from).collect();
vec.sort_by(|a, b| a.id.0.cmp(&b.id.0));
UserDeviceListResult::new(vec)
})
}
pub fn device_delete<'a>(
auth: &'a RequestAuth,
device_id: Option<&'a DeviceId>,
) -> impl Future<Item = DeviceId, Error = IronOxideErr> + 'a {
match device_id {
Some(device_id) => requests::device_delete::device_delete(auth, device_id),
None => requests::device_delete::device_delete_current(auth),
}
.map(|resp| resp.id)
}
pub fn user_key_list<'a>(
auth: &'a RequestAuth,
user_ids: &'a Vec<UserId>,
) -> impl Future<Item = HashMap<UserId, PublicKey>, Error = IronOxideErr> + 'a {
requests::user_key_list::user_key_list_request(auth, user_ids).map(
move |requests::user_key_list::UserKeyListResponse { result }| {
result
.into_iter()
.fold(HashMap::with_capacity(user_ids.len()), |mut acc, user| {
let maybe_pub_key = PublicKey::try_from(user.user_master_public_key.clone());
maybe_pub_key.into_iter().for_each(|pub_key| {
acc.insert(UserId::unsafe_from_string(user.id.clone()), pub_key);
});
acc
})
},
)
}
pub(crate) fn get_user_keys<'a>(
auth: &'a RequestAuth,
users: &'a Vec<UserId>,
) -> Box<dyn Future<Item = (Vec<UserId>, Vec<WithKey<UserId>>), Error = IronOxideErr> + 'a> {
if users.len() == 0 {
return Box::new(futures::future::ok((vec![], vec![])));
}
let cloned_users = users.clone();
let fetch_users = user_api::user_key_list(auth, &users);
Box::new(fetch_users.map(|ids_with_keys| {
cloned_users.into_iter().partition_map(|user_id| {
let maybe_public_key = ids_with_keys.get(&user_id).cloned();
match maybe_public_key {
Some(pk) => Either::Right(WithKey::new(user_id, pk)),
None => Either::Left(user_id),
}
})
}))
}
fn generate_device_add<CR: rand::CryptoRng + rand::RngCore>(
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
jwt: &Jwt,
user_master_keypair: &KeyPair,
signing_ts: &DateTime<Utc>,
) -> Result<DeviceAdd, IronOxideErr> {
let signing_keypair = recrypt.generate_ed25519_key_pair();
let (recrypt_priv_key, recrypt_pub_key) = recrypt.generate_key_pair()?;
let device_keypair = KeyPair::new(recrypt_pub_key, recrypt_priv_key);
let trans_key: TransformKey = recrypt
.generate_transform_key(
&user_master_keypair.private_key().recrypt_key(),
&device_keypair.public_key().into(),
&signing_keypair,
)?
.into();
let sig = gen_device_add_signature(recrypt, jwt, &user_master_keypair, &trans_key, signing_ts);
Ok(DeviceAdd {
user_public_key: user_master_keypair.public_key().clone(),
transform_key: trans_key.into(),
device_keys: device_keypair.into(),
signing_keys: signing_keypair.into(),
signature: sig,
signature_ts: signing_ts.to_owned(),
})
}
fn gen_device_add_signature<CR: rand::CryptoRng + rand::RngCore>(
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
jwt: &Jwt,
user_master_keypair: &KeyPair,
transform_key: &TransformKey,
signing_ts: &DateTime<Utc>,
) -> SchnorrSignature {
struct SignedMessage<'a> {
timestamp: &'a DateTime<Utc>,
transform_key: &'a TransformKey,
jwt: &'a Jwt,
user_public_key: &'a PublicKey,
};
impl<'a> recrypt::api::Hashable for SignedMessage<'a> {
fn to_bytes(&self) -> Vec<u8> {
let mut vec: Vec<u8> = vec![];
vec.extend_from_slice(&self.timestamp.timestamp_millis().to_string().as_bytes());
vec.extend_from_slice(&self.transform_key.to_bytes());
vec.extend_from_slice(&self.jwt.to_utf8());
vec.extend_from_slice(&self.user_public_key.as_bytes());
vec
}
}
let msg = SignedMessage {
timestamp: signing_ts,
transform_key,
jwt,
user_public_key: &user_master_keypair.public_key(),
};
recrypt
.schnorr_sign(
&user_master_keypair.private_key().recrypt_key(),
&user_master_keypair.public_key().into(),
&msg,
)
.into()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn user_id_validate_good() {
let user_id1 = "a_fo_real_good_group_id$";
let user_id2 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
assert_eq!(
UserId(user_id1.to_string()),
UserId::try_from(user_id1).unwrap()
);
assert_eq!(
UserId(user_id2.to_string()),
UserId::try_from(user_id2).unwrap()
)
}
#[test]
fn user_id_rejects_invalid() {
let user_id1 = UserId::try_from("not a good ID!");
let user_id2 = UserId::try_from("!!");
let user_id3 = UserId::try_from("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567891");
assert_that!(
&user_id1.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(
&user_id2.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(
&user_id3.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
}
#[test]
fn user_id_rejects_empty() {
let user_id = UserId::try_from("");
assert_that!(&user_id, is_variant!(Err));
assert_that!(
&user_id.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
let user_id = UserId::try_from("\n \t ");
assert_that!(&user_id, is_variant!(Err));
assert_that!(
&user_id.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
}
}