File Coverage

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


line stmt bran cond sub pod time code
1 5     5   434088 use 5.008001;
  5         53  
2 5     5   31 use strict;
  5         12  
  5         122  
3 5     5   27 use warnings;
  5         11  
  5         340  
4              
5             package Session::Storage::Secure;
6             # ABSTRACT: Encrypted, expiring, compressed, serialized session data with integrity
7              
8             our $VERSION = '0.903'; # TRIAL
9              
10 5     5   38 use Carp (qw/croak/);
  5         8  
  5         382  
11 5     5   3834 use Crypt::CBC 3.01 ();
  5         42753  
  5         156  
12 5     5   2738 use Crypt::Rijndael ();
  5         3229  
  5         160  
13 5     5   2513 use Crypt::URandom (qw/urandom/);
  5         28750  
  5         334  
14 5     5   3166 use Digest::SHA (qw/hmac_sha256/);
  5         16703  
  5         500  
15 5     5   2710 use Math::Random::ISAAC::XS ();
  5         2681  
  5         160  
16 5     5   1030 use MIME::Base64 3.12 ();
  5         1495  
  5         137  
17 5     5   34 use Sereal::Encoder 4.005 ();
  5         113  
  5         129  
18 5     5   29 use Sereal::Decoder 4.005 ();
  5         76  
  5         112  
19 5     5   2605 use String::Compare::ConstantTime qw/equals/;
  5         3319  
  5         370  
20 5     5   2610 use namespace::clean;
  5         80561  
  5         39  
21              
22 5     5   4364 use Moo;
  5         37299  
  5         30  
23 5     5   11151 use MooX::Types::MooseLike::Base 0.16 qw(:all);
  5         36119  
  5         9869  
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   882 my ($self) = @_;
159 31         1020 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   14874 my ($self) = @_;
170 20         606 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   853 my ($self) = @_;
181 31         109 return Math::Random::ISAAC::XS->new( map { unpack( "N", urandom(4) ) } 1 .. 256 );
  7936         238169  
