class Bnet::Authenticator

The Battle.net authenticator

Constants

VERSION

Attributes

region[R]

@!attribute [r] region @return [Symbol] region

restorecode[R]

@!attribute [r] restorecode @return [String] restoration code

secret[R]

@!attribute [r] secret @return [String] hexlified secret

serial[R]

@!attribute [r] serial @return [String] serial

Public Class Methods

create_one_time_pad(length) click to toggle source
# File lib/bnet/authenticator/core.rb, line 10
def create_one_time_pad(length)
  (0..1.0/0.0).reduce('') do |memo, i|
    break memo if memo.length >= length
    memo << Digest::SHA1.hexdigest(rand().to_s)
  end[0, length]
end
decrypt_response(text, key) click to toggle source
# File lib/bnet/authenticator/core.rb, line 17
def decrypt_response(text, key)
  text.bytes.zip(key.bytes).reduce('') do |memo, pair|
    memo + (pair[0] ^ pair[1]).chr
  end
end
get_token(secret, timestamp = nil) click to toggle source

Get token from given secret and timestamp @param secret [String] hexified secret @param timestamp [Integer] UNIX timestamp in seconds,

defaults to current time

@return [String, Integer] token and the next timestamp token to change

# File lib/bnet/authenticator.rb, line 105
def self.get_token(secret, timestamp = nil)
  secret = Bnet::Attributes::Secret.new secret

  current = (timestamp || Time.now.getutc.to_i) / 30
  digest = Digest::HMAC.digest([current].pack('Q>'), secret.binary, Digest::SHA1)
  start_position = digest[19].ord & 0xf

  token = digest[start_position, 4].unpack('L>')[0] & 0x7fffffff

  return '%08d' % (token % 100000000), (current + 1) * 30
end
new(serial, secret) click to toggle source

Create a new authenticator with given serial and secret @param serial [String] @param secret [String]

# File lib/bnet/authenticator.rb, line 34
def initialize(serial, secret)
  serial = Bnet::Attributes::Serial.new serial
  secret = Bnet::Attributes::Secret.new secret
  restorecode = Bnet::Attributes::Restorecode.new serial, secret

  @serial = serial.to_s
  @secret = secret.to_s
  @restorecode = restorecode.to_s
  @region = serial.region
end
request_authenticator(region) click to toggle source

Request a new authenticator from server @param region [Symbol] @return [Bnet::Authenticator]

# File lib/bnet/authenticator.rb, line 48
def self.request_authenticator(region)
  k = create_one_time_pad(37)

  payload_plain = "\1" + k + region.to_s + CLIENT_MODEL.ljust(16, "\0")[0, 16]
  e = rsa_encrypt_bin(payload_plain)

  response_body = request_for('new serial', region, ENROLLMENT_REQUEST_PATH, e)

  decrypted = decrypt_response(response_body[8, 37], k)

  Authenticator.new(decrypted[20, 17], decrypted[0, 20])
end
request_for(label, region, path, body = nil) click to toggle source
# File lib/bnet/authenticator/core.rb, line 28
def request_for(label, region, path, body = nil)
  raise BadInputError.new("bad region #{region}") unless AUTHENTICATOR_HOSTS.has_key? region

  request = body.nil? ? Net::HTTP::Get.new(path) : Net::HTTP::Post.new(path)
  request.content_type = 'application/octet-stream'
  request.body = body unless body.nil?

  response = Net::HTTP.new(AUTHENTICATOR_HOSTS[region]).start do |http|
    http.request request
  end

  if response.code.to_i != 200
    raise RequestFailedError.new("Error requesting #{label}: #{response.code}")
  end

  response.body
end
request_server_time(region) click to toggle source

Get server's time @param region [Symbol] @return [Integer] server timestamp in seconds

# File lib/bnet/authenticator.rb, line 95
def self.request_server_time(region)
  server_time_big_endian = request_for('server time', region, TIME_REQUEST_PATH)
  server_time_big_endian.unpack('Q>')[0].to_f / 1000
end
restore_authenticator(serial, restorecode) click to toggle source

Restore an authenticator from server @param serial [String] @param restorecode [String] @return [Bnet::Authenticator]

# File lib/bnet/authenticator.rb, line 65
def self.restore_authenticator(serial, restorecode)
  serial = Bnet::Attributes::Serial.new serial
  restorecode = Bnet::Attributes::Restorecode.new restorecode

  # stage 1
  challenge = request_for('restore (stage 1)',
                          serial.region,
                          RESTORE_INIT_REQUEST_PATH,
                          serial.normalized)

  # stage 2
  key = create_one_time_pad(20)

  digest = Digest::HMAC.digest(serial.normalized + challenge,
                               restorecode.binary,
                               Digest::SHA1)

  payload = serial.normalized + rsa_encrypt_bin(digest + key)

  response_body = request_for('restore (stage 2)',
                              serial.region,
                              RESTORE_VALIDATE_REQUEST_PATH,
                              payload)

  Authenticator.new(serial, decrypt_response(response_body, key))
end
rsa_encrypt_bin(bin) click to toggle source
# File lib/bnet/authenticator/core.rb, line 23
def rsa_encrypt_bin(bin)
  i = bin.unpack('C*').map{ |i| i.to_s(16).rjust(2, '0') }.join.to_i(16)
  (i ** RSA_KEY % RSA_MOD).to_s(16).scan(/.{2}/).map {|s| s.to_i(16)}.pack('C*')
end

Public Instance Methods

get_token(timestamp = nil) click to toggle source

Get authenticator's token from given timestamp @param timestamp [Integer] UNIX timestamp in seconds,

defaults to current time

@return [String, Integer] token and the next timestamp token to change

# File lib/bnet/authenticator.rb, line 121
def get_token(timestamp = nil)
  self.class.get_token(secret, timestamp)
end
to_hash() click to toggle source

Hash representation of this authenticator @return [Hash]

# File lib/bnet/authenticator.rb, line 127
def to_hash
  {
    :serial => serial,
    :secret => secret,
    :restorecode => restorecode,
    :region => region,
  }
end
to_readable_text() click to toggle source
# File lib/bnet/command.rb, line 5
def to_readable_text
  "Serial: #{serial}\nSecret: #{secret}\nRestoration Code: #{restorecode}"
end
to_s() click to toggle source

String representation of this authenticator @return [String]

# File lib/bnet/authenticator.rb, line 138
def to_s
  to_hash.to_s
end