bnet-authenticator 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a7056e70c2b6c9ef1287b439f7fdc4af94f7d12c
4
- data.tar.gz: ea57bd0c4df97f40c4d5b28176388c5401f62f4f
3
+ metadata.gz: f4191aafea73d608b418b1f57cc796f0b893c8eb
4
+ data.tar.gz: fe976d92aa9851508812f3d262a903d0996bc32c
5
5
  SHA512:
6
- metadata.gz: 25eb9dbf2d68c0367ef0a9166c0e7633ba0cf7b51bfa5e972f0288ff1f694ed3badfc70a63153c487d64666fbad325297e54686bd6a23efd434f6cd3f42834c8
7
- data.tar.gz: 9ba3f92663b958d3990dd161cf2d5563c3262d1a0c15831bd0ebc92b4fe2d08aa074be473efb44e907c1e557fc2f01b8f4fcd73ff34bd531cc89270ef67bbbdf
6
+ metadata.gz: 50f3ae4f46910cc95f96dd2e0b4736834c64711b9ba95ddf8a25b2951a3cf0e58197a287afad49df109bed7dc800a28023f345e5b881866566168ac6a3c8c947
7
+ data.tar.gz: 7be05aeeb5a6136f0a0fc4ca45fc64e80d29a793d9cb8902c99cf2d0dcff6f4e32c763b7b4873ee69a05ed918925b2c8a8d1447d715721a7c5d86499138e4aca
data/README.md CHANGED
@@ -16,29 +16,23 @@ Using the library
16
16
 
17
17
  Request a new authenticator
18
18
  ----
19
- >> authenticator = Bnet::Authenticator.new(:region => :US)
20
- => Serial: US-1402-2552-9200
21
- Secret: c1307afe865735653d981771dff04ceb79b1a353
22
- Restoration Code: EQXCPB2YVE
19
+ >> authenticator = Bnet::Authenticator.request_authenticator(:US)
20
+ => #<Bnet::Authenticator:0x007f83599ae848 @serial="US-1403-1677-5336", @secret="33a107e6a2927a2aa1be99cfe7b2d08c092a7a2a", @region=:US, @restorecode="4YV9XZVNMX">
23
21
 
24
22
  Get a token
25
23
  ----
26
- >> authenticator.caculate_token
27
- => 80185191
24
+ >> authenticator.get_token
25
+ => ["18338810", 1394965110]
28
26
 
29
27
  Restore an authenticator from server
30
28
  ----
31
- >> Bnet::Authenticator.new(:serial => 'CN-1402-1943-1283', :restorecode => '4CKBN08QEB')
32
- => Serial: CN-1402-1943-1283
33
- Secret: 4202aa2182640745d8a807e0fe7e34b30c1edb23
34
- Restoration Code: 4CKBN08QEB
29
+ >> Bnet::Authenticator.restore_authenticator('CN-1402-1943-1283', '4CKBN08QEB')
30
+ => #<Bnet::Authenticator:0x007f83599cf458 @serial="CN-1402-1943-1283", @secret="4202aa2182640745d8a807e0fe7e34b30c1edb23", @region=:CN, @restorecode="4CKBN08QEB">
35
31
 
36
32
  Initialize an authenticator with given serial and secret
37
33
  ----
38
- >> Bnet::Authenticator.new(:serial => 'CN-1402-1943-1283', :secret => '4202aa2182640745d8a807e0fe7e34b30c1edb23')
39
- => Serial: CN-1402-1943-1283
40
- Secret: 4202aa2182640745d8a807e0fe7e34b30c1edb23
41
- Restoration Code: 4CKBN08QEB
34
+ >> Bnet::Authenticator.new('CN-1402-1943-1283', '4202aa2182640745d8a807e0fe7e34b30c1edb23')
35
+ => #<Bnet::Authenticator:0x007f8359a17500 @serial="CN-1402-1943-1283", @secret="4202aa2182640745d8a807e0fe7e34b30c1edb23", @region=:CN, @restorecode="4CKBN08QEB">
42
36
 
43
37
  Using the command-line tool
44
38
  ====
