Il programma in Python per scambiare messaggi segreti

Cosa farai

In questo esempio di programma in Python abbiamo creato un sistema per scambiare messaggi con una tecnica di crittografia chiamata One-time Pad (OTP) che ti permette di scambiare messaggi segreti con i tuoi amici. Se starai attento, questi messaggi saranno impossibili da decifrare per chiunque altro!

Cosa imparerai

Scrivendo il programma con il tuo Microninja:

  • imparerai a utilizzare numeri casuali per crittografare i tuoi messaggi
  • imparerai a usare l’iterazione per crittografare singoli caratteri alfabetici
  • capirai le ragioni per cui alcuni metodi di crittografia, come ad esempio il Codice di Cesare, non sono sicuri
  • capirai le ragioni per cui è fondamentale non dire a nessuno le tue chiavi

Usare IDE su Microninja

Un fantastico modo di scrivere e testare codice è quello di usare Thonny Python IDE, un’applicazione di sviluppo per Python.
Lancia Thonny Python IDE da Menu/Coding/Thonny Python IDE.
Si accede al menu facendo clic sull’icona di Microninja in basso a sinistra dello schermo.
Una volta aperta la finestra di Python, avrai a disposizione un editor di testo, dove puoi scrivere, salvare e testare il tuo codice.

Salva subito il tuo file con il nome secretagent.py: fai clic su File e quindi su Save As. Seleziona la cartella .thonny, e scrivi secretagent.py nel campo di testo Nome del file. Fai quindi clic su Salva.

Messaggi segreti


Quando sei un agente segreto, mandare messaggi agli amici può rivelarsi molto pericoloso. Se venissero letti dai tuoi nemici ti potresti trovare nei guai!

La crittografia è un modo per celare il contenuto dei tuoi messaggi, in modo che sia difficile interpretarli per i tuoi nemici. Una delle prime forme di crittografia è stata usata da Giulio Cesare, e da lui prende il nome.

Immagina che Alice voglia mandare un messaggio a Roberto senza che Eva possa capirne il contenuto.
Per prima cosa Alice sceglie una chiave, che sarà un numero: ad esempio il numero 3.
Alice dice a Roberto la chiave.
Ogni volta che vuol mandare un messaggio a Roberto, sostituisce ogni lettera del messaggio con quella che si trova 3 posti dopo nell’alfabeto.

abcdefghilmnopqrstuvz
defghilmnopqrstuvzabc

Ad esempio, il messaggio:

Ci vediamo al parco alle tre

diventa:

Fn bhgndpr do sdufr dooh zuh

Il problema è che Eva può decifrare il messaggio abbastanza facilmente. Le basta provare a usare come chiave tutti i numeri compresi fra 1 e 21, e vedere con quale ottiene parole di senso compiuto. Inoltre, potrebbe anche cercare le parole più semplici, come gli articoli e le congiunzioni, per dedurre la chiave corretta.

Durante la seconda guerra mondiale i tedeschi credettero di aver sviluppato un metodo di crittografia perfetto, grazie all’uso di una macchina chiamata Enigma.

Si erano sbagliati… non esiste nessun metodo di crittografia perfetto. Infatti, grazie al lavoro di alcuni brillanti matematici polacchi e dell’inglese Alan Turing, i messaggi tedeschi sono stati decifrati e hanno contribuito alla vittoria della guerra da parte degli Alleati.

Il metodo chiamato One-time Pad (OTP) è uno dei più sicuri, perché prevede che fra Alice e Roberto venga scambiata una stringa di numeri generati in maniera casuale, e ogni lettera del messaggio venga sostituita da quella che corrisponde al suo numero nella pagina dell’OTP… e quindi, in sostanza, ogni lettera ha una chiave differente! Se Eva non conosce l’OTP, il messaggio è praticamente impossibile da decifrare!

La creazione dell’OTP

Apri il file secretagent.py in Thonny.

Per prima cosa abbiamo bisogno di importare la funzione randint dal modulo random, che ci permette di generare numeri in maniera casuale.
Nell’editor di Thonny, scrivi:

from random import randint

Avremo poi bisogno dell’alfabeto, quindi definisci:

ALFABETO = 'abcdefghijklmnopqrstuvwxyz'

