Hatena::ブログ(Diary)

ROOT MX127

October 17(Mon), 2011

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テーブルを確認するとこんな感じ。

cidnametypenotnulldflt_valuepk
0ROWIDINTEGER0 1
1addressTEXT0 0
2dateINTEGER0 0
3textTEXT0 0
4flagsINTEGER0 0
5replaceINTEGER0 0
6svc_centerTEXT0 0
7group_idINTEGER0 0
8association_idINTEGER0 0
9heightINTEGER0 0
10UIFlagsINTEGER0 0
11versionINTEGER0 0
12subjectTEXT0 0
13countryTEXT0 0
14headersBLOB0 0
15recipientsBLOB0 0
16readINTEGER0 0
17madrid_attributedBodyBLOB0 0
18madrid_handleTEXT0 0
19madrid_versionINTEGER0 0
20madrid_guidTEXT0 0
21madrid_typeINTEGER0 0
22madrid_roomnameTEXT0 0
23madrid_serviceTEXT0 0
24madrid_accountTEXT0 0
25madrid_flagsINTEGER0 0
26madrid_attachmentInfoBLOB0 0
27madrid_urlTEXT0 0
28madrid_errorINTEGER0 0
29is_madridINTEGER0 0
30madrid_date_readINTEGER0 0
31madrid_date_deliveredINTEGER0 0
32madrid_account_guidTEXT0 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テーブルはこんな感じ。

cidnametypenotnulldflt_valuepk
0ROWIDINTEGER0 1
1styleINTEGER0 0
2stateINTEGER0 0
3account_idTEXT0 0
4propertiesBLOB0 0
5chat_identifierTEXT0 0
6service_nameTEXT0 0
7guidTEXT0 0
8room_nameTEXT0 0
9account_loginTEXT0 0
10participantsBLOB0 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:たまに未来からメッセージが来ませんか?