// 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 #include #include #include #include #include #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 make_store(const TmpHome& h, store::SealPort& seal) { return std::make_unique(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("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("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("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("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("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; }