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
- .char_to_bits(char) ⇒ Object
- .decode_base32(secret) ⇒ Object
- .generate(secret, at: Time.now, digits: 6, period: 30, digest: "SHA1") ⇒ Object
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 |