diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 6379b82ce8..11a6663efd 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -28,6 +28,7 @@ on: - 'Tests/test_stdin_password.py' - 'Tests/test_mount_helper.py' - 'Tests/test_linux_mount_point_protection.py' + - 'Tests/test_password_cache_kdf.py' - 'Tests/test.*.hc' - 'Tests/test_volume_read.py' - 'Tests/volume_read_test.cpp' @@ -64,6 +65,7 @@ on: - 'Tests/test_stdin_password.py' - 'Tests/test_mount_helper.py' - 'Tests/test_linux_mount_point_protection.py' + - 'Tests/test_password_cache_kdf.py' - 'Tests/test.*.hc' - 'Tests/test_volume_read.py' - 'Tests/volume_read_test.cpp' @@ -221,6 +223,7 @@ jobs: sudo python3 Tests/test_stdin_password.py --require-locales sudo python3 Tests/test_mount_helper.py --helper /usr/sbin/mount.veracrypt sudo python3 Tests/test_linux_mount_point_protection.py + sudo python3 Tests/test_password_cache_kdf.py sudo apt remove -y veracrypt - name: Install and test VeraCrypt Console .deb packages diff --git a/Tests/test_password_cache_kdf.py b/Tests/test_password_cache_kdf.py new file mode 100644 index 0000000000..6617134334 --- /dev/null +++ b/Tests/test_password_cache_kdf.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Regression test for issue #1969 on Linux, against the installed veracrypt. + +Usage (as root, after installing a package): + python3 Tests/test_password_cache_kdf.py [--veracrypt PATH] + +Creates three volumes with a PIM of 100: A (SHA-512) and C (SHA-256) share +a password, B (SHA-512) has another one. With password caching enabled, +mounts them as favorites in one run, typing only the passwords of A and B, +with KDF autodetection. Checked: + +- B is mounted after its password is typed, and the cached password of A + that is tried on B first fails without trying Argon2: with that PIM, + Argon2 uses 1 GiB and 82 passes, so the run would take much longer than + mounting the volumes one by one; +- C is mounted with the cached password of A, without a prompt, although + that password was cached with another KDF. +""" + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +PASSWORD_A = "regression-cache-password-a" +PASSWORD_B = "regression-cache-password-b" +PIM = "100" +# Argon2 with PIM 100 takes far longer than this on any machine that can run +# it, while the PBKDF2 attempts for the volumes here take a fraction of it. +MARGIN = 15 + + +class Checks: + def __init__(self): + self.failures = 0 + + def check(self, desc, cond, detail=""): + print(("ok " if cond else "FAIL ") + desc, flush=True) + if not cond: + self.failures += 1 + if detail: + print(" " + detail.strip().replace("\n", "\n ")) + + +def text(result): + return (result.stdout or b"").decode("utf-8", "replace") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--veracrypt", + default=shutil.which("veracrypt") or "veracrypt") + args = parser.parse_args() + if os.geteuid() != 0: + sys.exit("Run as root: the test mounts volumes") + + work = Path(tempfile.mkdtemp(prefix="vc-regression-")) + config = work / "config" / "VeraCrypt" + config.mkdir(parents=True) + env = dict(os.environ, XDG_CONFIG_HOME=str(work / "config")) + (work / "random").write_bytes(os.urandom(4096)) + c = Checks() + + def run(arguments, stdin=None): + return subprocess.run([args.veracrypt, "--text"] + arguments, + input=stdin, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, cwd=work, env=env, + timeout=1800) + + def mounted(): + """Paths of the mounted volumes; raises if they cannot be listed.""" + r = run(["--list"]) + if r.returncode != 0: + if "No volumes mounted" in text(r): + return set() + raise RuntimeError("veracrypt --list failed: " + text(r)) + return {line.split()[1] for line in text(r).splitlines() + if len(line.split()) >= 4 and line.split()[0].endswith(":")} + + def dismount(paths): + """Dismounts only these volumes; returns whether none remains.""" + paths = {str(path) for path in paths} + for path in paths & mounted(): + run(["--non-interactive", "-d", path]) + return not paths & mounted() + + mount_options = ["--pim=" + PIM, "--keyfiles=", "--protect-hidden=no", + "--filesystem=none", "--mount-options=nokernelcrypto"] + volumes = {name: work / ("%s.hc" % name) for name in ("a", "b", "c")} + try: + for name, password, hash in (("a", PASSWORD_A, "SHA-512"), + ("b", PASSWORD_B, "SHA-512"), + ("c", PASSWORD_A, "SHA-256")): + r = run(["--non-interactive", "--create", str(volumes[name]), + "--size=2M", "--volume-type=normal", "--encryption=AES", + "--hash=" + hash, "--filesystem=none", "-p", password, + "--pim=" + PIM, "--keyfiles=", + "--random-source=" + str(work / "random")]) + c.check("create %s.hc" % name, r.returncode == 0, text(r)) + if c.failures: + sys.exit("Cannot create the test volumes") + + start = time.monotonic() + r = run(["--non-interactive", "-p", PASSWORD_A, str(volumes["a"])] + + mount_options) + single = time.monotonic() - start + c.check("mount a.hc with KDF autodetection", + r.returncode == 0 and dismount([volumes["a"]]), text(r)) + + (config / "Configuration.xml").write_text( + '\n\n' + '\t\n\t\t1\n' + '\t\n\n') + (config / "Favorite Volumes.xml").write_text( + '\n\n' + '\t\n' + "".join( + '\t\t%s\n' % (slot, volumes[name]) + for slot, name in ((1, "a"), (2, "b"), (3, "c"))) + + '\t\n\n') + + start = time.monotonic() + # --load-preferences enables the password cache of Configuration.xml + r = run(["--load-preferences", "--auto-mount=favorites"] + + mount_options, + stdin=("%s\n%s\n" % (PASSWORD_A, PASSWORD_B)).encode()) + favorites = time.monotonic() - start + listed = mounted() + c.check("all three favorites are mounted", + r.returncode == 0 + and all(str(volumes[name]) in listed for name in volumes), + text(r) + "\n" + str(listed)) + c.check("c.hc (another KDF) is mounted from the cache, without a " + "prompt", + text(r).count("Enter password") == 2, text(r)) + c.check("cached passwords that do not match are not tried with " + "Argon2 (%.1f s, one mount %.1f s)" % (favorites, single), + favorites < 3 * single + MARGIN) + finally: + # Only the volumes of this test: unrelated ones may be mounted + if not dismount(volumes.values()): + sys.exit("A test volume is still mounted; not removing %s" % work) + shutil.rmtree(work, ignore_errors=True) + + if c.failures: + sys.exit("%d check(s) failed" % c.failures) + print("All checks passed") + + +if __name__ == "__main__": + main() diff --git a/src/Core/Unix/CoreServiceProxy.h b/src/Core/Unix/CoreServiceProxy.h index 37f05c9a13..f8e4ac22c7 100644 --- a/src/Core/Unix/CoreServiceProxy.h +++ b/src/Core/Unix/CoreServiceProxy.h @@ -117,12 +117,53 @@ namespace VeraCrypt { finally_do_arg (MountOptions*, &options, { if (finally_arg->Password) finally_arg->Password.reset(); }); + // A selected KDF is used with every cached password. With autodetection, + // each cached password is tried with the KDF it opened a volume with, + // then with the other PBKDF2 KDFs, which are cheap. Argon2, whose time + // and memory grow steeply with the PIM, is tried only for passwords that + // opened an Argon2 volume, or a volume with an unknown KDF: through + // autodetection, which also keeps its handling of derivation failures. + // If nothing matches, the user is asked for the password instead of + // trying Argon2 with every cached password. + CachedPasswordList cachedPasswords = VolumePasswordCache::GetPasswords(); + CachedAttemptList attempts; + + if (options.Kdf) + { + foreach (const CachedPassword &cached, cachedPasswords) + attempts.push_back (CachedAttempt (cached.Password, options.Kdf)); + } + else + { + foreach (const CachedPassword &cached, cachedPasswords) + { + if (cached.Kdf && !cached.Kdf->IsArgon2()) + AddCachedAttempt (attempts, cached.Password, cached.Kdf); + } + + foreach (const CachedPassword &cached, cachedPasswords) + { + foreach (shared_ptr kdf, Pkcs5Kdf::GetAvailableAlgorithms()) + { + if (!kdf->IsArgon2()) + AddCachedAttempt (attempts, cached.Password, kdf); + } + } + + foreach (const CachedPassword &cached, cachedPasswords) + { + if (!cached.Kdf || cached.Kdf->IsArgon2()) + AddCachedAttempt (attempts, cached.Password, shared_ptr ()); + } + } + PasswordIncorrect passwordException; - foreach (shared_ptr password, VolumePasswordCache::GetPasswords()) + foreach (const CachedAttempt &attempt, attempts) { try { - newOptions.Password = password; + newOptions.Password = attempt.first; + newOptions.Kdf = attempt.second; mountedVolume = CoreService::RequestMountVolume (newOptions); break; } @@ -170,7 +211,14 @@ namespace VeraCrypt if (!useCachedPasswords && options.CachePassword && ((options.Password && !options.Password->IsEmpty()) || (options.Keyfiles && !options.Keyfiles->empty()))) { - VolumePasswordCache::Store (*newOptions.Password); + shared_ptr kdf; + try + { + kdf = Pkcs5Kdf::GetAlgorithm (mountedVolume->Pkcs5PrfName); + } + catch (ParameterIncorrect&) { } + + VolumePasswordCache::Store (*newOptions.Password, kdf); } VolumeEventArgs eventArgs (mountedVolume); @@ -193,6 +241,22 @@ namespace VeraCrypt { VolumePasswordCache::Clear(); } + + protected: + // A cached password with a KDF to try, or with autodetection if null + typedef pair < shared_ptr , shared_ptr > CachedAttempt; + typedef list CachedAttemptList; + + static void AddCachedAttempt (CachedAttemptList &attempts, shared_ptr password, shared_ptr kdf) + { + foreach (const CachedAttempt &attempt, attempts) + { + if (*attempt.first == *password && (attempt.second ? attempt.second->GetName() : wstring()) == (kdf ? kdf->GetName() : wstring())) + return; + } + + attempts.push_back (CachedAttempt (password, kdf)); + } }; } diff --git a/src/Volume/VolumePasswordCache.cpp b/src/Volume/VolumePasswordCache.cpp index c9fb3bf5a9..2b1a5972d0 100644 --- a/src/Volume/VolumePasswordCache.cpp +++ b/src/Volume/VolumePasswordCache.cpp @@ -18,26 +18,34 @@ namespace VeraCrypt { CachedPasswordList passwords; - foreach_ref (const VolumePassword &password, CachedPasswords) - passwords.push_back (make_shared (VolumePassword (password))); + foreach (const CachedPassword &cached, CachedPasswords) + { + CachedPassword copy; + copy.Password = make_shared (VolumePassword (*cached.Password)); + copy.Kdf = cached.Kdf; + passwords.push_back (copy); + } return passwords; } - void VolumePasswordCache::Store (const VolumePassword &newPassword) + void VolumePasswordCache::Store (const VolumePassword &newPassword, shared_ptr kdf) { - CachedPasswordList::iterator iter = CachedPasswords.begin(); - foreach_ref (const VolumePassword &password, CachedPasswords) + // A password used with several KDFs gets an entry for each of them + for (CachedPasswordList::iterator iter = CachedPasswords.begin(); iter != CachedPasswords.end(); ++iter) { - if (newPassword == password) + if (*iter->Password == newPassword + && (iter->Kdf ? iter->Kdf->GetName() : wstring()) == (kdf ? kdf->GetName() : wstring())) { CachedPasswords.erase (iter); break; } - iter++; } - CachedPasswords.push_front (make_shared (VolumePassword (newPassword))); + CachedPassword cached; + cached.Password = make_shared (VolumePassword (newPassword)); + cached.Kdf = kdf; + CachedPasswords.push_front (cached); if (CachedPasswords.size() > Capacity) CachedPasswords.pop_back(); diff --git a/src/Volume/VolumePasswordCache.h b/src/Volume/VolumePasswordCache.h index 40a6a1c083..eaf1aca8fe 100644 --- a/src/Volume/VolumePasswordCache.h +++ b/src/Volume/VolumePasswordCache.h @@ -14,18 +14,25 @@ #define TC_HEADER_Volume_VolumePasswordCache #include "Platform/Platform.h" +#include "Pkcs5Kdf.h" #include "VolumePassword.h" namespace VeraCrypt { - typedef list < shared_ptr < VolumePassword > > CachedPasswordList; + struct CachedPassword + { + shared_ptr Password; + shared_ptr Kdf; // KDF that opened a volume with the password; null if unknown + }; + + typedef list CachedPasswordList; class VolumePasswordCache { public: static CachedPasswordList GetPasswords (); static bool IsEmpty () { return CachedPasswords.empty(); } - static void Store (const VolumePassword &newPassword); + static void Store (const VolumePassword &newPassword, shared_ptr kdf); static void Clear () { CachedPasswords.clear(); } static const size_t Capacity = 4;