I don't have any experience in cryptography and I've written some crypto code that seem to be working, but I'm not sure if it safe and if the tools and methods I used are right.
Objective
- Make it possible to store data (JSON) on remote server without this server being able to read it in plaintext.
My Approach
- Upon registration, don't send the real user password to the server, use Argon2 hash of the real password instead. In my understanding, it should be pretty hard for a server to infer the real password based on Argon2 hash and without salt.
- Use sha256(email + password) to create deterministic salt. It allows users to log in from other devices and still pass password validation. In my understanding, deterministic salt doesn't make the resulting "API password" weaker.
- Using the password and salt, create an "API password" (Argon2(password, salt))
- Use API password to sign up. Server would only know the derived password and not the original one (entered and known by the user).
- Create another Argon2 hash and use it as a key to AES-encrypt the JSON payload (AES-256/CBC/Pkcs).
- Attach IV and random salt to encrypted payload. All of that is necessary to decrypt the payload later.
- Push the resulting data to back end server.
Code
use argon2::{self, Config};
use sha2::{Digest, Sha256};
use crypto::{ symmetriccipher, buffer, aes, blockmodes };
use crypto::buffer::{ ReadBuffer, WriteBuffer, BufferResult };
use rand::{ Rng };
fn main() {
// Step 1: Generating "password" that will be used for API sign up
let email = "[email protected]";
println!("Email: {}", email);
let password = "foobar";
// Never leaves client
println!("Password: {}", password);
let mut sha_hasher = Sha256::new();
sha_hasher.input(format!("{}{}", email, password));
let salt = sha_hasher.result();
let argon_config = Config::default();
let password_hash = argon2::hash_encoded(password.as_bytes(), &salt, &argon_config).unwrap();
println!("Password hash (full): {}", password_hash);
let password_hash_parts: Vec<&str> = password_hash.split("$").collect();
let password_hash = password_hash_parts[5];
// will be used for API sign up
println!("Password hash (short): {}", password_hash);
// Step 2: Encrypting some JSON client side
let payload = "[{ name: \"Secret Name\" }]";
println!("Original payload: {}", payload);
let encrypted_payload = encrypt_payload(&payload, &password);
let decrypted_payload = decrypt_payload(encrypted_payload, &password);
println!("Decrypted payload: {}", decrypted_payload);
// Step 3: Pushing encrypted payload to server
// (omitted, out of scope of review)
}
fn encrypt_payload(payload: &str, password: &str) -> Vec<u8> {
let salt = rand::thread_rng().gen::<[u8; 16]>();
let argon_config = Config::default();
let key = argon2::hash_encoded(password.as_bytes(), &salt, &argon_config).unwrap();
let iv = rand::thread_rng().gen::<[u8; 16]>();
let encrypted_bytes = encrypt(payload.as_bytes(), key.as_bytes(), &iv).ok().unwrap();
let encrypted_bytes : Vec<u8> = salt.iter().chain(&iv).chain(&encrypted_bytes).cloned().collect();
encrypted_bytes
}
fn decrypt_payload(encrypted_payload: Vec<u8>, password: &str) -> String {
let salt = &encrypted_payload[0..16];
let iv = &encrypted_payload[16..32];
let encrypted_message = &encrypted_payload[32..];
let argon_config = Config::default();
let key = argon2::hash_encoded(password.as_bytes(), &salt, &argon_config).unwrap();
let decrypted_message = decrypt(&encrypted_message[..], key.as_bytes(), &iv).ok().unwrap();
String::from_utf8(decrypted_message).unwrap()
}
Full code (Rust crate)
Here is the full code with encrypt/decrypt boilerplate based on this example: