2
\$\begingroup\$

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

  1. 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.
  2. 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.
  3. Using the password and salt, create an "API password" (Argon2(password, salt))
  4. Use API password to sign up. Server would only know the derived password and not the original one (entered and known by the user).
  5. Create another Argon2 hash and use it as a key to AES-encrypt the JSON payload (AES-256/CBC/Pkcs).
  6. Attach IV and random salt to encrypted payload. All of that is necessary to decrypt the payload later.
  7. 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:

crypto.tar.gz

\$\endgroup\$
2
  • \$\begingroup\$ what's the use case for this? I'm guessing the client is a web browser that doesn't have persistent storage, but would be good to confirm. assuming that's the case, what's to stop the server from "going rogue" and inserting a gadget that reveals the password to the server? \$\endgroup\$
    – Sam Mason
    Commented Feb 7, 2020 at 16:09
  • \$\begingroup\$ @SamMason I'm prototyping a portfolio tracking service. The clients are native mobile apps (Android and iOS for now). Most of the API endpoints are not sensitive (asset price data, etc) but when it comes to user portfolios, especially the amounts of assets held, I don't really want to hold that data. One reason for that is pragmatic: that would make data breaches less consequential. Another reason is mostly ethical: I don't want to hold other people's personal data which is not critical for providing a service. \$\endgroup\$ Commented Feb 8, 2020 at 5:58

2 Answers 2

3
\$\begingroup\$

a few comments, from high to low-level:

Usability

users are generally pretty terrible with passwords! having them "lose" all their data if they forget their password often isn't something they expect. i.e. they just want to do a "password reset" and get access to everything again. I know this is at odds with what you (and lots of similar designs) are trying to do, but thought it useful to point out

Separate authentication from encryption

I'd be tempted to use a more conventional protocol for authentication, probably also separating out passwords used for authentication and encryption. This would let you, e.g., incrementally increase the Argon2 complexity without invalidating their login (this is something you really need to allow for). It would also let you use some sort of federated authentication system which might be nice

Crypto usage

  • CBC doesn't provide any authenticity. i.e. you won't know if the right password has been used / some attacker is trying to decieve you. I'd suggest using another block mode, or maybe a stream cipher like chacha20-poly1305
  • I'd probably just use the hash of the email as the salt, using password as well doesn't seem to help much.
  • your use of Argon2 in encrypt_payload seems to misunderstand its purpose as a general KDF. You should just request the amount of output you want, e.g., 48 bytes of keying material from Argon2 and use 32 bytes of that as key and 16 bytes as IV. that means you just need a single 128bit "key" per file rather than a "salt" and an "IV". your usage isn't any more secure, 128bits is a big enough state space to stop a brute force attack while access to your storage server would give them the header anyway

API usage

  • argon2::hash_encoded is mostly designed for hashing passwords for authentication. you almost certainly want to be using hash_raw to just get the keying material out
  • it might be useful if your design allowed the Config parameters over time to increase computation complexity. you really want to record the parameter values used somewhere so the right ones can be used
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for a detailed answer! Not that I understand it completely but it has all of the keywords to dig further so I guess it's just a matter of time :) \$\endgroup\$ Commented Feb 8, 2020 at 16:47
  • \$\begingroup\$ I agree on usability and it a tough compromise indeed. The idea of separating auth and encryption looks interesting. Data encryption might be implemented as an opt-in feature so users could decide for themselves. \$\endgroup\$ Commented Feb 8, 2020 at 16:58
2
\$\begingroup\$

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.

It makes it harder, by a constant factor. If the password is weak, it can still be found. Password based authentication should not be preferred, and if it is used, it should be surrounded by as many extra measures you can think of (with a maximum amount of tries and password strength indication to the user, for instance).

Use sha(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.

Well, kind of. If a adversary can guess then they can still precompute tables for a specific user, who may use different passwords on the same site for instance. This is detectable if you don't use a random 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).

So you don't use a username to authenticate? Or is that simply missing from your scheme? And you use a static value to sign up, send over an unencrypted line, that anybody may intercept? That cannot be right?

Are you going to search through all password hashes to get to the right one?

Create another Argon2 hash and use it as a key to AES-encrypt the JSON payload (AES-256/CBC/Pkcs).

CBC with PKCS#7 padding is absolutely terrible as it allows for padding oracle attacks. 128 tries per byte, and that is if other plaintext oracle attacks cannot be made even more efficiently.

Creating another Argon2 hash means double the work for you, and still single the work to an attacker. Instead, you can split the key into two using a KBKDF function such as HKDF (or a poor mans KDF such as using HMAC with the secret as key and an info string as message to distinguish two or more keys).

Attach IV and random salt to encrypted payload. All of that is necessary to decrypt the payload later.

Ah, so now you are using a random salt? If that's true, you don't need a random IV, because the key would be unique.


All in all, your scheme is far from complete and far from bullet proof. Showing CBC in here shows the limited understanding of creating secure protocols. You're much better off using an existing one, e.g. TLS also has options for pre-shared keys. Those you can use if both sides can create a password hash.

If you would log in using that email address then you can send a random salt stored with the hash, and have the user perform HMAC over a random challenge using the result of passsword derivation Argon2 with the hash as key. That would be much more secure as a standalone authentication method.


The code is already shortly referred to by Sam, so I won't go into it. It seems relatively clear, but I think you are due to rewrite it anyway.

\$\endgroup\$
4
  • \$\begingroup\$ ... on the bright side, you are looking at a password hash at least. \$\endgroup\$ Commented Feb 12, 2020 at 18:02
  • \$\begingroup\$ "So you don't use a username to authenticate?" - nope. Just email + password. "And you use a static value to sign up, send over an unencrypted line, that anybody may intercept?" - it supposed to be sent via secure connection, of course. It's not about protecting the data in transit, my understanding is: SSL takes care of this. "Are you going to search through all password hashes to get to the right one?" - not sure if I understand your question. Same password + deterministic salt should give the same hash on any device so server would log it in just fine. \$\endgroup\$ Commented Feb 12, 2020 at 18:20
  • \$\begingroup\$ Hey, if you don't say what data you use to login then don't blame me for not getting it right. There is not a word about TLS either. Getting somebody to log in is not hard; securing it is the trick. \$\endgroup\$ Commented Feb 12, 2020 at 18:23
  • \$\begingroup\$ Sure, there is a lot of stuff going on out of the scope of this code and I really appreciate the feedback. I did some reading and asked around since posting that code and now I think that sha256(app name + email + password) would make a better salt (more likely to be globally unique, although still enables pre-computation attacks) and it seems like it's better to split the auth scheme and data encryption. I think I get the auth part now and the code seems to be fine for my use case, but encryption is certainly not and I'll look into it and read/ask more. Thanks for your feedback! \$\endgroup\$ Commented Feb 12, 2020 at 18:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.