iphone_backup.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. const log = require('./log')
  2. const path = require('path')
  3. const sqlite3 = require('sqlite3')
  4. const bplist = require('bplist-parser')
  5. const fs = require('fs')
  6. const plist = require('plist')
  7. // Cookie Parser
  8. const cookieParser = require('./cookies.js')
  9. // Normalize mac addresses in wifi output
  10. const macParse = require('./mac_address_parse')
  11. // Derive filenames based on domain + file path
  12. const fileHash = require('./backup_filehash')
  13. // Manifest.mbdb parser
  14. const manifestMBDBParse = require('./manifest_mbdb_parse')
  15. const databases = {
  16. SMS: fileHash('Library/SMS/sms.db'),
  17. Contacts: fileHash('Library/AddressBook/AddressBook.sqlitedb'),
  18. Calendar: fileHash('Library/Calendar/Calendar.sqlitedb'),
  19. Reminders: fileHash('Library/Calendar/Calendar.sqlitedb'),
  20. Notes: fileHash('Library/Notes/notes.sqlite'),
  21. Notes2: fileHash('NoteStore.sqlite', 'AppDomainGroup-group.com.apple.notes'),
  22. AddressBook: fileHash('Library/AddressBook/AddressBook.sqlitedb'),
  23. 'Cookies.binarycookies': '69b1865768101bacde5b77ccc44445cea9ce1261',
  24. Calls: '2b2b0084a1bc3a5ac8c27afdf14afb42c61a19ca',
  25. Calls2: fileHash('Library/CallHistoryDB/CallHistory.storedata'),
  26. Locations: fileHash('Library/Caches/locationd/consolidated.db', 'RootDomain'),
  27. WebHistory: fileHash('Library/Safari/History.db', 'AppDomain-com.apple.mobilesafari'),
  28. Photos: fileHash('Media/PhotoData/Photos.sqlite', 'CameraRollDomain'),
  29. WiFi: fileHash('SystemConfiguration/com.apple.wifi.plist', 'SystemPreferencesDomain'),
  30. Voicemail: fileHash('Library/Voicemail/voicemail.db')
  31. }
  32. var cache = {}
  33. class IPhoneBackup {
  34. constructor (id, status, info, manifest, base) {
  35. this.id = id
  36. this.status = status
  37. this.info = info
  38. this.manifest = manifest
  39. this.base = base
  40. }
  41. // Open a backup with a specified ID
  42. // base is optional and will be computed if not used.
  43. static fromID (id, base) {
  44. // Get the path of the folder.
  45. if (base) {
  46. base = path.join(base, id)
  47. } else {
  48. base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', id)
  49. }
  50. // Parse manifest bplist files
  51. try {
  52. log.verbose('parsing status', base)
  53. var status = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Status.plist')))[0]
  54. } catch (e) {
  55. log.error('Cannot open Status.plist', e)
  56. }
  57. try {
  58. log.verbose('parsing manifest', base)
  59. var manifest = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Manifest.plist')))[0]
  60. } catch (e) {
  61. log.error('Cannot open Manifest.plist', e)
  62. }
  63. try {
  64. log.verbose('parsing status', base)
  65. var info = plist.parse(fs.readFileSync(path.join(base, 'Info.plist'), 'utf8'))
  66. } catch (e) {
  67. log.error('Cannot open Info.plist', e)
  68. }
  69. return new IPhoneBackup(id, status, info, manifest, base)
  70. }
  71. get iOSVersion () {
  72. return this.manifest.Lockdown.ProductVersion
  73. }
  74. getFileName (fileID, isAbsoulte) {
  75. isAbsoulte = isAbsoulte || false
  76. // const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', this.id)
  77. // Return v2 filename
  78. if (this.status.Version < 3 || isAbsoulte) {
  79. return path.join(this.base, fileID)
  80. } else {
  81. // v3 has folders
  82. return path.join(this.base, fileID.substr(0, 2), fileID)
  83. }
  84. }
  85. openDatabase (fileID, isAbsoulte) {
  86. return new Promise((resolve, reject) => {
  87. isAbsoulte = isAbsoulte || false
  88. // Get the backup folder
  89. // Return v2 filename
  90. if (this.status.Version < 3 || isAbsoulte) {
  91. let db = new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY, (err) => {
  92. if (err) {
  93. return reject(err)
  94. }
  95. resolve(db)
  96. })
  97. } else {
  98. // v3 has folders
  99. let db = new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY, (err) => {
  100. if (err) {
  101. return reject(err)
  102. }
  103. resolve(db)
  104. })
  105. }
  106. })
  107. }
  108. /// This is deprecated. Use openDatabase Instead.
  109. getDatabase (fileID, isAbsoulte) {
  110. isAbsoulte = isAbsoulte || false
  111. // Get the backup folder
  112. // Return v2 filename
  113. if (this.status.Version < 3 || isAbsoulte) {
  114. return new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY, (err) => {
  115. if (err) {
  116. log.error('PANIC::', err)
  117. process.exit(1)
  118. }
  119. })
  120. } else {
  121. // v3 has folders
  122. return new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY, (err) => {
  123. if (err) {
  124. log.error('PANIC::', err)
  125. process.exit(1)
  126. }
  127. })
  128. }
  129. }
  130. queryDatabase (databaseID, sql) {
  131. return new Promise((resolve, reject) => {
  132. var messagedb = this.getDatabase(databaseID)
  133. messagedb.all(sql, async function (err, rows) {
  134. if (err) reject(err)
  135. resolve(rows)
  136. })
  137. })
  138. }
  139. getName (messageDest) {
  140. return new Promise((resolve, reject) => {
  141. if (messageDest.indexOf('@') === -1) {
  142. messageDest = messageDest.replace(/[\s+\-()]*/g, '')
  143. if (messageDest.length === 11 && messageDest[0] === '1') {
  144. messageDest = messageDest.substring(1)
  145. }
  146. }
  147. if (cache[messageDest] !== undefined) {
  148. return resolve(cache[messageDest])
  149. }
  150. var contactdb = this.getDatabase(databases.Contacts)
  151. contactdb.get(`SELECT
  152. c0First as first,
  153. c1Last as last,
  154. c2Middle as middle,
  155. c15Phone as phones
  156. from ABPersonFullTextSearch_content WHERE c15Phone like '%${messageDest}%'`,
  157. (err, row) => {
  158. if (err) return resolve()
  159. if (!row) return resolve()
  160. var result = {
  161. name: [row.first, row.middle, row.last].filter(el => el != null).join(' '),
  162. phones: row.phones.split(' '),
  163. query: messageDest
  164. }
  165. if (row) cache[messageDest] = result
  166. resolve(result)
  167. })
  168. })
  169. }
  170. getMessagesiOS9 (chatId) {
  171. var backup = this
  172. return new Promise((resolve, reject) => {
  173. var messagedb = this.getDatabase(databases.SMS)
  174. messagedb.all(`
  175. SELECT
  176. message.*,
  177. handle.id as sender_name,
  178. datetime(date + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
  179. FROM chat_message_join
  180. INNER JOIN message
  181. ON message.rowid = chat_message_join.message_id
  182. INNER JOIN handle
  183. ON handle.rowid = message.handle_id
  184. WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
  185. async function (err, chats) {
  186. if (err) return reject(err)
  187. chats = chats || []
  188. // Compute the user's name
  189. for (var i in chats) {
  190. var el = chats[i]
  191. el.x_sender = el.is_from_me ? 'Me' : el.sender_name
  192. if (!el.is_from_me) {
  193. var contact = await backup.getName(el.sender_name)
  194. if (contact) {
  195. el.x_sender = `${contact.name} <${contact.query}>`
  196. }
  197. }
  198. }
  199. resolve(chats)
  200. })
  201. })
  202. }
  203. getMessagesiOS10iOS11 (chatId) {
  204. var backup = this
  205. return new Promise((resolve, reject) => {
  206. var messagedb = this.getDatabase(databases.SMS)
  207. messagedb.all(`
  208. SELECT
  209. message.*,
  210. handle.id as sender_name,
  211. datetime(date / 1000000000 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
  212. FROM chat_message_join
  213. INNER JOIN message
  214. ON message.rowid = chat_message_join.message_id
  215. INNER JOIN handle
  216. ON handle.rowid = message.handle_id
  217. WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
  218. async function (err, chats) {
  219. if (err) return reject(err)
  220. chats = chats || []
  221. // Compute the user's name
  222. for (var i in chats) {
  223. var el = chats[i]
  224. el.x_sender = el.is_from_me ? 'Me' : el.sender_name
  225. if (!el.is_from_me) {
  226. var contact = await backup.getName(el.sender_name)
  227. if (contact) {
  228. el.x_sender = `${contact.name} <${contact.query}>`
  229. }
  230. }
  231. }
  232. resolve(chats)
  233. })
  234. })
  235. }
  236. getMessages (chatId) {
  237. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  238. return this.getMessagesiOS9(chatId)
  239. } else {
  240. return this.getMessagesiOS10iOS11(chatId)
  241. }
  242. }
  243. getConversationsiOS9 () {
  244. var backup = this
  245. return new Promise((resolve, reject) => {
  246. var messagedb = this.getDatabase(databases.SMS)
  247. messagedb.all(`SELECT * FROM chat ORDER BY ROWID ASC`, async function (err, rows) {
  248. if (err) return reject(err)
  249. rows = rows || []
  250. // We need to do some manual parsing of these records.
  251. // The timestamp information is stored in a binary blob named `properties`
  252. // Which is formatted as a binary PLIST.
  253. for (var el of rows) {
  254. if (el.properties) el.properties = bplist.parseBuffer(el.properties)[0]
  255. // Interestingly, some of these do not have dates attached.
  256. if (el.properties) {
  257. el.date = new Date(el.properties.CKChatWatermarkTime * 1000)
  258. } else {
  259. el.date = new Date(0)
  260. }
  261. var contact = await backup.getName(el.chat_identifier)
  262. if (contact) {
  263. el.display_name = `${contact.name} <${contact.query}>`
  264. }
  265. // Format as YY-MM-DD HH:MM:SS
  266. try {
  267. el.XFORMATTEDDATESTRING = el.date.toISOString()
  268. .split('T')
  269. .join(' ')
  270. .split('Z')
  271. .join(' ')
  272. .split('.')[0]
  273. .trim()
  274. } catch (e) {
  275. el.XFORMATTEDDATESTRING = ''
  276. }
  277. }
  278. // Sort by the date.
  279. rows = rows.sort(function (a, b) {
  280. return (a.date.getTime() || 0) - (b.date.getTime() || 0)
  281. })
  282. resolve(rows)
  283. })
  284. })
  285. }
  286. getConversationsiOS10iOS11 () {
  287. return new Promise((resolve, reject) => {
  288. var messagedb = this.getDatabase(databases.SMS)
  289. 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) {
  290. if (err) return reject(err)
  291. rows = rows || []
  292. resolve(rows)
  293. })
  294. })
  295. }
  296. getConversations () {
  297. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 14) {
  298. return this.getConversationsiOS9()
  299. } else {
  300. return this.getConversationsiOS10iOS11()
  301. }
  302. }
  303. getFileManifest () {
  304. return new Promise((resolve, reject) => {
  305. var messagedb = this.getDatabase('Manifest.db', true)
  306. messagedb.all('SELECT * from FILES', async function (err, rows) {
  307. if (err) reject(err)
  308. resolve(rows)
  309. })
  310. })
  311. }
  312. getOldFileManifest () {
  313. return new Promise((resolve, reject) => {
  314. let mbdbPath = this.getFileName('Manifest.mbdb', true)
  315. manifestMBDBParse.process(mbdbPath, resolve, reject)
  316. })
  317. }
  318. getOldNotes () {
  319. return new Promise((resolve, reject) => {
  320. var messagedb = this.getDatabase(databases.Notes)
  321. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZNOTE LEFT JOIN ZNOTEBODY ON ZBODY = ZNOTEBODY.Z_PK`, async function (err, rows) {
  322. if (err) reject(err)
  323. resolve(rows)
  324. })
  325. })
  326. }
  327. getNewNotesiOS9 () {
  328. return new Promise((resolve, reject) => {
  329. var messagedb = this.getDatabase(databases.Notes2)
  330. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
  331. if (err) reject(err)
  332. resolve(rows)
  333. })
  334. })
  335. }
  336. getNewNotesiOS10iOS11 () {
  337. return new Promise((resolve, reject) => {
  338. var messagedb = this.getDatabase(databases.Notes2)
  339. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, datetime(ZCREATIONDATE1 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING1 FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
  340. if (err) reject(err)
  341. resolve(rows)
  342. })
  343. })
  344. }
  345. getNotes () {
  346. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  347. // Legacy iOS 9 support
  348. // May work for earlier but I haven't tested it
  349. return this.getNewNotesiOS9()
  350. } else {
  351. return this.getNewNotesiOS10iOS11()
  352. }
  353. }
  354. getWebHistory () {
  355. return new Promise((resolve, reject) => {
  356. var messagedb = this.getDatabase(databases.WebHistory)
  357. 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) {
  358. if (err) reject(err)
  359. resolve(rows)
  360. })
  361. })
  362. }
  363. getPhotoLocationHistory () {
  364. return new Promise((resolve, reject) => {
  365. var messagedb = this.getDatabase(databases.Photos)
  366. messagedb.all(`SELECT ZDATECREATED, ZLATITUDE, ZLONGITUDE, ZFILENAME, datetime(ZDATECREATED + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZGENERICASSET ORDER BY ZDATECREATED ASC`, async function (err, rows) {
  367. if (err) reject(err)
  368. resolve(rows)
  369. })
  370. })
  371. }
  372. getGeofencesList () {
  373. return new Promise((resolve, reject) => {
  374. var messagedb = this.getDatabase(databases.Locations)
  375. messagedb.all(`SELECT datetime(Timestamp + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, Latitude, Longitude, Distance FROM Fences ORDER BY Timestamp ASC`, async function (err, rows) {
  376. if (err) reject(err)
  377. resolve(rows)
  378. })
  379. })
  380. }
  381. getCallsStatisticsiOS7 () {
  382. return new Promise((resolve, reject) => {
  383. var messagedb = this.getDatabase(databases.Calls)
  384. messagedb.all(`SELECT * from _SqliteDatabaseProperties`, async function (err, rows) {
  385. if (err) reject(err)
  386. resolve(rows)
  387. })
  388. })
  389. }
  390. getCallsStatistics () {
  391. return new Promise((resolve, reject) => {
  392. var messagedb = this.getDatabase(databases.Calls2)
  393. messagedb.all(`SELECT * from ZCALLDBPROPERTIES`, async function (err, rows) {
  394. if (err) reject(err)
  395. resolve(rows)
  396. })
  397. })
  398. }
  399. getCallsList () {
  400. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  401. // Legacy iOS 9 support
  402. // May work for earlier but I haven't tested it
  403. return this.getCallsListiOS7()
  404. } else {
  405. return this.getCallsListLater()
  406. }
  407. }
  408. getCallsListiOS7 () {
  409. return new Promise((resolve, reject) => {
  410. var messagedb = this.getDatabase(databases.Calls)
  411. messagedb.all(`SELECT
  412. ROWID as Z_PK,
  413. datetime(date, 'unixepoch') AS XFORMATTEDDATESTRING,
  414. answered as ZANSWERED,
  415. duration as ZDURATION,
  416. address as ZADDRESS,
  417. country_code as ZISO_COUNTRY_CODE,
  418. country_code as ZISO_COUNTRY_CODE,
  419. * from call ORDER BY date ASC`, async function (err, rows) {
  420. if (err) reject(err)
  421. resolve(rows)
  422. })
  423. })
  424. }
  425. getCallsListLater () {
  426. return new Promise((resolve, reject) => {
  427. var messagedb = this.getDatabase(databases.Calls2)
  428. messagedb.all(`SELECT *, datetime(ZDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZCALLRECORD ORDER BY ZDATE ASC`, async function (err, rows) {
  429. if (err) reject(err)
  430. resolve(rows)
  431. })
  432. })
  433. }
  434. getVoicemailsList () {
  435. return new Promise((resolve, reject) => {
  436. var messagedb = this.getDatabase(databases.Voicemail)
  437. messagedb.all(`SELECT *, datetime(date, 'unixepoch') AS XFORMATTEDDATESTRING from voicemail ORDER BY date ASC`, async function (err, rows) {
  438. if (err) reject(err)
  439. resolve(rows)
  440. })
  441. })
  442. }
  443. getVoicemailFileList () {
  444. return new Promise((resolve, reject) => {
  445. this.openDatabase('Manifest.db', true)
  446. .then(manifestdb => {
  447. manifestdb.all(`SELECT * from FILES where relativePath like 'Library/Voicemail/%.amr'`, async (err, rows) => {
  448. if (err) reject(err)
  449. resolve(rows)
  450. })
  451. })
  452. .catch(reject)
  453. })
  454. }
  455. getWifiList () {
  456. return new Promise((resolve, reject) => {
  457. var filename = this.getFileName(databases.WiFi)
  458. try {
  459. let wifiList = bplist.parseBuffer(fs.readFileSync(filename))[0]
  460. wifiList['List of known networks'] = wifiList['List of known networks']
  461. .map(el => {
  462. if (el.BSSID) {
  463. el.BSSID = macParse.pad_zeros(el.BSSID) + ''
  464. }
  465. return el
  466. })
  467. resolve(wifiList)
  468. } catch (e) {
  469. reject(e)
  470. }
  471. })
  472. }
  473. getCookies () {
  474. return new Promise(async (resolve, reject) => {
  475. this.openDatabase('Manifest.db', true)
  476. .then(manifestdb => {
  477. manifestdb.all(`SELECT fileID,domain,relativePath from FILES where relativePath like 'Library/Cookies/Cookies.binarycookies'`, async (err, rows) => {
  478. if (err) return reject(err)
  479. let cookiesResult = []
  480. const iterateElements = (elements, index, callback) => {
  481. if (index === elements.length) { return callback() }
  482. // do parse call with element
  483. var ele = elements[index]
  484. cookieParser.parse(this.getFileName(ele.fileID))
  485. .then(cookies => {
  486. // Map to include domain
  487. let formatted = cookies.map(el => { return { domain: ele.domain, cookie: el } })
  488. // Append result
  489. cookiesResult = [...cookiesResult, ...formatted]
  490. // Next file.
  491. iterateElements(elements, index + 1, callback)
  492. })
  493. }
  494. iterateElements(rows, 0, () => {
  495. resolve(cookiesResult)
  496. })
  497. })
  498. })
  499. .catch(reject)
  500. })
  501. }
  502. getAddressBook () {
  503. return new Promise((resolve, reject) => {
  504. var addressbookdb = this.getDatabase(databases.AddressBook)
  505. try {
  506. const query = `
  507. select ABPerson.ROWID
  508. , ABPerson.first
  509. , ABPerson.middle
  510. , ABPerson.last
  511. , ABPerson.Organization as organization
  512. , ABPerson.Department as department
  513. , ABPerson.Birthday as birthday
  514. , ABPerson.JobTitle as jobtitle
  515. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Work>!$_')) as phone_work
  516. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Mobile>!$_')) as phone_mobile
  517. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Home>!$_')) as phone_home
  518. , (select value from ABMultiValue where property = 4 and record_id = ABPerson.ROWID) as email
  519. , (select value from ABMultiValueEntry where parent_id in (select ROWID from ABMultiValue where record_id = ABPerson.ROWID) and key = (select ROWID from ABMultiValueEntryKey where lower(value) = 'street')) as address
  520. , (select value from ABMultiValueEntry where parent_id in (select ROWID from ABMultiValue where record_id = ABPerson.ROWID) and key = (select ROWID from ABMultiValueEntryKey where lower(value) = 'city')) as city
  521. from ABPerson
  522. order by ABPerson.ROWID
  523. `
  524. addressbookdb.all(query, async function (err, rows) {
  525. if (err) reject(err)
  526. resolve(rows)
  527. })
  528. } catch (e) {
  529. reject(e)
  530. }
  531. })
  532. }
  533. }
  534. module.exports.availableBackups = function () {
  535. const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
  536. return new Promise((resolve, reject) => {
  537. resolve(fs.readdirSync(base, {
  538. encoding: 'utf8'
  539. })
  540. .map(file => IPhoneBackup.fromID(file)))
  541. })
  542. }
  543. module.exports.iPhoneBackup = IPhoneBackup
  544. module.exports.IPhoneBackup = IPhoneBackup