Codici fiscali, GDPR & Python

come anonimizzare un codice fiscale

GDPR
Python
Opendata
Base64
Autore/Autrice

Paolo Volterra

Data di Pubblicazione

20 dicembre 2024

1 obiettivi dello studio

  • anonimizzazione di dati
  • gestione di immagini in formato base64
  • inserire immagini in base64

si può trasformare il codice fiscale in una forma che non sia immediatamente identificabile da altri, ma decifrabile solo dal titolare (ad esempio, attraverso una chiave o un algoritmo noto…)

Con la pseudonimizzazione a chiave privata, attraverso una di queste soluzioni

  • Hashing con Salt Privato
  • Cifratura Simmetrica
  • Sostituzione Parziale
  • Tokenizzazione

si può trasformare il codice fiscale in una forma che non è immediatamente identificabile da altri, ma che può essere decifrata dal titolare (ad esempio, attraverso una chiave o un algoritmo noto solo a lui).

2 Hashing con Salt Privato

L’hashing crea una stringa univoca dal codice fiscale utilizzando un algoritmo crittografico (es. SHA-256). Il salt privato (una stringa univoca aggiunta al dato originale) rende l’hash interpretabile solo dal titolare.

Procedura:

  • Aggiungo un salt univoco noto solo al titolare (es. data di nascita o un valore condiviso).
  • Applico l’algoritmo di hashing.
import hashlib
def anonimizza_cf(codice_fiscale, salt):        
    dato = codice_fiscale + salt        
    hash_object = hashlib.sha256(dato.encode())    
    return hash_object.hexdigest()

2.1 Esempio

  • codice_fiscale = “RSSMRA80A01H501U”
  • salt = “01/01/1980” # Salt privato conosciuto dal titolare
  • hash_cf = anonimizza_cf(codice_fiscale, salt)
  • Pro: Non è possibile risalire al codice fiscale senza il salt.
  • Contro: Il titolare deve conoscere o avere accesso al salt.

3 Cifratura Simmetrica

La cifratura simmetrica utilizza un algoritmo come AES per codificare il codice fiscale. Il titolare può decifrare il codice con una chiave segreta condivisa.

Procedura:

  • Scelgo un algoritmo di cifratura (es. AES).
  • Uso una chiave segreta condivisa (es. una password).
  • Cripto il codice fiscale
  • Fornisco il dato criptato al titolare.
  • Pro: Garantisce riservatezza elevata; solo il titolare con la chiave può decifrare.
  • Contro: Il titolare deve conservare la chiave.
from cryptography.fernet import Fernet

# Genera una chiave
key = Fernet.generate_key()

# Crea un oggetto Fernet con la chiave generata
cipher = Fernet(key)

# Codice fiscale da cifrare
codice_fiscale = "RSSMRA80A01H501U"

# Cifra il codice fiscale
encrypted_cf = cipher.encrypt(codice_fiscale.encode())
print("Codice cifrato:", encrypted_cf)

# Decifra il codice fiscale
decrypted_cf = cipher.decrypt(encrypted_cf).decode()
print("Codice decifrato:", decrypted_cf)
Codice cifrato: b'gAAAAABnrCyDU2Qksz7-wmAr4CXD9Zf1t9cfFphwWiz6fjP0W5-FSlPoDu9xTEkzqOKi8wRYRlRtTmqW_R2X40BvzJCCn9L3EfB5gwxmh7DhrHvnefqcrmw='
Codice decifrato: RSSMRA80A01H501U

3.1 Risultato

Eseguendo il codice, otterrai l’output:

Codice cifrato: una stringa cifrata in base64 (non leggibile direttamente).
Codice decifrato: il valore originale del codice fiscale (RSSMRA80A01H501U).

Note importanti:

Gestione della chiave: La chiave generata (key) deve essere conservata in modo sicuro se hai bisogno di decifrare i dati in futuro.
Sicurezza: Non condividere mai la chiave pubblicamente; è fondamentale per mantenere i dati al sicuro.

