File Coverage

blib/lib/DBIx/Squirrel/Crypt/Fernet.pm
Criterion Covered Total %
statement 78 78 100.0
branch 12 12 100.0
condition 6 9 66.6
subroutine 25 25 100.0
pod 11 11 100.0
total 132 135 97.7


line stmt bran cond sub pod time code
1 10     10   213540 use strict;
  10         23  
  10         524  
2 10     10   58 use warnings;
  10         20  
  10         665  
3 10     10   190 use 5.010_001;
  10         41  
4              
5             package # hide from PAUSE
6             DBIx::Squirrel::Crypt::Fernet;
7              
8             =head1 NAME
9              
10             DBIx::Squirrel::Crypt::Fernet - Fernet symmetric encryption
11              
12             =head1 SYNOPSIS
13              
14             #############################
15             # Object-oriented Interface #
16             #############################
17              
18             # Import the helper
19             use DBIx::Squirrel::Crypt::Fernet 'Fernet';
20              
21             # Generate random key
22             $fernet = Fernet();
23              
24             # Use pre-defined Base64-encoded key
25             $fernet = Fernet($key);
26              
27             # Import nothing
28             use DBIx::Squirrel::Crypt::Fernet;
29              
30             # Generate random key
31             $fernet = DBIx::Squirrel::Crypt::Fernet->new();
32              
33             # Use pre-defined Base64-encoded key
34             $fernet = DBIx::Squirrel::Crypt::Fernet->new($key);
35              
36             # Encrypt message
37             $token = $fernet->encrypt($message);
38              
39             # Decrypt token
40             $message = $fernet->decrypt($token);
41              
42             # Verify token
43             $bool = $fernet->verify($token);
44              
45             # Decrypt token, check time-to-live (secs) has not expired
46             $message = $fernet->decrypt($token, $ttl);
47              
48             # Verify token, check time-to-live (secs) has not expired
49             $bool = $fernet->verify($token, $ttl);
50              
51             # Retrieve Base64-encoded key
52             $key = $fernet->to_string();
53             $key = "$fernet";
54              
55             ######################
56             # Exported functions #
57             ######################
58              
59             # Import functions
60             use DBIx::Squirrel::Crypt::Fernet qw(
61             generatekey
62             encrypt
63             decrypt
64             verify
65             );
66              
67             # Import Crypt::Fernet-like interface
68             use DBIx::Squirrel::Crypt::Fernet qw(
69             fernet_genkey
70             fernet_encrypt
71             fernet_decrypt
72             fernet_verify
73             );
74              
75             # Generate a Base64-encoded random key
76             $key = generatekey();
77             $key = fernet_genkey();
78              
79             # Encrypt message
80             $token = encrypt($key, $message);
81             $token = fernet_encrypt($key, $message);
82              
83             # Decrypt token
84             $message = decrypt($key, $token);
85             $message = fernet_decrypt($key, $token);
86              
87             # Verify token
88             $bool = verify($key, $token);
89             $bool = fernet_verify($key, $token);
90              
91             # Decrypt token, check time-to-live (secs) has not expired
92             $message = decrypt($key, $token, $ttl);
93             $message = fernet_decrypt($key, $token, $ttl);
94              
95             # Verify token, check time-to-live (secs) has not expired
96             $bool = verify($key, $token, $ttl);
97             $bool = fernet_verify($key, $token, $ttl);
98              
99             =head1 DESCRIPTION
100              
101             Fernet takes a user-provided message (an arbitrary sequence of bytes), a
102             256-bit key, and the current time, and it produces a token containing the
103             message in a form that can't be read or altered without the key.
104              
105             See L for more detail.
106              
107             =cut
108              
109             our @ISA = qw(Exporter);
110             our @EXPORT;
111             our %EXPORT_TAGS = ( all => [
112             our @EXPORT_OK = qw(
113             fernet_decrypt
114             fernet_encrypt
115             fernet_genkey
116             fernet_verify
117             decrypt
118             encrypt
119             generatekey
120             verify
121             Fernet
122             )
123             ] );
124             our $VERSION = '1.0.0';
125              
126             require Crypt::CBC;
127             require Crypt::Rijndael;
128             require Exporter;
129              
130 10     10   5588 use Const::Fast 'const';
  10         17106  
  10         80  
131 10     10   6709 use Digest::SHA 'hmac_sha256';
  10         36676  
  10         1308  
