Riflessi Quotidiani logo
Riflessi quotidiani
Pubblicato il

Gli OTP sono ovunque: ma come funzionano, esattamente?

Authors
  • avatar
    Name
    Matteo Vignoli

Negli ultimi anni gli OTP (One-Time Passwords) sono diventati uno dei metodi più diffusi per l'autenticazione sicura; si tratta di password che sono valide per una singola sessione o transazione e che scadono dopo un breve periodo di tempo (di solito 30 secondi), rendendo l’accesso ai sistemi molto più sicuro rispetto alle sole password tradizionali.

Un OTP (One-Time Password) è una sequenza di caratteri alfanumerica temporanea generata in modo casuale oppure tramite un algoritmo e utilizzata per autenticare un utente una sola volta. Dopo il primo utilizzo o dopo che il periodo di validità è scaduto, l'OTP diventa inutile e non può essere riutilizzato.

Esistono due tipi principali di OTP:

  1. TOTP (Time-based One-Time Password): Generato in base all'ora corrente e ha una durata limitata (ad esempio, 30 secondi).
  2. HOTP (HMAC-based One-Time Password): Generato in base a un contatore incrementale. L'OTP viene utilizzato una sola volta e il contatore viene incrementato ogni volta che viene generata una nuova password.

In questo post ci concentreremo su come implementare un TOTP, uno degli standard più diffusi, basato su tempo e hash crittografico.

Come Funziona l'Algoritmo TOTP?

L'algoritmo TOTP (che fondamentalmente è un HOTP in cui il contatore è sostituito dal momento temporale) genera un OTP utilizzando tre elementi principali:

  • Una secret condiviso tra il client e il server, normalmente una stringa di almeno 16 caratteri formattata in base-32;
  • Il periodo temporale corrente, suddiviso in intervalli di tempo (tipicamente 30 secondi);
  • Una funzione di HMAC-SHA1 (ma la specifica permette anche altre varianti come SHA-256 o SHA-512) per creare un codice crittografico sicuro;

Il processo consiste nel combinare il tempo corrente con il secret condiviso per produrre una password temporanea, che sarà quindi valida solo per un breve periodo di tempo - di solito 30 secondi, ma è permessa una tolleranza fino a 60 o 90 secondi.

Implementazione dell'Algoritmo TOTP

Per implementare l'algoritmo TOTP possiamo seguire questi passaggi:

1. Calcolo dell'intervallo di tempo

Prima di tutto dobbiamo prendere il tempo corrente e dividerlo per un passo temporale fisso, generalmente di 30 secondi. Questo ci permette di mappare il tempo su un contatore che cambia ogni 30 secondi.

const timeStep = 30;
// Prendo l'intervallo di tempo corrente
const getTimeStep = (): number => {
  const currentTime = Math.floor(Date.now() / 1000); 
  return Math.floor(currentTime / timeStep); // intervallo
};

2. Generazione dell'HMAC

Usiamo ora l'algoritmo HMAC-SHA1 per combinare il secret condiviso con l'intervallo di tempo attuale:

import crypto from 'crypto';

// Funzione per generare HMAC-SHA1
const generateHMAC = (key: string, message: string): Buffer => {
  return crypto.createHmac('sha1', key).update(message).digest();
};

3. Troncamento dinamico

L'hash così generato sarebbe un tantino scomodo da digitare per l'utente (sarebbe di 20 byte con SHA-1, ad esempio), soprattutto senza errori e nel tempo limitato di 30 secondi! Il passo successivo, quindi, è prenderne soltanto una parte in un processo chiamato troncamento dinamico, dal quale otterremo poi un numero intero da cui deriveremo l'OTP finale.

  1. Selezione dell'offset: L'ultimo byte dell'HMAC è utilizzato per determinare l'offset di inizio della porzione da estrarre. Gli ultimi 4 bit di questo byte sono presi come valore dell'offset, che sarà quindi un numero compreso tra 0 e 15 (0x0 e 0xF) - l'algoritmo selezionerà uno dei 16 possibili punti di partenza nei 20 byte dell'HMAC.

  2. Estrazione dei 4 byte: A partire dall'offset calcolato, si selezionano i successivi 4 byte dell'HMAC.

  3. Conversione in intero: I 4 byte estratti vengono trattati come un numero intero non negativo a 31 bit. Il bit più significativo (MSB) del primo byte viene azzerato per assicurarsi che il numero risultante sia positivo.

// Funzione per il troncamento dinamico
const dynamicTruncate = (hmac: Buffer): number => {
  const offset = hmac[hmac.length - 1] & 0xf; // ultimi 4 bit dell'ultimo byte
  const trunc = 
    ((hmac[offset] & 0x7f) << 24) | // Primo byte, maschera il bit più significativo
    ((hmac[offset + 1] & 0xff) << 16) | // Secondo byte
    ((hmac[offset + 2] & 0xff) << 8) |  // Terzo byte
    (hmac[offset + 3] & 0xff);          // Quarto byte

  return truc; // Ritorno il numero a 31 bit
};
  1. Modulo: L'intero ottenuto viene infine ridotto tramite un'operazione di modulo 10n dove n è il numero di cifre desiderato per l'OTP finale (ad esempio 6).
const generateTOTP = (trunc: string, digits = 6): string => {
  const otp = trunc % Math.pow(10, digits);
  return otp.toString().padStart(digits, '0'); // Padding con zeri iniziali se necessario
};

5. Verifica dell'OTP

Per verificare l'OTP il server deve rigenerare l'OTP utilizzando lo stesso secret e intervallo temporale dell'utente. Per migliorare l'usabilità spesso si permette un piccolo margine di tolleranza - verificando l'OTP anche per l'intervallo precedente o successivo, così da compensare eventuali differenze di orologio o ritardi nella digitazione.

const verifyTOTP = (token: string, secret: string, threshold = 1): boolean => {
  const currentStep = getTimeStep();

  // Verifica nell'intervallo attuale, precedente e successivo
  for (let tolerance = -threshold; tolerance <= threshold; tolerance++) {
    const timeStepVal = (currentStep + tolerance).toString(16).padStart(16, '0');
    const hmac = generateHMAC(secret, timeStepVal);
    const truncated = dynamicTruncate(hmac);
    const otp = (truncatedVal % Math.pow(10, token.length)).toString().padStart(token.length, '0');
    if (otp === token) {
      return true; // l'OTP + valido
    }
  }
  return false; // l'OTP non è corretto
};

Conclusione

Sebbene il meccanismo dietro un OTP, alla fine, non sià poi così complicato una volta applicato l'algoritmo corretto, tuttavia non è proprio il caso di usare una versione "fatta in casa" per questo tipo di operazioni! L'autenticazione a 2 fattori (2FA) è estremamente importante nel prevenire attacchi e furti di credenziali, meglio lasciar fare a librerie testate e verificate, o meglio ancora a provider esterni qualificati.