@@ -1,108 +1,217 @@
1
- require 'bnet/authenticator/core'
1
+ require 'bnet/support'
2
+ require 'bnet/authenticator/errors'
3
+ require 'bnet/authenticator/constants'
4
+ require 'digest/sha1'
5
+ require 'digest/hmac'
6
+ require 'net/http'
2
7
 
3
8
  module Bnet
4
9
 
5
- # The battle.net authenticator
10
+ # The Battle.net authenticator
6
11
  class Authenticator
7
12
 
8
13
  # @!attribute [r] serial
9
- # @return [String] the serial of the authenticator
14
+ # @return [String] serial
10
15
  attr_reader :serial
11
16
 
12
17
  # @!attribute [r] secret
13
- # @return [String] hexified secret of the authenticator
18
+ # @return [String] hexified secret
14
19
  attr_reader :secret
15
20
 
16
21
  # @!attribute [r] restorecode
17
- # @return [String] the restoration code of the authenticator
22
+ # @return [String] restoration code
18
23
  attr_reader :restorecode
19
24
 
20
25
  # @!attribute [r] region
21
- # @return [Symbol] the region of the authenticator
26
+ # @return [Symbol] region
22
27
  attr_reader :region
23
28
 
24
- # Create a new authenticator object
25
- # @param options [Hash] read the examples for more infomation
26
- #
27
- # == Examples:
28
- # >> # Create an authenticator object with given serial and secret
29
- # >> Bnet::Authenticator.new(:serial => 'CN-1402-1943-1283', :secret => '4202aa2182640745d8a807e0fe7e34b30c1edb23')
30
- # => Serial: CN-1402-1943-1283
31
- # Secret: 4202aa2182640745d8a807e0fe7e34b30c1edb23
32
- # Restoration Code: 4CKBN08QEB
33
- #
34
- # >> # Request server for a new authenticator
35
- # >> Bnet::Authenticator.new(:region => :US)
36
- # => Serial: US-1402-2552-9200
37
- # Secret: c1307afe865735653d981771dff04ceb79b1a353
38
- # Restoration Code: EQXCPB2YVE
39
- #
40
- # >> # Reqeust to restore an authenticator
41
- # >> Bnet::Authenticator.new(:serial => 'CN-1402-1943-1283', :restorecode => '4CKBN08QEB')
42
- # => Serial: CN-1402-1943-1283
43
- # Secret: 4202aa2182640745d8a807e0fe7e34b30c1edb23
44
- # Restoration Code: 4CKBN08QEB
45
- #
46
- def initialize(options = {})
47
- options = Core.normalize_options(options)
48
-
49
- if options.has_key?(:serial) && options.has_key?(:secret)
50
- @serial, @secret = options[:serial], options[:secret]
51
- elsif options.has_key?(:region)
52
- @serial, @secret = Core.request_new_serial(options[:region], options[:model])
53
- elsif options.has_key?(:serial) && options.has_key?(:restorecode)
54
- @serial, @secret = Core.request_restore(options[:serial], options[:restorecode])
55
- else
56
- raise BadInputError.new('invalid options')
57
- end
29
+ # Create a new authenticator with given serial and secret
30
+ # @param serial [String]
31
+ # @param secret [String]
32
+ def initialize(serial, secret)
33
+ raise BadInputError.new("bad serial #{serial}") unless self.class.is_valid_serial?(serial)
34
+ raise BadInputError.new("bad secret #{secret}") unless self.class.is_valid_secret?(secret)
35
+
36
+ normalized_serial = self.class.normalize_serial(serial)
37
+
38
+ @serial = self.class.prettify_serial(normalized_serial)
39
+ @secret = secret
40
+ @region = self.class.extract_region(normalized_serial)
41
+
42
+ restorecode_bin = Digest::SHA1.digest(normalized_serial + secret.as_hex_to_bin)
43
+ @restorecode = self.class.encode_restorecode(restorecode_bin.split(//).last(10).join)
58
44
  end
59
45
 
60
- # Get the restoration code of this authenticator
61
- # @return [String]
62
- def restorecode
63
- return nil if @serial.nil? or @secret.nil?
46
+ # Request a new authenticator from server
47
+ # @param region [Symbol]
48
+ # @return [Bnet::Authenticator]
49
+ def self.request_authenticator(region)
50
+ region = region.to_s.upcase.to_sym
51
+ raise BadInputError.new("bad region #{region}") unless is_valid_region?(region)
52
+
53
+ k = create_one_time_pad(37)
54
+
55
+ payload_plain = "\1" + k + region.to_s + CLIENT_MODEL.ljust(16, "\0")[0, 16]
56
+ e = rsa_encrypted(payload_plain.as_bin_to_i)
64
57
 
65
- code_bin = Digest::SHA1.digest(Core.normalize_serial(@serial) + @secret.as_hex_to_bin).reverse[0, 10].reverse
66
- Core.encode_restorecode(code_bin)
58
+ response_body = request_for('new serial', region, ENROLLMENT_REQUEST_PATH, e)
59
+
60
+ decrypted = decrypt_response(response_body[8, 37], k)
61
+
62
+ Authenticator.new(decrypted[20, 17], decrypted[0, 20].as_bin_to_hex)
67
63
  end
68
64
 
69
- # Get the region of this authenticator
70
- # @return [Symbol]
71
- def region
72
- Core.extract_region(@serial)
65
+ # Restore an authenticator from server
66
+ # @param serial [String]
67
+ # @param restorecode [String]
68
+ # @return [Bnet::Authenticator]
69
+ def self.restore_authenticator(serial, restorecode)
70
+ raise BadInputError.new("bad serial #{serial}") unless is_valid_serial?(serial)
71
+ raise BadInputError.new("bad restoration code #{restorecode}") unless is_valid_restorecode?(restorecode)
72
+
73
+ normalized_serial = normalize_serial(serial)
74
+ region = extract_region(normalized_serial)
75
+
76
+ # stage 1
77
+ challenge = request_for('restore (stage 1)', region, RESTORE_INIT_REQUEST_PATH, normalized_serial)
78
+
79
+ # stage 2
80
+ key = create_one_time_pad(20)
81
+
82
+ digest = Digest::HMAC.digest(normalized_serial + challenge,
83
+ decode_restorecode(restorecode),
84
+ Digest::SHA1)
85
+
86
+ payload = normalized_serial + rsa_encrypted((digest + key).as_bin_to_i)
87
+
88
+ response_body = request_for('restore (stage 2)', region, RESTORE_VALIDATE_REQUEST_PATH, payload)
89
+
90
+ Authenticator.new(prettify_serial(normalized_serial), decrypt_response(response_body, key).as_bin_to_hex)
73
91
  end
74
92
 
75
- # Caculate token using this authenticator's secret and given timestamp
76
- # (in seconds, defaults to current timestamp)
77
- #
78
- # @param timestamp [Integer] a UNIX timestamp in seconds
79
- # @return [String] current token
80
- def caculate_token(timestamp = nil)
81
- Core.caculate_token(@secret, timestamp)
93
+ # Get server's time
94
+ # @param region [Symbol]
95
+ # @return [Integer] server timestamp in seconds
96
+ def self.request_server_time(region)
97
+ request_for('server time', region, TIME_REQUEST_PATH).as_bin_to_i.to_f / 1000
98
+ end
99
+
100
+ # Get token from given secret and timestamp
101
+ # @param secret [String] hexified secret
102
+ # @param timestamp [Integer] UNIX timestamp in seconds,
103
+ # defaults to current time
104
+ # @return [String, Integer] token and the next timestamp token to change
105
+ def self.get_token(secret, timestamp = nil)
106
+ raise BadInputError.new("bad seret #{secret}") unless is_valid_secret?(secret)
107
+
108
+ current = (timestamp || Time.now.getutc.to_i) / 30
109
+ digest = Digest::HMAC.digest([current].pack('Q>'), secret.as_hex_to_bin, Digest::SHA1)
110
+ start_position = digest[19].ord & 0xf
111
+ token = '%08d' % (digest[start_position, 4].as_bin_to_i % 100000000)
112
+
113
+ return token, (current + 1) * 30
114
+ end
115
+
116
+ # Get authenticator's token from given timestamp
117
+ # @param timestamp [Integer] UNIX timestamp in seconds,
118
+ # defaults to current time
119
+ # @return [String, Integer] token and the next timestamp token to change
120
+ def get_token(timestamp = nil)
121
+ self.class.get_token(@secret, timestamp)
122
+ end
123
+
124
+ # Hash representation of this authenticator
125
+ # @return [Hash]
126
+ def to_hash
127
+ {
128
+ :serial => serial,
129
+ :secret => secret,
130
+ :restorecode => restorecode,
131
+ }
82
132
  end
83
133
 
84
134
  # String representation of this authenticator
85
135
  # @return [String]
86
136
  def to_s
87
- "Serial: #{serial}\nSecret: #{secret}\nRestoration Code: #{restorecode}"
137
+ to_hash.to_s
88
138
  end
89
139
 
90
- # Caculate token using given secret and timestamp
91
- # (in seconds, defaults to current timestamp)
92
- #
93
- # @param secret [String] hexified secret string of an authenticator
94
- # @param timestamp [Integer] a UNIX timestamp in seconds
95
- # @return [String] current token
96
- def self.caculate_token(secret, timestamp = nil)
97
- Core.caculate_token(secret, timestamp)
98
- end
140
+ class << self
141
+ def is_valid_serial?(serial)
142
+ normalized_serial = normalize_serial(serial)
143
+ normalized_serial =~ Regexp.new("^(#{AUTHENTICATOR_HOSTS.keys.join('|')})\\d{12}$") && is_valid_region?(extract_region(normalized_serial))
144
+ end
145
+
146
+ def normalize_serial(serial)
147
+ serial.upcase.gsub(/-/, '')
148
+ end
149
+
150
+ def extract_region(serial)
151
+ serial[0, 2].upcase.to_sym
152
+ end
153
+
154
+ def prettify_serial(serial)
155
+ "#{serial[0, 2]}-" + serial[2, 12].scan(/.{4}/).join('-')
156
+ end
157
+
158
+ def is_valid_secret?(secret)
159
+ secret =~ /[0-9a-f]{40}/i
160
+ end
161
+
162
+ def is_valid_region?(region)
163
+ AUTHENTICATOR_HOSTS.has_key? region
164
+ end
165
+
166
+ def is_valid_restorecode?(restorecode)
167
+ restorecode =~ /[0-9A-Z]{10}/
168
+ end
169
+
170
+ def encode_restorecode(bin)
171
+ bin.bytes.map do |v|
172
+ RESTORECODE_MAP[v & 0x1f]
173
+ end.as_bytes_to_bin
174
+ end
175
+
176
+ def decode_restorecode(str)
177
+ str.bytes.map do |c|
178
+ RESTORECODE_MAP_INVERSE[c]
179
+ end.as_bytes_to_bin
180
+ end
181
+
182
+ def create_one_time_pad(length)
183
+ (0..1.0/0.0).reduce('') do |memo, i|
184
+ break memo if memo.length >= length
185
+ memo << Digest::SHA1.digest(rand().to_s)
186
+ end[0, length]
187
+ end
188
+
189
+ def decrypt_response(text, key)
190
+ text.bytes.zip(key.bytes).reduce('') do |memo, pair|
191
+ memo + (pair[0] ^ pair[1]).chr
192
+ end
193
+ end
194
+
195
+ def rsa_encrypted(integer)
196
+ (integer ** RSA_KEY % RSA_MOD).to_bin
197
+ end
198
+
199
+ def request_for(label, region, path, body = nil)
200
+ request = body.nil? ? Net::HTTP::Get.new(path) : Net::HTTP::Post.new(path)
201
+ request.content_type = 'application/octet-stream'
202
+ request.body = body unless body.nil?
203
+
204
+ response = Net::HTTP.new(AUTHENTICATOR_HOSTS[region]).start do |http|
205
+ http.request request
206
+ end
207
+
208
+ if response.code.to_i != 200
209
+ raise RequestFailedError.new("Error requesting #{label}: #{response.code}")
210
+ end
211
+
212
+ response.body
213
+ end
99
214
 
100
- # Get server's time
101
- #
102
- # @param region [Symbol]
103
- # @return [Integer] server timestamp in seconds
104
- def self.request_server_time(region)
105
- Core.request_server_time(region)
106
215
  end
107
216
 
108
217
  end
@@ -0,0 +1,35 @@
1
+ module Bnet
2
+
3
+ class Authenticator
4
+
5
+ CLIENT_MODEL = 'bn/authenticator'
6
+ RSA_MOD = 104890018807986556874007710914205443157030159668034197186125678960287470894290830530618284943118405110896322835449099433232093151168250152146023319326491587651685252774820340995950744075665455681760652136576493028733914892166700899109836291180881063097461175643998356321993663868233366705340758102567742483097
7
+ RSA_KEY = 257
8
+ AUTHENTICATOR_HOSTS = {
9
+ :CN => "mobile-service.battlenet.com.cn",
10
+ :EU => "m.eu.mobileservice.blizzard.com",
11
+ :US => "m.us.mobileservice.blizzard.com",
12
+ }
13
+ ENROLLMENT_REQUEST_PATH = '/enrollment/enroll.htm'
14
+ TIME_REQUEST_PATH = '/enrollment/time.htm'
15
+ RESTORE_INIT_REQUEST_PATH = '/enrollment/initiatePaperRestore.htm'
16
+ RESTORE_VALIDATE_REQUEST_PATH = '/enrollment/validatePaperRestore.htm'
17
+
18
+ RESTORECODE_MAP = (0..32).reduce({}) do |memo, c|
19
+ memo[c] = case
20
+ when c < 10 then c + 48
21
+ else
22
+ c += 55
23
+ c += 1 if c > 72 # S
24
+ c += 1 if c > 75 # O
25
+ c += 1 if c > 78 # L
26
+ c += 1 if c > 82 # I
27
+ c
28
+ end
29
+ memo
30
+ end
31
+ RESTORECODE_MAP_INVERSE = RESTORECODE_MAP.invert
32
+
33
+ end
34
+
35
+ end
@@ -1,5 +1,5 @@
1
1
  module Bnet
2
2
  class Authenticator
3
- VERSION = "0.1.2"
3
+ VERSION = "0.1.3"
4
4
  end
5
5
  end
@@ -1,5 +1,13 @@
1
1
  module Bnet
2
2
 
3
+ class Authenticator
4
+
5
+ def to_readable_text
6
+ "Serial: #{serial}\nSecret: #{secret}\nRestoration Code: #{restorecode}"
7
+ end
8
+
9
+ end
10
+
3
11
  class InvalidCommandException < StandardError
4
12
  attr_accessor :command
5
13
  attr_accessor :message
@@ -18,8 +18,8 @@ module Bnet
18
18
  serial = @args.shift
19
19
  secret = @args.shift
20
20
 
21
- authenticator = Authenticator.new(:serial => serial, :secret => secret)
22
- puts authenticator.to_s
21
+ authenticator = Authenticator.new(serial, secret)
22
+ puts authenticator.to_readable_text
23
23
  end
24
24
 
25
25
  end
@@ -18,8 +18,8 @@ module Bnet
18
18
  region = args.shift || 'US'
19
19
  region = region.to_sym
20
20
 
21
- authenticator = Authenticator.new(:region => region)
22
- puts authenticator.to_s
21
+ authenticator = Authenticator.request_authenticator(region)
22
+ puts authenticator.to_readable_text
23
23
  end
24
24
 
25
25
  end
@@ -18,11 +18,8 @@ module Bnet
18
18
  serial = @args.shift
19
19
  restorecode = @args.shift
20
20
 
21
- authenticator = Authenticator.new(
22
- :serial => serial,
23
- :restorecode => restorecode
24
- )
25
- puts authenticator.to_s
21
+ authenticator = Authenticator.restore_authenticator(serial, restorecode)
22
+ puts authenticator.to_readable_text
26
23
  end
27
24
 
28
25
  end
@@ -24,7 +24,7 @@ module Bnet
24
24
  def run
25
25
  secret = @args.shift
26
26
 
27
- token, next_timestamp = Authenticator.caculate_token(secret)
27
+ token, next_timestamp = Authenticator.get_token(secret)
28
28
 
29
29
  puts token
30
30
  if @options.repeat
@@ -39,7 +39,7 @@ module Bnet
39
39
  next
40
40
  end
41
41
 
42
- token, next_timestamp = Authenticator.caculate_token(secret)
42
+ token, next_timestamp = Authenticator.get_token(secret)
43
43
  puts token
44
44
  end
45
45
  end
@@ -12,31 +12,27 @@ class Bnet::AuthenticatorTest < Minitest::Test
12
12
  DEFAULT_REGION = :CN
13
13
 
14
14
  def test_load
15
- authenticator = Bnet::Authenticator.new(:serial => DEFAULT_SERIAL, :secret => DEFAULT_SECRET)
15
+ authenticator = Bnet::Authenticator.new(DEFAULT_SERIAL, DEFAULT_SECRET)
16
16
  is_default_authenticator authenticator
17
17
  end
18
18
 
19
19
  def test_argument_error
20
20
  assert_raises ::Bnet::Authenticator::BadInputError do
21
- Bnet::Authenticator.new
21
+ Bnet::Authenticator.new('ABC', '')
22
22
  end
23
23
 
24
24
  assert_raises ::Bnet::Authenticator::BadInputError do
25
- Bnet::Authenticator.new(:serial => 'ABC')
25
+ Bnet::Authenticator.request_authenticator('SG')
26
26
  end
27
27
 
28
28
  assert_raises ::Bnet::Authenticator::BadInputError do
29
- Bnet::Authenticator.new(:region => 'SG')
30
- end
31
-
32
- assert_raises ::Bnet::Authenticator::BadInputError do
33
- Bnet::Authenticator.new(:restorecode => 'DDDD')
29
+ Bnet::Authenticator.restore_authenticator('DDDD', 'EEE')
34
30
  end
35
31
  end
36
32
 
37
33
  def test_request_new_serial
38
34
  begin
39
- authenticator = Bnet::Authenticator.new(:region => :US)
35
+ authenticator = Bnet::Authenticator.request_authenticator(:US)
40
36
  assert_equal :US, authenticator.region
41
37
  refute_nil authenticator.serial
42
38
  refute_nil authenticator.secret
@@ -48,7 +44,7 @@ class Bnet::AuthenticatorTest < Minitest::Test
48
44
 
49
45
  def test_restore
50
46
  begin
51
- authenticator = Bnet::Authenticator.new(:serial => DEFAULT_SERIAL, :restorecode => DEFAULT_RSCODE)
47
+ authenticator = Bnet::Authenticator.restore_authenticator(DEFAULT_SERIAL, DEFAULT_RSCODE)
52
48
  is_default_authenticator authenticator
53
49
  rescue Bnet::Authenticator::RequestFailedError => e
54
50
  puts e
@@ -70,8 +66,8 @@ class Bnet::AuthenticatorTest < Minitest::Test
70
66
  assert_equal DEFAULT_SERIAL, authenticator.serial
71
67
  assert_equal DEFAULT_SECRET, authenticator.secret
72
68
  assert_equal DEFAULT_RSCODE, authenticator.restorecode
73
- assert_equal ['61459300', 1347279360], authenticator.caculate_token(1347279358)
74
- assert_equal ['61459300', 1347279360], authenticator.caculate_token(1347279359)
75
- assert_equal ['23423634', 1347279390], authenticator.caculate_token(1347279360)
69
+ assert_equal ['61459300', 1347279360], authenticator.get_token(1347279358)
70
+ assert_equal ['61459300', 1347279360], authenticator.get_token(1347279359)
71
+ assert_equal ['23423634', 1347279390], authenticator.get_token(1347279360)
76
72
  end
77
73
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bnet-authenticator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ZHANG Yi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-11 00:00:00.000000000 Z
11
+ date: 2014-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -82,7 +82,7 @@ files:
82
82
  - bin/bna
83
83
  - bnet-authenticator.gemspec
84
84
  - lib/bnet/authenticator.rb
85
- - lib/bnet/authenticator/core.rb
85
+ - lib/bnet/authenticator/constants.rb
86
86
  - lib/bnet/authenticator/errors.rb
87
87
  - lib/bnet/authenticator/version.rb
88
88
  - lib/bnet/command.rb
@@ -1,240 +0,0 @@
1
- require 'digest/sha1'
2
- require 'digest/hmac'
3
- require 'net/http'
4
- require 'bnet/support'
5
- require 'bnet/authenticator/errors'
6
-
7
- module Bnet
8
-
9
- class Authenticator
10
-
11
- module Core
12
-
13
- RSA_MOD = 104890018807986556874007710914205443157030159668034197186125678960287470894290830530618284943118405110896322835449099433232093151168250152146023319326491587651685252774820340995950744075665455681760652136576493028733914892166700899109836291180881063097461175643998356321993663868233366705340758102567742483097
14
- RSA_KEY = 257
15
- AUTHENTICATOR_HOSTS = {
16
- :CN => "mobile-service.battlenet.com.cn",
17
- :EU => "m.eu.mobileservice.blizzard.com",
18
- :US => "m.us.mobileservice.blizzard.com",
19
- }
20
- ENROLLMENT_REQUEST_PATH = '/enrollment/enroll.htm'
21
- TIME_REQUEST_PATH = '/enrollment/time.htm'
22
- RESTORE_INIT_REQUEST_PATH = '/enrollment/initiatePaperRestore.htm'
23
- RESTORE_VALIDATE_REQUEST_PATH = '/enrollment/validatePaperRestore.htm'
24
-
25
- RESTORECODE_MAP = (0..32).reduce({}) do |memo, c|
26
- memo[c] = case
27
- when c < 10 then c + 48
28
- else
29
- c += 55
30
- c += 1 if c > 72 # S
31
- c += 1 if c > 75 # O
32
- c += 1 if c > 78 # L
33
- c += 1 if c > 82 # I
34
- c
35
- end
36
- memo
37
- end
38
- RESTORECODE_MAP_INVERSE = RESTORECODE_MAP.invert
39
-
40
- def self.normalize_options(options)
41
- return nil if options.nil?
42
-
43
- %w(serial region restorecode secret).each do |attr|
44
- if options.has_key? attr.to_sym
45
- options[attr.to_sym] = send "normalize_#{attr}".to_sym, options[attr.to_sym] do |value|
46
- raise BadInputError.new("bad #{attr} #{value}")
47
- end
48
- end
49
- end
50
-
51
- options[:serial] = prettify_serial(options[:serial]) if options.has_key?(:serial)
52
-
53
- options
54
- end
55
-
56
- def self.encode_restorecode(bin)
57
- bin.bytes.map do |v|
58
- RESTORECODE_MAP[v & 0x1f]
59
- end.as_bytes_to_bin
60
- end
61
-
62
- def self.extract_region(serial)
63
- serial.to_s[0, 2].upcase.to_sym
64
- end
65
-
66
- def self.caculate_token(secret, timestamp = nil)
67
- secret = normalize_secret secret do |invalid_secret|
68
- return nil
69
- end
70
-
71
- current = (timestamp || Time.now.getutc.to_i) / 30
72
-
73
- digest = Digest::HMAC.digest([current].pack('Q>'), secret.as_hex_to_bin, Digest::SHA1)
74
-
75
- start_position = digest[19].ord & 0xf
76
-
77
- token = '%08d' % (digest[start_position, 4].as_bin_to_i % 100000000)
78
-
79
- return token, (current + 1) * 30
80
- end
81
-
82
- def self.request_new_serial(region, model = nil)
83
- e, k = prepair_serial_request(region, model || 'bn/authenticator')
84
-
85
- # request to server
86
- response_body = request_for('new serial', region, ENROLLMENT_REQUEST_PATH, e)
87
-
88
- # the first 8 bytes be server timestamp in milliseconds
89
- # the rest 37 bytes, to be XORed with `k`
90
- decrypted = decrypt_response(response_body[8, 37], k)
91
-
92
- # now
93
- # the first 20 bytes be the authenticator secret
94
- # the rest 17 bytes be the authenticator serial (readable string begins with CN-, US-, EU-, etc.)
95
- secret = decrypted[0, 20]
96
- serial = decrypted[20, 17]
97
-
98
- return serial, secret.as_bin_to_hex
99
- end
100
-
101
- def self.request_restore(serial, restorecode)
102
- serial_normalized = normalize_serial(serial)
103
- region = extract_region(serial_normalized)
104
- restorecode_bin = decode_restorecode(restorecode)
105
-
106
- # stage 1
107
- challenge = request_for('restore (stage 1)', region, RESTORE_INIT_REQUEST_PATH, serial_normalized)
108
-
109
- # stage 2
110
- key = create_one_time_pad(20)
111
-
112
- digest = Digest::HMAC.digest(serial_normalized + challenge,
113
- restorecode_bin,
114
- Digest::SHA1)
115
-
116
- payload = serial_normalized + rsa_encrypted((digest + key).as_bin_to_i)
117
-
118
- response_body = request_for('restore (stage 2)', region, RESTORE_VALIDATE_REQUEST_PATH, payload)
119
-
120
- secret = decrypt_response(response_body, key).as_bin_to_hex
121
-
122
- return prettify_serial(serial), secret
123
- end
124
-
125
- def self.request_server_time(region)
126
- request_for('server time', region, TIME_REQUEST_PATH).as_bin_to_i.to_f / 1000
127
- end
128
-
129
- class << self
130
-
131
- def normalize_serial(serial)
132
- s = serial.to_s.gsub(/-/, '').upcase
133
-
134
- if block_given?
135
- region = extract_region(s)
136
- yield serial unless (AUTHENTICATOR_HOSTS.has_key?(region) && s =~ /\d{12}/)
137
- end
138
-
139
- s
140
- end
141
-
142
- def prettify_serial(serial)
143
- serial = normalize_serial(serial) { |bad_serial| return nil }
144
- "#{serial[0, 2]}-" + serial[2, 12].scan(/.{4}/).join('-')
145
- end
146
-
147
- def normalize_region(region)
148
- normalized_region = region.to_s.upcase.to_sym
149
-
150
- if block_given? && !AUTHENTICATOR_HOSTS.has_key?(normalized_region)
151
- yield region
152
- end
153
-
154
- normalized_region
155
- end
156
-
157
- def normalize_restorecode(restorecode)
158
- restorecode = restorecode.upcase
159
-
160
- if block_given? && !(restorecode =~ /[0-9A-Z]{10}/)
161
- yield restorecode
162
- end
163
-
164
- restorecode
165
- end
166
-
167
- def normalize_secret(secret)
168
- if block_given? && !(secret =~ /[0-9a-f]{40}/i)
169
- yield secret
170
- end
171
-
172
- secret
173
- end
174
-
175
- def create_one_time_pad(length)
176
- (0..1.0/0.0).reduce('') do |memo, i|
177
- break memo if memo.length >= length
178
- memo << Digest::SHA1.digest(rand().to_s)
179
- end[0, length]
180
- end
181
-
182
- def decode_restorecode(str)
183
- str.bytes.map do |c|
184
- RESTORECODE_MAP_INVERSE[c]
185
- end.as_bytes_to_bin
186
- end
187
-
188
- def decrypt_response(text, key)
189
- text.bytes.zip(key.bytes).reduce('') do |memo, pair|
190
- memo + (pair[0] ^ pair[1]).chr
191
- end
192
- end
193
-
194
- def rsa_encrypted(integer)
195
- (integer ** RSA_KEY % RSA_MOD).to_bin
196
- end
197
-
198
- def prepair_serial_request(region, model)
199
- # one-time key of 37 bytes
200
- k = create_one_time_pad(37)
201
-
202
- # make byte[56]
203
- # 00 byte[1] 固定为1
204
- # 01 byte[37] 37位的随机数据,只使用一次,用来解密服务器返回数据
205
- # 38 byte[2] 区域码: CN, US, EU, etc.
206
- # 40 byte[16] 设备模型数据(手机型号字符串,可随意)
207
- bytes = [1]
208
- bytes.concat(k.bytes.to_a)
209
- bytes.concat(region.to_s.bytes.take(2))
210
- bytes.concat(model.ljust(16, "\0").bytes.take(16))
211
-
212
- # encrypted using RSA
213
- e = rsa_encrypted(bytes.as_bytes_to_i)
214
-
215
- return e, k
216
- end
217
-
218
- def request_for(label, region, path, body = nil)
219
- request = body.nil? ? Net::HTTP::Get.new(path) : Net::HTTP::Post.new(path)
220
- request.content_type = 'application/octet-stream'
221
- request.body = body unless body.nil?
222
-
223
- response = Net::HTTP.new(AUTHENTICATOR_HOSTS[region]).start do |http|
224
- http.request request
225
- end
226
-
227
- if response.code.to_i != 200
228
- raise RequestFailedError.new("Error requesting #{label}: #{response.code}")
229
- end
230
-
231
- response.body
232
- end
233
-
234
- end
235
-
236
- end
237
-
238
- end
239
-
240
- end