Skip to content
Merged
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
66 changes: 41 additions & 25 deletions src/FileOperations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,71 +235,87 @@ seal::secure_string<seal::locked_allocator<char>> 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 <class A>
bool FileOperations::parseTriples(std::string_view plain,
std::vector<seal::secure_triplet16<A>>& 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<unsigned char>(sv[a])))
++a;
while (b > a && std::isspace(static_cast<unsigned char>(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<wchar_t, A> 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;
Expand Down
9 changes: 3 additions & 6 deletions tests/test_crypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
correctPassword);

// Try to decrypt with wrong password
EXPECT_THROW(

Check warning on line 81 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / build

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 81 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / test

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 81 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / sonar

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]
seal::Cryptography::decryptPacket(std::span<const unsigned char>(packet), wrongPassword),
std::runtime_error);
}
Expand All @@ -99,7 +99,7 @@
corrupted[corrupted.size() / 2] ^= 0xFF;

// Should fail authentication
EXPECT_THROW(

Check warning on line 102 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / build

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 102 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / test

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 102 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / sonar

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]
seal::Cryptography::decryptPacket(std::span<const unsigned char>(corrupted), password),
std::runtime_error);
}
Expand All @@ -108,7 +108,6 @@
{
auto password = make_secure_string("test_password");
std::string plaintext = "verify me";

std::vector<unsigned char> plainBytes(plaintext.begin(), plaintext.end());

auto packet =
Expand All @@ -120,14 +119,13 @@

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<unsigned char> plainBytes(plaintext.begin(), plaintext.end());

auto packet =
seal::Cryptography::encryptPacket(std::span<const unsigned char>(plainBytes), password);
auto packet = seal::Cryptography::encryptPacket(std::span<const unsigned char>(plainBytes),
correctPassword);

EXPECT_THROW(
seal::Cryptography::verifyPacket(std::span<const unsigned char>(packet), wrongPassword),
Expand All @@ -138,7 +136,6 @@
{
auto password = make_secure_string("test_password");
std::string plaintext = "verify me";

std::vector<unsigned char> plainBytes(plaintext.begin(), plaintext.end());

auto packet =
Expand All @@ -157,7 +154,7 @@
auto password = make_secure_string("test_password");
std::vector<unsigned char> shortPacket(10); // Too short

EXPECT_THROW(

Check warning on line 157 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / build

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 157 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / test

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 157 in tests/test_crypto.cpp

View workflow job for this annotation

GitHub Actions / sonar

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]
seal::Cryptography::decryptPacket(std::span<const unsigned char>(shortPacket), password),
std::runtime_error);
}
Expand Down
23 changes: 23 additions & 0 deletions tests/test_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
auto password = make_secure_string("test_password");
std::string invalidHex = "not_valid_hex";

EXPECT_THROW(seal::FileOperations::decryptLine(invalidHex, password), std::runtime_error);

Check warning on line 55 in tests/test_integration.cpp

View workflow job for this annotation

GitHub Actions / build

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 55 in tests/test_integration.cpp

View workflow job for this annotation

GitHub Actions / test

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 55 in tests/test_integration.cpp

View workflow job for this annotation

GitHub Actions / sonar

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]
}

TEST_F(EncryptDecryptLineTest, HexWithSpaces)
Expand Down Expand Up @@ -89,7 +89,7 @@
std::string hexOutput = seal::FileOperations::encryptLine(plaintext, correctPassword);

// Try to decrypt with wrong password
EXPECT_THROW(seal::FileOperations::decryptLine(hexOutput, wrongPassword), std::runtime_error);

Check warning on line 92 in tests/test_integration.cpp

View workflow job for this annotation

GitHub Actions / build

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 92 in tests/test_integration.cpp

View workflow job for this annotation

GitHub Actions / test

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]

Check warning on line 92 in tests/test_integration.cpp

View workflow job for this annotation

GitHub Actions / sonar

discarding return value of function with [[nodiscard]] attribute [D:\a\seal\seal\build\seal_tests.vcxproj]
}

TEST_F(EncryptDecryptLineTest, UnicodeText)
Expand Down Expand Up @@ -297,3 +297,26 @@
seal::FileOperations::decryptFileTo(encFile.string(), decFile.string(), password);
EXPECT_FALSE(decryptSuccess);
}

TEST_F(FileOperationsTest, ParseTriplesRejectsEmptyFields)
{
std::vector<seal::secure_triplet16_t> 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<seal::secure_triplet16_t> 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");
}
10 changes: 7 additions & 3 deletions tests/test_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#include "test_helpers.hpp"

#include <gtest/gtest.h>

#include <string>
#include <string_view>
#include <vector>

using namespace std::string_literals;
Expand Down Expand Up @@ -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);
}
}
Loading