4 Sostituzione Parziale

Sostituisco parte del codice fiscale con caratteri generici, mantenendo solo alcune informazioni identificative per il titolare.

4.1 Esempio:

  • Codice fiscale: RSSMRA80A01H501U
  • Anonimizzato: RM80__**U
  • Interpretazione: Mantieni iniziali e anno di nascita per rendere il codice interpretabile dal titolare.
  • Pro: Facile da implementare, leggibile dal titolare.
  • Contro: Non è completamente sicuro contro reidentificazione.
def anonimizza_cf_parziale(codice_fiscale):
    """
    Funzione per anonimizzare parzialmente un codice fiscale.
    Mantiene visibili solo i primi 3 caratteri, i caratteri 6 e 7, 
    e gli ultimi 4 caratteri.
    """
    return f"{codice_fiscale[:3]}***{codice_fiscale[6:8]}******{codice_fiscale[-4:]}"

# Codice fiscale da anonimizzare
codice_fiscale = "RSSMRA80A01H501U"

# Anonimizzazione parziale
cf_anonimizzato = anonimizza_cf_parziale(codice_fiscale)
print("Codice Fiscale Anonimizzato:", cf_anonimizzato)
Codice Fiscale Anonimizzato: RSS***80******501U

Spiegazione:

codice_fiscale[:3]: Prende i primi 3 caratteri.
codice_fiscale[6:8]: Prende i caratteri dalla posizione 6 alla posizione 7 inclusa.
codice_fiscale[-4:]: Prende gli ultimi 4 caratteri.
I caratteri mascherati sono sostituiti da *.

5 Tokenizzazione

Sostituisco il codice fiscale con un identificatore univoco casuale (token). Solo il titolare o un sistema può ricondurre il token al codice originale.

5.1 Esempio:

  • Codice fiscale: RSSMRA80A01H501U
  • Token: ABC12345XYZ
  • Tabella di mapping (conservata separatamente): ABC12345XYZ -> RSSMRA80A01H501U
  • Pro: Nessun dato sensibile è esposto.
  • Contro: Richiede un sistema per mantenere il mapping.
import uuid

def anonimizza_cf_token(codice_fiscale):
    """
    Funzione per anonimizzare un codice fiscale utilizzando un token UUID.
    """
    # Genera un token unico (primi 8 caratteri di un UUID)
    token = str(uuid.uuid4())[:8]
    
    # Crea un mapping tra il token e il codice fiscale
    mapping = {token: codice_fiscale}
    print("Mapping (da salvare):", mapping)
    
    # Ritorna il token
    return token

# Codice fiscale da anonimizzare
codice_fiscale = "RSSMRA80A01H501U"

# Anonimizzazione con token
cf_anonimizzato = anonimizza_cf_token(codice_fiscale)
print("Codice Fiscale Anonimizzato:", cf_anonimizzato)
Mapping (da salvare): {'b7d6f13f': 'RSSMRA80A01H501U'}
Codice Fiscale Anonimizzato: b7d6f13f

5.2 Dettagli:

UUID Token: La funzione genera un identificatore unico (uuid.uuid4) e ne utilizza i primi 8 caratteri come token per rappresentare il codice fiscale.
Mapping: Salva il mapping tra il token e il codice fiscale originale. Questo mapping deve essere conservato in modo sicuro (es. database) per consentire una futura deanonimizzazione.
Ritorno: Il token viene restituito come codice anonimo.

Nota Importante

Il mapping è temporaneo nel codice fornito. Per un'applicazione reale, è essenziale salvare il mapping in un archivio persistente (come un database o un file) per potervi accedere successivamente.
Sicurezza: Considera l'accesso controllato al mapping per prevenire abusi e garantire la privacy degli utenti.

5.3 Quale tecnica scegliere per pubblicare un elenco di codici fiscali??

