Skip to content

Commit 1554831

Browse files
authored
Add :algorithm option (#41)
1 parent 5083304 commit 1554831

2 files changed

Lines changed: 67 additions & 11 deletions

File tree

lib/nimble_totp.ex

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ defmodule NimbleTOTP do
6565
uri |> EQRCode.encode() |> EQRCode.svg()
6666
#=> "<?xml version=\\"1.0\\" standalone=\\"yes\\"?>\\n<svg version=\\"1.1\\" ...
6767
68-
uri = NimbleTOTP.otpauth_uri("Acme", "alice", secret, digits: 8)
69-
#=> "otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme"
68+
uri = NimbleTOTP.otpauth_uri("Acme", "alice", secret, digits: 8, algorithm: :sha512)
69+
#=> "otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme&digits=8&algorithm=sha512"
7070
uri |> EQRCode.encode() |> EQRCode.svg()
7171
#=> "<?xml version=\\"1.0\\" standalone=\\"yes\\"?>\\n<svg version=\\"1.1\\" ...
7272
@@ -138,6 +138,7 @@ defmodule NimbleTOTP do
138138
import Bitwise
139139
@default_digits 6
140140
@default_totp_period 30
141+
@default_algorithm :sha
141142

142143
@typedoc "Unix time in seconds, `t:DateTime.t/0` or `t:NaiveDateTime.t/0`."
143144
@type time() :: DateTime.t() | NaiveDateTime.t() | integer()
@@ -224,6 +225,8 @@ defmodule NimbleTOTP do
224225
If this option is given to `verification_code/2`, it must also be given to `valid?/3`.
225226
* `:digits` - The desired length of the totp. Default is 6.
226227
If this option is given to `verification_code/2`, it must also be given to `valid?/3` and `otpauth_uri/3`/`otpauth_uri/4`.
228+
* `:algorithm` - The algorithm to use for the totp. Default is :sha.
229+
If this option is given to `verification_code/2`, it must also be given to `valid?/3` and `otpauth_uri/3`/`otpauth_uri/4`.
227230
228231
## Examples
229232
@@ -237,31 +240,37 @@ defmodule NimbleTOTP do
237240
time = opts |> Keyword.get_lazy(:time, fn -> System.os_time(:second) end) |> to_unix()
238241
period = Keyword.get(opts, :period, @default_totp_period)
239242
digits = Keyword.get(opts, :digits, @default_digits)
243+
algorithm = Keyword.get(opts, :algorithm, @default_algorithm)
240244

241245
digits not in 6..10 && raise ArgumentError, "digits must be between 6 and 10"
242246

243-
verification_code(secret, time, period, digits)
247+
algorithm not in [:sha, :sha256, :sha512] &&
248+
raise ArgumentError, "algorithm must be one of :sha, :sha256, :sha512"
249+
250+
verification_code(secret, time, period, digits, algorithm)
244251
end
245252

246-
@spec verification_code(binary(), integer(), pos_integer(), integer()) :: binary()
247-
defp verification_code(secret, time, period, digits) do
253+
@spec verification_code(binary(), integer(), pos_integer(), integer(), atom()) :: binary()
254+
defp verification_code(secret, time, period, digits, algorithm) do
248255
secret
249-
|> hmac(time, period)
256+
|> hmac(time, period, algorithm)
250257
|> hmac_truncate()
251258
|> rem(Integer.pow(10, digits))
252259
|> to_string()
253260
|> String.pad_leading(digits, "0")
254261
end
255262

256-
defp hmac(secret, time, period) do
263+
defp hmac(secret, time, period, algorithm) do
257264
moving_factor = <<Integer.floor_div(time, period)::64>>
258-
hmac_sha(secret, moving_factor)
265+
hmac_sha(secret, moving_factor, algorithm)
259266
end
260267

261-
defp hmac_sha(key, data), do: :crypto.mac(:hmac, :sha, key, data)
268+
defp hmac_sha(key, data, algorithm), do: :crypto.mac(:hmac, algorithm, key, data)
262269

263270
defp hmac_truncate(hmac) do
264-
<<_::19-binary, _::4, offset::4>> = hmac
271+
key_length = byte_size(hmac) - 1
272+
273+
<<_::size(key_length)-binary, _::4, offset::4>> = hmac
265274
<<_::size(offset)-binary, p::4-binary, _::binary>> = hmac
266275
<<_::1, bits::31>> = p
267276
bits
@@ -284,6 +293,9 @@ defmodule NimbleTOTP do
284293
* `:digits` - The desired length of the totp. Default is 6.
285294
If this option is given to `verification_code/2`, it must also be given to `valid?/3` and `otpauth_uri/3`/`otpauth_uri/4`.
286295
296+
* `:algorithm` - The algorithm to use for the totp. Default is :sha.
297+
If this option is given to `verification_code/2`, it must also be given to `valid?/3` and `otpauth_uri/3`/`otpauth_uri/4`.
298+
287299
## Preventing TOTP code reuse
288300
289301
The `:since` option can be used to prevent TOTP codes from being reused. When set
@@ -317,10 +329,14 @@ defmodule NimbleTOTP do
317329
time = opts |> Keyword.get(:time, System.os_time(:second)) |> to_unix()
318330
period = Keyword.get(opts, :period, @default_totp_period)
319331
digits = Keyword.get(opts, :digits, @default_digits)
332+
algorithm = Keyword.get(opts, :algorithm, @default_algorithm)
320333

321334
digits not in 6..10 && raise ArgumentError, "digits must be between 6 and 10"
322335

323-
code = verification_code(secret, time, period, digits)
336+
algorithm not in [:sha, :sha256, :sha512] &&
337+
raise ArgumentError, "algorithm must be one of :sha, :sha256, :sha512"
338+
339+
code = verification_code(secret, time, period, digits, algorithm)
324340

325341
byte_size(code) == byte_size(otp) and validate_digits(code, otp) == 0 and
326342
not reused?(time, period, opts)

test/nimble_totp_test.exs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ defmodule NimbleTOTPTest do
5757
"""
5858
end
5959

60+
test "Generate the uri with extra params (deprecated, with algorithm option)" do
61+
secret = Base.decode32!("PTEPUGZ7DUWTBGMW4WLKB6U63MGKKMCA")
62+
app = "Bytepack App"
63+
64+
assert NimbleTOTP.otpauth_uri("#{app}:user@test.com", secret,
65+
issuer: app,
66+
algorithm: :sha512
67+
) == """
68+
otpauth://totp/Bytepack%20App:user@test.com?\
69+
secret=PTEPUGZ7DUWTBGMW4WLKB6U63MGKKMCA&\
70+
issuer=Bytepack%20App&\
71+
algorithm=sha512\
72+
"""
73+
end
74+
6075
test "raises error if issuer contains a colon (otpauth_uri/4)" do
6176
secret = Base.decode32!("PTEPUGZ7DUWTBGMW4WLKB6U63MGKKMCA")
6277

@@ -137,6 +152,22 @@ defmodule NimbleTOTPTest do
137152

138153
assert code1 == code2
139154
end
155+
156+
test "generate different codes with different algorithm" do
157+
secret = NimbleTOTP.secret()
158+
time1 = ~N[2020-04-08 17:49:59Z]
159+
time2 = ~N[2020-04-08 17:50:00Z]
160+
time3 = ~N[2020-04-08 17:50:30Z]
161+
162+
code1 = NimbleTOTP.verification_code(secret, time: time1, algorithm: :sha)
163+
assert code1 == NimbleTOTP.verification_code(secret, time: time1, algorithm: :sha)
164+
165+
code2 = NimbleTOTP.verification_code(secret, time: time2, algorithm: :sha256)
166+
assert code2 == NimbleTOTP.verification_code(secret, time: time2, algorithm: :sha256)
167+
168+
code3 = NimbleTOTP.verification_code(secret, time: time3, algorithm: :sha512)
169+
assert code3 == NimbleTOTP.verification_code(secret, time: time3, algorithm: :sha512)
170+
end
140171
end
141172

142173
describe "valid?/2" do
@@ -233,6 +264,15 @@ defmodule NimbleTOTPTest do
233264
end
234265
end
235266

267+
test "rejects if given an invalid algorithm" do
268+
time = System.os_time(:second)
269+
secret = NimbleTOTP.secret()
270+
271+
assert_raise ArgumentError, "algorithm must be one of :sha, :sha256, :sha512", fn ->
272+
NimbleTOTP.verification_code(secret, time: time, algorithm: :invalid)
273+
end
274+
end
275+
236276
test "returns false if the otp is not a binary" do
237277
secret = NimbleTOTP.secret()
238278
refute NimbleTOTP.valid?(secret, 123_456, time: System.os_time(:second))

0 commit comments

Comments
 (0)