commit 946b13465a01a0d69357575e9869bfd61bd2d289
parent 4397e3ba38deb84ef080b52531eeb453df4ef419
Author: seestem <seestem@noreply.codeberg.org>
Date: Thu, 18 Aug 2022 13:12:43 +0200
Merge pull request 'scrypt' (#1) from scrypt into root
Reviewed-on: https://codeberg.org/seestem/cyrtophora/pulls/1
Diffstat:
3 files changed, 48 insertions(+), 81 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -3,10 +3,9 @@ name = "bizkit-crypto"
version = "0.1.0"
edition = "2021"
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
[dependencies]
anyhow = "1.0.57"
ring = "0.16.20"
chacha20poly1305 = "0.9.0"
-base64 = "0.13.0"
-\ No newline at end of file
+base64 = "0.13.0"
+scrypt = "0.10.0"
+\ No newline at end of file
diff --git a/doc/password-hashing.md b/doc/password-hashing.md
@@ -18,26 +18,26 @@ A cool way to prevent this type of leak is by __obfuscating__ the
users password with a [__hash function__](https://en.wikipedia.org/wiki/Hash_function).
There are lots of hash functions that can be used, but most of these
-are not recommended. For example if you use SHA-256 or other
-computationally cheap (hash function without a __work factor__
-parameter) functions, they are vulnerable to dictionary attacks.
+will be a bad idea to use. For example if you use SHA-256 or other
+computationally cheap functions (hash function without a __work factor__
+parameter), they are vulnerable to rainbow table attacks.
Bruteforce is also possible if the password length is short/known,
asic miners can generate 100 TeraHashes PER Second.
The server can increase the passwords entropy by concatenating it with
-a random string. Users can also protect themselves by using longer
-passwords.
+a random string aka the __salt__. Users can also protect themselves
+by using longer passwords.
-The best method to use against plaintext password leaks and dictionary
-attacks is to use a __Password Hash Function__. Which is a hash
-function specially designed to be slow/expensive to compute which
-makes it impossible to bruteforce with current machines.
+The best method to use against plaintext password leaks and rainbow
+table attacks is to use a __Password Hash Function__. Which is a hash
+function specially designed to be slow/expensive to compute even on
+specialized hardware.
## Scrypt [recommended]
-The [scrypt](https://en.wikipedia.org/wiki/Scrypt) hash function uses large amounts of memory when hashing
+The [scrypt](https://www.tarsnap.com/scrypt.html) hash function uses large amounts of memory when hashing
making it expensive to scale to the point of reasonable bruteforce
-attacks.
+attacks. Secure against hardware brute-force attacks.
A number of cryptocurrencies use __scrypt__ for proof of work.
@@ -46,7 +46,8 @@ Created by Colin Percival of [Tarsnap](https://en.wikipedia.org/wiki/Tarsnap)
## Argon2d [recommended]
The [Argon2d](https://en.wikipedia.org/wiki/Argon2) function is
-designed to resist GPU cracking attacks.
+designed to resist GPU cracking attacks. Secure against hardware
+brute-force attacks.
It is the winner of [Password Hashing Competition](https://www.password-hashing.net/).
@@ -55,12 +56,15 @@ It is the winner of [Password Hashing Competition](https://www.password-hashing.
[Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) is based on the
[blowfish](https://en.wikipedia.org/wiki/Blowfish_(cipher)) cipher.
+Vulnerable against hardware brute-force attacks.
+
## PBKDF2
[PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) is an key derivation
function with a sliding computational cost to reduce bruteforce
search.
+Vulnerable against hardware brute-force attacks.
## Conclusion
diff --git a/src/lib.rs b/src/lib.rs
@@ -5,21 +5,16 @@ use chacha20poly1305::{
};
use ring::{
digest::{self, digest, Digest},
- pbkdf2,
rand::SystemRandom,
signature::{Ed25519KeyPair, KeyPair},
};
-use std::num::NonZeroU32;
+use scrypt::{
+ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
+ Scrypt,
+};
-static PBKDF2_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
const CREDENTIAL_LEN: usize = digest::SHA256_OUTPUT_LEN;
-const PBKDF2_ITERATIONS: u32 = 100_000;
-/// Database seed used to generate password hashes,
-/// so that an attacker cannot crack the same user's password across
-/// databases in the unfortunate but common case that the user has
-/// used the same password for multiple systems.
-const DATABASE_SEED: &str =
- "The cipher has an interesting history: although its true origins are unknown";
+
/// Nonce used when encrypting the Crypto's keypair (Crypto::encrypt_keypair_document())
const AED_NONCE: &str = "people the information ";
pub type Credential = [u8; CREDENTIAL_LEN];
@@ -52,57 +47,28 @@ impl Crypto {
Ok(cred)
}
- /// Generate pbkdf2 based hashed password
- pub fn hash_password(username: &str, password: &str) -> Result<String> {
- let pbkdf2_iterations: NonZeroU32 = NonZeroU32::new(PBKDF2_ITERATIONS).unwrap();
- let salt = Crypto::gen_password_salt(username);
- let mut to_store: Credential = [0u8; CREDENTIAL_LEN];
- pbkdf2::derive(
- PBKDF2_ALG,
- pbkdf2_iterations,
- &salt,
- password.as_bytes(),
- &mut to_store,
- );
-
- let hash = base64::encode(to_store);
- Ok(hash)
+ /// Generate scrypt based hashed password
+ pub fn hash_password(password: &str) -> Result<String> {
+ let salt = SaltString::generate(&mut OsRng);
+ Ok(Scrypt
+ .hash_password(password.as_bytes(), &salt)?
+ .to_string())
}
- /// Verify pbkdf2 based password hash
- fn verify_password_hash(username: &str, attempted_password: &str, hash: &str) -> Result<bool> {
- let pbkdf2_iterations: NonZeroU32 = NonZeroU32::new(PBKDF2_ITERATIONS).unwrap();
- let salt = Crypto::gen_password_salt(username);
- let hash_bytes = base64::decode(hash)?;
- let res = pbkdf2::verify(
- PBKDF2_ALG,
- pbkdf2_iterations,
- &salt,
- attempted_password.as_bytes(),
- &hash_bytes,
- );
-
- match res {
- Ok(()) => Ok(true),
- Err(_e) => Ok(false),
+ /// Verify scrypt based password hash
+ fn verify_password_hash(attempted_password: &str, hash: &str) -> Result<bool> {
+ let parsed_hash = PasswordHash::new(hash)?;
+
+ if Scrypt
+ .verify_password(attempted_password.as_bytes(), &parsed_hash)
+ .is_ok()
+ {
+ Ok(true)
+ } else {
+ Ok(false)
}
}
- /// Generate password salt
- /// The salt should have a user-specific component so that an attacker
- /// cannot crack one password for multiple users in the database. It
- /// should have a database-unique component so that an attacker cannot
- /// crack the same user's password across databases in the unfortunate
- /// but common case that the user has used the same password for
- /// multiple systems.
- fn gen_password_salt(username: &str) -> Vec<u8> {
- let db_salt_component = DATABASE_SEED.as_bytes();
- let mut salt = Vec::with_capacity(db_salt_component.len() + username.as_bytes().len());
- salt.extend(db_salt_component.as_ref());
- salt.extend(username.as_bytes());
- salt
- }
-
/// Encrypt the keypair (ed25519) document, using XChaCha20Poly1305
/// so that it can be safely persisted into the database. The
/// Crypto's login password is used a the secret key.
@@ -147,16 +113,14 @@ mod tests {
#[test]
fn hash_password() {
let password1 = "a bad password";
- let username1 = "goldephoenix";
- let hash1 = Crypto::hash_password(username1, password1).unwrap();
+ let hash1 = Crypto::hash_password(password1).unwrap();
let password2 = "a bad password";
- let username2 = "alexis";
- let hash2 = Crypto::hash_password(username2, password2).unwrap();
+ let hash2 = Crypto::hash_password(password2).unwrap();
- // The generated hashes are all 32 bytes long
- assert_eq!(hash1.len(), 44);
- assert_eq!(hash2.len(), 44);
+ // The generated hashes are all 88 bytes long
+ assert_eq!(hash1.len(), 88);
+ assert_eq!(hash2.len(), 88);
// Even if the user passwords are the same they do not generate
// the same hashes because the username is also used as a
@@ -164,8 +128,8 @@ mod tests {
assert_ne!(hash1, hash2);
// Verify password
- assert!(Crypto::verify_password_hash(username1, "a bad password", &hash1).unwrap());
- assert!(!Crypto::verify_password_hash(username1, "wrong password", &hash1).unwrap());
+ assert!(Crypto::verify_password_hash("a bad password", &hash1).unwrap());
+ assert!(!Crypto::verify_password_hash("wrong password", &hash1).unwrap());
}
#[test]