Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: amiri/Digest-PBKDF2
base: master
...
head fork: LoonyPandora/Digest-PBKDF2
compare: master
Checking mergeability… Don’t worry, you can still create the pull request.
  • 11 commits
  • 8 files changed
  • 0 commit comments
  • 1 contributor
Commits on Apr 03, 2012
@LoonyPandora LoonyPandora Change digest method to return the digest alone.
First draft of changes, still need to write tests and update documentation
b7bb46d
@LoonyPandora LoonyPandora Update documentation to reflect current features 9186de9
Commits on Apr 04, 2012
@LoonyPandora LoonyPandora Clarify usage of the salt method
Also revert to using the VERSION number provided when building via Dist::Zilla
c6f3e49
@LoonyPandora LoonyPandora Add tests for new functionality 7c98e54
@LoonyPandora LoonyPandora Bump version number b0253d1
@LoonyPandora LoonyPandora Centralise error handling when no salt is specified
Slight refactor to improve creation of Crypt::PBKDF2 object. It's a shame the encoding method is readonly in that module, else this would be even more simple.
d35d2b0
@LoonyPandora LoonyPandora This is a backwards incompatible change - bump the major version number cb89329
@LoonyPandora LoonyPandora Add a bare-bones CHANGES file 1dcd293
@LoonyPandora LoonyPandora Typo fixes 00e1dd1
Commits on Jun 07, 2012
@LoonyPandora LoonyPandora Add iteration count parameter
Since we output the raw digest without the salt and iteration count, we should die if they are not specified, rather than using some unknowable default.
6946175
@LoonyPandora LoonyPandora Update tests to support new iterations parameter 087bad7
View
55 CHANGES
@@ -0,0 +1,55 @@
+1.0.0 2012-04-04
+
+ [ DOCUMENTATION ]
+ * Added example usage to all methods
+ (James Aitken)
+
+ [ FEATURE ]
+ * The digest method now matches other Digest:: namespace modules,
+ and only returns the digest.
+
+ This means specifing the encoding when creating a new Digest::PBKDF2
+ object is no longer necessary or supported.
+
+ To access the different encodings, one should use the new as_ldap
+ and as_crypt methods
+
+ This is a backwards incompatible change.
+ (James Aitken)
+
+ * Added as_ldap and as_crypt methods to mitigate the changing of the
+ digest method to only return the digest
+ (James Aitken)
+
+ * Added salt method to specify the salt directly, rather than parsing
+ it from the data that was added, simplifying the interface. You must
+ explicitly set the empty string to create a salt-less hash.
+
+ The salt method acts as a getter to read the salt as currently set
+ (James Aitken)
+
+ [ BUGFIX ]
+ * hexdigest and b64digest now correctly return the encoded version of the digest
+ (James Aitken)
+
+ [ TESTS ]
+ * Updated tests and split into multiple test files for readability
+ (James Aitken)
+
+
+0.009 2011-08-05
+
+0.008 2011-08-03
+
+0.007 2011-08-03
+
+0.006 2011-07-25
+
+0.005 2011-02-18
+
+0.004 2011-02-18
+
+0.003 2011-02-15
+
+0.001 2011-02-11
+
View
2  dist.ini
@@ -4,7 +4,7 @@ license = Perl_5
copyright_holder = Amiri Barksdale
copyright_year = 2011
-version = 0.009
+version = 1.0.0
[@Basic]
[InstallGuide]
View
267 lib/Digest/PBKDF2.pm
@@ -1,65 +1,143 @@
package Digest::PBKDF2;
+# ABSTRACT: A minimalist Digest module using the PBKDF2 algorithm.
+
use strict;
-use warnings;
+
use parent "Digest::base";
+use Carp qw(croak);
use Crypt::PBKDF2 0.112020;
-BEGIN {
- # VERSION
-}
-
-#ABSTRACT: This module is a subclass of Digest using the Crypt::PBKDF2 algorithm.
+# VERSION
sub new {
- my ( $class, %params ) = @_;
- my $encoding = $params{encoding} || 'crypt';
- return bless { _entries => [], _data => undef, encoding => $encoding }, $class;
+ my $class = shift;
+
+ return bless {
+ _data => '',
+ _options => {},
+ }, ref($class) || $class;
}
-sub clone {
- my $self = shift;
- my $clone = {
- _data => $self->{_data},
- _entries => $self->{_entries},
- encoding => $self->{encoding},
- };
- return bless $clone, ref $self;
+
+sub as_crypt {
+ my $self = shift;
+
+ $self->{_options }->{encoding} = 'crypt';
+
+ return $self->_digest_with_encoding;
}
-sub add {
+
+sub as_ldap {
my $self = shift;
- if (@_) {
- push @{ $self->{_entries} }, join '', @_;
- $self->{_data} .= join '', @_;
+
+ $self->{_options }->{encoding} = 'ldap';
+
+ return $self->_digest_with_encoding;
+}
+
+
+sub iterations {
+ my ($self, $iterations) = @_;
+
+ # Though PBKDF2 does not enforce a min / max iteration count,
+ # the recommended minimum is 1000, and 99,999,999 is a practical max
+ # 99,999,999 takes ~15 minutes to generate a single hash on modern hardware
+ if (defined $iterations && $iterations =~ /^\d{4,8}$/) {
+ $self->{_options }->{iterations} = $iterations;
+ return $self;
+ }
+
+ return $self->{_options }->{iterations};
+}
+
+
+sub salt {
+ my ($self, $salt) = @_;
+
+ # Salt set to the empty string is valid, though strongly discouraged.
+ # It is only accepted for backwards compatibility.
+ if (defined $salt) {
+ $self->{salt} = $salt;
+ return $self;
}
- $self;
+
+ return $self->{salt};
}
+
+sub clone {
+ my $self = shift;
+
+ return bless {
+ salt => $self->salt,
+ _options => $self->{_options},
+ _data => $self->{_data},
+ }, ref($self);
+}
+
+
sub reset {
my $self = shift;
+
+ delete $self->{salt};
+ delete $self->{_options};
delete $self->{_data};
- delete $self->{_entries};
- delete $self->{encoding};
- $self;
+
+ return $self->new;
}
+
+sub add {
+ my $self = shift;
+
+ $self->{_data} .= join('', @_);
+
+ return $self;
+}
+
+
sub digest {
my $self = shift;
- my @string = split '', $self->{_data};
- my $salt;
+ my $hash = $self->_crypt->PBKDF2($self->salt, $self->{_data});
+
+ $self->reset;
+
+ return $hash;
+}
+
- $salt = join( '', splice( @string, 0, length( $self->{_entries}->[0] ) ) )
- if @{ $self->{_entries} } > 1;
- my $data = join( '', @string );
+# Returns the digest, salt, and algorithm as a crypt or ldap string
+sub _digest_with_encoding {
+ my $self = shift;
+
+ my $hash = $self->_crypt->generate($self->{_data}, $self->salt);
- my $crypt = Crypt::PBKDF2->new( encoding => $self->{encoding}, salt_len => length($salt||'') );
- my $return = $crypt->generate( $data, $salt );
$self->reset;
- $return;
+
+ return $hash;
}
+
+# Returns a Crypt::PBKDF2 object, with the encoding set as required
+sub _crypt {
+ my $self = shift;
+
+ if (!defined $self->salt) {
+ croak "No salt specified. The empty string must be set to use a blank salt";
+ }
+
+ if (!defined $self->iterations) {
+ croak "Invalid iteration count. An Iteration count in the range 1,000 - 99,999,999 is required";
+ }
+
+ return Crypt::PBKDF2->new(%{$self->{_options}});
+}
+
+
+
1;
__END__
@@ -67,26 +145,33 @@ __END__
=head1 NAME
Digest::PBKDF2
+
A minimalist Digest module using the PBKDF2 algorithm.
-=head1 NOTICE
+=head1 SYNOPSIS
-You can only use one salt, a pre-salt, with this module. It is not smart enough
-to do post-salts.
+ # via the Digest module (recommended)
+ use Digest;
-=head1 SYNOPSIS
+ my $pbkdf2 = Digest->new('PBKDF2');
+
+ # $salt is essential, and should be cryptographically random
+ $pbkdf2->salt($salt);
+
+ $pbkdf2->add('password'); # password = 'password'
+ $pbkdf2->add('extension'); # password = 'passwordextension'
- my $digest = Digest::PBKDF2->new; # Or...
- my $digest = Digest::PBKDF2->new(encoding => 'ldap');
- $digest->add('mysalt'); # salt = 'mysalt'
- $digest->add('k3wLP@$$w0rd'); # password = 'k3wLP@$$w0rd'
+ $digest = $pbkdf2->digest; # Binary version, 20 bytes
+ $digest = $pbkdf2->hexdigest; # Hex-encoded, 40 bytes
+ $digest = $pbkdf2->b64digest; # base64 encoded with no padding. 27 bytes
- $digest->add('eX+ens10n'); # password = 'k3wLP@$$w0rdeX+ens10n'
+ # [...]
- my $result = $digest->digest; # $PBKDF2$HMACSHA1:1000:bXlzYWx0$4P9pwp
- # LoF+eq5jwUbMw05qRQyZs=
+ # Using the module directly (same interface as above)
-That's about it.
+ use Digest::PBKDF2;
+
+ my $pbkdf2 = Digest::PBKDF2->new();
=head1 METHODS
@@ -94,28 +179,100 @@ That's about it.
=item new
-Create a new Digest::PBKDF2 object. This defaults to using the "crypt" encoding
-available in Crypt::PBKDF2--please see L<Crypt::PBKDF2> for details.
+ my $pbkdf2 = Digest->new('PBKDF2');
+
+Creates a new C<Digest::PBKDF2> object.
+
+You can also use this module directly
+
+ my $pbkdf2 = Digest::PBKDF2->new();
=item clone
-Copies the data and state from the original Digest::PBKDF2 object,
+ my $pbkdf2->clone;
+
+Copies the data and state from the original C<Digest::PBKDF2> object,
and returns a new object.
=item add
-Pass this method your salt and data chunks. They are stored up
-until you call digest.
+ $pbkdf2->add("a"); $pbkdf2->add("b"); $pbkdf2->add("c");
+ $pbkdf2->add("a")->add("b")->add("c");
+ $pbkdf2->add("a", "b", "c");
+ $pbkdf2->add("abc");
+
+Adds data to the message we are calculating the digest for.
+
+All the above examples have the same effect
+
+=item salt
+
+ $pbkdf2->salt($salt);
+
+Sets the value to be used as a salt. You must specify a salt.
+
+It is recommenced that you use a module like L<Data::Entropy::Algorithms> to
+provide a truly randomised salt.
+
+When called with no arguments, will return the whatever is the current salt
=item digest
-This encrypts your data and returns the encrypted string.
+ $pbkdf2->digest;
+
+Return the binary digest for the message.
+
+The returned string will be 20 bytes long.
+
+=item hexdigest
+
+ $pbkdf2->hexdigest;
+
+Same as L</"digest">, but will return the digest in hexadecimal form.
+
+The C<length> of the returned string will be 40 and will only contain
+characters from the ranges C<'0'..'9'> and C<'a'..'f'>.
+
+=item b64digest
+
+ $pbkdf2->b64digest;
+
+Same as L</"digest">, but will return the digest base64 encoded.
+
+The C<length> of the returned string will be 27 and will only contain characters
+from the ranges C<'0'..'9'>, C<'A'..'Z'>, C<'a'..'z'>, C<'+'>, and C<'.'>
+
+The base64 encoded string returned is not padded to be a multiple of 4 bytes long.
=item reset
-After calling digest, the module calls reset on its self,
-clearing data and the record of how many additions were made to the data
-to be digested.
+ $pbkdf2->reset;
+
+Resets the object to the same internal state it was in when it was constructed.
+
+=item as_crypt
+
+ $pbkdf2->as_crypt;
+
+Returns the digest, salt, and algorithm as a string that is similar to that used
+by the C<crypt()> function. Example:
+
+ $PBKDF2$HMACSHA1:1000:4q9OTg==$9Pb6bCRgnct/dga+4v4Lyv8x31s=
+
+The output of this method is the same as the outpout from the
+C<generate> function of L<Crypt::PBKDF2> when using the C<crypt> encoding method
+
+=item as_ldap
+
+ $pbkdf2->as_ldap;
+
+Returns the digest, salt, and algorithm as a string that is intended to be
+compatible with RFC 2307. Example:
+
+ {X-PBKDF2}HMACSHA1:AAAD6A:8ODUPA==:1HSdSVVwlWSZhbPGO7GIZ4iUbrk=
+
+The output of this method is the same as the outpout from the
+C<generate> function of L<Crypt::PBKDF2> when using the C<crypt> encoding method
=back
@@ -128,6 +285,8 @@ L<Digest>
Amiri Barksdale, E<lt>abarksdale@campusexplorer.comE<gt>
+James Aitken, E<lt>jaitken@cpan.orgE<gt>
+
=head1 COPYRIGHT
Copyright (c) 2011 by Campus Explorer, Inc.
View
98 t/01_basic.t 100644 → 100755
@@ -1,101 +1,7 @@
-#!/usr/bin/env perl
+use Test::More tests => 1;
use strict;
use warnings;
-use Test::More qw/no_plan/;
-use Test::Exception;
-use Scalar::Util qw/refaddr/;
-use lib "lib";
-use_ok "Digest::PBKDF2";
-
-### Base class testing
-
-my $rawsalted = Crypt::PBKDF2->new;
-my $rawunsalted = Crypt::PBKDF2->new;
-
-is( $rawsalted->salt_len, 4, "By default my salt_len is 4" );
-throws_ok { $rawsalted->salt_len(5) } qr/read-only accessor/,
- "I cannot change my salt_len";
-
-my $salted = $rawsalted->generate('bebop');
-my $unsalted = $rawunsalted->generate( 'bebop', salt => undef );
-
-isnt( $salted, $unsalted, "The unsalted does not equal the salted" );
-
-ok( $rawsalted->validate( $salted, 'bebop' ),
- "I can validate my salted password without specifying the salt" );
-ok( $rawunsalted->validate( $unsalted, 'bebop' ),
- "I can validate my unsalted password"
-);
-
-### My module testing
-
-my $orig = Digest::PBKDF2->new;
-can_ok( $orig, qw/new clone add digest/ );
-
-diag "I will try a password_pre_salt first";
-lives_ok( sub { $orig->add('cool') }, "I can add one chunk" );
-lives_ok( sub { $orig->add('jazz') }, "I can add another chunk" );
-my $clone;
-lives_ok( sub { $clone = $orig->clone }, "I can clone my object" );
-isnt(
- refaddr $orig,
- refaddr $clone,
- "Cloning gives me a new Digest::PBKDF2 object"
-);
-isnt(
- refaddr \$orig->{_data},
- refaddr \$clone->{_data},
- "Cloning gives me a new data slot"
-);
-lives_ok( sub { delete $clone->{_data} },
- "I can delete the data in my clone" );
-is( $clone->{_data}, undef, "And the data is gone" );
-is( $orig->{_data}, 'cooljazz', "And the original remains intact" );
-lives_ok( sub { $clone->add('cooljazz') }, "I can put back the clone data" );
-
-###
-my ( $clone_digest, $clone2_digest, $orig_digest, $orig2_digest );
-
-( $clone_digest, $orig_digest ) = ( $clone->digest, $orig->digest );
-
-is( $clone_digest, $orig_digest,
- "Clone and orginal produce the same string" );
-
-is( $clone_digest,
- '$PBKDF2$HMACSHA1:1000:Y29vbA==$6LZU9raZ5BzrMRo0mwa8Z7ON+Mc=',
- "And that string is what it should be"
-);
-is( $orig_digest,
- '$PBKDF2$HMACSHA1:1000:Y29vbA==$6LZU9raZ5BzrMRo0mwa8Z7ON+Mc=',
- "Making sure it is..."
-);
-
-### No salt
-
-diag "I will try no salt this time";
-
-my $orig2 = Digest::PBKDF2->new;
-
-lives_ok( sub { $orig2->add('jazz') }, "I can add the password chunk" );
-
-my $clone2;
-lives_ok( sub { $clone2 = $orig2->clone }, "I can clone my object" );
-isnt(
- refaddr $orig2,
- refaddr $clone2,
- "Cloning gives me a new Digest::PBKDF2 object"
-);
-( $clone2_digest, $orig2_digest ) = ( $clone2->digest, $orig2->digest );
-is( $clone2_digest, $orig2_digest,
- "Clone and orginal produce the same string" );
-is( $clone2_digest,
- '$PBKDF2$HMACSHA1:1000:$zpYCcE4kGAQD37LhEQa56B7/kCc=',
- "And that string is what it should be"
-);
-is( $orig2_digest,
- '$PBKDF2$HMACSHA1:1000:$zpYCcE4kGAQD37LhEQa56B7/kCc=',
- "Making sure it is..."
-);
+use_ok "Digest::PBKDF2";
View
14 t/02_methods.t
@@ -0,0 +1,14 @@
+use Test::More tests => 2;
+
+use strict;
+use warnings;
+
+use Digest;
+use Digest::PBKDF2;
+
+
+my $direct = Digest::PBKDF2->new;
+can_ok($direct, qw/new clone add digest hexdigest b64digest salt iterations as_crypt as_ldap reset/);
+
+my $indirect = Digest->new('PBKDF2');
+can_ok($indirect, qw/new clone add digest hexdigest b64digest salt iterations as_crypt as_ldap reset/);
View
52 t/03_cloning.t
@@ -0,0 +1,52 @@
+use Test::More tests => 13;
+
+use strict;
+use warnings;
+
+use Digest::PBKDF2;
+use Scalar::Util qw(refaddr);
+use Test::Exception;
+
+
+my $orig = Digest::PBKDF2->new;
+
+lives_ok( sub { $orig->add('cool') }, "I can add one chunk" );
+lives_ok( sub { $orig->add('jazz') }, "I can add another chunk" );
+lives_ok( sub { $orig->salt('salt') }, "I can add salt" );
+lives_ok( sub { $orig->iterations(1000) }, "I can add an iteration count" );
+
+my $clone;
+
+lives_ok( sub { $clone = $orig->clone }, "I can clone my object" );
+
+isnt(
+ refaddr $orig,
+ refaddr $clone,
+ "Cloning gives me a new Digest::PBKDF2 object"
+);
+
+isnt(
+ refaddr \$orig->{_data},
+ refaddr \$clone->{_data},
+ "Cloning gives me a new data slot"
+);
+
+lives_ok(sub { delete $clone->{_data} }, "I can delete the data in my clone");
+
+is($clone->{_data}, undef, "And the data is gone");
+
+is($orig->{_data}, 'cooljazz', "And the original remains intact");
+
+lives_ok(sub { $clone->add('cooljazz') }, "I can put back the clone data");
+
+
+my ($clone_digest, $orig_digest) = ($clone->hexdigest, $orig->hexdigest);
+
+is($clone_digest, $orig_digest, "Clone and orginal produce the same string");
+
+is(
+ $orig_digest,
+ 'ec97d051d529a3d016e17d7a71a69c1124e37f89',
+ "And that string is what it should be"
+);
+
View
52 t/04_create digests.t
@@ -0,0 +1,52 @@
+use Test::More tests => 6;
+
+use strict;
+use warnings;
+
+use Digest::PBKDF2;
+
+# $ctx is reset after each digest, hence why we re-add the salt / data
+my $ctx = Digest::PBKDF2->new;
+
+$ctx->add('cool');
+$ctx->salt('salt');
+$ctx->iterations(1000);
+
+ok($ctx->digest, "Creates Binary Digest");
+
+
+$ctx->add('cool');
+$ctx->salt('');
+$ctx->iterations(1000);
+
+ok($ctx->hexdigest eq '7d564b4dd7566702bbea294abc256b7ecff7dc69', "Creates Correct Digest With Empty Salt");
+
+
+$ctx->add('cool');
+$ctx->salt('salt');
+$ctx->iterations(1000);
+
+ok($ctx->hexdigest eq '889a3bf0c83f691ed3dd09be4dca141a561fbb90', "Creates Correct Hex Digest");
+
+
+$ctx->add('cool');
+$ctx->salt('salt');
+$ctx->iterations(1000);
+
+ok($ctx->b64digest eq 'iJo78Mg/aR7T3Qm+TcoUGlYfu5A', "Creates Correct Base 64 Digest");
+
+
+$ctx->add('cool');
+$ctx->salt('salt');
+$ctx->iterations(1000);
+
+ok($ctx->as_crypt eq '$PBKDF2$HMACSHA1:1000:c2FsdA==$iJo78Mg/aR7T3Qm+TcoUGlYfu5A=', "Creates Correct Crypt Digest");
+
+
+$ctx->add('cool');
+$ctx->salt('salt');
+$ctx->iterations(1000);
+
+ok($ctx->as_ldap eq '{X-PBKDF2}HMACSHA1:AAAD6A:c2FsdA==:iJo78Mg/aR7T3Qm+TcoUGlYfu5A=', "Creates Correct LDAP / RFC 2307 Digest");
+
+
View
35 t/05_error_handling.t
@@ -0,0 +1,35 @@
+use Test::More tests => 6;
+
+use strict;
+use warnings;
+
+use Digest::PBKDF2;
+use Test::Exception;
+
+
+my $ctx = Digest::PBKDF2->new;
+
+$ctx->add('passphrase');
+
+throws_ok(sub { $ctx->digest }, qr/No salt specified/, 'Dies when salt not specified');
+
+lives_ok(sub { $ctx->salt('foo') }, 'Can set salt');
+lives_ok(sub { $ctx->salt('bar') }, 'Can re-set the salt to another value');
+
+is($ctx->salt, 'bar', 'Salt method replaces data');
+
+
+my $ctx2 = Digest::PBKDF2->new;
+$ctx2->add('passphrase');
+$ctx2->salt('salt');
+
+throws_ok(sub { $ctx2->digest }, qr/Invalid iteration count/, 'Dies when iteration count not specified');
+
+throws_ok(
+ sub {
+ $ctx2->iterations(999);
+ $ctx2->digest;
+ },
+ qr/Invalid iteration count/,
+ 'Dies when iteration count out of range'
+);

No commit comments for this range

Something went wrong with that request. Please try again.