from crypto.PBKDF2 import PBKDF2
from crypto.aes import AESdecryptCBC
from crypto.aeswrap import AESUnwrap
from crypto.aeswrap import AESwrap
from crypto.curve25519 import curve25519
from hashlib import sha256, sha1
from util.bplist import BPlistReader
from util.tlv import loopTLVBlocks, tlvToDict
import hmac
import struct

KEYBAG_TAGS = ["VERS", "TYPE", "UUID", "HMCK", "WRAP", "SALT", "ITER"]
CLASSKEY_TAGS = ["CLAS","WRAP","WPKY", "KTYP", "PBKY"]  #UUID
KEYBAG_TYPES = ["System", "Backup", "Escrow", "OTA (icloud)"]
SYSTEM_KEYBAG = 0
BACKUP_KEYBAG = 1
ESCROW_KEYBAG = 2
OTA_KEYBAG = 3

#ORed flags in TYPE since iOS 5
FLAG_UIDPLUS = 0x40000000   # UIDPlus hardware key (>= iPad 3)
FLAG_UNKNOWN = 0x80000000

WRAP_DEVICE = 1
WRAP_PASSCODE = 2

KEY_TYPES = ["AES", "Curve25519"]
PROTECTION_CLASSES={
    1:"NSFileProtectionComplete",
    2:"NSFileProtectionCompleteUnlessOpen",
    3:"NSFileProtectionCompleteUntilFirstUserAuthentication",
    4:"NSFileProtectionNone",
    5:"NSFileProtectionRecovery?",

    6: "kSecAttrAccessibleWhenUnlocked",
    7: "kSecAttrAccessibleAfterFirstUnlock",
    8: "kSecAttrAccessibleAlways",
    9: "kSecAttrAccessibleWhenUnlockedThisDeviceOnly",
    10: "kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly",
    11: "kSecAttrAccessibleAlwaysThisDeviceOnly"
}

