2012-10-25 7 views
11

Ho un EncryptedCharField personalizzato, che voglio fondamentalmente apparire come un CharField quando si interfaccia l'interfaccia utente, ma prima di archiviare/recuperare nel DB lo crittografa/decrittografa.Come posso creare un campo django criptato che converte i dati quando vengono recuperati dal database?

La custom fields documentation dice:

  1. aggiungere __metaclass__ = models.SubfieldBase
  2. esclusione to_python per convertire i dati da esso è formato grezzo nel formato desiderato
  3. esclusione get_prep_value per convertire il valore prima di memorizzare ot il db.

Quindi pensi che sarebbe abbastanza facile - per 2. decrittografare il valore e 3. basta crittografarlo.

liberamente ispirato a django snippet, e la documentazione questo campo assomiglia:

class EncryptedCharField(models.CharField): 
    """Just like a char field, but encrypts the value before it enters the database, and decrypts it when it 
    retrieves it""" 
    __metaclass__ = models.SubfieldBase 
    def __init__(self, *args, **kwargs): 
    super(EncryptedCharField, self).__init__(*args, **kwargs) 
    cipher_type = kwargs.pop('cipher', 'AES') 
    self.encryptor = Encryptor(cipher_type) 

    def get_prep_value(self, value): 
    return encrypt_if_not_encrypted(value, self.encryptor) 

    def to_python(self, value): 
    return decrypt_if_not_decrypted(value, self.encryptor) 


def encrypt_if_not_encrypted(value, encryptor): 
    if isinstance(value, EncryptedString): 
    return value 
    else: 
    encrypted = encryptor.encrypt(value) 
    return EncryptedString(encrypted) 

def decrypt_if_not_decrypted(value, encryptor): 
    if isinstance(value, DecryptedString): 
    return value 
    else: 
    encrypted = encryptor.decrypt(value) 
    return DecryptedString(encrypted) 


class EncryptedString(str): 
    pass 

class DecryptedString(str): 
    pass 

e l'Encryptor assomiglia:

class Encryptor(object): 
    def __init__(self, cipher_type): 
    imp = __import__('Crypto.Cipher', globals(), locals(), [cipher_type], -1) 
    self.cipher = getattr(imp, cipher_type).new(settings.SECRET_KEY[:32]) 

    def decrypt(self, value): 
    #values should always be encrypted no matter what! 
    #raise an error if tthings may have been tampered with 
    return self.cipher.decrypt(binascii.a2b_hex(str(value))).split('\0')[0] 

    def encrypt(self, value): 
    if value is not None and not isinstance(value, EncryptedString): 
     padding = self.cipher.block_size - len(value) % self.cipher.block_size 
     if padding and padding < self.cipher.block_size: 
     value += "\0" + ''.join([random.choice(string.printable) for index in range(padding-1)]) 
     value = EncryptedString(binascii.b2a_hex(self.cipher.encrypt(value))) 
    return value 

Quando si salva un modello, un errore, stringa di Odd-length, si verifica, come risultato del tentativo di decrittografare una stringa già decrittografata. Quando esegue il debug, appare come to_python che viene chiamato due volte, il primo con il valore crittografato e la seconda volta con il valore decrittografato, ma non in realtà come un tipo Decrittografato, ma come una stringa non elaborata, che causa l'errore. Inoltre, get_prep_value non viene mai chiamato.

Cosa sto sbagliando?

Questo non dovrebbe essere così difficile - qualcun altro pensa che questo codice di campo Django sia scritto molto male, specialmente quando si tratta di campi personalizzati, e non così estensibile? Semplici metodi pre_save e post_fetch sovrascrivibili risolvono facilmente questo problema.

+0

Posso chiedere come hai fatto risolto, perché ho bisogno di fare qualcosa di simile a un 'IntegerField'. Questa è una vecchia domanda, quindi stai ancora usando questa crittografia personalizzata o qualche applicazione di terze parti? – PetarP

risposta

7

Penso che il problema è che to_python viene anche chiamato quando si assegna un valore al campo personalizzato (come parte della convalida può essere, in base a this link). Quindi il problema è quello di distinguere tra chiamate to_python nelle seguenti situazioni:

  1. Quando un valore dal database viene assegnato al campo da Django (Questo è quando si desidera decrittografare il valore)
  2. quando si assegnano manualmente un valore per il campo personalizzato, ad es record.field = valore

