iMessageのバックアップファイルを読み解こうと試みる

(2011/11/11)Rubyスクリプトファイルを公開しました。sms2gmail iMessage暫定対応版

iOS5にアップデートしたらsms2gmailが思うように動いてくれなかったのでiPhoneのバックアップファイルを攻めてみました。

(最終更新 2011/11/07)

iOS4でのSMSやMMSと同様、iMessageの各メッセージは
~/Library/Application Support/MobileSync/Backup/{適当な文字列}/3d0d7e5fb2ce288813306e4d4636395e047a3d28
というSQLiteのデータベースファイルに保存されています*1
このファイルを読み込んで、messageテーブルを確認するとこんな感じ。

cid name type notnull dflt_value pk
0 ROWID INTEGER 0 1
1 address TEXT 0 0
2 date INTEGER 0 0
3 text TEXT 0 0
4 flags INTEGER 0 0
5 replace INTEGER 0 0
6 svc_center TEXT 0 0
7 group_id INTEGER 0 0
8 association_id INTEGER 0 0
9 height INTEGER 0 0
10 UIFlags INTEGER 0 0
11 version INTEGER 0 0
12 subject TEXT 0 0
13 country TEXT 0 0
14 headers BLOB 0 0
15 recipients BLOB 0 0
16 read INTEGER 0 0
17 madrid_attributedBody BLOB 0 0
18 madrid_handle TEXT 0 0
19 madrid_version INTEGER 0 0
20 madrid_guid TEXT 0 0
21 madrid_type INTEGER 0 0
22 madrid_roomname TEXT 0 0
23 madrid_service TEXT 0 0
24 madrid_account TEXT 0 0
25 madrid_flags INTEGER 0 0
26 madrid_attachmentInfo BLOB 0 0
27 madrid_url TEXT 0 0
28 madrid_error INTEGER 0 0
29 is_madrid INTEGER 0 0
30 madrid_date_read INTEGER 0 0
31 madrid_date_delivered INTEGER 0 0
32 madrid_account_guid TEXT 0 0

SMS/MMSかiMessageかの判別

SMS/MMSとiMessageが一緒に入っているのですが、

  • is_madrid=0: SMS/MMS
  • is_madrid=1: iMessage

ということでOK。is_madrid=0のデータはこれまでのSMS/MMSのデータと同じ扱いで大丈夫です。

iMessageのデータで注意すべき点

iMessageで新たに追加されたカラムには"madrid"がついていますが、"madrid"の付いていないカラムはほとんどが未使用です。"address"カラムさえ使われていません。
"date", "madrid_date_read", "madrid_date_delivered"カラムにはUNIX timeが入っていますが、SMS/MMSのデータでは1970/01/01 00:00:00からの経過秒が記録されているのに対して、iMessageのデータでは2001/01/01 00:00:00からの経過秒が記録されています。978307200を足したり引いたりすることで対処できます。
また、iMessageを"date"カラムの値で並べ替えると一部メッセージの順序がおかしくなるという問題があるので、"madrid_date_read", "madrid_date_delivered"カラムのデータを併用しなければなりません。ややこしいことに"madrid_date_read", "madrid_date_delivered"は中身がない場合もあるので、"madrid_date_delivered"→"madrid_date_read"→"date"という順番に参照することにしました。
"date"カラムには送信時刻が入っています。iMessageの画面で表示される時刻はこれを参照しているのですが、たまに時間がずれているようにも思えます*2。送信者と受信者のiPhoneの時刻がずれているか、iMessageサーバがなにかしら悪さをしているのでしょう。

フラグによるメッセージの種類の判別

現時点で確認しているフラグは以下の通り。

madrid_flags=12289 (0x3001), 77825 (0x13001)

受信メッセージです。宛先は自分ひとりの場合も複数人の場合もあります。"madrid_date_delivered"は常に0です。

madrid_flags=32773 (0x8005), 93809 (0x18005)

グループチャットでの送信メッセージです。"madrid_date_read"も"madrid_date_delivered"も常に0です。送信先が明確ではないため"madrid_handle"も空であることに注意。

madrid_flags=36869 (0x9005), 102405 (0x19005)

ひとりに対する送信メッセージです。"madrid_date_read"は常に0です。

madrid_flags=45061 (0xB005), 110597 (0x1B005)

ひとりに対する送信メッセージです。相手が「開封証明を送信」オプションをONにしているため、"madrid_date_read"にも"madrid_date_delivered"にも値が入っています。

