Created
July 13, 2016 20:10
-
-
Save johntdyer/55b9a1c1840bd3de7d934a95734d5af8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'openssl' | |
# Module for encoding and decoding in Base32 per RFC 3548 | |
module Base32 | |
TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.freeze | |
@table = TABLE | |
class <<self | |
attr_reader :table | |
end | |
class Chunk | |
def initialize(bytes) | |
@bytes = bytes | |
end | |
def decode | |
bytes = @bytes.take_while {|c| c != 61} # strip padding | |
n = (bytes.length * 5.0 / 8.0).floor | |
p = bytes.length < 8 ? 5 - (n * 8) % 5 : 0 | |
c = bytes.inject(0) do |m,o| | |
i = Base32.table.index(o.chr) | |
raise ArgumentError, "invalid character '#{o.chr}'" if i.nil? | |
(m << 5) + i | |
end >> p | |
(0..n-1).to_a.reverse.collect {|i| ((c >> i * 8) & 0xff).chr} | |
end | |
def encode | |
n = (@bytes.length * 8.0 / 5.0).ceil | |
p = n < 8 ? 5 - (@bytes.length * 8) % 5 : 0 | |
c = @bytes.inject(0) {|m,o| (m << 8) + o} << p | |
[(0..n-1).to_a.reverse.collect {|i| Base32.table[(c >> i * 5) & 0x1f].chr}, | |
("=" * (8-n))] | |
end | |
end | |
def self.chunks(str, size) | |
result = [] | |
bytes = str.bytes | |
while bytes.any? do | |
result << Chunk.new(bytes.take(size)) | |
bytes = bytes.drop(size) | |
end | |
result | |
end | |
def self.encode(str) | |
chunks(str, 5).collect(&:encode).flatten.join | |
end | |
def self.decode(str) | |
chunks(str, 8).collect(&:decode).flatten.join | |
end | |
def self.random_base32(length=16, padding=true) | |
random = '' | |
OpenSSL::Random.random_bytes(length).each_byte do |b| | |
random << self.table[b % 32] | |
end | |
padding ? random.ljust((length / 8.0).ceil * 8, '=') : random | |
end | |
def self.table=(table) | |
raise ArgumentError, "Table must have 32 unique characters" unless self.table_valid?(table) | |
@table = table | |
end | |
def self.table_valid?(table) | |
table.bytes.to_a.size == 32 && table.bytes.to_a.uniq.size == 32 | |
end | |
end | |
module TOTP | |
# Generate a random secret | |
def self.secret | |
return Base32.encode((0...10).map { rand(255).chr }.join) | |
end | |
# Return whether or not the key is valid for the given secret | |
def self.valid?(secret, pass, time = Time.now) | |
return self.passwords(secret, time).include?(pass) | |
end | |
def self.totp(hmac, time) | |
bytes = [time].pack('>q').reverse | |
hmac.reset | |
hmac.update(bytes) | |
code = hmac.digest | |
offs = code[-1].ord & 0x0F | |
hash = code[offs...offs + 4] | |
pass = hash.reverse.unpack('L')[0] | |
pass &= 0x7FFFFFFF | |
pass %= 1000000 | |
return pass | |
end | |
# Generate passwords based on the secret and time | |
def self.passwords(secret, time = Time.now) | |
interval = time.to_i / 30 | |
hmac = OpenSSL::HMAC.new( | |
Base32.decode(secret), | |
OpenSSL::Digest::SHA1.new, | |
) | |
# Cover three 30 second intervals | |
return [ | |
totp(hmac, interval.pred), | |
totp(hmac, interval), | |
totp(hmac, interval.succ), | |
] | |
end | |
end | |
# Start code | |
$mySecret = 'YYZ27CO4WZTPZAYX' | |
log "## mySecret-> #{$mySecret}" | |
log "## OTP ----> #{TOTP.passwords($mySecret)}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'openssl' | |
# Module for encoding and decoding in Base32 per RFC 3548 | |
module Base32 | |
TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.freeze | |
@table = TABLE | |
class <<self | |
attr_reader :table | |
end | |
class Chunk | |
def initialize(bytes) | |
@bytes = bytes | |
end | |
def decode | |
bytes = @bytes.take_while {|c| c != 61} # strip padding | |
n = (bytes.length * 5.0 / 8.0).floor | |
p = bytes.length < 8 ? 5 - (n * 8) % 5 : 0 | |
c = bytes.inject(0) do |m,o| | |
i = Base32.table.index(o.chr) | |
raise ArgumentError, "invalid character '#{o.chr}'" if i.nil? | |
(m << 5) + i | |
end >> p | |
(0..n-1).to_a.reverse.collect {|i| ((c >> i * 8) & 0xff).chr} | |
end | |
def encode | |
n = (@bytes.length * 8.0 / 5.0).ceil | |
p = n < 8 ? 5 - (@bytes.length * 8) % 5 : 0 | |
c = @bytes.inject(0) {|m,o| (m << 8) + o} << p | |
[(0..n-1).to_a.reverse.collect {|i| Base32.table[(c >> i * 5) & 0x1f].chr}, | |
("=" * (8-n))] | |
end | |
end | |
def self.chunks(str, size) | |
result = [] | |
bytes = str.bytes | |
while bytes.any? do | |
result << Chunk.new(bytes.take(size)) | |
bytes = bytes.drop(size) | |
end | |
result | |
end | |
def self.encode(str) | |
chunks(str, 5).collect(&:encode).flatten.join | |
end | |
def self.decode(str) | |
chunks(str, 8).collect(&:decode).flatten.join | |
end | |
def self.random_base32(length=16, padding=true) | |
random = '' | |
OpenSSL::Random.random_bytes(length).each_byte do |b| | |
random << self.table[b % 32] | |
end | |
padding ? random.ljust((length / 8.0).ceil * 8, '=') : random | |
end | |
def self.table=(table) | |
raise ArgumentError, "Table must have 32 unique characters" unless self.table_valid?(table) | |
@table = table | |
end | |
def self.table_valid?(table) | |
table.bytes.to_a.size == 32 && table.bytes.to_a.uniq.size == 32 | |
end | |
end | |
module TOTP | |
# Generate a random secret | |
def self.secret | |
return Base32.encode((0...10).map { rand(255).chr }.join) | |
end | |
# Return whether or not the key is valid for the given secret | |
def self.valid?(secret, pass, time = Time.now) | |
return self.passwords(secret, time).include?(pass) | |
end | |
def self.totp(hmac, time) | |
bytes = [time].pack('>q').reverse | |
hmac.reset | |
hmac.update(bytes) | |
code = hmac.digest | |
offs = code[-1].ord & 0x0F | |
hash = code[offs...offs + 4] | |
pass = hash.reverse.unpack('L')[0] | |
pass &= 0x7FFFFFFF | |
pass %= 1000000 | |
return pass | |
end | |
# Generate passwords based on the secret and time | |
def self.passwords(secret, time = Time.now) | |
interval = time.to_i / 30 | |
hmac = OpenSSL::HMAC.new( | |
Base32.decode(secret), | |
OpenSSL::Digest::SHA1.new, | |
) | |
# Cover three 30 second intervals | |
return [ | |
totp(hmac, interval.pred), | |
totp(hmac, interval), | |
totp(hmac, interval.succ), | |
] | |
end | |
end | |
puts TOTP.valid?('YYZ27CO4WZTPZAYX', ARGV[0].to_i) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment