Module: Browserctl::Flows::TOTP

Defined in:
lib/browserctl/flows/stdlib/totp_2fa.rb

Overview

RFC 6238 TOTP code generation from a base32 secret. Pure Ruby; no network and no external gem.

Constant Summary collapse

BASE32_ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"

Class Method Summary collapse

Class Method Details

.char_to_bits(char) ⇒ Object



32
33
34
35
36
# File 'lib/browserctl/flows/stdlib/totp_2fa.rb', line 32

def char_to_bits(char)
  idx = BASE32_ALPHABET.index(char) or
    raise ArgumentError, "invalid base32 char #{char.inspect}"
  idx.to_s(2).rjust(5, "0")
end

.decode_base32(secret) ⇒ Object



25
26
27
28
29
30
# File 'lib/browserctl/flows/stdlib/totp_2fa.rb', line 25

def decode_base32(secret)
  cleaned = secret.to_s.upcase.gsub(/[^A-Z2-7]/, "")
  bits = cleaned.each_char.map { |c| char_to_bits(c) }.join
  whole_bytes = bits[0, (bits.length / 8) * 8]
  whole_bytes.scan(/.{8}/).map { |b| b.to_i(2).chr }.join
end

.generate(secret, at: Time.now, digits: 6, period: 30, digest: "SHA1") ⇒ Object



13
14
15
16
17
18
19
20
21
# File 'lib/browserctl/flows/stdlib/totp_2fa.rb', line 13

def generate(secret, at: Time.now, digits: 6, period: 30, digest: "SHA1")
  counter   = (at.to_i / period).to_i
  key       = decode_base32(secret)
  counter_b = [counter].pack("Q>") # 64-bit big-endian
  hmac      = OpenSSL::HMAC.digest(digest, key, counter_b)
  offset    = hmac[-1].ord & 0x0f
  truncated = hmac[offset, 4].unpack1("N") & 0x7fffffff
  truncated.to_s.rjust(digits, "0")[-digits..]
end