Skip to main content
  1. Posts/

Password Security: Why Hashing is Essential

Sven Ruppert
Author
Sven Ruppert
20+ years of Java, specialised in Security, Vaadin and Developer Relations. When not coding, you’ll find me in the woods with an axe.
Table of Contents

Password security is an often underestimated but critical topic in software development. Databases containing millions of user logins are repeatedly compromised - and shockingly, often, it turns out that passwords have been stored in plain text. This gives attackers direct access to sensitive account data and opens the door to identity theft, account takeovers and other attacks.

In this blog post, we discuss why passwords should always be stored hashed, the attack methods available, and how you can implement an initial secure implementation with Java in your application. We also examine the differences between PBKDF2, BCrypt, and Argon2 and explain best practices for handling passwords in software development.

  1. Passwords and hashing
  2. What are brute force and rainbow table attacks on passwords?
  3. And how do I do this now in my application?
  4. How safe is PBKDF2?
  5. Using stronger hashing algorithms like Argon2
  6. Application of Argon2 in Java
  7. Generate the SALT value.
  8. What is the procedure for checking the username-password combination during the login process?
  9. A few more words about strings in Java
    1. So what can you do about it now?
  10. Conclusion

Passwords and hashing
#

Passwords should never be stored in plain text but should always be hashed to ensure the security of user data and prevent misuse. If a database is compromised and passwords are only stored in plain text, attackers have direct access to users’ sensitive credentials. This can have serious consequences, as many people use the same password for multiple services. Hashing passwords significantly reduces this risk because attackers only see the hash values ​​, not the actual ones.

A key advantage of hashing is its one-way function. A hash value can be generated from a password, but inferring the original password is virtually impossible. This makes it extremely difficult to misuse the access data resulting from a data leak. This protection mechanism applies not only to external attacks but also to internal security risks. If passwords are stored in plain text, employees with access to the database, for example, could view and misuse this information. Hashed passwords largely eliminate such insider threats.

In addition, compliance with legal requirements and security standards such as the General Data Protection Regulation (GDPR) or the Payment Card Industry Data Security Standard (PCI-DSS) requires the protection of passwords. Hashing passwords is important to meet these standards and avoid legal consequences.

What are brute force and rainbow table attacks on passwords?
#

Brute force attacks and rainbow table attacks are two methods attackers use to decrypt passwords or other secrets. A Brute force attack works by systematically trying every possible password combination until finding the right one. We start with the simplest combinations, such as “aaa” or “1234”, and check every possible variant. Although brute force attacks can theoretically always be successful, their effectiveness depends heavily on the complexity of the password and computing power. A short password can be cracked in a few seconds, while a long and complex password increases the effort significantly. Measures such as longer passwords, limiting the number of login attempts per unit of time (e.g. blocking after several failed attempts) and the use of hashing algorithms with many iterations make brute force attacks significantly more difficult and time-consuming.

In contrast, the Rainbow table attacks pre-built tables that contain many hash values ​​and their associated plaintext passwords. Attackers compare the stored hash of a password with the hashes in the table to find the original password. This method is significantly faster than brute force because the passwords must not be re-hashed every time. However, rainbow tables only work if the hashing method does not use a salt value. A salt value is a random value added to each password hash, so even identical passwords produce different hashes. Without salt, attackers could use the same table across many other systems, but this is no longer possible with salt.

Using modern hashing algorithms such as PBKDF2, BCrypt, or Argon2 is crucial to protecting yourself from both attack methods. These algorithms combine salts and multiple iterations to significantly increase attacker effort. Additionally, long and complex passwords make brute-force attacks virtually impossible as the number of possible combinations increases exponentially. In summary, combining strong passwords, salts, and secure hashing algorithms effectively defends against brute force and rainbow table attacks.

And how do I do this now in my application?
#

To securely hash a password in Java, it is recommended to use a specialised hash function like PBKDF2 , BCrypt , or Argon2. These algorithms are specifically designed to make attacks such as brute force or rainbow table attacks more difficult. One way to implement this with the Java standard library is to use PBKDF2.

First, a Salt is generated, a random sequence of bytes generated for each password individually to ensure that two identical passwords have different hashes. The class SecureRandom can be used to create a 16-byte salt. Then, with the class PBEKeySpec, a key is generated based on the password, the salt, the desired number of iterations (e.g. 65,536) and the key length (e.g. 256 bits). With the help of SecretKeyFactory and the algorithm specification “PBKDF2WithHmacSHA256” the password hash is created. The resulting hash is finally encoded in Base64 to store it in a readable form.

