iphone_backup.js 26 KB

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