Dipende dal contesto:

  • Per alta sicurezza: Cifratura simmetrica o hashing con salt.
  • Per facilità d’uso: Sostituzione parziale.
  • Per sistemi centralizzati: Tokenizzazione.

Se il codice fiscale deve essere stampato per il titolare (ad esempio in un report o un documento) occorre considerare contemporaneamente:

  • l’interpretabilità per il titolare
  • la riservatezza

La Sostituzione Parziale mantiene solo alcune parti identificative del codice fiscale, come le iniziali e un suffisso significativo. Questo metodo è semplice e leggibile (anche dagli altri, però).

RSS80*__501U

Il codice fiscale Anonimizzato con Hashing e Salt Privato utilizza l’hashing per creare un codice unico che il titolare può interpretare solo conoscendo il salt.

  • codice_fiscale = “RSSMRA80A01H501U”
  • salt = “01/01/1980” # Il salt può essere condiviso solo con il titolare
  • Codice Fiscale Anonimizzato: 9F4E8D12

La Cifratura Simmetrica (Stampabile e Decifrabile Solo dal Titolare) produce un codice stampabile e leggibile, decifrabile solo con la chiave privata.

  • codice_fiscale = “RSSMRA80A01H501U”
  • Codice Fiscale Anonimizzato: gAAAAABlcO9Z8H
  • Il titolare può decifrare l’intero codice fiscale utilizzando la chiave segreta.

La Tokenizzazione genera un token casuale per anonimizzare il codice fiscale, con un mapping separato conservato nel sistema.

  • codice_fiscale = “RSSMRA80A01H501U”
  • cf_anonimizzato = anonimizza_cf_token(codice_fiscale)
  • print(“Codice Fiscale Anonimizzato:”, cf_anonimizzato)

Output:

  • Mapping (da salvare): {‘1a2b3c4d’: ‘RSSMRA80A01H501U’}
  • Codice Fiscale Anonimizzato: 1a2b3c4d

5.4 Conclusione

  • Leggibile direttamente dal titolare: sostituzione parziale o tokenizzazione.
  • Maggiore sicurezza: hashing con salt o cifratura simmetrica.

5.5 i codici fiscali delle imprese sono dati sensibili?

Il codice fiscale di un’impresa è considerato un dato personale, ma non rientra nella categoria dei dati sensibili.

Secondo il Regolamento Generale sulla Protezione dei Dati (GDPR), i dati personali sono informazioni che identificano o rendono identificabile una persona fisica, come il nome, l’indirizzo o il codice fiscale.

I dati sensibili, invece, includono informazioni che rivelano l’origine razziale o etnica, le opinioni politiche, le convinzioni religiose o filosofiche, l’appartenenza sindacale, dati genetici, dati biometrici, dati relativi alla salute o alla vita sessuale o all’orientamento sessuale di una persona (Commissione Europea)

È importante notare che il codice fiscale di una ditta individuale, essendo associato a una persona giuridica, non è soggetto alle stesse tutele previste per i dati personali delle persone fisiche.

Tuttavia, l’utilizzo del codice fiscale deve avvenire nel rispetto delle normative vigenti in materia di protezione dei dati.

5.6 Come è composto il codice fiscale

Il codice fiscale italiano è composto da 16 caratteri alfanumerici, che rappresentano una combinazione di lettere e numeri, e viene calcolato sulla base dei dati anagrafici del soggetto. La sua struttura è la seguente:

5.7 1. Cognome (3 lettere)

  • Le prime tre lettere sono prese dal cognome:
  • Vengono usate le prime tre consonanti.
  • Se il cognome ha meno di tre consonanti, si usano le vocali per arrivare a tre lettere.
  • Se il cognome ha meno di tre lettere, si aggiunge la lettera “X”.

5.8 2. Nome (3 lettere)

  • Le successive tre lettere derivano dal nome:
  • Si prendono le prime tre consonanti.
  • Se il nome ha più di tre consonanti, si prendono la prima, la terza e la quarta.
  • Se il nome ha meno di tre consonanti, si completano con le vocali.
  • Anche in questo caso, se il nome ha meno di tre lettere, si aggiunge la “X”.