The hashed password is often stored along with the salt, separated by a colon (:). This makes it easier to verify later by extracting the salt again and using it for the hash calculation. In addition, there are also external libraries such as Spring Security or BouncyCastle , which allow easier integration of algorithms like BCrypt or Argon2 make possible. These often offer even more security and flexibility.

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class PasswordHasher {
    private static final int ITERATIONS = 65536;
    private static final int KEY_LENGTH = 256;
    private static final String ALGORITHM = "PBKDF2WithHmacSHA256";

    public static String hashPassword(String password, String salt) {
        try {
            KeySpec spec = new PBEKeySpec(
                                 password.toCharArray(), 
                                 salt.getBytes(), 
                                 ITERATIONS, 
                                 KEY_LENGTH);
            SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
            byte[] hash = factory.generateSecret(spec).getEncoded();
            return Base64.getEncoder().encodeToString(hash);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException("Error while hashing password", e);
        }
    }

    public static void main(String[] args) {
        String password = "myPassword";
        String salt = "randomSalt";

        String hashedPassword = hashPassword(password, salt);
        System.out.println("hasehd password: " + hashedPassword);
    }
}

However, there are a few points here that you should take a closer look at.

How safe is PBKDF2?
#

PBKDF2 is a proven and secure password hashing algorithm, but its security depends heavily on the correct implementation and parameters used. Various factors influence the safety of PBKDF2. A key aspect is the number of iterations, representing the work factor. The higher the number of iterations, the more computing power is required for hashing. Experts recommend at least 100,000 iterations, although even higher values ​​are often chosen for modern applications to make brute-force attacks more difficult. Another crucial factor is the salt value, a random and unique value that is regenerated for each password. In addition, PBKDF2 can be configured with a variable key length, for example, 256 bits, which increases security. Modern applications often use hash functions such as SHA-256 or SHA-512.

PBKDF2 has several strengths. The algorithm is proven and has been used for years in security-critical applications such as WPA2 and encrypted storage systems. It is easy to implement as extensive support in many programming languages ​​and libraries is based on a recognised standard defined in RFC 8018. Nevertheless, PBKDF2 also has weaknesses. The algorithm is not optimised explicitly against GPU or ASIC-based attacks, allowing attackers with specialised hardware to efficiently carry out brute force attacks. Compared to modern algorithms such as Argon2, PBKDF2 requires a higher number of iterations to achieve similar levels of security. In addition, the further development of PBKDF2 is progressing more slowly, which means it is considered less adapted to current threats than modern alternatives such as Argon2.

However, PBKDF2 can be used safely if a few key conditions are met: a unique salt value, the iteration count should be at least 100,000 (ideally more), and a modern hash function such as SHA-256 should be used. In addition, strong password guidelines, such as a sufficient minimum length, should complement security.

Argon2 is increasingly recommended as an alternative to PBKDF2. Argon2, the Password Hashing Competition (PHC) winner in 2015, is more modern and better adapted to current threats. Its memory intensity provides better protection against GPU and ASIC attacks and offers flexible configuration options regarding work factor, memory requirements and parallelism.

In summary, PBKDF2 is secure when implemented correctly but has weaknesses compared to specialised hardware. Argon2 is preferable for new applications because it is better suited to modern security requirements.

Using stronger hashing algorithms like Argon2
#

Argon2 is a modern algorithm for secure password hashing and was developed in 2015 as part of the Password Hashing Competition (PHC). It is considered one of the most secure approaches to storing passwords today and, as already mentioned, offers adequate protection against brute force attacks as well as attacks using GPUs or specialised hardware solutions such as ASICs. This is achieved due to the fact that Argon2 is both memory and compute-intensive, forcing attackers with parallel hardware to expend significant resources.

Argon2 exists in three variants, each optimised for different use cases. The first variant, Argon2i , is designed to be particularly memory-safe. It performs memory-intensive operations independently of the input data and is resistant to side-channel attacks such as timing attacks. This makes Argon2i ideal for applications where data protection is a top priority.

The second variant, Argon2d , is specifically optimised for attack resistance. It works data-dependently, which makes it particularly robust against GPU-based attacks. However, Argon2d is more vulnerable to side-channel attacks and is, therefore, less suitable for securing sensitive data.

The third variant, Argon2id, offers a balanced combination of both approaches. It starts with a memory-safe approach, like that used by Argon2i, and then moves to a data-dependent process that ensures attack resistance. This mix makes Argon2id the preferred choice for most use cases, as it combines data protection and attack resilience.

One of Argon2’s key strengths is its customizability. The algorithm allows developers to configure three primary parameters: memory consumption, computational cost and parallelism. Memory consumption defines how much memory is used during the hashing operation and makes parallel hardware attacks expensive. Computational cost indicates the number of iterations the algorithm goes through, while parallelism determines the number of threads working simultaneously. By adjusting these parameters, the algorithm can be tailored to the specific requirements of an application or the available hardware.

