File Coverage

blib/lib/Crypt/OpenToken.pm
Criterion Covered Total %
statement 163 169 96.4
branch 35 60 58.3
condition n/a
subroutine 31 31 100.0
pod 2 2 100.0
total 231 262 88.1


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