Resolve WhatsApp LID chat identifiers via the LID pair table

Recent WhatsApp versions key 1:1 chat sessions by an opaque LID rather
than the contact's phone number. Extract the ZWAPHONENUMBERLIDPAIR
table from the dedicated LID.sqlite database (or from ChatStorage
itself in versions that store it there) and use it to populate
partner_resolved_phone_number on chat session records and in timeline
events, without requiring the often-missing ContactsV2.sqlite. Each
pair is also extracted as a record and produces a lid_pair_recorded
timeline event marking when the association was learned.
This commit is contained in:
Donncha Ó Cearbhaill
2026-08-19 13:06:41 +02:00
parent 07066feba9
commit 06a949dafe
6 changed files with 149 additions and 15 deletions
+22 -5
View File
@@ -18,14 +18,24 @@ def test_extraction():
messages = [r for r in m.results if "ZTEXT" in r]
sessions = [r for r in m.results if r.get("record_type") == "chat_session"]
pairs = [
r for r in m.results
if r.get("record_type") == "lid_phone_number_pair"
]
assert len(messages) == 3
assert len(sessions) == 2
assert len(pairs) == 1
assert pairs[0]["lid"] == "100000000000001"
assert pairs[0]["phone_number"] == "14155550100"
assert pairs[0]["pair_timestamp"] == "2025-08-25 07:33:20.000000"
linked = next(r for r in messages if r.get("links"))
assert linked["links"] == ["https://example.org/news"]
alice = next(s for s in sessions if s["partner_name"] == "Alice Example")
assert alice["contact_jid"] == "14155550100@s.whatsapp.net"
assert alice["contact_jid"] == "100000000000001@lid"
assert alice["partner_resolved_phone_number"] == "+14155550100"
assert alice["first_stored_message_date"] == "2025-08-27 15:06:40.000000"
assert alice["last_message_date"] == "2025-08-28 18:53:20.000000"
assert alice["group_creation_date"] is None
@@ -39,19 +49,26 @@ def test_extraction():
assert group["last_stored_message_date"] == "2025-08-29 22:40:00.000000"
assert group["last_message_date"] == "2025-08-31 02:26:40.000000"
# 3 message events plus first/last per chat and the group creation.
assert len(m.timeline) == 8
# 3 message events, first/last per chat, the group creation and the
# LID-phone number pair.
assert len(m.timeline) == 9
events = {
(entry["event"], entry["timestamp"]): entry["data"]
for entry in m.timeline
}
# Alice's session is keyed by LID but labelled with the phone number
# resolved through LID.sqlite.
assert events[("chat_first_message", "2025-08-27 15:06:40.000000")] == (
"First stored message in WhatsApp chat with "
"'Alice Example' (14155550100@s.whatsapp.net)"
"'Alice Example' (+14155550100)"
)
assert events[("chat_last_message", "2025-08-28 18:53:20.000000")] == (
"Last message in WhatsApp chat with "
"'Alice Example' (14155550100@s.whatsapp.net)"
"'Alice Example' (+14155550100)"
)
assert events[("lid_pair_recorded", "2025-08-25 07:33:20.000000")] == (
"WhatsApp associated LID 100000000000001 with "
"phone number 14155550100"
)
assert events[("group_created", "2025-08-21 20:13:20.000000")] == (
"WhatsApp group chat 'Example Group' "
+4 -4
View File
@@ -15,8 +15,8 @@ class TestFilesystem:
def test_filesystem(self):
m = Filesystem(target_path=get_ios_backup_folder())
run_module(m)
assert len(m.results) == 21
assert len(m.timeline) == 21
assert len(m.results) == 23
assert len(m.timeline) == 23
assert len(m.alertstore.alerts) == 0
def test_detection(self, indicator_file):
@@ -29,6 +29,6 @@ class TestFilesystem:
)
m.indicators = ind
run_module(m)
assert len(m.results) == 21
assert len(m.timeline) == 21
assert len(m.results) == 23
assert len(m.timeline) == 23
assert len(m.alertstore.alerts) == 1