Skip to content

Commit 23f85fe

Browse files
committed
:octocat: remove BattleNet secret creation/restore (#6)
(cherry picked from commit 0d0f61f)
1 parent 5da490d commit 23f85fe

File tree

3 files changed

+2
-368
lines changed

3 files changed

+2
-368
lines changed

examples/battlenet.php

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
12-
use chillerlan\Authenticator\Authenticators\{AuthenticatorInterface, BattleNet};
12+
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
1313

1414
require_once '../vendor/autoload.php';
1515

@@ -34,13 +34,3 @@
3434
// allow 2 adjacent codes
3535
$options->adjacent = 2;
3636
var_dump($auth->verify($code, (time() + 2 * $options->period))); // -> true
37-
38-
// request a new authenticator from the Battle.net API
39-
// this requires the BattleNet class to be invoked directly as we're using non-interface methods for this
40-
$auth = new BattleNet;
41-
$data = $auth->createAuthenticator('EU');
42-
// the serial can be used to attach this authenticator to an existing Battle.net account
43-
var_dump($data);
44-
// it's also possible to retreive an authenticator secret from an existing serial and restore code, e.g. from WinAuth
45-
$data = $auth->restoreSecret($data['serial'], $data['restore_code']);
46-
var_dump($data);

src/Authenticators/BattleNet.php

Lines changed: 1 addition & 341 deletions
Original file line numberDiff line numberDiff line change
@@ -13,97 +13,17 @@
1313
namespace chillerlan\Authenticator\Authenticators;
1414

1515
use chillerlan\Authenticator\Common\Hex;
16-
use InvalidArgumentException;
1716
use RuntimeException;
18-
use function array_reverse;
19-
use function array_unshift;
20-
use function curl_close;
21-
use function curl_exec;
22-
use function curl_getinfo;
23-
use function curl_init;
24-
use function curl_setopt_array;
25-
use function floor;
26-
use function gmp_cmp;
27-
use function gmp_div;
28-
use function gmp_import;
29-
use function gmp_init;
30-
use function gmp_intval;
31-
use function gmp_mod;
32-
use function gmp_powm;
33-
use function hash_hmac;
34-
use function hexdec;
35-
use function implode;
36-
use function in_array;
37-
use function pack;
38-
use function preg_match;
39-
use function random_bytes;
40-
use function sha1;
41-
use function sprintf;
4217
use function str_pad;
43-
use function str_replace;
44-
use function str_split;
45-
use function strlen;
46-
use function strtoupper;
47-
use function substr;
48-
use function time;
49-
use function trim;
50-
use function unpack;
51-
use const CURLOPT_HTTP_VERSION;
52-
use const CURLOPT_HTTPHEADER;
53-
use const CURLOPT_POST;
54-
use const CURLOPT_POSTFIELDS;
55-
use const CURLOPT_RETURNTRANSFER;
5618
use const STR_PAD_LEFT;
5719

5820
/**
5921
* @see https://github.com/winauth/winauth/blob/master/Authenticator/BattleNetAuthenticator.cs
6022
* @see https://github.com/krtek4/php-bma
23+
* @see https://github.com/jleclanche/python-bna/issues/38
6124
*/
6225
final class BattleNet extends TOTP{
6326

64-
/**
65-
* @var array
66-
*/
67-
private const regions = ['EU', 'KR', 'US']; // 'CN',
68-
69-
/**
70-
* HTTPS requests with HTTP version 1.1 only!
71-
*
72-
* @var array
73-
*/
74-
private const servers = [
75-
# 'CN' => 'https://mobile-service.battlenet.com.cn', // ???
76-
'EU' => 'https://eu.mobile-service.blizzard.com',
77-
'KR' => 'https://kr.mobile-service.blizzard.com',
78-
'US' => 'https://us.mobile-service.blizzard.com',
79-
];
80-
81-
/**
82-
* @var array
83-
*/
84-
private const endpoints = [
85-
'public_key' => '/enrollment/initiatePaperRestore.htm',
86-
'validate' => '/enrollment/validatePaperRestore.htm',
87-
'create' => '/enrollment/enroll.htm',
88-
'servertime' => '/enrollment/time.htm',
89-
];
90-
91-
private const rsa_exp_base10 = '257';
92-
private const rsa_mod_base10 = '1048900188079865568740077109142054431570301596680341971861256789'.
93-
'6028747089429083053061828494311840511089632283544909943323209315'.
94-
'1168250152146023319326491587651685252774820340995950744075665455'.
95-
'6817606521365764930287339148921667008991098362911808810630974611'.
96-
'75643998356321993663868233366705340758102567742483097';
97-
98-
# private const rsa_exp_base16 = '0101';
99-
# private const rsa_mod_base16 = '955e4bd989f3917d2f15544a7e0504eb9d7bb66b6f8a2fe470e453c779200e5e'.
100-
# '3ad2e43a02d06c4adbd8d328f1a426b83658e88bfd949b2af4eaf30054673a14'.
101-
# '19a250fa4cc1278d12855b5b25818d162c6e6ee2ab4a350d401d78f6ddb99711'.
102-
# 'e72626b48bd8b5b0b7f3acf9ea3c9e0005fee59e19136cdb7c83f2ab8b0a2a99';
103-
104-
/** @var array */
105-
private $curlInfo = [];
106-
10727
/**
10828
* @inheritDoc
10929
*/
@@ -163,264 +83,4 @@ public function getOTP(int $code):string{
16383
return str_pad((string)$code, 8, '0', STR_PAD_LEFT);
16484
}
16585

166-
/**
167-
* @inheritDoc
168-
*/
169-
public function getServerTime():int{
170-
171-
if($this->options->forceTimeRefresh === false && $this->serverTime !== 0){
172-
return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime);
173-
}
174-
175-
$servertime = $this->request('servertime', 'US');
176-
177-
$this->setServertime($servertime);
178-
179-
return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime);
180-
}
181-
182-
/**
183-
* Retrieves the secret from Battle.net using the given serial and restore code.
184-
* If the public key for the serial is given (from a previous retrieval), it saves a server request.
185-
*/
186-
public function restoreSecret(string $serial, string $restore_code, string $public_key = null):array{
187-
$serial = $this->cleanSerial($serial);
188-
$region = $this->getRegion($serial);
189-
190-
// fetch public key if none is given
191-
$pubkey = ($public_key !== null)
192-
? Hex::decode($public_key)
193-
: $this->request('public_key', $region, $serial);
194-
195-
// create HMAC hash from serial and restore code
196-
$hmac_key = $this->convertRestoreCodeToByte($restore_code);
197-
$hmac = hash_hmac('sha1', $serial.$pubkey, $hmac_key, true);
198-
// encrypt and send validation request
199-
$nonce = random_bytes(20);
200-
$encrypted_secret = $this->request('validate', $region, $serial.$this->encrypt($hmac.$nonce));
201-
$secret = $this->decrypt($encrypted_secret, $nonce);
202-
203-
return [
204-
'region' => $region,
205-
'serial' => $this->formatSerial($serial),
206-
'restore_code' => $restore_code,
207-
'public_key' => Hex::encode($pubkey),
208-
'secret' => Hex::encode($secret),
209-
];
210-
}
211-
212-
/**
213-
* Creates a new authenticator that can be linked to an existing Battle.net account
214-
*/
215-
public function createAuthenticator(string $region, string $device = null):array{
216-
$region = $this->getRegion($region);
217-
$device = str_pad(($device ?? 'BlackBerry Pearl'), 16, "\x00");
218-
$nonce = random_bytes(37);
219-
$response = $this->request('create', $region, $this->encrypt("\x01".$nonce.$region.$device));
220-
// timestamp, first 8 bytes of the response
221-
$this->setServertime(substr($response, 0, 8));
222-
// decrypt rest of the response (37 bytes)
223-
$data = $this->decrypt(substr($response, 8), $nonce);
224-
// secret, first 20 bytes
225-
$secret = substr($data, 0, 20);
226-
// serial, last 17 bytes
227-
$serial = $this->cleanSerial(substr($data, 20));
228-
// the restore code is taken from the last 10 bytes of a SHA1 hashed serial and (binary) secret
229-
$restore_code = substr(sha1($serial.$secret, true), -10);
230-
231-
// feed the result into the restore function to verify the restore code and fetch the public key
232-
return $this->restoreSecret($serial, $this->convertRestoreCodeToChar($restore_code));
233-
}
234-
235-
/**
236-
*
237-
*/
238-
private function setServertime(string $encodedTimestamp):void{
239-
$this->serverTime = (int)floor(hexdec(Hex::encode($encodedTimestamp)) / 1000);
240-
$this->lastRequestTime = (time() - (int)floor($this->curlInfo['total_time']));
241-
}
242-
243-
/**
244-
* @throws \RuntimeException
245-
*/
246-
private function getRegion(string $serial):string{
247-
$region = substr(strtoupper($serial), 0, 2);
248-
249-
if(!in_array($region, self::regions)){
250-
throw new RuntimeException('invalid region in serial number detected');
251-
}
252-
253-
return $region;
254-
}
255-
256-
/**
257-
* cleans the given serial in (EU-1111-2222-3333) and strips hyphens (EU111122223333) for use in API requests
258-
*
259-
* @throws \InvalidArgumentException
260-
*/
261-
private function cleanSerial(string $serial):string{
262-
$serial = str_replace('-', '', strtoupper(trim($serial)));
263-
264-
if(!preg_match('/^[CNEUSKR]{2}\d{12}$/', $serial)){
265-
throw new InvalidArgumentException('invalid serial');
266-
}
267-
268-
return $serial;
269-
}
270-
271-
/**
272-
*
273-
*/
274-
private function formatSerial(string $serial):string{
275-
$serial = $this->cleanSerial($serial);
276-
// split the numeric part into 3x 4 numbers
277-
$blocks = str_split(substr($serial, 2), 4);
278-
// prepend the region
279-
array_unshift($blocks, substr($serial, 0, 2));
280-
281-
return implode('-', $blocks);
282-
}
283-
284-
/**
285-
* @throws \RuntimeException
286-
*/
287-
private function request(string $endpoint, string $region, string $data = null):string{
288-
289-
$options = [
290-
CURLOPT_RETURNTRANSFER => true,
291-
CURLOPT_HTTP_VERSION => '1.1', // we need to force http 1.1, h2 will return a HTTP/600 error (???) from Battle.net
292-
CURLOPT_HTTPHEADER => [sprintf('User-Agent: %s', $this::userAgent)],
293-
];
294-
295-
if($data !== null){
296-
$options[CURLOPT_POST] = true;
297-
$options[CURLOPT_POSTFIELDS] = $data;
298-
$options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/octet-stream';
299-
}
300-
301-
$ch = curl_init(self::servers[$region].self::endpoints[$endpoint]);
302-
303-
curl_setopt_array($ch, $options);
304-
305-
$response = curl_exec($ch);
306-
$this->curlInfo = curl_getinfo($ch);
307-
308-
curl_close($ch);
309-
310-
if($this->curlInfo['http_code'] !== 200){
311-
// I'm not going to investigate the error further as this shouldn't happen usually
312-
throw new RuntimeException(sprintf('Battle.net API request error: HTTP/%s', $this->curlInfo['http_code'])); // @codeCoverageIgnore
313-
}
314-
315-
return $response;
316-
}
317-
318-
/**
319-
* Convert restore code char to byte but with appropriate mapping to exclude I,L,O and S.
320-
* e.g. A=10 but J=18 not 19 (as I is missing)
321-
*/
322-
private function convertRestoreCodeToByte(string $restore_code):string{
323-
$chars = unpack('C*', $restore_code);
324-
325-
foreach($chars as &$c){
326-
if($c > 47 && $c < 58){
327-
$c -= 48;
328-
}
329-
else{
330-
// S
331-
if($c > 82){
332-
$c--;
333-
}
334-
// O
335-
if($c > 78){
336-
$c--;
337-
}
338-
// L
339-
if($c > 75){
340-
$c--;
341-
}
342-
// I
343-
if($c > 72){
344-
$c--;
345-
}
346-
347-
$c -= 55;
348-
}
349-
350-
}
351-
352-
return pack('C*', ...$chars);
353-
}
354-
355-
/**
356-
* Convert restore code byte to char but with appropriate mapping to exclude I,L,O and S.
357-
*/
358-
private function convertRestoreCodeToChar(string $data):string{
359-
$chars = unpack('C*', $data);
360-
361-
foreach($chars as &$c){
362-
$c &= 0x1F;
363-
364-
if($c < 10){
365-
$c += 48;
366-
}
367-
else{
368-
$c += 55;
369-
// I
370-
if($c > 72){
371-
$c++;
372-
}
373-
// L
374-
if($c > 75){
375-
$c++;
376-
}
377-
// O
378-
if($c > 78){
379-
$c++;
380-
}
381-
// S
382-
if($c > 82){
383-
$c++;
384-
}
385-
}
386-
}
387-
388-
return pack('C*', ...$chars);
389-
}
390-
391-
/**
392-
*
393-
*/
394-
private function encrypt(string $data):string{
395-
$num = gmp_powm(gmp_import($data), self::rsa_exp_base10, self::rsa_mod_base10); // gmp_init(self::rsa_mod_base16, 16)
396-
$zero = gmp_init('0', 10);
397-
$ret = [];
398-
399-
while(gmp_cmp($num, $zero) > 0){
400-
$ret[] = gmp_intval(gmp_mod($num, 256));
401-
$num = gmp_div($num, 256);
402-
}
403-
404-
return pack('C*', ...array_reverse($ret));
405-
}
406-
407-
/**
408-
* @throws \RuntimeException
409-
*/
410-
private function decrypt(string $data, string $key):string{
411-
412-
if(strlen($data) !== strlen($key)){
413-
throw new RuntimeException('The decryption key size and data size doesn\'t match');
414-
}
415-
416-
$data = unpack('C*', $data);
417-
$key = unpack('C*', $key);
418-
419-
foreach($data as $i => &$c){
420-
$c ^= $key[$i];
421-
}
422-
423-
return pack('C*', ...$data);
424-
}
425-
42686
}

0 commit comments

Comments
 (0)