Эх сурвалжийг харах

Refactor, Add better support for iOS 9

Rich Infante 7 жил өмнө
parent
commit
52cc9d996d

+ 18 - 9
Readme.md

@@ -42,12 +42,19 @@ UDID="0c1bc52c50016933679b0980ccff3680e5831162"
 ### Reports
 - run using `ibackuptool -b <udid> --report <type>`
 - Current types:
-    - `apps`: List all installed bundle ids and groups.
-    - `notes`: List all notes data
-    - `oldnotes`: List old notes data that may have been retained from around iOS 8ish
-    - `webhistory`: List recent web history
-    - `photolocations`: List photo locations (lat/lng) and timestamp.
-    - `manifest`: List all files in the backup manifest and what they are.
+    - `apps` - List all installed applications and container IDs.
+    - `calls` - List all call records contained in the backup.
+    - `conversations` - List all SMS and iMessage conversations
+    - `list` - List of all backups. alias for -l
+    - `manifest` - List all the files contained in the backup (iOS 10+)
+    - `messages` - List all SMS and iMessage messages in a conversation
+    - `notes` - List all iOS notes
+    - `oldnotes` - List all iOS notes (from older unused database)
+    - `photolocations` - List all geolocation information for iOS photos (iOS 10+)
+    - `voicemail-files` - List all or extract voicemail files (iOS 10+)
+    - `voicemail` - List all or extract voicemails on device
+    - `webhistory` - List all web history
+    - `wifi` - List associated wifi networks and their usage information
 
 ```bash
 # Using a UDID from the previous step, now you can run:
@@ -57,7 +64,7 @@ ibackuptool -b $UDID --report apps
 # List Recent Web History
 ibackuptool -b $UDID --report webhistory
 
-# List Recent Photos Geolocations (iOS 9+)
+# List Recent Photos Geolocations (iOS 10+)
 ibackuptool -b $UDID --report photolocations
 
 # List iOS Notes
@@ -72,10 +79,10 @@ ibackuptool -b $UDID --report calls
 # List voicemails 
 ibackuptool -b $UDID --report voicemail
 
-# List voicemail files (iOS 9+)
+# List voicemail files (iOS 10+)
 ibackuptool -b $UDID --report voicemail-files
 
-# Export voicemail files (iOS 9+)
+# Export voicemail files (iOS 10+)
 ibackuptool -b $UDID --report voicemail-files --export ./ExportedVoicemails
 
 # List wifi networks 
@@ -88,10 +95,12 @@ ibackuptool -b $UDID --report wifi
 # List of all conversations, indexed by ID.
 # Each row starts with an ID number, which is needed for the next step.
 ibackuptool -b $UDID --conversations
+ibackuptool -b $UDID --report conversations
 
 # Now, Fetch the messages with the following command
 # Replace $CONVERSATION_ID with a row ID from `ibackuptool -b $UDID --conversations`
 ibackuptool -b $UDID --messages $CONVERSATION_ID
+ibackuptool -b $UDID --report messages --messages $CONVERSATION_ID
 ```
 
 ## Need More Data?

+ 69 - 483
tools/index.js

@@ -1,507 +1,93 @@
 #!/usr/bin/env node
 
-const chalk = require('chalk')
-const fs = require('fs-extra')
 const program = require('commander')
 const path = require('path')
