iphone_backup.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. const path = require('path')
  2. const sqlite3 = require('sqlite3')
  3. const bplist = require('bplist-parser')
  4. const fs = require('fs')
  5. const plist = require('plist')
  6. const mac_address_parse = require('./mac_address_parse')
  7. const tz_offset = 5
  8. const databases = {
  9. SMS: '3d0d7e5fb2ce288813306e4d4636395e047a3d28',
  10. Contacts: '31bb7ba8914766d4ba40d6dfb6113c8b614be442',
  11. Calendar: '2041457d5fe04d39d0ab481178355df6781e6858',
  12. Reminders: '2041457d5fe04d39d0ab481178355df6781e6858',
  13. Notes: 'ca3bc056d4da0bbf88b5fb3be254f3b7147e639c',
  14. Notes2: '4f98687d8ab0d6d1a371110e6b7300f6e465bef2',
  15. Calls: '2b2b0084a1bc3a5ac8c27afdf14afb42c61a19ca',
  16. Calls2: '5a4935c78a5255723f707230a451d79c540d2741',
  17. Locations: '4096c9ec676f2847dc283405900e284a7c815836',
  18. WebHistory: 'e74113c185fd8297e140cfcf9c99436c5cc06b57',
  19. Photos: '12b144c0bd44f2b3dffd9186d3f9c05b917cee25',
  20. WiFi: 'ade0340f576ee14793c607073bd7e8e409af07a8',
  21. Voicemail: '992df473bbb9e132f4b3b6e4d33f72171e97bc7a'
  22. }
  23. var cache = {}
  24. class iPhoneBackup {
  25. constructor (id, status, info, manifest, base) {
  26. this.id = id
  27. this.status = status
  28. this.info = info
  29. this.manifest = manifest
  30. this.base = base
  31. }
  32. // Open a backup with a specified ID
  33. // base is optional and will be computed if not used.
  34. static fromID (id, base) {
  35. // Get the path of the folder.
  36. if (base) {
  37. base = path.join(base, id)
  38. } else {
  39. base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', id)
  40. }
  41. // Parse manifest bplist files
  42. try {
  43. if (global.verbose) console.log('parsing status', base)
  44. var status = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Status.plist')))[0]
  45. } catch (e) {
  46. console.log('Cannot open Status.plist', e)
  47. }
  48. try {
  49. if (global.verbose) console.log('parsing manifest', base)
  50. var manifest = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Manifest.plist')))[0]
  51. } catch (e) {
  52. console.log('Cannot open Manifest.plist', e)
  53. }
  54. try {
  55. if (global.verbose) console.log('parsing status', base)
  56. var info = plist.parse(fs.readFileSync(path.join(base, 'Info.plist'), 'utf8'))
  57. } catch (e) {
  58. console.log('Cannot open Info.plist', e)
  59. }
  60. return new iPhoneBackup(id, status, info, manifest, base)
  61. }
  62. get iOSVersion () {
  63. return this.manifest.Lockdown.ProductVersion
  64. }
  65. getFileName (fileID, isAbsoulte) {
  66. isAbsoulte = isAbsoulte || false
  67. //const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', this.id)
  68. // Return v2 filename
  69. if (this.status.Version < 3 || isAbsoulte) {
  70. return path.join(this.base, fileID)
  71. } else {
  72. // v3 has folders
  73. return path.join(this.base, fileID.substr(0, 2), fileID)
  74. }
  75. }
  76. getDatabase (fileID, isAbsoulte) {
  77. isAbsoulte = isAbsoulte || false
  78. // Get the backup folder
  79. // Return v2 filename
  80. if (this.status.Version < 3 || isAbsoulte) {
  81. return new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY)
  82. } else {
  83. // v3 has folders
  84. return new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY)
  85. }
  86. }
  87. queryDatabase (databaseID, sql) {
  88. return new Promise((resolve, reject) => {
  89. var messagedb = this.getDatabase(databaseID)
  90. messagedb.all(sql, async function (err, rows) {
  91. if (err) reject(err)
  92. resolve(rows)
  93. })
  94. })
  95. }
  96. getName (messageDest) {
  97. return new Promise((resolve, reject) => {
  98. if (messageDest.indexOf('@') === -1) {
  99. messageDest = messageDest.replace(/[\s+\-()]*/g, '')
  100. if (messageDest.length == 11 && messageDest[0] == '1') {
  101. messageDest = messageDest.substring(1)
  102. }
  103. }
  104. if (cache[messageDest] !== undefined) {
  105. return resolve(cache[messageDest])
  106. }
  107. var contactdb = this.getDatabase(databases.Contacts)
  108. contactdb.get(`SELECT
  109. c0First as first,
  110. c1Last as last,
  111. c2Middle as middle,
  112. c15Phone as phones
  113. from ABPersonFullTextSearch_content WHERE c15Phone like '%${messageDest}%'`,
  114. (err, row) => {
  115. if (err) return resolve()
  116. if (!row) return resolve()
  117. var result = {
  118. name: [row.first, row.middle, row.last].filter(el => el != null).join(' '),
  119. phones: row.phones.split(' '),
  120. query: messageDest
  121. }
  122. if (row) cache[messageDest] = result
  123. resolve(result)
  124. })
  125. })
  126. }
  127. getMessagesiOS9 (chatId) {
  128. var backup = this
  129. return new Promise((resolve, reject) => {
  130. var messagedb = this.getDatabase(databases.SMS)
  131. messagedb.all(`
  132. SELECT
  133. message.*,
  134. handle.id as sender_name,
  135. datetime(date + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
  136. FROM chat_message_join
  137. INNER JOIN message
  138. ON message.rowid = chat_message_join.message_id
  139. INNER JOIN handle
  140. ON handle.rowid = message.handle_id
  141. WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
  142. async function (err, chats) {
  143. if (err) return reject(err)
  144. chats = chats || []
  145. // Compute the user's name
  146. for (var i in chats) {
  147. var el = chats[i]
  148. el.x_sender = el.is_from_me ? 'Me' : el.sender_name
  149. if (!el.is_from_me) {
  150. var contact = await backup.getName(el.sender_name)
  151. if (contact) {
  152. el.x_sender = `${contact.name} <${contact.query}>`
  153. }
  154. }
  155. }
  156. resolve(chats)
  157. })
  158. })
  159. }
  160. getMessagesiOS10iOS11 (chatId) {
  161. var backup = this
  162. return new Promise((resolve, reject) => {
  163. var messagedb = this.getDatabase(databases.SMS)
  164. messagedb.all(`
  165. SELECT
  166. message.*,
  167. handle.id as sender_name,
  168. datetime(date / 1000000000 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
  169. FROM chat_message_join
  170. INNER JOIN message
  171. ON message.rowid = chat_message_join.message_id
  172. INNER JOIN handle
  173. ON handle.rowid = message.handle_id
  174. WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
  175. async function (err, chats) {
  176. if (err) return reject(err)
  177. chats = chats || []
  178. // Compute the user's name
  179. for (var i in chats) {
  180. var el = chats[i]
  181. el.x_sender = el.is_from_me ? 'Me' : el.sender_name
  182. if (!el.is_from_me) {
  183. var contact = await backup.getName(el.sender_name)
  184. if (contact) {
  185. el.x_sender = `${contact.name} <${contact.query}>`
  186. }
  187. }
  188. }
  189. resolve(chats)
  190. })
  191. })
  192. }
  193. getMessages (chatId) {
  194. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  195. return this.getMessagesiOS9(chatId)
  196. } else {
  197. return this.getMessagesiOS10iOS11(chatId)
  198. }
  199. }
  200. getConversationsiOS9 () {
  201. var backup = this
  202. return new Promise((resolve, reject) => {
  203. var messagedb = this.getDatabase(databases.SMS)
  204. messagedb.all(`SELECT * FROM chat ORDER BY ROWID ASC`, async function (err, rows) {
  205. if (err) return reject(err)
  206. rows = rows || []
  207. // We need to do some manual parsing of these records.
  208. // The timestamp information is stored in a binary blob named `properties`
  209. // Which is formatted as a binary PLIST.
  210. for (var el of rows) {
  211. if (el.properties) el.properties = bplist.parseBuffer(el.properties)[0]
  212. // Interestingly, some of these do not have dates attached.
  213. if (el.properties) {
  214. el.date = new Date(el.properties.CKChatWatermarkTime * 1000)
  215. } else {
  216. el.date = new Date(0)
  217. }
  218. var contact = await backup.getName(el.chat_identifier)
  219. if (contact) {
  220. el.display_name = `${contact.name} <${contact.query}>`
  221. }
  222. // Format as YY-MM-DD HH:MM:SS
  223. try {
  224. el.XFORMATTEDDATESTRING = el.date.toISOString()
  225. .split('T')
  226. .join(' ')
  227. .split('Z')
  228. .join(' ')
  229. .split('.')[0]
  230. .trim()
  231. } catch (e) {
  232. el.XFORMATTEDDATESTRING = ''
  233. }
  234. }
  235. // Sort by the date.
  236. rows = rows.sort(function (a, b) {
  237. return (a.date.getTime() || 0) - (b.date.getTime() || 0)
  238. })
  239. resolve(rows)
  240. })
  241. })
  242. }
  243. getConversationsiOS10iOS11 () {
  244. return new Promise((resolve, reject) => {
  245. var messagedb = this.getDatabase(databases.SMS)
  246. messagedb.all(`SELECT *, datetime(last_read_message_timestamp / 1000000000 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM chat ORDER BY last_read_message_timestamp ASC`, async function (err, rows) {
  247. if (err) return reject(err)
  248. rows = rows || []
  249. resolve(rows)
  250. })
  251. })
  252. }
  253. getConversations () {
  254. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 14) {
  255. return this.getConversationsiOS9()
  256. } else {
  257. return this.getConversationsiOS10iOS11()
  258. }
  259. }
  260. getFileManifest () {
  261. return new Promise((resolve, reject) => {
  262. var messagedb = this.getDatabase('Manifest.db', true)
  263. messagedb.all('SELECT * from FILES', async function (err, rows) {
  264. if (err) reject(err)
  265. resolve(rows)
  266. })
  267. })
  268. }
  269. getOldNotes () {
  270. return new Promise((resolve, reject) => {
  271. var messagedb = this.getDatabase(databases.Notes)
  272. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZNOTE LEFT JOIN ZNOTEBODY ON ZBODY = ZNOTEBODY.Z_PK`, async function (err, rows) {
  273. if (err) reject(err)
  274. resolve(rows)
  275. })
  276. })
  277. }
  278. getNewNotesiOS9 () {
  279. return new Promise((resolve, reject) => {
  280. var messagedb = this.getDatabase(databases.Notes2)
  281. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
  282. if (err) reject(err)
  283. resolve(rows)
  284. })
  285. })
  286. }
  287. getNewNotesiOS10iOS11 () {
  288. return new Promise((resolve, reject) => {
  289. var messagedb = this.getDatabase(databases.Notes2)
  290. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, datetime(ZCREATIONDATE1 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING1 FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
  291. if (err) reject(err)
  292. resolve(rows)
  293. })
  294. })
  295. }
  296. getNotes () {
  297. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  298. // Legacy iOS 9 support
  299. // May work for earlier but I haven't tested it
  300. return this.getNewNotesiOS9()
  301. } else {
  302. return this.getNewNotesiOS10iOS11()
  303. }
  304. }
  305. getWebHistory () {
  306. return new Promise((resolve, reject) => {
  307. var messagedb = this.getDatabase(databases.WebHistory)
  308. messagedb.all(`SELECT *, datetime(visit_time + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from history_visits LEFT JOIN history_items ON history_items.ROWID = history_visits.history_item`, async function (err, rows) {
  309. if (err) reject(err)
  310. resolve(rows)
  311. })
  312. })
  313. }
  314. getPhotoLocationHistory () {
  315. return new Promise((resolve, reject) => {
  316. var messagedb = this.getDatabase(databases.Photos)
  317. messagedb.all(`SELECT ZDATECREATED, ZLATITUDE, ZLONGITUDE, ZFILENAME, datetime(ZDATECREATED + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZGENERICASSET ORDER BY ZDATECREATED ASC`, async function (err, rows) {
  318. if (err) reject(err)
  319. resolve(rows)
  320. })
  321. })
  322. }
  323. getGeofencesList () {
  324. return new Promise((resolve, reject) => {
  325. var messagedb = this.getDatabase(databases.Locations)
  326. messagedb.all(`SELECT datetime(Timestamp + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, Latitude, Longitude, Distance FROM Fences ORDER BY Timestamp ASC`, async function (err, rows) {
  327. if (err) reject(err)
  328. resolve(rows)
  329. })
  330. })
  331. }
  332. getCallsList () {
  333. return new Promise((resolve, reject) => {
  334. var messagedb = this.getDatabase(databases.Calls2)
  335. messagedb.all(`SELECT *, datetime(ZDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZCALLRECORD ORDER BY ZDATE ASC`, async function (err, rows) {
  336. if (err) reject(err)
  337. resolve(rows)
  338. })
  339. })
  340. }
  341. getVoicemailsList () {
  342. return new Promise((resolve, reject) => {
  343. var messagedb = this.getDatabase(databases.Voicemail)
  344. messagedb.all(`SELECT *, datetime(date, 'unixepoch') AS XFORMATTEDDATESTRING from voicemail ORDER BY date ASC`, async function (err, rows) {
  345. if (err) reject(err)
  346. resolve(rows)
  347. })
  348. })
  349. }
  350. getVoicemailFileList () {
  351. return new Promise((resolve, reject) => {
  352. var messagedb = this.getDatabase('Manifest.db', true)
  353. messagedb.all(`SELECT * from FILES where relativePath like 'Library/Voicemail/%.amr'`, async function (err, rows) {
  354. if (err) reject(err)
  355. resolve(rows)
  356. })
  357. })
  358. }
  359. getWifiList () {
  360. return new Promise((resolve, reject) => {
  361. var filename = this.getFileName(databases.WiFi)
  362. try {
  363. let wifiList = bplist.parseBuffer(fs.readFileSync(filename))[0];
  364. wifiList['List of known networks'] = wifiList['List of known networks']
  365. .map(el => {
  366. if (el.BSSID)
  367. el.BSSID = mac_address_parse.pad_zeros(el.BSSID) + ''
  368. return el;
  369. });
  370. resolve(wifiList);
  371. } catch (e) {
  372. reject(e)
  373. }
  374. })
  375. }
  376. }
  377. module.exports.availableBackups = function () {
  378. const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
  379. return new Promise((resolve, reject) => {
  380. resolve(fs.readdirSync(base, { encoding: 'utf8' })
  381. .map(file => iPhoneBackup.fromID(file)))
  382. })
  383. }
  384. module.exports.iPhoneBackup = iPhoneBackup