Class: Bnet::Authenticator

Inherits:
Object
  • Object
show all
Defined in:
lib/bnet/authenticator.rb

Overview

The Battle.net authenticator

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(serial, secret) ⇒ Authenticator

Create a new authenticator with given serial and secret

Parameters:

  • serial (String)
  • secret (String)

Raises:

  • (BadInputError)


32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/bnet/authenticator.rb', line 32

def initialize(serial, secret)
  raise BadInputError.new("bad serial #{serial}") unless self.class.is_valid_serial?(serial)
  raise BadInputError.new("bad secret #{secret}") unless self.class.is_valid_secret?(secret)

  normalized_serial = self.class.normalize_serial(serial)

  @serial = self.class.prettify_serial(normalized_serial)
  @secret = secret
  @region = self.class.extract_region(normalized_serial)

  restorecode_bin = Digest::SHA1.digest(normalized_serial + secret.as_hex_to_bin)
  @restorecode = self.class.encode_restorecode(restorecode_bin.split(//).last(10).join)
end

Instance Attribute Details

#regionSymbol (readonly)

Returns region.

Returns:

  • (Symbol)

    region



27
28
29
# File 'lib/bnet/authenticator.rb', line 27

def region
  @region
end

#restorecodeString (readonly)

Returns restoration code.

Returns:

  • (String)

    restoration code



23
24
25
# File 'lib/bnet/authenticator.rb', line 23

def restorecode
  @restorecode
end

#secretString (readonly)

Returns hexified secret.

Returns:

  • (String)

    hexified secret



19
20
21
# File 'lib/bnet/authenticator.rb', line 19

def secret
  @secret
end

#serialString (readonly)

Returns serial.

Returns:

  • (String)

    serial



15
16
17
# File 'lib/bnet/authenticator.rb', line 15

def serial
  @serial
end

Class Method Details

.create_one_time_pad(length) ⇒ Object



182
183
184
185
186
187
# File 'lib/bnet/authenticator.rb', line 182

def create_one_time_pad(length)
  (0..1.0/0.0).reduce('') do |memo, i|
    break memo if memo.length >= length
    memo << Digest::SHA1.digest(rand().to_s)
  end[0, length]
end

.decode_restorecode(str) ⇒ Object



176
177
178
179
180
# File 'lib/bnet/authenticator.rb', line 176

def decode_restorecode(str)
  str.bytes.map do |c|
    RESTORECODE_MAP_INVERSE[c]
  end.as_bytes_to_bin
end

.decrypt_response(text, key) ⇒ Object



189
190
191
192
193
# File 'lib/bnet/authenticator.rb', line 189

def decrypt_response(text, key)
  text.bytes.zip(key.bytes).reduce('') do |memo, pair|
    memo + (pair[0] ^ pair[1]).chr
  end
end

.encode_restorecode(bin) ⇒ Object



170
171
172
173
174
# File 'lib/bnet/authenticator.rb', line 170

def encode_restorecode(bin)
  bin.bytes.map do |v|
    RESTORECODE_MAP[v & 0x1f]
  end.as_bytes_to_bin
end

.extract_region(serial) ⇒ Object



150
151
152
# File 'lib/bnet/authenticator.rb', line 150

def extract_region(serial)
  serial[0, 2].upcase.to_sym
end

.get_token(secret, timestamp = nil) ⇒ String, Integer

Get token from given secret and timestamp

Parameters:

  • secret (String)

    hexified secret

  • timestamp (Integer) (defaults to: nil)

    UNIX timestamp in seconds, defaults to current time

Returns:

  • (String, Integer)

    token and the next timestamp token to change

Raises:

  • (BadInputError)


105
106
107
108
109
110
111
112
113
114
# File 'lib/bnet/authenticator.rb', line 105

def self.get_token(secret, timestamp = nil)
  raise BadInputError.new("bad seret #{secret}") unless is_valid_secret?(secret)

  current = (timestamp || Time.now.getutc.to_i) / 30
  digest = Digest::HMAC.digest([current].pack('Q>'), secret.as_hex_to_bin, Digest::SHA1)
  start_position = digest[19].ord & 0xf
  token = '%08d' % (digest[start_position, 4].as_bin_to_i % 100000000)

  return token, (current + 1) * 30
end

.is_valid_region?(region) ⇒ Boolean

Returns:

  • (Boolean)


162
163
164
# File 'lib/bnet/authenticator.rb', line 162

def is_valid_region?(region)
  AUTHENTICATOR_HOSTS.has_key? region
end

.is_valid_restorecode?(restorecode) ⇒ Boolean

Returns:

  • (Boolean)


166
167
168
# File 'lib/bnet/authenticator.rb', line 166

def is_valid_restorecode?(restorecode)
  restorecode =~ /[0-9A-Z]{10}/
end

.is_valid_secret?(secret) ⇒ Boolean

Returns:

  • (Boolean)


158
159
160
# File 'lib/bnet/authenticator.rb', line 158

def is_valid_secret?(secret)
  secret =~ /[0-9a-f]{40}/i
end

.is_valid_serial?(serial) ⇒ Boolean

Returns:

  • (Boolean)


141
142
143
144
# File 'lib/bnet/authenticator.rb', line 141

def is_valid_serial?(serial)
  normalized_serial = normalize_serial(serial)
  normalized_serial =~ Regexp.new("^(#{AUTHENTICATOR_HOSTS.keys.join('|')})\\d{12}$") && is_valid_region?(extract_region(normalized_serial))
end

.normalize_serial(serial) ⇒ Object



146
147
148
# File 'lib/bnet/authenticator.rb', line 146

def normalize_serial(serial)
  serial.upcase.gsub(/-/, '')
end

.prettify_serial(serial) ⇒ Object



154
155
156
# File 'lib/bnet/authenticator.rb', line 154

def prettify_serial(serial)
  "#{serial[0, 2]}-" + serial[2, 12].scan(/.{4}/).join('-')
end

.request_authenticator(region) ⇒ Bnet::Authenticator

Request a new authenticator from server

Parameters:

  • region (Symbol)

Returns:

Raises:

  • (BadInputError)


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/bnet/authenticator.rb', line 49

def self.request_authenticator(region)
  region = region.to_s.upcase.to_sym
  raise BadInputError.new("bad region #{region}") unless is_valid_region?(region)

  k = create_one_time_pad(37)

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

  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].as_bin_to_hex)