Una trucco è possibile utilizzare è quello di aggiungere prefisso o suffisso al Valore stringa e controllare che invece di fare isinstance controllo.

Stavo per scrivere un esempio, ma ho trovato questo (ancora meglio :)).

check BaseEncryptedField: https://github.com/django-extensions/django-extensions/blob/master/django_extensions/db/fields/encrypted.py

Fonte: Django Custom Field: Only run to_python() on values from DB?

+0

Mi piace l'idea di hacking !!!! Sucks dobbiamo hackerarlo a causa di un design scadente, ma lo proverò –

+0

Il problema è che Django usa anche to_python per la validazione ed è per questo che to_python non è buono per la traduzione dove l'input dell'utente è dello stesso tipo di dati del tipo di database cose diverse, ad es entrambi sono dati str, ma db one è criptato e l'input è decodificato. – maulik13

4

Si dovrebbe ignorare lo to_python, come ha fatto lo snippet.

Se si dà un'occhiata alla classe CharField si può vedere che non ha un metodo value_to_string:

Il docs dicono che il metodo to_python deve affrontare tre cose:

  • Un'istanza del tipo corretto
  • Una stringa (ad es. Da un deserializzatore).
  • Qualunque sia il database restituito per il tipo di colonna che si sta utilizzando.

Attualmente si tratta solo del terzo caso.

Un modo per gestire questa situazione è quello di creare una classe speciale per una stringa decriptato:

class DecryptedString(str): 
    pass 

Poi si può rilevare questa classe e gestirlo in to_python():

def to_python(self, value): 
    if isinstance(value, DecryptedString): 
     return value 

    decrypted = self.encrypter.decrypt(encrypted) 
    return DecryptedString(decrypted) 

Ciò impedisce di decrittografia più di una volta.

+0

L'ho provato, ma sembra che venga richiamato quando l'elemento viene salvato, interrompendo l'app in quanto è già un valore decrittografato che sta tentando di decrittografarlo. –

+0

Capito perché il mio get_db_prep_value chiama il metodo base get_db_prep_value, che chiama get_prep_value, e in CharField, questo viene sovrascritto per chiamare a_python causando questo errore –

+0

to_python get chiamato nel metodo del campo base clean() - e per quanto mi riguarda capisci questo succede quando il campo viene convalidato prima di salvare. Quindi non sono sicuro di come ottenere che questo non venga chiamato? –

3

Hai dimenticato di impostare la metaclasse:

class EncryptedCharField(models.CharField): 
    __metaclass__ = models.SubfieldBase 

Il custom fields documentation spiega perché ciò sia necessario.

+0

Questo provoca un comportamento strano, come quando si aggiunge questo, to_python viene sempre chiamato due volte, con il tipo originale che viene perso e si verifica una doppia decrittografia, che genera un errore. –

1

è necessario aggiungere un metodo to_python che si occupa di un certo numero di casi, tra cui il passaggio su un valore già decifrato

(avvertimento: snippet è tagliato dal mio codice - solo per l'illustrazione)

def to_python(self, value): 
    if not value: 
     return 
    if isinstance(value, _Param): #THIS IS THE PASSING-ON CASE 
     return value 
    elif isinstance(value, unicode) and value.startswith('{'): 
     param_dict = str2dict(value) 
    else: 
     try: 
      param_dict = pickle.loads(str(value)) 
     except: 
      raise TypeError('unable to process {}'.format(value)) 
    param_dict['par_type'] = self.par_type 
    classname = '{}_{}'.format(self.par_type, param_dict['rule']) 
    return getattr(get_module(self.par_type), classname)(**param_dict) 

a proposito:

Invece di get_db_prep_value si dovrebbe usare get_prep_value (il primo è per le conversioni specifiche DB - vedi https://docs.djangoproject.com/en/1.4/howto/custom-model-fields/#converting-python-objects-to-query-values)

+0

completato ... ma questo non risolve il problema in quanto non viene mai chiamato. Il codice aggiornato è superiore a –

Problemi correlati