diff --git a/src/FileOperations.cpp b/src/FileOperations.cpp index aa4d4a6..ff144ef 100644 --- a/src/FileOperations.cpp +++ b/src/FileOperations.cpp @@ -235,7 +235,7 @@ seal::secure_string> FileOperations::decryptLine( return out; } -// Parse a flat string of colon-delimited triples separated by commas or +// Parse secret material of colon-delimited triples separated by commas or // newlines. Expected format: "svc1:user1:pass1,svc2:user2:pass2\n...". // Each token must contain EXACTLY 2 colons (3 fields). template @@ -243,63 +243,79 @@ bool FileOperations::parseTriples(std::string_view plain, std::vector>& out) { out.clear(); - std::string tok; - // Flush: Validate and consume one accumulated token. - auto flush = [&](std::string& t) -> bool + // Trim leading/trailing whitespace as a sub-view. + auto trimView = [](std::string_view sv) -> std::string_view { - std::string s = seal::utils::trim(t); - t.clear(); + size_t a = 0, b = sv.size(); + while (a < b && std::isspace(static_cast(sv[a]))) + ++a; + while (b > a && std::isspace(static_cast(sv[b - 1]))) + --b; + return sv.substr(a, b - a); + }; + + // Validate one token (sub-view of `plain`) and append it to `out`. + auto flush = [&](std::string_view tok) -> bool + { + const std::string_view s = trimView(tok); if (s.empty()) return true; // Locate the first and second colons. Reject if there are fewer // than 2 (incomplete triple) or more than 2 (ambiguous fields). - size_t c1 = s.find(':'), - c2 = (c1 == std::string::npos ? std::string::npos : s.find(':', c1 + 1)); - if (c1 == std::string::npos || c2 == std::string::npos || - s.find(':', c2 + 1) != std::string::npos) + const size_t c1 = s.find(':'); + const size_t c2 = + (c1 == std::string_view::npos ? std::string_view::npos : s.find(':', c1 + 1)); + if (c1 == std::string_view::npos || c2 == std::string_view::npos || + s.find(':', c2 + 1) != std::string_view::npos) return false; - auto mk = [&](size_t off, size_t len) + auto mk = [&](std::string_view field) { seal::basic_secure_string r; - if (len == 0) + if (field.empty()) return r; - const char* src = s.data() + off; - int need = MultiByteToWideChar(CP_UTF8, 0, src, (int)len, nullptr, 0); + int need = MultiByteToWideChar(CP_UTF8, 0, field.data(), (int)field.size(), nullptr, 0); if (need > 0) { r.s.resize(need); - MultiByteToWideChar(CP_UTF8, 0, src, (int)len, r.s.data(), need); + MultiByteToWideChar(CP_UTF8, 0, field.data(), (int)field.size(), r.s.data(), need); } return r; }; // Split into (service, username, password) around the two colons. - out.emplace_back(mk(0, c1), mk(c1 + 1, c2 - (c1 + 1)), mk(c2 + 1, s.size() - (c2 + 1))); + const std::string_view service = trimView(s.substr(0, c1)); + const std::string_view user = trimView(s.substr(c1 + 1, c2 - c1 - 1)); + const std::string_view pass = s.substr(c2 + 1); + if (service.empty() || user.empty() || pass.empty()) + return false; + + out.emplace_back(mk(service), mk(user), mk(pass)); return true; }; // Commas and newlines both act as triple separators, so a single - // encrypted blob can carry multiple credentials in one payload. - for (char ch : plain) + // encrypted blob can carry multiple credentials in one payload. We walk + // `plain` with offsets so each token is just a sub-view of the caller's + // locked buffer. + size_t start = 0; + for (size_t i = 0; i < plain.size(); ++i) { + const char ch = plain[i]; if (ch == ',' || ch == '\n' || ch == '\r') { - if (!flush(tok)) + if (!flush(plain.substr(start, i - start))) { out.clear(); return false; } - } - else - { - tok.push_back(ch); + start = i + 1; } } - // Don't forget the last token (no trailing delimiter required). - if (!flush(tok)) + + if (!flush(plain.substr(start))) { out.clear(); return false; diff --git a/tests/test_crypto.cpp b/tests/test_crypto.cpp index 7396d9f..5c65544 100644 --- a/tests/test_crypto.cpp +++ b/tests/test_crypto.cpp @@ -108,7 +108,6 @@ TEST_F(CryptoTest, VerifyPacketAcceptsValidPacket) { auto password = make_secure_string("test_password"); std::string plaintext = "verify me"; - std::vector plainBytes(plaintext.begin(), plaintext.end()); auto packet = @@ -120,14 +119,13 @@ TEST_F(CryptoTest, VerifyPacketAcceptsValidPacket) TEST_F(CryptoTest, VerifyPacketRejectsWrongPassword) { - auto password = make_secure_string("test_password"); + auto correctPassword = make_secure_string("correct_password"); auto wrongPassword = make_secure_string("wrong_password"); std::string plaintext = "verify me"; - std::vector plainBytes(plaintext.begin(), plaintext.end()); - auto packet = - seal::Cryptography::encryptPacket(std::span(plainBytes), password); + auto packet = seal::Cryptography::encryptPacket(std::span(plainBytes), + correctPassword); EXPECT_THROW( seal::Cryptography::verifyPacket(std::span(packet), wrongPassword), @@ -138,7 +136,6 @@ TEST_F(CryptoTest, VerifyPacketRejectsCorruptedPacket) { auto password = make_secure_string("test_password"); std::string plaintext = "verify me"; - std::vector plainBytes(plaintext.begin(), plaintext.end()); auto packet = diff --git a/tests/test_integration.cpp b/tests/test_integration.cpp index f5c35bb..6dabe20 100644 --- a/tests/test_integration.cpp +++ b/tests/test_integration.cpp @@ -297,3 +297,26 @@ TEST_F(FileOperationsTest, DecryptCorruptedFileFails) seal::FileOperations::decryptFileTo(encFile.string(), decFile.string(), password); EXPECT_FALSE(decryptSuccess); } + +TEST_F(FileOperationsTest, ParseTriplesRejectsEmptyFields) +{ + std::vector out; + + EXPECT_FALSE(seal::FileOperations::parseTriples(":user:pass", out)); + EXPECT_TRUE(out.empty()); + + EXPECT_FALSE(seal::FileOperations::parseTriples("svc::pass", out)); + EXPECT_TRUE(out.empty()); + + EXPECT_FALSE(seal::FileOperations::parseTriples("svc:user:", out)); + EXPECT_TRUE(out.empty()); +} + +TEST_F(FileOperationsTest, ParseTriplesTrimsServiceAndUser) +{ + std::vector out; + + EXPECT_TRUE(seal::FileOperations::parseTriples(" svc : user :pass", out)); + ASSERT_EQ(out.size(), 1U); + EXPECT_EQ(seal::FileOperations::tripleToUtf8(out[0]), "svc:user:pass"); +} diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp index 5768554..05d6d42 100644 --- a/tests/test_utils.cpp +++ b/tests/test_utils.cpp @@ -2,7 +2,9 @@ #include "test_helpers.hpp" #include + #include +#include #include using namespace std::string_literals; @@ -279,10 +281,12 @@ TEST_F(PasswordGenTest, ClampsLength) TEST_F(PasswordGenTest, UsesDocumentedCharset) { - static constexpr std::string_view allowed = + static constexpr std::string_view kAllowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+"; auto password = seal::GeneratePassword(64); - for (char ch : password.view()) - EXPECT_NE(allowed.find(ch), std::string_view::npos); + for (const char ch : password.view()) + { + EXPECT_NE(kAllowed.find(ch), std::string_view::npos); + } } \ No newline at end of file