Adesso siamo pronti per creare una funzione per generare la nostra OTP. Scrivi:

def genera_otp(pagine, lunghezza):
      for pagina in range(pagine):
          with open("otp" + str(pagina) + ".txt","w") as f:
              for i in range(lunghezza):
                  f.write(str(randint(0,26))+"\n")

Salva il programma e prova a eseguirlo.
Se non hai ottenuto messaggi di errore, scrivi nella finestra interattiva di Thonny:

genera_otp(5, 100)

Premi Invio.
Adesso apri il file browser. Nella stessa cartella in cui hai salvato il tuo programma troverai i file odt1.txt, odt2.txt, odt3.txt, odt4.txt e odt5.txt.
Aprine uno. Vedrai che contiene una lista di 100 numeri generati casualmente.
Questi file costituiscono il tuo One-time Pad.

Sei riuscito a capire come funziona la definizione che abbiamo creato?

def genera_otp(pagine, lunghezza):

Nella prima riga abbiamo definito la funzione genera_otp che ha per attributi il numero di pagine che vogliamo creare pagine e quello dei caratteri che possono essere crittografati in ogni pagina lunghezza.
Nell’esempio precedente abbiamo creato un OTP di 5 pagine ciascuna delle quali contiene 100 caratteri.

Le due istruzioni seguenti servono per creare i file.
Vogliamo creare un file per ciascuna pagina del nostro OTP, con un estensione .txt e con un nome del tipo odt1.txt, odt2.txt, odt3.txt.
A tal fine usuamo un loop for:

	  for pagina in range(pagine):
          with open("otp" + str(pagina) + ".txt","w") as f:

Infine per questa funzione abbiamo scritto due istruzioni per generare dei numeri in maniera casuale nelle pagine.
Nota che abbiamo usato /n in modo che ogni numero venga scritto su una nuova riga:

			  for i in range(lunghezza):
                  f.write(str(randint(0,26))+"\n")

Caricare una pagina dall’OTP

Una volta creato l’OTP dobbiamo trovare un modo di caricare una sua pagina e salvare in una lista i numeri in essa contenuti.
Per farlo, dobbiamo prima di tutto creare una funzione per caricare una pagina:

def carica_pagina(nomefile):
     with open(nomefile, "r") as f:

Quindi, carichiamo il contenuto in una lista:

def carica_pagina(nomefile):
     with open(nomefile, "r") as f:
         contenuto = f.read().splitlines()
      return contenuto

splitlines() serve a ridurre ciascuna linea a un elemento della lista e a rimuovere il carattere \n che fa andare a capo.

Salva di nuovo il tuo file e lancialo per assicurarti che funzioni correttamente.

Ora nella finestra interattiva di Thonny (Shell) scrivi:

pagina = carica_pagina('otp0.txt')
print(pagina)

Otterrai un risultato sarà simile a:
>>> pagina = carica_pagina(‘otp0.txt’)
print(pagina)
[‘7’, ‘7’, ‘4’, ’20’, ’20’, ’25’, ’10’, ’17’, ’14’, ’23’, ‘3’, ‘5’, ’21’, ’16’, ‘7’, ’22’, ‘0’, ‘6’, ’15’, ’18’, ‘3’, ’25’, ’13’, ‘0’, ‘6’, ‘8’, ’20’, ‘4’, ’19’, ‘8’, ’10’, ‘5’, ‘4’, ’23’, ’21’, ‘9’, ’10’, ‘2’, ’13’, ’18’, ‘2’, ’13’, ‘1’, ’17’, ’17’, ‘5’, ’13’, ’15’, ‘1’, ’11’, ‘3’, ’13’, ’17’, ‘2’, ’11’, ‘7’, ’17’, ’12’, ’17’, ’17’, ’16’, ‘4’, ’23’, ’21’, ‘6’, ’11’, ’20’, ’18’, ’12’, ’12’, ‘6’, ’23’, ‘6’, ‘1’, ’15’, ‘5’, ‘9’, ‘4’, ’26’, ‘6’, ’18’, ‘6’, ‘1’, ’14’, ’26’, ’12’, ’19’, ‘5’, ‘8’, ’26’, ‘0’, ’22’, ‘8’, ’15’, ’17’, ’17’, ‘6’, ’18’, ‘2’, ’13’]

