From 92aa8d8eacecf3696f89d53f78c49724b841a681 Mon Sep 17 00:00:00 2001 From: Bernard Ladenthin Date: Fri, 7 Aug 2026 21:52:26 +0200 Subject: [PATCH] Tests: cover the security-critical paths of the crypto and platform layers. --- src/Main/UserInterface.cpp | 135 +++++++ src/Main/UserInterface.h | 2 + src/Platform/PlatformTest.cpp | 337 ++++++++++++++++++ src/Platform/PlatformTest.h | 4 + src/Volume/EncryptionTest.cpp | 647 ++++++++++++++++++++++++++++++++++ src/Volume/EncryptionTest.h | 8 + 6 files changed, 1133 insertions(+) diff --git a/src/Main/UserInterface.cpp b/src/Main/UserInterface.cpp index 8ea98fb6e0..4a1a8c4d86 100644 --- a/src/Main/UserInterface.cpp +++ b/src/Main/UserInterface.cpp @@ -29,6 +29,8 @@ #include "Platform/SystemException.h" #include "Common/SecurityToken.h" #include "Volume/EncryptionTest.h" +#include "Core/RandomNumberGenerator.h" +#include "Platform/MemoryStream.h" #include "Application.h" #include "FavoriteVolume.h" #include "UserInterface.h" @@ -1755,12 +1757,145 @@ const FileManager fileManagers[] = { return s.str(); } + // The random number generator is what produces master keys and salts, yet nothing in + // --test ever started it, so none of it ran under CI: Start() carries the generator's + // own pool-mixing self-test, and that self-test only executed when the graphical or the + // text interface happened to start the generator. + void UserInterface::TestRandomNumberGenerator () const + { + if (RandomNumberGenerator::IsRunning()) + throw TestFailed (SRC_POS); + + Buffer first (32), second (32); + + // Reading before the generator runs must be refused rather than return weak data + bool rejected = false; + try { RandomNumberGenerator::GetData (first); } catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + // Feeding entropy in must be refused for the same reason. This guard is the only + // thing enforcing the invariant: Start() has seven call sites but Stop() only two, + // both in user-interface destructors, so "the generator runs while a dialog is still + // delivering mouse events" rests entirely on object destruction order. Without the + // guard the write lands on a null pool pointer and the process takes a SIGSEGV. + uint8 entropy[16]; + memset (entropy, 0x5A, sizeof (entropy)); + + rejected = false; + try { RandomNumberGenerator::AddToPool (ConstBufferPtr (entropy, sizeof (entropy))); } + catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + RandomNumberGenerator::Start(); // runs the built-in pool-mixing self-test + + try + { + if (!RandomNumberGenerator::IsRunning()) + throw TestFailed (SRC_POS); + + RandomNumberGenerator::GetData (first); + RandomNumberGenerator::GetData (second); + + // Consecutive reads must differ, and neither may come back all zero + if (first.Size() != second.Size() || memcmp (first.Ptr(), second.Ptr(), first.Size()) == 0) + throw TestFailed (SRC_POS); + + bool allZero = true; + for (size_t i = 0; i < first.Size(); i++) + { + if (first[i] != 0) + { + allZero = false; + break; + } + } + + if (allZero) + throw TestFailed (SRC_POS); + + // A request larger than the pool is only legal when explicitly allowed + Buffer oversized (RandomNumberGenerator::PoolSize + 1); + + rejected = false; + try { RandomNumberGenerator::GetData (oversized); } catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + RandomNumberGenerator::GetData (oversized, true); + } + catch (...) + { + RandomNumberGenerator::Stop(); + throw; + } + + RandomNumberGenerator::Stop(); + + if (RandomNumberGenerator::IsRunning()) + throw TestFailed (SRC_POS); + } + + // MountOptions is what the unprivileged process hands to the privileged core service. + // It carries the password, the PIM and -- most importantly -- the protection mode. A + // field lost in transit would mount a volume writable that the user asked to protect, + // with no error anywhere. The Serializer validates names positionally, so ordering + // matters as much as completeness. None of this was covered. + void UserInterface::TestMountOptionsSerialization () const + { + MountOptions original; + + const uint8 secret[] = { 's', 'e', 'c', 'r', 'e', 't' }; + original.Password = make_shared (secret, sizeof (secret)); + original.Pim = 485; + original.Protection = VolumeProtection::HiddenVolumeReadOnly; + original.ProtectionPim = 191; + original.PreserveTimestamps = false; + original.CachePassword = true; + original.Removable = true; + original.NoFilesystem = true; + original.NoHardwareCrypto = true; + original.UseBackupHeaders = true; + original.SharedAccessAllowed = true; + + shared_ptr stream (new MemoryStream); + original.Serialize (stream); + + shared_ptr restored = Serializable::DeserializeNew (stream); + if (!restored) + throw TestFailed (SRC_POS); + + // The protection mode and its PIM must survive intact + if (restored->Protection != original.Protection || restored->ProtectionPim != original.ProtectionPim) + throw TestFailed (SRC_POS); + + if (restored->Pim != original.Pim) + throw TestFailed (SRC_POS); + + // The password must arrive byte-for-byte, or the volume simply will not open + if (!restored->Password || !(*restored->Password == *original.Password)) + throw TestFailed (SRC_POS); + + // Every boolean must keep its value; a silently defaulted flag is the dangerous case + if (restored->PreserveTimestamps != original.PreserveTimestamps + || restored->CachePassword != original.CachePassword + || restored->Removable != original.Removable + || restored->NoFilesystem != original.NoFilesystem + || restored->NoHardwareCrypto != original.NoHardwareCrypto + || restored->UseBackupHeaders != original.UseBackupHeaders + || restored->SharedAccessAllowed != original.SharedAccessAllowed) + throw TestFailed (SRC_POS); + } + void UserInterface::Test () const { if (!PlatformTest::TestAll()) throw TestFailed (SRC_POS); EncryptionTest::TestAll(); + TestRandomNumberGenerator(); + TestMountOptionsSerialization(); // StringFormatter if (static_cast(StringFormatter (L"{9} {8} {7} {6} {5} {4} {3} {2} {1} {0} {{0}}", "1", L"2", '3', L'4', 5, 6, 7, 8, 9, 10)) != L"10 9 8 7 6 5 4 3 2 1 {0}") diff --git a/src/Main/UserInterface.h b/src/Main/UserInterface.h index 6f6792bb60..c08b1ab9bc 100644 --- a/src/Main/UserInterface.h +++ b/src/Main/UserInterface.h @@ -79,6 +79,8 @@ namespace VeraCrypt virtual wxString SizeToString (uint64 size) const; virtual wxString SpeedToString (uint64 speed) const; virtual void Test () const; + virtual void TestRandomNumberGenerator () const; + virtual void TestMountOptionsSerialization () const; virtual wxString TimeSpanToString (uint64 seconds) const; virtual bool VolumeHasUnrecommendedExtension (const VolumePath &path) const; virtual void Yield () const = 0; diff --git a/src/Platform/PlatformTest.cpp b/src/Platform/PlatformTest.cpp index 11f6a49b1e..f9a144173a 100644 --- a/src/Platform/PlatformTest.cpp +++ b/src/Platform/PlatformTest.cpp @@ -346,9 +346,346 @@ namespace VeraCrypt SerializerTest(); ThreadTest(); + BufferTest(); + StringConverterTest(); + FileTest(); + ExceptionTransportTest(); return true; } + // File::Copy is what moves keyfiles and header backups around, and ReadCompleteBuffer is + // used wherever a short read would be a silent truncation. Neither had coverage. + void PlatformTest::FileTest () + { + const char *sourcePath = "veracrypt-test-file-src.tmp"; + const char *copyPath = "veracrypt-test-file-dst.tmp"; + + struct TempFiles + { + const char *A, *B; + ~TempFiles () + { + const char *paths[] = { A, B }; + for (size_t i = 0; i < 2; i++) + { + try { File f; f.Open (FilePath (paths[i]), File::OpenReadWrite); f.Delete(); } + catch (...) { } + } + } + } cleanup = { sourcePath, copyPath }; + + Buffer content (4096); + for (size_t i = 0; i < content.Size(); i++) + content[i] = (uint8) (i * 11 + 3); + + { + File source; + source.Open (FilePath (sourcePath), File::CreateReadWrite); + source.Write (content); + + if (source.Length() != (uint64) content.Size()) + throw TestFailed (SRC_POS); + + if (string (source.GetPath()) != string (sourcePath)) + throw TestFailed (SRC_POS); + } + + // A copy has to reproduce the source byte for byte + File::Copy (FilePath (sourcePath), FilePath (copyPath)); + + { + File copy; + copy.Open (FilePath (copyPath), File::OpenRead); + + if (copy.Length() != (uint64) content.Size()) + throw TestFailed (SRC_POS); + + Buffer readBack (content.Size()); + copy.ReadCompleteBuffer (readBack); + + if (memcmp (readBack.Ptr(), content.Ptr(), content.Size()) != 0) + throw TestFailed (SRC_POS); + } + + // Asking for more than the file holds must fail rather than return a partial buffer + { + File copy; + copy.Open (FilePath (copyPath), File::OpenRead); + + Buffer tooLarge (content.Size() * 2); + bool rejected = false; + try { copy.ReadCompleteBuffer (tooLarge); } + catch (InsufficientData&) { rejected = true; } + catch (ParameterIncorrect&) { rejected = true; } + + if (!rejected) + throw TestFailed (SRC_POS); + } + + // A default-constructed File reports itself as not open. Note that the accessors do + // NOT enforce this: every ValidateState() call in Platform/Unix/File.cpp sits behind + // if_debug and is compiled out of release builds, so Length() on a closed file runs + // lseek on an uninitialised handle rather than throwing. Only the flag is contractual. + { + File closed; + if (closed.IsOpen()) + throw TestFailed (SRC_POS); + } + + // Opening a path that does not exist must fail rather than yield an unusable handle + { + File missing; + bool rejected = false; + try { missing.Open (FilePath ("veracrypt-test-no-such-file.tmp"), File::OpenRead); } + catch (SystemException&) { rejected = true; } + catch (Exception&) { rejected = true; } + + if (!rejected || missing.IsOpen()) + throw TestFailed (SRC_POS); + } + } + + // When the privileged core service fails, it serialises the exception to its stderr and + // the unprivileged side reconstructs and rethrows it (CoreService.cpp:582-590). If a type + // is missing from the factory the real cause is replaced by a generic failure, so this + // checks that the dynamic type and the message both survive the round trip. + void PlatformTest::ExceptionTransportTest () + { + // A plain Exception carrying a subject + { + Exception original (SRC_POS, L"subject-text"); + + shared_ptr stream (new MemoryStream); + original.Serialize (stream); + + unique_ptr restored (Serializable::DeserializeNew (stream)); + if (!restored) + throw TestFailed (SRC_POS); + + Exception *asException = dynamic_cast (restored.get()); + if (!asException) + throw TestFailed (SRC_POS); + + if (asException->GetSubject() != original.GetSubject()) + throw TestFailed (SRC_POS); + } + + // A derived type must come back as that same type, not as its base + { + ParameterIncorrect original (SRC_POS); + + shared_ptr stream (new MemoryStream); + original.Serialize (stream); + + unique_ptr restored (Serializable::DeserializeNew (stream)); + if (!restored) + throw TestFailed (SRC_POS); + + if (dynamic_cast (restored.get()) == nullptr) + throw TestFailed (SRC_POS); + } + + // ... and the same for one carrying extra state + { + ExecutedProcessFailed original (SRC_POS, "/bin/false", 1, "stderr text"); + + shared_ptr stream (new MemoryStream); + original.Serialize (stream); + + unique_ptr restored (Serializable::DeserializeNew (stream)); + ExecutedProcessFailed *typed = dynamic_cast (restored.get()); + + if (!typed) + throw TestFailed (SRC_POS); + + if (typed->GetCommand() != original.GetCommand() + || typed->GetExitCode() != original.GetExitCode() + || typed->GetErrorOutput() != original.GetErrorOutput()) + throw TestFailed (SRC_POS); + } + } + + // StringConverter parses command-line input: the PIM, volume sizes, favourite-volume + // attributes and hotkey codes all pass through here. The behaviour on malformed input was + // never pinned down, so this records what the parsers actually do -- including the + // deliberate rejection of the all-ones value, which CommandLineInterface uses as its + // "maximum available size" marker and which user input must therefore never produce. + void PlatformTest::StringConverterTest () + { + // Well-formed input round-trips + if (StringConverter::ToUInt32 ("4294967294") != 4294967294U) + throw TestFailed (SRC_POS); + if (StringConverter::ToUInt64 ("18446744073709551614") != 18446744073709551614ULL) + throw TestFailed (SRC_POS); + if (StringConverter::ToInt32 ("-42") != -42) + throw TestFailed (SRC_POS); + if (StringConverter::FromNumber ((uint32) 4294967295U) != L"4294967295") + throw TestFailed (SRC_POS); + if (StringConverter::FromNumber ((int64) -9223372036854775807LL) != L"-9223372036854775807") + throw TestFailed (SRC_POS); + + // Empty and non-numeric input is refused + const char *rejected[] = { "", "abc", "4294967296" }; + for (size_t i = 0; i < array_capacity (rejected); i++) + { + bool threw = false; + try { StringConverter::ToUInt32 (rejected[i]); } catch (ParameterIncorrect&) { threw = true; } + if (!threw) + throw TestFailed (SRC_POS); + } + + // The all-ones sentinel must never come out of user input + { + bool threw = false; + try { StringConverter::ToUInt64 ("18446744073709551615"); } catch (ParameterIncorrect&) { threw = true; } + if (!threw) + throw TestFailed (SRC_POS); + + threw = false; + try { StringConverter::ToUInt32 ("4294967295"); } catch (ParameterIncorrect&) { threw = true; } + if (!threw) + throw TestFailed (SRC_POS); + } + + // Splitting and trimming, as used when parsing option lists + vector parts = StringConverter::Split ("a,b,,c", ","); + if (parts.size() != 3 || parts[0] != "a" || parts[1] != "b" || parts[2] != "c") + throw TestFailed (SRC_POS); + + parts = StringConverter::Split ("a,b,,c", ",", true); + if (parts.size() != 4 || !parts[2].empty()) + throw TestFailed (SRC_POS); + + if (StringConverter::Trim ("\t hello \r\n") != "hello") + throw TestFailed (SRC_POS); + + if (StringConverter::ToLower ("MiXeD") != "mixed") + throw TestFailed (SRC_POS); + + // GetTrailingNumber / StripTrailingNumber are a pair and must agree + if (StringConverter::GetTrailingNumber ("sda12") != "12") + throw TestFailed (SRC_POS); + if (StringConverter::StripTrailingNumber ("sda12") != "sda") + throw TestFailed (SRC_POS); + + { + bool threw = false; + try { StringConverter::GetTrailingNumber ("sda"); } catch (ParameterIncorrect&) { threw = true; } + if (!threw) + throw TestFailed (SRC_POS); + } + + // Erase must actually clear the string, not just resize it + { + string s = "secret"; + StringConverter::Erase (s); + for (size_t i = 0; i < s.size(); i++) + { + if (s[i] != ' ' && s[i] != 0) + throw TestFailed (SRC_POS); + } + } + } + + // Buffer and SecureBuffer hold key material, so the properties that matter are that a + // SecureBuffer wipes itself before releasing memory and that every out-of-range access + // is refused rather than silently truncated. None of the rejection paths, neither + // destructor and none of Memory::Compare had ever been executed by the test suite. + void PlatformTest::BufferTest () + { + // A zero-sized allocation is not a valid request + bool rejected = false; + try { Memory::Allocate (0); } catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + rejected = false; + try { Memory::AllocateAligned (0, 16); } catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + // Memory::Compare orders by size first, then by content + const uint8 a[] = { 1, 2, 3 }; + const uint8 b[] = { 1, 2, 4 }; + + if (Memory::Compare (a, sizeof (a), b, sizeof (b) - 1) <= 0) // longer -> positive + throw TestFailed (SRC_POS); + if (Memory::Compare (a, sizeof (a) - 1, b, sizeof (b)) >= 0) // shorter -> negative + throw TestFailed (SRC_POS); + if (Memory::Compare (a, sizeof (a), a, sizeof (a)) != 0) // identical + throw TestFailed (SRC_POS); + if (Memory::Compare (a, sizeof (a), b, sizeof (b)) >= 0) // same size, a < b + throw TestFailed (SRC_POS); + + // An unallocated buffer must not pretend to hold data, and releasing one is an error + { + Buffer buffer; + if (buffer.Size() != 0 || buffer.IsAllocated()) + throw TestFailed (SRC_POS); + + rejected = false; + try { buffer.Free(); } catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + } + + // Out-of-range access is refused, not truncated + { + Buffer buffer (64); + + rejected = false; + try { buffer.GetRange (32, 64); } catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + Buffer oversized (128); + rejected = false; + try { buffer.CopyFrom (oversized); } catch (ParameterTooLarge&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + // A range fully inside the buffer must work and must alias the same memory + BufferPtr range = buffer.GetRange (16, 16); + if (range.Size() != 16 || range.Get() != buffer.Ptr() + 16) + throw TestFailed (SRC_POS); + } + + // Erase() must actually clear the bytes, and SecureBuffer::Free() must erase first + { + SecureBuffer secure (64); + memset (secure.Ptr(), 0xA5, secure.Size()); + + bool anyNonZero = false; + for (size_t i = 0; i < secure.Size(); i++) + { + if (secure[i] != 0) + { + anyNonZero = true; + break; + } + } + if (!anyNonZero) + throw TestFailed (SRC_POS); + + secure.Erase(); + + for (size_t i = 0; i < secure.Size(); i++) + { + if (secure[i] != 0) + throw TestFailed (SRC_POS); + } + } + + // Freeing a SecureBuffer that owns nothing is a programming error, not a no-op + { + SecureBuffer secure; + rejected = false; + try { secure.Free(); } catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + } + } + bool PlatformTest::TestFlag; } diff --git a/src/Platform/PlatformTest.h b/src/Platform/PlatformTest.h index 3914b16b80..861d3771e1 100644 --- a/src/Platform/PlatformTest.h +++ b/src/Platform/PlatformTest.h @@ -36,7 +36,11 @@ namespace VeraCrypt }; PlatformTest (); + static void BufferTest (); + static void ExceptionTransportTest (); + static void FileTest (); static void SerializerTest (); + static void StringConverterTest (); static void ThreadTest (); static TC_THREAD_PROC ThreadTestProc (void *param); diff --git a/src/Volume/EncryptionTest.cpp b/src/Volume/EncryptionTest.cpp index 721f91430d..40983ecd8d 100644 --- a/src/Volume/EncryptionTest.cpp +++ b/src/Volume/EncryptionTest.cpp @@ -22,6 +22,12 @@ #include "EncryptionTest.h" #include "Pkcs5Kdf.h" #include "VolumeHeader.h" +#include "VolumePasswordCache.h" +#include "Keyfile.h" +#include "VolumeInfo.h" +#include "VolumeLayout.h" +#include "Platform/File.h" +#include "Platform/MemoryStream.h" namespace VeraCrypt { @@ -85,6 +91,647 @@ namespace VeraCrypt TestXtsAES(); TestXts(); TestPkcs5(); + TestEdgeCases(); + TestHashClasses(); + TestKdfSelection(); + TestVolumeHeaderRejection(); + TestPasswordHandling(); + TestKeyfileApplication(); + TestVolumeInfoSerialization(); + TestVolumeLayouts(); + } + + // The layouts decide where a header sits and how much of a host file is usable. Getting an + // offset wrong points header reads at the wrong bytes; getting a size wrong lets a volume + // claim space it does not own. None of the four layouts was ever instantiated by a test. + void EncryptionTest::TestVolumeLayouts () + { + const uint64 hostSize = 100ULL * BYTES_PER_MB; + + VolumeLayoutList all = VolumeLayout::GetAvailableLayouts(); + if (all.size() < 4) + throw TestFailed (SRC_POS); + + foreach_ref (VolumeLayout &layout, all) + { + if (layout.GetHeaderSize() == 0) + throw TestFailed (SRC_POS); + + // A layout must offer at least one algorithm and one mode, otherwise nothing + // could ever be mounted with it + if (layout.GetSupportedEncryptionAlgorithms().empty() + || layout.GetSupportedEncryptionModes().empty() + || layout.GetSupportedKeyDerivationFunctions().empty()) + throw TestFailed (SRC_POS); + + // GetMaxDataSize is deliberately NotApplicable on the legacy and the + // system-encryption layout; where it is available it must not exceed the host + bool maxDataSizeApplicable = true; + uint64 maxDataSize = 0; + try { maxDataSize = layout.GetMaxDataSize (hostSize); } + catch (NotApplicable&) { maxDataSizeApplicable = false; } + + if (maxDataSizeApplicable && maxDataSize > hostSize) + throw TestFailed (SRC_POS); + + // GetDataOffset/GetDataSize read through Header on the V2 layouts, so a header + // has to be present first. GetHeader() creates one on demand, which is also the + // only guard against the null dereference at VolumeLayout.cpp:131/136/180/185 -- + // those two accessors do not check, unlike the rest of the code base. + shared_ptr header = layout.GetHeader(); + if (!header) + throw TestFailed (SRC_POS); + + // The data area has to fit inside the host + uint64 dataOffset = layout.GetDataOffset (hostSize); + uint64 dataSize = layout.GetDataSize (hostSize); + + if (dataOffset > hostSize || dataSize > hostSize || dataOffset + dataSize > hostSize) + throw TestFailed (SRC_POS); + + // A backup header, where one exists, must lie inside the host as well. Layouts + // without one report NotApplicable rather than a bogus offset. + if (layout.HasBackupHeader()) + { + int backupOffset = layout.GetBackupHeaderOffset(); + uint64 absolute = (backupOffset < 0) + ? hostSize - (uint64) (-(int64) backupOffset) + : (uint64) backupOffset; + + if (absolute >= hostSize) + throw TestFailed (SRC_POS); + } + else + { + bool notApplicable = false; + try { layout.GetBackupHeaderOffset(); } catch (NotApplicable&) { notApplicable = true; } + if (!notApplicable) + throw TestFailed (SRC_POS); + } + } + + // Filtering by type must return only layouts of that type, and never more than all + VolumeType::Enum types[] = { VolumeType::Normal, VolumeType::Hidden }; + + for (size_t t = 0; t < array_capacity (types); t++) + { + VolumeType::Enum type = types[t]; + VolumeLayoutList filtered = VolumeLayout::GetAvailableLayouts (type); + if (filtered.empty() || filtered.size() > all.size()) + throw TestFailed (SRC_POS); + + foreach_ref (const VolumeLayout &layout, filtered) + { + if (layout.GetType() != type) + throw TestFailed (SRC_POS); + } + } + } + + // VolumeInfo crosses the IPC boundary to the privileged core service, carrying among other + // things the protection mode of a mounted volume. The Serializer validates field names + // positionally, so a reordering breaks the transfer as surely as a missing field does -- + // and neither end had any test coverage. + void EncryptionTest::TestVolumeInfoSerialization () + { + VolumeInfo original; + + original.EncryptionAlgorithmBlockSize = 16; + original.EncryptionAlgorithmKeySize = 64; + original.EncryptionAlgorithmMinBlockSize = 16; + original.EncryptionAlgorithmName = L"AES-Twofish"; + original.EncryptionModeName = L"XTS"; + original.HiddenVolumeProtectionTriggered = true; + original.MinRequiredProgramVersion = 0x10b; + original.Pkcs5IterationCount = 500000; + original.Pkcs5PrfName = L"HMAC-SHA-512"; + original.ProgramVersion = 0x126; + original.Protection = VolumeProtection::HiddenVolumeReadOnly; + original.SerialInstanceNumber = 0x0123456789abcdefULL; + original.Size = 1024ULL * 1024 * 1024; + original.Type = VolumeType::Hidden; + + shared_ptr stream (new MemoryStream); + original.Serialize (stream); + + shared_ptr restored = Serializable::DeserializeNew (stream); + if (!restored) + throw TestFailed (SRC_POS); + + // The protection mode is the field that matters most: losing it would silently mount + // a volume writable that the user asked to be protected + if (restored->Protection != original.Protection) + throw TestFailed (SRC_POS); + + if (restored->HiddenVolumeProtectionTriggered != original.HiddenVolumeProtectionTriggered) + throw TestFailed (SRC_POS); + + if (restored->Type != original.Type) + throw TestFailed (SRC_POS); + + if (restored->Size != original.Size + || restored->SerialInstanceNumber != original.SerialInstanceNumber + || restored->Pkcs5IterationCount != original.Pkcs5IterationCount + || restored->ProgramVersion != original.ProgramVersion + || restored->MinRequiredProgramVersion != original.MinRequiredProgramVersion) + throw TestFailed (SRC_POS); + + if (restored->EncryptionAlgorithmName != original.EncryptionAlgorithmName + || restored->EncryptionModeName != original.EncryptionModeName + || restored->Pkcs5PrfName != original.Pkcs5PrfName) + throw TestFailed (SRC_POS); + + if (restored->EncryptionAlgorithmBlockSize != original.EncryptionAlgorithmBlockSize + || restored->EncryptionAlgorithmKeySize != original.EncryptionAlgorithmKeySize + || restored->EncryptionAlgorithmMinBlockSize != original.EncryptionAlgorithmMinBlockSize) + throw TestFailed (SRC_POS); + } + + // Keyfiles are mixed into the password with a CRC32 cascade before key derivation, so a + // fault here weakens every key derived from that password without any visible symptom. + // The whole path was unexecuted. Temporary files are used because Keyfile::Apply reads + // from the filesystem; they are removed again even if an assertion fires. + void EncryptionTest::TestKeyfileApplication () + { + const char *pathA = "veracrypt-test-keyfile-a.tmp"; + const char *pathB = "veracrypt-test-keyfile-b.tmp"; + const char *pathEmpty = "veracrypt-test-keyfile-empty.tmp"; + + struct TempFiles + { + const char *A, *B, *Empty; + ~TempFiles () + { + const char *paths[] = { A, B, Empty }; + for (size_t i = 0; i < 3; i++) + { + try + { + File f; + f.Open (FilePath (paths[i]), File::OpenReadWrite); + f.Delete(); + } + catch (...) { } + } + } + } cleanup = { pathA, pathB, pathEmpty }; + + // Two keyfiles with different content, and one that is empty + { + Buffer contentA (4096), contentB (4096); + for (size_t i = 0; i < contentA.Size(); i++) + { + contentA[i] = (uint8) (i * 3 + 1); + contentB[i] = (uint8) (i * 5 + 7); + } + + File fileA; + fileA.Open (FilePath (pathA), File::CreateReadWrite); + fileA.Write (contentA); + fileA.Close(); + + File fileB; + fileB.Open (FilePath (pathB), File::CreateReadWrite); + fileB.Write (contentB); + fileB.Close(); + + File fileEmpty; + fileEmpty.Open (FilePath (pathEmpty), File::CreateReadWrite); + fileEmpty.Close(); + } + + const uint8 secret[] = { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }; + make_shared_auto (VolumePassword, password); + password->Set (secret, sizeof (secret)); + + // An absent or empty keyfile list must hand the password back untouched + { + shared_ptr unchanged = Keyfile::ApplyListToPassword (shared_ptr (), password); + if (!unchanged || !(*unchanged == *password)) + throw TestFailed (SRC_POS); + + shared_ptr emptyList (new KeyfileList); + unchanged = Keyfile::ApplyListToPassword (emptyList, password); + if (!unchanged || !(*unchanged == *password)) + throw TestFailed (SRC_POS); + } + + // Applying a keyfile must change the password, and do so reproducibly + shared_ptr listA (new KeyfileList); + listA->push_back (make_shared (FilesystemPath (pathA))); + + shared_ptr withA = Keyfile::ApplyListToPassword (listA, password); + if (!withA || *withA == *password) + throw TestFailed (SRC_POS); + + shared_ptr withAAgain = Keyfile::ApplyListToPassword (listA, password); + if (!withAAgain || !(*withA == *withAAgain)) + throw TestFailed (SRC_POS); + + // Different keyfile content must produce a different password + { + shared_ptr listB (new KeyfileList); + listB->push_back (make_shared (FilesystemPath (pathB))); + + shared_ptr withB = Keyfile::ApplyListToPassword (listB, password); + if (!withB || *withA == *withB) + throw TestFailed (SRC_POS); + } + + // A different base password must produce a different result from the same keyfile + { + const uint8 otherSecret[] = { 'p', 'a', 's', 's', 'w', 'o', 'r', 'e' }; + make_shared_auto (VolumePassword, otherPassword); + otherPassword->Set (otherSecret, sizeof (otherSecret)); + + shared_ptr other = Keyfile::ApplyListToPassword (listA, otherPassword); + if (!other || *withA == *other) + throw TestFailed (SRC_POS); + } + + // An empty keyfile carries no entropy and must be refused rather than ignored + { + shared_ptr listEmpty (new KeyfileList); + listEmpty->push_back (make_shared (FilesystemPath (pathEmpty))); + + bool rejected = false; + try { Keyfile::ApplyListToPassword (listEmpty, password); } + catch (InsufficientData&) { rejected = true; } + + if (!rejected) + throw TestFailed (SRC_POS); + } + } + + // Passwords are held in a bounded in-memory cache and travel over the IPC channel to the + // privileged core service in serialised form. Neither the cache nor that round trip had + // ever been executed, so an oversized password, a full cache or a mangled transfer would + // all have gone unnoticed. + void EncryptionTest::TestPasswordHandling () + { + // A password longer than the maximum is refused rather than truncated + { + Buffer oversized (VolumePassword::MaxSize + 1); + memset (oversized.Ptr(), 'x', oversized.Size()); + + bool rejected = false; + try + { + VolumePassword tooLong; + tooLong.Set (oversized.Ptr(), oversized.Size()); + } + catch (PasswordTooLong&) { rejected = true; } + + if (!rejected) + throw TestFailed (SRC_POS); + } + + // Serialising a password and reading it back must reproduce it exactly, because this + // is how it reaches the privileged service + { + const uint8 secret[] = { 'c', 'o', 'r', 'r', 'e', 'c', 't', '-', 'h', 'o', 'r', 's', 'e' }; + VolumePassword original (secret, sizeof (secret)); + + shared_ptr stream (new MemoryStream); + original.Serialize (stream); + + // Serialize() writes a type header, so the counterpart is DeserializeNew rather + // than Deserialize -- the same call the core service uses on the receiving end + shared_ptr restored = Serializable::DeserializeNew (stream); + + if (!restored || restored->Size() != original.Size() || !(*restored == original)) + throw TestFailed (SRC_POS); + + // A different password must not compare equal + const uint8 other[] = { 'c', 'o', 'r', 'r', 'e', 'c', 't', '-', 'h', 'o', 'r', 's', 'f' }; + VolumePassword different (other, sizeof (other)); + if (*restored == different) + throw TestFailed (SRC_POS); + } + + // The cache is bounded, deduplicating and ordered most-recent-first + { + VolumePasswordCache::Clear(); + if (!VolumePasswordCache::IsEmpty()) + throw TestFailed (SRC_POS); + + // Fill beyond capacity; the oldest entry has to be dropped + for (size_t i = 0; i < VolumePasswordCache::Capacity + 2; i++) + { + uint8 buf[8]; + memset (buf, 0, sizeof (buf)); + buf[0] = (uint8) ('a' + i); + VolumePasswordCache::Store (VolumePassword (buf, sizeof (buf))); + } + + CachedPasswordList cached = VolumePasswordCache::GetPasswords(); + if (cached.size() != VolumePasswordCache::Capacity) + throw TestFailed (SRC_POS); + + // The most recently stored password is at the front + uint8 newest[8]; + memset (newest, 0, sizeof (newest)); + newest[0] = (uint8) ('a' + VolumePasswordCache::Capacity + 1); + if (!(*cached.front() == VolumePassword (newest, sizeof (newest)))) + throw TestFailed (SRC_POS); + + // Storing an already cached password moves it to the front instead of duplicating. + // Counting the occurrences is what makes this detectable: a duplicate would + // otherwise be hidden again by the capacity trim, leaving size and front intact. + uint8 again[8]; + memset (again, 0, sizeof (again)); + again[0] = (uint8) ('a' + VolumePasswordCache::Capacity); + VolumePassword repeated (again, sizeof (again)); + VolumePasswordCache::Store (repeated); + + cached = VolumePasswordCache::GetPasswords(); + if (cached.size() != VolumePasswordCache::Capacity) + throw TestFailed (SRC_POS); + if (!(*cached.front() == repeated)) + throw TestFailed (SRC_POS); + + size_t occurrences = 0; + foreach_ref (const VolumePassword &cachedPassword, cached) + { + if (cachedPassword == repeated) + occurrences++; + } + + if (occurrences != 1) + throw TestFailed (SRC_POS); + + VolumePasswordCache::Clear(); + if (!VolumePasswordCache::IsEmpty()) + throw TestFailed (SRC_POS); + } + } + + // The C++ Hash wrappers had never been executed: the known-answer tests call the C + // routines directly, so the dispatch layer around them, including both parameter + // checks, went untested. + void EncryptionTest::TestHashClasses () + { + HashList hashes = Hash::GetAvailableAlgorithms(); + if (hashes.empty()) + throw TestFailed (SRC_POS); + + foreach_ref (Hash &hash, hashes) + { + if (hash.GetName().empty() || hash.GetAltName().empty()) + throw TestFailed (SRC_POS); + + if (hash.GetDigestSize() == 0 || hash.GetBlockSize() == 0) + throw TestFailed (SRC_POS); + + // GetNew() must hand out an independent instance of the same algorithm + shared_ptr fresh = hash.GetNew(); + if (fresh->GetName() != hash.GetName() || fresh->GetDigestSize() != hash.GetDigestSize()) + throw TestFailed (SRC_POS); + + // Empty input and an undersized digest buffer are both rejected + bool rejected = false; + try { hash.ValidateDataParameters (ConstBufferPtr ((const uint8 *) "", 0)); } + catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + Buffer tooSmall (hash.GetDigestSize() - 1); + rejected = false; + try { hash.ValidateDigestParameters (tooSmall); } + catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + // Hashing is deterministic, sensitive to its input, and Init() resets state + Buffer input (64), digestA (hash.GetDigestSize()), digestB (hash.GetDigestSize()); + for (size_t i = 0; i < input.Size(); i++) + input[i] = (uint8) i; + + hash.Init(); + hash.ProcessData (input); + hash.GetDigest (digestA); + + hash.Init(); + hash.ProcessData (input); + hash.GetDigest (digestB); + + if (memcmp (digestA.Ptr(), digestB.Ptr(), digestA.Size()) != 0) + throw TestFailed (SRC_POS); + + input[0] ^= 0x01; + hash.Init(); + hash.ProcessData (input); + hash.GetDigest (digestB); + + if (memcmp (digestA.Ptr(), digestB.Ptr(), digestA.Size()) == 0) + throw TestFailed (SRC_POS); + } + } + + // Selecting a KDF by name or by hash is how the mount path picks its PRF, yet neither + // lookup nor the rejection of an unknown name had ever run. + void EncryptionTest::TestKdfSelection () + { + Pkcs5KdfList kdfs = Pkcs5Kdf::GetAvailableAlgorithms(); + if (kdfs.empty()) + throw TestFailed (SRC_POS); + + foreach_ref (Pkcs5Kdf &kdf, kdfs) + { + // Looking a KDF up by its own name must return the same algorithm + shared_ptr byName = Pkcs5Kdf::GetAlgorithm (kdf.GetName()); + if (byName->GetName() != kdf.GetName()) + throw TestFailed (SRC_POS); + + // ... and so must looking it up by its hash, for the non-Argon2 ones + if (!kdf.IsArgon2()) + { + shared_ptr byHash = Pkcs5Kdf::GetAlgorithm (*kdf.GetHash()); + if (byHash->GetName() != kdf.GetName()) + throw TestFailed (SRC_POS); + } + + // Degenerate derivation parameters are refused. Going through DeriveKey rather + // than the protected validator also covers the path the mount code takes; both + // cases fail the check before any iteration runs, so this stays cheap. + Buffer key (32), salt (64); + key.Zero(); salt.Zero(); + VolumePassword password ((const uint8 *) "test", 4); + + bool rejected = false; + try { kdf.DeriveKey (key, password, salt, 0); } + catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + rejected = false; + try { kdf.DeriveKey (key, password, ConstBufferPtr (salt.Ptr(), 0), 1000); } + catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + } + + // An unknown PRF name must be refused rather than silently defaulted + bool rejected = false; + try { Pkcs5Kdf::GetAlgorithm (L"no-such-prf"); } + catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + } + + // A volume header is the first thing that touches attacker-supplied bytes, and the + // guards on that path had no coverage at all. + void EncryptionTest::TestVolumeHeaderRejection () + { + VolumeHeader header (TC_VOLUME_HEADER_EFFECTIVE_SIZE); + + // An empty password must be refused before any key derivation happens + { + Buffer encrypted (TC_VOLUME_HEADER_EFFECTIVE_SIZE); + encrypted.Zero(); + + VolumePassword empty; + bool rejected = false; + try + { + header.Decrypt (encrypted, empty, 0, shared_ptr (), + Pkcs5Kdf::GetAvailableAlgorithms(), + EncryptionAlgorithm::GetAvailableAlgorithms(), + EncryptionMode::GetAvailableModes()); + } + catch (PasswordEmpty&) { rejected = true; } + + if (!rejected) + throw TestFailed (SRC_POS); + } + + // Creating a header with a mismatched key or an impossible sector size is refused + { + shared_ptr ea = EncryptionAlgorithm::GetAvailableAlgorithms().front(); + + Buffer headerBuffer (TC_VOLUME_HEADER_EFFECTIVE_SIZE); + Buffer salt (VolumeHeader::GetSaltSize()); + Buffer headerKey (VolumeHeader::GetLargestSerializedKeySize()); + Buffer dataKey (ea->GetKeySize() * 2); + salt.Zero(); headerKey.Zero(); dataKey.Zero(); + + VolumeHeaderCreationOptions options; + options.EA = ea; + options.Kdf = Pkcs5Kdf::GetAvailableAlgorithms().front(); + options.Type = VolumeType::Normal; + options.SectorSize = TC_SECTOR_SIZE_FILE_HOSTED_VOLUME; + options.VolumeDataSize = TC_MIN_VOLUME_SIZE; + options.VolumeDataStart = 0; + options.Salt = salt; + options.HeaderKey = headerKey; + + // data key of the wrong length + Buffer shortKey (ea->GetKeySize()); + shortKey.Zero(); + options.DataKey = shortKey; + + bool rejected = false; + try { header.Create (headerBuffer, options); } + catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + // sector size that is not a multiple of the encryption data unit + options.DataKey = dataKey; + options.SectorSize = TC_MIN_VOLUME_SECTOR_SIZE + 1; + + rejected = false; + try { header.Create (headerBuffer, options); } + catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + } + } + + // Exercises the rejection paths and the accessors of the public encryption API. The + // known-answer tests above only ever take the success path, so none of this code was + // reached before: a wrong key size, block operations on an uninitialised cipher and a + // mode without ciphers all went untested, as did the size and name accessors. + void EncryptionTest::TestEdgeCases () + { + // A cipher must refuse block operations until a key has been set + foreach_ref (Cipher &cipher, Cipher::GetAvailableCiphers()) + { + Buffer block (cipher.GetBlockSize()); + memset (block.Ptr(), 0, block.Size()); + + bool rejected = false; + try { cipher.EncryptBlock (block); } catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + rejected = false; + try { cipher.DecryptBlock (block); } catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + rejected = false; + try { cipher.EncryptBlocks (block, 1); } catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + rejected = false; + try { cipher.DecryptBlocks (block, 1); } catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + // Reporting its geometry must work regardless of initialisation + if (cipher.GetBlockSize() == 0 || cipher.GetKeySize() == 0 || cipher.GetName().empty()) + throw TestFailed (SRC_POS); + } + + // A mode without ciphers cannot report a key size + { + EncryptionModeXTS mode; + bool rejected = false; + try { mode.GetKeySize(); } catch (NotInitialized&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + } + + foreach_ref (EncryptionAlgorithm &ea, EncryptionAlgorithm::GetAvailableAlgorithms()) + { + if (ea.IsDeprecated()) + continue; + + // Block size accessors must be consistent with the ciphers involved + size_t minBlockSize = ea.GetMinBlockSize(); + size_t maxBlockSize = ea.GetMaxBlockSize(); + + if (minBlockSize == 0 || maxBlockSize < minBlockSize) + throw TestFailed (SRC_POS); + + // Both name forms must be non-empty; the GUI form of a cascade is parenthesised + if (ea.GetName().empty() || ea.GetName (true).empty()) + throw TestFailed (SRC_POS); + + // A key of the wrong length must be refused, one byte short and one byte long + Buffer tooShort (ea.GetKeySize() - 1); + memset (tooShort.Ptr(), 0, tooShort.Size()); + + bool rejected = false; + try { ea.SetKey (tooShort); } catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + Buffer tooLong (ea.GetKeySize() + 1); + memset (tooLong.Ptr(), 0, tooLong.Size()); + + rejected = false; + try { ea.SetKey (tooLong); } catch (ParameterIncorrect&) { rejected = true; } + if (!rejected) + throw TestFailed (SRC_POS); + + // The correct length must still be accepted afterwards + Buffer correct (ea.GetKeySize()); + memset (correct.Ptr(), 0, correct.Size()); + ea.SetKey (correct); + } } diff --git a/src/Volume/EncryptionTest.h b/src/Volume/EncryptionTest.h index ed6a9cdc89..dd452af261 100644 --- a/src/Volume/EncryptionTest.h +++ b/src/Volume/EncryptionTest.h @@ -26,6 +26,14 @@ namespace VeraCrypt protected: static void TestCiphers (); + static void TestEdgeCases (); + static void TestHashClasses (); + static void TestKdfSelection (); + static void TestKeyfileApplication (); + static void TestPasswordHandling (); + static void TestVolumeHeaderRejection (); + static void TestVolumeInfoSerialization (); + static void TestVolumeLayouts (); static void TestLegacyModes (); static void TestPkcs5 (); static void TestXts ();