5.9 3. Data di nascita e sesso (6 caratteri)

  • Anno di nascita (2 cifre): le ultime due cifre dell’anno (es. 1990 → 90).
  • Mese di nascita (1 lettera): ogni mese è rappresentato da una lettera:
  • Gennaio: A, Febbraio: B, Marzo: C, Aprile: D, Maggio: E, Giugno: H, Luglio: L, Agosto: M, Settembre: P, Ottobre: R, Novembre: S, Dicembre: T.
  • Giorno di nascita e sesso (2 cifre):
  • Per gli uomini si usa il giorno di nascita (es. 05 → 05).
  • Per le donne, al giorno si aggiunge 40 (es. 05 → 45).

5.10 4. Comune o Stato estero di nascita (4 caratteri)

  • È rappresentato da un codice numerico o alfanumerico, assegnato a ogni comune italiano o stato estero.

5.11 5. Carattere di controllo (1 carattere)

  • L’ultimo carattere è un carattere di controllo, calcolato secondo un algoritmo basato su tutti i caratteri precedenti. Serve a verificare la correttezza del codice fiscale.

5.12 Esempio

Una persona nata il 15 marzo 1990 a Roma, di sesso femminile, con cognome “Rossi” e nome “Maria”:

  • Cognome: ROS → ROS
  • Nome: MAR → MAR
  • Anno di nascita: 90
  • Mese: C (marzo)
  • Giorno e sesso: 55 (15 + 40)
  • Comune: H501 (Roma)
  • Codice di controllo: calcolato come “Z” (esempio).

Codice fiscale: ROSMAR90C55H501Z

5.13 La pseudoanomizzazione di R**M********501Z

Il punto 4 è una parte “delicata”: i codici dei comuni sono univoci; non sono univoci se si rimuove la lettera iniziale e si considerano solo le cifre.

I codici dei comuni italiani sono composti da una lettera (che identifica la provincia) seguita da un numero a tre cifre (che identifica il comune all’interno della provincia).

Il codice fiscale contiene il codice catastale (noto anche come codice Belfiore) del luogo di nascita

La combinazione completa (es. “H501” per Roma) è univoca, ma se si considera solo la parte numerica (es. “501”), ci potrebbero essere più comuni con lo stesso numero in province diverse:

  • Roma ha il codice H501.
  • Torino ha il codice L501.

Se togliamo la lettera, rimarrebbe solo 501, che non è più sufficiente per distinguere i comuni.

5.14 L’hash di una parola è sempre lo stesso?

Se una parola non l’abbiamo mai dichiarata in chiaro, l’hash soprebbe essere inattaccabile. Ma se da qualche altra parte la parola è stata pubblicata, abbiamo un problema. Mi spiego: “df37f9bd1129720eb09c8fb2c70201c58612e5546cba99235cba4d6af8072b00” è l’hash SHA-256 univoco di “roSSi” e differisce da tutte le altre eventuali combinazioni:

Parola SHA-256
ROSSI c3ae8397a9b549bcb3f59dac761585227ee3ee6320e4719957a844e35a4ff50e
rossi 30cc6dd8ef8458e679e13ae3bf3f634cace9810e2eea03318b16e011385f93b8
RosSi 92272e94fa44b8a6eb53d63528b75bf6e7d43f30e2143b2f6523f3c2f94e2b6f
roSSi df37f9bd1129720eb09c8fb2c70201c58612e5546cba99609769e57c03d09a58
ROSSi 7e88d9c1e14ea00a4e760122d890854d7a7cfd8c95cb3dd85d48d5c9c2536eaa

Se non altero la parola iniziale ad es con maiuscole/minuscole, punteggiatua iniziale/finale, numeri al posto delle lettere, avrò sempre lo stesso hash - L’hash di una parola è sempre lo stesso se uso lo stesso algoritmo e la stessa parola in input - Se cambio anche solo un carattere (anche la maiuscola/minuscola), l’hash cambia completamente