Scrivere un messaggio segreto

La funzione che creiamo adesso è molto semplice: serve a chiedere all’utente di scrivere il messaggio che vuole venga cifrato.

def ottieni_messaggio_originale():
    messaggio_originale = input('Per piacere scrivi il tuo messaggio ')
    return messaggio_originale.lower()

lower() serve a trasformare il messaggio dell’utente in caratteri minuscoli.

Caricare e salvare i messaggi

Dobbiamo ora scrivere delle istruzioni che ci permettano di aprire i messaggi che ci vengono scritti e di salvare quelli che sono stati cifrati.
Abbiamo cioè bisogno di definire due nuove funzioni: una per aprire e leggere un file, l’altra per aprire e scrivere un file:

def carica_file(nomefile):
    with open(nomefile, "r") as f:
        contenuto = f.read()
    return contenuto

def salva_file(nomefile, data):
    with open(nomefile, 'w') as f:
        f.write(data)

Cifrare un messaggio

Adesso inizia la parete divertente: codificherai un messaggio usando una pagina del tuo OTP.

Scrivi:

def codifica(messaggio_originale, pagina):
    messaggio_codificato = ''
    for posizione, lettera in enumerate(messaggio_originale):
        if lettera not in ALFABETO:
            messaggio_codificato += lettera
        else:
            codificato = (ALFABETO.index(lettera) + int(pagina[posizione])) % 26
            messaggio_codificato += ALFABETO[codificato]
    return messaggio_codificato

Ancora una volta ci siamo serviti di una funzione. In questo caso, con due parametri: il testo del messaggio messaggio_originale e la pagina dell’OTP pagina.

def codifica(messaggio_originale, pagina):

Una volta che iniziamo a codificare il messaggio originale abbiamo bisogno di un posto dove salvare il messaggio codificato. Creiamo perciò la variabile messaggio_codificato. Il suo valore iniziale sarà una stringa vuota '':

messaggio_codificato = ''

La funzione codifica(messaggio_originale, pagina) dovrà agire su ciascuna lettera presente nel messaggio originale messaggio_originale; questo processo viene chiamato iterazione. In particolare, dovrà tener conto di quale lettera si tratta e quale posizione occupa nel messaggio originale. Per svolgere questa operazione possiamo far uso di una funzione a disposizione in Python: enumerate():

for posizione, lettera in enumerate(messaggio_originale):

La prima cosa da fare è verificare che la lettera presa in considerazione sia presente nell’alfabeto:

if lettera not in ALFABETO:

Visto che in questo programma non ci occupiamo della punteggiatura, se una lettera non è presente nell’alfabeto, la possiamo aggiungere direttamente al messaggio codificato:

if lettera not in ALFABETO:
messaggio_codificato += lettera

Le istruzioni che seguono sono quelle più complicate:

  1. dobbiamo prima di tutto determinare la posizione che la lettera occupa nel messaggio originale: ALFABETO.index(lettera)
  2. quindi, dobbiamo aggiungere a questo numero quello associato alla posizione equivalente nella pagina del nostro OTP
  3. I numeri devono essere poi convertiti nelle lettere corrispondenti: se il numero fosse 0, dovrebbe diventare a, se fosse 5 una f e così via.
    Ma cosa succederebbe se il numero fosse maggiore di 25? Se ad essempio fosse 26, dovrebbe diventare ancora una volta uno 0, se fosse 30, un 4.
    Per farlo, possiamo usare l’operatore resto % che ha come risultato il resto di una divisione.

    else:
    	codificato = (ALFABETO.index(lettera) + int(pagina[posizione])) % 26
  4. Infine, dobbiamo convertire il valore numero in lettera:
    messaggio_codificato += ALFABETO[codificato]

Riprendendo quanto abbiamo fatto fino ad ora, il nostro programma è il seguente:

from random import randint

ALFABETO = 'abcdefghijklmnopqrstuvwxyz'

def genera_otp(pagine, lunghezza):
      for pagina in range(pagine):
          with open("otp" + str(pagina) + ".txt","w") as f:
              for i in range(lunghezza):
                  f.write(str(randint(0,26))+"\n")
                  
