Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 

Repository files navigation

Reverse-engineering WhatsApp Android → already configured iPhone migration without factory reset

Status: field notes / proof of concept
Scope: Android → iPhone, where the iPhone is already configured and must not be factory-reset
Approach: local, open-source tooling, no jailbreak, no root required for the successful path described here

Why this exists

Official WhatsApp migration from Android to iPhone is designed primarily for a new or factory-reset iPhone during initial setup. This repository documents a successful reverse-engineered migration into an already configured iPhone, while preserving the existing iPhone WhatsApp state.

This is not an official WhatsApp, Apple, or Meta procedure.

The goal of this document is not to provide a polished end-user utility. It is to preserve technical findings that may help future developers, researchers, maintainers of open-source tools, and AI coding agents avoid repeating the same reverse-engineering work.


⚠️ Important warning

This workflow modifies WhatsApp's private iOS database and uses a partial MobileBackup2 restore.

A mistake can corrupt WhatsApp data or force you to restore the phone.

Before doing anything:

  • keep the original Android WhatsApp backup;
  • keep the original Android media directory;
  • make an encrypted local backup of the iPhone;
  • never overwrite your only working backup;
  • work on copies;
  • keep WhatsApp closed during database/restore operations;
  • do not publish real databases, media, phone numbers, LIDs, group JIDs, UDIDs, backup passwords, or WhatsApp encryption keys.

Use at your own risk.


Tested environment

The successful migration was tested with a contemporary 2026 stack roughly equivalent to:

  • Android 16
  • iOS 26.x
  • current WhatsApp versions on both platforms
  • Windows 11
  • Python 3.12
  • ADB
  • wa-crypt-tools
  • ios-backup-decryptor
  • pymobiledevice3

No Android root and no iPhone jailbreak were used.

Useful upstream projects:

  • wa-crypt-tools
  • ios-backup-decryptor
  • pymobiledevice3
  • SwiftWABackupAPI
  • historical reference: watoi

High-level architecture

Android WhatsApp
        │
        ▼
encrypted msgstore.db.crypt15
        │
        ▼
decrypt locally
        │
        ▼
Android msgstore.db
        │
        ├── chats
        ├── messages
        ├── groups
        ├── participants
        └── media metadata
        │
        ▼
normalize / map identities
        │
        ├── @s.whatsapp.net
        ├── @lid
        └── @g.us
        │
        ▼
merge into iOS ChatStorage.sqlite
        │
        ├── ZWACHATSESSION
        ├── ZWAMESSAGE
        ├── ZWAMEDIAITEM
        ├── ZWAGROUPINFO
        └── ZWAGROUPMEMBER
        │
        ▼
