2

I read an article about how to use Argon2id in C# here.

Below is the code they wrote (slightly edited):

using System;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Konscious.Security.Cryptography;   

namespace Playground
{
    class Program
    {
        // No. of CPU Cores x 2.
        private const int DEGREE_OF_PARALLELISM = 16;

        // Recommended minimum value.
        private const int NUMBER_OF_ITERATIONS = 4;

        // 600 MB.
        private const int MEMORY_TO_USE_IN_KB = 600000;

        static void Main(string[] args)
        {
            var password = "SomeSecurePassword";               
            byte[] salt = CreateSalt();
            byte[] hash = HashPassword(password, salt);                

            var otherPassword = "SomeSecurePassword";                                
            var success = VerifyHash(otherPassword, salt, hash);                
            Console.WriteLine(success ? "Passwords match!" : "Passwords do not match.");                
        }

        private static byte[] CreateSalt()
        {
            var buffer = new byte[16];
            var rng = new RNGCryptoServiceProvider();
            rng.GetBytes(buffer);

            return buffer;
        }

        private static byte[] HashPassword(string password, byte[] salt)
        {
            var argon2id = new Argon2id(Encoding.UTF8.GetBytes(password));
            argon2id.Salt = salt;
            argon2id.DegreeOfParallelism = DEGREE_OF_PARALLELISM;
            argon2id.Iterations = NUMBER_OF_ITERATIONS;
            argon2id.MemorySize = MEMORY_TO_USE_IN_KB;

            return argon2id.GetBytes(16);
        }

        private static bool VerifyHash(string password, byte[] salt, byte[] hash)
        {
            var newHash = HashPassword(password, salt);
            return hash.SequenceEqual(newHash);
        }
    }
}

I have the following questions:

  1. On the Konscious.Security.Cryptography README page, instead of argon2id.GetBytes(16), they are using argon2.GetBytes(128) which returns a longer value.

Assuming the configurations are the same, is the 128 approach more secure than the 16 one because it's longer?

  1. From what I understand, the more memory we let Argon2id use, the more secure it will be against customized hardware attacks.

I therefore assume that even if 40 iterations with 70 MB and 4 iterations with 600 MB take roughly the same time, the latter configuration's larger memory cost is justified because it's more secure. Is this correct?

1 Answers1

4

According to the Argon2 Specs, specifically section 9 Recommended Parameters, the following procedures should be done to determine which parameters to use:

We recommend the following procedure to select the type and the parameters for practical use of Argon2:

  1. Select the type y. If you do not know the difference between them or you consider side-channel attacks as viable threat, choose Argon2i. Otherwise any choice is fine, including optional types.
  2. Figure out the maximum number h of threads that can be initiated by each call to Argon2.
  3. Figure out the maximum amount m of memory that each call can afford.
  4. Figure out the maximum amount x of time (in seconds) that each call can afford.
  5. Select the salt length. 128 bits is sufficient for all applications, but can be reduced to 64 bits in the case of space constraints.
  6. Select the tag length. 128 bits is sufficient for most applications, including key derivation. If longer keys are needed, select longer tags.
  7. If side-channel attacks is a viable threat, enable the memory wiping option in the library call.
  8. Run the scheme of type y, memory m and h lanes and threads, using different number of passes t. Figure out the maximum t such that the running time does not exceed x. If it exceeds x even for t = 1, reduce m accordingly.
  9. Hash all the passwords with the just determined values m, h, and t.

The "tag" in this case refers to the output of Argon2. So as the document highlights, 128 bits (or 16 bytes), should be enough for most applications. Generating a longer output will not give you more security. You should generate a longer output if your application needs the output to be longer for some other purpose (e.g. generating 256 bits output to use as a key for AES-256).


As for the memory cost compared to the number of iterations, I asked a similar question. The answer to that question, as well as the specs, recommend the maximum amount of memory possible given your environment. This question and its corresponding answers explain why it makes sense to maximize memory usage, as long as you have at least 3 iterations over that memory. In your code, NUMBER_OF_ITERATIONS is set to 4 as a recommended minimum value, which seems safe as a minimum.


