Skip to content
Migrating Argon2 Hashes from Earlier Releases

Migrating Argon2 Hashes from Earlier Releases

cryptopp-modern 2025.11.0 through 2026.9.1 computed Argon2i and Argon2id differently from RFC 9106 for affected parameter sets. 2026.10.0 corrects this, so affected hashes now differ from those produced by earlier releases and match RFC 9106-compliant implementations. Argon2d was not affected.

This guide is for anyone who stored Argon2i or Argon2id hashes, or derived encryption keys with them, using one of the affected releases.

Do you need to do anything?

Your situationAction
You started using Argon2 with 2026.10.0 or laterNothing.
You only use Argon2dNothing. Argon2d output is unchanged.
You stored Argon2i or Argon2id password hashes with 2025.11.0 through 2026.9.1Verify with the legacy variant, then rehash. See below.
You encrypted data under keys derived with Argon2i or Argon2id in those releasesDerive the key with the legacy variant to decrypt, then re-encrypt under a key from the standard variant.
You compare hashes with another Argon2 implementationAffected hashes may not match. Migrate them, then they will.

No security weakness in the non-standard output is currently known; it simply does not match the standard. Treat migration as an interoperability task to schedule, not an incident.

The legacy variants

2026.10.0 adds two variants that reproduce the earlier output exactly:

VariantReproducesUse for
Argon2::ARGON2I_LEGACYArgon2i as computed by 2025.11.0 through 2026.9.1Verifying old Argon2i hashes, recovering old Argon2i-derived keys
Argon2::ARGON2ID_LEGACYArgon2id as computed by 2025.11.0 through 2026.9.1Verifying old Argon2id hashes, recovering old Argon2id-derived keys

They take the same parameters as the standard variants and report their names as Argon2i-legacy and Argon2id-legacy. Do not use them for new hashes or keys. Nothing else about the API changed, and there is no ABI change.

Migrating password hashes

The pattern is the usual verify-then-upgrade flow, with the legacy variant doing the verification. Your stored record needs two facts beyond the salt and parameters: which algorithm produced the hash, Argon2i or Argon2id, and which computation, standard or legacy. The algorithm is a property of the hash itself and stays with it; the computation is what migration changes. Store the algorithm (Argon2i or Argon2id) and whether the hash uses the legacy computation. If existing records lack a computation marker, use the verification fallback described below. You must know the original algorithm.

#include <cryptopp/argon2.h>
#include <cryptopp/misc.h>
#include <cryptopp/secblock.h>
#include <string>

using namespace CryptoPP;

struct StoredHash {
    std::string salt;        // raw bytes
    std::string hash;        // raw bytes
    word32 timeCost, memoryCost, parallelism;
    bool argon2i;            // true for Argon2i, false for Argon2id
    bool legacy;             // true if computed by 2025.11.0 through 2026.9.1
};

static Argon2::Variant VariantFor(bool argon2i, bool legacy)
{
    if (argon2i)
        return legacy ? Argon2::ARGON2I_LEGACY : Argon2::ARGON2I;
    return legacy ? Argon2::ARGON2ID_LEGACY : Argon2::ARGON2ID;
}

static std::string Derive(Argon2::Variant variant, const std::string& password, const StoredHash& s)
{
    Argon2 argon2(variant);
    SecByteBlock out(s.hash.size());
    argon2.DeriveKey(out, out.size(),
        reinterpret_cast<const byte*>(password.data()), password.size(),
        reinterpret_cast<const byte*>(s.salt.data()), s.salt.size(),
        s.timeCost, s.memoryCost, s.parallelism);
    return std::string(reinterpret_cast<const char*>(out.data()), out.size());
}

static bool Check(Argon2::Variant variant, const std::string& password, const StoredHash& s)
{
    const std::string out = Derive(variant, password, s);
    return out.size() == s.hash.size() &&
        VerifyBufsEqual(reinterpret_cast<const byte*>(out.data()),
                        reinterpret_cast<const byte*>(s.hash.data()), out.size());
}

// Writes the record to your database. Returns false if the update failed.
bool SaveHash(const StoredHash& s);

bool LoginAndMigrate(const std::string& password, StoredHash& s)
{
    if (!Check(VariantFor(s.argon2i, s.legacy), password, s))
        return false;                               // wrong password

    if (s.legacy) {
        // Correct password: recompute with the standard variant of the same algorithm.
        StoredHash replacement = s;
        replacement.hash = Derive(VariantFor(s.argon2i, false), password, s);
        replacement.legacy = false;
        if (SaveHash(replacement))
            s = replacement;                        // the legacy record stays until the save succeeds
    }
    return true;
}

Save the new hash and version marker together in a single database transaction. Do not discard the legacy hash until the replacement has been stored successfully. In a concurrent application, make the save a compare-and-update so it cannot overwrite a newer record. Validate the hash length and cost parameters of a stored record before passing them to DeriveKey; a malformed record could otherwise throw or request excessive memory.

The example keeps each hash on its original algorithm and reuses its salt to stay short; generating a fresh salt when rehashing is a sensible default. Moving Argon2i hashes to Argon2id is a separate decision; if you take it, make it in the same step, while the password is in hand.

If you don’t know which computation produced a hash, verify with the standard variant first, then try the corresponding legacy variant if verification fails. Rehash with the standard variant after successful legacy verification. You must know whether the original hash used Argon2i or Argon2id. Fallback verification performs two expensive derivations for an incorrect password, so normal login rate limiting still matters. Users who never log in keep their legacy hash; decide how long you are prepared to carry the fallback.

Recovering derived keys

Where Argon2i or Argon2id derived an encryption key, the stored ciphertext can only be opened with the key the affected release produced. Derive that key with the legacy variant, decrypt, then derive a fresh key with the standard variant and re-encrypt:

SecByteBlock oldKey(32), newKey(32);
Argon2(Argon2::ARGON2ID_LEGACY).DeriveKey(oldKey, oldKey.size(), password, passwordLen, salt, saltLen, t, m, p);
Argon2(Argon2::ARGON2ID).DeriveKey(newKey, newKey.size(), password, passwordLen, salt, saltLen, t, m, p);
// decrypt with oldKey, re-encrypt with newKey, store the new ciphertext

Decrypt and authenticate the old data successfully before replacing it, use a fresh nonce or IV as required by the encryption scheme, and keep the original ciphertext and its metadata until the replacement is durably stored. If the original derivation used the optional secret or associated data parameters, supply them unchanged when recovering the historical key. The same salt and parameters can reproduce the corrected key, although a fresh salt is preferable when re-encrypting.

What not to do

  • Do not keep writing new hashes or keys with the legacy variants. They exist for reading old data.
  • Do not switch stored hashes to the legacy variant to avoid migrating. That locks the data to a non-standard computation.
  • Do not assume Argon2d data needs migrating. It does not.

See Also