Files
tai/taicpp/tests/seal_test.cpp
T

330 lines
12 KiB
C++

// Seal unit tests: test_seal.py ported 1:1 (class SealTests) plus known-answer vectors for
// the primitives. Cross-implementation checks live in tests/seal_diff.py.
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include "config/config.h"
#include "repl/boot.h"
#include "seal/seal_base64.h"
#include "seal/seal_factory.h"
#include "seal/seal_hmac.h"
#include "seal/seal_pbkdf2.h"
#include "seal/seal_rotation.h"
#include "seal/seal_sha256.h"
#include "store/json.h"
#include "store/store.h"
using namespace detz;
namespace fs = std::filesystem;
static int g_checks = 0, g_fail = 0;
#define CHECK(cond) do { g_checks++; if (!(cond)) { g_fail++; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } } while (0)
#define CHECK_THROWS_SEAL(expr) do { g_checks++; bool hit = false; try { expr; } catch (const store::SealError&) { hit = true; } \
if (!hit) { g_fail++; std::printf("FAIL %s:%d no SealError: %s\n", __FILE__, __LINE__, #expr); } } while (0)
// One throwaway TAI_HOME (the python setUp/tearDown pair).
class TmpHome {
public:
TmpHome() {
char tmpl[] = "/tmp/seal-test-XXXXXX";
root = mkdtemp(tmpl);
fs::create_directories(fs::path(root) / "profiles");
}
~TmpHome() { std::error_code ec; fs::remove_all(root, ec); }
Config config() const {
Config c = Config::FromEnv();
c.home = root;
c.profilesDir = (fs::path(root) / "profiles").string();
c.dbPath = (fs::path(root) / "memory.db").string();
c.profile = "default";
return c;
}
std::string root;
};
static bool starts(const std::string& s, const char* p) { return s.rfind(p, 0) == 0; }
static bool contains(const std::string& s, const std::string& p) { return s.find(p) != std::string::npos; }
static std::string slurp(const std::string& path) { std::ifstream f(path, std::ios::binary); std::stringstream ss; ss << f.rdbuf(); return ss.str(); }
static std::unique_ptr<store::Store> make_store(const TmpHome& h, store::SealPort& seal) { return std::make_unique<store::Store>(h.config(), seal); }
static void test_vectors() {
using namespace seal;
CHECK(SealHmac::to_hex(SealSha256::digest("abc")) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
CHECK(SealHmac::to_hex(SealSha256::digest("")) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
CHECK(SealHmac::to_hex(SealSha256::digest(std::string(1000, 'a'))) == "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3");
// RFC 4231 test case 2
CHECK(SealHmac::hex("Jefe", "what do ya want for nothing?") == "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843");
// RFC 7914 section 11: PBKDF2-HMAC-SHA-256, P="passwd", S="salt", c=1, dkLen=64
CHECK(SealHmac::to_hex(SealPbkdf2::derive("passwd", "salt", 1, 64)) ==
"55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783");
std::string out;
CHECK(SealBase64::encode("") == "" && SealBase64::encode("f") == "Zg==" && SealBase64::encode("fo") == "Zm8=" && SealBase64::encode("foo") == "Zm9v");
CHECK(SealBase64::decode("Zm9vYg==", out) && out == "foob");
CHECK(!SealBase64::decode("Zm9vY", out));
CHECK(!SealBase64::decode("Zg", out));
}
static void test_round_trip() {
TmpHome h;
seal::Seal s(h.root, "test-secret-1");
std::string locked = s.lock("the wifi password is orchid-4421");
CHECK(starts(locked, "tai1$"));
CHECK(!contains(locked, "orchid"));
CHECK(s.unlock(locked) == "the wifi password is orchid-4421");
}
static void test_unique_nonce() {
TmpHome h;
seal::Seal s(h.root, "test-secret-1");
CHECK(s.lock("same") != s.lock("same"));
}
static void test_wrong_passphrase() {
TmpHome h;
seal::Seal first(h.root, "correct-horse");
CHECK_THROWS_SEAL(seal::Seal(h.root, "wrong-guess"));
try { seal::Seal(h.root, "wrong-guess"); } catch (const store::SealError& e) { CHECK(std::string(e.what()) == "wrong TAI_PASSPHRASE"); }
}
static void test_tamper_detected() {
TmpHome h;
seal::Seal s(h.root, "test-secret-1");
std::string locked = s.lock("precious");
std::string tampered = locked;
size_t i = tampered.size() - 4;
tampered[i] = locked[i] != 'A' ? 'A' : 'B';
CHECK_THROWS_SEAL(s.unlock(tampered));
CHECK_THROWS_SEAL(s.unlock("plain"));
CHECK_THROWS_SEAL(s.unlock("tai1$onlytwo$parts"));
CHECK(s.safe_unlock(tampered) == "[sealed: decrypt failed]");
}
static void test_sql_functions() {
TmpHome h;
seal::Seal s(h.root, "test-secret-1");
auto st = make_store(h, s);
std::string row = st->db().execute("SELECT tai_enc('hello-sql')").rows[0][0].text();
CHECK(starts(row, "tai1$"));
CHECK(st->db().execute("SELECT tai_dec(?)", {row}).rows[0][0].text() == "hello-sql");
st->close();
}
static void test_sealed_events_recall() {
TmpHome h;
seal::Seal s(h.root, "test-secret-1");
auto st = make_store(h, s);
st->log_event("default", "user", "message", "my vault code is zebra-9");
std::string raw = st->db().execute("SELECT text FROM events").rows[0][0].text();
CHECK(starts(raw, "tai1$"));
CHECK(!contains(raw, "zebra"));
auto rows = st->search_events("default", "zebra");
CHECK(rows.size() == 1);
CHECK(!rows.empty() && contains(rows[0].text, "zebra-9"));
st->close();
}
static void test_sealed_profile_files() {
TmpHome h;
seal::Seal s(h.root, "test-secret-1");
auto st = make_store(h, s);
st->save_system("work", "system knows orchid-4421");
CHECK(starts(slurp(st->profile_path("work")), "tai1$"));
CHECK(st->load_system("work") == std::optional<std::string>("system knows orchid-4421"));
st->save_session("work", {store::Json::loads("{\"role\": \"user\", \"content\": \"secret hello\"}")});
CHECK(starts(slurp(st->session_path("work")), "tai1$"));
auto msgs = st->load_session("work");
CHECK(msgs.size() == 1 && msgs[0].get_string("content") == "secret hello");
st->close();
}
static void test_migration_from_plaintext() {
TmpHome h;
{
seal::Seal plain(h.root, "");
auto st = make_store(h, plain);
st->log_event("default", "user", "message", "plain memory");
st->save_system("default", "plain system");
st->close();
}
seal::Seal sealed(h.root, "new-secret-2");
auto st = make_store(h, sealed);
CHECK(starts(st->db().execute("SELECT text FROM events").rows[0][0].text(), "tai1$"));
CHECK(starts(slurp(st->profile_path("default")), "tai1$"));
CHECK(st->load_system("default") == std::optional<std::string>("plain system"));
st->close();
}
static void test_sealed_home_requires_passphrase() {
TmpHome h;
{
seal::Seal s(h.root, "test-secret-1");
auto st = make_store(h, s);
st->log_event("default", "user", "message", "locked memory");
st->close();
}
seal::Seal none(h.root, "");
CHECK_THROWS_SEAL(store::Store(h.config(), none));
}
static void test_passphrase_default() {
unsetenv("TAI_PASSPHRASE");
auto [value, def] = repl::Boot::resolve_passphrase();
CHECK(value == seal::Seal::kDefaultPassphrase);
CHECK(def);
setenv("TAI_PASSPHRASE", "personal-1", 1);
auto [v2, d2] = repl::Boot::resolve_passphrase();
CHECK(v2 == "personal-1" && !d2);
setenv("TAI_PASSPHRASE", "", 1);
auto [v3, d3] = repl::Boot::resolve_passphrase();
CHECK(v3.empty() && !d3);
unsetenv("TAI_PASSPHRASE");
}
static void test_rotate_default_to_personal() {
TmpHome h;
seal::Seal old(h.root, seal::Seal::kDefaultPassphrase);
{
auto st = make_store(h, old);
st->log_event("default", "user", "message", "rotate me");
st->save_system("default", "rotate system");
st->close();
}
auto fresh = seal::SealRotation::rotate(seal::SealRotation::Paths::ForHome(h.root), old, "personal-2");
CHECK(fresh->enabled());
CHECK_THROWS_SEAL(seal::Seal(h.root, seal::Seal::kDefaultPassphrase));
seal::Seal again(h.root, "personal-2");
auto re = make_store(h, again);
CHECK(re->load_system("default") == std::optional<std::string>("rotate system"));
CHECK(contains(re->search_events("default", "rotate")[0].text, "rotate me"));
CHECK(starts(re->db().execute("SELECT text FROM events").rows[0][0].text(), "tai1$"));
re->close();
}
static void test_plaintext_home_unaffected() {
TmpHome h;
seal::Seal plain(h.root, "");
auto st = make_store(h, plain);
st->log_event("default", "user", "message", "open memory");
CHECK(st->search_events("default", "open")[0].text == "open memory");
st->save_system("default", "open system");
CHECK(slurp(st->profile_path("default")) == "open system");
CHECK(!fs::exists(fs::path(h.root) / ".seal"));
st->close();
}
static void test_rotate_moves_secrets() {
TmpHome h;
seal::Seal old(h.root, seal::Seal::kDefaultPassphrase);
{
auto st = make_store(h, old);
st->save_secret("api", "rotate-me-1");
st->close();
}
seal::SealRotation::rotate(seal::SealRotation::Paths::ForHome(h.root), old, "personal-3");
seal::Seal again(h.root, "personal-3");
auto re = make_store(h, again);
CHECK(re->load_secret("api") == std::optional<std::string>("rotate-me-1"));
CHECK(starts(re->db().execute("SELECT value FROM secrets WHERE name = 'api'").rows[0][0].text(), "tai1$"));
re->close();
}
static void test_sealed_secrets_require_passphrase() {
TmpHome h;
{
seal::Seal s(h.root, "test-secret-1");
auto st = make_store(h, s);
st->save_secret("k", "locked-value");
st->close();
}
seal::Seal none(h.root, "");
CHECK_THROWS_SEAL(store::Store(h.config(), none));
}
static void test_schedule_prompt_sealed() {
TmpHome h;
seal::Seal s(h.root, "test-secret-1");
auto st = make_store(h, s);
st->add_schedule("job", "prompt-secret-3", "t", 0, "2026-01-01T00:00:00+00:00", 60);
std::string raw = st->db().execute("SELECT prompt FROM schedules").rows[0][0].text();
CHECK(starts(raw, "tai1$"));
CHECK(!contains(raw, "prompt-secret"));
CHECK(st->list_schedules("t")[0].get_string("prompt") == "prompt-secret-3");
st->close();
}
static void test_rotate_moves_schedules() {
TmpHome h;
seal::Seal old(h.root, seal::Seal::kDefaultPassphrase);
{
auto st = make_store(h, old);
st->add_schedule("job", "rotate-prompt-4", "t", 3600, "2026-01-01T00:00:00+00:00", 60);
st->close();
}
seal::SealRotation::rotate(seal::SealRotation::Paths::ForHome(h.root), old, "personal-4");
seal::Seal again(h.root, "personal-4");
auto re = make_store(h, again);
CHECK(re->list_schedules("t")[0].get_string("prompt") == "rotate-prompt-4");
CHECK(starts(re->db().execute("SELECT prompt FROM schedules").rows[0][0].text(), "tai1$"));
re->close();
}
// boot(): default-key home upgraded when TAI_PASSPHRASE is set; default flag follows the key.
static void test_factory_upgrade() {
TmpHome h;
{
auto s = seal::SealFactory::make(h.root, seal::Seal::kDefaultPassphrase, true);
CHECK(s->default_key() && s->enabled());
auto st = make_store(h, *s);
st->save_secret("api", "factory-1");
st->close();
}
auto s2 = seal::SealFactory::make(h.root, "personal-5", false);
CHECK(s2->enabled() && !s2->default_key());
auto st = make_store(h, *s2);
CHECK(st->load_secret("api") == std::optional<std::string>("factory-1"));
st->close();
// default key against a personal home: refused, not rotated
CHECK_THROWS_SEAL(seal::SealFactory::make(h.root, seal::Seal::kDefaultPassphrase, true));
// wrong personal passphrase and no default key behind it: original error
try { seal::SealFactory::make(h.root, "nope", false); CHECK(false); } catch (const store::SealError& e) { CHECK(std::string(e.what()) == "wrong TAI_PASSPHRASE"); }
// empty passphrase: disabled seal, never default
TmpHome plain;
auto s3 = seal::SealFactory::make(plain.root, "", false);
CHECK(!s3->enabled() && !s3->default_key());
}
static void test_corrupt_seal_file() {
TmpHome h;
{ std::ofstream f(fs::path(h.root) / ".seal"); f << "{not json"; }
CHECK_THROWS_SEAL(seal::Seal(h.root, "x"));
{ std::ofstream f(fs::path(h.root) / ".seal"); f << "{\"check\": \"a\"}"; }
CHECK_THROWS_SEAL(seal::Seal(h.root, "x"));
}
int main() {
test_vectors();
test_round_trip();
test_unique_nonce();
test_wrong_passphrase();
test_tamper_detected();
test_sql_functions();
test_sealed_events_recall();
test_sealed_profile_files();
test_migration_from_plaintext();
test_sealed_home_requires_passphrase();
test_passphrase_default();
test_rotate_default_to_personal();
test_plaintext_home_unaffected();
test_rotate_moves_secrets();
test_sealed_secrets_require_passphrase();
test_schedule_prompt_sealed();
test_rotate_moves_schedules();
test_factory_upgrade();
test_corrupt_seal_file();
std::printf("seal_test: %d checks, %d failed\n", g_checks, g_fail);
return g_fail ? 1 : 0;
}