File Coverage

blib/lib/Crypt/OpenToken.pm
Criterion Covered Total %
statement 160 166 96.3
branch 35 60 58.3
condition n/a
subroutine 30 30 100.0
pod 2 2 100.0
total 227 258 87.9


line stmt bran cond sub pod time code
1             package Crypt::OpenToken;
2              
3 6     6   740522 use Moose;
  6         2957824  
  6         47  
4 6     6   50830 use Fcntl qw();
  6         15  
  6         134  
5 6     6   31 use Carp qw(croak);
  6         11  
  6         467  
6 6     6   1528 use MIME::Base64 qw(encode_base64 decode_base64);
  6         1550  
  6         755  
7 6     6   4760 use Compress::Zlib;
  6         404764  
  6         1399  
8 6     6   3264 use Digest::SHA1;
  6         4408  
  6         276  
9 6     6   2909 use Digest::HMAC_SHA1;
  6         29149  
  6         300  
10 6     6   1384 use Data::Dumper qw(Dumper);
  6         12995  
  6         333  
11 6     6   2849 use Crypt::OpenToken::KeyGenerator;
  6         19  
  6         196  
12 6     6   2611 use Crypt::OpenToken::Serializer;
  6         17  
  6         196  
13 6     6   2658 use Crypt::OpenToken::Token;
  6         22  
  6         719  
14              
15             our $VERSION = '0.08';
16             our $DEBUG = 0;
17              
18             # shared encryption password
19             has 'password' => (
20             is => 'rw',
21             isa => 'Str',
22             required => 1,
23             );
24              
25             # http://tools.ietf.org/html/draft-smith-opentoken-02
26 6         398 use constant TOKEN_PACK =>
27             'a3'. # literal 'OTK'
28             'C'. # version (unsigned-byte)
29             'C'. # cipher
30             'a20'. # hmac string (20 bytes for SHA1/SHA1_HMAC)
31             'C/a*'. # IV (with unsigned-byte length-prefix)
32             'C/a*'. # key (with unsigned-byte length-prefix)
33 6     6   62 'n/a*'; # payload (with network-endian short length-prefix)
  6         11  
34              
35             # List of ciphers supported by OpenToken
36 6     6   39 use constant CIPHER_NULL => 0;
  6         11  
  6         310  
37 6     6   35 use constant CIPHER_AES256 => 1;
  6         15  
  6         254  
38 6     6   34 use constant CIPHER_AES128 => 2;
  6         10  
  6         297  
39 6     6   48 use constant CIPHER_DES3 => 3;
  6         14  
  6         325  
40 6     6   35 use constant CIPHERS => [qw(null AES256 AES128 DES3)];
  6         12  
  6         6201  
