File Coverage

blib/lib/UID2/Client/Decryption.pm
Criterion Covered Total %
statement 190 210 90.4
branch 70 86 81.4
condition 15 24 62.5
subroutine 24 25 96.0
pod 0 7 0.0
total 299 352 84.9


line stmt bran cond sub pod time code
1             package UID2::Client::Decryption;
2 3     3   17 use strict;
  3         8  
  3         72  
3 3     3   14 use warnings;
  3         5  
  3         57  
4              
5 3     3   11 use Carp;
  3         3  
  3         119  
6 3     3   1102 use Crypt::Cipher::AES;
  3         678  
  3         61  
7 3     3   18 use Crypt::Mode::CBC;
  3         4  
  3         51  
8 3     3   1090 use Crypt::AuthEnc::GCM;
  3         843  
  3         110  
9 3     3   19 use Crypt::Misc qw(encode_b64 decode_b64);
  3         5  
  3         108  
10 3     3   30 use Crypt::PRNG qw(random_bytes);
  3         5  
  3         83  
11              
12 3     3   1004 use UID2::Client::DecryptionStatus;
  3         6  
  3         121  
13 3     3   971 use UID2::Client::EncryptionStatus;
  3         6  
  3         93  
14 3     3   958 use UID2::Client::Timestamp;
  3         10  
  3         5388  
