package otp
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file otp.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148open Stdint (* Paramètres de l'OTP *) let = 160 (* bits : doit être un multiple de 8 *) let counter_length = 8 (* longueur du compteur en octets *) let totp_max_drift = Uint64.of_int 2 (* latence en nombre de step entre le client et le serveur *) let digit = 6 (* doit pouvoir être 6, 7 ou 8 *) let default_period = Uint64.of_int 30 (* temps en secondes d'un pallier (step) *) let default_date = Uint64.zero (* horodatage par default de date d'enregistrement de l'OTP au format Unix epoch (en seconde depuis 1970) *) let seuil_synchro = 15 (* le nombre maximum de fois ou le compteur est incrémenté pour vérifier le code *) type counter = Counter of bytes module Core = struct (* fonction de debug pour afficher une date correspondant à un compteur (float) *) let mk_time t = let tm = Unix.localtime t in let y = tm.tm_year + 1900 in let m = tm.tm_mon + 1 in Printf.sprintf "%d-%d-%d %2d:%2d:%2d" y m tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec (* counter to integer string conversion *) let c2is counter = let Counter c = counter in Uint64.to_string (Uint64.of_bytes_big_endian c 0) (* counter to byte string conversion *) let c2bs counter = let Counter c = counter in Bytes.to_string c (* calcul du hmac_sha1 à partir d'un secret et d'un compteur *) let hmac_sha1 secret counter = let hash = Cryptokit.MAC.hmac_sha1 secret in Cryptokit.hash_string hash (c2bs counter) (* hs (hmac string) doit avoir 20 octets et digit ne peut être que 6, 7 ou 8 *) let dynamic_truncation hs nb_digits = let l = String.length hs in let offset = String.get_uint8 hs (l-1) land 0xf in (* 4 bits de poid faibles du dernier octet *) let p = String.get_int32_be (String.sub hs offset 4) 0 in (* 4 octets à l'offset calculé précédemment *) let snum = Int32.logand p 0x7fffffffl in (* masque du bit de poid fort (signe) *) let rec pow10 e = (* définition de la fonction puissance 10 pour les entiers de 32 bits *) if (e=0) then 1l else (Int32.mul 10l (pow10 (e-1))) in let m = pow10 nb_digits in (* calcul du modulo : 10^digit *) Int32.to_int (Stdlib.Int32.unsigned_rem snum m) (* 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 *) let hotp ?(nb_digits=digit) k c = let hs = hmac_sha1 k c in dynamic_truncation hs nb_digits (* pour incrémenter un compteur *) let increment counter = match counter with | Counter c -> let i = Uint64.of_bytes_big_endian c 0 in let j = Uint64.add i Uint64.one in let () = Uint64.to_bytes_big_endian j c 0 in Counter c (* pour debug : affiche tous les paramètres *) let debug_check d_server d s c = let t = mk_time (Float.mul (Float.of_string (c2is c)) 30.0) in Printf.printf "server code : %6d - client code : %6d - secret : %s - counter : %s - time : %s\n" d_server d (Base32.encode_string s) (c2is c) t end (* Génération d'un secret aléatoire *) let generate_secret ?(nb_bits=shared_secret_length) rng = let byte_length = nb_bits / 8 in Cryptokit.Random.string rng byte_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] *) let totp_counter ?(period=default_period) ?(t0=default_date) ?(drift = totp_max_drift) () = let open Stdint in let t1 = Uint64.of_float @@ Float.trunc @@ Unix.time () in let step = Uint64.sub (Uint64.div (Uint64.sub t1 t0) period) drift in let b = Bytes.create 8 in let () = Uint64.to_bytes_big_endian step b 0 in Counter b (* s : le secret c : le compteur d : les digits (le code) *) let check s c d = let nb_digit = String.length (string_of_int d) in let d_server = Core.hotp ~nb_digits:nb_digit s c in (*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. *) let rec verify ?(threshold=seuil_synchro) s c d = match threshold with | 0 -> Result.Error "Invalid threshold" | _ -> if ( d < 100000 || d > 99999999) then Result.Error "Invalid number of digits in the code. Must be 6, 7 or 8 digits" else if (check s c d) then (Result.Ok (seuil_synchro - threshold)) else verify ~threshold:(threshold-1) s (Core.increment c) d (* Génère une uri au format clé pour les clients authenticator. Testé avec : - Google Authenticator - Microsoft Authenticator - Synology Secure Sign In *) let generate_totp_uri ?(algo="SHA1") ?(nb_digits=digit) ?(period=default_period) label secret issuer = let b32_secret = Base32.encode_string secret in let a = if algo="SHA1" then algo else "SHA1" in (* SHA1 only for the time being *) let n = Int.to_string nb_digits in let p = Uint64.to_string period in "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 *) let uri2qrcode uri = match Qrc.encode uri with | None -> "Capacité maximale atteinte" | Some m -> Qrc.Matrix.to_svg m