As a side note, you should actually benchmark your values. What primarily matters is the amount of time it takes to perform the calculation of one Argon2 tag. The specification added above, as well as the questions I linked to, detail this process well.

  • Many thanks for the detailed response! I'm going to read the links you provided as well. :) – Floating Sunfish Mar 31 '20 at 08:06
  • 1
    Three iterations is the recommended minimum for Argon2i. For Argon2id it's two. For Argon2d it's one. (Prior to version 1.3 there were a lot of recommendations to use many more passes for Argon2i. The concern that drove that was addressed in version 1.3, so three is now sufficient.) – Future Security Apr 01 '20 at 02:44
  • 1
    *"What primarily matters is the amount of time it takes to perform the calculation of one Argon2 tag."*   Run time (defender-side) doesn't matter as much as how many hashes per second an attacker can perform. So it's *run time and how difficult it is to parallelize the hash* that matters. That's why the maximize memory step is prioritized. Fortunately users don't need to do more than benchmark on their own hardware if they follow that advice. (Preferably using 100's or 1000's of MB. Maybe more in the future.) – Future Security Apr 01 '20 at 03:04
  • 2
    (It's one of my missions to change the usual "slow hash function" or "maximizing runtime" narrative to a "maximizing cracking costs" narrative. I think people still recommend PBKDF2 for that reason, despite it being much easier to crack. Equal hash computation latency doesn't mean equal security. Running PBKDF2 for 1 second is much weaker than running Argon2 for 1 second. Sadly we've got too many others recommending primitive, easily parallelized password hashing function. Often with a hint of "Argon2 is too new/complicated" and NIH syndrome.) – Future Security Apr 01 '20 at 03:19
  • @FutureSecurity Very informative, many thanks for sharing! – Floating Sunfish Apr 01 '20 at 07:36
  • @FutureSecurity Using 1 GB memory only works in *some* use cases. If you, for example, use Argon2 for a login system, then using 1 GB memory per login attempt is going to clog up your own resources very fast. –  Apr 01 '20 at 07:38
  • You're right. I'm just not comfortable suggesting less memory if you're using the min number of iterations. There is some lower value for which time-memory tradeoffs might be profitable. (Despite the time-memory product being larger.) I don't know enough to say what range that might be in. I was being conservative in that number to avoid being responsible for bad advice. It's likely that significantly less memory is sufficient in practice. It's absolutely still better to use low memory than something like PBKDF2. (Maxing memory first (even if low), then time really is THE way to pick values.) – Future Security Apr 01 '20 at 08:47
  • @FutureSecurity I would argue 64M memory with plenty of iterations offers a higher time-to-crack to PBKDF2 with just high iterations, assuming both have equal time-to-run on the host machine. –  Apr 01 '20 at 09:13
  • 1
    Any amount of memory will be safer than PBKDF2. When it comes to simple algorithms like PBKDF2, s2k, md5crypt, shacrypt, etc. they're all about equally easy to parallelize and are sequential calculations. In a competition between those algorithms you merely want to maximize the efficiency of the iterated function on the defender's computer.relative to the most optimized (software or hardware) implementation of that algorithm. They all use functions with a huge gap between pure-software speed and hardware-acceleration speed. That gap is small for Argon2's internals, so Argon2 would always win. – Future Security Apr 01 '20 at 10:08
  • @FutureSecurity Correct me if I am wrong, but is this because Argon2 was optimized to be "efficiently" computable using off-the-shelf hardware, with very little benefit of using FPGAa or ASICs? At least, that's how I interpreted their reasoning in [the paper](https://www.cryptolux.org/images/0/0d/Argon2.pdf). –  Apr 01 '20 at 10:27
  • You're correct. It utilizes some of the most performance-optimized features of 64-bit AMD/Intel. - Namely 64-bit multiplication (it was believed that there were no multiplier circuit designs that spit out a result much faster than around 1 ns, about the latency of a MUL instruction), vector instructions (acting on 1 register at a time or acting on several have about the same overhead, but the useful work achieved by the latter is greater), and block-based RAM access (if an ASIC design were better, some of those ideas would (hopefully!) transfer to faster/denser commodity RAM). – Future Security Apr 01 '20 at 20:20