#!/usr/bin/env python # Written by Dhiru Kholia in July 2012 for JtR project. # Copyright (c) 2012-2013, Dhiru Kholia. # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # Code borrowed from https://github.com/Roguelazer/onepasswordpy # # Copyright (c) 2013, James Brown # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # Code borrowed from https://bitbucket.org/gwik/agilekeychain # # Copyright (c) 2009 Antonin Amand # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose and without fee is hereby granted, # provided that the above copyright notice appear in all copies and that # both that copyright notice and this permission notice appear in # supporting documentation. # # THE AUTHOR PROVIDES THIS SOFTWARE 'AS IS' AND ANY EXPRESSED OR IMPLIED # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # References, # # https://support.1password.com/agile-keychain-design/ # https://support.1password.com/opvault-design/ (formerly called "Cloud Keychain") # # August 2017 notes, # # In December 2012, AgileBits introduced the OPVault format to replace the # Agile Keychain format, which had been introduced in 2008. OPVault format is # used by default when syncing with iCloud or Dropbox. # # December 2017 notes, # # Thanks goes to Matt Page and philsmd for figuring out the details related to # 1Password 6 for macOS. # # https://github.com/mpage/onepassword/blob/master/vault.go has some new # "attack vectors" as well. # # https://github.com/mpage/onepassword/blob/master/notes.md is useful too. # # Agile Keychain format uses encryptionKeys.js file. Cloud Keychain (and # OPVault) format uses profile.js file. However, 1Password 6 (and 1Password 4) # for macOS stores data in .sqlite files. # # $ ls -l ~/Library/Application\ Support/1Password\ 4/Data/ # macOS 10.13.2 + 1Password 6.8.5 # -rw-r--r-- 1 lulu staff 4096 Dec 29 15:29 B5.sqlite # -rw-r--r-- 1 lulu staff 4096 Dec 29 15:29 OnePassword.sqlite # # From https://blog.elcomsoft.com/2017/08/attacking-the-1password-master-password-follow-up/, # # B5.sqlite -> this database is used in macOS for synchronizing with 1Password # proprietary cloud service. # # We can only extract the "hash" from the OnePassword.sqlite file (and not the # B5.sqlite file) at the moment. # # Please note that the "profiles" table in OnePassword.sqlite can be empty, # unless the "Local Vaults" option under 1Password 6 -> Preferences -> Advanced # is selected. import os import sys import struct import base64 import sqlite3 PY3 = sys.version_info[0] == 3 if not PY3: reload(sys) sys.setdefaultencoding('utf8') try: import json assert json except ImportError: try: import simplejson as json except ImportError: sys.stderr.write("Please install json / simplejson module which is currently not installed.\n") sys.exit(1) from base64 import b64decode import binascii OPDATA1_MINIMUM_SIZE = 80 DEFAULT_PBKDF_ITERATIONS = 1000 MINIMUM_PBKDF_ITERATIONS = 1000 A_AES_SIZE = 128 C_AES_SIZE = 256 KEY_SIZE = { 128: 16, 192: 24, 256: 32, } INITIAL_KEY_OFFSET = 12 PY3 = sys.version_info[0] == 3 PMV = sys.version_info[1] >= 6 class Key(object): """ A Key in the keyring """ if PY3 or PMV: exec("SALTED_PREFIX=b'Salted__'") else: SALTED_PREFIX = 'Salted__' ZERO_IV = "\0" * 16 ITERATIONS = 1000 BLOCK_SIZE = 16 Nr = 14 Nb = 4 Nk = 8 def __init__(self, identifier, level, data, validation, iterations): """ initialize key """ self.identifier = identifier self.level = level self.validation = validation bin_data = data if self.__is_salted(bin_data): self.salt = bin_data[8:16] self.data = bin_data[16:] else: self.salt = self.ZERO_IV self.data = bin_data self.iterations = iterations def __is_salted(self, data): return self.SALTED_PREFIX == data[:len(self.SALTED_PREFIX)] def opdata1_unpack(data): HEADER_LENGTH = 8 TOTAL_HEADER_LENGTH = 32 HMAC_LENGTH = 32 if data[:HEADER_LENGTH] != b"opdata01": data = base64.b64decode(data) if PY3 or PMV: MAGIC = b"opdata01" else: MAGIC = "opdata01" if data[:HEADER_LENGTH] != MAGIC: raise TypeError("expected opdata1 format message") plaintext_length, iv = struct.unpack("' % (self.__module__, self.path) def __open_keys_file(self): try: keys_file_path = \ os.path.join(self.path, 'default', 'profile.js') if os.path.exists(keys_file_path): self.processed = True else: return f = open(keys_file_path, 'rb') ds = f.read()[INITIAL_KEY_OFFSET:-1] data = json.loads(ds) salt = base64.b64decode(data['salt']) masterKey = base64.b64decode(data['masterKey']) sys.stdout.write("$cloudkeychain$%s$%s$%s$%s$%s" % (len(salt), binascii.hexlify(salt).decode("ascii"), data["iterations"], len(masterKey), binascii.hexlify(masterKey).decode("ascii"))) plaintext_length, iv, cryptext, expected_hmac, hmac_d_data = \ opdata1_unpack(data['masterKey']) sys.stdout.write("$%s$%s$%s$%s$%s$%s$%s$%s$%s\n" % (plaintext_length, len(iv), binascii.hexlify(iv).decode("ascii"), len(cryptext), binascii.hexlify(cryptext).decode("ascii"), len(expected_hmac), binascii.hexlify(expected_hmac).decode("ascii"), len(hmac_d_data), binascii.hexlify(hmac_d_data).decode("ascii"))) except (IOError, KeyError, ValueError, TypeError): e = sys.exc_info()[1] sys.stderr.write('Error while opening the keychain, %s\n' % str(e)) class AgileKeychain(object): def __init__(self, path, name='default'): self.path = path self.name = name self.entries = None self.keys = list() ret = self.__open_keys_file() if ret: self.john_output() def __repr__(self): return '<%s.AgileKeychain path="%s">' % (self.__module__, self.path) def __open_keys_file(self): """Open the json file containing the keys for decrypting the real keychain and parse it """ try: keys_file_path = \ os.path.join(self.path, 'data', self.name, 'encryptionKeys.js') keys_file = open(keys_file_path, 'r') try: keys = json.loads(keys_file.read()) self.keys = [] for kd in keys['list']: try: key = Key(kd['identifier'], kd['level'], b64decode(kd['data'][:-1]), b64decode(kd['validation'][:-1]), kd.get('iterations', Key.ITERATIONS)) self.keys.append(key) except TypeError: key = Key(kd['identifier'], kd['level'], b64decode(kd['data']), b64decode(kd['validation']), kd.get('iterations', Key.ITERATIONS)) self.keys.append(key) finally: keys_file.close() except (IOError, KeyError, ValueError, TypeError): e = sys.exc_info()[1] sys.stderr.write('Error while opening the keychain, %s\n' % str(e)) return False return True def john_output(self): sys.stdout.write("%s:$agilekeychain$%s" % (os.path.basename(self.path), len(self.keys))) for i in range(0, len(self.keys)): sys.stdout.write("*%s*%s*%s*%s*%s" % (self.keys[i].iterations, len(self.keys[i].salt), binascii.hexlify(self.keys[i].salt).decode("ascii"), len(self.keys[i].data), binascii.hexlify(self.keys[i].data).decode("ascii"))) sys.stdout.write("\n") def process_sqlite(filename): db = sqlite3.connect(filename) cursor = db.cursor() rows = cursor.execute('SELECT master_key_data, salt, iterations FROM profiles') found = False for row in rows: masterKey, salt, iterations = row sys.stdout.write("$cloudkeychain$%s$%s$%s$%s$%s" % ( len(salt), binascii.hexlify(salt).decode("ascii"), iterations, len(masterKey), binascii.hexlify(masterKey).decode("ascii"))) plaintext_length, iv, cryptext, expected_hmac, hmac_d_data = \ opdata1_unpack(masterKey) sys.stdout.write("$%s$%s$%s$%s$%s$%s$%s$%s$%s\n" % ( plaintext_length, len(iv), binascii.hexlify(iv).decode("ascii"), len(cryptext), binascii.hexlify(cryptext).decode("ascii"), len(expected_hmac), binascii.hexlify(expected_hmac).decode("ascii"), len(hmac_d_data), binascii.hexlify(hmac_d_data).decode("ascii"))) found = True return found def process_file(keychain): found = False if "sqlite" in keychain: # XXX weak hack found = process_sqlite(keychain) if found: return keychainobj = CloudKeychain(keychain) if not keychainobj.processed: keychain = AgileKeychain(keychain) if __name__ == "__main__": if len(sys.argv) < 2: sys.stderr.write("Usage: %s <1Password Agile Keychain(s) / Cloud Keychain(s)> / OnePassword.sqlite\n" % sys.argv[0]) sys.exit(1) for j in range(1, len(sys.argv)): process_file(sys.argv[j])