"""
    device key : key 0x835
"""
class Keybag(object):
    def __init__(self, data):
        self.type = None
        self.uuid = None
        self.wrap = None
        self.deviceKey = None
        self.unlocked = False
        self.passcodeComplexity = 0
        self.attrs = {}
        self.classKeys = {}
        self.KeyBagKeys = None #DATASIGN blob
        self.parseBinaryBlob(data)

    @staticmethod
    def getSystemkbfileWipeID(filename):
        mkb = BPlistReader.plistWithFile(filename)
        return mkb["_MKBWIPEID"]

    @staticmethod
    def createWithPlist(pldict):
        k835 = pldict.key835.decode("hex")
        data = ""
        if pldict.has_key("KeyBagKeys"):
            data = pldict["KeyBagKeys"].data
        else:
            data = ""
        keybag = Keybag.createWithDataSignBlob(data, k835)

        if pldict.has_key("passcodeKey"):
            if keybag.unlockWithPasscodeKey(pldict["passcodeKey"].decode("hex")):
                print "Keybag unlocked with passcode key"
            else:
                print "FAILed to unlock keybag with passcode key"
        #HAX: inject DKey
        keybag.setDKey(pldict)
        return keybag
    
    def setDKey(self, device_infos):
        self.classKeys[4] = {"CLAS": 4, "KEY": device_infos["DKey"].decode("hex")}
        
    @staticmethod
    def createWithSystemkbfile(filename, bag1key, deviceKey=None):
        if filename.startswith("bplist"): #HAX
            mkb = BPlistReader.plistWithString(filename)
        else:
            mkb = BPlistReader.plistWithFile(filename)
        try:
            decryptedPlist  = AESdecryptCBC(mkb["_MKBPAYLOAD"].data, bag1key, mkb["_MKBIV"].data, padding=True)
        except:
            print "FAIL: AESdecryptCBC _MKBPAYLOAD => wrong BAG1 key ?"
            return None
        if not decryptedPlist.startswith("bplist"):
            print "FAIL: decrypted _MKBPAYLOAD is not bplist"
            return None
        decryptedPlist = BPlistReader.plistWithString(decryptedPlist)
        blob = decryptedPlist["KeyBagKeys"].data
        kb = Keybag.createWithDataSignBlob(blob, deviceKey)
        if decryptedPlist.has_key("OpaqueStuff"):
            OpaqueStuff = BPlistReader.plistWithString(decryptedPlist["OpaqueStuff"].data)
            kb.passcodeComplexity = OpaqueStuff.get("keyboardType")
        return kb

    
    @staticmethod
    def createWithDataSignBlob(blob, deviceKey=None):
        keybag = tlvToDict(blob)
        
        kb = Keybag(keybag.get("DATA", ""))
        kb.deviceKey = deviceKey
        kb.KeyBagKeys = blob
        kb.unlockAlwaysAccessible()
        
        if len(keybag.get("SIGN", "")):
            hmackey = AESUnwrap(deviceKey, kb.attrs["HMCK"])
            #hmac key and data are swapped (on purpose or by mistake ?)
            sigcheck = hmac.new(key=keybag["DATA"], msg=hmackey, digestmod=sha1).digest()
            #fixed in ios 7
            if kb.attrs["VERS"] >= 4:
                sigcheck = hmac.new(key=hmackey, msg=keybag["DATA"], digestmod=sha1).digest()
            if sigcheck != keybag.get("SIGN", ""):
                print "Keybag: SIGN check FAIL"
        return kb
    
    @staticmethod
    def createWithBackupManifest(manifest, password, deviceKey=None):
        kb = Keybag(manifest["BackupKeyBag"].data)
        kb.deviceKey = deviceKey
        if not kb.unlockBackupKeybagWithPasscode(password):
            print "Cannot decrypt backup keybag. Wrong password ?"
            return
        return kb
    
    def isBackupKeybag(self):
        return self.type == BACKUP_KEYBAG
    
    def parseBinaryBlob(self, data):
        currentClassKey = None
        
        for tag, data in loopTLVBlocks(data):
            if len(data) == 4:
                data = struct.unpack(">L", data)[0]
            if tag == "TYPE":
                self.type = data & 0x3FFFFFFF #ignore the flags
                if self.type > 3:
                    print "FAIL: keybag type > 3 : %d" % self.type
            elif tag == "UUID" and self.uuid is None:
                self.uuid = data
            elif tag == "WRAP" and self.wrap is None:
                self.wrap = data
            elif tag == "UUID":
                if currentClassKey:
                    self.classKeys[currentClassKey["CLAS"]] = currentClassKey
                currentClassKey = {"UUID": data}
            elif tag in CLASSKEY_TAGS:
                currentClassKey[tag] = data
            else:
                self.attrs[tag] = data
        if currentClassKey:
            self.classKeys[currentClassKey["CLAS"]] = currentClassKey

    def getPasscodekeyFromPasscode(self, passcode):
        if self.type == BACKUP_KEYBAG or self.type == OTA_KEYBAG:
            return PBKDF2(passcode, self.attrs["SALT"], iterations=self.attrs["ITER"]).read(32)
        else:
            #Warning, need to run derivation on device with this result
            return PBKDF2(passcode, self.attrs["SALT"], iterations=1).read(32)
    
    def unlockBackupKeybagWithPasscode(self, passcode):
        if self.type != BACKUP_KEYBAG and self.type != OTA_KEYBAG:
            print "unlockBackupKeybagWithPasscode: not a backup keybag"
            return False
        return self.unlockWithPasscodeKey(self.getPasscodekeyFromPasscode(passcode))

    def unlockAlwaysAccessible(self):
        for classkey in self.classKeys.values():
            k = classkey["WPKY"]
            if classkey["WRAP"] ==  WRAP_DEVICE:
                if not self.deviceKey:
                    continue
                k = AESdecryptCBC(k, self.deviceKey)
                classkey["KEY"] = k
        return True
                 
    def unlockWithPasscodeKey(self, passcodekey):
        if self.type != BACKUP_KEYBAG and self.type != OTA_KEYBAG:
            if not self.deviceKey:
                print "ERROR, need device key to unlock keybag"
                return False

        for classkey in self.classKeys.values():
            if not classkey.has_key("WPKY"):
                continue
            k = classkey["WPKY"]
            if classkey["WRAP"] & WRAP_PASSCODE:
                k = AESUnwrap(passcodekey, classkey["WPKY"])
                if not k:
                    return False
            if classkey["WRAP"] & WRAP_DEVICE:
                if not self.deviceKey:
                    continue
                k = AESdecryptCBC(k, self.deviceKey)
            classkey["KEY"] = k
        self.unlocked =  True
        return True

    def unwrapCurve25519(self, persistent_class, persistent_key):
        assert len(persistent_key) == 0x48
        #assert persistent_class == 2    #NSFileProtectionCompleteUnlessOpen
        mysecret = self.classKeys[persistent_class]["KEY"]
        mypublic = self.classKeys[persistent_class]["PBKY"]
        hispublic = persistent_key[:32]
        shared = curve25519(mysecret, hispublic)
        md = sha256('\x00\x00\x00\x01' + shared + hispublic + mypublic).digest()
        return AESUnwrap(md, persistent_key[32:])

    def unwrapKeyForClass(self, clas, persistent_key, printError=True):
        if not self.classKeys.has_key(clas) or not self.classKeys[clas].has_key("KEY"):
            if printError: print "Keybag key %d missing or locked" % clas
            return ""
        ck = self.classKeys[clas]["KEY"]
        #if self.attrs.get("VERS", 2) >= 3 and clas == 2:
        if self.attrs.get("VERS", 2) >= 3 and self.classKeys[clas].get("KTYP", 0) == 1:
            return self.unwrapCurve25519(clas, persistent_key)
        if len(persistent_key) == 0x28:
            return AESUnwrap(ck, persistent_key)
        return

    def wrapKeyForClass(self, clas, persistent_key):
        if not self.classKeys.has_key(clas) or not self.classKeys[clas].has_key("KEY"):
            print "Keybag key %d missing or locked" % clas
            return ""
        ck = self.classKeys[clas]["KEY"]
        return AESwrap(ck, persistent_key)
    
    def printClassKeys(self):
        print "Keybag type : %s keybag (%d)" % (KEYBAG_TYPES[self.type], self.type)
        print "Keybag version : %d" % self.attrs["VERS"]
        print "Keybag UUID : %s" % self.uuid.encode("hex")
        print "-"*128
        print "".join(["Class".ljust(53),
                      "WRAP".ljust(5),
                      "Type".ljust(11),
                      "Key".ljust(65),
                      "Public key"])
        print "-"*128
        for k, ck in self.classKeys.items():
            if k == 6: print ""
            print "".join([PROTECTION_CLASSES.get(k).ljust(53),
                                          str(ck.get("WRAP","")).ljust(5),
                                          KEY_TYPES[ck.get("KTYP",0)].ljust(11),
                                          ck.get("KEY", "").encode("hex").ljust(65),
                                          ck.get("PBKY", "").encode("hex")])
        print ""

    def getClearClassKeysDict(self):
        if self.unlocked:
            d = {}
            for ck in self.classKeys.values():
                d["%d" % ck["CLAS"]] = ck.get("KEY","").encode("hex")
            return d