"madrid_flags"の中身の予想
20の位 常に1
21の位 常に0
22の位 送信メッセージなら1、受信メッセージなら0
23の位 常に0
24〜211の位 常に0
212の位 madrid_date_*関係?
213の位 madrid_date_*関係?
214の位 常に0
215の位 送信メッセージなら1、受信メッセージなら0
216の位 本文にURLまたはメールアドレスが入っているとき1、入っていなければ0

ということは、

if madrid_flags & 0b100 == 0
  # 受信メッセージ
else
  # 送信メッセージ
end

ですね。

自分のアカウント名の読み取り

"madrid_account"に記録されています。おそらく「発信者ID」と対応していて

  • メールアドレスをIDにしているときは e:foo@bar.com
  • 電話番号をIDにしているときは p:+8190XXXXXXXX (※090番号のとき)

という文字列が入っています。

相手のアカウント名の読み取り

SMS/MMSで使われていた"address"カラムは使われていません。
そのかわり"madrid_handle"が使われており、

  • foo@bar.com
  • +8190XXXXXXXX (※090番号のとき)

のように、e:やp:をつけずそのまま相手の連絡先が入っているようです。

グループチャット参加者リストの読み取り

  • madrid_type=0: 1対1のメッセージ
  • madrid_type=1: 3人以上でのグループチャット

グループチャットをしている場合に限り、"madrid_roomname"カラムにチャットルームのIDが入ります。チャットの参加者リストは"madrid_roomname"の値をキーにしてmadrid_chatテーブルから引っ張り出してこなければいけません。
madrid_chatテーブルはこんな感じ。

cid name type notnull dflt_value pk
0 ROWID INTEGER 0 1
1 style INTEGER 0 0
2 state INTEGER 0 0
3 account_id TEXT 0 0
4 properties BLOB 0 0
5 chat_identifier TEXT 0 0
6 service_name TEXT 0 0
7 guid TEXT 0 0
8 room_name TEXT 0 0
9 account_login TEXT 0 0
10 participants BLOB 0 0

ちなみにこのテーブルには2人でのやりとりも含めiMessageのスレッド(?)全てが記録されていますが、参加者リストを引っ張り出してくる以外で使う必要はないと思われます。
madrid_chatテーブルの"room_name"にはグループチャットのスレッドにのみ、先ほどのmessageテーブルの"madrid_roomname"の値と同じものが入っています。これにより"participants"の値を取得して読み取ればグループチャットの参加者が特定できるのですが・・・"participants"カラムはBLOB型、入っているデータを見るとBinary plist形式でした。データ中にnull文字も入っているので単純にsqlite3から出力をリダイレクトするとデータがちょん切れてしまいますので注意。XMLに変換すると

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <array>
    <string>foo@bar.com</string>
    ...
  </array>
</plist>

のような単純な配列でした。Mac+RubyCocoaなら

group_member_plist = @db.get_first_value("select participants from madrid_chat where room_name=?", madrid_roomname)
group_member_array = OSX.load_plist(group_member_plist).map{|a| a.to_s.gsub(/ /,'')}

添付ファイルの取り出し(途中)

RubyCocoaは最新の1.0.2にアップデートしてください。それ以前はたぶんバグで落ちます。

require 'osx/cocoa'
require 'sqlite3'
db = SQLite3::Database.new("3d0d7e5fb2ce288813306e4d4636395e047a3d28")
db.execute("select madrid_attachmentInfo from message where is_madrid=1") do |att_info, dummy|
  if att_info != nil
    att_id_array = OSX::NSUnarchiver.unarchiveObjectWithData(OSX::NSData.dataWithRubyString(att_info))
    att_id_array.each do |att_id|
      db.execute("select filename,created_date from madrid_attachment where attachment_guid=?", att_id.to_s) do |filename, date|
        p filename
      end
    end
  end
end

ファイル名までは読めるので、Manifest.mbdbを調べて本体ファイルを拾えば良さそう。
http://stackoverflow.com/questions/3085153/how-to-parse-the-manifest-mbdb-file-in-an-ios-4-0-itunes-backup
このあたりを参考にして書く。iOS5からはmbdxファイルが存在しないのでDomainNameとPathをハイフンでつないでSHA1する。


とりあえず本日(10/30)、バックアップをとることはできました。

*1:iCloudでのバックアップはOFFにしています。ONのときはバックアップファイルが存在するかどうか未確認。

*2:たまに未来からメッセージが来ませんか?