41              
42             sub _cipher {
43 16     16   39 my ($self, $cipher) = @_;
44              
45 16         43 my $impl = CIPHERS->[$cipher];
46 16 100       66 croak "unsupported OTK cipher; '$cipher'" unless ($impl);
47              
48 15         47 my $mod = "Crypt::OpenToken::Cipher::$impl";
49 15         1106 eval "require $mod";
50 15 50       80 if ($@) {
51 0         0 croak "unable to load cipher '$impl'; $@";
52             }
53 15 50       49 print "selected cipher: $impl\n" if $DEBUG;
54 15         84 return $mod->new;
55             }
56              
57             sub parse {
58 10     10 1 22872 my ($self, $token_str) = @_;
59 10 50       39 print "parsing token: $token_str\n" if $DEBUG;
60              
61             # base64 decode the OTK
62 10         64 $token_str = $self->_base64_decode($token_str);
63              
64             # unpack the OTK token into its component fields
65 10         53 my $fields = $self->_unpack($token_str);
66 10 50       41 print "unpacked fields: " . Dumper($fields) if $DEBUG;
67              
68             # get the chosen cipher, and make sure the IV length is valid
69 10         45 my $cipher = $self->_cipher( $fields->{cipher} );
70 10         2518 my $iv_len = $fields->{iv_len};
71 10 50       40 unless ($iv_len == $cipher->iv_len) {
72 0         0 croak "invalid IV length ($iv_len) for selected cipher ($cipher)";
73             }
74              
75             # generate a decryption key for this cipher
76 10         347 my $key = Crypt::OpenToken::KeyGenerator::generate(
77             $self->password, $cipher->keysize,
78             );
79 10 50       67 print "generated key: " . encode_base64($key) if $DEBUG;
80              
81             # decrypt the payload
82 10         54 my $crypto = $cipher->cipher($key, $fields->{iv});
83 10         73 my $decrypted = $crypto->decrypt($fields->{payload});
84 10 50       681 print "decrypted payload: " . encode_base64($decrypted) if $DEBUG;
85              
86             # uncompress the payload
87 10         50 my $plaintext = Compress::Zlib::uncompress($decrypted);
88 10 50       1095 print "plaintext:\n$plaintext\n" if $DEBUG;
89              
90             # verify the HMAC
91 10         54 my $hmac = $self->_create_hmac($key, $fields, $plaintext);
92 10 50       239 unless ($hmac eq $fields->{hmac}) {
93 0         0 croak "invalid HMAC";
94             }
95              
96             # deserialize the plaintext payload
97 10         44 my %params = Crypt::OpenToken::Serializer::thaw($plaintext);
98 10 50       45 print "payload: " . Dumper(\%params) if $DEBUG;
99 10         45 $fields->{data} = \%params;
100              
101             # instantiate the token object
102 10         100 my $token = Crypt::OpenToken::Token->new($fields);
103 10         16230 return $token;
104             }
105              
106             sub create {
107 6     6 1 13614 my ($self, $cipher, $data) = @_;
108              
109             # get the chosen cipher, and generate a random IV for the encryption
110 6         32 my $cipher_obj = $self->_cipher($cipher);
111 5         994 my $iv = '';
112 5 100       22 if (my $len = $cipher_obj->iv_len) {
113 4         16 $iv = _rand_iv($len);
114             }
115              
116             # generate an encryption key for this cipher
117 5         189 my $key = Crypt::OpenToken::KeyGenerator::generate(
118             $self->password, $cipher_obj->keysize,
119             );
120 5 50       29 print "generated key: " . encode_base64($key) if $DEBUG;
121              
122             # serialize the data into a payload
123 5         12 my $plaintext = Crypt::OpenToken::Serializer::freeze(%{$data});
  5         40  
124 5 50       25 print "plaintext:\n$plaintext\n" if $DEBUG;
125              
126             # compress the payload
127 5         30 my $compressed = Compress::Zlib::compress($plaintext);
128 5 50       2065 print "compressed plaintext: " . encode_base64($compressed) if $DEBUG;
129              
130             # encrypt the token, w/PKCS5 padding
131 5         41 my $crypto = $cipher_obj->cipher($key, $iv);
132 5         37 my $padded = $self->_pkcs5_padded($compressed, $crypto->blocksize());
133 5         41 my $encrypted = $crypto->encrypt($padded);
134 5 50       423 print "encrypted payload: " . encode_base64($encrypted) if $DEBUG;
135              
136             # gather up all of the fields
137 5         22 my %fields = (
138             version => 1,
139             cipher => $cipher,
140             iv_len => bytes::length($iv),
141             iv => $iv,
142             key_len => bytes::length($key),
143             key => $key,
144             payload_len => bytes::length($encrypted),
145             payload => $encrypted,
146             );
147              
148             # create an HMAC
149 5         71 my $hmac = $self->_create_hmac($key, \%fields, $plaintext);
150 5 50       122 print "calculated hmac: " . encode_base64($hmac) if $DEBUG;
151 5         16 $fields{hmac} = $hmac;
152              
153             # pack the OTK token together from its component fields
154 5         49 my $token = $self->_pack(%fields);
155 5 50       22 print "binary token: $token\n" if $DEBUG;
156              
157             # base64 encode the token
158 5         24 my $token_str = $self->_base64_encode($token);
159 5 50       20 print "token created: $token_str\n" if $DEBUG;
160 5         48 return $token_str;
161             }
162              
163             sub _rand_iv {
164 4     4   10 my $len = shift;
165 4         9 my $iv = '';
166 6     6   56 use bytes;
  6         13  
  6         52  
167              
168             # try to use a reasonably unguessable source of random bytes.
169             # /dev/random isn't needed for IVs in general.
170 4         10 eval {
171 4 50       261 sysopen my $urand, '/dev/urandom', Fcntl::O_RDONLY() or die $!;
172 4 50       32 binmode $urand or die $!;
173 4 50       126 sysread $urand, $iv, $len or die $!;
174             };
175 4 50       30 warn __PACKAGE__."::_rand_iv can't use /dev/urandom: $@" if $@;
176              
177             # fill up with less random bytes
178 4 50       22 if (length($iv) < $len) {
179 0         0 $iv .= chr(int(rand(256))) until (length($iv) == $len);
180             }
181              
182 4         15 return $iv;
183             }
184              
185             sub _pkcs5_padded {
186 5     5   23 my ($self, $data, $bsize) = @_;
187 5 50       17 if ($bsize) {
188 5         45 my $data_len = bytes::length($data);
189 5         3957 my $pad_needed = $bsize - ($data_len % $bsize);
190 5         30 $data .= chr($pad_needed) x $pad_needed;
191             }
192 5         18 return $data;
193             }
194              
195             sub _create_hmac {
196 15     15   47 my ($self, $key, $fields, $plaintext) = @_;
197              
198             # NULL cipher uses SHA1 digest, all other ciphers use an HMAC_SHA1
199             my $digest =
200 15 100       95 ($fields->{cipher} == CIPHER_NULL)
201             ? Digest::SHA1->new()
202             : Digest::HMAC_SHA1->new($key);
203              
204 15         901 $digest->add(chr($fields->{version}));
205 15         129 $digest->add(chr($fields->{cipher}));
206 15 100       129 $digest->add($fields->{iv}) if ($fields->{iv_len} > 0);
207 15 100       114 $digest->add($fields->{key}) if ($fields->{key_len} > 0);
208 15         78 $digest->add($plaintext);
209              
210 15         108 return $digest->digest;
211             }
212              
213             sub _unpack {
214 10     10   30 my ($self, $token_str) = @_;
215 6     6   2273 use bytes;
  6         31  
  6         58  
216              
217 10         119 my ($otk, $ver, $cipher, $hmac, $iv, $key, $payload)
218             = unpack(TOKEN_PACK, $token_str);
219 10 50       55 unless ($otk eq 'OTK') {
220 0         0 croak "invalid literal identifier in OTK; '$otk'";
221             }
222 10 50       37 unless ($ver == 1) {
223 0         0 croak "unsupported OTK version; '$ver'";
224             }
225              
226             return {
227 10         117 version => $ver,
228             cipher => $cipher,
229             hmac => $hmac,
230             iv_len => length($iv),
231             iv => $iv,
232             key_len => length($key),
233             key => $key,
234             payload_len => length($payload),
235             payload => $payload,
236             };
237             }
238              
239             sub _pack {
240 5     5   34 my ($self, %fields) = @_;
241              
242             # truncate to specified lengths
243 5         18 for (qw(iv key payload)) {
244 15         49 substr($fields{$_}, $fields{ $_ . "_len" }) = '';
245             }
246              
247             my $token_str = pack(TOKEN_PACK,
248 5         53 'OTK', @fields{qw(version cipher hmac iv key payload)}
249             );
250 5         26 return $token_str;
251             }
252              
253             # Custom Base64 decoding; OTK has some oddities in how they encode things
254             # using Base64.
255             sub _base64_decode {
256 11     11   107027 my ($self, $token_str) = @_;
257              
258             # fixup: convert trailing "*"s into "="s (OTK specific encoding)
259 11         74 $token_str =~ s/(\*+)$/'=' x length($1)/e;
  10         59  
260              
261             # fixup: convert "_" to "/" (PingId PHP bindings encode this way)
262             # fixup: convert "-" to "+" (PingId PHP bindings encode this way)
263 11         43 $token_str =~ tr{_-}{/+};
264              
265             # Base64 decode it, and we're done.
266 11         62 my $decoded = decode_base64($token_str);
267 11         34 return $decoded;
268             }
269              
270             # Custom Base64 encoding; OTK has some oddities in how they encode things
271             # using Base64.
272             sub _base64_encode {
273 6     6   20 my ($self, $token_str) = @_;
274              
275             # Base64 encode the token string
276 6         37 my $encoded = encode_base64($token_str, '');
277              
278             # fixup: convert "+" to "-" (PingId PHP bindings encode this way)
279             # fixup: convert "/" to "_" (PingId PHP bindings encode this way)
280 6         19 $encoded =~ tr{/+}{_-};
281              
282             # fixup: convert trailing "="s to "*"s (OTK specific encoding)
283 6         48 $encoded =~ s/(\=+)$/'*' x length($1)/e;
  5         27  
284              
285 6         29 return $encoded;
286             }
287              
288 6     6   3031 no Moose;
  6         15  
  6         40  