stage Message/Media/*
        │
        ▼
build partial encrypted iOS backup
        │
        ├── Manifest.db
        ├── MBFile metadata
        ├── per-file encryption keys
        └── hashed payload files
        │
        ▼
MobileBackup2 partial restore
        │
        ▼
reboot
        │
        ▼
existing WhatsApp installation now contains migrated history

Key discovery: why this is difficult

The problem is not just "copy a SQLite file".

Android and iOS WhatsApp use different internal databases, different identity representations, different media layouts, and different backup systems.

The difficult parts are:

  1. converting Android message data to the current iOS Core Data schema;
  2. resolving modern WhatsApp LIDs;
  3. merging history into already existing iPhone chats;
  4. recreating group participants correctly;
  5. preserving chronological message ordering;
  6. rebuilding media references;
  7. adding new files into an encrypted iOS backup;
  8. restoring only the WhatsApp App Group without factory-resetting the phone.

1. Android extraction

1.1 Pull the encrypted WhatsApp database

Typical Android location:

/sdcard/Android/media/com.whatsapp/WhatsApp/Databases/

Example:

adb pull `
  "/sdcard/Android/media/com.whatsapp/WhatsApp/Databases/msgstore.db.crypt15" `
  "C:\WA-transfer\msgstore.db.crypt15"

A normal non-root ADB session cannot read WhatsApp's private app directory under /data/user/0/....

That is expected.

1.2 Enable WhatsApp end-to-end encrypted backup

A practical route is to enable WhatsApp's end-to-end encrypted backup and use the locally known backup key.

Do not paste the raw backup key into chat logs, scripts, GitHub issues, shell history, or public repositories.

Decrypt locally using a tool such as wa-crypt-tools.

The result should be a valid SQLite database:

C:\WA-transfer\msgstore.db

Validate it:

PRAGMA integrity_check;

Expected:

ok

2. Android schema observations

Modern Android WhatsApp databases may contain tables including:

chat
message
message_text
message_media
jid
group_participant_user

Relevant identity information is commonly available through jid.

Useful fields include:

jid._id
jid.user
jid.server
jid.raw_string

Common server suffixes:

@s.whatsapp.net   classic user JID
@lid              modern LID identity
@g.us             group

Do not assume every chat is represented by a classic phone-number JID.


3. iPhone backup

Create an encrypted local iPhone backup.

With pymobiledevice3, a WhatsApp-focused backup can be created with a command similar to:

uvx --python 3.12 pymobiledevice3 backup2 backup `
  --full `
  --only whatsapp `
  --udid "<IPHONE_UDID>" `
  "C:\WA-transfer\iphone-backup"

The important WhatsApp App Group domain is:

AppDomainGroup-group.net.whatsapp.WhatsApp.shared

The main database is:

ChatStorage.sqlite

A frequently observed MobileBackup file ID for this file is:

7c7fba66680ef796b916b067077cc246adacf01d

Do not assume a hard-coded file ID forever; resolving by domain + relative path is safer.


4. Important iOS databases

Current WhatsApp installations may use at least:

ChatStorage.sqlite
ContactsV2.sqlite
LID.sqlite

LID.sqlite is extremely important for modern identity mapping.

A useful relationship observed in current databases:

ZIDENTIFIER  -> something@lid
ZPHONENUMBER -> normalized phone number

ContactsV2.sqlite may also contain fields such as:

ZLID
ZPHONENUMBER
ZWHATSAPPID
ZFULLNAME

5. iOS ChatStorage schema

Important Core Data tables include:

ZWACHATSESSION
ZWAMESSAGE
ZWAMEDIAITEM
ZWAMESSAGEINFO
ZWAGROUPINFO
ZWAGROUPMEMBER
Z_PRIMARYKEY

Observed Core Data entity IDs in the tested version:

WAChatSession   -> 4
WAGroupInfo     -> 5
WAGroupMember   -> 6
WAMediaItem     -> 8
WAMessage       -> 9
WAMessageInfo   -> 11

Never assume these IDs are permanent.

Always inspect:

SELECT Z_ENT, Z_NAME, Z_MAX
FROM Z_PRIMARYKEY;

6. Timestamp conversion

Android message timestamps are Unix milliseconds.

iOS WhatsApp stores timestamps relative to the Apple epoch.

Conversion:

ios_timestamp = android_timestamp_ms / 1000.0 - 978307200.0

The reverse is:

android_timestamp_ms = (ios_timestamp + 978307200.0) * 1000.0

7. Individual chat mapping

A robust mapping strategy is:

Android classic JID
        │
        ▼
normalized phone number
        │
        ▼
LID.sqlite / ContactsV2.sqlite
        │
        ▼
current iOS @lid
        │
        ▼
ZWACHATSESSION

Do not blindly convert:

123456789@s.whatsapp.net

into some guessed:

123456789@lid

LIDs are separate identifiers.

For existing iOS chats, useful fields are:

ZWACHATSESSION.ZCONTACTJID
ZWACHATSESSION.ZCONTACTIDENTIFIER

In the tested environment, a chat could contain:

ZCONTACTJID        = <contact-lid>@lid
ZCONTACTIDENTIFIER = <phone>@s.whatsapp.net

8. Minimal historical text message

A useful discovery was that historical plain-text messages can work with a minimal ZWAMESSAGE row.

It was not necessary to fabricate:

ZWAMEDIAITEM
ZWAMESSAGEINFO
messageSecret

for ordinary imported historical text messages.

This greatly simplifies text migration.

Important fields include:

Z_PK
Z_ENT
Z_OPT
ZFLAGS
ZISFROMME
ZMESSAGESTATUS
ZMESSAGETYPE
ZSORT
ZCHATSESSION
ZMESSAGEDATE
ZSENTDATE
ZFROMJID
ZTOJID
ZSTANZAID
ZTEXT

For the tested version, native text flags were observed approximately as:

outgoing: 16777280
incoming: 16777216

Do not hard-code these without first checking a native message from the target version.


9. Core Data primary-key bookkeeping

When inserting rows manually, update the corresponding Core Data high-water mark:

UPDATE Z_PRIMARYKEY
SET Z_MAX = <new max PK>
WHERE Z_NAME = 'WAMessage';

Likewise for media:

UPDATE Z_PRIMARYKEY
SET Z_MAX = <new max PK>
WHERE Z_NAME = 'WAMediaItem';

Failure to maintain these values can result in future collisions when WhatsApp inserts new records.


10. Message ordering and ZSORT

ZSORT is per-chat ordering.

Do not simply append imported historical messages to the end.

A safe approach is to build a combined chronology:

existing native messages
+
imported text messages
+
imported media messages

Then assign sequential ZSORT values.

For some group chats, the current iOS database may already contain system messages at early ZSORT positions. Preserve those when necessary.

Always validate:

unique sort positions
monotonic chronology
correct last message
correct message counter

11. Group chats

Groups use exact @g.us identifiers.

If the same group already exists on iOS, map directly to its ZWACHATSESSION.

Incoming group messages require the correct:

ZWAMESSAGE.ZGROUPMEMBER

which references:

ZWAGROUPMEMBER.Z_PK

A message being in the correct group is not sufficient to show the correct author.

For incoming group messages, the tested structure looked roughly like:

ZCHATSESSION = <group chat>
ZGROUPMEMBER = <specific sender member PK>
ZFROMJID     = <group>@g.us
ZTOJID       = <local account>@s.whatsapp.net
ZLASTSESSION = <group chat>

Outgoing:

ZGROUPMEMBER = NULL
ZFROMJID     = NULL
ZTOJID       = <group>@g.us

12. Media discovery

Android media metadata is found in message_media.

Common Android message types observed:

1   image
2   audio / voice
3   video
9   document
13  GIF
20  sticker

Some special types such as view-once media should be treated separately.

Do not convert private app paths such as:

/data/user/0/com.whatsapp/files/ViewOnce/...

unless you explicitly understand the semantics and have legitimate access to the source file.

A normal non-root ADB session cannot read those paths.


13. iOS media type mapping

Observed / externally corroborated iOS message types:

1   image
2   video
3   audio
8   document
11  GIF
15  sticker

Therefore a useful Android → iOS mapping is:

Android 1   -> iOS 1
Android 3   -> iOS 2
Android 2   -> iOS 3
Android 9   -> iOS 8
Android 13  -> iOS 11
Android 20  -> iOS 15

Validate against native messages from the target WhatsApp version whenever possible.


14. ZWAMEDIAITEM

Observed schema included:

Z_PK
Z_ENT
Z_OPT
ZCLOUDSTATUS
ZFILESIZE
ZMEDIAORIGIN
ZMOVIEDURATION
ZMESSAGE
ZASPECTRATIO
ZHACCURACY
ZLATITUDE
ZLONGITUDE
ZMEDIAURLDATE
ZAUTHORNAME
ZCOLLECTIONNAME
ZMEDIALOCALPATH
ZMEDIAURL
ZTHUMBNAILLOCALPATH
ZTITLE
ZVCARDNAME
ZVCARDSTRING
ZXMPPTHUMBPATH
ZMEDIAKEY
ZMETADATA

The relationship is bidirectional:

ZWAMESSAGE.ZMEDIAITEM -> ZWAMEDIAITEM.Z_PK
ZWAMEDIAITEM.ZMESSAGE -> ZWAMESSAGE.Z_PK

Validate both directions after insertion.


15. Critical media-path discovery

For a native image, WhatsApp stored something like:

ZWAMEDIAITEM.ZMEDIALOCALPATH =
Media/<lid>/5/0/<uuid>.jpg

while the actual MobileBackup relative path was:

Message/Media/<lid>/5/0/<uuid>.jpg

This distinction matters.

In other words:

backup_relative_path = "Message/" + ZMEDIALOCALPATH

A native thumbnail followed the same pattern:

ZWAMEDIAITEM.ZXMPPTHUMBPATH =
Media/<lid>/5/0/<uuid>.thumb

Backup path:

Message/Media/<lid>/5/0/<uuid>.thumb

16. Caption and MIME type discovery

For an image with a caption:

ZWAMESSAGE.ZTEXT = NULL
ZWAMEDIAITEM.ZTITLE = <caption>
ZWAMEDIAITEM.ZVCARDSTRING = image/jpeg

This is easy to miss if you search only ZWAMESSAGE.ZTEXT.


17. Encrypted iOS backup structure

Adding a file into an encrypted iOS backup is not equivalent to dropping a JPEG into a folder.

A MobileBackup entry involves:

Manifest.db row
MBFile / NSKeyedArchiver metadata
fileID
relativePath
flags
file size
digest
protection class
per-file encryption key
wrapped encryption key
encrypted payload

The usual file ID formula is:

sha1(f"{domain}-{relative_path}".encode()).hexdigest()

Physical payload location:

<backup>/<first two chars of fileID>/<fileID>

18. Important encrypted-backup lesson

When cloning a native file entry as a template for a new file, updating only:

Size
InodeNumber
EncryptionKey

was not enough.

The archived MBFile object must also contain the correct internal:

RelativePath
Digest

A mismatch can cause MobileBackup2 restore errors.

This was one of the most important failure modes discovered during the migration.


19. Partial restore manifest

For a minimal WhatsApp App Group restore, do not keep only:

ChatStorage.sqlite

The root App Group directory entry must also remain.

A successful minimal database-only restore required at least:

AppDomainGroup-group.net.whatsapp.WhatsApp.shared
    relativePath=""
    flags=2

AppDomainGroup-group.net.whatsapp.WhatsApp.shared
    relativePath="ChatStorage.sqlite"
    flags=1

Dropping the App Group root caused restore failure.

For media, retain all required directory entries such as:

Message
Message/Media
Message/Media/<jid>
Message/Media/<jid>/5
Message/Media/<jid>/5/0

plus each encrypted media file entry.


20. Restore behavior

A partial restore can be initiated through pymobiledevice3.

Conceptually:

await service.restore(
    backup_directory=BACKUP,
    system=True,
    reboot=True,
    copy=False,
    settings=True,
    remove=False,
    password=password,
    source=UDID,
    skip_apps=True,
)

Important observations:

reboot=False

A restore performed without reboot appeared to stage the new files but did not make the modified WhatsApp database live.

reboot=True

The modified files became active after reboot.

Therefore the reboot was part of the successful commit path in the tested environment.


21. Find My iPhone

The restore may fail if Find My iPhone is enabled.

One observed failure was associated with:

MBErrorDomain/211

Temporarily disabling Find My allowed the restore to proceed.

Remember to enable Find My again after the migration.


22. Setup Assistant after partial restore

A partial restore can cause iOS to boot into Setup Assistant even though the phone was not erased.

In the tested case, the assistant could be completed/skipped and the phone returned to its previous configured state.

This is still an unsupported workflow, so do not assume identical behavior on every iOS version.


23. MobileBackup2 errors observed

MBErrorDomain/1

Example:

Timeout waiting for SpringBoard notification from SpringBoard
that it's ready for a restore

This may happen when the phone is still stuck in a previous restore state.

A force restart returned the device to a usable state, after which the restore could be retried.

pymobiledevice3 KeyError: 22

A failed file-transfer operation may be obscured by a secondary exception such as:

KeyError: 22

inside device_link.py.

Do not immediately assume this is the root cause.

Inspect the generated backup metadata and the device state.


24. Validation checklist before restore

Never restore immediately after generating the database.

At minimum:

PRAGMA integrity_check;
PRAGMA foreign_key_check;

Also validate programmatically:

[ ] every imported message has the expected chat
[ ] every stanza ID is unique where expected
[ ] every media message points to a real ZWAMEDIAITEM
[ ] every ZWAMEDIAITEM points back to the correct message
[ ] every staged media path exists
[ ] every staged media file has a Manifest.db row
[ ] every Manifest.db file row has a physical encrypted payload
[ ] every required directory is in Manifest.db
[ ] Z_PRIMARYKEY high-water marks are correct
[ ] source backups were not modified

25. Recommended safety model

Use separate paths for every stage:

original Android files
        │
        ├── read only
        ▼
working Android DB
        │
        ▼
working iOS DB
        │
        ▼
final iOS DB
        │
        ▼
media staging
        │
        ▼
new restore backup

Never modify the only copy of:

msgstore.db.crypt15
msgstore.db
original iPhone backup

26. Privacy model for a future public tool

A future open-source utility should ideally be:

100% local
no cloud processing
no telemetry containing message data
no raw WhatsApp key logging
no backup-password logging
no media uploads

Backup passwords should be entered using a hidden prompt such as Python's:

getpass.getpass()

Never place them in command-line arguments or traceback output.


27. What should be automated in a real tool

A useful future CLI could look like:

wabridge doctor
wabridge extract-android
wabridge analyze
wabridge build
wabridge verify
wabridge restore

Internally, it should avoid direct Android-SQL → iOS-SQL spaghetti.

A better design is:

Android adapter
      │
      ▼
common internal model
      │
      ├── Chat
      ├── Participant
      ├── Message
      ├── Attachment
      ├── Group
      ├── Reaction
      └── Quote
      │
      ▼
iOS adapter

This would also make an eventual iPhone → Android direction possible.


28. Avoid hard-coding versions

WhatsApp changes schemas frequently.

A robust tool should inspect schemas dynamically:

PRAGMA table_info(...)
SELECT * FROM sqlite_master;
SELECT * FROM Z_PRIMARYKEY;

Then select a compatibility adapter based on observed columns and structures.

Do not build the entire program around one exact WhatsApp version.


29. Known limitations of this proof of concept

The successful path intentionally did not attempt to perfectly migrate every WhatsApp feature.

Examples that need more work:

view-once media
some private/service media types
reactions
polls
stickers using newer container formats
edited-message history
advanced reply metadata
channels
status content
payments
live locations
every possible system/group event

The goal was preservation of normal chat history, groups, and common media.


30. Things we learned the hard way

This is the section most likely to save somebody time.

1. Modern WhatsApp identity is not just phone-number JIDs

You need to understand @lid.

2. Group author mapping matters

The correct group chat alone is not enough.

ZGROUPMEMBER must identify the sender.

3. Historical text messages can be simpler than native current messages

A minimal ZWAMESSAGE worked without fabricating media shells or message-secret data.

4. ZMEDIALOCALPATH is not the full backup path

Database:

Media/...

Backup:

Message/Media/...

5. Image captions may be in ZWAMEDIAITEM.ZTITLE

Not in ZWAMESSAGE.ZTEXT.

6. An encrypted backup file needs more than ciphertext

The Manifest metadata matters just as much as the payload.

7. Cloned MBFile metadata must have a correct internal RelativePath

Changing only the SQL Files.relativePath is not sufficient.

8. The App Group root directory must remain in a partial restore

Keeping only ChatStorage.sqlite can fail.

9. reboot=False may only stage the restore

A reboot was required before WhatsApp used the modified database.

10. Repeated full snapshots are expensive

Once the schema is understood, prefer:

offline DB work
offline validation
one final restore

rather than repeated device backups after every experiment.


31. Result

The proof of concept successfully demonstrated that it is technically possible to:

  • decrypt a modern Android WhatsApp backup;
  • merge historical chats into an already configured iPhone;
  • preserve existing iPhone WhatsApp conversations;
  • migrate individual chats;
  • migrate group chats;
  • preserve group senders;
  • migrate common media;
  • use an encrypted local iOS backup as the transport;
  • perform a targeted restore;
  • avoid a factory reset;
  • avoid jailbreak/root.

This does not make the procedure officially supported or universally safe.


32. If you want to continue this work

Useful project directions:

  • package the workflow into a Python CLI;
  • add schema adapters;
  • write automated database validators;
  • add synthetic test databases;
  • support reactions/replies/polls;
  • improve media metadata generation;
  • support Android ↔ iOS in both directions;
  • build a GUI only after the migration engine is reliable.

The most valuable contribution would probably be a heavily tested compatibility layer rather than a pretty interface.


33. Responsible publishing notes

If you publish your own findings, do not include:

real phone numbers
real LIDs
real group JIDs
device UDIDs
real message text
contact names
actual WhatsApp databases
actual media
WhatsApp E2E backup keys
iPhone backup passwords
decrypted Manifest.db files from real devices

Prefer placeholders:

<PHONE>@s.whatsapp.net
<CONTACT_LID>@lid
<GROUP_ID>@g.us
<IPHONE_UDID>
C:\Users\<user>\

License / reuse

These notes are intended to help open-source research.

If code from third-party projects is copied into a future implementation, follow the license of each upstream project. Some useful iOS/WhatsApp tooling is GPL-licensed while other projects use permissive licenses.

Do not assume that snippets from different projects can be combined into a closed-source product without checking their licenses.


Final note

The important conclusion is simple:

Android → already configured iPhone WhatsApp migration without factory reset is possible, but it currently requires understanding both WhatsApp database formats and the encrypted iOS MobileBackup2 format.

The procedure is complex mainly because there is no supported "merge Android WhatsApp history into my existing iPhone WhatsApp" API.

Hopefully these notes save the next person — or the next AI agent — several hours of archaeology.

About

Reverse-engineering notes for migrating WhatsApp from Android to an already configured iPhone without factory reset.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors