use anyhow::{anyhow, Result};
use ironoxide::prelude::*;
use lazy_static::lazy_static;
use mut_static::MutStatic;
use std::{
collections::HashSet,
convert::TryFrom,
fs::File,
iter::FromIterator,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
#[derive(Clone)]
struct Customer {
id: u32,
name: String,
email: String,
}
#[derive(Clone)]
struct EncryptedCustomer {
id: u32,
enc_name: Vec<u8>,
name_keys: Vec<u8>,
enc_email: Vec<u8>,
email_keys: Vec<u8>,
}
#[tokio::main]
async fn main() -> Result<()> {
let sdk = initialize_sdk_from_file(&PathBuf::from("example.devcon")).await?;
let start_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string();
let salt_group_id = create_group_id("indexedSearchGroup", &start_time)?;
create_group(&sdk, &salt_group_id, "PII Search").await?;
let encrypted_salt = sdk.create_blind_index(&salt_group_id).await?;
save_name_salt_to_app_server(encrypted_salt);
let encrypted_salt = get_name_salt_from_app_server();
let blind_index = encrypted_salt.initialize_search(&sdk).await?;
let group_id = create_group_id("customerService", &start_time)?;
create_group(&sdk, &group_id, "Customer Service").await?;
let cust1 = Customer {
id: 1,
name: "Gumby".to_string(),
email: "gumby@gumby.io".to_string(),
};
save_customer(&cust1, &group_id, &sdk, &blind_index).await?;
let cust2 = Customer {
id: 2,
name: "Æ neid 北亰".to_string(),
email: "".to_string(),
};
save_customer(&cust2, &group_id, &sdk, &blind_index).await?;
let cust3 = Customer {
id: 3,
name: "aeneid bei jing".to_string(),
email: "".to_string(),
};
save_customer(&cust3, &group_id, &sdk, &blind_index).await?;
let cust4 = Customer {
id: 4,
name: "J. Fred Muggs".to_string(),
email: "j.fred.muggs@nowhere.com".to_string(),
};
save_customer(&cust4, &group_id, &sdk, &blind_index).await?;
add_customers(&4, &group_id, &sdk, &blind_index).await?;
let query_str = get_search_query();
display_customer_matches_by_name(&sdk, &blind_index, &query_str).await?;
let encrypted_salt2 = sdk.create_blind_index(&salt_group_id).await?;
save_email_salt_to_app_server(encrypted_salt2);
let encrypted_salt2 = get_email_salt_from_app_server();
let blind_index2 = encrypted_salt2.initialize_search(&sdk).await?;
let cust24 = Customer {
id: 24,
name: "Pokey".to_string(),
email: "pokey@gumby.io".to_string(),
};
save_customer_with_2_indices(&cust24, &group_id, &sdk, &blind_index, &blind_index2).await?;
Ok(())
}
async fn add_customers(
last_id: &u32,
group_id: &ironoxide::group::GroupId,
sdk: &ironoxide::IronOxide,
blind_index: &ironoxide::search::BlindIndexSearch,
) -> Result<()> {
let mut next_id = last_id + 1;
loop {
let mut name = String::new();
println!("Enter next customer name, or blank line to quit");
std::io::stdin()
.read_line(&mut name)
.expect("error: couldn't read input");
name = name.trim().to_string();
if &name == "" {
break;
} else {
let mut email = String::new();
println!("Enter customer's email");
std::io::stdin()
.read_line(&mut email)
.expect("error: couldn't read input");
email = email.trim().to_string();
let cust = Customer {
id: next_id,
name: name,
email: email,
};
save_customer(&cust, &group_id, &sdk, &blind_index).await?;
}
next_id += 1;
}
Ok(())
}
async fn save_customer(
cust: &Customer,
group_id: &ironoxide::group::GroupId,
sdk: &ironoxide::IronOxide,
blind_index: &ironoxide::search::BlindIndexSearch,
) -> Result<()> {
let name_tokens = blind_index
.tokenize_data(&cust.name, None)?
.into_iter()
.collect::<Vec<u32>>();
let encrypt_opts = DocumentEncryptOpts::with_explicit_grants(
None, None, false, vec![group_id.into()], );
let enc_name = sdk
.document_encrypt_unmanaged(cust.name.as_bytes(), &encrypt_opts)
.await?;
let enc_email = sdk
.document_encrypt_unmanaged(cust.email.as_bytes(), &encrypt_opts)
.await?;
let enc_cust = EncryptedCustomer {
id: cust.id,
enc_name: enc_name.encrypted_data().to_vec(),
name_keys: enc_name.encrypted_deks().to_vec(),
enc_email: enc_email.encrypted_data().to_vec(),
email_keys: enc_email.encrypted_deks().to_vec(),
};
save_customer_to_app_server(enc_cust, name_tokens, vec![]);
Ok(())
}
async fn save_customer_with_2_indices(
cust: &Customer,
group_id: &ironoxide::group::GroupId,
sdk: &ironoxide::IronOxide,
name_blind_index: &ironoxide::search::BlindIndexSearch,
email_blind_index: &ironoxide::search::BlindIndexSearch,
) -> Result<()> {
let name_tokens = name_blind_index
.tokenize_data(&cust.name, None)?
.into_iter()
.collect::<Vec<u32>>();
let email_tokens = email_blind_index
.tokenize_data(&cust.email, None)?
.into_iter()
.collect::<Vec<u32>>();
let encrypt_opts = DocumentEncryptOpts::with_explicit_grants(
None, None, false, vec![group_id.into()], );
let enc_name = sdk
.document_encrypt_unmanaged(cust.name.as_bytes(), &encrypt_opts)
.await?;
let enc_email = sdk
.document_encrypt_unmanaged(cust.email.as_bytes(), &encrypt_opts)
.await?;
let enc_cust = EncryptedCustomer {
id: cust.id,
enc_name: enc_name.encrypted_data().to_vec(),
name_keys: enc_name.encrypted_deks().to_vec(),
enc_email: enc_email.encrypted_data().to_vec(),
email_keys: enc_email.encrypted_deks().to_vec(),
};
save_customer_to_app_server(enc_cust, name_tokens, email_tokens);
Ok(())
}
async fn initialize_sdk_from_file(device_path: &PathBuf) -> Result<IronOxide> {
if device_path.is_file() {
let device_context_file = File::open(&device_path)?;
let device_context: DeviceContext = serde_json::from_reader(device_context_file)?;
println!("Found DeviceContext in \"{}\"", device_path.display());
Ok(ironoxide::initialize(&device_context, &Default::default()).await?)
} else {
Err(anyhow!(
"Couldn't open file {} containing DeviceContext",
device_path.display()
))
}
}
fn create_group_id(id_str: &str, start_time: &String) -> Result<ironoxide::group::GroupId> {
let gid = id_str.to_owned() + start_time;
Ok(ironoxide::group::GroupId::try_from(gid)?)
}
async fn create_group(
sdk: &IronOxide,
group_id: &ironoxide::group::GroupId,
name: &str,
) -> Result<GroupCreateResult> {
let opts = GroupCreateOpts::new(
Some(group_id.to_owned()), Some(GroupName::try_from(name.to_owned())?), true, true, None, vec![], vec![], false, );
let group = sdk.group_create(&opts).await?;
Ok(group)
}
async fn filter_customer(
sdk: &IronOxide,
cust: &EncryptedCustomer,
name_parts: &Vec<&str>,
) -> Result<Option<String>> {
let dec_result = sdk
.document_decrypt_unmanaged(&cust.enc_name, &cust.name_keys)
.await?;
let dec_name = std::str::from_utf8(&dec_result.decrypted_data()).unwrap();
let dec_name_trans = ironoxide::search::transliterate_string(&dec_name);
if name_parts
.iter()
.all(|name_part| dec_name_trans.contains(name_part))
{
Ok(Some(dec_name.to_string()))
} else {
Ok(None)
}
}
async fn display_customer_matches_by_name(
sdk: &IronOxide,
name_index: &BlindIndexSearch,
query_str: &str,
) -> Result<()> {
let query_tokens = name_index
.tokenize_query(query_str, None)?
.into_iter()
.collect();
let customer_recs = search_customers_by_name(query_tokens);
let trans_query = ironoxide::search::transliterate_string(&query_str);
let name_parts: Vec<&str> = trans_query.split_whitespace().collect();
for cust in customer_recs.iter() {
let result = filter_customer(&sdk, &cust, &name_parts).await?;
match result {
Some(decrypted_name) => println!("{} {} matched query", cust.id, decrypted_name),
None => println!("{} did not match query", cust.id),
}
}
Ok(())
}
struct AppServerMock {
pub name_salt: Option<EncryptedBlindIndexSalt>,
pub email_salt: Option<EncryptedBlindIndexSalt>,
pub customers: Vec<(HashSet<u32>, HashSet<u32>, EncryptedCustomer)>,
}
impl Default for AppServerMock {
fn default() -> Self {
AppServerMock {
name_salt: None,
email_salt: None,
customers: vec![],
}
}
}
lazy_static! {
static ref MOCK_APP_SERVER: MutStatic<AppServerMock> =
MutStatic::from(AppServerMock::default());
}
fn save_name_salt_to_app_server(encrypted_salt: EncryptedBlindIndexSalt) {
MOCK_APP_SERVER.write().unwrap().name_salt = Some(encrypted_salt);
}
fn get_name_salt_from_app_server() -> EncryptedBlindIndexSalt {
MOCK_APP_SERVER.read().unwrap().name_salt.clone().unwrap()
}
fn save_email_salt_to_app_server(encrypted_salt: EncryptedBlindIndexSalt) {
MOCK_APP_SERVER.write().unwrap().email_salt = Some(encrypted_salt);
}
fn get_email_salt_from_app_server() -> EncryptedBlindIndexSalt {
MOCK_APP_SERVER.read().unwrap().email_salt.clone().unwrap()
}
fn save_customer_to_app_server(
customer: EncryptedCustomer,
name_tokens: Vec<u32>,
email_tokens: Vec<u32>,
) {
let name_set = HashSet::from_iter(name_tokens);
let email_set = HashSet::from_iter(email_tokens);
MOCK_APP_SERVER
.write()
.unwrap()
.customers
.push((name_set, email_set, customer));
}
fn search_customers_by_name(tokens: Vec<u32>) -> Vec<EncryptedCustomer> {
let name_query = HashSet::from_iter(tokens);
let cust_list = &MOCK_APP_SERVER.read().unwrap().customers;
let matching_cust: Vec<(HashSet<u32>, HashSet<u32>, EncryptedCustomer)> = cust_list
.iter()
.filter(|(name_set, _email_set, _cust)| name_query.is_subset(&name_set))
.cloned()
.collect();
matching_cust
.iter()
.map(|(_name_tokens, _email_tokens, cust)| cust.clone())
.collect()
}
fn get_search_query() -> String {
let mut query = String::new();
println!("Enter query string");
std::io::stdin()
.read_line(&mut query)
.expect("error: couldn't read input");
query.trim().to_string()
}