Comprendre les mot de passe à usage unique basé sur le temps
Maintenant que tous les services web nous encouragent, de plus en plus, à utiliser une MFA pour sécuriser nos compte, l'un d'entre eux reste le plus utilisé que les autres : Mot de passe à usage unique basé sur le temps ou TOTP génère un code unique de 6
chiffres ou plus à saisir juste après avoir tapé votre mot de passe.
Le serveur ou l'application Web permettant de configurer le TOTP donne un QRCode à scanner (ou une chaîne Base32) à configurer dans une application génératrice de TOTP comme Microsoft Authenticator, Google Authenticator, Bitwarden ou plus.
Nous l'utilisons tous, mais comment ça marche ? Est-ce sécurisé ? Mon compte est-il sécurisé lorsque j'utilise un générateur de TOTP tiers ??
Le protocole
Je vais approfondir dans les détails le protocole sous-jacent vous permettant de générer des TOTP et comment le serveur le vérifie.
Bases
L'idée de base est assez simple. Vous et le serveur avez partagé un secret, une chaîne Base32 de 16
caractères (parfois stockée dans un QRCode). Puis vous synchronisez tous les deux vos horloges.
Enfin, lorsque vous souhaitez vous connecter, vous exécutez une fonction magique sophistiquée et vous obtenez un TOTP. Le serveur calcule le TOTP de son côté et vous demande celui que vous avez généré de votre côté. Si les deux TOTP correspondent, le serveur vous autorise à entrer.
Protocole TOTP
Explorons le protocole TOTP. Selon la RFC 6238 vous avez besoin :
- Un
compteur
changeant qui doit être synchronisé entre les deux parties - Un
secret
partagé entre les deux parties
Le compteur
est calculé sur la base du temps epoch (Ainsi tout le monde est synchronisé) avec la formule suivante :
Avec :
C
la valeur du compteurT
l'heure Unix actuelle en secondes depuis epochT0
une heure Unix de début arbitraire.0
par défautX
le pas de temps en secondes ou la fréquence de mise à jour du code.30
par défaut
Si nous essayons de faire une implémentation Python, cela ressemblera à :
t0 = 0
# https://docs.python.org/3/library/time.html#time.time
current_unix_time = time.time()
time_step = 30
counter = int((current_unix_time - t0) / time_step)
Maintenant la valeur TOTP peut être récupérée avec la formule suivante :
Avec:
C
la valeur du compteurK
la clé secrèteD
le nombre de chiffres dans nos TOTPsHOTP
la fonction magique qui calcule l'OTP
En Python, on obtient :
Protocole HOTP
Ce deuxième protocole est utilisé pour calculer une valeur unique, une valeur compteur et une clé secrete. De la RFC rfc4226, nous apprenons qu'il est basé sur le protocole de hachage HMAC.
Asseyez-vous bien car cela va commencer à être difficile et j'espère que vous êtes à l'aise avec les opérations binaires.
Gérer les entrées
Tout d'abord, nous devons gérer les 2 de nos 3 paramètres d'entrée : counter
, key
counter
doit être converti en une liste d'octets de typeunsigned long long
etbig-endian
.
En Python :
import struct
# https://docs.python.org/3/library/struct.html#struct.pack
# https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment
# https://docs.python.org/3/library/struct.html#format-characters
bytes_counter = struct.pack('>Q', counter)
counter: 55036474
bytes_counter(bin) => 00000011 01000111 11001010 00111010
bytes_counter(hex) => 03 47 ca 3a
key
doit être une chaîne Base32 valide. La RFC 4648 nous indique que la longueur de la chaîne doit être un facteur de8
. Ainsi, du "padding" peut être ajouté avec le caractère=
à la fin s'il est trop court. Par exemple :AA
=>AA======
,BBBBBBBBBB
=>BBBBBBBBBB======
,CCCCCCCC
=>CCCCCCCC
Ensuite, nous convertissons la clé mise à jour en valeur en liste d'octets :
valid_key: ABCDEFGHIJKLMNOP
bytes_key(bin) => 10001000 01100100 00101001 10001110 10000100 10101001 01101100 0110101 11001111
bytes_key(hex) => 44 32 14 c7 42 54 b6 35 cf
Calculer le HMAC
En utilisant la RFC 2104 nous pouvons en comprendre le fonctionnement de la fonction HMAC. Nous devons choisir une fonction de hachage. HOTP ou RFC 4226 utilise la fonction de hachage cryptographique SHA-1.
Python implémente la bibliothèque HMAC :
import hmac
# Calculate the HMAC-SHA1 value
# Produce a 20 bytes (or 160 bites) long string
mac = hmac.new(bytes_key, bytes_counter, "SHA1").digest()
bytes_counter(hex): 03 47 ca 3a
bytes_key(hex): 44 32 14 c7 42 54 b6 35 cf
mac(bin) => 10110100 11010010 01111010 10110100 10111101 00110101 11111110 00100011 11101101 01011001 01111110 10111100 11110000 01111001 11000001 01001010 00000110 01101100 01010001 00101111
mac(hex) => b4 d2 7a b4 bd 35 fe 23 ed 59 7e bc f0 79 c1 4a 06 6c 51 2f
Calculer le HOTP
Conformément à la section 5.3
de la RFC 4226.
Trouver le décalage
Nous devons d'abord trouver la valeur offset
en obtenant les 4
bits les moins significatifs du MAC
(Les 4
à droite)
en utilisation de opération bit à bit AND
En Python &
peut être utilisé pour faire une opération binaire AND
entre deux nombres. Avec la valeur 0x0F
(en hexadécimal et équivalent à 00001111
en binaire) on peut extraire deux 4
derniers bits. Étant donné que offset
est un entier stocké sur 4
bits, la valeur est comprise entre 0
et 15
. En Python, nous pouvons utiliser mac[-1]
pour obtenir le dernier octet de la liste d'octets converti en entier.
mac(bin): 10110100 11010010 01111010 10110100 10111101 00110101 11111110 00100011 11101101 01011001 01111110 10111100 11110000 01111001 11000001 01001010 00000110 01101100 01010001 00101111
mac(hex): b4 d2 7a b4 bd 35 fe 23 ed 59 7e bc f0 79 c1 4a 06 6c 51 2f
offset(bin) => 1111
offset(hex) => f
offset(dec) => 15
Recherche de la troncation dynamique
En utilisant le offset
trouvé précédemment, nous allons extraire la Troncation dynamique
du MAC
. Nous devons récupérer les 4
octets à la position offset
, offset+1
, offset+2
et offset+3
dans la liste d'octets MAC
.
Nous avons :
En Python, la valeur après le :
n'est pas incluse. Afin d'extraire la valeur offset + 3
, nous devons mettre (offset + 3) + 1
offset(bin): 1111
offset(hex): f
offset(dec): 15
offset(dec) from 15 to 18
mac(bin): 10110100 11010010 01111010 10110100 10111101 00110101 11111110 00100011 11101101 01011001 01111110 10111100 11110000 01111001 11000001 01001010 00000110 01101100 01010001 00101111
mac(hex): b4 d2 7a b4 bd 35 fe 23 ed 59 7e bc f0 79 c1 4a 06 6c 51 2f
dynamic_truncation(bin) => 10010100 00001100 11011000 1010001
dynamic_truncation(hex) => 4a 06 6c 51
Extraire les 31 bits
Enfin, nous allons extraire les 31
bits les moins significatifs de notre Dynamic Truncation
La solution serait de convertir la liste d'octets de résultat en une longueur non signée qui est de 32
bits avec le bit le plus significatif (à gauche) contenant la valeur signée (+
ou -
).
En utilisant la même astuce que précédemment, nous utilisons une opération bit à bit AND pour les extraire.
0x7fffffff
(en hexadécimal et équivalent à 01111111 11111111 11111111 11111111
en binaire). Résultant en une valeur de 31
bits en ignorant le 0
en tête.
# https://docs.python.org/3/library/struct.html#struct.unpack
extract_31 = struct.unpack('>L', dynamic_truncation)[0] & 0x7fffffff
dynamic_truncation(bin): 10010100 00001100 11011000 1010001
dynamic_truncation(hex): 4a 06 6c 51
extract_31(bin) => 10010100 00001100 11011000 1010001
extract_31(hex) => 4a 06 6c 51
extract_31(dec) => 1241934929
Chiffres tronqués
Maintenant que nous avons notre nombre final après avoir extrait les derniers 31
bits, nous devons cette fois tronquer la valeur décimale. Sur la base de la valeur digits
configurée, nous voulons uniquement conserver les X
derniers chiffres de la valeur décimale de extract_31
.
X
est la longueur du code TOTP que nous voulons et doit être comprise entre 6
et 10
.
6
minimum en raison des exigences de sécurité et 10
maximum car la valeur maximale d'un entier signé 32
bits est 2147483647
, une valeur numérique 10
.
L'utilisation d'une longueur de 10
sur 9
n'ajoute pas beaucoup de sécurité car le premier chiffre ne peut prendre que les valeurs 0
, 1
, 2
.
Dans le cas où extract_31
est une valeur trop faible où le nombre de chiffres est inférieur au nombre digits
, nous ajoutons des 0
au début à gauche du code HOTP pour lui donner la bonne longueur.
Conclusion
Maintenant, vous savez vraiment comment les codes TOTP sont générés. Comme on peut le voir, c'est assez simple et vous pouvez développer votre propre script pour les générer.
Code complet
import base64
import hmac
import struct
import time
def hotp(counter, key, digits_count=6):
bytes_counter = struct.pack('>Q', counter)
key = key + '=' * ((8 - len(key)) % 8)
bytes_key = base64.b32decode(key)
mac = hmac.new(bytes_key, bytes_counter, "SHA1").digest()
offset = mac[-1] & 0x0F
dynamic_truncation = mac[offset:offset+4]
extract_31 = struct.unpack('>L', dynamic_truncation)[0] & 0x7fffffff
return str(extract_31)[-digits_count:].zfill(digits_count)
def totp(key):
counter = int(time.time() / 30)
return hotp(counter, key)
if __name__ == "__main__":
key = input("Enter secret key : ").upper()
print("TOTP code :", totp(key))
Références
- TOTP: Time-Based One-Time Password Algorithm
- HOTP: An HMAC-Based One-Time Password Algorithm
- The Base16, Base32, and Base64 Data Encodings
- HMAC: Keyed-Hashing for Message Authentication
- Mot de passe à usage unique basé sur le temps
- HMAC-based one-time password
- Code QR
- SHA-1
- Opération bit à bit
- epoch
- Python HMAC lib