File Coverage

blib/lib/UID2/Client.pm
Criterion Covered Total %
statement 91 104 87.5
branch 11 20 55.0
condition 6 17 35.2
subroutine 23 24 95.8
pod 8 8 100.0
total 139 173 80.3


line stmt bran cond sub pod time code
1             package UID2::Client;
2 3     3   150646 use strict;
  3         22  
  3         69  
3 3     3   13 use warnings;
  3         3  
  3         126  
4              
5             our $VERSION = '0.01';
6              
7             use Class::Accessor::Lite (
8 3         17 rw => [qw(endpoint auth_key secret_key identity_scope http keys)],
9 3     3   1132 );
  3         3058  
10              
11 3     3   313 use Carp;
  3         6  
  3         124  
12 3     3   1755 use HTTP::Tiny;
  3         121477  
  3         97  
13 3     3   948 use JSON;
  3         6869  
  3         17  
14 3     3   691 use Crypt::PRNG qw(random_bytes);
  3         2457  
  3         123  
15 3     3   412 use Crypt::Misc qw(encode_b64 decode_b64);
  3         6136  
  3         125  
16              
17 3     3   1141 use UID2::Client::Decryption;
  3         8  
  3         77  
18 3     3   982 use UID2::Client::Key;
  3         6  
  3         115  
19 3     3   936 use UID2::Client::KeyContainer;
  3         6  
  3         72  
20 3     3   15 use UID2::Client::Timestamp;
  3         5  
  3         47  
21 3     3   1002 use UID2::Client::IdentityScope;
  3         6  
  3         2507  
22              
23             sub new {
24 59     59 1 83156 my ($class, $options) = @_;
25 59   33     147 my $secret_key = $options->{secret_key} // croak 'secret_key required';
26 59         202 $secret_key = decode_b64($secret_key);
27 59         68 my $http = do {
28 59 50 33     224 if ($options->{http_options} && $options->{http}) {
    50          
    100          
29 0         0 croak 'only one of http_options or http can be specified';
30             } elsif ($options->{http_options}) {
31 0         0 HTTP::Tiny->new(%{$options->{http_options}});
  0         0  
32             } elsif ($options->{http}) {
33 40         64 $options->{http};
34             } else {
35 19         66 HTTP::Tiny->new;
36             }
37             };
38             bless {
39             endpoint => $options->{endpoint} // croak('endpoint required'),
40             auth_key => $options->{auth_key} // croak('auth_key required'),
41             secret_key => $secret_key,
42 59   33     1615 identity_scope => $options->{identity_scope} // UID2::Client::IdentityScope::UID2,
      33        
      50        
43             http => $http,
44             keys => undef,
45             }, $class;
46             }
47              
48             sub new_euid {
49 0     0 1 0 my ($class, $options) = @_;
50 0         0 $class->new({ %$options, identity_scope => UID2::Client::IdentityScope::EUID });
51             }
52              
53             sub refresh {
54 40     40 1 939 my $self = shift;
55 40         54 eval {
56 40         59 $self->keys(_parse_json($self->get_latest_keys));
57 40 50       211 }; if ($@) {
58 0         0 return { is_success => undef, reason => $@ };
59             }
60 40         84 +{ is_success => 1 };
61             }
62              
63             sub refresh_json {
64 14     14 1 2879 my ($self, $json) = @_;
65 14         21 eval {
66 14         27 $self->keys(_parse_json($json));
67 14 50       81 }; if ($@) {
68 0         0 return { is_success => undef, reason => $@ };
69             }
70 14         32 +{ is_success => 1 };
71             }
72              
73             my $V2_NONCE_LEN = 8;
74              
75             sub get_latest_keys {
76 40     40 1 45 my $self = shift;
77 40         85 my $nonce = random_bytes($V2_NONCE_LEN);
78 40         451 my $res = $self->http->post($self->endpoint . '/v2/key/latest', {
79             headers => {
80             'Authorization' => 'Bearer ' . $self->auth_key,
81             'Content-Type' => 'text/plain',
82             },
83             content => $self->_make_v2_request($nonce),
84             });
85 40 50       386 unless ($res->{success}) {
86 0 0       0 if ($res->{status} == 599) {
87 0         0 chomp(my $content = $res->{content});
88 0         0 croak $content;
89             } else {
90 0         0 croak "$res->{status} $res->{reason}";
91             }
92             }
93 40         79 $self->_parse_v2_response($res->{content}, $nonce);
94             }
95              
96             sub _make_v2_request {
97 40     40   389 my ($self, $nonce, $now) = @_;
98 40   33     153 $now //= UID2::Client::Timestamp->now;
99 40         78 my $data = pack 'q> a*', $now->get_epoch_milli, $nonce;
100 40         83 my $payload = UID2::Client::Decryption::encrypt_gcm($data, $self->secret_key),
101             my $version = 1;
102 40         117 my $envelope = pack 'C a*', $version, $payload;
103 40         287 encode_b64($envelope);
104             }
105              
106             sub _parse_v2_response {
107 40     40   68 my ($self, $envelope, $nonce) = @_;
108 40         201 my $envelope_bytes = decode_b64($envelope);
109 40         86 my $payload = UID2::Client::Decryption::decrypt_gcm($envelope_bytes, $self->secret_key);
110 40 50       88 if (length($payload) < 16) {
111 0         0 croak 'invalid payload';
112             }
113 40         135 my ($res_nonce, $data) = unpack "x8 a${V2_NONCE_LEN} a*", $payload;
114 40 50       73 if ($res_nonce ne $nonce) {
115 0         0 croak 'nonce mismatch';
116             }
117 40         125 $data;
118             }
119              
120             sub _parse_json {
121 54     54   89 my $content = shift;
122 54         449 my $obj = decode_json($content);
123 54         76 my @keys;
124 54         58 for my $entry (@{$obj->{body}}) {
  54         113  
125 132         855 $entry->{secret} = decode_b64($entry->{secret});
126 132         265 push @keys, UID2::Client::Key->new($entry);
127             }
128 54         531 UID2::Client::KeyContainer->new(@keys);
129             }
130              
131             sub decrypt {
132 18     18 1 925 my ($self, $token, $now) = @_;
133 18         40 UID2::Client::Decryption::decrypt_token($token, $now, $self->keys, $self->identity_scope);
134             }
135              
136             sub encrypt_data {
137 31     31 1 1245 my ($self, $data, $request) = @_;
138 31         60 $request->{identity_scope} = $self->identity_scope;
139 31 100       138 $request->{keys} = $self->keys unless $request->{key};
140 31         103 UID2::Client::Decryption::encrypt_data($data, $request);
141             }
142              
143             sub decrypt_data {
144 30     30 1 6975 my ($self, $data) = @_;
145 30         62 UID2::Client::Decryption::decrypt_data($data, $self->keys, $self->identity_scope);
146             }
147              
148             1;
149             __END__