Skip to content

Commit c0c4cf1

Browse files
committed
ObfuscatedStream: decode URL-safe base64 and dd-prefixed base64 MTProxy secrets
Proxy links carry secrets in URL-safe base64 (- and _, without padding), which base64_decode() doesn't know: since secrets are decoded strictly they became false, and before that the - and _ were silently dropped, giving a wrong key. They are now translated to standard base64 first. The dd prefix of padded intermediate secrets was only stripped from hex secrets, so the key of a base64 one was hashed with its prefix byte: it is now stripped whatever the encoding. Secrets that aren't 16 bytes once decoded (like fake TLS ee secrets, not supported) are refused with an explicit error instead of producing a wrong key.
1 parent 3aacf40 commit c0c4cf1

2 files changed

Lines changed: 107 additions & 10 deletions

File tree

‎src/Stream/MTProtoTransport/ObfuscatedStream.php‎

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
namespace danog\MadelineProto\Stream\MTProtoTransport;
2222

23+
use danog\MadelineProto\Exception;
2324
use danog\MadelineProto\Stream\BufferedProxyStreamInterface;
2425
use danog\MadelineProto\Stream\Common\CtrStream;
2526
use danog\MadelineProto\Stream\ConnectionContext;
@@ -81,19 +82,40 @@ public function connect(ConnectionContext $ctx, string $header = ''): void
8182
public function setExtra($extra): void
8283
{
8384
if (isset($extra['secret'])) {
84-
if (!preg_match('/^[a-f0-9]+$/i', $extra['secret'])) {
85-
$extra['secret'] = base64_decode($extra['secret'], true);
86-
} else {
87-
if (\strlen($extra['secret']) > 17) {
88-
$extra['secret'] = hex2bin($extra['secret']);
89-
}
90-
if (\strlen($extra['secret']) == 17) {
91-
$extra['secret'] = substr($extra['secret'], 1, 16);
92-
}
93-
}
85+
$extra['secret'] = self::decodeSecret($extra['secret']);
9486
}
9587
$this->extra = $extra;
9688
}
89+
90+
/**
91+
* The 16-byte key of an MTProxy secret: raw, or hex or (URL-safe, as in proxy links) base64
92+
* encoded, optionally with the dd prefix of padded intermediate secrets.
93+
*
94+
* @psalm-pure
95+
*/
96+
private static function decodeSecret(string $secret): string
97+
{
98+
if (\strlen($secret) !== 16) {
99+
$decoded = preg_match('/^(?:[a-f0-9]{2})+$/i', $secret)
100+
? hex2bin($secret)
101+
: base64_decode(strtr($secret, '-_', '+/'), true);
102+
if ($decoded === false) {
103+
throw new Exception('The MTProxy secret is neither hex nor base64');
104+
}
105+
$secret = $decoded;
106+
}
107+
if (\strlen($secret) === 17 && $secret[0] === "\xdd") {
108+
$secret = substr($secret, 1);
109+
}
110+
if (\strlen($secret) !== 16) {
111+
throw new Exception(
112+
($secret[0] ?? '') === "\xee"
113+
? 'MTProxy secrets for fake TLS (starting with ee) are not supported'
114+
: 'Invalid MTProxy secret: it must be 16 bytes long, or 17 starting with dd'
115+
);
116+
}
117+
return $secret;
118+
}
97119
/**
98120
* @psalm-pure
99121
*/
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* This file is part of MadelineProto.
5+
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
6+
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7+
* See the GNU Affero General Public License for more details.
8+
* You should have received a copy of the GNU General Public License along with MadelineProto.
9+
* If not, see <http://www.gnu.org/licenses/>.
10+
*
11+
* @author Daniil Gentili <daniil@daniil.it>
12+
* @copyright 2016-2025 Daniil Gentili <daniil@daniil.it>
13+
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
14+
* @link https://docs.madelineproto.xyz MadelineProto documentation
15+
*/
16+
17+
namespace danog\MadelineProto\Test;
18+
19+
use danog\MadelineProto\Exception;
20+
use danog\MadelineProto\Stream\MTProtoTransport\ObfuscatedStream;
21+
use PHPUnit\Framework\TestCase;
22+
use ReflectionMethod;
23+
24+
/**
25+
* The encodings of MTProxy secrets.
26+
*/
27+
final class ObfuscatedStreamTest extends TestCase
28+
{
29+
private const KEY = "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff";
30+
31+
/**
32+
* @return iterable<string, array{string}>
33+
*/
34+
public static function secrets(): iterable
35+
{
36+
yield 'raw' => [self::KEY];
37+
yield 'hex' => [bin2hex(self::KEY)];
38+
yield 'hex with the dd prefix' => ['dd'.bin2hex(self::KEY)];
39+
yield 'base64' => [base64_encode(self::KEY)];
40+
// As in proxy links: URL-safe, without padding.
41+
yield 'URL-safe base64 with the dd prefix' => [rtrim(strtr(base64_encode("\xdd".self::KEY), '+/', '-_'), '=')];
42+
}
43+
44+
/**
45+
* @dataProvider secrets
46+
*/
47+
public function testSecretsAreDecoded(string $secret): void
48+
{
49+
self::assertSame(self::KEY, self::decode($secret));
50+
}
51+
52+
/**
53+
* @return iterable<string, array{string}>
54+
*/
55+
public static function invalidSecrets(): iterable
56+
{
57+
yield 'fake TLS' => ['ee'.bin2hex(self::KEY).bin2hex('example.com')];
58+
yield 'neither hex nor base64' => ['not a secret!'];
59+
yield 'too short' => [bin2hex(substr(self::KEY, 0, 4))];
60+
}
61+
62+
/**
63+
* @dataProvider invalidSecrets
64+
*/
65+
public function testInvalidSecretsAreRefused(string $secret): void
66+
{
67+
$this->expectException(Exception::class);
68+
self::decode($secret);
69+
}
70+
71+
private static function decode(string $secret): string
72+
{
73+
return (string) (new ReflectionMethod(ObfuscatedStream::class, 'decodeSecret'))->invoke(null, $secret);
74+
}
75+
}

0 commit comments

Comments
 (0)