iphone_backup.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. const log = require('./log')
  2. const path = require('path')
  3. const sqlite3 = require('sqlite3')
  4. const fs = require('fs')
  5. const plist = require('./plist')
  6. // Cookie Parser
  7. const cookieParser = require('./cookies.js')
  8. // Normalize mac addresses in wifi output
  9. const macParse = require('./mac_address_parse')
  10. // Derive filenames based on domain + file path
  11. const fileHash = require('./backup_filehash')
  12. // Manifest.mbdb parser
  13. const manifestMBDBParse = require('./manifest_mbdb_parse')
  14. // Pushstore plist parser
  15. const pushstoreParse = require('./pushstore_parse')
  16. const databases = {
  17. SMS: fileHash('Library/SMS/sms.db'),
  18. Contacts: fileHash('Library/AddressBook/AddressBook.sqlitedb'),
  19. Calendar: fileHash('Library/Calendar/Calendar.sqlitedb'),
  20. Reminders: fileHash('Library/Calendar/Calendar.sqlitedb'),
  21. Notes: fileHash('Library/Notes/notes.sqlite'),
  22. Notes2: fileHash('NoteStore.sqlite', 'AppDomainGroup-group.com.apple.notes'),
  23. AddressBook: fileHash('Library/AddressBook/AddressBook.sqlitedb'),
  24. AddressBookImages: fileHash('Library/AddressBook/AddressBookImages.sqlitedb'),
  25. 'Cookies.binarycookies': '69b1865768101bacde5b77ccc44445cea9ce1261',
  26. Calls: '2b2b0084a1bc3a5ac8c27afdf14afb42c61a19ca',
  27. Calls2: fileHash('Library/CallHistoryDB/CallHistory.storedata'),
  28. Locations: fileHash('Library/Caches/locationd/consolidated.db', 'RootDomain'),
  29. WebHistory: fileHash('Library/Safari/History.db', 'AppDomain-com.apple.mobilesafari'),
  30. Photos: fileHash('Media/PhotoData/Photos.sqlite', 'CameraRollDomain'),
  31. WiFi: fileHash('SystemConfiguration/com.apple.wifi.plist', 'SystemPreferencesDomain'),
  32. Voicemail: fileHash('Library/Voicemail/voicemail.db'),
  33. SafariBookmarks: fileHash('Library/Safari/Bookmarks.db')
  34. }
  35. var cache = {}
  36. class IPhoneBackup {
  37. constructor (id, status, info, manifest, base) {
  38. log.warning('v3 reporting API is deprecated, this report may need to be updated to ensure stability with all iOS versions')
  39. log.warning('https://github.com/richinfante/iphonebackuptools/wiki/V4-API-Migration-Notes')
  40. this.id = id
  41. this.status = status
  42. this.info = info
  43. this.manifest = manifest
  44. this.base = base
  45. }
  46. // Open a backup with a specified ID
  47. // base is optional and will be computed if not used.
  48. static fromID (id, base) {
  49. id = id || ''
  50. // Get the path of the folder.
  51. if (base) {
  52. base = path.join(base, id)
  53. } else {
  54. base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', id)
  55. }
  56. // Parse manifest plist files
  57. try {
  58. log.verbose('parsing status', base)
  59. var status = plist.parseFile(path.join(base, 'Status.plist'))
  60. } catch (e) {
  61. log.error('Cannot open Status.plist', e)
  62. }
  63. try {
  64. log.verbose('parsing manifest', base)
  65. var manifest = plist.parseFile(path.join(base, 'Manifest.plist'))
  66. } catch (e) {
  67. log.error('Cannot open Manifest.plist', e)
  68. }
  69. try {
  70. log.verbose('parsing status', base)
  71. var info = plist.parseFile(path.join(base, 'Info.plist'))
  72. } catch (e) {
  73. log.error('Cannot open Info.plist', e)
  74. }
  75. return new IPhoneBackup(id, status, info, manifest, base)
  76. }
  77. get iOSVersion () {
  78. return this.manifest.Lockdown.ProductVersion
  79. }
  80. getFileName (fileID, isAbsoulte) {
  81. isAbsoulte = isAbsoulte || false
  82. // const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', this.id)
  83. // Return v2 filename
  84. if (this.status.Version < 3 || isAbsoulte) {
  85. return path.join(this.base, fileID)
  86. } else {
  87. // v3 has folders
  88. return path.join(this.base, fileID.substr(0, 2), fileID)
  89. }
  90. }
  91. openDatabase (fileID, isAbsoulte) {
  92. return new Promise((resolve, reject) => {
  93. isAbsoulte = isAbsoulte || false
  94. // Get the backup folder
  95. // Return v2 filename
  96. if (this.status.Version < 3 || isAbsoulte) {
  97. let db = new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY, (err) => {
  98. if (err) {
  99. return reject(err)
  100. }
  101. resolve(db)
  102. })
  103. } else {
  104. // v3 has folders
  105. let db = new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY, (err) => {
  106. if (err) {
  107. return reject(err)
  108. }
  109. resolve(db)
  110. })
  111. }
  112. })
  113. }
  114. /// This is deprecated. Use openDatabase Instead.
  115. getDatabase (fileID, isAbsoulte) {
  116. isAbsoulte = isAbsoulte || false
  117. // Get the backup folder
  118. // Return v2 filename
  119. if (this.status.Version < 3 || isAbsoulte) {
  120. return new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY, (err) => {
  121. if (err) {
  122. log.error('PANIC::', err)
  123. process.exit(1)
  124. }
  125. })
  126. } else {
  127. // v3 has folders
  128. return new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY, (err) => {
  129. if (err) {
  130. log.error('PANIC::', err)
  131. process.exit(1)
  132. }
  133. })
  134. }
  135. }
  136. queryDatabase (databaseID, sql) {
  137. return new Promise((resolve, reject) => {
  138. var messagedb = this.getDatabase(databaseID)
  139. messagedb.all(sql, async function (err, rows) {
  140. if (err) reject(err)
  141. resolve(rows)
  142. })
  143. })
  144. }
  145. getName (messageDest) {
  146. return new Promise((resolve, reject) => {
  147. if (messageDest.indexOf('@') === -1) {
  148. messageDest = messageDest.replace(/[\s+\-()]*/g, '')
  149. if (messageDest.length === 11 && messageDest[0] === '1') {
  150. messageDest = messageDest.substring(1)
  151. }
  152. }
  153. if (cache[messageDest] !== undefined) {
  154. return resolve(cache[messageDest])
  155. }
  156. var contactdb = this.getDatabase(databases.Contacts)
  157. contactdb.get(`SELECT
  158. c0First as first,
  159. c1Last as last,
  160. c2Middle as middle,
  161. c15Phone as phones
  162. from ABPersonFullTextSearch_content WHERE c15Phone like '%${messageDest}%'`,
  163. (err, row) => {
  164. if (err) return resolve()
  165. if (!row) return resolve()
  166. var result = {
  167. name: [row.first, row.middle, row.last].filter(el => el != null).join(' '),
  168. phones: row.phones.split(' '),
  169. query: messageDest
  170. }
  171. if (row) cache[messageDest] = result
  172. resolve(result)
  173. })
  174. })
  175. }
  176. getMessagesiOS9 (chatId) {
  177. var backup = this
  178. return new Promise((resolve, reject) => {
  179. var messagedb = this.getDatabase(databases.SMS)
  180. messagedb.all(`
  181. SELECT
  182. message.*,
  183. handle.id as sender_name,
  184. datetime(date + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
  185. FROM chat_message_join
  186. INNER JOIN message
  187. ON message.rowid = chat_message_join.message_id
  188. INNER JOIN handle
  189. ON handle.rowid = message.handle_id
  190. WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
  191. async function (err, chats) {
  192. if (err) return reject(err)
  193. chats = chats || []
  194. // Compute the user's name
  195. for (var i in chats) {
  196. var el = chats[i]
  197. el.x_sender = el.is_from_me ? 'Me' : el.sender_name
  198. if (!el.is_from_me) {
  199. var contact = await backup.getName(el.sender_name)
  200. if (contact) {
  201. el.x_sender = `${contact.name} <${contact.query}>`
  202. }
  203. }
  204. }
  205. resolve(chats)
  206. })
  207. })
  208. }
  209. getMessagesiOS10iOS11 (chatId) {
  210. var backup = this
  211. return new Promise((resolve, reject) => {
  212. var messagedb = this.getDatabase(databases.SMS)
  213. messagedb.all(`
  214. SELECT
  215. message.*,
  216. handle.id as sender_name,
  217. datetime(date / 1000000000 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
  218. FROM chat_message_join
  219. INNER JOIN message
  220. ON message.rowid = chat_message_join.message_id
  221. INNER JOIN handle
  222. ON handle.rowid = message.handle_id
  223. WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
  224. async function (err, chats) {
  225. if (err) return reject(err)
  226. chats = chats || []
  227. // Compute the user's name
  228. for (var i in chats) {
  229. var el = chats[i]
  230. el.x_sender = el.is_from_me ? 'Me' : el.sender_name
  231. if (!el.is_from_me) {
  232. var contact = await backup.getName(el.sender_name)
  233. if (contact) {
  234. el.x_sender = `${contact.name} <${contact.query}>`
  235. }
  236. }
  237. }
  238. resolve(chats)
  239. })
  240. })
  241. }
  242. getMessages (chatId) {
  243. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  244. return this.getMessagesiOS9(chatId)
  245. } else {
  246. return this.getMessagesiOS10iOS11(chatId)
  247. }
  248. }
  249. getMessageAttachments (messageId) {
  250. var backup = this
  251. return new Promise((resolve, reject) => {
  252. const messagedb = this.getDatabase(databases.SMS)
  253. const query = `
  254. SELECT
  255. attachment.*
  256. FROM message_attachment_join
  257. INNER JOIN attachment
  258. ON attachment.ROWID = message_attachment_join.attachment_id
  259. WHERE message_attachment_join.message_id = ${parseInt(messageId)}
  260. `
  261. messagedb.all(query,
  262. async function (err, attachments) {
  263. if (err) return reject(err)
  264. attachments = attachments || []
  265. resolve(attachments)
  266. })
  267. })
  268. }
  269. getConversationsiOS9 () {
  270. var backup = this
  271. return new Promise((resolve, reject) => {
  272. var messagedb = this.getDatabase(databases.SMS)
  273. messagedb.all(`SELECT * FROM chat ORDER BY ROWID ASC`, async function (err, rows) {
  274. if (err) return reject(err)
  275. rows = rows || []
  276. // We need to do some manual parsing of these records.
  277. // The timestamp information is stored in a binary blob named `properties`
  278. // Which is formatted as a binary PLIST.
  279. for (var el of rows) {
  280. if (el.properties) el.properties = plist.parseBuffer(el.properties)
  281. // Interestingly, some of these do not have dates attached.
  282. if (el.properties) {
  283. el.date = new Date(el.properties.CKChatWatermarkTime * 1000)
  284. } else {
  285. el.date = new Date(0)
  286. }
  287. var contact = await backup.getName(el.chat_identifier)
  288. if (contact) {
  289. el.display_name = `${contact.name} <${contact.query}>`
  290. }
  291. // Format as YY-MM-DD HH:MM:SS
  292. try {
  293. el.XFORMATTEDDATESTRING = el.date.toISOString()
  294. .split('T')
  295. .join(' ')
  296. .split('Z')
  297. .join(' ')
  298. .split('.')[0]
  299. .trim()
  300. } catch (e) {
  301. el.XFORMATTEDDATESTRING = ''
  302. }
  303. }
  304. // Sort by the date.
  305. rows = rows.sort(function (a, b) {
  306. return (a.date.getTime() || 0) - (b.date.getTime() || 0)
  307. })
  308. resolve(rows)
  309. })
  310. })
  311. }
  312. getConversationsiOS10iOS11 () {
  313. return new Promise((resolve, reject) => {
  314. var messagedb = this.getDatabase(databases.SMS)
  315. 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) {
  316. if (err) return reject(err)
  317. rows = rows || []
  318. resolve(rows)
  319. })
  320. })
  321. }
  322. getConversations () {
  323. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 14) {
  324. return this.getConversationsiOS9()
  325. } else {
  326. return this.getConversationsiOS10iOS11()
  327. }
  328. }
  329. getFileManifest () {
  330. return new Promise((resolve, reject) => {
  331. var messagedb = this.getDatabase('Manifest.db', true)
  332. messagedb.all('SELECT * from FILES', async function (err, rows) {
  333. if (err) reject(err)
  334. resolve(rows)
  335. })
  336. })
  337. }
  338. getOldFileManifest () {
  339. return new Promise((resolve, reject) => {
  340. let mbdbPath = this.getFileName('Manifest.mbdb', true)
  341. manifestMBDBParse.process(mbdbPath, resolve, reject)
  342. })
  343. }
  344. getOldNotes () {
  345. return new Promise((resolve, reject) => {
  346. var messagedb = this.getDatabase(databases.Notes)
  347. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZNOTE LEFT JOIN ZNOTEBODY ON ZBODY = ZNOTEBODY.Z_PK`, async function (err, rows) {
  348. if (err) reject(err)
  349. resolve(rows)
  350. })
  351. })
  352. }
  353. getNewNotesiOS9 () {
  354. return new Promise((resolve, reject) => {
  355. var messagedb = this.getDatabase(databases.Notes2)
  356. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
  357. if (err) reject(err)
  358. resolve(rows)
  359. })
  360. })
  361. }
  362. getNewNotesiOS10iOS11 () {
  363. return new Promise((resolve, reject) => {
  364. var messagedb = this.getDatabase(databases.Notes2)
  365. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, datetime(ZCREATIONDATE1 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING1 FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
  366. if (err) reject(err)
  367. resolve(rows)
  368. })
  369. })
  370. }
  371. getNotes () {
  372. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  373. // Legacy iOS 9 support
  374. // May work for earlier but I haven't tested it
  375. return this.getNewNotesiOS9()
  376. } else {
  377. return this.getNewNotesiOS10iOS11()
  378. }
  379. }
  380. getWebHistory () {
  381. return new Promise((resolve, reject) => {
  382. var messagedb = this.getDatabase(databases.WebHistory)
  383. 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) {
  384. if (err) reject(err)
  385. resolve(rows)
  386. })
  387. })
  388. }
  389. getPhotoLocationHistory () {
  390. return new Promise((resolve, reject) => {
  391. var messagedb = this.getDatabase(databases.Photos)
  392. messagedb.all(`SELECT ZDATECREATED, ZLATITUDE, ZLONGITUDE, ZFILENAME, datetime(ZDATECREATED + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZGENERICASSET ORDER BY ZDATECREATED ASC`, async function (err, rows) {
  393. if (err) reject(err)
  394. resolve(rows)
  395. })
  396. })
  397. }
  398. getGeofencesList () {
  399. return new Promise((resolve, reject) => {
  400. var messagedb = this.getDatabase(databases.Locations)
  401. messagedb.all(`SELECT datetime(Timestamp + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, Latitude, Longitude, Distance FROM Fences ORDER BY Timestamp ASC`, async function (err, rows) {
  402. if (err) reject(err)
  403. resolve(rows)
  404. })
  405. })
  406. }
  407. getCallsStatisticsiOS7 () {
  408. return new Promise((resolve, reject) => {
  409. var messagedb = this.getDatabase(databases.Calls)
  410. messagedb.all(`SELECT * from _SqliteDatabaseProperties`, async function (err, rows) {
  411. if (err) reject(err)
  412. resolve(rows)
  413. })
  414. })
  415. }
  416. getCallsStatistics () {
  417. return new Promise((resolve, reject) => {
  418. var messagedb = this.getDatabase(databases.Calls2)
  419. messagedb.all(`SELECT * from ZCALLDBPROPERTIES`, async function (err, rows) {
  420. if (err) reject(err)
  421. resolve(rows)
  422. })
  423. })
  424. }
  425. getCallsList () {
  426. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  427. // Legacy iOS 9 support
  428. // May work for earlier but I haven't tested it
  429. return this.getCallsListiOS7()
  430. } else {
  431. return this.getCallsListLater()
  432. }
  433. }
  434. getCallsListiOS7 () {
  435. return new Promise((resolve, reject) => {
  436. var messagedb = this.getDatabase(databases.Calls)
  437. messagedb.all(`SELECT
  438. ROWID as Z_PK,
  439. datetime(date, 'unixepoch') AS XFORMATTEDDATESTRING,
  440. answered as ZANSWERED,
  441. duration as ZDURATION,
  442. address as ZADDRESS,
  443. country_code as ZISO_COUNTRY_CODE,
  444. country_code as ZISO_COUNTRY_CODE,
  445. * from call ORDER BY date ASC`, async function (err, rows) {
  446. if (err) reject(err)
  447. resolve(rows)
  448. })
  449. })
  450. }
  451. getCallsListLater () {
  452. return new Promise((resolve, reject) => {
  453. var messagedb = this.getDatabase(databases.Calls2)
  454. messagedb.all(`SELECT *, datetime(ZDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZCALLRECORD ORDER BY ZDATE ASC`, async function (err, rows) {
  455. if (err) reject(err)
  456. resolve(rows)
  457. })
  458. })
  459. }
  460. getVoicemailsList () {
  461. return new Promise((resolve, reject) => {
  462. var messagedb = this.getDatabase(databases.Voicemail)
  463. messagedb.all(`SELECT *, datetime(date, 'unixepoch') AS XFORMATTEDDATESTRING from voicemail ORDER BY date ASC`, async function (err, rows) {
  464. if (err) reject(err)
  465. resolve(rows)
  466. })
  467. })
  468. }
  469. getVoicemailFileList () {
  470. return new Promise((resolve, reject) => {
  471. this.openDatabase('Manifest.db', true)
  472. .then(manifestdb => {
  473. manifestdb.all(`SELECT * from FILES where relativePath like 'Library/Voicemail/%.amr'`, async (err, rows) => {
  474. if (err) reject(err)
  475. resolve(rows)
  476. })
  477. })
  478. .catch(reject)
  479. })
  480. }
  481. getWifiList () {
  482. return new Promise((resolve, reject) => {
  483. var filename = this.getFileName(databases.WiFi)
  484. try {
  485. let wifiList = plist.parseFile(filename)
  486. wifiList['List of known networks'] = wifiList['List of known networks']
  487. .map(el => {
  488. if (el.BSSID) {
  489. el.BSSID = macParse.pad_zeros(el.BSSID) + ''
  490. }
  491. return el
  492. })
  493. resolve(wifiList)
  494. } catch (e) {
  495. reject(e)
  496. }
  497. })
  498. }
  499. getCookies () {
  500. return new Promise(async (resolve, reject) => {
  501. this.openDatabase('Manifest.db', true)
  502. .then(manifestdb => {
  503. manifestdb.all(`SELECT fileID,domain,relativePath from FILES where relativePath like 'Library/Cookies/Cookies.binarycookies'`, async (err, rows) => {
  504. if (err) return reject(err)
  505. let cookiesResult = []
  506. const iterateElements = (elements, index, callback) => {
  507. if (index === elements.length) { return callback() }
  508. // do parse call with element
  509. var ele = elements[index]
  510. cookieParser.parse(this.getFileName(ele.fileID))
  511. .then(cookies => {
  512. // Map to include domain
  513. let formatted = cookies.map(el => { return { domain: ele.domain, cookie: el } })
  514. // Append result
  515. cookiesResult = [...cookiesResult, ...formatted]
  516. // Next file.
  517. iterateElements(elements, index + 1, callback)
  518. })
  519. }
  520. iterateElements(rows, 0, () => {
  521. resolve(cookiesResult)
  522. })
  523. })
  524. })
  525. .catch(reject)
  526. })
  527. }
  528. getAddressBook () {
  529. return new Promise((resolve, reject) => {
  530. let addressbookdb = this.getDatabase(databases.AddressBook)
  531. const self = this
  532. try {
  533. // Query basic Address Book fields
  534. const query = `
  535. select ABPerson.ROWID
  536. , ABPerson.first
  537. , ABPerson.middle
  538. , ABPerson.last
  539. , ABPerson.Organization as organization
  540. , ABPerson.Department as department
  541. , ABPerson.Birthday as birthday
  542. , ABPerson.JobTitle as jobtitle
  543. , datetime(ABPerson.CreationDate + 978307200, 'unixepoch') as created_date
  544. , datetime(ABPerson.ModificationDate + 978307200, 'unixepoch') as updated_date
  545. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Work>!$_')) as phone_work
  546. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Mobile>!$_')) as phone_mobile
  547. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Home>!$_')) as phone_home
  548. , (select value from ABMultiValue where property = 4 and record_id = ABPerson.ROWID) as email
  549. , (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
  550. , (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
  551. , ABPerson.Note as note
  552. from ABPerson
  553. order by ABPerson.ROWID
  554. `
  555. addressbookdb.all(query, async function (err, rows) {
  556. if (err) reject(err)
  557. const iterateElements = (elements, index, callback) => {
  558. if (index === elements.length) { return callback() }
  559. // do parse call with element
  560. let ele = elements[index]
  561. //Query username and profile links for other services (facebook etc)
  562. const query = `
  563. select (select value from ABMultiValue where property = 22 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = 'PROFILE')) as google_profile
  564. , (select value from ABMultiValue where property = 22 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = 'profile')) as google_profile1
  565. , (select value from ABMultiValue where property = 4 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = 'iCloud')) as icloud
  566. , (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) = 'service')) as service
  567. , (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) = 'username')) as username
  568. , (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) = 'url')) as url
  569. from ABPerson
  570. where ABPerson.ROWID = ${ele.ROWID}
  571. order by ABPerson.ROWID
  572. `
  573. addressbookdb.all(query, async function (err, rows1) {
  574. if (err) reject(err)
  575. rows1[0].google_profile = rows1[0].google_profile || rows1[0].google_profile1
  576. delete rows1[0].google_profile1
  577. ele.services = rows1[0]
  578. const addressbookimagesdb = self.getDatabase(databases.AddressBookImages)
  579. //Query profile picture extraction from /Library/AddressBook/AddressBookImages.sqlitedb
  580. const query = `
  581. select data
  582. from ABFullSizeImage
  583. where ABFullSizeImage.record_id = ${ele.ROWID}
  584. `
  585. addressbookimagesdb.get(query, async function (err, row) {
  586. if (err) reject(err)
  587. ele.profile_picture = null
  588. if (row) {
  589. ele.profile_picture = (row.data || '').toString('base64')
  590. }
  591. iterateElements(elements, index + 1, callback)
  592. })
  593. })
  594. }
  595. iterateElements(rows, 0, () => {
  596. resolve(rows)
  597. })
  598. })
  599. } catch (e) {
  600. reject(e)
  601. }
  602. })
  603. }
  604. getSafariBookmarks () {
  605. return new Promise((resolve, reject) => {
  606. var bookmarksdb = this.getDatabase(databases.SafariBookmarks)
  607. try {
  608. const query = `
  609. select bookmarks.id
  610. , bookmarks.title
  611. , bookmarks.url
  612. , bookmarks.parent as parent_id
  613. , bookmarks.special_id
  614. , bookmarks.type
  615. , bookmarks.num_children
  616. , bookmarks.editable
  617. , bookmarks.deletable
  618. , bookmarks.hidden
  619. , bookmarks.hidden_ancestor_count
  620. , bookmarks.order_index
  621. , bookmarks.external_uuid
  622. , bookmarks.read
  623. , bookmarks.last_modified
  624. , bookmarks.server_id
  625. , bookmarks.sync_key
  626. , bookmarks.added
  627. , bookmarks.deleted
  628. , bookmarks.fetched_icon
  629. , bookmarks.dav_generation
  630. , bookmarks.locally_added
  631. , bookmarks.archive_status
  632. , bookmarks.syncable
  633. , bookmarks.web_filter_status
  634. , bookmarks.modified_attributes
  635. , parent_bookmarks.title as parent_title
  636. from bookmarks
  637. left join bookmarks as parent_bookmarks on parent_bookmarks.id = bookmarks.parent
  638. where bookmarks.type = 0
  639. order by bookmarks.id
  640. `
  641. bookmarksdb.all(query, async function (err, rows) {
  642. if (err) reject(err)
  643. resolve(rows)
  644. })
  645. } catch (e) {
  646. reject(e)
  647. }
  648. })
  649. }
  650. getSafariBookmarksiOS7 () {
  651. return new Promise((resolve, reject) => {
  652. var bookmarksdb = this.getDatabase(databases.SafariBookmarks)
  653. try {
  654. const query = `
  655. select bookmarks.id
  656. , bookmarks.special_id
  657. , bookmarks.parent as parent_id
  658. , bookmarks.type
  659. , bookmarks.title
  660. , bookmarks.url
  661. , bookmarks.num_children
  662. , bookmarks.editable
  663. , bookmarks.deletable
  664. , bookmarks.hidden
  665. , bookmarks.hidden_ancestor_count
  666. , bookmarks.order_index
  667. , bookmarks.external_uuid
  668. , bookmarks.read
  669. , bookmarks.last_modified
  670. , bookmarks.server_id
  671. , bookmarks.sync_key
  672. , bookmarks.sync_data
  673. , bookmarks.added
  674. , bookmarks.deleted
  675. , bookmarks.extra_attributes
  676. , bookmarks.local_attributes
  677. , bookmarks.fetched_icon
  678. , bookmarks.icon
  679. , bookmarks.dav_generation
  680. , bookmarks.locally_added
  681. , bookmarks.archive_status
  682. , bookmarks.syncable
  683. , bookmarks.web_filter_status
  684. , parent_bookmarks.title as parent_title
  685. from bookmarks
  686. left join bookmarks as parent_bookmarks on parent_bookmarks.id = bookmarks.parent
  687. where bookmarks.type = 0
  688. order by bookmarks.id
  689. `
  690. bookmarksdb.all(query, async function (err, rows) {
  691. if (err) reject(err)
  692. resolve(rows)
  693. })
  694. } catch (e) {
  695. reject(e)
  696. }
  697. })
  698. }
  699. getPushstore () {
  700. return new Promise((resolve, reject) => {
  701. this.getFileManifest().then((manifest) => {
  702. try {
  703. let files = manifest.filter((file) => {
  704. if (file.relativePath)
  705. return ~file.relativePath.indexOf("Library/SpringBoard/PushStore/")
  706. return false
  707. })
  708. const pushstores = []
  709. files.forEach((file) => {
  710. let data = plist.parseFile(this.getFileName(file.fileID))
  711. pushstores.push(...pushstoreParse.run(data))
  712. })
  713. resolve(pushstores)
  714. } catch (e) {
  715. reject(e)
  716. }
  717. })
  718. })
  719. }
  720. getOldPushstore () {
  721. return new Promise((resolve, reject) => {
  722. this.getOldFileManifest().then((manifest) => {
  723. try {
  724. let files = manifest.filter((file) => {
  725. if (file.filename) {
  726. return ~file.filename.indexOf("Library/SpringBoard/PushStore/")
  727. }
  728. return false
  729. })
  730. let pushstores = []
  731. files.forEach((file) => {
  732. let data = plist.parseFile(this.getFileName(file.fileID))
  733. pushstores.push(...pushstoreParse.run(data))
  734. })
  735. resolve(pushstores)
  736. } catch (e) {
  737. reject(e)
  738. }
  739. })
  740. })
  741. }
  742. }
  743. module.exports.availableBackups = function () {
  744. const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
  745. return new Promise((resolve, reject) => {
  746. resolve(fs.readdirSync(base, {
  747. encoding: 'utf8'
  748. })
  749. .map(file => IPhoneBackup.fromID(file)))
  750. })
  751. }
  752. module.exports.iPhoneBackup = IPhoneBackup
  753. module.exports.IPhoneBackup = IPhoneBackup