Class: KeeperSecretsManager::TOTP
- Inherits:
-
Object
- Object
- KeeperSecretsManager::TOTP
- Defined in:
- lib/keeper_secrets_manager/totp.rb
Constant Summary collapse
- ALGORITHMS =
{ 'SHA1' => OpenSSL::Digest::SHA1, 'SHA256' => OpenSSL::Digest::SHA256, 'SHA512' => OpenSSL::Digest::SHA512 }.freeze
Class Method Summary collapse
-
.generate_code(secret, time: Time.now, algorithm: 'SHA1', digits: 6, period: 30) ⇒ String
Generate a TOTP code.
-
.generate_secret(length: 20) ⇒ String
Generate a random secret suitable for TOTP.
-
.generate_url(account, secret, issuer: nil, algorithm: 'SHA1', digits: 6, period: 30) ⇒ String
Generate TOTP URL.
-
.parse_url(url) ⇒ Hash
Parse TOTP URL (otpauth://totp/…).
-
.validate_code(secret, code, time: Time.now, window: 1, algorithm: 'SHA1', digits: 6, period: 30) ⇒ Boolean
Validate a TOTP code.
Class Method Details
.generate_code(secret, time: Time.now, algorithm: 'SHA1', digits: 6, period: 30) ⇒ String
Generate a TOTP code
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 |
# File 'lib/keeper_secrets_manager/totp.rb', line 23 def self.generate_code(secret, time: Time.now, algorithm: 'SHA1', digits: 6, period: 30) # Validate inputs raise ArgumentError, "Invalid algorithm: #{algorithm}" unless ALGORITHMS.key?(algorithm) raise ArgumentError, 'Digits must be 6 or 8' unless [6, 8].include?(digits) raise ArgumentError, 'Period must be positive' unless period.positive? # Decode base32 secret key = Base32.decode(secret.upcase.tr(' ', '')) # Calculate time counter counter = (time.to_i / period).floor # Convert counter to 8-byte string (big-endian) counter_bytes = [counter].pack('Q>') # Generate HMAC digest = ALGORITHMS[algorithm].new hmac = OpenSSL::HMAC.digest(digest, key, counter_bytes) # Extract dynamic binary code offset = hmac[-1].ord & 0x0f code = (hmac[offset].ord & 0x7f) << 24 | (hmac[offset + 1].ord & 0xff) << 16 | (hmac[offset + 2].ord & 0xff) << 8 | (hmac[offset + 3].ord & 0xff) # Generate final OTP value otp = code % (10**digits) # Pad with leading zeros if necessary otp.to_s.rjust(digits, '0') end |
.generate_secret(length: 20) ⇒ String
Generate a random secret suitable for TOTP
135 136 137 138 |
# File 'lib/keeper_secrets_manager/totp.rb', line 135 def self.generate_secret(length: 20) random_bytes = OpenSSL::Random.random_bytes(length) Base32.encode(random_bytes).delete('=') end |
.generate_url(account, secret, issuer: nil, algorithm: 'SHA1', digits: 6, period: 30) ⇒ String
Generate TOTP URL
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/keeper_secrets_manager/totp.rb', line 95 def self.generate_url(account, secret, issuer: nil, algorithm: 'SHA1', digits: 6, period: 30) label = issuer ? "#{issuer}:#{account}" : account params = { 'secret' => secret, 'algorithm' => algorithm, 'digits' => digits, 'period' => period } params['issuer'] = issuer if issuer query = URI.encode_www_form(params) "otpauth://totp/#{URI.encode_www_form_component(label)}?#{query}" end |
.parse_url(url) ⇒ Hash
Parse TOTP URL (otpauth://totp/…)
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 |
# File 'lib/keeper_secrets_manager/totp.rb', line 59 def self.parse_url(url) uri = URI(url) raise ArgumentError, 'Invalid TOTP URL scheme' unless uri.scheme == 'otpauth' raise ArgumentError, 'Invalid TOTP URL type' unless uri.host == 'totp' # Extract label (issuer:account or just account) path = uri.path[1..-1] # Remove leading / if path.include?(':') issuer, account = path.split(':', 2) else account = path issuer = nil end # Parse query parameters params = URI.decode_www_form(uri.query || '').to_h { 'account' => URI.decode_www_form_component(account || ''), 'issuer' => issuer ? URI.decode_www_form_component(issuer) : params['issuer'], 'secret' => params['secret'], 'algorithm' => params['algorithm'] || 'SHA1', 'digits' => (params['digits'] || '6').to_i, 'period' => (params['period'] || '30').to_i } end |
.validate_code(secret, code, time: Time.now, window: 1, algorithm: 'SHA1', digits: 6, period: 30) ⇒ Boolean
Validate a TOTP code
120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/keeper_secrets_manager/totp.rb', line 120 def self.validate_code(secret, code, time: Time.now, window: 1, algorithm: 'SHA1', digits: 6, period: 30) # Check current time and window (-window..window).each do |offset| test_time = time + (offset * period) test_code = generate_code(secret, time: test_time, algorithm: algorithm, digits: digits, period: period) return true if test_code == code end false end |