def carica_pagina(nomefile):
    with open(nomefile, "r") as f:
         contenuto = f.read().splitlines()
         return contenuto

def ottieni_messaggio_originale():
    messaggio_originale = input('Per piacere scrivi il tuo messaggio ')
    return messaggio_originale.lower()

def carica_file(nomefile):
    with open(nomefile, "r") as f:
        contenuto = f.read()
    return contenuto

def salva_file(nomefile, data):
    with open(nomefile, 'w') as f:
        f.write(data)

def codifica(messaggio_originale, pagina):
    messaggio_codificato = ''
    for posizione, lettera in enumerate(messaggio_originale):
        if lettera not in ALFABETO:
            messaggio_codificato += lettera
        else:
            codificato = (ALFABETO.index(lettera) + int(pagina[posizione])) % 26
            messaggio_codificato += ALFABETO[codificato]
    return messaggio_codificato

Salvalo ed eseguilo per verificare che non ci siano errori.

Nella Shell scrivi:

pagina = carica_pagina('otp0.txt')

Premi invio e scrivi:

codifica('Questo è un messaggio segreto.', pagina)

Otterrai come risultato un messaggio cifrato del tipo:

'Qbimnn è rq huzoamvar femzyxh.'

Decifrare un messaggio

Per decifrare il messaggio, scriviamo una funzione simile alla precedente dove, però, al posto di aggiungere il valore dalla pagina dell’OTP, lo sottraiamo:

def decodifica(messaggio_codificato, pagina):
    messaggio_originale = ''
    for posizione, lettera in enumerate(messaggio_codificato):
        if lettera not in ALFABETO:
            messaggio_originale += lettera
        else:
            decodificato = (ALFABETO.index(lettera) - int(pagina[posizione])) % 26
            messaggio_originale += ALFABETO[decodificato]
    return messaggio_originale

Salva il programma e scrivi nella Shell:

pagina = carica_pagina('otp0.txt')

Premi invio e scrivi:

messaggio_codificato = codifica('Nessuno può leggere questo messaggio - ehehehe', pagina)

Premi ancora invio e scrivi:

messaggio_codificato

Premi invio.
Visualizzerai il messaggio codificato.

Ora scrivi:

decodifica(messaggio_codificato, pagina)

Premi invio.
Ecco il messaggio segreto!!!

Aggiungere un menu al programma

Adesso il nostro programma funziona ma dovremmo renderlo più semplice da usare. Potremmo, per esempio, pensare di fare in modo che il messaggio segreto possa essere salvato e inviato via email.

Iniziamo a definire una funzione per il menu:

def menu():

e creiamo 4 scelte possibili al suo interno:

def menu():
      scelte = ['1', '2', '3', '4']
      scelta = '0'
      while True:
          while scelta not in scelte:

e aggiungiamo 4 opzioni per il menu salvando la scelta dell’utente come variabile scelta:

def menu():
      scelte = ['1', '2', '3', '4']
      scelta = '0'
      while True:
          while scelta not in scelte:
              print('Che cosa vorresti fare?')
              print('1. Genera OTP')
              print('2. Codifica messaggio')
              print('3. Decodifica messaggio')
              print('4. Esci dal programma')
              scelta = input('Per piacere digita 1, 2, 3 or 4 e premi Invio ')

Se viene scelto 1, dobbiamo chiedere all’utente quante pagine desidera creare e quanto caratteri devono contenere. Questi valori saranno gli attributi della funzione genera_otp():

  if scelta == '1':
      pagine = int(input('Quante pagine vuoi creare? '))
      lunghezza = int(input('Quale sarà la lunghezza massima del messaggio? '))
      genera_otp(pagine, lunghezza)

Se viene scelto 2, dobbiamo chiedere all’utente il nome del file otp che desidera usare e, quindi, dobbiamo chiedergli di digitare il messaggio che vuole cifrare. Questo messaggio deve essere codificato e salvato in un file con nome a sua scelta:

  elif scelta == '2':
      nomefile = input('Digita il nome del file OTP che desideri usare ')
      pagina = carica_pagina(nomefile)
      messaggio_originale = ottieni_messaggio_originale()
      messaggio_codificato = codifica(messaggio_originale, pagina)
      filename = input('Come vuoi chiamare il file contenente il messaggio cifrato? ')
      salva_file(nomefile, messaggio_codificato)

