File: otp.rb

package info (click to toggle)
ruby-rotp 2.1.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: buster, stretch
  • size: 340 kB
  • ctags: 79
  • sloc: ruby: 726; makefile: 16
file content (90 lines) | stat: -rw-r--r-- 2,524 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
55
56
57
58
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
86
87
88
89
90
module ROTP
  class OTP
    attr_reader :secret, :digits, :digest

    # @param [String] secret in the form of base32
    # @option options digits [Integer] (6)
    #     Number of integers in the OTP
    #     Google Authenticate only supports 6 currently
    # @option options digest [String] (sha1)
    #     Digest used in the HMAC
    #     Google Authenticate only supports 'sha1' currently
    # @returns [OTP] OTP instantiation
    def initialize(s, options = {})
      @digits = options[:digits] || 6
      @digest = options[:digest] || "sha1"
      @secret = s
    end

    # @param [Integer] input the number used seed the HMAC
    # @option padded [Boolean] (false) Output the otp as a 0 padded string
    # Usually either the counter, or the computed integer
    # based on the Unix timestamp
    def generate_otp(input, padded=true)
      hmac = OpenSSL::HMAC.digest(
        OpenSSL::Digest.new(digest),
        byte_secret,
        int_to_bytestring(input)
      )

      offset = hmac[-1].ord & 0xf
      code = (hmac[offset].ord & 0x7f) << 24 |
        (hmac[offset + 1].ord & 0xff) << 16 |
        (hmac[offset + 2].ord & 0xff) << 8 |
        (hmac[offset + 3].ord & 0xff)
      if padded
        (code % 10 ** digits).to_s.rjust(digits, '0')
      else
        code % 10 ** digits
      end
    end

    private

    def verify(input, generated)
      unless input.is_a?(String) && generated.is_a?(String)
        raise ArgumentError, "ROTP only verifies strings - See: https://github.com/mdp/rotp/issues/32"
      end
      time_constant_compare(input, generated)
    end

    def byte_secret
      Base32.decode(@secret)
    end

    # Turns an integer to the OATH specified
    # bytestring, which is fed to the HMAC
    # along with the secret
    #
    def int_to_bytestring(int, padding = 8)
      result = []
      until int == 0
        result << (int & 0xFF).chr
        int >>=  8
      end
      result.reverse.join.rjust(padding, 0.chr)
    end

    # A very simple param encoder
    def encode_params(uri, params)
      params_str = "?"
      params.each do |k,v|
        if v
          params_str << "#{k}=#{CGI::escape(v.to_s)}&"
        end
      end
      params_str.chop!
      uri + params_str
    end

    # constant-time compare the strings
    def time_constant_compare(a, b)
      return false if a.empty? || b.empty? || a.bytesize != b.bytesize
      l = a.unpack "C#{a.bytesize}"
      res = 0
      b.each_byte { |byte| res |= byte ^ l.shift }
      res == 0
    end

  end
end