Another advantage of Argon2 is its resistance to modern attacks. The combination of memory and computing-intensive processes makes it difficult for attackers to crack passwords using brute force or specialised hardware. This makes Argon2 ideal for use in safety-critical applications.

Argon2 is used in many modern cryptography and password management libraries. Developers can implement the algorithm in various programming languages ​​, such as Java, Python, or C.

In summary, Argon2 is one of the safest password-hashing algorithms. The variant Argon2id is especially recommended as a standard for new projects because it provides protection against both side-channel attacks and high attack resistance.

Application of Argon2 in Java
#

One Argon2 in Java To use it, you can use libraries like Jargon2 or Bouncy Castle because Java does not natively support Argon2. With Jargon2 , Argon2 is very easy to integrate and use. To do this, first, add the following Maven dependency:

<dependency>
    <groupId>com.kosprov</groupId>
    <artifactId>jargon2-api</artifactId>
    <version>Latest Version Number</version>
</dependency>

The code to create a hash and verify a password might look like this:

import com.kosprov.jargon2.api.Jargon2;

public class Argon2Example {
    public static void main(String[] args) {
        String password = "DeinSicheresPasswort";

        Jargon2.Hasher hasher = Jargon2.jargon2Hasher()
                .type(Jargon2.Type.ARGON2id)
                .memoryCost(65536)
                .timeCost(3)
                .parallelism(4)
                .saltLength(16)
                .hashLength(32);

        String hashedPassword = hasher
                                 .password(password.getBytes())
                                 .encodedHash();
        System.out.println("hashed password: " + hashedPassword);

        boolean matches = Jargon2.jargon2Verifier()
                .hash(hashedPassword)
                .password(password.getBytes())
                .verifyEncoded();
        System.out.println("password correct: " + matches);
    }
}

If you want to use a more comprehensive cryptography library, you can use Bouncy Castle. The corresponding Maven dependency is:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>Latest Version Number</version>
</dependency>

An example of using Argon2 with Bouncy Castle looks like this:

import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import java.security.SecureRandom;

public class Argon2BouncyCastleExample {
    public static void main(String[] args) {
        String password = "securePassword";
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);

        Argon2Parameters.Builder builder = new Argon2Parameters
                .Builder(Argon2Parameters.ARGON2_id)
                .withSalt(salt)
                .withMemoryPowOfTwo(16)
                .withParallelism(4)
                .withIterations(3);

        Argon2BytesGenerator generator 
                               = new Argon2BytesGenerator();
        generator.init(builder.build());

        byte[] hash = new byte[32];
        generator.generateBytes(password.getBytes(), hash);

        System.out.println("hashed password: " 
                              + bytesToHex(hash));
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : bytes) {
            hexString.append(String.format("%02x", b));
        }
        return hexString.toString();
    }
}

Jargon2 was developed specifically for Argon2 and is, therefore, easy to use Bouncy Castle for more complex cryptographic requirements. It is recommended in most cases for the pure use of Argon2.

Generate the SALT value.
#

So far, it has always been mentioned that producing a good SALT value is important. But how can you do that?

We will examine the topic in detail in the next blog post. Unfortunately, at this point, it would go beyond the scope of this article.

In this blog post, we will examine a basic initial implementation. You will quickly be confronted with an implementation that could look like this.

public String generateSalt(int length) { 
    SecureRandom random = new SecureRandom(); 
    byte[] salt = new byte[length]; 
    random.nextBytes(salt); 
    return Base64.getEncoder().encodeToString(salt); 
}

However, there are some minor comments here.

Basically, creating a salt with a newly instantiated SecureRandom is not fundamentally wrong in any method, but it increases the chance that the same seeds are used at very short intervals or under certain circumstances. In practice, that is rarely a problem; however, it is good practice to have a single (static) instance of SecureRandom to be used per application (or per class).

Why?

  • SecureRandom gets its seed (among other things) from the operating system (e.g /dev/urandom in Linux).
  • Each re-creation of one SecureRandom can lead to unnecessary system load and, theoretically, minimal increased risk of repetitions.
  • At just one SecureRandominstance, a pseudo-random number generator is continued internally, making duplicates very unlikely.
import java.security.SecureRandom;
import java.util.Base64;

public class SaltGenerator {

    private static final SecureRandom RANDOM 
                = new SecureRandom();

    public static String generateSalt(int length) {
        byte[] salt = new byte[length];
        RANDOM.nextBytes(salt);
        return Base64.getEncoder().encodeToString(salt);
    }
}

This way, (a) is not repeated every time it is called SecureRandom, and (b) reduces the risk of randomly generating identical salts. Of course, purely statistically speaking, collisions can theoretically still occur, but the probability is negligible if the salt length is long enough.