182             }
183              
184             sub BUILD {
185 52     52 0 1987 my ($self) = @_;
186 52         213 $self->_check_version_for( encoding => $self->protocol_version );
187             }
188              
189             sub _check_version_for {
190 89     89   220 my ( $self, $action, $pv ) = @_;
191 89 100 100     687 if ( $pv < 1 || $pv > 2 ) {
192 3         426 croak "Invalid protocol version for $action: $pv";
193             }
194             }
195              
196             sub _get_cbc {
197 51     51   155 my ( $self, $pv, $key, $salt ) = @_;
198              
199 51         184 my $cbc_opts = {
200             -key => $key,
201             -cipher => 'Rijndael',
202             };
203              
204 51 100       149 if ( $pv == 1 ) {
205 23         46 $cbc_opts->{-pbkdf} = 'opensslv1';
206 23         40 $cbc_opts->{-nodeprecate} = 1;
207             }
208             else {
209 28         67 $cbc_opts->{-pbkdf} = 'none';
210 28         59 $cbc_opts->{-keysize} = 32;
211 28         62 $cbc_opts->{-header} = 'none';
212 28         345 my $cipher = Crypt::Rijndael->new($key);
213 28         269 $cbc_opts->{-iv} = substr( $cipher->encrypt($salt), 0, 16 );
214             }
215              
216 51         381 return Crypt::CBC->new(%$cbc_opts);
217             }
218              
219             #pod =method encode
220             #pod
221             #pod my $string = $store->encode( $data, $expires );
222             #pod
223             #pod The C<$data> argument should be a reference to a data structure. By default,
224             #pod it must not contain objects. (See L for
225             #pod rationale and alternatives.) If it is undefined, an empty hash reference will
226             #pod be encoded instead.
227             #pod
228             #pod The optional C<$expires> argument should be the session expiration time
229             #pod expressed as epoch seconds. If the C<$expires> time is in the past, the
230             #pod C<$data> argument is cleared and an empty hash reference is encoded and returned.
231             #pod If no C<$expires> is given, then if the C attribute is set, it
232             #pod will be used to calculate an expiration time.
233             #pod
234             #pod The method returns a string that securely encodes the session data. All binary
235             #pod components are protected via the L attribute.
236             #pod
237             #pod An exception is thrown on any errors.
238             #pod
239             #pod =cut
240              
241             sub encode {
242 31     31 1 12926 my ( $self, $data, $expires ) = @_;
243 31 100       108 $data = {} unless defined $data;
244 31         104 my $sep = $self->separator;
245              
246             # If expiration is set, we want to check it and possibly clear data;
247             # if not set, we might add an expiration based on default_duration
248 31 100       93 if ( defined $expires ) {
249 4 100       18 $data = {} if $expires < time;
250             }
251             else {
252 27 100       118 $expires = $self->has_default_duration ? time + $self->default_duration : "";
253             }
254              
255             # Random salt used to derive unique encryption/MAC key for each cookie
256 31         54 my $salt;
257 31 100       101 if ( $self->protocol_version == 1 ) {
258             # numeric salt
259 14         473 $salt = $self->_irand;
260             }
261             else {
262             # binary salt
263 17         58 $salt = pack( "N*", map { $self->_irand } 1 .. 8 );
  136         7047  
264             }
265              
266 31         2903 my $key = hmac_sha256( $salt, $self->secret_key );
267              
268 31         138 my $cbc = $self->_get_cbc( $self->protocol_version, $key, $salt );
269              
270 31         8018 my ( $ciphertext, $mac );
271 31         66 eval {
272 31         712 $ciphertext = $self->transport_encoder->( $cbc->encrypt( $self->_freeze($data) ) );
273 30         36700 $mac = $self->transport_encoder->( hmac_sha256( "$expires$sep$ciphertext", $key ) );
274             };
275 31 100       693 croak "Encoding error: $@" if $@;
276              
277 30         55 my $output;
278 30 100       105 if ( $self->protocol_version == 1 ) {
279 14         48 $output = join( $sep, $salt, $expires, $ciphertext, $mac );
280             }
281             else {
282 16         53 $salt = $self->transport_encoder->($salt);
283 16         197 $output = join( $sep, $salt, $expires, $ciphertext, $mac, $self->protocol_version );
284             }
285 30         433 return $output;
286             }
287              
288             #pod =method decode
289             #pod
290             #pod my $data = $store->decode( $string );
291             #pod
292             #pod The C<$string> argument must be the output of C.
293             #pod
294             #pod If the message integrity check fails or if expiration exists and is in
295             #pod the past, the method returns undef or an empty list (depending on context).
296             #pod
297             #pod An exception is thrown on any errors.
298             #pod
299             #pod =cut
300              
301             sub decode {
302 43     43 1 245231 my ( $self, $string ) = @_;
303 43 100       150 return unless length $string;
304              
305             # Having a string implies at least salt; expires is optional; rest required
306 41         126 my $sep = $self->separator;
307 41         440 my ( $salt, $expires, $ciphertext, $mac, $version ) = split qr/\Q$sep\E/, $string;
308 41 100 66     283 return unless defined($ciphertext) && length($ciphertext);
309 39 100 66     170 return unless defined($mac) && length($mac);
310 37 100       96 $version = 1 unless defined $version;
311 37         119 $self->_check_version_for( decoding => $version );
312              
313 37 100       107 if ( $version == 1 ) {
314             # $salt is a decimal
315             }
316             else {
317             # Decode salt to binary
318 20         87 $salt = $self->transport_decoder->($salt);
319             }
320              
321             # Try to decode against all known secret keys
322 37 100       319 my @secrets = ( $self->secret_key, @{ $self->old_secrets || [] } );
  37         187  
323 37         83 my $key;
324 37         83 CHECK: foreach my $secret (@secrets) {
325 50         447 $key = hmac_sha256( $salt, $secret );
326 50         107 my $check_mac = eval {
327 50         457 $self->transport_encoder->( hmac_sha256( "$expires$sep$ciphertext", $key ) );
328             };
329 50 100 33     970 last CHECK
      66        
330             if (
331             defined($check_mac)
332             && length($check_mac)
333             && equals( $check_mac, $mac ) # constant time comparison
334             );
335 26         69 undef $key;
336             }
337              
338             # Check MAC integrity
339 37 100       128 return unless defined($key);
340              
341             # Check expiration
342 24 100 100     109 return if length($expires) && $expires < time;
343              
344             # Decrypt and deserialize the data
345 20         55 my $cbc = $self->_get_cbc( $version, $key, $salt );
346              
347 20         4804 my $data;
348 20         52 eval {
349 20         107 $self->_thaw( $cbc->decrypt( $self->transport_decoder->($ciphertext) ), $data );
350             };
351 20 50       1516 croak "Decoding error: $@" if $@;
352              
353 20         176 return $data;
354             }
355              
356             1;
357              
358              
359             # vim: ts=4 sts=4 sw=4 et:
360              
361             __END__