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;