mirror of
https://github.com/veracrypt/VeraCrypt.git
synced 2026-10-20 21:32:36 +00:00
Compare volume contents before and after a rejected password change. Check before reopening, which rewrites both headers and could hide damage.
242 lines
10 KiB
Python
242 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Check that volume headers are compatible between little- and big-endian builds.
|
|
|
|
Usage:
|
|
python3 Tests/test_byte_order_interop.py --binary-a /path/to/veracrypt \
|
|
[--binary-b /path/to/other/veracrypt] [--skip-official] \
|
|
[--fixture test.sha512.hc ...]
|
|
|
|
Each binary is a console (or GUI) veracrypt. A binary built for another CPU
|
|
can be run through qemu-user binfmt support. No root is needed: volumes are
|
|
created with --filesystem=none and only their passwords are changed, which
|
|
decrypts and re-encrypts the volume headers without mounting anything.
|
|
|
|
For every binary, the volumes in Tests/test.*.hc (created by little-endian
|
|
VeraCrypt, password "test") must open with the KDF named in the file name,
|
|
accept a new password, and then open with the new password (with KDF
|
|
autodetection for the first volume).
|
|
|
|
With two binaries, for every encryption algorithm and cascade (the PRFs are
|
|
used in turn, so every PRF is covered too), in both directions:
|
|
|
|
- a volume created by one binary opens with the other,
|
|
- after that password change, the first binary opens it again,
|
|
- a wrong password is rejected without changing the volume's contents,
|
|
and the volume still opens.
|
|
|
|
The KDF is passed with --hash, except in one case per direction, which
|
|
exercises KDF autodetection.
|
|
|
|
Build one binary on a little-endian and one on a big-endian CPU (or
|
|
cross-compile one) to check byte-order compatibility. Only the headers are
|
|
exercised here; the data-area XTS code is covered by the self-test vectors
|
|
(--test). Volumes are created in a temporary directory that is removed
|
|
afterwards. VeraCrypt runs with the C locale and an empty configuration
|
|
directory, so its messages are in English whatever the user's settings.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
OFFICIAL_PASSWORD = "test"
|
|
PASSWORD = "abcdefghijklmnopqrstuvwxyz0123"
|
|
NEW_PASSWORD = "zyxwvutsrqponmlkjihgfedcba9876"
|
|
WRONG_PASSWORD = "wrongwrongwrongwrongwrongwrong"
|
|
|
|
ALGORITHMS = [
|
|
"AES", "Serpent", "Twofish", "Camellia", "Kuznyechik",
|
|
"AES-Twofish", "AES-Twofish-Serpent", "Camellia-Kuznyechik",
|
|
"Camellia-Serpent", "Kuznyechik-AES", "Kuznyechik-Serpent-Camellia",
|
|
"Kuznyechik-Twofish", "Serpent-AES", "Serpent-Twofish-AES",
|
|
"Twofish-Serpent",
|
|
]
|
|
PRFS = ["sha512", "sha256", "blake2s", "whirlpool", "streebog", "argon2"]
|
|
|
|
COMMON = ["--text", "--non-interactive", "--keyfiles=",
|
|
"--random-source=/dev/urandom"]
|
|
|
|
|
|
class StepFailed(Exception):
|
|
pass
|
|
|
|
|
|
class Runner:
|
|
def __init__(self, workdir, timeout):
|
|
self.timeout = timeout
|
|
# VeraCrypt takes its language from LANG and from the saved
|
|
# preferences, so use the C locale and an empty config directory.
|
|
config = workdir / "config"
|
|
config.mkdir()
|
|
self.env = os.environ.copy()
|
|
self.env.update(LANG="C", LC_ALL="C", XDG_CONFIG_HOME=str(config))
|
|
|
|
def run(self, binary, args):
|
|
command = [binary] + COMMON + args
|
|
try:
|
|
return subprocess.run(command, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, text=True,
|
|
env=self.env, timeout=self.timeout,
|
|
check=False)
|
|
except subprocess.TimeoutExpired:
|
|
raise StepFailed(f"timed out after {self.timeout} s: "
|
|
f"{' '.join(command)}")
|
|
|
|
def step(self, name, binary, args):
|
|
result = self.run(binary, args)
|
|
if result.returncode != 0:
|
|
raise StepFailed(f"{name}: exit {result.returncode}\n"
|
|
f"{result.stderr.strip() or result.stdout.strip()}")
|
|
|
|
|
|
def change_args(volume, kdf, old, pim, new, new_pim):
|
|
args = ["--change", str(volume), f"--password={old}", f"--pim={pim}",
|
|
f"--new-password={new}", f"--new-pim={new_pim}", "--new-keyfiles="]
|
|
if kdf:
|
|
args.append(f"--hash={kdf}")
|
|
return args
|
|
|
|
|
|
def official_volumes(runner, binary, fixtures, workdir):
|
|
failures = 0
|
|
for i, source in enumerate(fixtures):
|
|
kdf = source.name.split(".")[1]
|
|
volume = workdir / source.name
|
|
shutil.copyfile(source, volume)
|
|
start = time.monotonic()
|
|
try:
|
|
# The fixtures use PIM 0; the new header uses PIM 1, so only
|
|
# opening the original header costs a full key derivation.
|
|
runner.step("open and change password", binary,
|
|
change_args(volume, kdf, OFFICIAL_PASSWORD, 0,
|
|
PASSWORD, 1))
|
|
# The command line has no way to only check a password without
|
|
# mounting, so another password change verifies the new header.
|
|
# The first volume is opened without --hash, which covers KDF
|
|
# autodetection (cheap with PIM 1).
|
|
runner.step("open with the new password", binary,
|
|
change_args(volume, kdf if i else None, PASSWORD, 1,
|
|
NEW_PASSWORD, 1))
|
|
print(f"ok {binary}: {source.name} "
|
|
f"({time.monotonic() - start:.0f} s)", flush=True)
|
|
except StepFailed as e:
|
|
print(f"FAIL {binary}: {source.name} "
|
|
f"({time.monotonic() - start:.0f} s): {e}", flush=True)
|
|
failures += 1
|
|
volume.unlink()
|
|
return failures
|
|
|
|
|
|
def interop_case(runner, maker, opener, algorithm, prf, kdf, volume):
|
|
volume.unlink(missing_ok=True)
|
|
runner.step(f"create by {maker}", maker,
|
|
["--create", str(volume), "--size=1M", "--volume-type=normal",
|
|
f"--encryption={algorithm}", f"--hash={prf}",
|
|
"--filesystem=none", f"--password={PASSWORD}", "--pim=1"])
|
|
runner.step(f"open by {opener}", opener,
|
|
change_args(volume, kdf, PASSWORD, 1, NEW_PASSWORD, 1))
|
|
runner.step(f"reopen by {maker}", maker,
|
|
change_args(volume, kdf, NEW_PASSWORD, 1, PASSWORD, 1))
|
|
before_wrong_password = volume.read_bytes()
|
|
result = runner.run(opener, change_args(volume, kdf, WRONG_PASSWORD, 1,
|
|
NEW_PASSWORD, 1))
|
|
if result.returncode != 1 or "Incorrect password" not in result.stderr:
|
|
raise StepFailed(f"wrong password by {opener}: exit "
|
|
f"{result.returncode}\n{result.stderr.strip()}")
|
|
# Check before another password change can repair a damaged header.
|
|
if volume.read_bytes() != before_wrong_password:
|
|
raise StepFailed(f"wrong password by {opener}: volume contents changed")
|
|
runner.step(f"open by {opener} after a wrong password", opener,
|
|
change_args(volume, kdf, PASSWORD, 1, NEW_PASSWORD, 1))
|
|
|
|
|
|
def interop(runner, maker, opener, workdir):
|
|
failures = 0
|
|
for i, algorithm in enumerate(ALGORITHMS):
|
|
prf = PRFS[i % len(PRFS)]
|
|
# One case leaves out --hash to exercise KDF autodetection. sha512
|
|
# is tried first anyway, so use the second case (sha256).
|
|
kdf = None if i == 1 else prf
|
|
name = f"{algorithm}/{prf}{'' if kdf else ' (autodetect)'}"
|
|
start = time.monotonic()
|
|
try:
|
|
interop_case(runner, maker, opener, algorithm, prf, kdf,
|
|
workdir / "interop.hc")
|
|
print(f"ok {name}: {maker} -> {opener} "
|
|
f"({time.monotonic() - start:.0f} s)", flush=True)
|
|
except StepFailed as e:
|
|
print(f"FAIL {name}: {maker} -> {opener} "
|
|
f"({time.monotonic() - start:.0f} s): {e}", flush=True)
|
|
failures += 1
|
|
return failures
|
|
|
|
|
|
def select_fixtures(names):
|
|
available = sorted((ROOT / "Tests").glob("test.*.hc"))
|
|
if not names:
|
|
if not available:
|
|
sys.exit(f"No Tests/test.*.hc volumes found in {ROOT / 'Tests'}")
|
|
return available
|
|
selected = []
|
|
for name in names:
|
|
path = ROOT / "Tests" / name
|
|
if path not in available:
|
|
sys.exit(f"Fixture not found: {path}")
|
|
selected.append(path)
|
|
return selected
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--binary-a", required=True, help="veracrypt binary")
|
|
parser.add_argument("--binary-b",
|
|
help="second veracrypt binary (other byte order)")
|
|
parser.add_argument("--skip-official", action="store_true",
|
|
help="do not open the Tests/test.*.hc volumes")
|
|
parser.add_argument("--fixture", action="append", default=[],
|
|
metavar="NAME",
|
|
help="open only this volume from Tests/, given by "
|
|
"file name, e.g. test.sha512.hc (repeatable; "
|
|
"default: all)")
|
|
parser.add_argument("--timeout", type=int, default=900,
|
|
help="seconds per veracrypt call (default: 900)")
|
|
args = parser.parse_args()
|
|
|
|
if args.skip_official and not args.binary_b:
|
|
parser.error("--skip-official needs --binary-b, "
|
|
"otherwise there is nothing to test")
|
|
if args.skip_official and args.fixture:
|
|
parser.error("--fixture and --skip-official exclude each other")
|
|
|
|
binaries = [str(Path(args.binary_a).resolve())]
|
|
if args.binary_b:
|
|
binaries.append(str(Path(args.binary_b).resolve()))
|
|
fixtures = [] if args.skip_official else select_fixtures(args.fixture)
|
|
|
|
failures = 0
|
|
with tempfile.TemporaryDirectory(prefix="vc-byte-order-") as tmp:
|
|
workdir = Path(tmp)
|
|
runner = Runner(workdir, args.timeout)
|
|
for binary in binaries:
|
|
failures += official_volumes(runner, binary, fixtures, workdir)
|
|
if len(binaries) == 2:
|
|
failures += interop(runner, binaries[0], binaries[1], workdir)
|
|
failures += interop(runner, binaries[1], binaries[0], workdir)
|
|
|
|
if failures:
|
|
sys.exit(f"Failed: {failures} check(s)")
|
|
print("All checks passed", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|