File Coverage

blib/lib/Session/Storage/Secure.pm
Criterion Covered Total %
statement 122 122 100.0
branch 37 38 97.3
condition 13 18 72.2
subroutine 24 24 100.0
pod 2 3 66.6
total 198 205 96.5


line stmt bran cond sub pod time code
1 5     5   408866 use 5.008001;
  5         51  
2 5     5   28 use strict;
  5         10  
  5         113  
3 5     5   24 use warnings;
  5         11  
  5         314  
4              
5             package Session::Storage::Secure;
6             # ABSTRACT: Encrypted, expiring, compressed, serialized session data with integrity
7              
8             our $VERSION = '0.901'; # TRIAL
9              
10 5     5   33 use Carp (qw/croak/);
  5         10  
  5         312  
11 5     5   3604 use Crypt::CBC 2.33 ();
  5         40826  
  5         130  
12 5     5   2287 use Crypt::Rijndael ();
  5         3178  
  5         149  
13 5     5   2477 use Crypt::URandom (qw/urandom/);
  5         26479  
  5         283  
14 5     5   2880 use Digest::SHA (qw/hmac_sha256/);
  5         16104  
  5         428  
15 5     5   2621 use Math::Random::ISAAC::XS ();
  5         2540  
  5         161  
16 5     5   949 use MIME::Base64 3.12 ();
  5         1297  
  5         191  
17 5     5   34 use Sereal::Encoder 4.005 ();
  5         95  
  5         121  
18 5     5   26 use Sereal::Decoder 4.005 ();
  5         75  
  5         107  
19 5     5   2533 use String::Compare::ConstantTime qw/equals/;
  5         3288  
  5         289  
20 5     5   2669 use namespace::clean;
  5         76668  
  5         30  
21              
22 5     5   4123 use Moo;
  5         35688  
  5         26  
23 5     5   10654 use MooX::Types::MooseLike::Base 0.16 qw(:all);
  5         35114  
  5         9456  
