use crate::{
crypto::aes::{self, EncryptedMasterKey},
internal::{rest::IronCoreRequest, *},
};
use chrono::{DateTime, Utc};
use itertools::{Either, Itertools};
use rand::rngs::OsRng;
use recrypt::prelude::*;
use std::{
collections::HashMap,
convert::{TryFrom, TryInto},
result::Result,
sync::Mutex,
};
mod requests;
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
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, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
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 > (i64::max_value() 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, Eq, Hash, 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(Clone, Debug, Eq, Hash, PartialEq)]
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(Clone, Debug, Eq, Hash, PartialEq)]
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(Clone, Debug, Eq, Hash, PartialEq)]
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, Debug, Eq, Hash, PartialEq)]
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 async fn user_verify(
jwt: Jwt,
request: IronCoreRequest,
) -> Result<Option<UserResult>, IronOxideErr> {
requests::user_verify::user_verify(&jwt, &request)
.await?
.map(|resp| resp.try_into())
.transpose()
}
pub async fn user_create<CR: rand::CryptoRng + rand::RngCore>(
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
jwt: Jwt,
passphrase: Password,
needs_rotation: bool,
request: IronCoreRequest,
) -> Result<UserCreateResult, IronOxideErr> {
let (encrypted_priv_key, recrypt_pub) = 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))?)
})?;
requests::user_create::user_create(
&jwt,
recrypt_pub.into(),
encrypted_priv_key.into(),
needs_rotation,
request,
)
.await?
.try_into()
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct EncryptedPrivateKey(Vec<u8>);
impl EncryptedPrivateKey {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
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 async fn user_get_current(auth: &RequestAuth) -> Result<UserResult, IronOxideErr> {
requests::user_get::get_curr_user(auth)
.await
.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: result.id,
})
})
}
pub async fn user_rotate_private_key<CR: rand::CryptoRng + rand::RngCore>(
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
password: Password,
auth: &RequestAuth,
) -> Result<UserUpdatePrivateKeyResult, IronOxideErr> {
let requests::user_get::CurrentUserResponse {
user_private_key: encrypted_priv_key,
current_key_id,
id: curr_user_id,
..
} = requests::user_get::get_curr_user(auth).await?;
let (user_id, curr_key_id, new_encrypted_priv_key, aug_factor) = {
let priv_key: PrivateKey = aes::decrypt_user_master_key(
&password.0,
&aes::EncryptedMasterKey::new_from_slice(&encrypted_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(OsRng::default()),
&password.0,
new_priv_key.as_bytes(),
)?;
(
curr_user_id,
current_key_id,
new_encrypted_priv_key,
aug_factor,
)
};
Ok(requests::user_update_private_key::update_private_key(
auth,
user_id,
curr_key_id,
new_encrypted_priv_key.into(),
aug_factor.into(),
)
.await?
.into())
}
pub async fn generate_device_key<CR: rand::CryptoRng + rand::RngCore>(
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
jwt: &Jwt,
password: Password,
device_name: Option<DeviceName>,
signing_ts: &DateTime<Utc>,
request: &IronCoreRequest,
) -> Result<DeviceAddResult, IronOxideErr> {
let requests::user_verify::UserVerifyResponse {
user_private_key,
user_master_public_key,
id: account_id,
segment_id,
..
} = requests::user_verify::user_verify(jwt, request)
.await?
.ok_or_else(|| {
IronOxideErr::UserDoesNotExist(
"Device cannot be added to a user that doesn't exist".to_string(),
)
})?;
let (device_add, account_id) = (
{
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));
generate_device_add(recrypt, jwt, &user_keypair, signing_ts)?
},
account_id.try_into()?,
);
let device_add_response =
requests::device_add::user_device_add(jwt, &device_add, &device_name, request).await?;
Ok(DeviceAddResult {
account_id,
segment_id,
device_private_key: device_add.device_keys.private_key,
signing_private_key: device_add.signing_keys,
device_id: device_add_response.device_id,
name: device_add_response.name,
created: device_add_response.created,
last_updated: device_add_response.updated,
})
}
pub async fn device_list(auth: &RequestAuth) -> Result<UserDeviceListResult, IronOxideErr> {
let resp = requests::device_list::device_list(auth).await?;
let devices = {
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));
vec
};
Ok(UserDeviceListResult::new(devices))
}
pub async fn device_delete(
auth: &RequestAuth,
device_id: Option<&DeviceId>,
) -> Result<DeviceId, IronOxideErr> {
match device_id {
Some(device_id) => requests::device_delete::device_delete(auth, device_id).await,
None => requests::device_delete::device_delete_current(auth).await,
}
.map(|resp| resp.id)
}
pub async fn user_key_list(
auth: &RequestAuth,
user_ids: &Vec<UserId>,
) -> Result<HashMap<UserId, PublicKey>, IronOxideErr> {
requests::user_key_list::user_key_list_request(auth, user_ids)
.await
.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) async fn get_user_keys(
auth: &RequestAuth,
users: &Vec<UserId>,
) -> Result<(Vec<UserId>, Vec<WithKey<UserId>>), IronOxideErr> {
if users.is_empty() {
Ok((vec![], vec![]))
} else {
user_api::user_key_list(auth, users)
.await
.map(|ids_with_keys| {
users.clone().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,
device_keys: device_keypair,
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)]
pub(crate) mod tests {
use super::*;
pub fn create_user_result(
account_id: UserId,
segment_id: usize,
user_public_key: PublicKey,
needs_rotation: bool,
) -> UserResult {
UserResult {
account_id,
segment_id,
user_public_key,
needs_rotation,
}
}
#[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)
);
}
}