index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. #!/usr/bin/env node
  2. const chalk = require('chalk')
  3. const fs = require('fs')
  4. const program = require('commander')
  5. const path = require('path')
  6. const { URL } = require('url')
  7. const stripAnsi = require('strip-ansi')
  8. const iPhoneBackup = require('./util/iphone_backup.js').iPhoneBackup
  9. const normalizeCols = require('./util/normalize.js')
  10. var base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
  11. program
  12. .version('2.0.0')
  13. .option('-l, --list', 'List Backups')
  14. .option('-c, --conversations', 'List Conversations')
  15. .option('-m, --messages <conversation_id>', 'List Conversations')
  16. .option('-r, --report <report_type>', 'Report types: apps, notes, webhistory, photolocations, manifest')
  17. .option(`-d, --dir <directory>`, `Backup Directory (default: ${base})`)
  18. .option(`-u, --device <device>`, 'Device UUID')
  19. .option(`-b, --backup <backup>`, 'Backup ID')
  20. .option(`-v, --verbose`, 'Verbose debugging output')
  21. .option(`-x, --no-color`, 'Disable colorized output')
  22. .option('-z, --dump', 'Dump a ton of raw JSON formatted data instead of formatted output')
  23. program.on('--help', function(){
  24. console.log('')
  25. console.log("If you're interested to know how this works, check out my post:")
  26. console.log("https://www.richinfante.com/2017/3/16/reverse-engineering-the-ios-backup")
  27. console.log('')
  28. })
  29. program.parse(process.argv);
  30. if(!process.stdout.isTTY) { program.color = false }
  31. base = program.dir || base
  32. if(program.verbose) console.log('Using source:', base)
  33. if(program.list) {
  34. var items = fs.readdirSync(base, { encoding: 'utf8' })
  35. .filter(el => (el.length == 40))
  36. .map(file => iPhoneBackup.fromID(file, base))
  37. // Possibly dump output
  38. if(program.dump) {
  39. console.log(JSON.stringify(items, null, 4))
  40. return
  41. }
  42. items = items.map(el => {
  43. return {
  44. encrypted: el.manifest ? el.manifest.IsEncrypted
  45. ? chalk.green('encrypted')
  46. : chalk.red('not encrypted')
  47. : 'unknown encryption',
  48. device_name: el.manifest ? el.manifest.Lockdown.DeviceName : 'Unknown Device',
  49. device_id: el.id,
  50. serial: el.manifest.Lockdown.SerialNumber,
  51. iOSVersion: el.manifest.Lockdown.ProductVersion + '(' + el.manifest.Lockdown.BuildVersion + ')',
  52. backupVersion: el.status ? el.status.Version : '?',
  53. date: el.status ? new Date(el.status.Date).toLocaleString() : ''
  54. }})
  55. .map(el => [
  56. chalk.gray(el.device_id),
  57. el.encrypted,
  58. el.date,
  59. el.device_name,
  60. el.serial,
  61. el.iOSVersion,
  62. el.backupVersion
  63. ])
  64. items = [
  65. ['UDID', 'Encryption', 'Date', 'Device Name', 'Serial #', 'iOS Version', 'Backup Version'],
  66. ['-','-','-','-','-','-','-'],
  67. ...items
  68. ]
  69. items = normalizeCols(items)
  70. items = items.map(el => el.join(' | ')).join('\n')
  71. if(!program.color) { items = stripAnsi(items) }
  72. console.log('BACKUPS LIST')
  73. console.log(items)
  74. } else if (program.conversations) {
  75. if(!program.backup) {
  76. console.log('use -b or --backup <id> to specify backup.')
  77. process.exit(1)
  78. }
  79. // Grab the backup
  80. var backup = iPhoneBackup.fromID(program.backup, base)
  81. backup.getConversations(program.dump)
  82. .then((items) => {
  83. if(program.dump) return
  84. var items = items.map(el => [
  85. el.ROWID + '',
  86. chalk.gray(el.XFORMATTEDDATESTRING || '??'),
  87. el.chat_identifier + '',
  88. el.display_name + ''
  89. ])
  90. items = [['ID', 'DATE', 'Chat Name', 'Display Name'], ['-', '-', '-', '-',], ...items]
  91. items = normalizeCols(items).map(el => el.join(' | ')).join('\n')
  92. if(!program.color) { items = stripAnsi(items) }
  93. console.log(items)
  94. })
  95. .catch((e) => {
  96. console.log('[!] Encountered an Error:', e)
  97. })
  98. } else if(program.messages) {
  99. if(!program.backup) {
  100. console.log('use -b or --backup <id> to specify backup.')
  101. process.exit(1)
  102. }
  103. // Grab the backup
  104. var backup = iPhoneBackup.fromID(program.backup, base)
  105. backup.getMessages(program.messages, program.dump)
  106. .then((items) => {
  107. if(program.dump) return
  108. items = items.map(el => [
  109. chalk.gray(el.XFORMATTEDDATESTRING + ''),
  110. chalk.blue(el.x_sender + ''),
  111. el.text || ''
  112. ])
  113. items = normalizeCols(items, 2).map(el => el.join(' | ')).join('\n')
  114. if(!program.color) { items = stripAnsi(items) }
  115. console.log(items)
  116. })
  117. .catch((e) => {
  118. console.log('[!] Encountered an Error:', e)
  119. })
  120. } else if(program.report) {
  121. ///
  122. /// APPS REPORT
  123. ///
  124. if(program.report == 'apps') {
  125. if(!program.backup) {
  126. console.log('use -b or --backup <id> to specify backup.')
  127. process.exit(1)
  128. }
  129. // Grab the backup
  130. var backup = iPhoneBackup.fromID(program.backup, base)
  131. if (!backup.manifest) return {}
  132. // Possibly dump output
  133. if(program.dump) {
  134. console.log(JSON.stringify(backup.manifest, null, 4))
  135. return
  136. }
  137. // Enumerate the apps in the backup
  138. var apps = []
  139. for (var key in backup.manifest.Applications) {
  140. apps.push(key)
  141. }
  142. console.log(`Apps installed inside backup: ${backup.id}`)
  143. console.log(apps.map(el => '- ' + el).join('\n'))
  144. } else if(program.report == 'oldnotes') {
  145. if(!program.backup) {
  146. console.log('use -b or --backup <id> to specify backup.')
  147. process.exit(1)
  148. }
  149. // Grab the backup
  150. var backup = iPhoneBackup.fromID(program.backup, base)
  151. backup.getOldNotes(program.dump)
  152. .then((items) => {
  153. // Dump if needed
  154. if(program.dump) {
  155. console.log(JSON.stringify(items, null, 4))
  156. return
  157. }
  158. // Otherwise, format table
  159. items = items.map(el => [el.XFORMATTEDDATESTRING + '', (el.Z_PK + ''), (el.ZTITLE + '').substring(0, 128)])
  160. items = [['Modified', 'ID', 'Title'], ['-', '-', '-'], ...items]
  161. items = normalizeCols(items).map(el => el.join(' | ')).join('\n')
  162. if(!program.color) { items = stripAnsi(items) }
  163. console.log(items)
  164. })
  165. .catch((e) => {
  166. console.log('[!] Encountered an Error:', e)
  167. })
  168. } else if(program.report == 'notes') {
  169. if(!program.backup) {
  170. console.log('use -b or --backup <id> to specify backup.')
  171. process.exit(1)
  172. }
  173. // Grab the backup
  174. var backup = iPhoneBackup.fromID(program.backup, base)
  175. backup.getNotes(program.dump)
  176. .then((items) => {
  177. // Dump if needed
  178. if(program.dump) {
  179. console.log(JSON.stringify(items, null, 4))
  180. return
  181. }
  182. // Otherwise, format table
  183. items = items.map(el => [
  184. (el.XFORMATTEDDATESTRING || el.XFORMATTEDDATESTRING1 )+ '',
  185. (el.Z_PK + ''),
  186. (el.ZTITLE2+ '').trim().substring(0, 128),
  187. (el.ZTITLE1+ '').trim() || ''
  188. ])
  189. items = [['Modified', 'ID', 'Title2', 'Title1'], ['-', '-', '-', '-'], ...items]
  190. items = normalizeCols(items, 3).map(el => el.join(' | ')).join('\n')
  191. if(!program.color) { items = stripAnsi(items) }
  192. console.log(items)
  193. })
  194. .catch((e) => {
  195. console.log('[!] Encountered an Error:', e)
  196. })
  197. } else if(program.report == 'webhistory') {
  198. if(!program.backup) {
  199. console.log('use -b or --backup <id> to specify backup.')
  200. process.exit(1)
  201. }
  202. // Grab the backup
  203. var backup = iPhoneBackup.fromID(program.backup, base)
  204. backup.getWebHistory(program.dump)
  205. .then((history) => {
  206. if(program.dump) {
  207. console.log(JSON.stringify(history, null, 4))
  208. return
  209. }
  210. var items = history.map(el => [
  211. el.XFORMATTEDDATESTRING + '' || '',
  212. new URL(el.url || '').origin || '',
  213. (el.title || '').substring(0, 64)
  214. ])
  215. items = [['Time', 'URL', 'Title'], ['-', '-', '-'], ...items]
  216. items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
  217. if(!program.color) { items = stripAnsi(items) }
  218. console.log(items)
  219. })
  220. .catch((e) => {
  221. console.log('[!] Encountered an Error:', e)
  222. })
  223. } else if(program.report == 'photolocations') {
  224. if(!program.backup) {
  225. console.log('use -b or --backup <id> to specify backup.')
  226. process.exit(1)
  227. }
  228. // Grab the backup
  229. var backup = iPhoneBackup.fromID(program.backup, base)
  230. backup.getPhotoLocationHistory(program.dump)
  231. .then((history) => {
  232. if(program.dump) {
  233. console.log(JSON.stringify(history, null, 4))
  234. return
  235. }
  236. var items = history.map(el => [
  237. el.XFORMATTEDDATESTRING + '' || '',
  238. el.ZLATITUDE + '' || '',
  239. el.ZLONGITUDE + '' || '',
  240. el.ZFILENAME + '' || ''
  241. ])
  242. items = [['Time', 'Latitude', 'Longitude', 'Photo Name'], ['-', '-', '-'], ...items]
  243. items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
  244. if(!program.color) { items = stripAnsi(items) }
  245. console.log(items)
  246. })
  247. .catch((e) => {
  248. console.log('[!] Encountered an Error:', e)
  249. })
  250. } else if(program.report == 'manifest') {
  251. if(!program.backup) {
  252. console.log('use -b or --backup <id> to specify backup.')
  253. process.exit(1)
  254. }
  255. // Grab the backup
  256. var backup = iPhoneBackup.fromID(program.backup, base)
  257. backup.getFileManifest()
  258. .then((items) => {
  259. if(program.dump) {
  260. console.log(JSON.stringify(items, null, 4))
  261. return
  262. }
  263. var items = items.map(el => [
  264. el.fileID + '',
  265. el.relativePath + ''
  266. ])
  267. items = [['ID', 'Path'], ['-', '-'], ...items]
  268. items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
  269. if(!program.color) { items = stripAnsi(items) }
  270. console.log(items)
  271. })
  272. .catch((e) => {
  273. console.log('[!] Encountered an Error:', e)
  274. })
  275. } else {
  276. console.log('')
  277. console.log(' [!] Unknown Option type:', program.report)
  278. console.log('')
  279. program.outputHelp()
  280. }
  281. } else {
  282. program.outputHelp()
  283. }