Class: Junkfood::OneTime

Inherits:
Object
  • Object
show all
Includes:
Wrong::Assert
Defined in:
lib/junkfood/one_time.rb

Overview

Implements HMAC One Time Passwords using SHA-1 digests.

Examples:


# Using the class methods to get OTP one at a time.
key = '12345678901234567890'
puts Junkfood::OneTime.hotp(key, 0) #=> 755224
puts Junkfood::OneTime.hotp(key, 1) #=> 287082

# Changing the length of the OTP
key = '12345678901234567890'
puts Junkfood::OneTime.hotp(key, 0, :digits => 8) #=> 84755224

# Using the class methods to get multiple OTP at a time.
key = '12345678901234567890'
puts Junkfood::OneTime.hotp_multi(key, 0..1) #=> [755224, 287082]

# Create a new OTP generator starting at counter 0.
key = '12345678901234567890'
one_time = Junkfood::OneTime.new key
# Get an OTP, but don't advance the counter
puts one_time.otp #=> 755224
puts one_time.otp #=> 755224
# Get a range of OTP
puts one_time.otp :range => 2 #=> [755224, 287082]
# Get an OTP, and advance the counter
puts one_time.otp! #=> 755224
puts one_time.otp! #=> 287082
puts one_time.counter #=> 2
puts one_time.otp! :range => 2 #=> [359152, 969429]
puts one_time.counter #=> 4

# The current Time based OTP for the current epoch step.
key = '12345678901234567890'
one_time = Junkfood::OneTime.new key
puts one_time.totp
# A bunch of OTPs preceding and following the current epoch step OTP.
puts one_time.totp :radius => 2

# Setting the length and counter of the OTP on a OneTime instance
one_time = Junkfood::OneTime.new key, :digits => 8, :counter => 2
puts one_time.otp! #=> 37359152
puts one_time.otp! #=> 26969429

See Also:

Constant Summary collapse

DEFAULT_DIGITS =

Default number of digits for each OTP.

6
DEFAULT_STEP_SIZE =

Default number of seconds for each step in the Time Epoch calculation.

30
MAX_RADIUS =

Max number of OTPs preceding and following the current Time based OTP allowed in the Time based OTP method.

10

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(secret, options = {}) ⇒ OneTime

Returns a new instance of OneTime.

Parameters:

  • secret (String)

    the secret key used for the HMAC calculation.

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :counter (Fixnum) — default: 0

    the htop counter to start at.

  • :digits (Fixnum) — default: 6

    size of each OTP.

  • :time_digits (Fixnum) — default: 6

    size of each Time Based OTP.

  • :time_step_size (Fixnum) — default: 30

    number of seconds for each block in calculating current counter in Time Based OTP.



99
100
101
102
103
104
105
# File 'lib/junkfood/one_time.rb', line 99

def initialize(secret, options={})
  @secret = secret
  @counter = options[:counter] || 0
  @digits = options[:digits] || DEFAULT_DIGITS
  @time_digits = options[:time_digits] || @digits
  @time_step_size = options[:time_step_size] || DEFAULT_STEP_SIZE
end

Instance Attribute Details

#counterObject (readonly)

Returns the value of attribute counter.



82
83
84
# File 'lib/junkfood/one_time.rb', line 82

def counter
  @counter
end

Class Method Details

.epoch_counter(options = {}) ⇒ Object

Generate the counter based on the time and step_size.

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :time (Time) — default: Time.now

    the time to use.

  • :step_size (Fixnum) — default: 30

    the step size.

Returns:

  • the time based counter.



229
230
231
232
233
# File 'lib/junkfood/one_time.rb', line 229

def self.epoch_counter(options={})
  time = options[:time] || Time.now.to_i
  step_size = options[:step_size] || DEFAULT_STEP_SIZE
  return time / step_size
end

.hotp(secret, counter = 0, options = {}) ⇒ String

Generate an individual OTP.

Parameters:

  • secret (String)

    the secret key used for the HMAC.

  • counter (Fixnum) (defaults to: 0)

    the counter used to generate the OTP.

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :digits (Fixnum) — default: 6

    size of each OTP.

Returns:

  • (String)

    the generated OTP.



159
160
161
162
# File 'lib/junkfood/one_time.rb', line 159

def self.hotp(secret, counter=0, options={})
  results = hotp_raw secret, counter, (options[:digits] || DEFAULT_DIGITS)
  return results.first
end

.hotp_multi(secret, range, options = {}) ⇒ String

Generate a set of OTPs.