end

.request_for(label, region, path, body = nil) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/bnet/authenticator.rb', line 199

def request_for(label, region, path, body = nil)
  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) ⇒ Integer

Get server’s time

Parameters:

  • region (Symbol)

Returns:

  • (Integer)

    server timestamp in seconds



96
97
98
# File 'lib/bnet/authenticator.rb', line 96

def self.request_server_time(region)
  request_for('server time', region, TIME_REQUEST_PATH).as_bin_to_i.to_f / 1000
end

.restore_authenticator(serial, restorecode) ⇒ Bnet::Authenticator

Restore an authenticator from server

Parameters:

  • serial (String)
  • restorecode (String)

Returns:

Raises:

  • (BadInputError)


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/bnet/authenticator.rb', line 69

def self.restore_authenticator(serial, restorecode)
  raise BadInputError.new("bad serial #{serial}") unless is_valid_serial?(serial)
  raise BadInputError.new("bad restoration code #{restorecode}") unless is_valid_restorecode?(restorecode)

  normalized_serial = normalize_serial(serial)
  region = extract_region(normalized_serial)

  # stage 1
  challenge = request_for('restore (stage 1)', region, RESTORE_INIT_REQUEST_PATH, normalized_serial)

  # stage 2
  key = create_one_time_pad(20)

  digest = Digest::HMAC.digest(normalized_serial + challenge,
                               decode_restorecode(restorecode),
                               Digest::SHA1)

  payload = normalized_serial + rsa_encrypted((digest + key).as_bin_to_i)

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

  Authenticator.new(prettify_serial(normalized_serial), decrypt_response(response_body, key).as_bin_to_hex)
end

.rsa_encrypted(integer) ⇒ Object



195
196
197
# File 'lib/bnet/authenticator.rb', line 195

def rsa_encrypted(integer)
  (integer ** RSA_KEY % RSA_MOD).to_bin
end

Instance Method Details

#get_token(timestamp = nil) ⇒ String, Integer

Get authenticator’s token from given timestamp

Parameters:

  • timestamp (Integer) (defaults to: nil)

    UNIX timestamp in seconds, defaults to current time

Returns:

  • (String, Integer)

    token and the next timestamp token to change



120
121
122
# File 'lib/bnet/authenticator.rb', line 120

def get_token(timestamp = nil)
  self.class.get_token(@secret, timestamp)
end

#to_hashHash

Hash representation of this authenticator

Returns:

  • (Hash)


126
127
128
129
130
131
132
# File 'lib/bnet/authenticator.rb', line 126

def to_hash
  {
    :serial => serial,
    :secret => secret,
    :restorecode => restorecode,
  }
end

#to_sString

String representation of this authenticator

Returns:

  • (String)


136
137
138
# File 'lib/bnet/authenticator.rb', line 136

def to_s
  to_hash.to_s
end