File Coverage

blib/lib/Game/Tibia/Packet/Login.pm
Criterion Covered Total %
statement 94 103 91.2
branch 38 60 63.3
condition 10 24 41.6
subroutine 15 15 100.0
pod 2 2 100.0
total 159 204 77.9


line stmt bran cond sub pod time code
1 3     3   175628 use strict;
  3         35  
  3         72  
2 3     3   12 use warnings;
  3         5  
  3         75  
3 3     3   13 no warnings 'uninitialized';
  3         4  
  3         77  
4 3     3   39 use v5.16.0;
  3         9  
5             package Game::Tibia::Packet::Login;
6              
7             # ABSTRACT: Login packet support for the MMORPG Tibia
8             our $VERSION = '0.007'; # VERSION
9              
10 3     3   26 use Carp;
  3         6  
  3         196  
11 3     3   1375 use File::ShareDir 'dist_file';
  3         31930  
  3         141  
12 3     3   1249 use Crypt::OpenSSL::RSA;
  3         17072  
  3         89  
13 3     3   1234 use Digest::Adler32;
  3         987  
  3         92  
14 3     3   1219 use Game::Tibia::Packet;
  3         8  
  3         11  
15 3     3   22 use Scalar::Util qw(blessed);
  3         6  
  3         123  
16              
17 3     3   15 use constant GET_CHARLIST => 0x01;
  3         4  
  3         128  
18 3     3   15 use constant LOGIN_CHAR => 0x0A;
  3         28  
  3         3023  
