Rich Infante před 8 roky
revize
1ce6e6465d
5 změnil soubory, kde provedl 341 přidání a 0 odebrání
  1. 64 0
      .gitignore
  2. 19 0
      Readme.md
  3. 19 0
      package.json
  4. 52 0
      tools/index.js
  5. 187 0
      tools/util/iphone_backup.js

+ 64 - 0
.gitignore

@@ -0,0 +1,64 @@
+.vscode/
+
+####################################################################
+### Node GitIgnore Entries - https://github.com/github/gitignore ###
+####################################################################
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+

+ 19 - 0
Readme.md

@@ -0,0 +1,19 @@
+# iPhone backup tools
+Currently works on macOS, If someone wants to make the changes nessecary for windows, send a PR.
+
+## Installing
+clone this repo, then run:
+```bash
+npm install.
+```
+
+## Usage
+```bash
+npm start
+```
+
+- You should make a backup of the backups you look at using this tool, even though they are opened as read-only, you should still do that do you don't accidentally do something to lose data.
+
+## TODO
+- Contact name lookup for newer iOS10 backups
+- ??

+ 19 - 0
package.json

@@ -0,0 +1,19 @@
+{
+  "name": "iphonebackuptools",
+  "version": "1.0.0",
+  "description": "Read Messages from iOS Backups.",
+  "main": "tools/index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "start" : "node tools/index.js"
+  },
+  "author": "@richinfante",
+  "license": "MIT",
+  "dependencies": {
+    "bplist-parser": "^0.1.1",
+    "chalk": "^1.1.3",
+    "inquirer": "^3.0.6",
+    "plist": "^2.0.1",
+    "sqlite3": "^3.1.8"
+  }
+}

+ 52 - 0
tools/index.js

@@ -0,0 +1,52 @@
+const backup = require('./util/iphone_backup')
+const inquirer = require('inquirer')
+const chalk = require('chalk')
+
+async function main() {
+
+  var backups = await backup.availableBackups()
+
+  var result = await inquirer.prompt({
+    type: 'list',
+    name: 'backupid',
+    message: 'Select Backup:',
+    choices: backups.map(el => {
+      console.log(el)
+      return {
+        name: el.manifest ? 
+          `${el.manifest.Lockdown.DeviceName} <${el.id}> ${el.status ? new Date(el.status.Date).toLocaleString() : ''}` : 
+          `Unknown Device ${el.id} ${el.status ? new Date(el.status.Date).toLocaleString() : ''}`,
+        value: el.id
+      }
+    })
+  })
+  
+  const selectedBackup = backup.iPhoneBackup.fromID(result.backupid)
+  const conversations = await selectedBackup.getConversations()
+
+  var conversation = await inquirer.prompt({
+    type: 'list',
+    name: 'chat_id',
+    message: 'Select Conversation:',
+    choices: conversations.map(el => {
+      return {
+        name: chalk.gray(el.date ? el.date.toLocaleString() : '??') + ' ' + el.display_name + ' ' +  el.chat_identifier,
+        value: el.ROWID
+      }
+    })
+  })
+
+  //console.log(conversation)
+
+  const messages = await selectedBackup.getMessages(conversation.chat_id)
+  console.log(
+    messages.map(el => chalk.gray(el.date ? el.date.toLocaleString() : '') + ' ' + chalk.blue(el.sender + ': ') + el.text)
+    .join('\n')
+  )
+}
+
+try {
+  main()
+} catch (e) {
+  console.log(e)
+}

+ 187 - 0
tools/util/iphone_backup.js

