Files
tai/taicpp/tests/whatsapp_data/store_oracle.go
T
2026-10-28 03:42:07 +01:00

408 lines
18 KiB
Go

// Oracle generator for detz::wa M2 store vectors.
//
// Every value is produced by pinned whatsmeow itself (store/sqlstore over a
// scratch SQLite database), so the C++ store is checked against reference
// behavior, not against our own reimplementation. The op sequence below is
// replayed in order by tests/whatsapp_test.cpp (test_store_m2) against WaStore
// with detz-format signal addresses ("user.device" where the oracle uses
// whatsmeow's "user:device"); the JSON records observable outcomes, never
// addresses, so the comparison is format-agnostic. JID strings ARE compared
// byte-for-byte.
//
// Run from a module that requires go.mau.fi/whatsmeow and modernc.org/sqlite:
// go run store_oracle.go > store_vectors.json (inside such a module)
//go:build ignore
package main
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"time"
_ "modernc.org/sqlite"
"go.mau.fi/whatsmeow/proto/waAdv"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
)
var out = map[string]any{}
func set(k string, v any) { out[k] = v }
func must(err error, what string) {
if err != nil {
fmt.Fprintf(os.Stderr, "oracle: %s: %v\n", what, err)
os.Exit(1)
}
}
func hx(b []byte) string { return hex.EncodeToString(b) }
func key(fill byte) [32]byte {
var k [32]byte
for i := range k {
k[i] = fill
}
return k
}
func jid(user, server string) types.JID { return types.NewJID(user, server) }
func main() {
ctx := context.Background()
tmp, err := os.CreateTemp("", "wa-m2-oracle-*.db")
must(err, "temp db")
path := tmp.Name()
tmp.Close()
defer os.Remove(path)
container, err := sqlstore.New(ctx, "sqlite", "file:"+path+"?_foreign_keys=on", nil)
must(err, "sqlstore.New")
// A saved device row (the store tables key their rows to it by foreign
// key); the keys are random, only the row's presence matters here.
ourJID := jid("15550000000", types.DefaultUserServer)
full := container.NewDevice()
full.ID = &ourJID
full.Account = &waAdv.ADVSignedDeviceIdentity{Details: []byte{0}, AccountSignature: make([]byte, 64), AccountSignatureKey: make([]byte, 32), DeviceSignature: make([]byte, 64)}
must(full.Save(ctx), "device Save")
inner := sqlstore.NewSQLStore(container, ourJID)
var s store.SessionStore = inner
var idn store.IdentityStore = inner
var senderKeys store.SenderKeyStore = inner
var contacts store.ContactStore = inner
var chats store.ChatSettingsStore = inner
var secrets store.MsgSecretStore = inner
var tokens store.PrivacyTokenStore = inner
var lids store.LIDStore = container.LIDMap
dev := &store.Device{LIDs: container.LIDMap} // GetAltJID only consults LIDs
set("muted_forever_unix", store.MutedForever.Unix())
// --- JID normalization (types.JID; replayed against WaJid) ---
jids := []string{
"15551234567@s.whatsapp.net",
"15551234567:3@s.whatsapp.net",
"15551234567.4:3@s.whatsapp.net",
"s.whatsapp.net",
"1234@lid",
"1234:7@lid",
"a@b@c",
"120363000000@g.us",
}
norm := map[string]string{}
for _, raw := range jids {
parsed, err := types.ParseJID(raw)
must(err, "parse "+raw)
norm[raw] = parsed.ToNonAD().String()
set("signal_user:"+raw, parsed.SignalAddressUser())
}
set("jid_non_ad", norm)
badJIDs := []string{"", "@", "user@", "a.b.c@s.whatsapp.net", "u:x@s.whatsapp.net", "u.1.2@s.whatsapp.net", "u.1:2:3@s.whatsapp.net"}
bad := map[string]bool{}
for _, raw := range badJIDs {
_, err := types.ParseJID(raw)
bad[raw] = err != nil
}
set("jid_parse_fails", bad)
// --- identities ---
a1 := "15551234567:0"
set("trust_unknown", mustTrust(ctx, idn, a1, key(1)))
must(idn.PutIdentity(ctx, a1, key(1)), "PutIdentity")
set("trust_same", mustTrust(ctx, idn, a1, key(1)))
set("trust_other", mustTrust(ctx, idn, a1, key(2)))
must(idn.PutIdentity(ctx, a1, key(2)), "PutIdentity overwrite")
set("trust_after_overwrite_old", mustTrust(ctx, idn, a1, key(1)))
set("trust_after_overwrite_new", mustTrust(ctx, idn, a1, key(2)))
must(idn.PutIdentity(ctx, "15551234567:5", key(3)), "PutIdentity second device")
must(idn.DeleteIdentity(ctx, a1), "DeleteIdentity")
set("trust_after_delete", mustTrust(ctx, idn, a1, key(2)))
set("trust_sibling_after_delete", mustTrust(ctx, idn, "15551234567:5", key(3)))
must(idn.DeleteAllIdentities(ctx, "15551234567"), "DeleteAllIdentities")
set("trust_sibling_after_delete_all", mustTrust(ctx, idn, "15551234567:5", key(3)))
// Upstream DeleteIdentity accidentally uses the LIKE query: an address
// holding '%' deletes more rows. detz fixes this (exact match); the C++
// test asserts the fixed behavior and this records what upstream does.
must(idn.PutIdentity(ctx, "9%9:0", key(4)), "PutIdentity pct")
must(idn.PutIdentity(ctx, "999:0", key(5)), "PutIdentity pct sibling")
must(idn.DeleteIdentity(ctx, "9%9:0"), "DeleteIdentity pct")
set("trust_pct_sibling_after_pct_delete", mustTrust(ctx, idn, "999:0", key(5)))
// --- sessions ---
set("has_session_missing", mustHas(ctx, s, "15550001111:0"))
must(s.PutSession(ctx, "15550001111:0", []byte("sess-aaa")), "PutSession")
must(s.PutSession(ctx, "15550001111:2", []byte("sess-bbb")), "PutSession 2")
set("has_session_present", mustHas(ctx, s, "15550001111:0"))
set("get_session", hx(mustGet(ctx, s, "15550001111:0")))
set("get_session_missing_null", mustGet(ctx, s, "15550009999:0") == nil)
many, err := s.GetManySessions(ctx, []string{"15550001111:0", "15550009999:0", "15550001111:2"})
must(err, "GetManySessions")
set("get_many_present", hx(many["15550001111:0"]))
set("get_many_missing_null", many["15550009999:0"] == nil)
set("get_many_all_keys", len(many) == 3)
emptyMany, err := s.GetManySessions(ctx, nil)
must(err, "GetManySessions empty")
set("get_many_empty_null", emptyMany == nil)
must(s.PutManySessions(ctx, map[string][]byte{"15550002222:0": []byte("sess-ccc")}), "PutManySessions")
set("get_many_put", hx(mustGet(ctx, s, "15550002222:0")))
must(s.DeleteSession(ctx, "15550001111:2"), "DeleteSession")
set("has_session_after_delete", mustHas(ctx, s, "15550001111:2"))
must(s.DeleteAllSessions(ctx, "15550001111"), "DeleteAllSessions")
set("has_session_after_delete_all", mustHas(ctx, s, "15550001111:0"))
set("has_other_after_delete_all", mustHas(ctx, s, "15550002222:0"))
// --- migrate PN -> LID (sessions + identities + sender keys) ---
// Upstream renames over signal users, so the LID-side address embeds the
// agent ("100000000000001_1:0"); detz renames plain users ("...001.0").
// The vectors pin the upstream spelling; the C++ test asserts its own.
pn := jid("15550003333", types.DefaultUserServer)
lid := jid("100000000000001", types.HiddenUserServer)
pnAddr := pn.SignalAddressUser() + ":0"
lidAddr := lid.SignalAddressUser() + ":0"
set("mig_lid_addr", lidAddr)
must(s.PutSession(ctx, pnAddr, []byte("sess-mig")), "PutSession mig")
must(idn.PutIdentity(ctx, pnAddr, key(6)), "PutIdentity mig")
must(senderKeys.PutSenderKey(ctx, "group@g.us", pnAddr, []byte("sender-mig")), "PutSenderKey mig")
must(s.MigratePNToLID(ctx, pn, lid), "MigratePNToLID")
set("mig_session_moved", hx(mustGet(ctx, s, lidAddr)))
set("mig_session_gone", mustGet(ctx, s, pnAddr) == nil)
set("mig_identity_moved", mustTrust(ctx, idn, lidAddr, key(6)))
set("mig_identity_gone", mustTrust(ctx, idn, pnAddr, key(6)))
sk, err := senderKeys.GetSenderKey(ctx, "group@g.us", lidAddr)
must(err, "GetSenderKey migrated")
set("mig_sender_moved", hx(sk))
skOld, err := senderKeys.GetSenderKey(ctx, "group@g.us", pnAddr)
must(err, "GetSenderKey old")
set("mig_sender_gone", skOld == nil)
must(s.MigratePNToLID(ctx, pn, lid), "MigratePNToLID repeat")
set("mig_repeat_still_moved", hx(mustGet(ctx, s, lidAddr)))
// --- LID map ---
pnA := jid("15551112222", types.DefaultUserServer)
lidA := jid("100000000000002", types.HiddenUserServer)
_, err = lids.GetLIDForPN(ctx, pnA)
set("lid_miss_empty", err == nil)
must(lids.PutLIDMapping(ctx, lidA, pnA), "PutLIDMapping")
gotLID, err := lids.GetLIDForPN(ctx, pnA)
must(err, "GetLIDForPN")
set("lid_hit", gotLID.String())
pnDev := types.JID{User: "15551112222", Device: 3, Server: types.DefaultUserServer}
gotLIDDev, err := lids.GetLIDForPN(ctx, pnDev)
must(err, "GetLIDForPN device")
set("lid_hit_device", gotLIDDev.String())
gotPN, err := lids.GetPNForLID(ctx, lidA)
must(err, "GetPNForLID")
set("pn_hit", gotPN.String())
_, err = lids.GetLIDForPN(ctx, lidA)
set("lid_for_pn_rejects_lid", err != nil)
_, err = lids.GetPNForLID(ctx, pnA)
set("pn_for_lid_rejects_pn", err != nil)
set("put_mapping_rejects_swapped", lids.PutLIDMapping(ctx, pnA, lidA) != nil)
must(lids.PutManyLIDMappings(ctx, []store.LIDMapping{
{LID: jid("100000000000003", types.HiddenUserServer), PN: jid("15551113333", types.DefaultUserServer)},
{LID: jid("100000000000003", types.HiddenUserServer), PN: jid("15551113333", types.DefaultUserServer)},
{LID: pnA, PN: lidA}, // invalid: skipped, not an error
}), "PutManyLIDMappings")
multi, err := lids.GetManyLIDsForPNs(ctx, []types.JID{pnA, jid("15551113333", types.DefaultUserServer), jid("19998887777", types.DefaultUserServer), lidA})
must(err, "GetManyLIDsForPNs")
set("many_lids_hit", multi[pnA].String())
set("many_lids_new", multi[jid("15551113333", types.DefaultUserServer)].String())
_, missing := multi[jid("19998887777", types.DefaultUserServer)]
set("many_lids_missing_absent", !missing)
set("many_lids_size", len(multi))
// Remapping a PN to a new LID drops the old reverse mapping.
must(lids.PutLIDMapping(ctx, jid("100000000000004", types.HiddenUserServer), pnA), "PutLIDMapping remap")
remapped, err := lids.GetLIDForPN(ctx, pnA)
must(err, "GetLIDForPN remapped")
set("lid_remapped", remapped.String())
oldPN, err := lids.GetPNForLID(ctx, lidA)
must(err, "GetPNForLID stale")
set("lid_remap_old_gone", oldPN.String() == "")
// GetAltJID routing.
altPN, _ := dev.GetAltJID(ctx, pnA)
set("alt_pn", altPN.String())
altLID, _ := dev.GetAltJID(ctx, jid("100000000000004", types.HiddenUserServer))
set("alt_lid", altLID.String())
altGroup, _ := dev.GetAltJID(ctx, jid("120363000000", types.GroupServer))
set("alt_group_empty", altGroup.String() == "")
must(container.LIDMap.FillCache(ctx), "FillCache")
// --- contacts ---
u1 := jid("15552223333", types.DefaultUserServer)
changed, prev, err := contacts.PutPushName(ctx, u1, "Pushee")
must(err, "PutPushName")
set("push_first_changed", changed)
set("push_first_prev", prev)
changed, prev, err = contacts.PutPushName(ctx, u1, "Pushee")
must(err, "PutPushName same")
set("push_same_changed", changed)
changed, prev, err = contacts.PutPushName(ctx, u1, "Pushed")
must(err, "PutPushName change")
set("push_change_changed", changed)
set("push_change_prev", prev)
changed, _, err = contacts.PutBusinessName(ctx, u1, "Biz Co")
must(err, "PutBusinessName")
set("biz_first_changed", changed)
must(contacts.PutContactName(ctx, u1, "First", "First Last"), "PutContactName")
ci, err := contacts.GetContact(ctx, u1)
must(err, "GetContact")
set("contact_found", ci.Found)
set("contact_first", ci.FirstName)
set("contact_full", ci.FullName)
set("contact_push", ci.PushName)
set("contact_biz", ci.BusinessName)
missing2, err := contacts.GetContact(ctx, jid("15550004444", types.DefaultUserServer))
must(err, "GetContact missing")
set("contact_missing_found", missing2.Found)
must(contacts.PutAllContactNames(ctx, []store.ContactEntry{
{JID: jid("15550005555", types.DefaultUserServer), FirstName: "A", FullName: "A A"},
{JID: jid("15550005555", types.DefaultUserServer), FirstName: "B", FullName: "B B"},
{JID: jid("15550006666", types.DefaultUserServer), FirstName: "C", FullName: "C C"},
}), "PutAllContactNames")
dedup, err := contacts.GetContact(ctx, jid("15550005555", types.DefaultUserServer))
must(err, "GetContact dedup")
set("contact_dedup_first", dedup.FirstName)
must(contacts.PutManyRedactedPhones(ctx, []store.RedactedPhoneEntry{
{JID: jid("100000000000005", types.HiddenUserServer), RedactedPhone: "+1········80"},
}), "PutManyRedactedPhones")
red, err := contacts.GetContact(ctx, jid("100000000000005", types.HiddenUserServer))
must(err, "GetContact redacted")
set("contact_redacted", red.RedactedPhone)
all, err := contacts.GetAllContacts(ctx)
must(err, "GetAllContacts")
set("contact_all_has_u1", all[u1].PushName == "Pushed")
set("contact_all_count_ge", len(all) >= 4)
// --- chat settings ---
chat := jid("15553334444", types.DefaultUserServer)
group := jid("120363000001", types.GroupServer)
cs, err := chats.GetChatSettings(ctx, chat)
must(err, "GetChatSettings missing")
set("chat_missing_found", cs.Found)
set("chat_missing_muted", cs.MutedUntil.Unix())
set("chat_missing_muted_zero", cs.MutedUntil.IsZero())
must(chats.PutMutedUntil(ctx, chat, time.Unix(1700000000, 0)), "PutMutedUntil")
must(chats.PutPinned(ctx, chat, true), "PutPinned")
must(chats.PutArchived(ctx, chat, true), "PutArchived")
cs, err = chats.GetChatSettings(ctx, chat)
must(err, "GetChatSettings set")
set("chat_muted", cs.MutedUntil.Unix())
set("chat_pinned", cs.Pinned)
set("chat_archived", cs.Archived)
set("chat_found", cs.Found)
must(chats.PutMutedUntil(ctx, chat, store.MutedForever), "PutMutedUntil forever")
cs, err = chats.GetChatSettings(ctx, chat)
must(err, "GetChatSettings forever")
set("chat_forever", cs.MutedUntil.Equal(store.MutedForever))
must(chats.PutMutedUntil(ctx, chat, time.Time{}), "PutMutedUntil zero")
cs, err = chats.GetChatSettings(ctx, chat)
must(err, "GetChatSettings zero")
set("chat_unmuted", cs.MutedUntil.Unix())
set("chat_unmuted_zero", cs.MutedUntil.IsZero())
must(chats.PutPinned(ctx, chat, false), "PutPinned false")
cs, _ = chats.GetChatSettings(ctx, chat)
set("chat_unpinned", !cs.Pinned && cs.Archived)
must(chats.PutWASARootSecretID(ctx, group, "wasa-id-1"), "PutWASARootSecretID")
wasa, err := chats.GetWASARootSecretID(ctx, group)
must(err, "GetWASARootSecretID")
set("wasa_hit", string(wasa))
wasaMissing, err := chats.GetWASARootSecretID(ctx, chat)
must(err, "GetWASARootSecretID missing")
set("wasa_missing_empty", string(wasaMissing) == "")
// --- message secrets ---
pnB := jid("15554445555", types.DefaultUserServer)
lidB := jid("100000000000006", types.HiddenUserServer)
must(lids.PutLIDMapping(ctx, lidB, pnB), "PutLIDMapping secrets")
must(secrets.PutMessageSecret(ctx, pnB, pnB, "MSGID1", bytes.Repeat([]byte{0x42}, 32)), "PutMessageSecret")
sec, realSender, err := secrets.GetMessageSecret(ctx, pnB, pnB, "MSGID1")
must(err, "GetMessageSecret")
set("secret_hit", hx(sec))
set("secret_sender", realSender.String())
must(secrets.PutMessageSecret(ctx, pnB, pnB, "MSGID1", bytes.Repeat([]byte{0x43}, 32)), "PutMessageSecret dup")
sec, _, _ = secrets.GetMessageSecret(ctx, pnB, pnB, "MSGID1")
set("secret_dup_keeps_first", hx(sec))
secAlt, realAlt, err := secrets.GetMessageSecret(ctx, lidB, lidB, "MSGID1")
must(err, "GetMessageSecret alt")
set("secret_alt_hit", hx(secAlt))
set("secret_alt_sender", realAlt.String())
secMiss, _, err := secrets.GetMessageSecret(ctx, pnB, pnB, "NOPE")
must(err, "GetMessageSecret miss")
set("secret_miss_null", secMiss == nil)
must(secrets.PutMessageSecrets(ctx, []store.MessageSecretInsert{
{Chat: pnB, Sender: pnB, ID: "MSGID2", Secret: []byte("s2")},
{Chat: pnB, Sender: pnB, ID: "MSGID3"}, // empty secret: skipped
{Chat: types.EmptyJID, Sender: pnB, ID: "MSGID4", Secret: []byte("s4")},
}), "PutMessageSecrets")
sec2, _, _ := secrets.GetMessageSecret(ctx, pnB, pnB, "MSGID2")
set("secret_batch_hit", hx(sec2))
sec3, _, _ := secrets.GetMessageSecret(ctx, pnB, pnB, "MSGID3")
set("secret_batch_skipped", sec3 == nil)
// --- privacy tokens ---
t1 := store.PrivacyToken{User: pnB, Token: []byte("tok-1"), Timestamp: time.Unix(1700000000, 0)}
must(tokens.PutPrivacyTokens(ctx, t1), "PutPrivacyTokens")
got, err := tokens.GetPrivacyToken(ctx, pnB)
must(err, "GetPrivacyToken")
set("token_hit", hx(got.Token))
set("token_ts", got.Timestamp.Unix())
set("token_sender_ts_zero", got.SenderTimestamp.IsZero())
older := store.PrivacyToken{User: pnB, Token: []byte("tok-old"), Timestamp: time.Unix(1600000000, 0)}
must(tokens.PutPrivacyTokens(ctx, older), "PutPrivacyTokens older")
got, _ = tokens.GetPrivacyToken(ctx, pnB)
set("token_older_ignored", hx(got.Token))
withSender := store.PrivacyToken{User: pnB, Token: []byte("tok-2"), Timestamp: time.Unix(1700000001, 0), SenderTimestamp: time.Unix(1700000002, 0)}
must(tokens.PutPrivacyTokens(ctx, withSender), "PutPrivacyTokens newer")
got, _ = tokens.GetPrivacyToken(ctx, pnB)
set("token_newer_wins", hx(got.Token))
set("token_sender_ts", got.SenderTimestamp.Unix())
noSender := store.PrivacyToken{User: pnB, Token: []byte("tok-3"), Timestamp: time.Unix(1700000003, 0)}
must(tokens.PutPrivacyTokens(ctx, noSender), "PutPrivacyTokens no sender ts")
got, _ = tokens.GetPrivacyToken(ctx, pnB)
set("token_sender_ts_kept", got.SenderTimestamp.Unix())
gotAlt, err := tokens.GetPrivacyToken(ctx, lidB)
must(err, "GetPrivacyToken alt")
set("token_alt_hit", hx(gotAlt.Token))
set("token_alt_user", gotAlt.User.String())
must(tokens.PutPrivacyTokens(ctx,
store.PrivacyToken{User: jid("15550007777", types.DefaultUserServer), Token: []byte("tok-x"), Timestamp: time.Unix(1500000000, 0)},
store.PrivacyToken{User: jid("15550008888", types.DefaultUserServer), Token: []byte("tok-y"), Timestamp: time.Unix(1800000000, 0)},
), "PutPrivacyTokens batch")
n, err := tokens.DeleteExpiredPrivacyTokens(ctx, time.Unix(1600000000, 0))
must(err, "DeleteExpiredPrivacyTokens")
set("token_expired_deleted", n)
gotX, _ := tokens.GetPrivacyToken(ctx, jid("15550007777", types.DefaultUserServer))
set("token_expired_gone", gotX == nil)
set("put_tokens_empty_err", tokens.PutPrivacyTokens(ctx) != nil)
raw, err := json.MarshalIndent(out, "", " ")
must(err, "marshal")
os.Stdout.Write(raw)
os.Stdout.Write([]byte("\n"))
}
func mustTrust(ctx context.Context, idn store.IdentityStore, addr string, k [32]byte) bool {
t, err := idn.IsTrustedIdentity(ctx, addr, k)
must(err, "IsTrustedIdentity "+addr)
return t
}
func mustHas(ctx context.Context, s store.SessionStore, addr string) bool {
h, err := s.HasSession(ctx, addr)
must(err, "HasSession "+addr)
return h
}
func mustGet(ctx context.Context, s store.SessionStore, addr string) []byte {
b, err := s.GetSession(ctx, addr)
must(err, "GetSession "+addr)
return b
}