bnet-authenticator 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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