24              
25             #--------------------------------------------------------------------------#
26             # Attributes
27             #--------------------------------------------------------------------------#
28              
29             #pod =attr secret_key (required)
30             #pod
31             #pod This is used to secure the session data. The encryption and message
32             #pod authentication key is derived from this using a one-way function. Changing it
33             #pod will invalidate all sessions.
34             #pod
35             #pod =cut
36              
37             has secret_key => (
38             is => 'ro',
39             isa => Str,
40             required => 1,
41             );
42              
43             #pod =attr default_duration
44             #pod
45             #pod Number of seconds for which the session may be considered valid. If an
46             #pod expiration is not provided to C, this is used instead to expire the
47             #pod session after a period of time. It is unset by default, meaning that session
48             #pod expiration is not capped.
49             #pod
50             #pod =cut
51              
52             has default_duration => (
53             is => 'ro',
54             isa => Int,
55             predicate => 1,
56             );
57              
58             #pod =attr old_secrets
59             #pod
60             #pod An optional array reference of strings containing old secret keys no longer
61             #pod used for encryption but still supported for decrypting session data.
62             #pod
63             #pod =cut
64              
65             has old_secrets => (
66             is => 'ro',
67             isa => ArrayRef [Str],
68             );
69              
70             #pod =attr separator
71             #pod
72             #pod A character used to separate fields. It defaults to C<~>.
73             #pod
74             #pod =cut
75              
76             has separator => (
77             is => 'ro',
78             isa => Str,
79             default => '~',
80             );
81              
82             #pod =attr sereal_encoder_options
83             #pod
84             #pod A hash reference with constructor arguments for L. Defaults
85             #pod to C<< { snappy => 1, croak_on_bless => 1 } >>.
86             #pod
87             #pod =cut
88              
89             has sereal_encoder_options => (
90             is => 'ro',
91             isa => HashRef,
92             default => sub { { snappy => 1, croak_on_bless => 1 } },
93             );
94              
95             #pod =attr sereal_decoder_options
96             #pod
97             #pod A hash reference with constructor arguments for L. Defaults
98             #pod to C<< { refuse_objects => 1, validate_utf8 => 1 } >>.
99             #pod
100             #pod =cut
101              
102             has sereal_decoder_options => (
103             is => 'ro',
104             isa => HashRef,
105             default => sub { { refuse_objects => 1, validate_utf8 => 1 } },
106             );
107              
108             #pod =attr transport_encoder
109             #pod
110             #pod A code reference to convert binary data elements (the encrypted data and the
111             #pod MAC) into a transport-safe form. Defaults to
112             #pod L. The output must not include
113             #pod the C attribute used to delimit fields.
114             #pod
115             #pod =cut
116              
117             has transport_encoder => (
118             is => 'ro',
119             isa => CodeRef,
120             default => sub { \&MIME::Base64::encode_base64url },
121             );
122              
123             #pod =attr transport_decoder
124             #pod
125             #pod A code reference to extract binary data (the encrypted data and the
126             #pod MAC) from a transport-safe form. It must be the complement to C.
127             #pod Defaults to L.
128             #pod
129             #pod =cut
130              
131             has transport_decoder => (
132             is => 'ro',
133             isa => CodeRef,
134             default => sub { \&MIME::Base64::decode_base64url },
135             );
136              
137             #pod =attr protocol_version
138             #pod
139             #pod An integer representing the protocol used by C.
140             #pod Protocol 1 was the initial version, which used a now-deprecated mode of
141             #pod L. Protocol 2 is the current default.
142             #pod
143             #pod =cut
144              
145             has protocol_version => (
146             is => 'ro',
147             isa => Num,
148             default => 2,
149             );
150              
151             has _encoder => (
152             is => 'lazy',
153             isa => InstanceOf ['Sereal::Encoder'],
154             handles => { '_freeze' => 'encode' },
155             );
156              
157             sub _build__encoder {
158 31     31   854 my ($self) = @_;
159 31         922 return Sereal::Encoder->new( $self->sereal_encoder_options );
160             }
161              
162             has _decoder => (
163             is => 'lazy',
164             isa => InstanceOf ['Sereal::Decoder'],
165             handles => { '_thaw' => 'decode' },
166             );
167              
168             sub _build__decoder {
169 20     20   14669 my ($self) = @_;
170 20         546 return Sereal::Decoder->new( $self->sereal_decoder_options );
171             }
172              
173             has _rng => (
174             is => 'lazy',
175             isa => InstanceOf ['Math::Random::ISAAC::XS'],
176             handles => { '_irand' => 'irand' },
177             );
178              
179             sub _build__rng {
180 31     31   836 my ($self) = @_;
181 31         105 return Math::Random::ISAAC::XS->new( map { unpack( "N", urandom(4) ) } 1 .. 256 );
  7936         232869  
182             }
183              
184             sub BUILD {
185 52     52 0 1992 my ($self) = @_;
186 52         193 $self->_check_version_for( encoding => $self->protocol_version );
187             }
188              
189             sub _check_version_for {
190 89     89   215 my ( $self, $action, $pv ) = @_;
191 89 100 100     984 if ( $pv < 1 || $pv > 2 ) {
192 3         314 croak "Invalid protocol version for $action: $pv";
193             }
194             }
195              
196             sub _get_cbc {
197 51     51   152 my ( $self, $pv, $key, $salt ) = @_;
198              
199 51         187 my $cbc_opts = {
200             -key => $key,
201             -cipher => 'Rijndael',
202             };
203              
204 51 100       148 if ( $pv == 1 ) {
205 23         52 $cbc_opts->{-pbkdf} = 'opensslv1';
206 23         41 $cbc_opts->{-nodeprecate} = 1;
207             }
208             else {
209 28         68 $cbc_opts->{-pbkdf} = 'none';
210 28         55 $cbc_opts->{-keysize} = 32;
211 28         52 $cbc_opts->{-literal_key} = 1;
212 28         51 $cbc_opts->{-header} = 'none';
213 28         321 my $cipher = Crypt::Rijndael->new($key);
214 28         246 $cbc_opts->{-iv} = substr( $cipher->encrypt($salt), 0, 16 );
215             }
216              
217 51         345 return Crypt::CBC->new(%$cbc_opts);
218             }
219              
220             #pod =method encode
221             #pod
222             #pod my $string = $store->encode( $data, $expires );
223             #pod
224             #pod The C<$data> argument should be a reference to a data structure. By default,
225             #pod it must not contain objects. (See L for
226             #pod rationale and alternatives.) If it is undefined, an empty hash reference will
227             #pod be encoded instead.
228             #pod
229             #pod The optional C<$expires> argument should be the session expiration time
230             #pod expressed as epoch seconds. If the C<$expires> time is in the past, the
231             #pod C<$data> argument is cleared and an empty hash reference is encoded and returned.
232             #pod If no C<$expires> is given, then if the C attribute is set, it
233             #pod will be used to calculate an expiration time.
234             #pod
235             #pod The method returns a string that securely encodes the session data. All binary
236             #pod components are protected via the L attribute.
237             #pod
238             #pod An exception is thrown on any errors.
239             #pod
240             #pod =cut
241              
242             sub encode {
243 31     31 1 11799 my ( $self, $data, $expires ) = @_;
244 31 100       106 $data = {} unless defined $data;
245 31         96 my $sep = $self->separator;
246              
247             # If expiration is set, we want to check it and possibly clear data;
248             # if not set, we might add an expiration based on default_duration
249 31 100       85 if ( defined $expires ) {
250 4 100       17 $data = {} if $expires < time;
251             }
252             else {
253 27 100       144 $expires = $self->has_default_duration ? time + $self->default_duration : "";
254             }
255              
256             # Random salt used to derive unique encryption/MAC key for each cookie
257 31         102 my $salt;
258 31 100       99 if ( $self->protocol_version == 1 ) {
259             # numeric salt
260 14         340 $salt = $self->_irand;
261             }
262             else {
263             # binary salt
264 17         61 $salt = pack( "N*", map { $self->_irand } 1 .. 8 );
  136         6804  
265             }
266              
267 31         2632 my $key = hmac_sha256( $salt, $self->secret_key );
268              
269 31         135 my $cbc = $self->_get_cbc( $self->protocol_version, $key, $salt );
270              
271 31         8186 my ( $ciphertext, $mac );
272 31         70 eval {
273 31         749 $ciphertext = $self->transport_encoder->( $cbc->encrypt( $self->_freeze($data) ) );
274 30         36345 $mac = $self->transport_encoder->( hmac_sha256( "$expires$sep$ciphertext", $key ) );
275             };
276 31 100       703 croak "Encoding error: $@" if $@;
277              
278 30         61 my $output;
279 30 100       106 if ( $self->protocol_version == 1 ) {
280 14         55 $output = join( $sep, $salt, $expires, $ciphertext, $mac );
281             }
282             else {
283 16         50 $salt = $self->transport_encoder->($salt);
284 16         215 $output = join( $sep, $salt, $expires, $ciphertext, $mac, $self->protocol_version );
285             }
286 30         379 return $output;
287             }
288              
289             #pod =method decode
290             #pod
291             #pod my $data = $store->decode( $string );
292             #pod
293             #pod The C<$string> argument must be the output of C.
294             #pod
295             #pod If the message integrity check fails or if expiration exists and is in
296             #pod the past, the method returns undef or an empty list (depending on context).
297             #pod
298             #pod An exception is thrown on any errors.
299             #pod
300             #pod =cut
301              
302             sub decode {
303 43     43 1 235511 my ( $self, $string ) = @_;
304 43 100       155 return unless length $string;
305              
306             # Having a string implies at least salt; expires is optional; rest required
307 41         123 my $sep = $self->separator;
308 41         429 my ( $salt, $expires, $ciphertext, $mac, $version ) = split qr/\Q$sep\E/, $string;
309 41 100 66     281 return unless defined($ciphertext) && length($ciphertext);
310 39 100 66     175 return unless defined($mac) && length($mac);
311 37 100       94 $version = 1 unless defined $version;
312 37         113 $self->_check_version_for( decoding => $version );
313              
314 37 100       102 if ( $version == 1 ) {
315             # $salt is a decimal
316             }
317             else {
318             # Decode salt to binary
319 20         86 $salt = $self->transport_decoder->($salt);
320             }
321              
322             # Try to decode against all known secret keys
323 37 100       308 my @secrets = ( $self->secret_key, @{ $self->old_secrets || [] } );
  37         186  
324 37         70 my $key;
325 37         82 CHECK: foreach my $secret (@secrets) {
326 50         444 $key = hmac_sha256( $salt, $secret );
327 50         102 my $check_mac = eval {
328 50         461 $self->transport_encoder->( hmac_sha256( "$expires$sep$ciphertext", $key ) );
329             };
330 50 100 33     905 last CHECK
      66        
331             if (
332             defined($check_mac)
333             && length($check_mac)
334             && equals( $check_mac, $mac ) # constant time comparison
335             );
336 26         67 undef $key;
337             }
338              
339             # Check MAC integrity
340 37 100       121 return unless defined($key);
341              
342             # Check expiration
343 24 100 100     105 return if length($expires) && $expires < time;
344              
345             # Decrypt and deserialize the data
346 20         60 my $cbc = $self->_get_cbc( $version, $key, $salt );
347              
348 20         4861 my $data;
349 20         38 eval {
350 20         89 $self->_thaw( $cbc->decrypt( $self->transport_decoder->($ciphertext) ), $data );
351             };
352 20 50       1495 croak "Decoding error: $@" if $@;
353              
354 20         178 return $data;
355             }
356              
357             1;
358              
359              
360             # vim: ts=4 sts=4 sw=4 et:
361              
362             __END__