Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
155 changes: 155 additions & 0 deletions Tests/test_password_cache_kdf.py
Original file line number Diff line number Diff line change
@@ -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(
'<?xml version="1.0" encoding="utf-8"?>\n<VeraCrypt>\n'
'\t<configuration>\n\t\t<config key="CachePasswords">1</config>\n'
'\t</configuration>\n</VeraCrypt>\n')
(config / "Favorite Volumes.xml").write_text(
'<?xml version="1.0" encoding="utf-8"?>\n<VeraCrypt>\n'
'\t<favorites>\n' + "".join(
'\t\t<volume mountpoint="" slotnumber="%d" readonly="0" '
'system="0">%s</volume>\n' % (slot, volumes[name])
for slot, name in ((1, "a"), (2, "b"), (3, "c")))
+ '\t</favorites>\n</VeraCrypt>\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)
Comment thread
flatstik marked this conversation as resolved.

if c.failures:
sys.exit("%d check(s) failed" % c.failures)
print("All checks passed")


if __name__ == "__main__":
main()
70 changes: 67 additions & 3 deletions src/Core/Unix/CoreServiceProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Pkcs5Kdf> 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 <Pkcs5Kdf>());
}
}

PasswordIncorrect passwordException;
foreach (shared_ptr <VolumePassword> 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;
}
Expand Down Expand Up @@ -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 <Pkcs5Kdf> kdf;
try
{
kdf = Pkcs5Kdf::GetAlgorithm (mountedVolume->Pkcs5PrfName);
}
catch (ParameterIncorrect&) { }

VolumePasswordCache::Store (*newOptions.Password, kdf);
}

VolumeEventArgs eventArgs (mountedVolume);
Expand All @@ -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 <VolumePassword>, shared_ptr <Pkcs5Kdf> > CachedAttempt;
typedef list <CachedAttempt> CachedAttemptList;

static void AddCachedAttempt (CachedAttemptList &attempts, shared_ptr <VolumePassword> password, shared_ptr <Pkcs5Kdf> 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));
}
};
}

Expand Down
24 changes: 16 additions & 8 deletions src/Volume/VolumePasswordCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,34 @@ namespace VeraCrypt
{
CachedPasswordList passwords;

foreach_ref (const VolumePassword &password, CachedPasswords)
passwords.push_back (make_shared <VolumePassword> (VolumePassword (password)));
foreach (const CachedPassword &cached, CachedPasswords)
{
CachedPassword copy;
copy.Password = make_shared <VolumePassword> (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 <Pkcs5Kdf> 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> (VolumePassword (newPassword)));
CachedPassword cached;
cached.Password = make_shared <VolumePassword> (VolumePassword (newPassword));
cached.Kdf = kdf;
CachedPasswords.push_front (cached);
Comment thread
flatstik marked this conversation as resolved.

if (CachedPasswords.size() > Capacity)
CachedPasswords.pop_back();
Expand Down
11 changes: 9 additions & 2 deletions src/Volume/VolumePasswordCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <VolumePassword> Password;
shared_ptr <Pkcs5Kdf> Kdf; // KDF that opened a volume with the password; null if unknown
};

typedef list <CachedPassword> 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 <Pkcs5Kdf> kdf);
static void Clear () { CachedPasswords.clear(); }
static const size_t Capacity = 4;

Expand Down
Loading