15              
16             sub decrypt_token {
17 24     24 0 107 my $token = shift;
18 24         36 my $result = eval {
19 24         89 my $bytes = decode_b64($token);
20 24 100       47 unless (defined $bytes) {
21 5         12 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
22             }
23 19 100       57 if (ord(substr($bytes, 0, 1)) == 2) {
    50          
24 7         12 return _decrypt_token_v2($bytes, @_);
25             } elsif (ord(substr($bytes, 1, 1)) == 112) {
26 12         22 return _decrypt_token_v3($bytes, @_);
27             } else {
28 0         0 return _error_response(UID2::Client::DecryptionStatus::VERSION_NOT_SUPPORTED);
29             }
30 24 100       283 }; if ($@) {
31 2         7 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
32             }
33 22         72 $result;
34             }
35              
36             sub _decrypt_token_v2 {
37 7     7   14 my ($bytes, $now, $keys) = @_;
38 7 100       16 if (!$keys) {
39 1         3 return _error_response(UID2::Client::DecryptionStatus::NOT_INITIALIZED);
40             }
41 6   66     14 $now //= UID2::Client::Timestamp->now;
42 6 100       17 if (!$keys->is_valid($now)) {
43 1         4 return _error_response(UID2::Client::DecryptionStatus::KEYS_NOT_SYNCED);
44             }
45 5         24 my ($version, $master_key_id, $master_payload_encrypted) = unpack 'a N! a*', $bytes;
46 5 50       12 if (ord($version) != 2) {
47 0         0 return _error_response(UID2::Client::DecryptionStatus::VERSION_NOT_SUPPORTED);
48             }
49 5         12 my $master_key = $keys->get($master_key_id);
50 5 100       10 unless ($master_key) {
51 1         5 return _error_response(UID2::Client::DecryptionStatus::NOT_AUTHORIZED_FOR_KEY);
52             }
53 4         9 my $master_payload = decrypt_cbc($master_payload_encrypted, $master_key->secret);
54 3 50       66 unless (defined $master_payload) {
55 0         0 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
56             }
57 3         14 my ($expires, $site_key_id, $identity_encrypted) = unpack 'q> N! a*', $master_payload;
58 3         8 my $site_key = $keys->get($site_key_id);
59 3 50       6 unless ($site_key) {
60 0         0 return _error_response(UID2::Client::DecryptionStatus::NOT_AUTHORIZED_FOR_KEY);
61             }
62 3         9 my $identity = decrypt_cbc($identity_encrypted, $site_key->secret);
63 3 50       48 unless (defined $identity) {
64 0         0 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
65             }
66 3         9 my ($site_id, $id_len) = unpack 'N! N!', $identity;
67 3         12 my ($id_str, $established) = unpack "x4 x4 a${id_len} x4 q>", $identity;
68 3         9 my $result = {
69             site_id => $site_id,
70             site_key_site_id => $site_key->site_id,
71             established => UID2::Client::Timestamp->from_epoch_milli($established),
72             };
73 3 100       7 if ($expires < $now->get_epoch_milli) {
74 1         2 $result->{is_success} = undef;
75 1         3 $result->{status} = UID2::Client::DecryptionStatus::EXPIRED_TOKEN;
76             } else {
77 2         3 $result->{is_success} = 1;
78 2         4 $result->{status} = UID2::Client::DecryptionStatus::SUCCESS;
79 2         4 $result->{uid} = $id_str;
80             }
81 3         9 $result;
82             }
83              
84             sub _decrypt_token_v3 {
85 12     12   21 my ($bytes, $now, $keys, $identity_scope) = @_;
86 12 100       22 if (!$keys) {
87 1         4 return _error_response(UID2::Client::DecryptionStatus::NOT_INITIALIZED);
88             }
89 11   66     25 $now //= UID2::Client::Timestamp->now;
90 11 100       22 if (!$keys->is_valid($now)) {
91 1         3 return _error_response(UID2::Client::DecryptionStatus::KEYS_NOT_SYNCED);
92             }
93 10         41 my ($prefix, $master_key_id, $master_payload_encrypted) = unpack 'a x N! a*', $bytes;
94 10 50       19 if (_decode_identity_scope($prefix) != $identity_scope) {
95 0         0 return _error_response(UID2::Client::DecryptionStatus::INVALID_IDENTITY_SCOPE);
96             }
97 10         23 my $master_key = $keys->get($master_key_id);
98 10 100       18 unless ($master_key) {
99 1         3 return _error_response(UID2::Client::DecryptionStatus::NOT_AUTHORIZED_FOR_KEY);
100             }
101 9 50       17 if (length($master_payload_encrypted) > 256) {
102 0         0 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
103             }
104 9         18 my $master_payload = decrypt_gcm($master_payload_encrypted, $master_key->secret);
105 8 50       18 unless (defined $master_payload) {
106 0         0 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
107             }
108 8         29 my ($expires, $site_key_id, $site_payload_encrypted) = unpack 'q> x8 x4 x x4 x4 N! a*', $master_payload;
109 8         18 my $site_key = $keys->get($site_key_id);
110 8 50       15 unless ($site_key) {
111 0         0 return _error_response(UID2::Client::DecryptionStatus::NOT_AUTHORIZED_FOR_KEY);
112             }
113 8         14 my $site_payload = decrypt_gcm($site_payload_encrypted, $site_key->secret);
114 8 50       18 unless (defined $site_payload) {
115 0         0 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
116             }
117 8         24 my ($site_id, $established, $identity_bytes) = unpack 'N! x8 x4 x4 q> x8 a*', $site_payload;
118 8         18 my $result = {
119             site_id => $site_id,
120             site_key_site_id => $site_key->site_id,
121             established => UID2::Client::Timestamp->from_epoch_milli($established),
122             };
123 8 100       18 if ($expires < $now->get_epoch_milli) {
124 2         4 $result->{is_success} = undef;
125 2         3 $result->{status} = UID2::Client::DecryptionStatus::EXPIRED_TOKEN;
126             } else {
127 6         9 $result->{is_success} = 1;
128 6         9 $result->{status} = UID2::Client::DecryptionStatus::SUCCESS;
129 6         17 $result->{uid} = encode_b64($identity_bytes);
130             }
131 8         21 $result;
132             }
133              
134             my $EncryptedDataType = 128;
135              
136             my $EncryptedDataVersion = 1;
137              
138             my $EncryptedDataTypeV3 = 96;
139              
140             my $EncryptedDataVersionV3 = 112;
141              
142             my $CBC_IV_LEN = 16;
143              
144             my $GCM_IV_LEN = 12;
145              
146             my $GCM_AUTHTAG_LEN = 16;
147              
148             sub encrypt_data {
149 31     31 0 37 my $result = eval {
150 31         58 _encrypt_data_v3(@_);
151 31 100       81 }; if ($@) {
152 1         3 return _error_response(UID2::Client::EncryptionStatus::ENCRYPTION_FAILURE);
153             }
154 30         111 $result;
155             }
156              
157             sub _encrypt_data_v3 {
158 31     31   45 my ($data, $request) = @_;
159 31         43 my $keys = $request->{keys};
160 31         39 my $key = $request->{key};
161 31 50 66     114 if ($keys && $key) {
162 0         0 croak 'only one of keys and key can be specified';
163             }
164 31   66     81 my $now = $request->{now} // UID2::Client::Timestamp->now;
165 31         38 my $site_id;
166 31 100       67 if (!$key) {
    100          
167 21 50       41 if (!$keys) {
168 0         0 return _error_response(UID2::Client::EncryptionStatus::NOT_INITIALIZED);
169             }
170 21 50       47 if (!$keys->is_valid($now)) {
171 0         0 return _error_response(UID2::Client::EncryptionStatus::KEYS_NOT_SYNCED);
172             }
173 21         33 my $site_key_site_id;
174 21 100 100     72 if (defined $request->{site_id} && length $request->{advertising_token}) {
    100          
175 1         112 croak 'only one of siteId or advertisingToken can be specified';
176             } elsif (defined $request->{site_id}) {
177 14         18 $site_id = $request->{site_id};
178 14         19 $site_key_site_id = $site_id;
179             } else {
180 6         15 my $decrypted = decrypt_token($request->{advertising_token}, $now, $keys, $request->{identity_scope});
181 6 100       12 if (!$decrypted->{is_success}) {
182 2         5 return _error_response(UID2::Client::EncryptionStatus::TOKEN_DECRYPT_FAILURE);
183             }
184 4         6 $site_id = $decrypted->{site_id};
185 4         9 $site_key_site_id = $decrypted->{site_key_site_id};
186             }
187 18         34 $key = $keys->get_active_site_key($site_key_site_id, $now);
188 18 100       106 unless ($key) {
189 5         11 return _error_response(UID2::Client::EncryptionStatus::NOT_AUTHORIZED_FOR_KEY);
190             }
191             } elsif (!$key->is_active($now)) {
192 4         19 return _error_response(UID2::Client::EncryptionStatus::KEY_INACTIVE);
193             } else {
194 6         40 $site_id = $key->site_id;
195             }
196              
197 19         50 my $iv = $request->{initialization_vector};
198 19 50 66     38 if (defined $iv && length($iv) != $GCM_IV_LEN) {
199 0         0 croak "initialization vector size must be $GCM_IV_LEN";
200             }
201              
202 19         50 my $payload = pack 'q> N! a*', $now->get_epoch_milli, $site_id, $data;
203 19         37 my $identity = $EncryptedDataTypeV3 | ($request->{identity_scope} << 4) | 0xB;
204 19         26 my $version = $EncryptedDataVersionV3;
205 19         33 my $res = pack 'C C N! a*', $identity, $version, $key->id, encrypt_gcm($payload, $key->secret, $iv);
206             +{
207 19         93 is_success => 1,
208             status => UID2::Client::EncryptionStatus::SUCCESS,
209             encrypted_data => encode_b64($res),
210             };
211             }
212              
213             sub decrypt_data {
214 30     30 0 169 my $encrypted_data = shift;
215 30         38 my $result = eval {
216 30         101 my $bytes = decode_b64($encrypted_data);
217 30 100       70 unless (defined $bytes) {
218 4         9 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
219             }
220 26 100       69 if ((ord(substr($bytes, 0, 1)) & 224) == $EncryptedDataTypeV3) {
221 19         32 _decrypt_data_v3($bytes, @_);
222             } else {
223 7         17 _decrypt_data_v2($bytes, @_);
224             }
225 30 100       405 }; if ($@) {
226 4         11 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
227             }
228 26         57 $result;
229             }
230              
231             sub _decrypt_data_v2 {
232 7     7   13 my ($encrypted_bytes, $keys) = @_;
233 7         34 my ($type, $version, $encrypted_at, $site_id, $key_id, $bytes) = unpack 'a a q> N! N! a*', $encrypted_bytes;
234 7 100       26 if (ord($type) != $EncryptedDataType) {
235 2         7 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD_TYPE);
236             }
237 5 100       12 if (ord($version) != $EncryptedDataVersion) {
238 1         5 return _error_response(UID2::Client::DecryptionStatus::VERSION_NOT_SUPPORTED);
239             }
240 4         10 my $key = $keys->get($key_id);
241 4 100       10 unless ($key) {
242 1         5 return _error_response(UID2::Client::DecryptionStatus::NOT_AUTHORIZED_FOR_KEY);
243             }
244 3         8 my $data = decrypt_cbc($bytes, $key->secret);
245             +{
246 1         28 is_success => 1,
247             status => UID2::Client::DecryptionStatus::SUCCESS,
248             decrypted_data => $data,
249             encrypted_at => UID2::Client::Timestamp->from_epoch_milli($encrypted_at),
250             };
251             }
252              
253             sub _decrypt_data_v3 {
254 19     19   32 my ($encrypted_bytes, $keys, $identity_scope) = @_;
255 19         91 my ($identity, $version, $key_id, $bytes) = unpack 'a a N! a*', $encrypted_bytes;
256 19 50       34 if (_decode_identity_scope($identity) != $identity_scope) {
257 0         0 return _error_response(UID2::Client::DecryptionStatus::INVALID_IDENTITY_SCOPE);
258             }
259 19 100       35 if (ord($version) != $EncryptedDataVersionV3) {
260 1         5 return _error_response(UID2::Client::DecryptionStatus::VERSION_NOT_SUPPORTED);
261             }
262 18         37 my $key = $keys->get($key_id);
263 18 100       31 unless ($key) {
264 1         3 return _error_response(UID2::Client::DecryptionStatus::NOT_AUTHORIZED_FOR_KEY);
265             }
266 17         31 my $payload = decrypt_gcm($bytes, $key->secret);
267 15 50       30 unless (defined $payload) {
268 0         0 return _error_response(UID2::Client::DecryptionStatus::INVALID_PAYLOAD);
269             }
270 15         51 my ($encrypted_at, $data) = unpack 'q> x4 a*', $payload;
271             +{
272 15         44 is_success => 1,
273             status => UID2::Client::DecryptionStatus::SUCCESS,
274             decrypted_data => $data,
275             encrypted_at => UID2::Client::Timestamp->from_epoch_milli($encrypted_at),
276             };
277             }
278              
279             sub _error_response {
280             +{
281 39     39   129 is_success => undef,
282             status => $_[0],
283             };
284             }
285              
286             sub _decode_identity_scope {
287 29     29   71 (ord($_[0]) >> 4) & 1;
288             }
289              
290             sub encrypt_cbc {
291 0     0 0 0 my ($data, $secret, $iv) = @_;
292 0         0 my $cipher = Crypt::Mode::CBC->new('AES');
293 0   0     0 $iv //= random_bytes($CBC_IV_LEN);
294 0         0 $iv . $cipher->encrypt($data, $secret, $iv);
295             }
296              
297             sub decrypt_cbc {
298 10     10 0 54 my ($data, $secret) = @_;
299 10         17 my $iv = substr $data, 0, $CBC_IV_LEN;
300 10         62 my $cipher = Crypt::Mode::CBC->new('AES');
301 10         20 my $payload = substr $data, $CBC_IV_LEN;
302 10         26 $cipher->decrypt($payload, $secret, $iv);
303             }
304              
305             sub encrypt_gcm {
306 99     99 0 432 my ($data, $secret, $iv) = @_;
307 99   66     290 $iv //= random_bytes($GCM_IV_LEN);
308 99         17521 my $ae = Crypt::AuthEnc::GCM->new('AES', $secret, $iv);
309 99         547 my $ciphertext = $ae->encrypt_add($data);
310 99         676 $iv . $ciphertext . $ae->encrypt_done();
311             }
312              
313             sub decrypt_gcm {
314 114     114 0 2931 my ($data, $secret) = @_;
315 114         180 my $iv = substr $data, 0, $GCM_IV_LEN;
316 114         19329 my $ae = Crypt::AuthEnc::GCM->new('AES', $secret, $iv);
317 114         289 my $payload = substr $data, $GCM_IV_LEN, -$GCM_AUTHTAG_LEN;
318 114         540 my $plaintext = $ae->decrypt_add($payload);
319 114         173 my $authtag = substr $data, -$GCM_AUTHTAG_LEN, $GCM_AUTHTAG_LEN;
320 114 100       892 $ae->decrypt_done($authtag) or croak 'auth data check failed';
321 111         442 $plaintext;
322             }
323              
324             1;
325             __END__