# Reeder 3DES-EDE for USM (Internet draft)
# http://www.snmp.com/eso/draft-reeder-snmpv3-usm-3desede-00.txt
import random
from pysnmp.proto.secmod.rfc3414.priv import base
from pysnmp.proto.secmod.rfc3414.auth import hmacmd5, hmacsha
from pysnmp.proto.secmod.rfc3414 import localkey
from pysnmp.proto import errind, error
from pyasn1.type import univ
from pyasn1.compat.octets import null

try:
    from Crypto.Cipher import DES3
except ImportError:
    DES3 = None
    
random.seed()

# 5.1.1

class Des3(base.AbstractEncryptionService):
    serviceID = (1, 3, 6, 1, 6, 3, 10, 1, 2, 3) # usm3DESEDEPrivProtocol
    _localInt = random.randrange(0, 0xffffffff)

    def hashPassphrase(self, authProtocol, privKey):
        if authProtocol == hmacmd5.HmacMd5.serviceID:
            return localkey.hashPassphraseMD5(privKey)
        elif authProtocol == hmacsha.HmacSha.serviceID:
            return localkey.hashPassphraseSHA(privKey)
        else:
            raise error.ProtocolError(
                'Unknown auth protocol %s' % (authProtocol,)
                )
        
    def localizeKey(self, authProtocol, privKey, snmpEngineID):
        if authProtocol == hmacmd5.HmacMd5.serviceID:
            localPrivKey = localkey.localizeKeyMD5(privKey, snmpEngineID)
            localPrivKey = localPrivKey + localkey.localizeKeyMD5(
                localPrivKey, snmpEngineID
                )
        elif authProtocol == hmacsha.HmacSha.serviceID:
            localPrivKey = localkey.localizeKeySHA(privKey, snmpEngineID)
            localPrivKey = localPrivKey + localkey.localizeKeySHA(
                localPrivKey, snmpEngineID
                )
        else:
            raise error.ProtocolError(
                'Unknown auth protocol %s' % (authProtocol,)
                )
        return localPrivKey[:32] # key+IV
        
    # 5.1.1.1
    def __getEncryptionKey(self, privKey, snmpEngineBoots):
        # 5.1.1.1.1
        des3Key = privKey[:24]
        preIV = privKey[24:32]

        securityEngineBoots = int(snmpEngineBoots)

        salt = [
            securityEngineBoots>>24&0xff,
            securityEngineBoots>>16&0xff,
            securityEngineBoots>>8&0xff,
            securityEngineBoots&0xff,
            self._localInt>>24&0xff,
            self._localInt>>16&0xff,
            self._localInt>>8&0xff,
            self._localInt&0xff
            ]
        if self._localInt == 0xffffffff:
            self._localInt = 0
        else:
            self._localInt = self._localInt + 1

        # salt not yet hashed XXX
        
        return des3Key.asOctets(), \
               univ.OctetString(salt).asOctets(), \
               univ.OctetString(map(lambda x,y:x^y, salt, preIV.asNumbers())).asOctets()
    
    def __getDecryptionKey(self, privKey, salt):
        return privKey[:24].asOctets(), \
               univ.OctetString(map(lambda x,y:x^y, salt.asNumbers(), privKey[24:32].asNumbers())).asOctets()

    # 5.1.1.2
    def encryptData(self, encryptKey, privParameters, dataToEncrypt):
        if DES3 is None:
            raise error.StatusInformation(
                errorIndication=errind.encryptionError
                )

        snmpEngineBoots, snmpEngineTime, salt = privParameters
        
        des3Key, salt, iv = self.__getEncryptionKey(
            encryptKey, snmpEngineBoots
            )

        des3Obj = DES3.new(des3Key, DES3.MODE_CBC, iv)
        
        privParameters = univ.OctetString(salt)

        plaintext =  dataToEncrypt + univ.OctetString((0,) * (8 - len(dataToEncrypt) % 8)).asOctets()
        cipherblock = iv
        ciphertext = null
        while plaintext:
            cipherblock = des3Obj.encrypt(
                univ.OctetString(map(lambda x,y:x^y, univ.OctetString(cipherblock).asNumbers(), univ.OctetString(plaintext[:8]).asNumbers())).asOctets()
                )
            ciphertext = ciphertext + cipherblock
            plaintext = plaintext[8:]

        return univ.OctetString(ciphertext), privParameters
        
    # 5.1.1.3
    def decryptData(self, decryptKey, privParameters, encryptedData):
        if DES3 is None:
            raise error.StatusInformation(
                errorIndication=errind.decryptionError
                )
        snmpEngineBoots, snmpEngineTime, salt = privParameters
        
        if len(salt) != 8:
            raise error.StatusInformation(
                errorIndication=errind.decryptionError
                )
            
        des3Key, iv = self.__getDecryptionKey(decryptKey, salt)

        if len(encryptedData) % 8 != 0:
            raise error.StatusInformation(
                errorIndication=errind.decryptionError
                )

        des3Obj = DES3.new(des3Key, DES3.MODE_CBC, iv)

        plaintext = null
        ciphertext = encryptedData.asOctets()
        cipherblock = iv
        while ciphertext:
            plaintext = plaintext + univ.OctetString(map(lambda x,y: x^y, univ.OctetString(cipherblock).asNumbers(), univ.OctetString(des3Obj.decrypt(ciphertext[:8])).asNumbers())).asOctets()
            cipherblock = ciphertext[:8]
            ciphertext = ciphertext[8:]

        return plaintext