@@ -0,0 +1,187 @@
+const path = require('path')
+const sqlite3 = require('sqlite3')
+const bplist = require('bplist-parser')
+const fs = require('fs')
+const plist = require('plist')
+const tz_offset = 5;
+
+const databases = {
+  SMS: '3d0d7e5fb2ce288813306e4d4636395e047a3d28',
+  Contacts: '31bb7ba8914766d4ba40d6dfb6113c8b614be442',
+  Calendar: '2041457d5fe04d39d0ab481178355df6781e6858',
+  Reminders: '2041457d5fe04d39d0ab481178355df6781e6858',
+  Notes: 'ca3bc056d4da0bbf88b5fb3be254f3b7147e639c',
+  Calls: '2b2b0084a1bc3a5ac8c27afdf14afb42c61a19ca',
+  Locations: '4096c9ec676f2847dc283405900e284a7c815836'
+}
+
+var cache = {}
+
+class iPhoneBackup {
+  constructor(id, status, info, manifest) {
+    this.id = id;
+    this.status = status;
+    this.info = info;
+    this.manifest = manifest;
+  }
+
+
+  static fromID(id) {
+    // Get the path of the folder.
+    const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', id)
+
+    // Parse manifest bplist files
+    try {
+      var status = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Status.plist')))[0];
+    } catch (e) {
+    }
+    try {
+      var manifest = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Manifest.plist')))[0];
+    } catch (e) {
+    }
+    try {
+      var info = plist.parse(fs.readFileSync(path.join(base, 'Info.plist'), 'utf8'));
+    } catch (e) {
+    }
+
+    return new iPhoneBackup(id, status, info, manifest)
+  }
+
+  getDatabase(fileID) {
+    // Get the backup folder
+    const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', this.id)
+
+    // Return v2 filename
+    if (this.status.Version < 3) {
+      return new sqlite3.Database(path.join(base, fileID), sqlite3.OPEN_READONLY)
+    } else {
+      // v3 has folders
+      return new sqlite3.Database(path.join(base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY)
+    }
+  }
+
+  getName(messageDest) {
+
+    return new Promise((resolve, reject) => {
+      if(messageDest.indexOf('@') === -1) {
+        messageDest = messageDest.replace(/[\s+\-()]*/g, '')
+        if(messageDest.length == 11 && messageDest[0] == '1') {
+          messageDest = messageDest.substring(1)
+        }
+      }
+
+      if(cache[messageDest] !== undefined) {
+       return resolve(cache[messageDest])
+      }
+
+      var contactdb = this.getDatabase(databases.Contacts)
+
+      contactdb.get(`SELECT 
+        c0First as first,
+        c1Last as last,
+        c2Middle as middle,
+        c15Phone as phones
+        from ABPersonFullTextSearch_content WHERE c15Phone like '%${messageDest}%'`, 
+      (err, row) => {
+          if(err) return resolve()
+        
+          if(!row) return resolve()
+
+          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
+
+        resolve(result)
+      })
+    })
+  }
+
+  getMessages(chat_id) {
+    var backup = this;
+    return new Promise((resolve, reject) => {
+      var messagedb = this.getDatabase(databases.SMS)
+      
+      messagedb.all(`
+        SELECT 
+          message.*,
+          handle.id as sender_name
+        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(chat_id)], 
+     async function (err, chats) {
+        var offset = new Date('2001-01-01 00:00:00').getTime()
+        
+        for(var i in chats) {
+          var el = chats[i]
+          var date = new Date(offset + el.date * 1000 - tz_offset * 60 * 60 * 1000)
+          var text = el.text
+          var sender = el.is_from_me ? 'Me' : el.sender_name
+
+          if(!el.is_from_me) {
+            var contact = await backup.getName(el.sender_name)
+
+            if(contact) {
+              sender = `${contact.name} <${contact.query}>`
+            }
+          }
+
+          chats[i] = { sender, text, date }
+        }
+
+        resolve(chats)
+      })
+    })
+  }
+
+  getConversations() {
+    var backup = this
+    return new Promise((resolve, reject) => {
+      var messagedb = this.getDatabase(databases.SMS)
+
+      messagedb.all('SELECT * FROM chat', async function (err, rows) {
+        for(var el of rows) {
+          if (el.properties) el.properties = bplist.parseBuffer(el.properties)[0]
+          if (el.properties) {
+            el.date = new Date(el.properties.CKChatWatermarkTime * 1000)
+          } else {
+            el.date = new Date(0)
+          }
+
+          var contact = await backup.getName(el.chat_identifier)
+
+          if(contact) {
+            el.display_name = `${contact.name} <${contact.query}>`
+          }
+        }
+
+        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);
+        });
+
+        resolve(rows)
+      })
+    })
+  }
+}
+
+
+
+module.exports.availableBackups = function () {
+  const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
+  return new Promise((resolve, reject) => {
+    resolve(fs.readdirSync(base, { encoding: 'utf8' })
+      .filter(el => el.length == 40)
+      .map(file => iPhoneBackup.fromID(file)))
+  })
+}
+
+module.exports.iPhoneBackup = iPhoneBackup