Parameters:

  • secret (String)

    the secret key used for the HMAC calculation.

  • range (Fixnum, Range)

    counters for which to generate OTPs

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :digits (Fixnum) — default: 6

    size of each OTP.

Returns:

  • (String)

    the generated OTPs.



171
172
173
174
175
176
177
# File 'lib/junkfood/one_time.rb', line 171

def self.hotp_multi(secret, range, options={})
  digits = options[:digits] || DEFAULT_DIGITS
  range = range..range if range.kind_of? Fixnum
  range.map do |c|
    (hotp_raw secret, c, digits).first
  end
end

.hotp_raw(secret, counter = 0, digits = DEFAULT_DIGITS) ⇒ Array<String,Fixnum,String>

Generate the OTP along with additional debug information.

Parameters:

  • secret (String)

    the secret key used for the HMAC calculation.

  • counter (Fixnum) (defaults to: 0)

    the htop counter to use at.

  • digits (Fixnum) (defaults to: DEFAULT_DIGITS)

    size of each OTP.

Returns:

  • (Array<String,Fixnum,String>)

    The generated OTP, the Dynamic Binary Code, and the calculated HMAC digest for the OTP.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/junkfood/one_time.rb', line 188

def self.hotp_raw(secret, counter=0, digits=DEFAULT_DIGITS)
  # TODO: figure out a better way to turn fixnum into an 8byte buffer string
  counter_bytes = []
  x = counter
  for i in 0..7
    byte = x & 0xff
    x >>= 8
    counter_bytes.unshift byte
  end
  digest_data = counter_bytes.pack('C8')

  # SHA1 digest is guaranteed to produce a 20 byte binary string.
  # We unpack the string into an array of 8-bit bytes.
  digest = OpenSSL::HMAC.digest(
    OpenSSL::Digest::Digest.new('sha1'),
    secret,
    digest_data)
  digest_array = digest.unpack('C20')

  # Based on the HMAC OTP algorithm, we use the last 4 bits of the binary
  # string to find the 'dbc' value. The 4 bits is the offset of the
  # hmac bytes array. From which, we extract 4 bytes for the 'dbc'.
  # This is the "Dynamic Truncation".
  # We zero the most significant bit of the 'dbc' to get a 31-bit
  # unsigned big-endian integer. This dbc (converted to a Ruby Fixnum).
  # From the fix num, we modulo 10^digits to get the digits for the HOTP.
  # This is the "Compute HOTP value" step
  offset = digest_array.last & 0x0f
  dbc = digest_array[offset..(offset+3)]
  dbc[0] &= 0x7f
  dbc = dbc.pack('C4').unpack('N').first
  otp = (dbc % 10**digits).to_s.rjust(digits,'0')
  return otp, dbc, digest
end

Instance Method Details

#otp(options = {}) ⇒ Array<String>, String

Generate counter based OTPs without advancing the counter.

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :range (Fixnum) — default: 1

    number of OTPs to generate.

Returns:

  • (Array<String>, String)

    the generated OTPs.



124
125
126
127
128
129
130
131
132
133
134
# File 'lib/junkfood/one_time.rb', line 124

def otp(options={})
  range = options[:range] || 1
  if range <= 1
    return self.class.hotp(@secret, @counter, :digits => @digits)
  else
    return self.class.hotp_multi(
      @secret,
      @counter...(@counter + range),
      :digits => @digits)
  end
end

#otp!(options = {}) ⇒ Array<String>, String

Generate counter based OTPs and advance the counter.

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :range (Fixnum) — default: 1

    number of OTPs to generate.

Returns:

  • (Array<String>, String)

    the generated OTPs.



112
113
114
115
116
117
# File 'lib/junkfood/one_time.rb', line 112

def otp!(options={})
  range = options[:range] || 1
  result = otp :range => range
  @counter += range
  return result
end

#totp(options = {}) ⇒ Array<String>, String

Generate Time Based OTPs based on the current time and time steps.

Parameters:

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :radius (Fixnum) — default: 0

    number of additional OTPs preceding and following the current Time OTP to generate.

Returns:

  • (Array<String>, String)

    the generated OTPs.



142
143
144
145
146
147
148
149
150
# File 'lib/junkfood/one_time.rb', line 142

def totp(options={})
  radius = options[:radius] || 0
  assert{radius.kind_of?(Fixnum) && radius >= 0 && radius <= MAX_RADIUS}
  c = self.class.epoch_counter(:step_size => @time_step_size)
  start_counter = max(c - radius, 0)
  range = start_counter..(c+radius)
  results = self.class.hotp_multi @secret, range, :digits => @time_digits
  return results.size <= 1 ? results.first : results
end