200 lines
8.0 KiB
Go
200 lines
8.0 KiB
Go
package store
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
// R-1176 scenario A at the store. The first test is the C3 red-proof from the data-loss hunt
|
|
// (audits/dataloss-hunt-2026-10-20/C3-K1-escrow-after-host-delete/), now asserting the household's package
|
|
// reaches the rebuilt box — byte-identical, still sealed, with its hash.
|
|
|
|
func carrySetupOldBox(t *testing.T, s *Store, cust, host, sha string, identity []byte) {
|
|
t.Helper()
|
|
if err := s.UpsertHost(&Host{HostID: host, CustomerID: cust, APIKey: "k-" + host}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := s.SaveHostEscrow(host, []byte("k-blob-"+host), "fp-"+host, "zero_knowledge", "2026-10-18T00:00:00Z", sha); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if identity != nil {
|
|
if err := s.SaveHostDRBundle(host, identity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestR1176_RebuiltBoxGetsTheHouseholdsPackage(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "Tester4"
|
|
identity := []byte("sealed-identity-opening-the-old-repo")
|
|
carrySetupOldBox(t, s, cust, "Tester4-old", "SHA_OLD", identity)
|
|
if err := s.DeleteHost("Tester4-old", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.UpsertHost(&Host{HostID: "Tester4-new", CustomerID: cust, APIKey: "k2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, err := s.ReattachRetainedEscrow(cust)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := s.GetEscrowStatusForCustomer(cust)
|
|
if err != nil || st == nil {
|
|
t.Fatalf("the rebuilt box's ACK carries no escrow status (st=%v err=%v) — the R-1176 defect", st, err)
|
|
}
|
|
if !st.IdentityBlobPresent || st.ResticPwSHA256 != "SHA_OLD" {
|
|
t.Fatalf("status = %+v, want identity present with the household's hash", st)
|
|
}
|
|
if res.Outcome != CarryDone || res.FromHost != "Tester4-old" || res.ToHost != "Tester4-new" || res.ResticPwSHA256 != "SHA_OLD" {
|
|
t.Fatalf("carry result = %+v, want done old→new with SHA_OLD", res)
|
|
}
|
|
b, err := s.GetHostDRBundle("Tester4-new")
|
|
if err != nil || b == nil || !bytes.Equal(b.IdentityBlob, identity) || !bytes.Equal(b.KEscrowBlob, []byte("k-blob-Tester4-old")) {
|
|
t.Fatalf("the new host's package is not the household's bytes: %+v err=%v", b, err)
|
|
}
|
|
// One sealed package exists once: the retained copy is gone, the current row holds it.
|
|
if ret, _ := s.ListSupersededEscrow("Tester4-old"); len(ret) != 0 {
|
|
t.Fatalf("retained rows left under the deleted host: %d, want 0 (moved, not copied)", len(ret))
|
|
}
|
|
// Idempotent: the second report changes nothing.
|
|
if res2, err := s.ReattachRetainedEscrow(cust); err != nil || res2.Outcome != CarryNothing {
|
|
t.Fatalf("second call = %+v err=%v, want nothing", res2, err)
|
|
}
|
|
// And the carried package is never lost: deleting the new host (escrow acknowledged) demotes it again.
|
|
if err := s.DeleteHost("Tester4-new", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ret, _ := s.ListSupersededEscrow("Tester4-new"); len(ret) != 1 || !bytes.Equal(ret[0].IdentityBlob, identity) {
|
|
t.Fatalf("after deleting the rebuilt host, its retained rows = %d, want the household's package", len(ret))
|
|
}
|
|
}
|
|
|
|
// The rehearsal's own shape: the old box (with a package) deleted acknowledged, then a second box (never made a
|
|
// package) deleted un-acknowledged, then a third box. The un-acked deletion has nothing and must not hide the older one.
|
|
func TestR1176_AnUnackedLaterDeletionDoesNotHideThePackage(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "Tester4"
|
|
carrySetupOldBox(t, s, cust, "Tester4-816e21", "SHA_OLD", []byte("id"))
|
|
if err := s.DeleteHost("Tester4-816e21", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.UpsertHost(&Host{HostID: "Tester4-648b80", CustomerID: cust, APIKey: "k2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// (the hub of the rehearsal did not carry — simulate by deleting before any report)
|
|
if err := s.DeleteHost("Tester4-648b80", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d, _ := s.LatestHostDeletion(cust); d == nil || d.EscrowAcked {
|
|
t.Fatalf("precondition: the latest deletion must be un-acked, got %+v", d)
|
|
}
|
|
if err := s.UpsertHost(&Host{HostID: "Tester4-third", CustomerID: cust, APIKey: "k3"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, err := s.ReattachRetainedEscrow(cust)
|
|
if err != nil || res.Outcome != CarryDone || res.FromHost != "Tester4-816e21" {
|
|
t.Fatalf("carry = %+v err=%v, want the 816e21 package carried", res, err)
|
|
}
|
|
}
|
|
|
|
func TestR1176_RefusedWhileTheCustomerHasAnotherLiveHost(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "C"
|
|
carrySetupOldBox(t, s, cust, "C-old", "SHA_OLD", []byte("id"))
|
|
if err := s.DeleteHost("C-old", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, h := range []string{"C-a", "C-b"} {
|
|
if err := s.UpsertHost(&Host{HostID: h, CustomerID: cust, APIKey: "k" + h}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
res, err := s.ReattachRetainedEscrow(cust)
|
|
if err != nil || res.Outcome != CarryRefusedOtherLiveHost || len(res.LiveHosts) != 2 || res.FromHost != "C-old" {
|
|
t.Fatalf("carry = %+v err=%v, want refused with both live hosts named", res, err)
|
|
}
|
|
for _, h := range []string{"C-a", "C-b"} {
|
|
if e, _ := s.GetHostEscrow(h); e != nil {
|
|
t.Fatalf("%s got a package while the carry was refused", h)
|
|
}
|
|
}
|
|
if ret, _ := s.ListSupersededEscrow("C-old"); len(ret) != 1 {
|
|
t.Fatalf("the retained package moved although refused: %d rows", len(ret))
|
|
}
|
|
}
|
|
|
|
func TestR1176_NeverAnotherCustomersPackage(t *testing.T) {
|
|
s := newTestStore(t)
|
|
carrySetupOldBox(t, s, "A", "A-old", "SHA_A", []byte("id-A"))
|
|
if err := s.DeleteHost("A-old", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.UpsertHost(&Host{HostID: "B-new", CustomerID: "B", APIKey: "kb"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res, err := s.ReattachRetainedEscrow("B"); err != nil || res.Outcome != CarryNothing {
|
|
t.Fatalf("customer B got %+v err=%v — another customer's package", res, err)
|
|
}
|
|
if e, _ := s.GetHostEscrow("B-new"); e != nil {
|
|
t.Fatal("B-new holds a package it must never see")
|
|
}
|
|
}
|
|
|
|
func TestR1176_NotFromAHostDeletedWithoutAcknowledgement(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "C"
|
|
// A host with no escrow row deletes un-acked; a stray retained row under its id (a supersession earlier in its
|
|
// life) must not be carried — only an acknowledged deletion's package is.
|
|
if err := s.UpsertHost(&Host{HostID: "C-old", CustomerID: cust, APIKey: "k"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
carrySetupOldBox(t, s, cust, "C-old", "SHA1", []byte("id1"))
|
|
if _, _, err := s.SaveHostEscrow("C-old", []byte("k2"), "fp2", "zk", "2026-10-19T00:00:00Z", "SHA2"); err != nil {
|
|
t.Fatal(err) // SHA1's package retained under C-old
|
|
}
|
|
if _, err := s.db.Exec(`DELETE FROM host_escrow WHERE host_id = 'C-old'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.DeleteHost("C-old", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.UpsertHost(&Host{HostID: "C-new", CustomerID: cust, APIKey: "k3"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res, err := s.ReattachRetainedEscrow(cust); err != nil || res.Outcome != CarryNothing {
|
|
t.Fatalf("carry = %+v err=%v, want nothing from an un-acknowledged deletion", res, err)
|
|
}
|
|
}
|
|
|
|
func TestR1176_ABoxWithItsOwnPackageIsNeverOverwritten(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "C"
|
|
carrySetupOldBox(t, s, cust, "C-old", "SHA_OLD", []byte("id-old"))
|
|
if err := s.DeleteHost("C-old", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
carrySetupOldBox(t, s, cust, "C-new", "SHA_NEW", []byte("id-new"))
|
|
if res, err := s.ReattachRetainedEscrow(cust); err != nil || res.Outcome != CarryNothing {
|
|
t.Fatalf("carry = %+v err=%v, want nothing", res, err)
|
|
}
|
|
if e, _ := s.GetHostEscrow("C-new"); e == nil || e.ResticPwSHA256 != "SHA_NEW" {
|
|
t.Fatalf("the new box's own package was replaced: %+v", e)
|
|
}
|
|
}
|
|
|
|
func TestR1176_APackageWithNoIdentityIsNotCarried(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "C"
|
|
carrySetupOldBox(t, s, cust, "C-old", "", nil) // a pre-v0.93.0 shape: K only, no repository password
|
|
if err := s.DeleteHost("C-old", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.UpsertHost(&Host{HostID: "C-new", CustomerID: cust, APIKey: "k"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res, err := s.ReattachRetainedEscrow(cust); err != nil || res.Outcome != CarryNothing {
|
|
t.Fatalf("carry = %+v err=%v, want nothing", res, err)
|
|
}
|
|
}
|