289              
290             1;
291              
292             =head1 NAME
293              
294             Crypt::OpenToken - Perl implementation of Ping Identity's "OpenToken"
295              
296             =head1 SYNOPSIS
297              
298             use Crypt::OpenToken;
299              
300             $data = {
301             foo => 'bar',
302             bar => 'baz',
303             };
304              
305             # create an OpenToken factory based on a given shared password
306             $factory = Crypt::OpenToken->new($password);
307              
308             # encrypt a hash-ref of data into an OpenToken.
309             $token_str = $factory->create(
310             Crypt::OpenToken::CIPHER_AES128,
311             $data,
312             );
313              
314             # decrypt an OpenToken, check if its valid, and get data back out
315             $token = $factory->parse($token_str);
316             if ($token->is_valid) {
317             $data = $token->data();
318             }
319              
320             =head1 DESCRIPTION
321              
322             This module provides a Perl implementation of the "OpenToken" standard as
323             defined by Ping Identity in their IETF Draft.
324              
325             =head1 METHODS
326              
327             =over
328              
329             =item Crypt::OpenToken->new($password)
330              
331             Instantiates a new OpenToken factory, which can encrypt/decrypt OpenTokens
332             using the specified shared C<$password>.
333              
334             =item $factory->create($cipher, $data)
335              
336             Encrypts the given hash-ref of C<$data> using the specified C<$cipher> (which
337             should be one of the C<CIPHER_*> constants).
338              
339             Returns back to the caller a Base64 encoded string which represents the
340             OpenToken.
341              
342             B<NOTE:> during the encryption of the OpenToken, a random Initialization
343             Vector will be selected; as such it is I<not> possible to encrypt the same
344             data more than once and get the same OpenToken back.
345              
346             =item $factory->parse($token)
347              
348             Decrypts a Base64 encoded OpenToken, returning a C<Crypt::OpenToken::Token>
349             object back to the caller. Throws a fatal exception in the event of an error.
350              
351             It is the callers responsibility to then check to see if the token itself is
352             valid (see L<Crypt::OpenToken::Token> for details).
353              
354             =back
355              
356             =head1 CONSTANTS
357              
358             The following constant values are available for selecting an encrytion cipher
359             to use:
360              
361             =over
362              
363             =item Crypt::OpenToken::CIPHER_NULL
364              
365             "Null" encryption (e.g. no encryption whatsoever). Requires C<Crypt::NULL>.
366              
367             =item Crypt::OpenToken::CIPHER_AES256
368              
369             "AES" encryption, 256-bit. Requires C<Crypt::Rijndael>.
370              
371             =item Crypt::OpenToken::CIPHER_AES128
372              
373             "AES" encryption, 128-bit. Requires C<Crypt::Rijndael>.
374              
375             =item Crypt::OpenToken::CIPHER_DES3
376              
377             "TripleDES" encryption, 168-bit. Requires C<Crypt::DES>.
378              
379             =back
380              
381             =for Pod::Coverage CIPHERS TOKEN_PACK
382              
383             =head1 CAVEATS
384              
385             =over
386              
387             =item *
388              
389             This module does not (yet) support the "obfuscate password" option that is
390             configurable within PingFederate's OpenToken adapter.
391              
392             =back
393              
394             =head1 AUTHOR
395              
396             Graham TerMarsch (cpan@howlingfrog.com)
397              
398             Shawn Devlin (shawn.devlin@socialtext.com)
399              
400             =head2 Contributors
401              
402             Thanks to those who have provided feedback, comments, and patches:
403              
404             Jeremy Stashewsky
405             Travis Spencer
406              
407             =head2 Sponsors
408              
409             B<BIG> thanks also go out to those who sponsored C<Crypt::OpenToken>:
410              
411             =over
412              
413             =item Socialtext
414              
415             Thanks for sponsoring the initial development of C<Crypt::OpenToken>, and then
416             being willing to release it to the world.
417              
418             =item Ping Identity
419              
420             Thanks for your assistance during the initial development, providing feedback
421             along the way, and answering our questions as they arose.
422              
423             =back
424              
425             =head1 COPYRIGHT & LICENSE
426              
427             =head2 Crypt::OpenToken
428              
429             C<Crypt::OpenToken> is Copyright (C) 2010, Socialtext, and is released under
430             the Artistic-2.0 license.
431              
432             =head2 OpenToken specification
433              
434             The OpenToken specification is Copyright (C) 2007-2010 Ping Identity
435             Corporation, and released under the MIT License:
436              
437             =over
438              
439             Permission is hereby granted, free of charge, to any person obtaining a copy
440             of this software and associated documentation files (the "Software"), to deal
441             in the Software without restriction, including without limitation the rights
442             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
443             copies of the Software, and to permit persons to whom the Software is
444             furnished to do so, subject to the following conditions:
445              
446             The above copyright notice and this permission notice shall be included in all
447             copies or substantial portions of the Software.
448              
449             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
450             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
451             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
452             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
453             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
454             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
455             SOFTWARE.
456              
457             =back
458              
459             =head1 SEE ALSO
460              
461             L<http://tools.ietf.org/html/draft-smith-opentoken-02>
462             L<http://www.pingidentity.com/opentoken>
463              
464             =cut