132 10         870 use MIME::Base64::URLSafe qw(
133             urlsafe_b64decode
134             urlsafe_b64encode
135 10     10   5000 );
  10         17403  
136 10     10   5819 use namespace::clean;
  10         168177  
  10         86  
137 10     10   7418 use overload '""' => \&to_string; # overload after namespace::clean for stringification to work
  10         2562  
  10         142  
138              
139             const my $TOKEN_VERSION => pack( "H*", '80' );
140             const my $LEN_HDR => 25;
141             const my $LEN_DIGEST => 32;
142             const my $LEN_HDR_DIGEST => $LEN_HDR + $LEN_DIGEST;
143              
144             # Calculate the age (seconds) of a token from its timestamp field.
145             sub _age_sec {
146 10     10   1706 use bytes;
  10         29  
  10         96  
147 8     8   22 my($token) = @_;
148 8         78 return time - unpack( 'V', reverse( substr( $token, 1, 8 ) ) );
149             }
150              
151              
152             # Generate a timestamp field that can be embedded in a token.
153             sub _timestamp {
154 10     10   985 use bytes;
  10         24  
  10         49  
155 2     2   5 local $_;
156 2         4 my $t = time();
157 2         40 my @p = map( substr( pack( 'I', ( $t >> $_ * 8 ) & 0xFF ), 0, 1 ), 0 .. 7 );
158 2         27 return join( '', reverse(@p) );
159             }
160              
161              
162             # Generate a random 32-byte Fernet key.
163             sub _rand_key {
164 3     3   24 return Crypt::CBC->random_bytes(32);
165             }
166              
167              
168             # Encode a binary string as Base64 with padding.
169             sub _pad_b64encode {
170 6     6   105 my $b64 = urlsafe_b64encode(shift);
171 6         81 return $b64 . '=' x ( 4 - length($b64) % 4 );
172             }
173              
174              
175             =head2 METHODS
176              
177             =head3 C
178              
179             $obj = DBIx::Squirrel::Crypt::Fernet->new();
180             $obj = DBIx::Squirrel::Crypt::Fernet->new($key);
181              
182             A constructor (also see L).
183              
184             If no arguments are passed then a random 32-byte Fernet key is generated. If
185             a Base64-encoded key is passed then it will be decoded and its signing and
186             encryption key fields extracted.
187              
188             Take care never to display the binary signing and extraction keys, but to use
189             the C method (or stringification) to recombine them into a Base64-
190             encoded Fernet key.
191              
192             =cut
193              
194             sub new {
195 4     4 1 19 my( $class, $b64key ) = @_;
196 4 100       28 my $fernet_key = $b64key ? urlsafe_b64decode($b64key) : _rand_key();
197 4         164 my $self = {
198             signing_key => substr( $fernet_key, 0, 16 ),
199             encrypt_key => substr( $fernet_key, 16, 16 ),
200             };
201 4   33     32 return bless $self, ref($class) || $class;
202             }
203              
204              
205             =head3 C
206              
207             $key = $obj->generatekey();
208             $key = DBIx::Squirrel::Crypt::Fernet->generatekey();
209              
210             Returns a Base64-encoded randomly-generated key.
211              
212             =cut
213              
214             sub generatekey {
215 2     2 1 8 return _pad_b64encode( _rand_key() );
216             }
217              
218              
219             =head3 C
220              
221             $token = $obj->encrypt($message);
222              
223             Encrypts a message, returning a Base64-encode token.
224              
225             =cut
226              
227             sub encrypt {
228 2     2 1 7 my( $self_or_b64key, $data ) = @_;
229 2         4 my( $signing_key, $encrypt_key ) = do {
230 2 100       17 if ( UNIVERSAL::isa( $self_or_b64key, __PACKAGE__ ) ) {
231 1         2 @{$self_or_b64key}{qw(signing_key encrypt_key)};
  1         4  
232             }
233             else {
234 1         9 my $key = urlsafe_b64decode($self_or_b64key);
235 1         22 substr( $key, 0, 16 ), substr( $key, 16, 16 );
236             }
237             };
238 2         14 my $iv = Crypt::CBC->random_bytes(16);
239 2         135 my $t = $TOKEN_VERSION . _timestamp() . $iv . Crypt::CBC->new(
240             -cipher => 'Rijndael',
241             -header => 'none',
242             -iv => $iv,
243             -key => $encrypt_key,
244             -keysize => 16,
245             -literal_key => 1,
246             )->encrypt($data);
247 2         1139 return _pad_b64encode( $t . hmac_sha256( $t, $signing_key ) );
248             }
249              
250              
251             =head3 C
252              
253             $message = $obj->decrypt($token);
254             $message = $obj->decrypt($token, $ttl);
255              
256             Returns the decrypted message, or C if the token could not be
257             decrypted. If a time-to-live (seconds) is specified (C<$ttl>) then a
258             further check is made to ensure that the token has not expired.
259              
260             =cut
261              
262             sub decrypt {
263 6     6 1 1925 my( $self_or_b64key, $b64token, $ttl ) = @_;
264 6 100       18 return unless verify( $self_or_b64key, $b64token, $ttl );
265 4         8 my $encrypt_key = do {
266 4 100       17 if ( UNIVERSAL::isa( $self_or_b64key, __PACKAGE__ ) ) {
267 2         7 $self_or_b64key->{encrypt_key};
268             }
269             else {
270 2         8 substr( urlsafe_b64decode($self_or_b64key), 16, 16 );
271             }
272             };
273 4         29 my $t = urlsafe_b64decode($b64token);
274 4         74 return Crypt::CBC->new(
275             -cipher => 'Rijndael',
276             -header => 'none',
277             -iv => substr( $t, 9, 16 ),
278             -key => $encrypt_key,
279             -keysize => 16,
280             -literal_key => 1,
281             )->decrypt( substr( $t, $LEN_HDR, length($t) - $LEN_HDR_DIGEST ) );
282             }
283              
284              
285             =head3 C
286              
287             $bool = $obj->verify($token);
288             $bool = $obj->verify($token, $ttl);
289              
290             Returns true if the token was signed using the same signing key as that
291             embedded in the Fernet key. If a time-to-live (seconds) is specified (C<$ttl>)
292             then a further check is made to ensure that the token has not expired.
293              
294             =cut
295              
296             sub verify {
297 12     12 1 403 my( $self_or_b64key, $b64token, $ttl ) = @_;
298 12         17 my $signing_key = do {
299 12 100       66 if ( UNIVERSAL::isa( $self_or_b64key, __PACKAGE__ ) ) {
300 6         31 $self_or_b64key->{signing_key};
301             }
302             else {
303 6         51 substr( urlsafe_b64decode($self_or_b64key), 0, 16 );
304             }
305             };
306 12         136 my $t = urlsafe_b64decode($b64token);
307 12 100 100     205 return !!0
      66        
308             if $TOKEN_VERSION ne substr( $t, 0, 1 ) || $ttl && _age_sec($t) > $ttl;
309 8         28 my $digest = substr( $t, length($t) - $LEN_DIGEST, $LEN_DIGEST, '' ); # 4-arg substr removes $digest from $token
310 8         105 return $digest eq hmac_sha256( $t, $signing_key );
311             }
312              
313              
314             =head3 C
315              
316             $key = $obj->to_string();
317             $key = "$obj";
318              
319             Returns the Base64-encoded key.
320              
321             =cut
322              
323             sub to_string {
324 2     2 1 366 my($self) = @_;
325 2         5 return _pad_b64encode( join( '', @{$self}{qw(signing_key encrypt_key)} ) );
  2         37  
326             }
327              
328              
329             =head2 EXPORTS
330              
331             This package exports nothing by default.
332              
333             =head3 C
334              
335             $obj = Fernet();
336             $obj = Fernet($key);
337              
338             Alternative constructor (also see L).
339              
340             Returns a new C object.
341              
342             If no arguments are passed then a random 32-byte Fernet key is generated. If
343             a Base64-encoded key is passed then it will be decoded and its signing and
344             encryption key fields extracted.
345              
346             Take care never to display the binary signing and extraction keys, but to use
347             the C method (or stringification) to recombine them into a Base64-
348             encoded Fernet key.
349              
350             =cut
351              
352             sub Fernet {
353 4     4 1 182212 return __PACKAGE__->new(@_);
354             }
355              
356              
357             =head3 C
358              
359             $key = generatekey();
360              
361             Returns a Base64-encoded randomly-generated key.
362              
363             =head3 C
364              
365             $token = encrypt($key, $message);
366              
367             Encrypts a message, returning a Base64-encode token.
368              
369             While a Base64-encoded key may be passed as the first argument, it would be
370             more efficient to call the "two-faced" C as a method on a Fernet
371             object to avoid the repeated overhead of decoding and parsing-out the signing
372             and encryption keys.
373              
374             =head3 C
375              
376             $message = decrypt($key, $token);
377             $message = decrypt($key, $token, $ttl);
378              
379             Returns the decrypted message, or C if the token could not be
380             decrypted. If a time-to-live (seconds) is specified (C<$ttl>) then a
381             further check is made to ensure that the token has not expired.
382              
383             While a Base64-encoded key may be passed as the first argument, it would be
384             more efficient to call the "two-faced" C as a method on a Fernet
385             object to avoid the repeated overhead of decoding and parsing-out the signing
386             and encryption keys.
387              
388             =head3 C
389              
390             $bool = verify($key, $token);
391             $bool = verify($key, $token, $ttl);
392              
393             Returns true if the token was signed using the same signing key as that
394             embedded in the Fernet key. If a time-to-live (seconds) is specified (C<$ttl>)
395             then a further check is made to ensure that the token has not expired.
396              
397             While a Base64-encoded key may be passed as the first argument, it would be
398             more efficient to call the "two-faced" C as a method on a Fernet
399             object to avoid the repeated overhead of decoding and parsing-out the signing
400             and encryption keys.
401              
402             =cut
403              
404              
405             =head2 LEGACY C INTERFACE
406              
407             At the time I wanted to use Wan Leung Wong's C package, it had
408             a few testing failures and would not build. I'm pretty sure the C
409             dependency introduced a breaking change. I did submit a fix, but deployment
410             and communication have been problematic. It has probably been fixed by now,
411             but I have decided to rework the original package, extend the interface,
412             and have kept this namespace active. Nevertheless, the lion's share of the
413             credit should go to the author of the original work.
414              
415             The original C package exported four functions as its primary
416             public interface, and this package does the same on request:
417              
418             =over
419              
420             =item * C
421              
422             =item * C
423              
424             =item * C
425              
426             =item * C
427              
428             =back
429              
430             =head3 C
431              
432             $key = fernet_genkey();
433              
434             Returns a Base64-encoded randomly-generated key.
435              
436             =cut
437              
438             sub fernet_genkey {
439 1     1 1 3445 goto &generatekey;
440             }
441              
442              
443             =head3 C
444              
445             $token = fernet_encrypt($key, $message);
446              
447             Encrypts a message, returning a Base64-encode token.
448              
449             =cut
450              
451             sub fernet_encrypt {
452 1     1 1 711 goto &encrypt;
453             }
454              
455              
456             =head3 C
457              
458             $message = fernet_decrypt($key, $token);
459             $message = fernet_decrypt($key, $token, $ttl);
460              
461             Returns the decrypted message, or C if the token could not be
462             decrypted. If a time-to-live (seconds) is specified (C<$ttl>) then a
463             further check is made to ensure that the token has not expired.
464              
465             =cut
466              
467             sub fernet_decrypt {
468 3     3 1 2114 goto &decrypt;
469             }
470              
471              
472             =head3 C
473              
474             $bool = fernet_verify($key, $token);
475             $bool = fernet_verify($key, $token, $ttl);
476              
477             Returns true if the token was signed using the same signing key as that
478             embedded in the Fernet key. If a time-to-live (seconds) is specified (C<$ttl>)
479             then a further check is made to ensure that the token has not expired.
480              
481             =cut
482              
483             sub fernet_verify {
484 3     3 1 2011 goto &verify;
485             }
486              
487             =head1 AUTHORS
488              
489             Iain Campbell Ecpanic@cpan.orgE
490              
491             This is a rewrite of C, so credit for that original work belongs
492             to Wan Leung Wong Ewanleung@linkomnia.comE.
493              
494             =head1 COPYRIGHT AND LICENSE
495              
496             The DBIx::Squirrel module is Copyright (c) 2020-2025 Iain Campbell.
497             All rights reserved.
498              
499             You may distribute under the terms of either the GNU General Public
500             License or the Artistic License, as specified in the Perl 5.10.0 README file.
501              
502             =head1 SUPPORT / WARRANTY
503              
504             DBIx::Squirrel is free Open Source software. IT COMES WITHOUT WARRANTY OF ANY KIND.
505              
506             =cut
507              
508             1;