-const { URL } = require('url')
-const stripAnsi = require('strip-ansi')
-const iPhoneBackup = require('./util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('./util/normalize.js')
+const chalk = require('chalk')
 var base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
 
+var reportTypes = {
+  'apps': require('./reports/apps'),
+  'calls': require('./reports/calls'),
+  'conversations': require('./reports/conversations'),
+  'list': require('./reports/list'),
+  'manifest': require('./reports/manifest'),
+  'messages': require('./reports/messages'),
+  'notes': require('./reports/notes'),
+  'oldnotes': require('./reports/oldnotes'),
+  'photolocations': require('./reports/photolocations'),
+  'voicemail-files': require('./reports/voicemail-files'),
+  'voicemail': require('./reports/voicemail'),
+  'webhistory': require('./reports/webhistory'),
+  'wifi': require('./reports/wifi')
+}
+
 program
     .version('2.0.2')
     .option('-l, --list', 'List Backups')
+    .option(`-b, --backup <backup>`, 'Backup ID')
+    .option('-r, --report <report_type>', 'Select a report type. see below for a full list.')
     .option('-c, --conversations', 'List Conversations')
-    .option('-m, --messages <conversation_id>', 'List Conversations')
-    .option('-r, --report <report_type>', 'Report types: apps, notes, webhistory, photolocations, manifest')
+    .option('-m, --messages <conversation_id>', 'List messages')
+    .option(`-e, --extract <dir>`, 'Extract data for commands. supported by: voicemail-files')
     .option(`-d, --dir <directory>`, `Backup Directory (default: ${base})`)
-    .option(`-u, --device <device>`, 'Device UUID')
-    .option(`-b, --backup <backup>`, 'Backup ID')
     .option(`-v, --verbose`, 'Verbose debugging output')
     .option(`-x, --no-color`, 'Disable colorized output')
     .option('-z, --dump', 'Dump a ton of raw JSON formatted data instead of formatted output')
-    .option(`-e, --extract <dir>`, 'Extract data for commands. reports: voicemail')
-    
-program.on('--help', function(){
-    console.log('')
-    console.log("If you're interested to know how this works, check out my post:")
-    console.log("https://www.richinfante.com/2017/3/16/reverse-engineering-the-ios-backup")
-    console.log('')
+    .option(`--help-reports`, `Display supported report types.`)
+
+program.on('--help', function () {
+  console.log('')
+  console.log('Supported Report Types:')
+  for (var i in reportTypes) {
+    if (program.isTTY) {
+      console.log('  ', reportTypes[i].name, '-', reportTypes[i].description)
+    } else {
+      console.log('  ', chalk.green(reportTypes[i].name), '-', reportTypes[i].description)
+    }
+  }
+  console.log('')
+  console.log("If you're interested to know how this works, check out my post:")
+  console.log('https://www.richinfante.com/2017/3/16/reverse-engineering-the-ios-backup')
+  console.log('')
 })
-    
-program.parse(process.argv);
-
-if(!process.stdout.isTTY) { program.color = false }
 
-base = program.dir || base
-
-if(program.verbose) console.log('Using source:', base)
-
-if(program.list) {
-    var items = fs.readdirSync(base, { encoding: 'utf8' })
-        .filter(el => (el.length == 40))
-        .map(file => iPhoneBackup.fromID(file, base))
-        
-        
-        // Possibly dump output
-        if(program.dump) {
-            console.log(JSON.stringify(items, null, 4))
-            return
-        }
+program.parse(process.argv)
 
-    items = items.map(el => { 
-            return {
-            encrypted: el.manifest ? el.manifest.IsEncrypted 
-                                        ? chalk.green('encrypted') 
-                                        : chalk.red('not encrypted')
-                                   : 'unknown encryption',
-            device_name: el.manifest ? el.manifest.Lockdown.DeviceName : 'Unknown Device',
-            device_id: el.id,
-            serial: el.manifest.Lockdown.SerialNumber,
-            iOSVersion: el.manifest.Lockdown.ProductVersion + '(' + el.manifest.Lockdown.BuildVersion + ')',
-            backupVersion: el.status ? el.status.Version : '?',
-            date: el.status ? new Date(el.status.Date).toLocaleString() : ''
-        }})
-        .map(el => [
-            chalk.gray(el.device_id), 
-            el.encrypted,
-            el.date, 
-            el.device_name,
-            el.serial,
-            el.iOSVersion,
-            el.backupVersion
-        ])
+if (!process.stdout.isTTY) { program.color = false }
 
-    items = [
-        ['UDID', 'Encryption', 'Date', 'Device Name', 'Serial #', 'iOS Version', 'Backup Version'],
-        ['-','-','-','-','-','-','-'],
-            ...items
-    ]
-    items = normalizeCols(items)
-    items = items.map(el => el.join(' | ')).join('\n')
+base = program.dir || base
 
-    if(!program.color) { items = stripAnsi(items) }
+if (program.verbose) console.log('Using source:', base)
 
-    console.log('BACKUPS LIST')
-    console.log(items)
+if (program.list) {
+    // Shortcut for list report
+  reportTypes.list.func(program, base)
 } else if (program.conversations) {
-    if(!program.backup) {
-        console.log('use -b or --backup <id> to specify backup.')
-        process.exit(1)
-    }
-
-    // Grab the backup
-    var backup = iPhoneBackup.fromID(program.backup, base)
-
-
-    backup.getConversations(program.dump)
-    .then((items) => {
-        if(program.dump) return 
-
-        var items = items.map(el => [ 
-            el.ROWID + '', 
-            chalk.gray(el.XFORMATTEDDATESTRING || '??'),
-            el.chat_identifier + '',
-            el.display_name + ''
-        ])
-
-        items = [['ID', 'DATE', 'Chat Name', 'Display Name'], ['-', '-', '-', '-',], ...items]
-        items = normalizeCols(items).map(el => el.join(' | ')).join('\n')
-        
-        if(!program.color) { items = stripAnsi(items) }
-
-        console.log(items)
-    })
-    .catch((e) => {
-        console.log('[!] Encountered an Error:', e)
-    })
-} else if(program.messages) {
-    if(!program.backup) {
-        console.log('use -b or --backup <id> to specify backup.')
-        process.exit(1)
+    // Legacy shortcut for conversations report
+  reportTypes.conversations.func(program, base)
+} else if (program.messages) {
+    // Legacy shortcut for messages report
+  reportTypes.messages.func(program, base)
+} else if (program.report) {
+    // If the report is valid
+  if (reportTypes[program.report]) {
+    var report = reportTypes[program.report]
+
+        // Try to use it
+    if (report.func) {
+      try {
+        report.func(program, base)
+      } catch (e) {
+        console.log('[!] Encountered an error', e)
+      }
     }
-
-    // Grab the backup
-    var backup = iPhoneBackup.fromID(program.backup, base)
-
-
-    backup.getMessages(program.messages, program.dump)
-    .then((items) => {
-        if(program.dump) return 
-
-        items = items.map(el => [
-            chalk.gray(el.XFORMATTEDDATESTRING + ''),
-            chalk.blue(el.x_sender + ''),
-            el.text || ''
-        ])
-
-        items = normalizeCols(items, 2).map(el => el.join(' | ')).join('\n')
-
-        if(!program.color) { items = stripAnsi(items) }
-
-        console.log(items)
-    })
-    .catch((e) => {
-        console.log('[!] Encountered an Error:', e)
-    })
-} else if(program.report) {
-    ///
-    /// APPS REPORT
-    ///
-    if(program.report == 'apps') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-
-        if (!backup.manifest) return {}
-
-        // Possibly dump output
-        if(program.dump) {
-            console.log(JSON.stringify(backup.manifest, null, 4))
-            return
-        }
-
-        // Enumerate the apps in the backup
-        var apps = []
-        for (var key in backup.manifest.Applications) {
-            apps.push(key)
-        }
-
-        console.log(`Apps installed inside backup: ${backup.id}`)
-        console.log(apps.map(el => '- ' + el).join('\n'))
-    } else if(program.report == 'oldnotes') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getOldNotes(program.dump)
-            .then((items) => {
-                // Dump if needed
-                if(program.dump) {
-                    console.log(JSON.stringify(items, null, 4))
-                    return
-                }
-                
-                // Otherwise, format table
-                items = items.map(el => [el.XFORMATTEDDATESTRING + '', (el.Z_PK + ''), (el.ZTITLE + '').substring(0, 128)])
-                items = [['Modified', 'ID', 'Title'], ['-', '-', '-'], ...items]
-                items = normalizeCols(items).map(el => el.join(' | ')).join('\n')
-                
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    } else if(program.report == 'notes') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getNotes(program.dump)
-            .then((items) => {
-                // Dump if needed
-                if(program.dump) {
-                    console.log(JSON.stringify(items, null, 4))
-                    return
-                }
-                
-                // Otherwise, format table
-                items = items.map(el => [
-                    (el.XFORMATTEDDATESTRING || el.XFORMATTEDDATESTRING1 )+ '', 
-                    (el.Z_PK + ''), 
-                    (el.ZTITLE2+ '').trim().substring(0, 128), 
-                    (el.ZTITLE1+ '').trim() || ''
-                ])
-                items = [['Modified', 'ID', 'Title2', 'Title1'], ['-', '-', '-', '-'], ...items]
-                items = normalizeCols(items, 3).map(el => el.join(' | ')).join('\n')
-                
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    }  else if(program.report == 'webhistory') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getWebHistory(program.dump)
-            .then((history) => {
-
-                if(program.dump) {
-                    console.log(JSON.stringify(history, null, 4))
-                    return
-                }
-
-                var items = history.map(el => [
-                    el.XFORMATTEDDATESTRING + '' || '',
-                    new URL(el.url || '').origin || '',
-                    (el.title || '').substring(0, 64)
-                ])
-
-                items = [['Time', 'URL', 'Title'], ['-', '-', '-'], ...items]
-                items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
-                
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    } else if(program.report == 'photolocations') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getPhotoLocationHistory(program.dump)
-            .then((history) => {
-
-                if(program.dump) {
-                    console.log(JSON.stringify(history, null, 4))
-                    return
-                }
-
-                var items = history.map(el => [
-                    el.XFORMATTEDDATESTRING + '' || '',
-                    el.ZLATITUDE + '' || '',
-                    el.ZLONGITUDE  + '' || '',
-                    el.ZFILENAME + '' || ''
-                ])
-
-                items = [['Time', 'Latitude', 'Longitude', 'Photo Name'], ['-', '-', '-'], ...items]
-                items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
-
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    }  else if(program.report == 'manifest') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getFileManifest()
-            .then((items) => {
-
-                if(program.dump) {
-                    console.log(JSON.stringify(items, null, 4))
-                    return
-                }
-
-                var items = items.map(el => [
-                    el.fileID + '',
-                    el.relativePath + '' 
-                ])
-
-                items = [['ID', 'Path'], ['-', '-'], ...items]
-                items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
-                
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    } else if(program.report == 'calls') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getCallsList()
-            .then((items) => {
-
-                if(program.dump) {
-                    console.log(JSON.stringify(items, null, 4))
-                    return
-                }
-
-                var items = items.map(el => [
-                    el.Z_PK + '',
-                    el.XFORMATTEDDATESTRING,
-                    el.ZANSWERED + '',
-                    el.ZORIGINATED + '',
-                    el.ZCALLTYPE + '',
-                    el.ZDURATION + '',
-                    el.ZLOCATION + '',
-                    el.ZISO_COUNTRY_CODE + '',
-                    el.ZSERVICE_PROVIDER + '',
-                    (el.ZADDRESS || '').toString()
-                ])
-
-                items = [['ID', 'Date', 'Answered', 'Originated', 'Type', 'Duration', 'Location', 'Country', 'Service', 'Address'], ['-', '-', '-', '-', '-', '-', '-', '-', '-', '-'], ...items]
-                items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
-                
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    }  else if(program.report == 'voicemail') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getVoicemailsList()
-            .then((items) => {
-
-                if(program.dump) {
-                    console.log(JSON.stringify(items, null, 4))
-                    return
-                }
-
-                var items = items.map(el => [
-                    el.ROWID + '',
-                    el.XFORMATTEDDATESTRING,
-                    el.sender + '',
-                    el.token + '',
-                    el.duration + '',
-                    el.expiration + '',
-                    el.trashed_date + '',
-                    el.flags + ''
-                ])
-
-                items = [['ID', 'Date', 'Sender', 'Token', 'Duration', 'Expiration', 'Trashed', 'Flags'], ['-', '-', '-', '-', '-', '-', '-', '-'], ...items]
-                items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
-                
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    } else if(program.report == 'voicemail-files') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getVoicemailFileList()
-            .then((list) => {
-
-                if(program.dump) {
-                    console.log(JSON.stringify(list, null, 4))
-                    return
-                }
-
-                if(program.extract) {
-                    for(var item of list) {
-                        try {
-                        var outDir = path.join(program.extract, path.basename(item.relativePath))
-                        fs.ensureDirSync(path.dirname(outDir))
-                        fs.createReadStream(backup.getFileName(item.fileID)).pipe(fs.createWriteStream(outDir));
-                        item.output_dir = outDir
-                        }catch(e) {
-                            console.log(`Couldn't Export: ${item.relativePath}`, e)
-                        }
-                    }
-                }
-
-                var items = list.map(el => [
-                    el.fileID + '',
-                    el.relativePath,
-                    el.output_dir || '<not exported>'
-                ])
-
-                items = [['ID', 'Path', 'Exported Path'], ['-', '-', '-'], ...items]
-                items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
-                
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    } else if(program.report == 'wifi') {
-        if(!program.backup) {
-            console.log('use -b or --backup <id> to specify backup.')
-            process.exit(1)
-        }
-
-
-        // Grab the backup
-        var backup = iPhoneBackup.fromID(program.backup, base)
-        backup.getWifiList()
-            .then((items) => {
-
-                if(program.dump) {
-                    console.log(JSON.stringify(items, null, 4))
-                    return
-                }
-
-                var items = items['List of known networks'].map(el => [
-                    el.lastJoined + '' || '',
-                    el.lastAutoJoined + '' || '',
-                    el.SSID_STR + '',
-                    el.BSSID + '',
-                    el.SecurityMode || '',
-                    el.HIDDEN_NETWORK + '',
-                    el.enabled + '',
-                ]).sort((a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime())
-
-                items = [['Last Joined', 'Last AutoJoined', 'SSID', 'BSSID','Security', 'Hidden', 'Enabled'], ['-', '-', '-', '-', '-', '-'], ...items]
-                items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
-                
-                if(!program.color) { items = stripAnsi(items) }
-
-                console.log(items)
-            })
-            .catch((e) => {
-                console.log('[!] Encountered an Error:', e)
-            })
-    } else {
-        console.log('')
-        console.log('  [!] Unknown Option type:', program.report)
-        console.log('  [!] It\'s possible this tool is out-of date.')
-        console.log('')
-        program.outputHelp()
-    }
-} else {
+  } else {
+    console.log('')
+    console.log('  [!] Unknown Option type:', program.report)
+    console.log('  [!] It\'s possible this tool is out-of date.')
+    console.log('')
     program.outputHelp()
+  }
+} else {
+  program.outputHelp()
 }

+ 31 - 0
tools/reports/apps.js

@@ -0,0 +1,31 @@
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+
+module.exports.name = 'apps'
+module.exports.description = 'List all installed applications and container IDs.'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+        // Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+
+  if (!backup.manifest) return {}
+
+        // Possibly dump output
+  if (program.dump) {
+    console.log(JSON.stringify(backup.manifest, null, 4))
+    return
+  }
+
+        // Enumerate the apps in the backup
+  var apps = []
+  for (var key in backup.manifest.Applications) {
+    apps.push(key)
+  }
+
+  console.log(`Apps installed inside backup: ${backup.id}`)
+  console.log(apps.map(el => '- ' + el).join('\n'))
+}

+ 46 - 0
tools/reports/calls.js

@@ -0,0 +1,46 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'calls'
+module.exports.description = 'List all call records contained in the backup.'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+// Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getCallsList()
+    .then((items) => {
+      if (program.dump) {
+        console.log(JSON.stringify(items, null, 4))
+        return
+      }
+
+      items = items.map(el => [
+        el.Z_PK + '',
+        el.XFORMATTEDDATESTRING,
+        el.ZANSWERED + '',
+        el.ZORIGINATED + '',
+        el.ZCALLTYPE + '',
+        el.ZDURATION + '',
+        el.ZLOCATION + '',
+        el.ZISO_COUNTRY_CODE + '',
+        el.ZSERVICE_PROVIDER + '',
+        (el.ZADDRESS || '').toString()
+      ])
+
+      items = [['ID', 'Date', 'Answered', 'Originated', 'Type', 'Duration', 'Location', 'Country', 'Service', 'Address'], ['-', '-', '-', '-', '-', '-', '-', '-', '-', '-'], ...items]
+      items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 40 - 0
tools/reports/conversations.js

@@ -0,0 +1,40 @@
+const stripAnsi = require('strip-ansi')
+const chalk = require('chalk')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'conversations'
+module.exports.description = 'List all SMS and iMessage conversations'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+  // Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+
+  backup.getConversations(program.dump)
+    .then((items) => {
+      if (program.dump) return
+
+      items = items.map(el => [
+        el.ROWID + '',
+        chalk.gray(el.XFORMATTEDDATESTRING || '??'),
+        el.service_name + '', 
+        el.chat_identifier + '',
+        el.display_name + ''
+      ])
+
+      items = [['ID', 'DATE', 'Service', 'Chat Name', 'Display Name'], ['-', '-', '-', '-', '-'], ...items]
+      items = normalizeCols(items).map(el => el.join(' | ')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 56 - 0
tools/reports/list.js

@@ -0,0 +1,56 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+const chalk = require('chalk')
+const fs = require('fs-extra')
+
+module.exports.name = 'list'
+module.exports.description = 'List of all backups. alias for -l'
+
+module.exports.func = function (program, base) {
+  var items = fs.readdirSync(base, { encoding: 'utf8' })
+    .filter(el => (el.length === 40))
+    .map(file => iPhoneBackup.fromID(file, base))
+
+    // Possibly dump output
+  if (program.dump) {
+    console.log(JSON.stringify(items, null, 4))
+    return
+  }
+
+  items = items.map(el => {
+    return {
+      encrypted: el.manifest ? el.manifest.IsEncrypted
+                                    ? chalk.green('encrypted')
+                                    : chalk.red('not encrypted')
+                            : 'unknown encryption',
+      device_name: el.manifest ? el.manifest.Lockdown.DeviceName : 'Unknown Device',
+      device_id: el.id,
+      serial: el.manifest.Lockdown.SerialNumber,
+      iOSVersion: el.manifest.Lockdown.ProductVersion + '(' + el.manifest.Lockdown.BuildVersion + ')',
+      backupVersion: el.status ? el.status.Version : '?',
+      date: el.status ? new Date(el.status.Date).toLocaleString() : ''
+    }
+  })
+    .map(el => [
+      chalk.gray(el.device_id),
+      el.encrypted,
+      el.date,
+      el.device_name,
+      el.serial,
+      el.iOSVersion,
+      el.backupVersion
+    ])
+
+  items = [
+    ['UDID', 'Encryption', 'Date', 'Device Name', 'Serial #', 'iOS Version', 'Backup Version'],
+    ['-', '-', '-', '-', '-', '-', '-'],
+    ...items
+  ]
+  items = normalizeCols(items)
+  items = items.map(el => el.join(' | ')).join('\n')
+
+  if (!program.color) { items = stripAnsi(items) }
+
+  console.log(items)
+}

+ 38 - 0
tools/reports/manifest.js

@@ -0,0 +1,38 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'manifest'
+module.exports.description = 'List all the files contained in the backup (iOS 10+)'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+// Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getFileManifest()
+    .then((items) => {
+      if (program.dump) {
+        console.log(JSON.stringify(items, null, 4))
+        return
+      }
+
+      items = items.map(el => [
+        el.fileID + '',
+        el.relativePath + ''
+      ])
+
+      items = [['ID', 'Path'], ['-', '-'], ...items]
+      items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 37 - 0
tools/reports/messages.js

@@ -0,0 +1,37 @@
+const stripAnsi = require('strip-ansi')
+const chalk = require('chalk')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'messages'
+module.exports.description = 'List all SMS and iMessage messages in a conversation'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+  // Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+
+  backup.getMessages(program.messages, program.dump)
+    .then((items) => {
+      if (program.dump) return
+
+      items = items.map(el => [
+        chalk.gray(el.XFORMATTEDDATESTRING + ''),
+        chalk.blue(el.x_sender + ''),
+        el.text || ''
+      ])
+
+      items = normalizeCols(items, 2).map(el => el.join(' | ')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 41 - 0
tools/reports/notes.js

@@ -0,0 +1,41 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'notes'
+module.exports.description = 'List all iOS notes'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+// Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getNotes(program.dump)
+    .then((items) => {
+        // Dump if needed
+      if (program.dump) {
+        console.log(JSON.stringify(items, null, 4))
+        return
+      }
+
+        // Otherwise, format table
+      items = items.map(el => [
+        (el.XFORMATTEDDATESTRING || el.XFORMATTEDDATESTRING1) + '',
+            (el.Z_PK + ''),
+        (el.ZTITLE2 + '').trim().substring(0, 128),
+        (el.ZTITLE1 + '').trim() || ''
+      ])
+      items = [['Modified', 'ID', 'Title2', 'Title1'], ['-', '-', '-', '-'], ...items]
+      items = normalizeCols(items, 3).map(el => el.join(' | ')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 41 - 0
tools/reports/oldnotes.js

@@ -0,0 +1,41 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'oldnotes'
+module.exports.description = 'List all iOS notes (from older unused database)'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+  // Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getOldNotes(program.dump)
+    .then((items) => {
+      // Dump if needed
+      if (program.dump) {
+        console.log(JSON.stringify(items, null, 4))
+        return
+      }
+
+      // Otherwise, format table
+      items = items.map(el => [el.XFORMATTEDDATESTRING + '', (el.Z_PK + ''), (el.ZTITLE + '').substring(0, 128)])
+      items = [
+        ['Modified', 'ID', 'Title'],
+        ['-', '-', '-'], ...items
+      ]
+      items = normalizeCols(items).map(el => el.join(' | ')).join('\n')
+
+      if (!program.color) {
+        items = stripAnsi(items)
+      }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 40 - 0
tools/reports/photolocations.js

@@ -0,0 +1,40 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'photolocations'
+module.exports.description = 'List all geolocation information for iOS photos (iOS 10+)'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+// Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getPhotoLocationHistory(program.dump)
+    .then((history) => {
+      if (program.dump) {
+        console.log(JSON.stringify(history, null, 4))
+        return
+      }
+
+      var items = history.map(el => [
+        el.XFORMATTEDDATESTRING + '' || '',
+        el.ZLATITUDE + '' || '',
+        el.ZLONGITUDE + '' || '',
+        el.ZFILENAME + '' || ''
+      ])
+
+      items = [['Time', 'Latitude', 'Longitude', 'Photo Name'], ['-', '-', '-'], ...items]
+      items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 54 - 0
tools/reports/voicemail-files.js

@@ -0,0 +1,54 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+const path = require('path')
+const fs = require('fs-extra')
+
+module.exports.name = 'voicemail-files'
+module.exports.description = 'List all or extract voicemail files (iOS 10+)'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+// Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getVoicemailFileList()
+    .then((list) => {
+      if (program.dump) {
+        console.log(JSON.stringify(list, null, 4))
+        return
+      }
+
+      if (program.extract) {
+        for (var item of list) {
+          try {
+            var outDir = path.join(program.extract, path.basename(item.relativePath))
+            fs.ensureDirSync(path.dirname(outDir))
+            fs.createReadStream(backup.getFileName(item.fileID)).pipe(fs.createWriteStream(outDir))
+            item.output_dir = outDir
+          } catch (e) {
+            console.log(`Couldn't Export: ${item.relativePath}`, e)
+          }
+        }
+      }
+
+      var items = list.map(el => [
+        el.fileID + '',
+        el.relativePath,
+        el.output_dir || '<not exported>'
+      ])
+
+      items = [['ID', 'Path', 'Exported Path'], ['-', '-', '-'], ...items]
+      items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 44 - 0
tools/reports/voicemail.js

@@ -0,0 +1,44 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'voicemail'
+module.exports.description = 'List all or extract voicemails on device'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+// Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getVoicemailsList()
+    .then((items) => {
+      if (program.dump) {
+        console.log(JSON.stringify(items, null, 4))
+        return
+      }
+
+      items = items.map(el => [
+        el.ROWID + '',
+        el.XFORMATTEDDATESTRING,
+        el.sender + '',
+        el.token + '',
+        el.duration + '',
+        el.expiration + '',
+        el.trashed_date + '',
+        el.flags + ''
+      ])
+
+      items = [['ID', 'Date', 'Sender', 'Token', 'Duration', 'Expiration', 'Trashed', 'Flags'], ['-', '-', '-', '-', '-', '-', '-', '-'], ...items]
+      items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 40 - 0
tools/reports/webhistory.js

@@ -0,0 +1,40 @@
+const stripAnsi = require('strip-ansi')
+const { URL } = require('url')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'webhistory'
+module.exports.description = 'List all web history'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+// Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getWebHistory(program.dump)
+    .then((history) => {
+      if (program.dump) {
+        console.log(JSON.stringify(history, null, 4))
+        return
+      }
+
+      var items = history.map(el => [
+        el.XFORMATTEDDATESTRING + '' || '',
+        new URL(el.url || '').origin || '',
+        (el.title || '').substring(0, 64)
+      ])
+
+      items = [['Time', 'URL', 'Title'], ['-', '-', '-'], ...items]
+      items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 43 - 0
tools/reports/wifi.js

@@ -0,0 +1,43 @@
+const stripAnsi = require('strip-ansi')
+const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
+const normalizeCols = require('../util/normalize.js')
+
+module.exports.name = 'wifi'
+module.exports.description = 'List associated wifi networks and their usage information'
+
+module.exports.func = function (program, base) {
+  if (!program.backup) {
+    console.log('use -b or --backup <id> to specify backup.')
+    process.exit(1)
+  }
+
+// Grab the backup
+  var backup = iPhoneBackup.fromID(program.backup, base)
+  backup.getWifiList()
+    .then((items) => {
+      if (program.dump) {
+        console.log(JSON.stringify(items, null, 4))
+        return
+      }
+
+      items = items['List of known networks'].map(el => [
+        el.lastJoined + '' || '',
+        el.lastAutoJoined + '' || '',
+        el.SSID_STR + '',
+        el.BSSID + '',
+        el.SecurityMode || '',
+        el.HIDDEN_NETWORK + '',
+        el.enabled + ''
+      ]).sort((a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime())
+
+      items = [['Last Joined', 'Last AutoJoined', 'SSID', 'BSSID', 'Security', 'Hidden', 'Enabled'], ['-', '-', '-', '-', '-', '-'], ...items]
+      items = normalizeCols(items).map(el => el.join(' | ').replace(/\n/g, '')).join('\n')
+
+      if (!program.color) { items = stripAnsi(items) }
+
+      console.log(items)
+    })
+    .catch((e) => {
+      console.log('[!] Encountered an Error:', e)
+    })
+}

+ 146 - 63
tools/util/iphone_backup.js

@@ -3,7 +3,7 @@ const sqlite3 = require('sqlite3')
 const bplist = require('bplist-parser')
 const fs = require('fs')
 const plist = require('plist')
-const tz_offset = 5;
+const tz_offset = 5
 
 const databases = {
   SMS: '3d0d7e5fb2ce288813306e4d4636395e047a3d28',
@@ -24,18 +24,18 @@ const databases = {
 var cache = {}
 
 class iPhoneBackup {
-  constructor(id, status, info, manifest) {
-    this.id = id;
-    this.status = status;
-    this.info = info;
-    this.manifest = manifest;
+  constructor (id, status, info, manifest) {
+    this.id = id
+    this.status = status
+    this.info = info
+    this.manifest = manifest
   }
 
   // Open a backup with a specified ID
   // base is optional and will be computed if not used.
-  static fromID(id, base) {
+  static fromID (id, base) {
     // Get the path of the folder.
-    if(base) {
+    if (base) {
       base = path.join(base, id)
     } else {
       base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', id)
@@ -43,17 +43,17 @@ class iPhoneBackup {
 
     // Parse manifest bplist files
     try {
-      var status = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Status.plist')))[0];
+      var status = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Status.plist')))[0]
     } catch (e) {
       console.log('Cannot open Status.plist', e)
     }
     try {
-      var manifest = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Manifest.plist')))[0];
+      var manifest = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Manifest.plist')))[0]
     } catch (e) {
       console.log('Cannot open Manifest.plist', e)
     }
     try {
-      var info = plist.parse(fs.readFileSync(path.join(base, 'Info.plist'), 'utf8'));
+      var info = plist.parse(fs.readFileSync(path.join(base, 'Info.plist'), 'utf8'))
     } catch (e) {
       console.log('Cannot open Info.plist', e)
     }
@@ -88,18 +88,17 @@ class iPhoneBackup {
     }
   }
 
-  getName(messageDest) {
-    
+  getName (messageDest) {
     return new Promise((resolve, reject) => {
-      if(messageDest.indexOf('@') === -1) {
+      if (messageDest.indexOf('@') === -1) {
         messageDest = messageDest.replace(/[\s+\-()]*/g, '')
-        if(messageDest.length == 11 && messageDest[0] == '1') {
+        if (messageDest.length == 11 && messageDest[0] == '1') {
           messageDest = messageDest.substring(1)
         }
       }
 
-      if(cache[messageDest] !== undefined) {
-       return resolve(cache[messageDest])
+      if (cache[messageDest] !== undefined) {
+        return resolve(cache[messageDest])
       }
 
       var contactdb = this.getDatabase(databases.Contacts)
@@ -109,29 +108,70 @@ class iPhoneBackup {
         c1Last as last,
         c2Middle as middle,
         c15Phone as phones
-        from ABPersonFullTextSearch_content WHERE c15Phone like '%${messageDest}%'`, 
+        from ABPersonFullTextSearch_content WHERE c15Phone like '%${messageDest}%'`,
       (err, row) => {
-          if(err) return resolve()
-          if(!row) return resolve()
+        if (err) return resolve()
+        if (!row) return resolve()
 
-          var result = {
+        var result = {
           name: [row.first, row.middle, row.last].filter(el => el != null).join(' '),
           phones: row.phones.split(' '),
           query: messageDest
         }
 
-        if(row) cache[messageDest] = result
+        if (row) cache[messageDest] = result
 
         resolve(result)
       })
     })
   }
 
-  getMessages(chat_id, dumpAll) {
-    var backup = this;
+  getMessagesiOS9 (chatId, dumpAll) {
+    var backup = this
     return new Promise((resolve, reject) => {
       var messagedb = this.getDatabase(databases.SMS)
-      
+
+      messagedb.all(`
+        SELECT 
+          message.*,
+          handle.id as sender_name,
+          datetime(date + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
+        FROM chat_message_join 
+        INNER JOIN message 
+          ON message.rowid = chat_message_join.message_id 
+        INNER JOIN handle
+          ON handle.rowid = message.handle_id
+        WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
+     async function (err, chats) {
+       if (err) return reject(err)
+
+       chats = chats || []
+       if (dumpAll) console.log(JSON.stringify(chats, null, 4))
+
+        // Compute the user's name
+       for (var i in chats) {
+         var el = chats[i]
+         el.x_sender = el.is_from_me ? 'Me' : el.sender_name
+
+         if (!el.is_from_me) {
+           var contact = await backup.getName(el.sender_name)
+
+           if (contact) {
+             el.x_sender = `${contact.name} <${contact.query}>`
+           }
+         }
+       }
+
+       resolve(chats)
+     })
+    })
+  }
+
+  getMessagesiOS10iOS11 (chatId, dumpAll) {
+    var backup = this
+    return new Promise((resolve, reject) => {
+      var messagedb = this.getDatabase(databases.SMS)
+
       messagedb.all(`
         SELECT 
           message.*,
@@ -142,45 +182,55 @@ class iPhoneBackup {
           ON message.rowid = chat_message_join.message_id 
         INNER JOIN handle
           ON handle.rowid = message.handle_id
-        WHERE chat_message_join.chat_id = ?`, [parseInt(chat_id)], 
+        WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
      async function (err, chats) {
-      if(err) return reject(err)
+       if (err) return reject(err)
 
        chats = chats || []
-        if(dumpAll) console.log(JSON.stringify(chats, null, 4))
+       if (dumpAll) console.log(JSON.stringify(chats, null, 4))
 
         // Compute the user's name
-        for(var i in chats) {
-          var el = chats[i]
-          el.x_sender = el.is_from_me ? 'Me' : el.sender_name
+       for (var i in chats) {
+         var el = chats[i]
+         el.x_sender = el.is_from_me ? 'Me' : el.sender_name
 
-          if(!el.is_from_me) {
-            var contact = await backup.getName(el.sender_name)
+         if (!el.is_from_me) {
+           var contact = await backup.getName(el.sender_name)
 
-            if(contact) {
-              el.x_sender = `${contact.name} <${contact.query}>`
-            }
-          }
-        }
+           if (contact) {
+             el.x_sender = `${contact.name} <${contact.query}>`
+           }
+         }
+       }
 
-        resolve(chats)
-      })
+       resolve(chats)
+     })
     })
   }
 
-  getConversations(dumpAll) {
+  getMessages (chatId, dumpAll) {
+    if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
+      return this.getMessagesiOS9(chatId, dumpAll)
+    } else {
+      return this.getMessagesiOS10iOS11(chatId, dumpAll)
+    }
+  }
+
+  getConversationsiOS9 (dumpAll) {
     var backup = this
     return new Promise((resolve, reject) => {
       var messagedb = this.getDatabase(databases.SMS)
 
-      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) {
-        if(err) return reject(err)
+      messagedb.all(`SELECT *  FROM chat ORDER BY ROWID ASC`, async function (err, rows) {
+        if (err) return reject(err)
         rows = rows || []
 
-        for(var el of rows) {
+        // We need to do some manual parsing of these records.
+        // The timestamp information is stored in a binary blob named `properties`
+        // Which is formatted as a binary PLIST.
+        for (var el of rows) {
           if (el.properties) el.properties = bplist.parseBuffer(el.properties)[0]
 
-
           // Interestingly, some of these do not have dates attached.
           if (el.properties) {
             el.date = new Date(el.properties.CKChatWatermarkTime * 1000)
@@ -190,25 +240,60 @@ class iPhoneBackup {
 
           var contact = await backup.getName(el.chat_identifier)
 
-          if(contact) {
+          if (contact) {
             el.display_name = `${contact.name} <${contact.query}>`
           }
+
+          // Format as YY-MM-DD HH:MM:SS
+          try {
+            el.XFORMATTEDDATESTRING = el.date.toISOString()
+              .split('T')
+              .join(' ')
+              .split('Z')
+              .join(' ')
+              .split('.')[0]
+              .trim()
+          } catch (e) {
+            el.XFORMATTEDDATESTRING = ''
+          }
         }
 
+        // Sort by the date.
         rows = rows.sort(function (a, b) {
-          // Turn your strings into dates, and then subtract them
-          // to get a value that is either negative, positive, or zero.
-          return new Date(b.date) - new Date(a.date);
-        });
+          return (a.date.getTime() || 0) - (b.date.getTime() || 0)
+        })
 
-        if(dumpAll) console.log(JSON.stringify(rows, null, 4))
+        if (dumpAll) console.log(JSON.stringify(rows, null, 4))
 
         resolve(rows)
       })
     })
   }
 
-  getFileManifest() {
+  getConversationsiOS10iOS11 (dumpAll) {
+    return new Promise((resolve, reject) => {
+      var messagedb = this.getDatabase(databases.SMS)
+
+      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) {
+        if (err) return reject(err)
+        rows = rows || []
+
+        if (dumpAll) console.log(JSON.stringify(rows, null, 4))
+
+        resolve(rows)
+      })
+    })
+  }
+
+  getConversations (dumpAll) {
+    if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
+      return this.getConversationsiOS9(dumpAll)
+    } else {
+      return this.getConversationsiOS10iOS11(dumpAll)
+    }
+  }
+
+  getFileManifest () {
     return new Promise((resolve, reject) => {
       var messagedb = this.getDatabase('Manifest.db', true)
       messagedb.all('SELECT * from FILES', async function (err, rows) {
@@ -216,37 +301,36 @@ class iPhoneBackup {
 
         resolve(rows)
       })
-    
     })
   }
 
-  getOldNotes() {
+  getOldNotes () {
     return new Promise((resolve, reject) => {
       var messagedb = this.getDatabase(databases.Notes)
       messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZNOTE LEFT JOIN ZNOTEBODY ON ZBODY = ZNOTEBODY.Z_PK`, async function (err, rows) {
         if (err) reject(err)
-        
+
         resolve(rows)
       })
     })
   }
 
-  getNewNotesLegacyiOS9() {
+  getNewNotesiOS9 () {
     return new Promise((resolve, reject) => {
       var messagedb = this.getDatabase(databases.Notes2)
       messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
-        if(err) reject(err)
+        if (err) reject(err)
 
         resolve(rows)
       })
     })
   }
 
-  getNewNotesiOS10_iOS11() {
+  getNewNotesiOS10iOS11 () {
     return new Promise((resolve, reject) => {
       var messagedb = this.getDatabase(databases.Notes2)
       messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, datetime(ZCREATIONDATE1 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING1 FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
-        if(err) reject(err)
+        if (err) reject(err)
 
         resolve(rows)
       })
@@ -257,9 +341,9 @@ class iPhoneBackup {
     if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
       // Legacy iOS 9 support
       // May work for earlier but I haven't tested it
-      return this.getNewNotesLegacyiOS9()
+      return this.getNewNotesiOS9()
     } else {
-      return this.getNewNotesiOS10_iOS11()
+      return this.getNewNotesiOS10iOS11()
     }
   }
 
@@ -325,7 +409,6 @@ class iPhoneBackup {
 
         resolve(rows)
       })
-    
     })
   }
 
@@ -351,4 +434,4 @@ module.exports.availableBackups = function () {
   })
 }
 
-module.exports.iPhoneBackup = iPhoneBackup
+module.exports.iPhoneBackup = iPhoneBackup