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   1576903 use Moose;
  6         2927903  
  6         50  
4 6     6   45539 use Fcntl qw();
  6         15  
  6         149  
5 6     6   38 use Carp qw(croak);
  6         14  
  6         514  
6 6     6   1480 use MIME::Base64 qw(encode_base64 decode_base64);
  6         1641  
  6         394  
7 6     6   5240 use Compress::Zlib;
  6         412524  
  6         1423  
8 6     6   3701 use Digest::SHA1;
  6         4701  
  6         375  
9 6     6   3083 use Digest::HMAC_SHA1;
  6         32758  
  6         323  
10 6     6   4232 use Data::Dumper qw(Dumper);
  6         40591  
  6         613  
11 6     6   3373 use Crypt::OpenToken::KeyGenerator;
  6         27  
  6         221  
12 6     6   3007 use Crypt::OpenToken::Serializer;
  6         21  
  6         227  
13 6     6   2868 use Crypt::OpenToken::Token;
  6         23  
  6         300  
14 6     6   60 use namespace::autoclean;
  6         14  
  6         48  
15              
16             our $VERSION = '0.10';
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         461 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   858 'n/a*'; # payload (with network-endian short length-prefix)
  6         14  
35              
36             # List of ciphers supported by OpenToken
37 6     6   39 use constant CIPHER_NULL => 0;
  6         12  
  6         268  
38 6     6   56 use constant CIPHER_AES256 => 1;
  6         18  
  6         259  
39 6     6   33 use constant CIPHER_AES128 => 2;
  6         13  
  6         294  
40 6     6   38 use constant CIPHER_DES3 => 3;
  6         15  
  6         334  
41 6     6   42 use constant CIPHERS => [qw(null AES256 AES128 DES3)];
  6         13  
  6         6238  