However, this is only the beginning of the discussion; more on that in the second part.

What is the procedure for checking the username-password combination during the login process?
#

A standardised procedure is followed to check the combination of user name and password during a login process, ensuring that the data is processed securely. First, the user enters their login details via a form transmitted over HTTPS. The server then looks up the username in the database to retrieve relevant information such as password hash and salt. Care should be taken not to reveal whether a user exists.

The stored password hash is then compared with a recalculated hash of the entered password using the stored salt. If the values ​​match, the login is considered successful; otherwise, a generic error message is returned so as not to reveal additional information.

After successful verification, a session or JSON Web Token (JWT) is created to maintain the user’s authentication. The token does not contain any sensitive information, such as passwords.

A few more words about strings in Java
#

If we imagine that a password is in a text field of a (web) application, we will receive this password as a string. Yes, some will say, but what could be bad about that?

To do this, we need to examine briefly how strings are handled in the JVM and where attack vectors can lie.

In Java, strings are immutable, which means that once created, String-Object can no longer be changed. When a string is manipulated through concatenation, a new string object is created in memory while the old one continues to exist until it is removed by the garbage collector (GC). This behaviour can be problematic when sensitive data, such as passwords or cryptographic keys, is stored in strings. Because they cannot be overwritten directly, they may remain in memory longer than necessary and are potentially visible in a memory dump or readable by an attacker. Another problem is that the developer has no control over when the garbage collector removes the sensitive data. This can cause such data to remain in memory for an extended period and possibly become visible in logs or debugging tools.

So what can you do about it now?
#

A safer approach is to use this instead of strings char[] because they are changeable, and the memory can be specifically overwritten. This helps minimise the retention time of sensitive data, especially in security-critical applications where memory dumps or debugging tools could provide memory access.

The advantage of char[] lies in direct memory management: While the garbage collector decides itself when to remove objects, a char[]-Array is explicitly overwritten and thus immediately deleted. This significantly reduces the risk of unauthorised access.

import java.util.Arrays;

public class SensitiveDataExample {
    public static void main(String[] args) {
        char[] password = {'s', 'e', 'c', 'r', 'e', 't'};
        try {
            processPassword(password);
        } finally {
            // Überschreiben des Speichers mit Dummy-Daten
            Arrays.fill(password, '\0');
        }
    }
    private static void processPassword(char[] password) {
        // Beispielhafte Verarbeitung
        System.out.println("Passwort verarbeitet: " 
                 + String.valueOf(password));
   }
}

Next to char[], additional security measures should be taken, such as encrypting sensitive data and using SecretKeySpec from the package javax.crypto.spec. for cryptographic keys. This class allows handling cryptographic keys in byte arrays that can be overwritten after use, but I will write a separate blog post about that…

Conclusion
#

Secure storage of passwords is an essential part of every application to protect user data from misuse and unauthorised access. Plain text passwords pose a significant risk and should never be saved directly. Instead, modern hashing algorithms such as PBKDF2, BCrypt or Argon2 must be used to ensure security.

Brute force and rainbow table attacks illustrate the importance of salts and sufficiently complex hashing mechanisms. The risk of such attacks can be significantly reduced by using random salts and iterative hashing methods. Argon2id, in particular, offers high resistance to modern attack methods due to its storage and computing intensity and is recommended as the preferred solution.

In addition, passwords within the application should also be processed carefully. Using char[] instead of String to store sensitive data can help prevent unwanted memory leaks. It is also important not to store passwords unsecured in memory or in logs.

Secure password hashing procedures are a technical best practice and a legal requirement in many areas.

Related

CWE-778: Lack of control over error reporting in Java

Learn how inadequate control over error reporting leads to security vulnerabilities and how to prevent them in Java applications. # Safely handling error reports is a central aspect of software development, especially in safety-critical applications. CWE-778 describes a vulnerability caused by inadequate control over error reports. This post will analyse the risks associated with CWE-778 and show how developers can implement safe error-handling practices to avoid such vulnerabilities in Java programs.

CWE-416: Use After Free Vulnerabilities in Java

CWE-416: Use After Free # Use After Free (UAF) is a vulnerability that occurs when a program continues to use a pointer after it has been freed. This can lead to undefined behaviour, including crashes, data corruption, and security vulnerabilities. The problem arises because the memory referenced by the pointer may be reallocated for other purposes, potentially allowing attackers to exploit the situation.

CWE-787 - The Bird-Eye View for Java Developers

The term “CWE-787: Out-of-bounds Write " likely refers to a specific security vulnerability or error in software systems. Let’s break down what it means: Out-of-bounds Write : This is a type of vulnerability where a program writes data outside the boundaries of pre-allocated fixed-length buffers. This can corrupt data, crash the program, or lead to the execution of malicious code.