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 +4 -4
- data/README.md +8 -14
- data/lib/bnet/authenticator.rb +182 -73
- data/lib/bnet/authenticator/constants.rb +35 -0
- data/lib/bnet/authenticator/version.rb +1 -1
- data/lib/bnet/command.rb +8 -0
- data/lib/bnet/commands/info.rb +2 -2
- data/lib/bnet/commands/new.rb +2 -2
- data/lib/bnet/commands/restore.rb +2 -5
- data/lib/bnet/commands/token.rb +2 -2
- data/test/test_battlenet_authenticator.rb +9 -13
- metadata +3 -3
- data/lib/bnet/authenticator/core.rb +0 -240
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4191aafea73d608b418b1f57cc796f0b893c8eb
|
4
|
+
data.tar.gz: fe976d92aa9851508812f3d262a903d0996bc32c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
20
|
-
=>
|
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.
|
27
|
-
=>
|
24
|
+
>> authenticator.get_token
|
25
|
+
=> ["18338810", 1394965110]
|
28
26
|
|
29
27
|
Restore an authenticator from server
|
30
28
|
----
|
31
|
-
>> Bnet::Authenticator.
|
32
|
-
=>
|
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(
|
39
|
-
=>
|
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
|
====
|
data/lib/bnet/authenticator.rb
CHANGED
@@ -1,108 +1,217 @@
|
|
1
|
-
require 'bnet/
|
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
|
10
|
+
# The Battle.net authenticator
|
6
11
|
class Authenticator
|
7
12
|
|
8
13
|
# @!attribute [r] serial
|
9
|
-
#
|
14
|
+
# @return [String] serial
|
10
15
|
attr_reader :serial
|
11
16
|
|
12
17
|
# @!attribute [r] secret
|
13
|
-
#
|
18
|
+
# @return [String] hexified secret
|
14
19
|
attr_reader :secret
|
15
20
|
|
16
21
|
# @!attribute [r] restorecode
|
17
|
-
#
|
22
|
+
# @return [String] restoration code
|
18
23
|
attr_reader :restorecode
|
19
24
|
|
20
25
|
# @!attribute [r] region
|
21
|
-
#
|
26
|
+
# @return [Symbol] region
|
22
27
|
attr_reader :region
|
23
28
|
|
24
|
-
# Create a new authenticator
|
25
|
-
# @param
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
#
|
61
|
-
# @
|
62
|
-
|
63
|
-
|
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
|
-
|
66
|
-
|
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
|
-
#
|
70
|
-
# @
|
71
|
-
|
72
|
-
|
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
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
137
|
+
to_hash.to_s
|
88
138
|
end
|
89
139
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
data/lib/bnet/command.rb
CHANGED
data/lib/bnet/commands/info.rb
CHANGED
@@ -18,8 +18,8 @@ module Bnet
|
|
18
18
|
serial = @args.shift
|
19
19
|
secret = @args.shift
|
20
20
|
|
21
|
-
authenticator = Authenticator.new(
|
22
|
-
puts authenticator.
|
21
|
+
authenticator = Authenticator.new(serial, secret)
|
22
|
+
puts authenticator.to_readable_text
|
23
23
|
end
|
24
24
|
|
25
25
|
end
|
data/lib/bnet/commands/new.rb
CHANGED
@@ -18,8 +18,8 @@ module Bnet
|
|
18
18
|
region = args.shift || 'US'
|
19
19
|
region = region.to_sym
|
20
20
|
|
21
|
-
authenticator = Authenticator.
|
22
|
-
puts authenticator.
|
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.
|
22
|
-
|
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
|
data/lib/bnet/commands/token.rb
CHANGED
@@ -24,7 +24,7 @@ module Bnet
|
|
24
24
|
def run
|
25
25
|
secret = @args.shift
|
26
26
|
|
27
|
-
token, next_timestamp = Authenticator.
|
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.
|
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(
|
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.
|
25
|
+
Bnet::Authenticator.request_authenticator('SG')
|
26
26
|
end
|
27
27
|
|
28
28
|
assert_raises ::Bnet::Authenticator::BadInputError do
|
29
|
-
Bnet::Authenticator.
|
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.
|
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.
|
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.
|
74
|
-
assert_equal ['61459300', 1347279360], authenticator.
|
75
|
-
assert_equal ['23423634', 1347279390], authenticator.
|
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.
|
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
|
+
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/
|
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
|