19              
20             =pod
21              
22             =encoding utf8
23              
24             =head1 NAME
25              
26             Game::Tibia::Packet::Login - Login packet support for the MMORPG Tibia
27              
28              
29             =head1 SYNOPSIS
30              
31             use Game::Tibia::Packet::Login;
32              
33              
34             =head1 DESCRIPTION
35              
36             Decodes Tibia Login packets into hashes and vice versa. By default uses the OTServ RSA key, but allows different RSA keys to be supplied. Version 9.80 and above is not supported.
37              
38             =cut
39              
40             our %params;
41             sub import {
42 3     3   34 (undef, %params) = (shift, %params, @_);
43 3 50 66     1430 die "Malformed Tibia version\n" if exists $params{tibia} && $params{tibia} !~ /^\d+$/;
44             }
45              
46             my $otserv = Crypt::OpenSSL::RSA->new_private_key(
47             do { local $/; open my $rsa, '<', dist_file('Game-Tibia-Packet', 'otserv.private') or die "Couldn't open private key $!"; <$rsa>; }
48             );
49              
50             =head1 METHODS AND ARGUMENTS
51              
52             =over 4
53              
54             =item new(version => $version, [$character => undef, packet => $packet, rsa => OTSERV])
55              
56             Constructs a new C instance of version C<$version>. If C is supplied, decryption using the supplied rsa private key is attempted. If no C is supplied, the OTServ RSA key is used. If a C<$character> name is supplied, it's assumed to be a game server login packet.
57              
58             =cut
59              
60             sub new {
61 3     3 1 296 my $class = shift;
62              
63 3         17 my $self = {
64             packet => undef,
65             rsa => $otserv,
66              
67             @_
68             };
69              
70 3   33     13 $self->{version} //= $self->{versions}{client}{VERSION};
71 3   33     20 $self->{version} //= $params{tibia};
72 3 50 33     25 croak 'A protocol version < 9.80 must be supplied' if !defined $self->{version} || $self->{version} >= 980;
73              
74 3         18 $self->{versions}{client} = Game::Tibia::Packet::version($self->{version});
75              
76 3 100       15 if ($self->{versions}{client}{rsa}) {
77 2 50 33     21 if (defined $self->{rsa} and !blessed $self->{rsa}) {
78 0         0 $self->{rsa} = Crypt::OpenSSL::RSA->new_private_key($self->{rsa});
79             }
80 2 50       22 $self->{rsa}->use_no_padding if defined $self->{rsa};
81             }
82              
83 3 100       12 if (defined $self->{packet}) {
84             (my $len, my $cmd, $self->{os}, $self->{versions}{client}{VERSION}, my $payload)
85 2         27 = unpack 'v C (S S)< a*', $self->{packet};
86              
87 2 50 33     9 croak "Expected GET_CHARLIST (0x01) or LOGIN_CHAR (0x0A) packet type, but got $cmd" if $cmd ne GET_CHARLIST and $cmd ne LOGIN_CHAR;
88              
89 2 50       9 if ($cmd == GET_CHARLIST) {
90 2         14 ($self->{versions}{spr}, $self->{versions}{dat}, $self->{versions}{pic}, $payload)
91             = unpack('(L3)< a*', $payload);
92             }
93              
94 2 100       8 if ($self->{versions}{client}{rsa}) {
95 1         747 $payload = $self->{rsa}->decrypt($payload);
96 1 50       9 croak q(Decoded RSA doesn't start with zero.) if $payload !~ /^\0/;
97 1         6 $payload = substr $payload, 1;
98             }
99              
100 2 100       8 if ($self->{versions}{client}{xtea}) {
101 1         6 ($self->{xtea}, $payload) = unpack 'a16 a*', $payload;
102             }
103              
104 2 50       7 if ($cmd == LOGIN_CHAR) {
105 0         0 ($self->{gmflag}, $payload) = unpack "C a*", $payload;
106             }
107              
108 2 50       8 my $acc_data_pattern = $self->{versions}{client}{acc_name} ? '(S/a)<' : 'V';
109 2         10 ($self->{account}, $payload) = unpack "$acc_data_pattern a*", $payload;
110 2 50       8 if ($cmd == LOGIN_CHAR) {
111 0         0 ($self->{character}, $payload) = unpack "(S/a)< a*", $payload;
112             }
113 2         10 ($self->{password}, $payload) = unpack "(S/a)< a*", $payload;
114 2 50       7 if ($cmd == LOGIN_CHAR) {
115 0         0 ($self->{nonce}, $payload) = unpack "(a5) a*", $payload;
116             }
117 2         7 $self->{undecoded} = unpack "a*", $payload;
118             }
119              
120 3         9 bless $self, $class;
121 3         8 return $self;
122             }
123              
124             =item finalize([$rsa])
125              
126             Finalizes the packet. encrypts with RSA and prepends header
127              
128             =cut
129              
130              
131             sub finalize {
132 2     2 1 11 my $self = shift;
133 2   33     22 my $rsa = shift // $self->{rsa};
134 2 50       16 $self->{rsa}->use_no_padding if defined $self->{rsa};
135 2 50       7 $self->{versions}{client} = Game::Tibia::Packet::version $self->{versions}{client} unless ref $self->{versions}{client};
136              
137 2         5 my $payload = '';
138 2 100       7 if ($self->{versions}{client}{rsa}) {
139 1 50       5 $rsa = Crypt::OpenSSL::RSA->new_private_key($rsa) unless blessed $rsa;
140 1 50       5 $rsa->size == 128
141 0         0 or croak "Protocol $self->{versions}{client}{VERSION} expects 128 bit RSA key, but ${\($rsa->size*8)} bit were provided";
142 1         3 $payload .= "\0";
143             }
144              
145 2 50       7 $self->{packet} = defined $self->{character} ? "\x0a" : "\x01";
146 2         26 $self->{packet} .= pack '(S2)<', $self->{os}, $self->{versions}{client}{VERSION};
147              
148             $self->{packet} .= defined $self->{character} ? "\0" :
149 2 50       12 pack '(L3)<', $self->{versions}{spr}, $self->{versions}{dat}, $self->{versions}{pic};
150              
151 2 50       6 my $acc_pattern = $self->{versions}{client}{acc_name} ? '(S/a)<' : 'V';
152              
153 2 100       9 $payload .= $self->{xtea} if $self->{versions}{client}{xtea};
154 2 50       6 $payload .= pack "C", $self->{gmflag} if defined $self->{gmflag};
155 2         7 $payload .= pack $acc_pattern, $self->{account};
156 2 50       6 $payload .= pack '(S/a)<', $self->{character} if defined $self->{character};
157 2         10 $payload .= pack '(S/a)<', $self->{password};
158 2 50       6 $payload .= pack 'a5', $self->{nonce} if defined $self->{nonce};
159 2 100 66     14 $payload .= pack 'a*', $self->{undecoded} if defined $self->{undecoded} && $self->{undecoded} ne '';
160              
161 2 100       6 if ($self->{versions}{client}{rsa}) {
162 1         8 my $padding_len = 128 - length($payload);
163 1         3 $payload .= pack "a$padding_len", '';
164 1         22 $payload = $self->{rsa}->encrypt($payload);
165             }
166 2         5 $self->{packet} .= $payload;
167              
168              
169 2 50       7 if ($self->{versions}{client}{adler32}) {
170 0         0 my $a32 = Digest::Adler32->new;
171 0         0 $a32->add($self->{packet});
172 0         0 my $digest = pack "N", unpack "L", $a32->digest;
173 0         0 $self->{packet} = $digest.$self->{packet};
174             }
175              
176 2         9 $self->{packet} = pack("(S/a)<", $self->{packet});
177              
178 2         12 $self->{packet};
179             }
180              
181             1;
182             __END__