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 +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
|