@@ -55,12 +55,20 @@ defmodule NimbleTOTP do
5555 [eqrcode](https://github.com/SiliconJungles/eqrcode) to generate the QR
5656 code as **SVG**.
5757
58+ If you use more than 6 digits for the totp token you will need to specify
59+ it in the otpauth uri with the `digits` option.
60+
5861 Example:
5962
6063 uri = NimbleTOTP.otpauth_uri("Acme", "alice", secret)
6164 #=> "otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme"
6265 uri |> EQRCode.encode() |> EQRCode.svg()
6366 #=> "<?xml version=\\" 1.0\\" standalone=\\" yes\\" ?>\\n<svg version=\\" 1.1\\" ...
67+
68+ uri = NimbleTOTP.otpauth_uri("Acme", "alice", secret, digits: 8)
69+ #=> "otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme"
70+ uri |> EQRCode.encode() |> EQRCode.svg()
71+ #=> "<?xml version=\\" 1.0\\" standalone=\\" yes\\" ?>\\n<svg version=\\" 1.1\\" ...
6472
6573 ### Generating a Time-Based One-Time Password
6674
@@ -128,7 +136,7 @@ defmodule NimbleTOTP do
128136 """
129137
130138 import Bitwise
131- @ totp_size 6
139+ @ default_digits 6
132140 @ default_totp_period 30
133141
134142 @ typedoc "Unix time in seconds, `t:DateTime.t/0` or `t:NaiveDateTime.t/0`."
@@ -214,6 +222,8 @@ defmodule NimbleTOTP do
214222 *in seconds*) to be used. Default is `System.os_time(:second)`.
215223 * `:period` - The period (in seconds) in which the code is valid. Default is `30`.
216224 If this option is given to `verification_code/2`, it must also be given to `valid?/3`.
225+ * `:digits` - The desired length of the totp. Default is 6.
226+ If this option is given to `verification_code/2`, it must also be given to `valid?/3` and `otpauth_uri/3`/`otpauth_uri/4`.
217227
218228 ## Examples
219229
@@ -226,18 +236,21 @@ defmodule NimbleTOTP do
226236 def verification_code ( secret , opts \\ [ ] ) when is_binary ( secret ) and is_list ( opts ) do
227237 time = opts |> Keyword . get_lazy ( :time , fn -> System . os_time ( :second ) end ) |> to_unix ( )
228238 period = Keyword . get ( opts , :period , @ default_totp_period )
239+ digits = Keyword . get ( opts , :digits , @ default_digits )
240+
241+ digits not in 6 .. 10 && raise ArgumentError , "digits must be between 6 and 10"
229242
230- verification_code ( secret , time , period )
243+ verification_code ( secret , time , period , digits )
231244 end
232245
233- @ spec verification_code ( binary ( ) , integer ( ) , pos_integer ( ) ) :: binary ( )
234- defp verification_code ( secret , time , period ) do
246+ @ spec verification_code ( binary ( ) , integer ( ) , pos_integer ( ) , integer ( ) ) :: binary ( )
247+ defp verification_code ( secret , time , period , digits ) do
235248 secret
236249 |> hmac ( time , period )
237250 |> hmac_truncate ( )
238- |> rem ( 1_000_000 )
251+ |> rem ( Integer . pow ( 10 , digits ) )
239252 |> to_string ( )
240- |> String . pad_leading ( @ totp_size , "0" )
253+ |> String . pad_leading ( digits , "0" )
241254 end
242255
243256 defp hmac ( secret , time , period ) do
@@ -273,6 +286,9 @@ defmodule NimbleTOTP do
273286 * `:period` - The period (in seconds) in which the code is valid. Default is `30`.
274287 If this option is given to `verification_code/2`, it must also be given to `valid?/3`.
275288
289+ * `:digits` - The desired length of the totp. Default is 6.
290+ If this option is given to `verification_code/2`, it must also be given to `valid?/3` and `otpauth_uri/3`/`otpauth_uri/4`.
291+
276292 ## Preventing TOTP code reuse
277293
278294 The `:since` option can be used to prevent TOTP codes from being reused. When set
@@ -302,18 +318,30 @@ defmodule NimbleTOTP do
302318 @ spec valid? ( binary ( ) , String . t ( ) , [ option ( ) | validate_option ( ) ] ) :: boolean ( )
303319 def valid? ( secret , otp , opts \\ [ ] )
304320
305- def valid? ( secret , << a1 , a2 , a3 , a4 , a5 , a6 >> , opts ) do
321+ def valid? ( secret , otp , opts ) when is_binary ( otp ) do
306322 time = opts |> Keyword . get ( :time , System . os_time ( :second ) ) |> to_unix ( )
307323 period = Keyword . get ( opts , :period , @ default_totp_period )
324+ digits = Keyword . get ( opts , :digits , @ default_digits )
308325
309- << e1 , e2 , e3 , e4 , e5 , e6 >> = verification_code ( secret , time , period )
326+ digits not in 6 .. 10 && raise ArgumentError , "digits must be between 6 and 10"
310327
311- ( bxor ( e1 , a1 ) ||| bxor ( e2 , a2 ) ||| bxor ( e3 , a3 ) ||| bxor ( e4 , a4 ) ||| bxor ( e5 , a5 ) |||
312- bxor ( e6 , a6 ) ) === 0 and not reused? ( time , period , opts )
328+ code = verification_code ( secret , time , period , digits )
329+
330+ byte_size ( code ) == byte_size ( otp ) and validate_digits ( code , otp ) == 0 and
331+ not reused? ( time , period , opts )
313332 end
314333
315334 def valid? ( _secret , _otp , _opts ) , do: false
316335
336+ @ spec validate_digits ( integer ( ) , integer ( ) ) :: :error | integer ( )
337+ defp validate_digits ( << e , e_rest :: binary >> , << a , a_rest :: binary >> ) do
338+ bxor ( e , a ) ||| validate_digits ( e_rest , a_rest )
339+ end
340+
341+ defp validate_digits ( << >> , << >> ) do
342+ 0
343+ end
344+
317345 @ spec reused? ( integer ( ) , pos_integer ( ) , [ option ( ) | validate_option ( ) ] ) :: boolean ( )
318346 defp reused? ( time , period , opts ) do
319347 if since = Keyword . get ( opts , :since ) do
0 commit comments