This article will act as an implementation guide for Argon2 the memory-hard password hashing algorithm in real world application. It will provide practical steps for implementing Argon2 such as choosing libraries, generating secure salts, code example, managing edge cases, also best practices for security and performance considerations. What Is Argon2 & Why It Matter? What is so special about it? Another cryptographic algorithm with a fancy name added to your list of things to tediously care about. Password Hashing Champion Of course, the Password Hashing Competition prize was not given to Argon2 randomly; it deserved the crown by eliminating the very core problems afflicting the older hashing algorithms. Created by Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich, Argon2 was specifically designed to counter the most elegant attack vectors that we have today. This contest was not for ego-building but to solve actual world problems. Password Hashing Competition Why Argon2 Outshine Older Algorithms I am not judging you if you still use bcrypt or PBKDF2 (ok, a bit if you still use md5, though). These algorithms did well back in the day when memory was at a peak premium and hardware-specialized attacks weren't as sophisticated. Here's why Argon2 can outshine them: Memory-hardness: Argon2 uses huge amounts of memory while hashing. Therefore, scalable use of GPUs or ASICs cannot be relied upon by attackers in cracking passwords, which call for memory that is very expensive in parallel computing environments. Tunable parameters: You can adjust on memory capacity usage, parallelism and execution time to your particular security requirements and constraints of hardware. Defense-in-depth: Designed to resist not only brute-force attacks, but also side-channel attacks, time-memory trade-offs, and many other complicated ways hackers break passwords. Memory-hardness: Argon2 uses huge amounts of memory while hashing. Therefore, scalable use of GPUs or ASICs cannot be relied upon by attackers in cracking passwords, which call for memory that is very expensive in parallel computing environments. Memory-hardness: Tunable parameters: You can adjust on memory capacity usage, parallelism and execution time to your particular security requirements and constraints of hardware. Tunable parameters: Defense-in-depth: Designed to resist not only brute-force attacks, but also side-channel attacks, time-memory trade-offs, and many other complicated ways hackers break passwords. Defense-in-depth: That is its key innovation: memory-hardness, compared to bcrypt which use a fix small amount of memory, and PBKDF2 which use almost none. Argon2 can use gigabytes of RAM if you configure it to do so. This makes it costly for even well-funded password cracking. Setting Argon2 Up: Choosing the Best Tools You have chosen to strengthen the security of your passwords to the gold standard—this is indeed the right decision! Now you must delve into the various Argon2 implementations and select the suitable layer libraries for your environment. The Three Flavors of Argon2 Not to be confused with a single algorithm, Argon2 comprises three distinct variants, each with its own strengths and use cases: Argon2d: The speedster of the family, providing the highest resistance to GPU cracking attacks by accessing the memory array in a data-dependent way, rendering its dependencies harder for the aggressors to achieve parallelism. However, these very dependency are rendering it susceptible to side-channel timing attacks. Best use: Applications in which side-channel attacks are unlikely, such as in cryptocurrency mining or backendd applications in controlled environment. Argon2i: The paranoid prince, It accesses memory memory via a data-independent pattern to explicitly defend against any side-channel leak. While slightly slower than Argon2d against GPU attacks, it's your go-to when you're worried about more sophisticated timing attacks. Best: Frontend password hashing and situations in which the hashing process may be viewed by some potential attacker. Argon2id: The hybrid solution and the one most often recommended. This approach utilizes Argon2i for the first few passes before switching to Argon2d, combining the benefits of both designs to fend off side-channel attacks and GPU cracking alike. Best: Just about anything in production, especially when you can't decide. Argon2d: The speedster of the family, providing the highest resistance to GPU cracking attacks by accessing the memory array in a data-dependent way, rendering its dependencies harder for the aggressors to achieve parallelism. However, these very dependency are rendering it susceptible to side-channel timing attacks. Best use: Applications in which side-channel attacks are unlikely, such as in cryptocurrency mining or backendd applications in controlled environment. Argon2d Argon2i: The paranoid prince, It accesses memory memory via a data-independent pattern to explicitly defend against any side-channel leak. While slightly slower than Argon2d against GPU attacks, it's your go-to when you're worried about more sophisticated timing attacks. Best: Frontend password hashing and situations in which the hashing process may be viewed by some potential attacker. Argon2i Argon2id: The hybrid solution and the one most often recommended. This approach utilizes Argon2i for the first few passes before switching to Argon2d, combining the benefits of both designs to fend off side-channel attacks and GPU cracking alike. Best: Just about anything in production, especially when you can't decide. Argon2id Choosing Your Weapons: Argon2 Libraries After picking a variant that fits your needs (when in doubt, go with Argon2id), it's time to choose a library for your programming language of choice. There are several battle-tested implementations: Python Best of the lot in Python-land seems to be the argon2-cffi package, a well-maintained wrapper around C reference implementation. pip install argon2-cffi JavaScript/Node.js In the Node.js world, argon2 is probably the most famous package binding to the C implementation. npm install argon2 Go With several implementations in Go, the clear winner is golang.org/x/crypto/argon2 from the Go Crypto library. go get golang.org/x/crypto/argon2 Python Best of the lot in Python-land seems to be the argon2-cffi package, a well-maintained wrapper around C reference implementation. pip install argon2-cffi Python Python Best of the lot in Python-land seems to be the argon2-cffi package, a well-maintained wrapper around C reference implementation. argon2-cffi pip install argon2-cffi pip install argon2-cffi JavaScript/Node.js In the Node.js world, argon2 is probably the most famous package binding to the C implementation. npm install argon2 JavaScript/Node.js JavaScript/Node.js In the Node.js world, argon2 is probably the most famous package binding to the C implementation. argon2 npm install argon2 npm install argon2 Go With several implementations in Go, the clear winner is golang.org/x/crypto/argon2 from the Go Crypto library. go get golang.org/x/crypto/argon2 Go Go With several implementations in Go, the clear winner is golang.org/x/crypto/argon2 from the Go Crypto library. golang.org/x/crypto/argon2 go get golang.org/x/crypto/argon2 go get golang.org/x/crypto/argon2 And whatever you do, please don't invent your own cryptography implementation. Seriously, I once had a collegue who thought he could "optimize" a hashing algorithm; he's no longer allowed anywhere near our authentication systems. Actual implementation: Hashing Passwords with Argon2 With the theory under control, the next most crucial step is to have the atmosphere charged with the feel of the actual implementation. This is where the rubber meets the road: the code that sets it to work and protects the password of the user. Step 1: Generate a Cryptographically Secure Salt An excellent rule is always to use salts unique to each password. A salt is merely a random string of bytes added to the password before hashing. It foils all attacks that rely on pre-computed data table (rainbow tables) and ensures that even if both users have the same password, it will still be treated differently in terms of hashing. Stay away from using math/rand in go or Python's random module! These are pseudorandom and not for cryptographic purposes. Use these instead: math/rand Python: os.urandom() or secrets module Node.js: crypto.randomBytes() Go: crypto/rand package Python: os.urandom() or secrets module os.urandom() secrets Node.js: crypto.randomBytes() crypto.randomBytes() Go: crypto/rand package crypto/rand A salt length should preferably be 16 bytes or more (128bits) in size. Step 2: Set up Argon2 Parameters This is where the most common mistakes occur for many developers. The three crucial parameters are: Memory Cost: How much memory the algorithm will use. The more the better is for security, but it will also use resources on your server. The measure is in KiB (1024 bytes). Usually, you can set this between 32MiB (32,768 KiB) and some upper range of 1GiB (1,048,576 KiB) based on what your server can handle. Time Cost: The number of iterations or passes over the memory. High is slow but provides more security. Normal values are 1 to something like 10. Parallelism: How many parallel threads should it use? Usually, that should match the number of available CPU cores. Normal choices are 1, 2, 4, or 8. Hash Length: The output size of the hash expressed in bytes. Obviously, this does have implications for security-longer hashes have more resistance to collisions. Typical values go from 16 bytes (128 bits) to 64 bytes (512 bits) with 32 bytes (256 bits) probably being a good standard. Memory Cost: How much memory the algorithm will use. The more the better is for security, but it will also use resources on your server. The measure is in KiB (1024 bytes). Usually, you can set this between 32MiB (32,768 KiB) and some upper range of 1GiB (1,048,576 KiB) based on what your server can handle. Memory Cost: How much memory the algorithm will use. The more the better is for security, but it will also use resources on your server. The measure is in KiB (1024 bytes). Usually, you can set this between 32MiB (32,768 KiB) and some upper range of 1GiB (1,048,576 KiB) based on what your server can handle. Memory Cost Time Cost: The number of iterations or passes over the memory. High is slow but provides more security. Normal values are 1 to something like 10. Time Cost: The number of iterations or passes over the memory. High is slow but provides more security. Normal values are 1 to something like 10. Time Cost Parallelism: How many parallel threads should it use? Usually, that should match the number of available CPU cores. Normal choices are 1, 2, 4, or 8. Parallelism: How many parallel threads should it use? Usually, that should match the number of available CPU cores. Normal choices are 1, 2, 4, or 8. Parallelism Hash Length: The output size of the hash expressed in bytes. Obviously, this does have implications for security-longer hashes have more resistance to collisions. Typical values go from 16 bytes (128 bits) to 64 bytes (512 bits) with 32 bytes (256 bits) probably being a good standard. Hash Length: The output size of the hash expressed in bytes. Obviously, this does have implications for security-longer hashes have more resistance to collisions. Typical values go from 16 bytes (128 bits) to 64 bytes (512 bits) with 32 bytes (256 bits) probably being a good standard. Hash Length What sizing will you go with? If you want a rough method, consider benchmarking it on your server and calibrating your parameters to obtain a hashing time of 250-500ms. Slower for the attackers but quick for the users. Step 3: Call the Argon2 Hash Function Now, we put it all together and hash the password. Let us see how it is done in the three languages that we have chosen: Python Example Python Example import os import base64 import argon2 # Complete hashing function with parameters def hash_password(password): # Configure the algorithm time_cost = 2 # Number of iterations memory_cost = 102400 # 100 MB in KiB parallelism = 8 # Number of parallel threads hash_len = 32 # Length of the hash in bytes salt_len = 16 # Length of the salt in bytes # Create the hasher ph = argon2.PasswordHasher( time_cost=time_cost, memory_cost=memory_cost, parallelism=parallelism, hash_len=hash_len, salt_len=salt_len, type=argon2.Type.ID # Using Argon2id variant ) # Hash the password (salt is generated automatically) hash = ph.hash(password) return hash # Example usage password = "super_secret_password" hash_result = hash_password(password) print(f"Hashed password: {hash_result}") # This will produce something like: # $argon2id$v=19$m=102400,t=2,p=8$RTRrSEl2MTNpSnZ3ZmFpNg$wxJjHFEQpJXsLFO+T5xzHJGkUqJkL7SYgvUB4GQqKyQ # Which includes the algorithm, version, parameters, salt, and hash import os import base64 import argon2 # Complete hashing function with parameters def hash_password(password): # Configure the algorithm time_cost = 2 # Number of iterations memory_cost = 102400 # 100 MB in KiB parallelism = 8 # Number of parallel threads hash_len = 32 # Length of the hash in bytes salt_len = 16 # Length of the salt in bytes # Create the hasher ph = argon2.PasswordHasher( time_cost=time_cost, memory_cost=memory_cost, parallelism=parallelism, hash_len=hash_len, salt_len=salt_len, type=argon2.Type.ID # Using Argon2id variant ) # Hash the password (salt is generated automatically) hash = ph.hash(password) return hash # Example usage password = "super_secret_password" hash_result = hash_password(password) print(f"Hashed password: {hash_result}") # This will produce something like: # $argon2id$v=19$m=102400,t=2,p=8$RTRrSEl2MTNpSnZ3ZmFpNg$wxJjHFEQpJXsLFO+T5xzHJGkUqJkL7SYgvUB4GQqKyQ # Which includes the algorithm, version, parameters, salt, and hash Node.js Example Node.js Example const argon2 = require('argon2'); const crypto = require('crypto'); async function hashPassword(password) { // Configure the algorithm const options = { type: argon2.argon2id, // Variant of Argon2 memoryCost: 65536, // 64 MiB timeCost: 2, // 2 passes parallelism: 4, // 4 threads hashLength: 32, // 32 bytes output saltLength: 16, // 16 bytes salt // You can also provide your own salt: // salt: crypto.randomBytes(16) }; try { // Hash the password (salt is generated automatically by default) const hash = await argon2.hash(password, options); return hash; } catch (err) { console.error('Error hashing password:', err); throw err; } } // Example usage hashPassword('super_secret_password') .then(hash => console.log('Hashed password:', hash)) .catch(err => console.error(err)); // This will produce something like: // $argon2id$v=19$m=65536,t=3,p=4$G8NYSxrA+UMGHJbZVIXXXQ$UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8 const argon2 = require('argon2'); const crypto = require('crypto'); async function hashPassword(password) { // Configure the algorithm const options = { type: argon2.argon2id, // Variant of Argon2 memoryCost: 65536, // 64 MiB timeCost: 2, // 2 passes parallelism: 4, // 4 threads hashLength: 32, // 32 bytes output saltLength: 16, // 16 bytes salt // You can also provide your own salt: // salt: crypto.randomBytes(16) }; try { // Hash the password (salt is generated automatically by default) const hash = await argon2.hash(password, options); return hash; } catch (err) { console.error('Error hashing password:', err); throw err; } } // Example usage hashPassword('super_secret_password') .then(hash => console.log('Hashed password:', hash)) .catch(err => console.error(err)); // This will produce something like: // $argon2id$v=19$m=65536,t=3,p=4$G8NYSxrA+UMGHJbZVIXXXQ$UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8 Go Example Go Example package main import ( "crypto/rand" "encoding/base64" "fmt" "golang.org/x/crypto/argon2" "log" ) // A struct to hold the parameters and the generated hash type argon2Hash struct { HashRaw []byte Salt []byte TimeCost uint32 MemoryCost uint32 Threads uint8 KeyLength uint32 } func generateSalt(saltSize uint32) ([]byte, error) { salt := make([]byte, saltSize) _, err := rand.Read(salt) if err != nil { return nil, err } return salt, nil } func hashPassword(password string) (string, error) { // Argon2id parameters params := &argon2Hash{ TimeCost: 2, // Number of passes MemoryCost: 64 * 1024, // 64 MB Threads: 4, // 4 threads KeyLength: 32, // 32 bytes output } // Generate a salt salt, err := generateSalt(16) // 16 bytes salt if err != nil { return "", err } params.Salt = salt // Hash the password params.HashRaw = argon2.IDKey( []byte(password), params.Salt, params.TimeCost, params.MemoryCost, params.Threads, params.KeyLength, ) // Encode parameters, salt, and hash in a standard format // Note: In Go, we need to build this format ourselves unlike the other libraries encodedHash := fmt.Sprintf( "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.MemoryCost, params.TimeCost, params.Threads, base64.RawStdEncoding.EncodeToString(params.Salt), base64.RawStdEncoding.EncodeToString(params.HashRaw), ) return encodedHash, nil } func main() { hash, err := hashPassword("super_secret_password") if err != nil { log.Fatal(err) } fmt.Println("Hashed password:", hash) // Example output: // $argon2id$v=19$m=65536,t=1,p=4$c2FsdHNhbHRzYWx0c2FsdA$PasswordHashHere... } package main import ( "crypto/rand" "encoding/base64" "fmt" "golang.org/x/crypto/argon2" "log" ) // A struct to hold the parameters and the generated hash type argon2Hash struct { HashRaw []byte Salt []byte TimeCost uint32 MemoryCost uint32 Threads uint8 KeyLength uint32 } func generateSalt(saltSize uint32) ([]byte, error) { salt := make([]byte, saltSize) _, err := rand.Read(salt) if err != nil { return nil, err } return salt, nil } func hashPassword(password string) (string, error) { // Argon2id parameters params := &argon2Hash{ TimeCost: 2, // Number of passes MemoryCost: 64 * 1024, // 64 MB Threads: 4, // 4 threads KeyLength: 32, // 32 bytes output } // Generate a salt salt, err := generateSalt(16) // 16 bytes salt if err != nil { return "", err } params.Salt = salt // Hash the password params.HashRaw = argon2.IDKey( []byte(password), params.Salt, params.TimeCost, params.MemoryCost, params.Threads, params.KeyLength, ) // Encode parameters, salt, and hash in a standard format // Note: In Go, we need to build this format ourselves unlike the other libraries encodedHash := fmt.Sprintf( "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.MemoryCost, params.TimeCost, params.Threads, base64.RawStdEncoding.EncodeToString(params.Salt), base64.RawStdEncoding.EncodeToString(params.HashRaw), ) return encodedHash, nil } func main() { hash, err := hashPassword("super_secret_password") if err != nil { log.Fatal(err) } fmt.Println("Hashed password:", hash) // Example output: // $argon2id$v=19$m=65536,t=1,p=4$c2FsdHNhbHRzYWx0c2FsdA$PasswordHashHere... } Understanding the Output Format Argon2 libraries produce an output which normally adheres to the standard format, such as: $argon2id$v=19$m=65536,t=3,p=4$G8NYSxrA+UMGHJbZVIXXXQ$UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8 $argon2id$v=19$m=65536,t=3,p=4$G8NYSxrA+UMGHJbZVIXXXQ$UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8 Let's firstly decode the string: $argon2id: The variant of the algorithm (Argon2id) v=19: Version of the Argon2 algorithm (19 is the standard for now) m=65536,t=3,p=4: Parameters (memory=65536 KiB, time=3 iterations, parallelism=4 threads) G8NYSxrA+UMGHJbZVIXXXQ: Base64-encoded salt UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8: Base64-encoded hash (32 bytes/256 bits in this example) $argon2id: The variant of the algorithm (Argon2id) $argon2id v=19: Version of the Argon2 algorithm (19 is the standard for now) v=19 m=65536,t=3,p=4: Parameters (memory=65536 KiB, time=3 iterations, parallelism=4 threads) m=65536,t=3,p=4 G8NYSxrA+UMGHJbZVIXXXQ: Base64-encoded salt G8NYSxrA+UMGHJbZVIXXXQ UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8: Base64-encoded hash (32 bytes/256 bits in this example) UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8 The beauty of this format is that it contains all the parameters in the one string needed to verify the password later without storing them elsewhere. Best Practice for Implementing Argon2: Don't store raw parameters separately. Everything you need is already encoded in the hash itself. Always benchmark with your real production hardware. What works fine on your development machine may be too slow or weak for your production environment. Parameters must evolve over time. As time progresses, while the hardware becomes faster, you will have to increase memory and time costs. Most libraries provide some way to "upgrade" a hash when the user performs a login. Set a maximum password length. Although Argon2 can handle passwords of arbitrary lengths, enrolment of excessively long input might be used to mount denial-of-service attacks. Restrict it to the reasonable range of about 16 - 64 characters. Don't reinvent the wheel. Rather, utilize a well-tested library instead of implementing the algorithm on your own. Test your implementation. Create test cases with known passwords and confirm that valid passwords authenticate and invalid passwords do not. Don't store raw parameters separately. Everything you need is already encoded in the hash itself. Don't store raw parameters separately Always benchmark with your real production hardware. What works fine on your development machine may be too slow or weak for your production environment. Always benchmark with your real production hardware Parameters must evolve over time. As time progresses, while the hardware becomes faster, you will have to increase memory and time costs. Most libraries provide some way to "upgrade" a hash when the user performs a login. Parameters must evolve over time Set a maximum password length. Although Argon2 can handle passwords of arbitrary lengths, enrolment of excessively long input might be used to mount denial-of-service attacks. Restrict it to the reasonable range of about 16 - 64 characters. Set a maximum password length Don't reinvent the wheel. Rather, utilize a well-tested library instead of implementing the algorithm on your own. Don't reinvent the wheel Test your implementation. Create test cases with known passwords and confirm that valid passwords authenticate and invalid passwords do not. Test your implementation Keep in mind that the objective is not implementing Argon2, but implementing it correctly. An Argon2 poorly configured could perform worse than a well-configured bcrypt. Invest the time to learn the parameters and their security and performance implications. Verifying Passwords Securely Generating password hashes securely is just half of the approach. The other half involves verifying passwords provided by the users against the hashes stored without compromising security during the verification procedure. Let's see how we correct this. The Verification Process Password verification using Argon2 involves a straightforward set of steps: The parameters, salt, and hash are extracted from the stored hash string. Use the same parameters and salt to hash the plain password. The resulting hash from step 2 is compared to the hash stored in the database with a constant time comparison. The parameters, salt, and hash are extracted from the stored hash string. Use the same parameters and salt to hash the plain password. The resulting hash from step 2 is compared to the hash stored in the database with a constant time comparison. Most widely used Argon2 libraries will, thankfully, handle the whole business for you, making the verification relatively easy. Let's see how verification works in code examples. Python Example Python Example from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError, InvalidHash def verify_password(stored_hash, provided_password): ph = PasswordHasher() try: # The verify method returns True if the password matches # It raises an exception if the password doesn't match ph.verify(stored_hash, provided_password) return True except VerifyMismatchError: # Password doesn't match return False except InvalidHash: # The stored hash has an invalid format print("Invalid hash format. The hash may be corrupted.") return False # Example usage stored_hash = "$argon2id$v=19$m=102400,t=2,p=8$RTRrSEl2MTNpSnZ3ZmFpNg$wxJjHFEQpJXsLFO+T5xzHJGkUqJkL7SYgvUB4GQqKyQ" is_valid = verify_password(stored_hash, "super_secret_password") if is_valid: print("Password is correct!") else: print("Password is incorrect!") from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError, InvalidHash def verify_password(stored_hash, provided_password): ph = PasswordHasher() try: # The verify method returns True if the password matches # It raises an exception if the password doesn't match ph.verify(stored_hash, provided_password) return True except VerifyMismatchError: # Password doesn't match return False except InvalidHash: # The stored hash has an invalid format print("Invalid hash format. The hash may be corrupted.") return False # Example usage stored_hash = "$argon2id$v=19$m=102400,t=2,p=8$RTRrSEl2MTNpSnZ3ZmFpNg$wxJjHFEQpJXsLFO+T5xzHJGkUqJkL7SYgvUB4GQqKyQ" is_valid = verify_password(stored_hash, "super_secret_password") if is_valid: print("Password is correct!") else: print("Password is incorrect!") JavaScript/Node.js Example JavaScript/Node.js Example const argon2 = require('argon2'); async function verifyPassword(storedHash, providedPassword) { try { // The verify function returns true if the password matches // It returns false if the password doesn't match const isValid = await argon2.verify(storedHash, providedPassword); return isValid; } catch (err) { // Handle errors like invalid hash format console.error('Error during password verification:', err); return false; } } // Example usage const storedHash = '$argon2id$v=19$m=65536,t=3,p=4$G8NYSxrA+UMGHJbZVIXXXQ$UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8'; verifyPassword(storedHash, 'super_secret_password') .then(isValid => { if (isValid) { console.log('Password is correct!'); } else { console.log('Password is incorrect!'); } }) .catch(err => console.error(err)); const argon2 = require('argon2'); async function verifyPassword(storedHash, providedPassword) { try { // The verify function returns true if the password matches // It returns false if the password doesn't match const isValid = await argon2.verify(storedHash, providedPassword); return isValid; } catch (err) { // Handle errors like invalid hash format console.error('Error during password verification:', err); return false; } } // Example usage const storedHash = '$argon2id$v=19$m=65536,t=3,p=4$G8NYSxrA+UMGHJbZVIXXXQ$UrHyBcYfCEms+92QVzGmfYqrWtH54WJY9FuROBQi/X8'; verifyPassword(storedHash, 'super_secret_password') .then(isValid => { if (isValid) { console.log('Password is correct!'); } else { console.log('Password is incorrect!'); } }) .catch(err => console.error(err)); Go Example Go Example package main import ( "crypto/subtle" "encoding/base64" "errors" "fmt" "golang.org/x/crypto/argon2" "strings" ) // Parse the Argon2 hash string into its components func parseHash(encodedHash string) (params *argon2Hash, err error) { vals := strings.Split(encodedHash, "$") if len(vals) != 6 { return nil, errors.New("invalid hash format") } var version int // Extract algorithm and version if !strings.HasPrefix(vals[1], "argon2id") { return nil, errors.New("unsupported algorithm") } fmt.Sscanf(vals[2], "v=%d", &version) // Extract parameters p := &argon2Hash{} fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.MemoryCost, &p.TimeCost, &p.Threads) // Extract salt salt, err := base64.RawStdEncoding.DecodeString(vals[4]) if err != nil { return nil, err } p.Salt = salt // Extract hash hash, err := base64.RawStdEncoding.DecodeString(vals[5]) if err != nil { return nil, err } p.HashRaw = hash p.KeyLength = uint32(len(hash)) return p, nil } // Verify the password against the stored hash func verifyPassword(storedHash, password string) (bool, error) { // Parse the stored hash params, err := parseHash(storedHash) if err != nil { return false, err } // Hash the provided password with the same parameters hash := argon2.IDKey( []byte(password), params.Salt, params.TimeCost, params.MemoryCost, params.Threads, params.KeyLength, ) // Use constant-time comparison to prevent timing attacks match := subtle.ConstantTimeCompare(params.HashRaw, hash) == 1 return match, nil } func main() { storedHash := "$argon2id$v=19$m=65536,t=1,p=4$c2FsdHNhbHRzYWx0c2FsdA$PasswordHashHere" password := "super_secret_password" match, err := verifyPassword(storedHash, password) if err != nil { fmt.Println("Error:", err) return } if match { fmt.Println("Password is correct!") } else { fmt.Println("Password is incorrect!") } } package main import ( "crypto/subtle" "encoding/base64" "errors" "fmt" "golang.org/x/crypto/argon2" "strings" ) // Parse the Argon2 hash string into its components func parseHash(encodedHash string) (params *argon2Hash, err error) { vals := strings.Split(encodedHash, "$") if len(vals) != 6 { return nil, errors.New("invalid hash format") } var version int // Extract algorithm and version if !strings.HasPrefix(vals[1], "argon2id") { return nil, errors.New("unsupported algorithm") } fmt.Sscanf(vals[2], "v=%d", &version) // Extract parameters p := &argon2Hash{} fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.MemoryCost, &p.TimeCost, &p.Threads) // Extract salt salt, err := base64.RawStdEncoding.DecodeString(vals[4]) if err != nil { return nil, err } p.Salt = salt // Extract hash hash, err := base64.RawStdEncoding.DecodeString(vals[5]) if err != nil { return nil, err } p.HashRaw = hash p.KeyLength = uint32(len(hash)) return p, nil } // Verify the password against the stored hash func verifyPassword(storedHash, password string) (bool, error) { // Parse the stored hash params, err := parseHash(storedHash) if err != nil { return false, err } // Hash the provided password with the same parameters hash := argon2.IDKey( []byte(password), params.Salt, params.TimeCost, params.MemoryCost, params.Threads, params.KeyLength, ) // Use constant-time comparison to prevent timing attacks match := subtle.ConstantTimeCompare(params.HashRaw, hash) == 1 return match, nil } func main() { storedHash := "$argon2id$v=19$m=65536,t=1,p=4$c2FsdHNhbHRzYWx0c2FsdA$PasswordHashHere" password := "super_secret_password" match, err := verifyPassword(storedHash, password) if err != nil { fmt.Println("Error:", err) return } if match { fmt.Println("Password is correct!") } else { fmt.Println("Password is incorrect!") } } Handling Verification Errors A failed password verification can occur due to various reasons, and error handling must be done appropriately. Invalid Hash Format. You do not even get to compare the password; if a stored hash is corrupted or in an incorrect format, verification fails beforehand. Instead of telling the user "incorrect password," it is better to log the error for investigation while giving a generic error message like "Authentication failed" to the user. This could happen when: The hash was truncated in the database The hash was modified accidentally A different algorithm generate the hash, but it is being passed to Argon2 verification. Password Mismatch. This is expected in the case of a user entering the wrong password. The key to it is giving the exact same error message and taking as much time as you would for any other error. This prevents attackers from figuring out what type of failure it is. Parameter Extraction Errors. Some libraries fail to extract parameters from a hash string, which is another failure. Again log it but give a generic message to the user. Invalid Hash Format. You do not even get to compare the password; if a stored hash is corrupted or in an incorrect format, verification fails beforehand. Instead of telling the user "incorrect password," it is better to log the error for investigation while giving a generic error message like "Authentication failed" to the user. This could happen when: The hash was truncated in the database The hash was modified accidentally A different algorithm generate the hash, but it is being passed to Argon2 verification. Invalid Hash Format. "Authentication failed" The hash was truncated in the database The hash was modified accidentally A different algorithm generate the hash, but it is being passed to Argon2 verification. The hash was truncated in the database The hash was modified accidentally A different algorithm generate the hash, but it is being passed to Argon2 verification. Password Mismatch. This is expected in the case of a user entering the wrong password. The key to it is giving the exact same error message and taking as much time as you would for any other error. This prevents attackers from figuring out what type of failure it is. Password Mismatch. Parameter Extraction Errors. Some libraries fail to extract parameters from a hash string, which is another failure. Again log it but give a generic message to the user. Parameter Extraction Errors. Common Pitfalls Not to Step Into Do not modify the hash string: Some developers try to ''clean'' the hash by cutting certain parts or characters. This will lead you to failed verifications as the original parameters will not have been extracted correctly. Don't extract and store parameters separately: You do not need to extract and store the parameters separately in different database columns, as the parameters required are already included in the encoded hash string. Don't build a custom parser: Parsing the Argon2 hash format is prone to error, let the library do it for you. Do not log password failures with the attempted password: Exposed passwords may be possible if logs are compromised. Don't return different error messages for different failure types: This will make it easy for an attacker to probe your system. Don't optimize verification by skipping parameters: Some developers have a desire to speed up verification by extracting the salt and ignoring other parameters. This is very dangerous and breaks security. Do not modify the hash string: Some developers try to ''clean'' the hash by cutting certain parts or characters. This will lead you to failed verifications as the original parameters will not have been extracted correctly. Do not modify the hash string Don't extract and store parameters separately: You do not need to extract and store the parameters separately in different database columns, as the parameters required are already included in the encoded hash string. Don't extract and store parameters separately Don't build a custom parser: Parsing the Argon2 hash format is prone to error, let the library do it for you. Don't build a custom parser Do not log password failures with the attempted password: Exposed passwords may be possible if logs are compromised. Do not log password failures with the attempted password Don't return different error messages for different failure types: This will make it easy for an attacker to probe your system. Don't return different error messages for different failure types Don't optimize verification by skipping parameters: Some developers have a desire to speed up verification by extracting the salt and ignoring other parameters. This is very dangerous and breaks security. Don't optimize verification by skipping parameters Opportunity to Upgrade Hash Verification of password would be the best opportunity for upgrading your hash parameters because users have just entered their correct passwords. You can easily re-hash those password with fresh parameters. This enable you to upgrade your password hashes to better-suited parameters over time without requiring users to reset passwords. async function verifyAndUpgradeIfNeeded(storedHash, providedPassword) { try { // Verify the password const isValid = await argon2.verify(storedHash, providedPassword); if (isValid) { // Check if the hash needs to be upgraded // This could be based on version, or parameters below your current standards if (needsUpgrade(storedHash)) { // Hash with new parameters const newHash = await argon2.hash(providedPassword, { type: argon2.argon2id, memoryCost: 131072, // Increased memory cost timeCost: 4, // Increased time cost parallelism: 4 }); // Save the new hash to your database await updateUserHash(userId, newHash); } return true; } return false; } catch (err) { console.error('Verification error:', err); return false; } } async function verifyAndUpgradeIfNeeded(storedHash, providedPassword) { try { // Verify the password const isValid = await argon2.verify(storedHash, providedPassword); if (isValid) { // Check if the hash needs to be upgraded // This could be based on version, or parameters below your current standards if (needsUpgrade(storedHash)) { // Hash with new parameters const newHash = await argon2.hash(providedPassword, { type: argon2.argon2id, memoryCost: 131072, // Increased memory cost timeCost: 4, // Increased time cost parallelism: 4 }); // Save the new hash to your database await updateUserHash(userId, newHash); } return true; } return false; } catch (err) { console.error('Verification error:', err); return false; } } Testing and Validating Argon2 Implementation So you've put Argon2 into your application. Congratulations! However, how will you know if it's working appropriately? In Cryptography, "seems to work" is wholly unacceptable. One glitch could compromise the entire authentication system. So let's see how one can actually test and validate his or her Argon2 implementation. Basic Unit Tests These are the initial phrases of test cases for any Argon2 implementation. Verify that the same password and salt always give the same hash. Confirm that modifying even a single character in the password yields a different hash. Test verification correctly validates correct paswords and rejects incorrect ones Check empty strings, very long input, and special characters. Verify that same parameters (memory, iterations, parallelism) give consistent results. Verify that the same password and salt always give the same hash. Confirm that modifying even a single character in the password yields a different hash. Test verification correctly validates correct paswords and rejects incorrect ones Check empty strings, very long input, and special characters. Verify that same parameters (memory, iterations, parallelism) give consistent results. Stress Testing After proper verification of working functionality, it would be advisable to check stress behavior of your implementation, such as: Performance Test Performance Test Argon2 is meant to be "heavy" on computation - that's the whole point of it. But like every other application, there will come a time when multiple requests might come from multiple users hitting the application for authentication at the same time and you need to ensure your application can afford the overhead. import time import concurrent.futures import argon2 def benchmark_argon2(memory_cost, time_cost, parallelism, password="super_secret_password"): """Benchmark Argon2 with specific parameters""" ph = argon2.PasswordHasher(memory_cost=memory_cost, time_cost=time_cost, parallelism=parallelism) start_time = time.time() hash_val = ph.hash(password) duration = time.time() - start_time return { "memory_cost": memory_cost, "time_cost": time_cost, "parallelism": parallelism, "duration": duration, "hash": hash_val } def concurrent_benchmark(concurrency=10): """Test how Argon2 performs under concurrent load""" params = { "memory_cost": 64*1024, # 64 MB "time_cost": 2, "parallelism": 4, } print(f"Testing with concurrency level: {concurrency}") start_time = time.time() with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor: futures = [ executor.submit(benchmark_argon2, **params) for _ in range(concurrency) ] results = [future.result() for future in concurrent.futures.as_completed(futures)] total_duration = time.time() - start_time avg_duration = sum(r["duration"] for r in results) / len(results) print(f"Total time for {concurrency} concurrent hashes: {total_duration:.2f}s") print(f"Average time per hash: {avg_duration:.2f}s") print(f"Throughput: {concurrency/total_duration:.2f} hashes/second") if __name__ == "__main__": # Test with different concurrency levels for concurrency in [1, 2, 4, 8, 16]: concurrent_benchmark(concurrency) print("-" * 40) import time import concurrent.futures import argon2 def benchmark_argon2(memory_cost, time_cost, parallelism, password="super_secret_password"): """Benchmark Argon2 with specific parameters""" ph = argon2.PasswordHasher(memory_cost=memory_cost, time_cost=time_cost, parallelism=parallelism) start_time = time.time() hash_val = ph.hash(password) duration = time.time() - start_time return { "memory_cost": memory_cost, "time_cost": time_cost, "parallelism": parallelism, "duration": duration, "hash": hash_val } def concurrent_benchmark(concurrency=10): """Test how Argon2 performs under concurrent load""" params = { "memory_cost": 64*1024, # 64 MB "time_cost": 2, "parallelism": 4, } print(f"Testing with concurrency level: {concurrency}") start_time = time.time() with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor: futures = [ executor.submit(benchmark_argon2, **params) for _ in range(concurrency) ] results = [future.result() for future in concurrent.futures.as_completed(futures)] total_duration = time.time() - start_time avg_duration = sum(r["duration"] for r in results) / len(results) print(f"Total time for {concurrency} concurrent hashes: {total_duration:.2f}s") print(f"Average time per hash: {avg_duration:.2f}s") print(f"Throughput: {concurrency/total_duration:.2f} hashes/second") if __name__ == "__main__": # Test with different concurrency levels for concurrency in [1, 2, 4, 8, 16]: concurrent_benchmark(concurrency) print("-" * 40) Memory Consumption Testing Memory Consumption Testing Another memory parameter for Argon2, this is the security setting your application will have. You need to ensure that to monitor the usage of memory within your server, allowing it to endure memory configuration settings without running out of resources: import argon2 import os import psutil import time import gc def measure_memory_usage(memory_cost, time_cost=2, parallelism=4): """Measure memory usage of Argon2 with specific memory settings""" process = psutil.Process(os.getpid()) baseline_memory = process.memory_info().rss / 1024 / 1024 # MB ph = argon2.PasswordHasher(time_cost=time_cost, memory_cost=memory_cost, parallelism=parallelism) # Force garbage collection before measurement gc.collect() start_time = time.time() before_memory = process.memory_info().rss / 1024 / 1024 # MB hash_val = ph.hash("super_secret_password") after_memory = process.memory_info().rss / 1024 / 1024 # MB duration = time.time() - start_time print(f"Memory Cost: {memory_cost/1024:.1f} MB, Before: {before_memory:.1f} MB, After: {after_memory:.1f} MB") return { "memory_cost_setting": memory_cost, "baseline_memory_mb": baseline_memory, "peak_memory_mb": after_memory, "memory_increase_mb": after_memory - before_memory, "duration_seconds": duration } if __name__ == "__main__": # Test with different memory settings for memory_cost in [1024, 4096, 16384, 65536, 262144]: # 1MB to 256MB result = measure_memory_usage(memory_cost) print(f"Memory Cost: {memory_cost/1024:.1f} MB") print(f"Memory Used: {result['memory_increase_mb']:.1f} MB") print(f"Hashing Time: {result['duration_seconds']:.2f}s") print("-" * 40) import argon2 import os import psutil import time import gc def measure_memory_usage(memory_cost, time_cost=2, parallelism=4): """Measure memory usage of Argon2 with specific memory settings""" process = psutil.Process(os.getpid()) baseline_memory = process.memory_info().rss / 1024 / 1024 # MB ph = argon2.PasswordHasher(time_cost=time_cost, memory_cost=memory_cost, parallelism=parallelism) # Force garbage collection before measurement gc.collect() start_time = time.time() before_memory = process.memory_info().rss / 1024 / 1024 # MB hash_val = ph.hash("super_secret_password") after_memory = process.memory_info().rss / 1024 / 1024 # MB duration = time.time() - start_time print(f"Memory Cost: {memory_cost/1024:.1f} MB, Before: {before_memory:.1f} MB, After: {after_memory:.1f} MB") return { "memory_cost_setting": memory_cost, "baseline_memory_mb": baseline_memory, "peak_memory_mb": after_memory, "memory_increase_mb": after_memory - before_memory, "duration_seconds": duration } if __name__ == "__main__": # Test with different memory settings for memory_cost in [1024, 4096, 16384, 65536, 262144]: # 1MB to 256MB result = measure_memory_usage(memory_cost) print(f"Memory Cost: {memory_cost/1024:.1f} MB") print(f"Memory Used: {result['memory_increase_mb']:.1f} MB") print(f"Hashing Time: {result['duration_seconds']:.2f}s") print("-" * 40) Common Testing Pitfalls When testing Argon2, you should beware of the following common pitfalls: Edge cases are not tested. For example, empty passwords, extremely long passwords, and non-ASCII characters that may trip up implementations Hardcoded value of test salt: In production, always generate salts unique to each password. Insufficient memory testing: Particularly for multi-threaded applications, you can expect memory consumption to go beyond the limits of what you anticipated. Forgetting to test parameter encoding: The parameters must be stored also when saving an Argon2 hash; ensure that your parameter serialization and deserialization works correctly. Failure modes remain untested. How is your code reacting to the verification failure? What error messages are produced? Edge cases are not tested. For example, empty passwords, extremely long passwords, and non-ASCII characters that may trip up implementations Edge cases are not tested. For example, empty passwords, extremely long passwords, and non-ASCII characters that may trip up implementations Edge cases are not tested. Hardcoded value of test salt: In production, always generate salts unique to each password. Hardcoded value of test salt: In production, always generate salts unique to each password. Hardcoded value of test salt: Insufficient memory testing: Particularly for multi-threaded applications, you can expect memory consumption to go beyond the limits of what you anticipated. Insufficient memory testing: Particularly for multi-threaded applications, you can expect memory consumption to go beyond the limits of what you anticipated. Insufficient memory testing: Forgetting to test parameter encoding: The parameters must be stored also when saving an Argon2 hash; ensure that your parameter serialization and deserialization works correctly. Forgetting to test parameter encoding: The parameters must be stored also when saving an Argon2 hash; ensure that your parameter serialization and deserialization works correctly. Forgetting to test parameter encoding: Failure modes remain untested. How is your code reacting to the verification failure? What error messages are produced? Failure modes remain untested. How is your code reacting to the verification failure? What error messages are produced? Failure modes remain untested. Final Thoughts and Further Resources Correct implementation of Argon2 is a big stride toward securing the users' passwords. This shows that there is a dedication to security best practices and indicates that password storage is taken seriously. In this fast-paced world, security practices should keep up with technology. The recommendations in this article are current as of writing (2025), but be sure to scope out any newer, best-practice missives. After all: Security is not a product but a process. Each time hardware catches up to algorithm development, one must evaluate Argon2 implementation-the parameters may need adjustment. What was secure in 2025 might not be enough in 2028! Further Reading If you want to learn even more about Argon2 and password security, here are some excellent resources: Official Documentation and Papers Official Documentation and Papers The official Argon2 GitHub repository - Contains the reference implementation and the original paper The Argon2 IETF RFC draft - Technical specifications The official Argon2 GitHub repository - Contains the reference implementation and the original paper The official Argon2 GitHub repository The Argon2 IETF RFC draft - Technical specifications The Argon2 IETF RFC draft Cryptographic Tools and Resources Cryptographic Tools and Resources Online Has & Verify Tools - Manually generate and verify your Argon2 hash for testing & debuging. Cryptography Stack Exchange - For asking specific questions about cryptographic implementations OWASP Password Storage Cheat Sheet - Best practices for password storage Online Has & Verify Tools - Manually generate and verify your Argon2 hash for testing & debuging. Online Has & Verify Tools - Manually generate and verify your Argon2 hash for testing & debuging. Online Has & Verify Tools Cryptography Stack Exchange - For asking specific questions about cryptographic implementations Cryptography Stack Exchange - For asking specific questions about cryptographic implementations Cryptography Stack Exchange OWASP Password Storage Cheat Sheet - Best practices for password storage OWASP Password Storage Cheat Sheet - Best practices for password storage OWASP Password Storage Cheat Sheet