From c91f386f0059a02ab835746a91ecac8955eef95c Mon Sep 17 00:00:00 2001 From: Mounir IDRASSI Date: Mon, 28 Sep 2026 15:23:22 +0200 Subject: [PATCH] Unix: preserve automatic backup-header errors Limit backup retry recovery to credential errors. Restore header selection and rethrow other failures, including cancellation, in CLI and GUI. Add regression coverage for fallback and credential recovery. --- Tests/test_backup_header_retry.py | 312 ++++++++++++++++++++++++++++++ src/Main/GraphicUserInterface.cpp | 8 +- src/Main/TextUserInterface.cpp | 8 +- 3 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 Tests/test_backup_header_retry.py diff --git a/Tests/test_backup_header_retry.py b/Tests/test_backup_header_retry.py new file mode 100644 index 00000000..7695690a --- /dev/null +++ b/Tests/test_backup_header_retry.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +"""Exercise the production CLI/GUI mount loops with scripted backend outcomes. + +Run after building VeraCrypt: python3 Tests/test_backup_header_retry.py +--build-dir may point to another matching build's src directory. Requires a C++11 +compiler and Python 3 on macOS or Linux; no GUI, mounted volumes or sudo needed. +The real option, credential and exception types are linked from that build. +""" + +import argparse +import os +from pathlib import Path +import shlex +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + +HARNESS = r''' +#include "Core/MountOptions.h" +#include "Core/CoreException.h" +struct wxString : std::wstring { using std::wstring::wstring; }; +#include "Main/UserInterfaceException.h" +#include "Volume/VolumeInfo.h" +#include +#include +#include +#include + +namespace VeraCrypt { +static void Require (bool condition, const char *message) { + if (!condition) throw std::runtime_error (message); +} +enum Outcome { Success, Incorrect, KeyfilesIncorrect, MountOptionsIncorrect, + ProtectionIncorrect, ProtectionKeyfilesIncorrect, BackendFailure, IoFailure, + Cancelled, StandardFailure, UnknownFailure }; +struct UnknownError { int Code; }; +struct Attempt { + bool Backup, Cached; + shared_ptr Password; + int Pim; + shared_ptr Keyfiles; +}; +static struct { + vector Plan; + vector Calls; + vector Messages; + vector BackupWarnings; + std::exception_ptr Error; + bool Cache = false; + int OuterPrompts = 0, HiddenPrompts = 0; +} State; + +static shared_ptr Password () { + return make_shared (reinterpret_cast ("test"), 4); +} +static struct { + bool IsVolumeMounted (const VolumePath &) const { return false; } + bool IsPasswordCacheEmpty () const { return !State.Cache; } +} CoreInstance, *Core = &CoreInstance; +static struct { bool ArgNoHiddenVolumeProtection = true; } CommandLine, *CmdLine = &CommandLine; +static struct { + wstring operator[] (const char *key) const { string s (key); return wstring (s.begin(), s.end()); } +} LangString; +static wstring StringFormatter (const wstring &format, const wstring &argument) { return format + argument; } +#define _(s) L##s +struct wxBusyCursor {}; +static const int wxID_OK = 1; +struct UserPreferences { + bool NonInteractive = false, DisableKernelEncryptionModeWarning = true, UseKeyfiles = false; + KeyfileList DefaultKeyfiles; +}; +class UserInterface { +public: + UserPreferences Preferences; + const UserPreferences &GetPreferences () const { return Preferences; } + void CheckRequirementsForMountingVolume () const {} + void ShowInfo (const exception &e) const { State.Messages.push_back (e.what()); } + void ShowInfo (const wstring &) const {} + void ShowWarning (const exception &e) const { ShowInfo (e); } + void ShowWarning (const wstring &) const {} + void ShowWarning (const char *key) const { + if (string (key) == "HEADER_DAMAGED_AUTO_USED_HEADER_BAK") + State.BackupWarnings.push_back (State.Calls.size()); + } + void ShowError (const exception &) const { State.Error = std::current_exception(); } + void ShowString (const wstring &) const {} + bool AskYesNo (const wstring &, bool = false, bool = false) const { return false; } + shared_ptr AskPassword (const wstring &prompt) const { + if (prompt.find (L"hidden") != wstring::npos) ++State.HiddenPrompts; + else ++State.OuterPrompts; + return Password(); + } + int AskPim (const wstring &) const { return 7; } + shared_ptr AskKeyfiles (const wstring & = L"") const { return make_shared(); } + wstring AskString (const wstring &) const { return L""; } + shared_ptr AskVolumePath () const { throw std::runtime_error ("unexpected path prompt"); } + shared_ptr MountVolume (MountOptions &options) const { + Require (State.Calls.size() < State.Plan.size(), "unexpected additional mount attempt"); + bool cached = State.Cache && (!options.Password || options.Password->IsEmpty()) + && (!options.Keyfiles || options.Keyfiles->empty()); + Outcome outcome = State.Plan[State.Calls.size()]; + State.Calls.push_back ({options.UseBackupHeaders, cached, options.Password, options.Pim, options.Keyfiles}); + // Match CoreServiceProxy's cleanup after trying cached outer credentials. + if (cached) options.Password.reset(); + string message = options.UseBackupHeaders ? "backup" : "primary"; + switch (outcome) { + case Success: return make_shared(); + case Incorrect: throw PasswordIncorrect (message); + case KeyfilesIncorrect: throw PasswordKeyfilesIncorrect (message); + case MountOptionsIncorrect: throw PasswordOrMountOptionsIncorrect (message); + case ProtectionIncorrect: throw ProtectionPasswordIncorrect ("protection"); + case ProtectionKeyfilesIncorrect: throw ProtectionPasswordKeyfilesIncorrect ("protection-keyfiles"); + case BackendFailure: throw ExecutedProcessFailed (message, "mount", 37, "backend details"); + case IoFailure: throw SystemException (message, EIO); + case Cancelled: throw UserAbort (message, L"cancelled mount"); + case StandardFailure: throw std::runtime_error (message); + case UnknownFailure: throw UnknownError {42}; + } + throw std::runtime_error ("invalid outcome"); + } +}; +class TextUserInterface : public UserInterface { +public: + shared_ptr MountVolume (MountOptions &, bool) const; +}; +class GraphicUserInterface : public UserInterface { +public: + shared_ptr MountVolumeInternal (MountOptions &, bool, bool) const; + int GetActiveWindow () const { return 0; } + int GetTopWindow () const { return 0; } + VolumePath SelectVolumeFile (int) const { throw std::runtime_error ("unexpected file dialog"); } + void SetPreferences (const UserPreferences &) {} +}; +static GraphicUserInterface *Gui; +class MountOptionsDialog { + MountOptions &Options; + bool ProtectionRecovery = false; +public: + MountOptionsDialog (int, MountOptions &options) : Options (options) {} + void Hide () {} + void SetProtectionRecovery (bool recovery) { ProtectionRecovery = recovery; } + int ShowModal () { + Require (State.Calls.size() < 12, "unexpected credential retry loop"); + if (!ProtectionRecovery) { + ++State.OuterPrompts; + Options.Password = Password(); + Options.Pim = 7; + if (!Options.Keyfiles) Options.Keyfiles = make_shared(); + } else { + ++State.HiddenPrompts; + Options.ProtectionPassword = Password(); + Options.ProtectionPim = 7; + Options.ProtectionKeyfiles = make_shared(); + } + return wxID_OK; + } +}; + +// PRODUCTION_METHODS + +static shared_ptr Mount (bool gui, MountOptions &options, bool cached = false) { + try { + if (gui) return GraphicUserInterface().MountVolumeInternal (options, cached, false); + return TextUserInterface().MountVolume (options, cached); + } catch (...) { State.Error = std::current_exception(); } + return shared_ptr(); +} +static void Reset (MountOptions &options, const char *path, vector plan) { + State = decltype(State)(); + State.Plan = plan; + options.Path = make_shared (string (path)); + options.NoFilesystem = true; + options.Keyfiles = make_shared(); +} +static void CheckFailure (Outcome failure) { + Require (bool (State.Error), "backup failure was swallowed"); + try { std::rethrow_exception (State.Error); } + catch (ExecutedProcessFailed &e) { + Require (failure == BackendFailure && string (e.what()) == "backup" + && e.GetCommand() == "mount" && e.GetExitCode() == 37 && e.GetErrorOutput() == "backend details", + "backend failure details changed"); + } catch (SystemException &e) { + Require (failure == IoFailure && e.GetErrorCode() == EIO && string (e.what()) == "backup", + "I/O failure details changed"); + } catch (UserAbort &e) { + Require (failure == Cancelled && string (e.what()) == "backup" && e.GetSubject() == L"cancelled mount", + "cancellation changed"); + } catch (std::runtime_error &e) { + Require (failure == StandardFailure && string (e.what()) == "backup", "standard exception changed"); + } catch (UnknownError &e) { + Require (failure == UnknownFailure && e.Code == 42, "unknown exception changed"); + } +} +static void CheckHeaders (std::initializer_list expected) { + Require (State.Calls.size() == expected.size(), "wrong number of mount attempts"); + size_t i = 0; + for (bool backup : expected) Require (State.Calls[i++].Backup == backup, "wrong header selected"); +} +static void Run (bool gui, const char *path) { + for (Outcome failure : {BackendFailure, IoFailure, Cancelled, StandardFailure, UnknownFailure}) { + MountOptions options; + Reset (options, path, {Incorrect, Incorrect, Incorrect, failure, Success}); + Require (!Mount (gui, options), "backup failure retried until success"); + CheckFailure (failure); + CheckHeaders ({false, false, false, true}); + Require (!options.UseBackupHeaders, "temporary backup selection leaked after failure"); + Require (options.Password == State.Calls.back().Password && options.Pim == State.Calls.back().Pim, + "unrelated failure discarded credentials"); + Require (State.Messages == vector ({"primary", "primary"}), "failure misreported as bad credentials"); + Require (State.OuterPrompts == 3 && State.BackupWarnings.empty(), "failure prompted again or warned of success"); + } + for (Outcome failure : {Incorrect, KeyfilesIncorrect, MountOptionsIncorrect}) { + MountOptions options; + Reset (options, path, {Incorrect, Incorrect, Incorrect, failure, Success}); + Require (bool (Mount (gui, options)) && !State.Error, "credential retry failed"); + CheckHeaders ({false, false, false, true, false}); + Require (!options.UseBackupHeaders && State.BackupWarnings.empty(), "failed fallback reported as successful"); + Require (State.OuterPrompts == 4 && State.Messages == vector ({"primary", "primary", "primary"}), + "credential retry reporting changed"); + } + for (Outcome outcome : {Success, ProtectionIncorrect, ProtectionKeyfilesIncorrect}) { + MountOptions options; + Reset (options, path, {Incorrect, Incorrect, Incorrect, outcome, Success}); + options.Protection = VolumeProtection::HiddenVolumeReadOnly; + options.ProtectionPassword = Password(); + options.ProtectionPim = 7; + options.ProtectionKeyfiles = make_shared(); + Require (bool (Mount (gui, options)) && !State.Error, "backup/protection recovery failed"); + if (outcome == Success) CheckHeaders ({false, false, false, true}); + else { + CheckHeaders ({false, false, false, true, true}); + Require (State.Calls[3].Password == State.Calls[4].Password + && State.Calls[3].Pim == State.Calls[4].Pim && State.Calls[3].Keyfiles == State.Calls[4].Keyfiles, + "protection recovery replaced accepted outer credentials"); + Require (State.HiddenPrompts == 1, "hidden credentials were not recovered"); + } + Require (State.OuterPrompts == 3 && options.UseBackupHeaders, "accepted backup selection lost"); + Require (State.BackupWarnings == vector ({State.Calls.size()}), "backup warning not deferred until success"); + } + for (Outcome outcome : {Success, BackendFailure, Cancelled}) { + MountOptions options; + Reset (options, path, {outcome}); + options.UseBackupHeaders = true; + shared_ptr volume = Mount (gui, options); + if (outcome != Success) CheckFailure (outcome); + else Require (bool (volume) && !State.Error, "explicit backup mount failed"); + CheckHeaders ({true}); + Require (options.UseBackupHeaders && State.BackupWarnings.empty(), "explicit backup option changed"); + } + for (Outcome outcome : {Success, ProtectionIncorrect, ProtectionKeyfilesIncorrect}) { + MountOptions options; + Reset (options, path, {outcome, Success}); + State.Cache = true; + options.Protection = VolumeProtection::HiddenVolumeReadOnly; + options.ProtectionPassword = Password(); + options.ProtectionPim = 7; + options.ProtectionKeyfiles = make_shared(); + Require (bool (Mount (gui, options, true)) && !State.Error, "cached credential recovery failed"); + Require (State.Calls.size() == (outcome == Success ? 1 : 2), "unexpected cache retry"); + for (const Attempt &attempt : State.Calls) Require (attempt.Cached && !attempt.Backup, "cached source changed"); + Require (State.OuterPrompts == 0 && State.BackupWarnings.empty(), "cache recovery asked for outer password"); + } + std::cout << "PASS: " << (gui ? "GUI" : "CLI") << " backup failures, cancellation, credential retries, protection and cache recovery\n"; +} +} +int main (int argc, char **argv) { + if (argc != 2) return 2; + int failures = 0; + for (bool gui : {false, true}) { + try { VeraCrypt::Run (gui, argv[1]); } + catch (const std::exception &e) { + std::cerr << (gui ? "GUI: " : "CLI: ") << e.what() << '\n'; + ++failures; + } + catch (...) { std::cerr << "unexpected exception\n"; ++failures; } + } + return failures ? 1 : 0; +} +''' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--build-dir", type=Path, default=ROOT / "src") + args = parser.parse_args() + methods = [] + for filename, start, end in ( + ("TextUserInterface.cpp", "\tshared_ptr TextUserInterface::MountVolume (", "\tbool TextUserInterface::OnInit"), + ("GraphicUserInterface.cpp", "\tshared_ptr GraphicUserInterface::MountVolumeInternal (", "\tvoid GraphicUserInterface::OnAutoDismountAllEvent"), + ): + source = (ROOT / "src/Main" / filename).read_text() + methods.append(source[source.index(start):source.index(end)]) + with tempfile.TemporaryDirectory(prefix="vc-backup-retry-") as temporary: + work = Path(temporary) + unit = work / "backup_retry.cpp" + unit.write_text(HARNESS.replace("// PRODUCTION_METHODS", "\n".join(methods))) + command = shlex.split(os.environ.get("CXX", "c++")) + ["-std=c++11", "-DTC_UNIX"] + command += ["-DTC_MACOSX"] if sys.platform == "darwin" else ["-DTC_LINUX"] + command += ["-I" + str(ROOT / p) for p in ("src", "src/Crypto", "src/Crypto/Argon2/include")] + command += [str(unit)] + [str(args.build_dir.resolve() / p) for p in ("Core/Core.a", "Volume/Volume.a", "Platform/Platform.a")] + command += ["-Wl,-dead_strip"] if sys.platform == "darwin" else ["-Wl,--gc-sections", "-pthread", "-ldl"] + command += ["-o", str(work / "backup_retry")] + subprocess.run(command, check=True, timeout=120) + volume = work / "volume.hc" + volume.touch() + subprocess.run([str(work / "backup_retry"), str(volume)], check=True, timeout=20) + + +if __name__ == "__main__": + main() diff --git a/src/Main/GraphicUserInterface.cpp b/src/Main/GraphicUserInterface.cpp index e5477763..70fe5d2b 100644 --- a/src/Main/GraphicUserInterface.cpp +++ b/src/Main/GraphicUserInterface.cpp @@ -1091,12 +1091,18 @@ namespace VeraCrypt { volume = mountWithProtectionRecovery(); } - catch (...) + catch (PasswordException&) { options.UseBackupHeaders = false; autoBackupHeaderUsed = false; ShowWarning (e); } + catch (...) + { + options.UseBackupHeaders = false; + autoBackupHeaderUsed = false; + throw; + } } else ShowWarning (e); diff --git a/src/Main/TextUserInterface.cpp b/src/Main/TextUserInterface.cpp index 45ba6049..69821efd 100644 --- a/src/Main/TextUserInterface.cpp +++ b/src/Main/TextUserInterface.cpp @@ -1611,7 +1611,7 @@ namespace VeraCrypt { volume = mountWithProtectionRecovery(); } - catch (...) + catch (PasswordException&) { retryWithCachedPasswords = false; autoBackupHeaderUsed = false; @@ -1620,6 +1620,12 @@ namespace VeraCrypt options.Password.reset(); options.Pim = -1; } + catch (...) + { + autoBackupHeaderUsed = false; + options.UseBackupHeaders = false; + throw; + } } else {