File Coverage

blib/lib/VAPID.pm
Criterion Covered Total %
statement 71 92 77.1
branch 18 38 47.3
condition 2 6 33.3
subroutine 16 16 100.0
pod 7 7 100.0
total 114 159 71.7


line stmt bran cond sub pod time code
1             package VAPID;
2 2     2   139703 use 5.006; use strict; use warnings; our $VERSION = '0.03';
  2     2   19  
  2     2   11  
  2         4  
  2         55  
  2         10  
  2         4  
  2         93  
3 2     2   1261 use Crypt::JWT qw(encode_jwt); use Crypt::PK::ECC; use URI;
  2     2   124622  
  2     2   134  
  2         19  
  2         5  
  2         70  
  2         1255  
  2         9567  
  2         98  
4 2     2   922 use MIME::Base64 qw/encode_base64url decode_base64url/;
  2         1256  
  2         144  
5 2     2   15 use base 'Import::Export';
  2         5  
  2         950  
6              
7             our (%EX, $DEFAULT_SECONDS, $MAX_DEFAULT_SECONDS);
8              
9             BEGIN {
10 2     2   34825 $DEFAULT_SECONDS = 12 * 60 * 60; # 12 hours
11 2         5 $MAX_DEFAULT_SECONDS = 24 * 60 * 60; # 24 hours
12 2         1644 %EX = (
13             generate_vapid_keys => [qw/all generate/],
14             generate_future_expiration_timestamp => [qw/all generate/],
15             generate_vapid_header => [qw/all generate/],
16             validate_subject => [qw/all validate/],
17             validate_public_key => [qw/all validate/],
18             validate_private_key => [qw/all validate/],
19             validate_expiration_key => [qw/all validate/],
20             validate_expiration => [qw/all validate/],
21             );
22             }
23              
24             sub generate_vapid_keys {
25 1     1 1 96 my $curve = Crypt::PK::ECC->new();
26 1         3018 $curve->generate_key('prime256v1');
27 1         13 my $priv = $curve->export_key_raw('private');
28 1         9 my $pub = $curve->export_key_raw('public');
29            
30 1 50       6 if (length($priv) < 32) {
31 0         0 my $padding = 32 - length $priv;
32 0         0 $priv = (0 x $padding) . $priv;
33             }
34            
35 1 50       4 if (length($pub) < 65) {
36 0         0 my $padding = 65 - length $pub;
37 0         0 $pub = (0 x $padding) . $pub;
38             }
39              
40             return (
41 1         16 encode_base64url($pub),
42             encode_base64url($priv)
43             );
44             }
45              
46             sub generate_vapid_header {
47 1     1 1 5 my ($aud, $subject, $pub, $priv, $expiration, $enc) = @_;
48              
49 1 50       4 if (!$aud) {
50 0         0 die "No audience could be generated for VAPID.";
51             }
52              
53 1 50       3 if (ref $aud) {
54 0         0 die "The audience value must be a string containing the origin of a push service";
55             }
56              
57 1         8 my $aud_uri = URI->new($aud);
58              
59 1 50       8468 if (!$aud_uri->host) {
60 0         0 die "VAPID audience is not a url.";
61             }
62              
63 1         131 validate_subject($subject);
64 1         3 validate_public_key($pub);
65 1         4 $priv = validate_private_key($priv);
66              
67 1 50       3 if ($expiration) {
68 1         3 validate_expiration($expiration);
69             } else {
70 0         0 $expiration = generate_future_expiration_timestamp();
71             }
72              
73 1         6 my $payload = {
74             aud => $aud,
75             exp => $expiration,
76             sub => $subject
77             };
78              
79 1         9 my $key = Crypt::PK::ECC->new
80             ->import_key_raw($priv, 'prime256v1')
81             ->export_key_pem('private');
82              
83              
84 1         5849 my $jwt_token = encode_jwt(
85             payload=>$payload,
86             extra_headers => { typ => 'JWT' },
87             alg=>'ES256',
88             key => \$key
89             );
90              
91 1 50       9010 return $enc
92             ? {
93             Authorization => "vapit t=${jwt_token}, k=${pub}"
94             }
95             : {
96             Authorization => 'WebPush ' . $jwt_token,
97             'Crypto-Key' => 'p256ecdsa=' . $pub
98             };
99             }
100              
101             sub generate_future_expiration_timestamp {
102 2     2 1 3 my ($add) = shift;
103 2   33     7 return time + ($add || $DEFAULT_SECONDS);
104             }
105              
106             sub validate_subject {
107 2     2 1 555 my ($subject) = shift;
108            
109 2 50       7 if (!$subject) {
110 0         0 die "No subject passed to validate_subject";
111             }
112              
113 2 50       5 if (ref $subject) {
114 0         0 die "The subject value must be a string containing a URL or 'mailto: address.'";
115             }
116              
117 2 50       12 unless ($subject =~ m/^mailto\:/) {
118 0         0 my $uri = URI->new($subject);
119 0 0       0 if (!$uri->host) {
120 0         0 die "VAPID subject is not a url or mailto: address";
121             }
122             }
123              
124 2         5 return $subject;
125             }
126              
127             sub validate_public_key {
128 2     2 1 5 my ($pub) = shift;
129              
130 2 50       6 if (!$pub) {
131 0         0 die "No public key passed to validate_public_key";
132             }
133              
134 2 50       7 if (ref $pub) {
135 0         0 die "Vapid public key is must be a URL safe Base 64 encoded string";
136             }
137              
138 2         8 $pub = decode_base64url($pub);
139              
140 2 50       28 if (length $pub != 65) {
141 0         0 die "VAPID public key should be 65 bytes long when decoded.";
142             }
143            
144 2         5 return $pub;
145             }
146              
147             sub validate_private_key {
148 2     2 1 3 my ($priv) = shift;
149              
150 2 50       6 if (!$priv) {
151 0         0 die "No private key passed to validate_private_key";
152             }
153              
154 2 50       5 if (ref $priv) {
155 0         0 die "VAPID private key is must be a URL safe Base 64 encoded string";
156             }
157              
158 2         5 $priv = decode_base64url($priv);
159            
160 2 50       19 if (length $priv != 32) {
161 0         0 die "VAPID private key should be 32 bytes long when decoded.";
162             }
163              
164 2         7 return $priv;
165             }
166              
167             sub validate_expiration {
168 2     2 1 4 my $expiration = shift;
169              
170 2 50 33     19 if (!$expiration || $expiration !~ m/^\d+$/) {
171 0         0 die "expiration value must be a number";
172             }
173              
174 2         7 my $max = generate_future_expiration_timestamp($MAX_DEFAULT_SECONDS);
175              
176 2 50       6 if ($expiration >= $max) {
177 0         0 die "expiration value is greater than maximum of 24 hours";
178             }
179            
180 2         5 return $expiration;
181             }
182              
183             1;
184              
185             __END__