Skip to content

Add Signal Messenger Android parser (#1602)#2913

Open
cleitonaugusto wants to merge 5 commits into
sepinf-inc:masterfrom
cleitonaugusto:#1602_signal_parser
Open

Add Signal Messenger Android parser (#1602)#2913
cleitonaugusto wants to merge 5 commits into
sepinf-inc:masterfrom
cleitonaugusto:#1602_signal_parser

Conversation

@cleitonaugusto

@cleitonaugusto cleitonaugusto commented Jul 5, 2026

Copy link
Copy Markdown

Summary

Implements a native Signal Messenger Android parser for IPED, extracting individual and group conversations from `signal.db` forensic acquisitions.

Architecture

Class Responsibility
`SignalParser` Tika entry point; MIME `application/x-signal-db`; glob `signal.db`
`SignalExtractor` JDBC queries against the plaintext SQLite
`SignalContact` Display name resolution: `system_joined_name` → `profile_joined_name` → `profile_given_name+family` → `e164` → "Unknown"
`SignalChat` Thread model (individual and group)
`SignalMessage` Message model with `MessageType` enum
`ReportGenerator` Signal-themed chat bubble HTML report

Signal schema compatibility

Targets Signal Android pre-v5.15 (no `aci` column). Queries: `recipient`, `thread`, `message`, `groups`, `group_membership`. Message type classification uses the lower-5-bit mask from `MessageTypes.kt`.

ExtraProperties alignment

  • `USER_ACCOUNT_TYPE` set on every message document (required for IPED communication graph)
  • `GROUP_ID` uses the actual Signal `group_id` from the `recipient` table
  • `PARTICIPANTS`: device owner (self) is the first entry in every chat, identified from `from_recipient_id` on outgoing messages — consistent with the WhatsApp and Threema parsers
  • `MESSAGE_FROM` / `MESSAGE_TO` carry the device owner's identifier on outgoing and incoming messages respectively
  • `IS_GROUP_MESSAGE` / `MESSAGE_DATE` / `DECODED_DATA` / `HASCHILD` follow established parser conventions

Security

Template substitution in `ReportGenerator` uses a single-pass `Matcher.replaceAll()` with `Matcher.quoteReplacement()` to prevent user-controlled group names containing `${placeholder}` strings from cascading through sequential `String.replace()` calls — which would corrupt the forensic HTML report.

Configuration

  • `CustomSignatures.xml` — three MIME types registered; `message/x-signal-message` inherits from `message/x-chat-message`
  • `ParserConfig.xml` — `SignalParser` registered with `extractMessages=true`
  • `CategoriesConfig.json` — Signal appears under "Chats → Signal" and "Instant Messages"
  • `IconManager.java` — all three MIME types mapped to the existing `signal.png` icon

Device owner identification

`SignalExtractor.findSelfContact()` runs a single correlated subquery: it selects the `recipient` row whose `_id` appears most frequently as `from_recipient_id` in outgoing messages (`type & 31 IN (0,1,2,3,4,5)`). Returns `null` gracefully when no outgoing messages exist, preserving the Phase-1 fallback (empty string) without any exception.

Tests

25 tests via a synthetic SQLite fixture (`test_signal.db`) without `aci` column to prove pre-v5.15 compatibility. Coverage: named contacts, phone-only contacts, groups, sent/received/null-body messages, emoji + HTML special chars, call types (outgoing/incoming/missed), system message filtering, group outgoing `MESSAGE_TO`, `USER_ACCOUNT_TYPE` on all message docs, `GROUP_ID` equals actual Signal `group_id`, device owner in `PARTICIPANTS` for every chat, template injection prevention, `extractMessages` flag.

```
Tests run: 25, Failures: 0, Errors: 0, Skipped: 0
```

Closes #1602

Extracts individual and group conversations from Signal Android databases
(signal.db) obtained through full-filesystem forensic acquisition.

## Architecture

- SignalParser      – Tika AbstractParser; MIME type application/x-signal-db
- SignalExtractor   – JDBC queries against the plaintext SQLite; validates
                      required tables before parsing
- SignalContact     – Display name resolution priority:
                      system_joined_name → profile_joined_name →
                      profile_given_name+family → e164 → "Unknown"
- SignalChat        – Thread model (individual and group)
- SignalMessage     – Message model with MessageType enum
- ReportGenerator   – Produces Signal-themed chat bubble HTML

## Signal schema compatibility

Targets Signal Android ≤5.15 (no aci column). Queries: recipient,
thread, message, groups, group_membership tables. Message type
classification uses the lower-5-bit mask from MessageTypes.kt:
  0/2/3/4/5 → OUTGOING, 20 → INCOMING, 1 → CALL_OUTGOING,
  21 → CALL_INCOMING, 22/25 → CALL_MISSED, else → SYSTEM.

## ExtraProperties alignment (vs WhatsApp/Threema parsers)

- USER_ACCOUNT_TYPE set to "message/x-signal-message" on every message
  document (required for IPED's communication graph task).
- GROUP_ID uses the actual Signal group_id from recipient.group_id,
  not a synthetic "SignalThread_<id>".
- MESSAGE_FROM / MESSAGE_TO / IS_GROUP_MESSAGE / PARTICIPANTS /
  MESSAGE_DATE / DECODED_DATA / HASCHILD aligned with established
  parser conventions.

## Category registration (CategoriesConfig.json)

- Chats → Signal: added application/x-signal-db and
  application/x-signal-chat alongside existing UFED entry.
- Instant Messages Artifacts → Instant Messages: added
  message/x-signal-message alongside x-threema-message,
  x-whatsapp-message, etc.

## Known Phase-1 limitation

PARTICIPANTS does not include the device owner (self). Identifying self
requires matching against aci or SharedPreferences outside signal.db;
this is deferred to Phase 2.

## Tests (23 total)

Synthetic SQLite fixture without aci column mimics pre-v5.15 backups.
Coverage: named contacts, phone-only contacts, groups, sent/received/
null-body messages, emoji + HTML special chars, call types
(outgoing/incoming/missed), system message filtering, group outgoing
MESSAGE_TO (chat title, not "Unknown"), USER_ACCOUNT_TYPE on all
message docs, GROUP_ID equals actual Signal group_id, extractMessages
flag.
## Security: Template injection in ReportGenerator (evidence integrity)

Sequential String.replace() calls allowed user-controlled data containing
template placeholders (e.g. a group named "${messages}") to cascade through
substitution. A suspect could name a Signal group "${messages}" to cause the
forensic report to render all conversation messages in the title position
instead of the group name — corrupting evidence integrity.

Fix: replace all four placeholders in a single Matcher.replaceAll() pass.
Matcher.quoteReplacement() prevents replacement strings from being treated
as regex patterns. The replacement callback runs once per placeholder in
the original template; injected placeholders in substituted content are
never re-evaluated.

Adds testTemplateInjectionInGroupTitle to verify that a group named
"${messages}" renders the literal text in the title and does not cause
message content to be injected or duplicated.

## Also: Signal MIME types in CategoriesConfig.json

- Chats → Signal: application/x-signal-db and application/x-signal-chat
  added alongside the existing UFED entry so forensic analysts find
  Signal data under the expected category.
- Instant Messages Artifacts → Instant Messages: message/x-signal-message
  added alongside message/x-threema-message, message/x-whatsapp-message, etc.
…s.xml

Was incorrectly declared as a sub-class of itself. Correct parent type is
message/x-chat-message, consistent with message/x-whatsapp-message,
message/x-threema-message, and message/x-telegram-message.
Resolves the Phase-1 limitation where the device owner's identity was
absent from PARTICIPANTS and MESSAGE_FROM/MESSAGE_TO for outgoing messages.

Detection strategy: outgoing Signal messages carry from_recipient_id
pointing to the device owner's own recipient row. A single correlated
subquery selects the recipient who appears most frequently as
from_recipient_id in outgoing message types (type & 31 in {0,1,2,3,4,5}).
Returns null gracefully when no outgoing messages exist (Phase-1 fallback).

Behaviour changes:
- PARTICIPANTS: self is now the first entry in every chat, consistent
  with the WhatsApp and Threema parsers. Group chats add self first,
  then members from group_membership excluding self (no duplication).
  Individual chats add self first, then the other party.
- MESSAGE_FROM: outgoing messages carry self's full identifier instead
  of an empty string.
- MESSAGE_TO: incoming individual messages carry self's full identifier
  instead of an empty string.

Tests (25 total, 0 failures):
- AbstractPkgTest: participants collector updated to capture ALL values
  via metadata.getValues() — previously only the first entry per chat
  was captured, which would have hidden self after Phase 2 reordering.
- testCallOutgoingIsFromMe: assertion updated from empty MESSAGE_FROM
  to SELF_FULL_ID.
- testSelfInParticipants (new): verifies self appears once per chat
  across all 3 chats.
- test_signal.db fixture: recipient 5 (Device Owner +5511999990005)
  added; from_recipient_id on all outgoing messages set to 5; self
  added to group_membership for the test group.
@cleitonaugusto

Copy link
Copy Markdown
Author

Did another pass before asking for your time. A few things I fixed:

The catch (Exception e) block was swallowing SAXException, which means WriteLimitReachedException never propagated up — size-limit stops would surface as parse errors instead of clean pipeline terminations. Added catch (SAXException e) { throw e; } before the general handler.

shouldParseEmbedded was only checked for the parent database item, not for each chat report or message document individually. Added the per-child checks in both createReports() and extractMessages().

readResource() was silently returning an empty string when the HTML template or CSS wasn't found on the classpath, producing blank forensic reports with no indication of why. Now logs a warning instead.

- Re-throw SAXException before catch(Exception) so Tika's pipeline stop
  mechanism (WriteLimitReachedException) works correctly
- Call shouldParseEmbedded before each chat report and message item,
  honouring IPED's per-item filtering logic (same fix applied to Android
  call log parser in a previous commit)
- readResource() now logs a WARN when the HTML template or CSS file is
  missing from the classpath, instead of silently returning empty string
  that produces blank forensic reports
- Fix misleading test comment: "→ 8 indexed" corrected to "→ 9 indexed (3+3+3)"
  — the assertion was always correct (EXPECTED_MESSAGE_DOCS = 9)

All 25 Signal unit tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Develop a parser to decode signal databases

1 participant