Se applico una funzione di hash all’intero record (ad esempio "ROSSI|100|" e "ROSSI|200|"), perdo però la possibilità di fare analisi esplorativa dei dati #EDA sulla prima colonna. Questo perché l’hash trasforma tutto il record in un valore unico e non permette di ricostruire le singole parti.

5.14.1 Soluzioni possibili

  1. Hash solo della seconda colonna
    • Mantengola prima colonna in chiaro per l’EDA e applico l’hash solo alle altre colonne

    • Esempio:

      | Nome  | Valore Hashato |
      |-------|--------------------------------------------------|
      | ROSSI | dffb7c5d43b1a469f7d1a93c3d4e72af68d83a5b3f401cd7 |
      | ROSSI | a2ef4857c9d2c8b67e2f3a5dbf2b6c1d4e8a9e7c3b01f9a2 |
  2. Tokenizzazione delle colonne separata
    • Hash ogni colonna separatamente e poi unisci gli hash.

    • Ad esempio, invece di hashare "ROSSI|100|", faccio

      hash1 = hashlib.sha256("ROSSI".encode()).hexdigest()
      hash2 = hashlib.sha256("100".encode()).hexdigest()
      record_hash = f"{hash1}|{hash2}"

      Così posso ancora analizzare la distribuzione dei nomi.

  3. Usare una funzione hash reversibile per la prima colonna
    • Se la privacy non è un problema critico, potrei usare una funzione di codifica invece di un hash per la prima colonna (come Base64 o un ID numerico anonimo).
  4. Hash parziale
    • Se voglio anonimizzare ma mantenere qualche analisi, posso troncare l’hash (ad esempio, usare solo i primi 8 caratteri).

Se l’obiettivo è proteggere i dati mantenendo l’analizzabilità, la scelta migliore è hashare solo le colonne sensibili e lasciare le chiavi utili per l’EDA in chiaro.

Es: una tabella con 10 colonne, ma solo 5 contengono dati sensibili

La soluzione migliore per mantenere la possibilità di fare #EDA è hashare solo le colonne critiche lasciando in chiaro quelle che servono per l’analisi.

5.14.2 Soluzione consigliata: Hashare solo le colonne sensibili

  1. Mantieni in chiaro le colonne non sensibili
    • Es. Categoria, Data, Regione, Prodotto, Quantità
  2. Hasha le colonne sensibili con SHA-256
    • Es. Nome, Codice fiscale, Email, Telefono, Indirizzo
  3. Mantener gli hash separati per ogni colonna invece di fare un hash dell’intero record.
    • Così si può ancora fare ricerche e raggruppamenti sulle colonne non sensibili.

5.14.3 Esempio di implementazione in Python

import pandas as pd
import hashlib

# Simuliamo un dataset con 10 colonne, di cui 5 sensibili

df = pd.DataFrame({
    "ID": [1, 2, 3],
    "Nome": ["Mario Rossi", "Luca Bianchi", "Giulia Verdi"],
    "Codice Fiscale": ["RSSMRA80A01H501Z", "BNCLCU85B12F205X", "VRDGLI90C20H501Z"],
    "Email": ["mario@mail.com", "luca@mail.com", "giulia@mail.com"],
    "Telefono": ["3331234567", "3209876543", "3921112233"],
    "Indirizzo": ["Via Roma 1", "Piazza Duomo 2", "Viale Marconi 3"],
    "Categoria": ["A", "B", "A"],
    "Regione": ["Lombardia", "Lazio", "Toscana"],
    "Prodotto": ["PC", "Tablet", "Smartphone"],
    "Quantità": [10, 5, 8]
})


# Funzione per hashare solo le colonne critiche
def hash_column(value):
    return hashlib.sha256(value.encode()).hexdigest() if pd.notna(value) else value

# Colonne critiche da anonimizzare
columns_to_hash = ["Nome", "Codice Fiscale", "Email", "Telefono", "Indirizzo"]

