123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148openStdint(* Paramètres de l'OTP *)letshared_secret_length=160(* bits : doit être un multiple de 8 *)letcounter_length=8(* longueur du compteur en octets *)lettotp_max_drift=Uint64.of_int2(* latence en nombre de step entre le client et le serveur *)letdigit=6(* doit pouvoir être 6, 7 ou 8 *)letdefault_period=Uint64.of_int30(* temps en secondes d'un pallier (step) *)letdefault_date=Uint64.zero(* horodatage par default de date d'enregistrement de l'OTP
au format Unix epoch (en seconde depuis 1970) *)letseuil_synchro=15(* le nombre maximum de fois ou le compteur est incrémenté
pour vérifier le code *)typecounter=CounterofbytesmoduleCore=struct(* fonction de debug pour afficher une date correspondant à un compteur (float) *)letmk_timet=lettm=Unix.localtimetinlety=tm.tm_year+1900inletm=tm.tm_mon+1inPrintf.sprintf"%d-%d-%d %2d:%2d:%2d"ymtm.tm_mdaytm.tm_hourtm.tm_mintm.tm_sec(* counter to integer string conversion *)letc2iscounter=letCounterc=counterinUint64.to_string(Uint64.of_bytes_big_endianc0)(* counter to byte string conversion *)letc2bscounter=letCounterc=counterinBytes.to_stringc(* calcul du hmac_sha1 à partir d'un secret et d'un compteur *)lethmac_sha1secretcounter=lethash=Cryptokit.MAC.hmac_sha1secretinCryptokit.hash_stringhash(c2bscounter)(* hs (hmac string) doit avoir 20 octets et digit ne peut être que 6, 7 ou 8 *)letdynamic_truncationhsnb_digits=letl=String.lengthhsinletoffset=String.get_uint8hs(l-1)land0xfin(* 4 bits de poid faibles du dernier octet *)letp=String.get_int32_be(String.subhsoffset4)0in(* 4 octets à l'offset calculé précédemment *)letsnum=Int32.logandp0x7ffffffflin(* masque du bit de poid fort (signe) *)letrecpow10e=(* définition de la fonction puissance 10 pour les entiers de 32 bits *)if(e=0)then1lelse(Int32.mul10l(pow10(e-1)))inletm=pow10nb_digitsin(* calcul du modulo : 10^digit *)Int32.to_int(Stdlib.Int32.unsigned_remsnumm)(* snum % 10^digit *)(* calcul le hotp
k : le secret (la clé : key)
c : le compteur
nb_digits : le nombre de digits : 6, 7 ou 8
*)lethotp?(nb_digits=digit)kc=leths=hmac_sha1kcindynamic_truncationhsnb_digits(* pour incrémenter un compteur *)letincrementcounter=matchcounterwith|Counterc->leti=Uint64.of_bytes_big_endianc0inletj=Uint64.addiUint64.oneinlet()=Uint64.to_bytes_big_endianjc0inCounterc(* pour debug : affiche tous les paramètres *)letdebug_checkd_serverdsc=lett=mk_time(Float.mul(Float.of_string(c2isc))30.0)inPrintf.printf"server code : %6d - client code : %6d - secret : %s - counter : %s - time : %s\n"d_serverd(Base32.encode_strings)(c2isc)tend(* Génération d'un secret aléatoire *)letgenerate_secret?(nb_bits=shared_secret_length)rng=letbyte_length=nb_bits/8inCryptokit.Random.stringrngbyte_length(* génère un compteur totp
period : la durée, en seconde, de l'incrément du compteur [30s par défaut]. Cela permet de prendre en compte la latence du réseau.
t0 : la date initial (au format Unix epoch) d'enregistrement de l'OTP [0 par défaut]. Cela ajoute de l'aléa dans le processus.
drift : le nombre d'incréments (period) autorisés entre le client et le serveur, lié à une dérive [2 par defaut, ce qui correspond à 2x30+29 = 89s au max]
*)lettotp_counter?(period=default_period)?(t0=default_date)?(drift=totp_max_drift)()=letopenStdintinlett1=Uint64.of_float@@Float.trunc@@Unix.time()inletstep=Uint64.sub(Uint64.div(Uint64.subt1t0)period)driftinletb=Bytes.create8inlet()=Uint64.to_bytes_big_endianstepb0inCounterb(*
s : le secret
c : le compteur
d : les digits (le code)
*)letcheckscd=letnb_digit=String.length(string_of_intd)inletd_server=Core.hotp~nb_digits:nb_digitscin(*let () = debug_check d_server d s c in*)d_server=d(* vérifie le code en resynchronisant, au besoin, les compteurs.
s : secret
c : compteur
d : digits (code)
threshold : le nombre d'incréments maximum effectués pour vérifier le code si la première vérification échoue.
retour : le nombre de fois où le compteur doit être incrémenté pour être synchronisé.
si le seuil maximal de resynchronisation est atteint, renvoie une erreur. Sinon
retourne retourne le nombre d'incrément nécessaire à la synchronisation.
*)letrecverify?(threshold=seuil_synchro)scd=matchthresholdwith|0->Result.Error"Invalid threshold"|_->if(d<100000||d>99999999)thenResult.Error"Invalid number of digits in the code. Must be 6, 7 or 8 digits"elseif(checkscd)then(Result.Ok(seuil_synchro-threshold))elseverify~threshold:(threshold-1)s(Core.incrementc)d(* Génère une uri au format clé pour les clients authenticator.
Testé avec :
- Google Authenticator
- Microsoft Authenticator
- Synology Secure Sign In
*)letgenerate_totp_uri?(algo="SHA1")?(nb_digits=digit)?(period=default_period)labelsecretissuer=letb32_secret=Base32.encode_stringsecretinleta=ifalgo="SHA1"thenalgoelse"SHA1"in(* SHA1 only for the time being *)letn=Int.to_stringnb_digitsinletp=Uint64.to_stringperiodin"otpauth://totp/"^issuer^":"^label^"?"^"secret="^b32_secret^"&issuer="^issuer^"&algorithm="^a^"&digit="^n^"&period="^p(* Transforme une uri en un qrcode sous la forme d'une balise html intégrable dans un fichier html *)leturi2qrcodeuri=matchQrc.encodeuriwith|None->"Capacité maximale atteinte"|Somem->Qrc.Matrix.to_svgm