42              
43             sub _cipher {
44 16     16   47 my ($self, $cipher) = @_;
45              
46 16         43 my $impl = CIPHERS->[$cipher];
47 16 100       265 croak "unsupported OTK cipher; '$cipher'" unless ($impl);
48              
49 15         39 my $mod = "Crypt::OpenToken::Cipher::$impl";
50 15         1061 eval "require $mod";
51 15 50       94 if ($@) {
52 0         0 croak "unable to load cipher '$impl'; $@";
53             }
54 15 50       56 print "selected cipher: $impl\n" if $DEBUG;
55 15         82 return $mod->new;
56             }
57              
58             sub parse {
59 10     10 1 13073 my ($self, $token_str) = @_;
60 10 50       46 print "parsing token: $token_str\n" if $DEBUG;
61              
62             # base64 decode the OTK
63 10         49 $token_str = $self->_base64_decode($token_str);
64              
65             # unpack the OTK token into its component fields
66 10         43 my $fields = $self->_unpack($token_str);
67 10 50       37 print "unpacked fields: " . Dumper($fields) if $DEBUG;
68              
69             # get the chosen cipher, and make sure the IV length is valid
70 10         41 my $cipher = $self->_cipher( $fields->{cipher} );
71 10         2655 my $iv_len = $fields->{iv_len};
72 10 50       47 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         344 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         50 my $crypto = $cipher->cipher($key, $fields->{iv});
84 10         84 my $decrypted = $crypto->decrypt($fields->{payload});
85 10 50       2394 print "decrypted payload: " . encode_base64($decrypted) if $DEBUG;
86              
87             # uncompress the payload
88 10         64 my $plaintext = Compress::Zlib::uncompress($decrypted);
89 10 50       1171 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       238 unless ($hmac eq $fields->{hmac}) {
94 0         0 croak "invalid HMAC";
95             }
96              
97             # deserialize the plaintext payload
98 10         53 my %params = Crypt::OpenToken::Serializer::thaw($plaintext);
99 10 50       39 print "payload: " . Dumper(\%params) if $DEBUG;
100 10         31 $fields->{data} = \%params;
101              
102             # instantiate the token object
103 10         116 my $token = Crypt::OpenToken::Token->new($fields);
104 10         16605 return $token;
105             }
106              
107             sub create {
108 6     6 1 29790 my ($self, $cipher, $data) = @_;
109              
110             # get the chosen cipher, and generate a random IV for the encryption
111 6         29 my $cipher_obj = $self->_cipher($cipher);
112 5         1062 my $iv = '';
113 5 100       29 if (my $len = $cipher_obj->iv_len) {
114 4         17 $iv = _rand_iv($len);
115             }
116              
117             # generate an encryption key for this cipher
118 5         194 my $key = Crypt::OpenToken::KeyGenerator::generate(
119             $self->password, $cipher_obj->keysize,
120             );
121 5 50       25 print "generated key: " . encode_base64($key) if $DEBUG;
122              
123             # serialize the data into a payload
124 5         16 my $plaintext = Crypt::OpenToken::Serializer::freeze(%{$data});
  5         39  
125 5 50       26 print "plaintext:\n$plaintext\n" if $DEBUG;
126              
127             # compress the payload
128 5         36 my $compressed = Compress::Zlib::compress($plaintext);
129 5 50       2200 print "compressed plaintext: " . encode_base64($compressed) if $DEBUG;
130              
131             # encrypt the token, w/PKCS5 padding
132 5         38 my $crypto = $cipher_obj->cipher($key, $iv);
133 5         46 my $padded = $self->_pkcs5_padded($compressed, $crypto->blocksize());
134 5         43 my $encrypted = $crypto->encrypt($padded);
135 5 50       1249 print "encrypted payload: " . encode_base64($encrypted) if $DEBUG;
136              
137             # gather up all of the fields
138 5         23 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         82 my $hmac = $self->_create_hmac($key, \%fields, $plaintext);
151 5 50       139 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         35 my $token = $self->_pack(%fields);
156 5 50       22 print "binary token: $token\n" if $DEBUG;
157              
158             # base64 encode the token
159 5         31 my $token_str = $self->_base64_encode($token);
160 5 50       18 print "token created: $token_str\n" if $DEBUG;
161 5         52 return $token_str;
162             }
163              
164             sub _rand_iv {
165 4     4   9 my $len = shift;
166 4         11 my $iv = '';
167 6     6   83 use bytes;
  6         13  
  6         50  
168              
169             # try to use a reasonably unguessable source of random bytes.
170             # /dev/random isn't needed for IVs in general.
171 4         9 eval {
172 4 50       235 sysopen my $urand, '/dev/urandom', Fcntl::O_RDONLY() or die $!;
173 4 50       29 binmode $urand or die $!;
174 4 50       113 sysread $urand, $iv, $len or die $!;
175             };
176 4 50       40 warn __PACKAGE__."::_rand_iv can't use /dev/urandom: $@" if $@;
177              
178             # fill up with less random bytes
179 4 50       17 if (length($iv) < $len) {
180 0         0 $iv .= chr(int(rand(256))) until (length($iv) == $len);
181             }
182              
183 4         17 return $iv;
184             }
185              
186             sub _pkcs5_padded {
187 5     5   22 my ($self, $data, $bsize) = @_;
188 5 50       21 if ($bsize) {
189 5         45 my $data_len = bytes::length($data);
190 5         4040 my $pad_needed = $bsize - ($data_len % $bsize);
191 5         32 $data .= chr($pad_needed) x $pad_needed;
192             }
193 5         16 return $data;
194             }
195              
196             sub _create_hmac {
197 15     15   50 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       136 ($fields->{cipher} == CIPHER_NULL)
202             ? Digest::SHA1->new()
203             : Digest::HMAC_SHA1->new($key);
204              
205 15         503 $digest->add(chr($fields->{version}));
206 15         110 $digest->add(chr($fields->{cipher}));
207 15 100       118 $digest->add($fields->{iv}) if ($fields->{iv_len} > 0);
208 15 100       99 $digest->add($fields->{key}) if ($fields->{key_len} > 0);
209 15         72 $digest->add($plaintext);
210              
211 15         106 return $digest->digest;
212             }
213              
214             sub _unpack {
215 10     10   32 my ($self, $token_str) = @_;
216 6     6   2335 use bytes;
  6         23  
  6         35  
217              
218 10         110 my ($otk, $ver, $cipher, $hmac, $iv, $key, $payload)
219             = unpack(TOKEN_PACK, $token_str);
220 10 50       47 unless ($otk eq 'OTK') {
221 0         0 croak "invalid literal identifier in OTK; '$otk'";
222             }
223 10 50       142 unless ($ver == 1) {
224 0         0 croak "unsupported OTK version; '$ver'";
225             }
226              
227             return {
228 10         95 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   35 my ($self, %fields) = @_;
242              
243             # truncate to specified lengths
244 5         22 for (qw(iv key payload)) {
245 15         53 substr($fields{$_}, $fields{ $_ . "_len" }) = '';
246             }
247              
248             my $token_str = pack(TOKEN_PACK,
249 5         59 'OTK', @fields{qw(version cipher hmac iv key payload)}
250             );
251 5         21 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   104481 my ($self, $token_str) = @_;
258              
259             # fixup: convert trailing "*"s into "="s (OTK specific encoding)
260 11         79 $token_str =~ s/(\*+)$/'=' x length($1)/e;
  10         62  
261              
262             # fixup: convert "_" to "/" (PingId PHP bindings encode this way)
263             # fixup: convert "-" to "+" (PingId PHP bindings encode this way)
264 11         41 $token_str =~ tr{_-}{/+};
265              
266             # Base64 decode it, and we're done.
267 11         56 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   23 my ($self, $token_str) = @_;
275              
276             # Base64 encode the token string
277 6         36 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         25 $encoded =~ tr{/+}{_-};
282              
283             # fixup: convert trailing "="s to "*"s (OTK specific encoding)
284 6         45 $encoded =~ s/(\=+)$/'*' x length($1)/e;
  5         31  
285              
286 6         22 return $encoded;
287             }
288              
289 6     6   3215 no Moose;
  6         18  
  6         52  
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