# Applichiamo l'hashing solo alle colonne sensibili
for col in columns_to_hash:
    df[col] = df[col].astype(str).apply(hash_column)

# Mostrare il dataframe anonimo
import ace_tools as tools
tools.display_dataframe_to_user(name="Tabella anonimizzata", dataframe=df)

5.14.4 Vantaggi di questo approccio

EDA è ancora possibile su Categoria, Regione, Prodotto, Quantità
Non si può risalire ai dati originali senza conoscere i valori iniziali
Raggruppamenti e analisi sono ancora fattibili sulle colonne in chiaro
Privacy garantita perché le informazioni sensibili sono criptate

5.14.5 Se devo anche unire dati tra tabelle (Join)

Se devo fare join tra dataset e le colonne sensibili sono chiavi, uso sempre lo stesso algoritmo di hash, così l’unione sarà ancora possibile.

Ad esempio, se in due tabelle compare la colonna Codice Fiscale, hasho sempre nello stesso modo, così posso ancora unirle.

Ci sono diversi modi di fare hashing, a seconda dell’algoritmo e dell’uso specifico. Ecco una panoramica delle principali categorie e algoritmi:


6 Funzioni di hash crittografiche 🔒

Queste funzioni sono progettate per essere sicure e resistenti alle collisioni.

Algoritmo Output (bit) Velocità Sicurezza
MD5 128 Veloce 🚀 Non sicuro ❌ (facili collisioni)
SHA-1 160 Medio ⚡ Obsoleto ❌ (collisioni trovate)
SHA-256 256 Lento ⏳ Sicuro ✅
SHA-512 512 Più lento 🐌 Molto sicuro ✅✅
BLAKE2 Variabile Veloce 🚀 Sicuro ✅
SHA-3 Variabile Medio ⚡ Alternativa moderna ✅

6.1 📌 Quando usarli?

  • SHA-256 o SHA-512 per anonimizzare dati
  • BLAKE2 per efficienza
  • MD5/SHA-1 solo per checksum e NON per sicurezza

7 Funzioni hash per password 🔑

Queste funzioni sono progettate per essere lente e resistenti agli attacchi brute-force.

Algoritmo Caratteristiche
bcrypt Lento, con salting, usato in sicurezza
scrypt Più resistente a attacchi hardware
PBKDF2 Sicuro, ma meno efficiente rispetto a bcrypt/scrypt
Argon2 Il più sicuro oggi, vincitore del PHC (Password Hashing Competition)

7.1 📌 Quando usarli?

  • bcrypt/scrypt per salvare password
  • Argon2 per la massima sicurezza

8 Funzioni hash per strutture dati 🗂️

Queste funzioni sono usate in database, tabelle hash e strutture dati.

Algoritmo Usi
Python hash() Per dizionari e set, cambia a ogni esecuzione!
MurmurHash Veloce e ottimo per database
CityHash / FarmHash Ottimizzati per grandi dataset
xxHash Molto veloce e leggero

8.1 📌 Quando usarli?

  • Dizionari e database per indicizzazione veloce
  • Bloom filters per verifiche rapide

9 Hashing non crittografico per compressione 🔄

Questi algoritmi sono usati per ridurre il numero di bit mantenendo una distribuzione uniforme.

Algoritmo Usi
CRC32 Controllo errori nei file
Adler-32 Veloce, ma meno sicuro
FNV-1a Usato in alcune tabelle hash

9.1 📌 Quando usarli?

  • Checksum su file (MD5/SHA-1 sono comunque preferibili)
  • Compressione rapida senza esigenze di sicurezza

10 🛠 Quale usare?

  • Per proteggere dati sensibili → SHA-256, SHA-512, BLAKE2
  • Per salvare password → bcrypt, scrypt, Argon2
  • Per strutture dati e database → MurmurHash, xxHash
  • Per verificare integrità file → CRC32, SHA-1
Torna in cima