File Coverage

lib/Net/BitTorrent/Protocol/MSE/KeyExchange.pm
Criterion Covered Total %
statement 17 17 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 23 23 100.0


line stmt bran cond sub pod time code
1 20     20   259 use v5.40;
  20         86  
2 20     20   139 use feature 'class';
  20         47  
  20         3875  
3 20     20   204 no warnings qw[experimental::class experimental::builtin];
  20         43  
  20         1076  
4 20     20   113 use Net::BitTorrent::Emitter;
  20         38  
  20         2176  
5             class Net::BitTorrent::Protocol::MSE::KeyExchange v2.0.0 : isa(Net::BitTorrent::Emitter) {
6 20     20   782 use Digest::SHA qw[sha1];
  20         3768  
  20         1539  
7 20     20   33032 use Math::BigInt try => 'GMP';
  20         984912  
  20         129  
8              
9             # -- Parameters --
10             field $infohash : param : reader;
11             field $is_initiator : param : reader;
12              
13             # -- Internal State --
14             field $private_key;
15             field $public_key : reader;
16             field $shared_secret;
17              
18             # -- Cipher State --
19             field $encrypt_rc4 : reader;
20             field $decrypt_rc4 : reader;
21              
22             # Store the initial state (post-discard) for the decryptor
23             # to optimize the scan_for_vc loop.
24             field $decrypt_restore_point;
25              
26             # 768-bit Safe Prime (Big Endian)
27             my $P_STR
28             = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' .
29             '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' .
30             '4FE1356D6D51C245E485B576625E7EC6F44C42E9A63A36210000000000090563';
31             ADJUST {
32             my $p = Math::BigInt->from_hex($P_STR);
33             my $g = Math::BigInt->new(2);
34              
35             # Private Key: Random 160 bits
36             my $priv_hex = join '', map { sprintf "%02x", rand(256) } 1 .. 20;
37             $private_key = Math::BigInt->from_hex($priv_hex);
38              
39             # Public Key: Y = G^X mod P
40             my $pub_val = $g->copy->bmodpow( $private_key, $p );
41             $public_key = $self->_int_to_bytes($pub_val);
42             }
43              
44             method _int_to_bytes ($num) {
45             my $hex = $num->to_hex;
46             $hex =~ s/^0x//i;
47             $hex = "0$hex" if length($hex) % 2;
48             my $bin = pack( 'H*', $hex );
49             if ( length($bin) < 96 ) {
50             $bin = ( "\0" x ( 96 - length($bin) ) ) . $bin;
51             }
52             elsif ( length($bin) > 96 ) {
53             $bin = substr( $bin, -96 );
54             }
55             return $bin;
56             }
57              
58             method compute_secret ($remote_pub_bytes) {
59             if ( length($remote_pub_bytes) != 96 ) {
60             $self->_emit( log => "Remote public key must be 96 bytes", level => 'fatal' );
61             return undef;
62             }
63             my $p = Math::BigInt->from_hex($P_STR);
64             my $remote_val = Math::BigInt->from_bytes($remote_pub_bytes);
65              
66             # S = Y_remote ^ X_local mod P
67             my $s_val = $remote_val->copy->bmodpow( $private_key, $p );
68             $shared_secret = $self->_int_to_bytes($s_val);
69             return $shared_secret;
70             }
71             method get_secret () { return $shared_secret }
72              
73             method get_sync_data ( $override_ih = undef ) {
74             my $ih = $override_ih // $infohash;
75             return undef unless $ih;
76             my $s = $shared_secret;
77             my $sk = $ih;
78             my $req1_hash = sha1( 'req1' . $s );
79             my $req2_hash = sha1( 'req2' . $sk );
80             my $req3_hash = sha1( 'req3' . $s );
81             my $xor_mask = $req2_hash^.$req3_hash;
82             return ( $req1_hash, $xor_mask );
83             }
84              
85             method verify_skey ( $xor_block, $candidate_ih ) {
86             my $s = $shared_secret;
87             my $req3_hash = sha1( 'req3' . $s );
88             my $target_req2 = $xor_block^.$req3_hash;
89             my $check = sha1( 'req2' . $candidate_ih );
90             return $check eq $target_req2;
91             }
92              
93             method init_rc4 ($ih) {
94             $infohash = $ih;
95             my $keyA = sha1( "keyA" . $shared_secret . $infohash );
96             my $keyB = sha1( "keyB" . $shared_secret . $infohash );
97             my ( $key_enc, $key_dec );
98             if ($is_initiator) {
99             $key_enc = $keyA;
100             $key_dec = $keyB;
101             }
102             else {
103             $key_enc = $keyB;
104             $key_dec = $keyA;
105             }
106              
107             # Initialize Encryptor
108             $encrypt_rc4 = Net::BitTorrent::Protocol::MSE::RC4->new( key => $key_enc );
109             $encrypt_rc4->discard(1024);
110              
111             # Initialize Decryptor
112             $decrypt_rc4 = Net::BitTorrent::Protocol::MSE::RC4->new( key => $key_dec );
113             $decrypt_rc4->discard(1024);
114              
115             # Save state for efficient scanning
116             $decrypt_restore_point = $decrypt_rc4->snapshot();
117             }
118              
119             method scan_for_vc ($buffer) {
120             my $limit = length($buffer) - 8;
121             $limit = 512 if $limit > 512;
122              
123             # Use a temporary RC4 instance to avoid messing up the main decryptor
124             # during the brute force attempts.
125             my $trial_rc4 = Net::BitTorrent::Protocol::MSE::RC4->new( key => 'dummy' );
126             for my $offset ( 0 .. $limit ) {
127              
128             # Restore state to the "post-discard" point instantly (no math involved)
129             $trial_rc4->restore($decrypt_restore_point);
130             my $ciphertext = substr( $buffer, $offset, 8 );
131             my $plaintext = $trial_rc4->crypt($ciphertext);
132             if ( $plaintext eq "\0\0\0\0\0\0\0\0" ) {
133              
134             # Found it! We need to ensure the MAIN decryptor is now
135             # advanced by these 8 bytes so subsequent calls work.
136             # However, the padding (0..$offset) is plaintext garbage.
137             # We skip the padding, then decrypt the VC to sync state.
138             # Note: The caller handles substr($buffer, ...) logic.
139             # We just return the offset found.
140             # The caller MUST call $decrypt_rc4->crypt($vc_bytes)
141             # to sync the main object.
142             return $offset;
143             }
144             }
145             return -1;
146             }
147             }
148              
149             # -- Pure Perl RC4 Implementation --
150             class Net::BitTorrent::Protocol::MSE::RC4 v2.0.0 : isa(Net::BitTorrent::Emitter) {
151             field @S;
152             field $x = 0;
153             field $y = 0;
154             field $key : param;
155             ADJUST {
156             # KSA (Key Scheduling Algorithm)
157             @S = 0 .. 255;
158             my $len = length($key);
159             my @k = unpack( 'C*', $key );
160             my $j = 0;
161             for my $i ( 0 .. 255 ) {
162             $j = ( $j + $S[$i] + $k[ $i % $len ] ) & 0xFF;
163             @S[ $i, $j ] = @S[ $j, $i ];
164             }
165             }
166              
167             method discard ($bytes) {
168              
169             # Discard loop (PRGA without output)
170             for ( 1 .. $bytes ) {
171             $x = ( $x + 1 ) & 0xFF;
172             $y = ( $y + $S[$x] ) & 0xFF;
173             @S[ $x, $y ] = @S[ $y, $x ];
174             }
175             }
176              
177             method crypt ($data) {
178             my $out = '';
179              
180             # Use split for simple iteration over bytes
181             for my $c ( split //, $data ) {
182             $x = ( $x + 1 ) & 0xFF;
183             $y = ( $y + $S[$x] ) & 0xFF;
184             @S[ $x, $y ] = @S[ $y, $x ];
185              
186             # String XOR (^.) available in 5.40
187             $out .= $c^. chr( $S[ ( $S[$x] + $S[$y] ) & 0xFF ] );
188             }
189             return $out;
190             }
191              
192             # Create a lightweight state snapshot (Array ref + 2 ints)
193             method snapshot () {
194             return [ [@S], $x, $y ];
195             }
196              
197             # Restore state from snapshot
198             method restore ($snap) {
199             @S = $snap->[0]->@*;
200             $x = $snap->[1];
201             $y = $snap->[2];
202             }
203             } 1;