Se viene scelto 3, dobbiamo chiedere all’utente il nome della pagina dell’OTP usata per codificare il messaggio e il nome del file che deve essere decodificato. A questo punto, il file può essere aperto, il messaggio decodificato e il risultato visualizzato a schermo:

  elif scelta == '3':
      nomefile = input('Digita il nome del file OTP che desideri usare ')
      pagina = carica_pagina(nomefile)
      nomefile = input('Digita il nome del file che vuoi decodificare ')
      messaggio_codificato = carica_file(nomefile)
      messaggio_originale = decodifica(messaggio_codificato, pagina)
      print('Il messaggio segreto è:')
      print('')
      print(messaggio_originale)

Se viene scelto 4, si esce dal programma:

  elif choice == '4':
      exit()

Dobbiamo infine azzerare la funzione prima che si ripeta il ciclo:

  choice = '0'

Riassumento quanto fatto, la funzione menu() sarà:

def menu():
      scelte = ['1', '2', '3', '4']
      scelta = '0'
      while True:
          while scelta not in scelte:
              print('Che cosa vorresti fare?')
              print('1. Genera OTP')
              print('2. Codifica messaggio')
              print('3. Decodifica messaggio')
              print('4. Esci dal programma')
              scelta = input('Per piacere digita 1, 2, 3 or 4 e premi Invio ')
              if scelta == '1':
                  pagine = int(input('Quante pagine vuoi creare? '))
                  lunghezza = int(input('Quale sarà la lunghezza massima del messaggio? '))
                  genera_otp(pagine, lunghezza)
              elif scelta == '2':
                  nomefile = input('Digita il nome del file OTP che desideri usare ')
                  pagina = carica_pagina(nomefile)
                  messaggio_originale = ottieni_messaggio_originale()
                  messaggio_codificato = codifica(messaggio_originale, pagina)
                  nomefile = input('Come vuoi chiamare il file contenente il messaggio cifrato? ')
                  salva_file(nomefile, messaggio_codificato)
              elif scelta == '3':
                  nomefile = input('Digita il nome del file OTP che desideri usare ')
                  pagina = carica_pagina(nomefile)
                  nomefile = input('Digita il nome del file che vuoi decodificare ')
                  messaggio_codificato = carica_file(nomefile)
                  messaggio_originale = decodifica(messaggio_codificato, pagina)
                  print('Il messaggio segreto è:')
                  print('')
                  print(messaggio_originale)
              elif scelta == '4':
                  exit()
              scelta = '0'

Per finire dobbiamo solo aggiungere un’istruzione per richiamare la funzione menu():

menu()

Il programma completo

Riassumendo, il programma completo sarà:

from random import randint

ALFABETO = 'abcdefghijklmnopqrstuvwxyz'

def genera_otp(pagine, lunghezza):
      for pagina in range(pagine):
          with open("otp" + str(pagina) + ".txt","w") as f:
              for i in range(lunghezza):
                  f.write(str(randint(0,26))+"\n")
                  
def carica_pagina(nomefile):
    with open(nomefile, "r") as f:
         contenuto = f.read().splitlines()
         return contenuto

def ottieni_messaggio_originale():
    messaggio_originale = input('Per piacere scrivi il tuo messaggio ')
    return messaggio_originale.lower()

def carica_file(nomefile):
    with open(nomefile, "r") as f:
        contenuto = f.read()
    return contenuto

def salva_file(nomefile, data):
    with open(nomefile, 'w') as f:
        f.write(data)

def codifica(messaggio_originale, pagina):
    messaggio_codificato = ''
    for posizione, lettera in enumerate(messaggio_originale):
        if lettera not in ALFABETO:
            messaggio_codificato += lettera
        else:
            codificato = (ALFABETO.index(lettera) + int(pagina[posizione])) % 26
            messaggio_codificato += ALFABETO[codificato]
    return messaggio_codificato

def decodifica(messaggio_codificato, pagina):
    messaggio_originale = ''
    for posizione, lettera in enumerate(messaggio_codificato):
        if lettera not in ALFABETO:
            messaggio_originale += lettera
        else:
            decodificato = (ALFABETO.index(lettera) - int(pagina[posizione])) % 26
            messaggio_originale += ALFABETO[decodificato]
    return messaggio_originale

def menu():
      scelte = ['1', '2', '3', '4']
      scelta = '0'
      while True:
          while scelta not in scelte:
              print('Che cosa vorresti fare?')
              print('1. Genera OTP')
              print('2. Codifica messaggio')
              print('3. Decodifica messaggio')
              print('4. Esci dal programma')
              scelta = input('Per piacere digita 1, 2, 3 or 4 e premi Invio ')
              if scelta == '1':
                  pagine = int(input('Quante pagine vuoi creare? '))
                  lunghezza = int(input('Quale sarà la lunghezza massima del messaggio? '))
                  genera_otp(pagine, lunghezza)
              elif scelta == '2':
                  nomefile = input('Digita il nome del file OTP che desideri usare ')
                  pagina = carica_pagina(nomefile)
                  messaggio_originale = ottieni_messaggio_originale()
                  messaggio_codificato = codifica(messaggio_originale, pagina)
                  nomefile = input('Come vuoi chiamare il file contenente il messaggio cifrato? ')
                  salva_file(nomefile, messaggio_codificato)
              elif scelta == '3':
                  nomefile = input('Digita il nome del file OTP che desideri usare ')
                  pagina = carica_pagina(nomefile)
                  nomefile = input('Digita il nome del file che vuoi decodificare ')
                  messaggio_codificato = carica_file(nomefile)
                  messaggio_originale = decodifica(messaggio_codificato, pagina)
                  print('Il messaggio segreto è:')
                  print('')
                  print(messaggio_originale)
              elif scelta == '4':
                  exit()
              scelta = '0'
menu()

Prova a eseguirlo per vedere se funziona correttamente.
Un esempio è mostrato nella figura seguente:

Usare il programma

Sebbene questo programma di esempio scritto in Python sia sicuro, tieni in considerazione le seguenti problematiche:

  • per scambiare messaggi segreti con un tuo amico, puoi tranquillamente usare la email o addirittura i social media. Non ha importanza che siano pubblici e visibili a tutti perché nessun altro sarà in grado di decifrarli.
  • una volta che hai generato un OTP, per esempio di 100 pagine, devi condividerlo con chi vuoi scambiare messaggi. In questo caso, non è sicuro usare la email; sarebbe meglio usare una memoria esterna come una chiavetta usb o una scheda SD.
  • il metodo OTP è sicuro solo se la persona con cui scambi messaggi mantiene sicure le pagine dell’OTP.
  • sia tu che il tuo amico dovete essere certi di usare la stessa pagina dell’OTP. Il metodo più semplice per non sbagliarsi è quello di partire da quella chiamata otp0.txt, di cancellarla dopo aver codificato e decodificato un messaggio, e quindi di passare alla pagina seguente otp1.txt, e così via.
  • il metodo OTP si basa sulla generazione casuale di numeri. Se quest’ultima non è veramente casuale, allora esiste la possibilità che i messaggi vengano decifrati. Tieni presente che usare il modulo random di Python probabilmente non è il modo migliore di generare numeri in maniera casuale.
  • i tuoi messaggi non possono contenere più caratteri di quanti non ce ne siano in una pagina dell’OTP. Se non sei sicuro in partenza della loro lunghezza ti conviene generare pagine con più caratteri di quelli che credi ti serviranno.

E adesso?

Questo esempio di programma in Python è completato, ma può essere ancora migliorato…

  • Sei in grado di modificare il programma in modo che vengano preservate le lettere maiuscole?
  • Sei in grado di modificare il programma in modo che anche la punteggiatura venga codificata?
  • Sei in grado di modificare il programma in modo che venga cancellata una pagina dell’OTP dopo essere stata utilizzata per codificare e decodificare un messaggio?

This learning resource is provided for free by the Raspberry Pi Foundation under a Creative Commons licence.
Find more at raspberrypi.org/resources and github.com/raspberrypilearning.