Subject: | [PATCH] Adding a feature which implements "hash based key stretching" |
Hello,
the attached patch tries to add "hash based key stretching" (*1)
optional feature to Authen::Passphrase::SaltedDigest and to change
t/smd5.t and t/ssha.t for verify the feature.
The feature was mentioned in some standard books (*2) and implemented
in several software such as crypt (*3) and WAF (web application
framework) (*4). Users of the nice module will can opt for a stronger
way to hash a salted passphrase.
Feel free to adjust accordingly for the next release if you like it.
[A supplementary explanation]
Sorry to be speculation, perhaps you will think that the "optional"
feature should be implemented on the side of an WAF such as
Catalyst::Authentication::Credential::Password. However, in my opinion,
the it is not the best way.
Let me humbly say, the recent trend is not to depend on WAF for
model-associated procedures but to handle it by home-brew POPO (plain
old perl object). In doing so, we will gain more maintainability and
more testability because it POPO model will can be used in both WAF and
others such as CLI (command-line interface) and test scripts.
[Footnotes]
*1) http://en.wikipedia.org/wiki/Key_stretching#Hash_based_key_stretching
*2) "Cryptography Engineering"
by Niels Ferguson, Bruce Schneier and Tadayoshi Kohno
(ISBN-13: 978-0470474242)
*3) man 3 crypt
http://www.kernel.org/doc/man-pages/online/pages/man3/crypt.3.html
*4) "restful_authentication", the famous plug-in for Ruby on Rails
https://github.com/technoweenie/restful-authentication
I appreciate your consideration.
Regards,
--
MORIYA Masaki, alias Gardejo
Subject: | add-stretching.patch |
diff -crN original\lib\Authen\Passphrase\SaltedDigest.pm patched\lib\Authen\Passphrase\SaltedDigest.pm
*** original\lib\Authen\Passphrase\SaltedDigest.pm Sat Jul 31 03:41:06 2010
--- patched\lib\Authen\Passphrase\SaltedDigest.pm Tue May 10 01:18:44 2011
***************
*** 151,156 ****
--- 151,161 ----
A passphrase that will be accepted.
+ =item B<stretch_count>
+
+ A stretching frequency, as a positive integer. Defaults to the empty
+ integer, yielding a scheme that will be stretched one time.
+
=back
The digest algorithm must be given, and either the hash or the passphrase.
***************
*** 205,216 ****
--- 210,226 ----
if exists($self->{hash}) ||
defined($passphrase);
$passphrase = $value;
+ } elsif($attr eq "stretch_count") {
+ croak "\"$value\" is not a valid stretch count"
+ unless $value == int($value) && $value > 0;
+ $self->{stretch_count} = $value;
} else {
croak "unrecognised attribute `$attr'";
}
}
croak "algorithm not specified" unless exists $self->{algorithm};
$self->{salt} = "" unless exists $self->{salt};
+ $self->{stretch_count} = 1 unless exists $self->{stretch_count};
if(defined $passphrase) {
$self->{hash} = $self->_hash_of($passphrase);
} elsif(exists $self->{hash}) {
***************
*** 248,254 ****
);
sub from_rfc2307 {
! my($class, $userpassword) = @_;
return $class->SUPER::from_rfc2307($userpassword)
unless $userpassword =~ /\A\{([-0-9A-Za-z]+)\}/;
my $scheme = uc($1);
--- 258,264 ----
);
sub from_rfc2307 {
! my($class, $userpassword, $stretch_count) = @_;
return $class->SUPER::from_rfc2307($userpassword)
unless $userpassword =~ /\A\{([-0-9A-Za-z]+)\}/;
my $scheme = uc($1);
***************
*** 267,273 ****
--- 277,290 ----
if length($hash_and_salt) < $hash_len;
croak "too much hash data for {$scheme}"
if !$salt_allowed && length($hash_and_salt) > $hash_len;
+ if (defined $stretch_count) {
+ croak "\"$stretch_count\" is not a valid stretch count"
+ unless $stretch_count == int($stretch_count) && $stretch_count > 0;
+ } else {
+ $stretch_count = 1;
+ }
return $class->new(algorithm => $algorithm,
+ stretch_count => $stretch_count,
salt => substr($hash_and_salt, $hash_len),
hash => substr($hash_and_salt, 0, $hash_len));
}
***************
*** 373,381 ****
} else {
croak "algorithm specifier `$alg' is of an unrecognised type";
}
! $ctx->add($passphrase);
! $ctx->add($self->{salt});
! return $ctx->digest;
}
sub match {
--- 390,403 ----
} else {
croak "algorithm specifier `$alg' is of an unrecognised type";
}
! my $digest = "";
! foreach my $count (1 .. $self->{stretch_count}) {
! $ctx->add($digest);
! $ctx->add($passphrase);
! $ctx->add($self->{salt});
! $digest = $ctx->digest;
! }
! return $digest;
}
sub match {
diff -crN original\t\smd5.t patched\t\smd5.t
*** original\t\smd5.t Sat Jul 31 03:41:06 2010
--- patched\t\smd5.t Tue May 10 01:10:52 2011
***************
*** 1,7 ****
use warnings;
use strict;
! use Test::More tests => 81;
use MIME::Base64 2.21 qw(encode_base64);
--- 1,7 ----
use warnings;
use strict;
! use Test::More tests => 86;
use MIME::Base64 2.21 qw(encode_base64);
***************
*** 25,38 ****
is $ppr->hash_hex, "04bd9d36c5c5497984a2670ed2442f9d";
like $ppr->as_rfc2307, qr/\A\{SMD5\}/;
! $ppr = Authen::Passphrase::SaltedDigest
! ->new(algorithm => "MD5", salt_random => 13,
! passphrase => "wibble");
! ok $ppr;
! is length($ppr->salt), 13;
! is length($ppr->hash), 16;
! like $ppr->as_rfc2307, qr/\A\{SMD5\}/;
! ok $ppr->match("wibble");
my %pprs;
my $i = 0;
--- 25,40 ----
is $ppr->hash_hex, "04bd9d36c5c5497984a2670ed2442f9d";
like $ppr->as_rfc2307, qr/\A\{SMD5\}/;
! foreach my $stretch_count (1, 10) {
! $ppr = Authen::Passphrase::SaltedDigest
! ->new(algorithm => "MD5", salt_random => 13,
! passphrase => "wibble", stretch_count => $stretch_count);
! ok $ppr;
! is length($ppr->salt), 13;
! is length($ppr->hash), 16;
! like $ppr->as_rfc2307, qr/\A\{SMD5\}/;
! ok $ppr->match("wibble");
! }
my %pprs;
my $i = 0;
diff -crN original\t\ssha.t patched\t\ssha.t
*** original\t\ssha.t Sat Jul 31 03:41:06 2010
--- patched\t\ssha.t Tue May 10 01:28:16 2011
***************
*** 1,7 ****
use warnings;
use strict;
! use Test::More tests => 70;
use MIME::Base64 2.21 qw(encode_base64);
--- 1,7 ----
use warnings;
use strict;
! use Test::More tests => 80;
use MIME::Base64 2.21 qw(encode_base64);
***************
*** 13,18 ****
--- 13,41 ----
is $ppr->algorithm, "SHA-1";
is $ppr->salt_hex, "f5895a0c";
is $ppr->hash_hex, "4aedd0ba6148469d7115f459e3d8706899a67bea";
+
+ my $salt_hex = "112233";
+ my $hash_hex = "893a490e37b4795e29f7f93f15c9b8d30b25b506";
+ my $rightphrase = "passphrase";
+ $ppr = Authen::Passphrase::SaltedDigest
+ ->from_rfc2307("{SSHA}iTpJDje0eV4p9/k/Fcm40wsltQYRIjM=", 10);
+ ok $ppr;
+ is $ppr->salt_hex, $salt_hex;
+ is $ppr->hash_hex, $hash_hex;
+ ok $ppr->match($rightphrase);
+
+ my @options = ({hash_hex => $hash_hex}, {passphrase => $rightphrase});
+ foreach my $option (@options) {
+ my $ppr = Authen::Passphrase::SaltedDigest
+ ->new(algorithm => "SHA-1",
+ salt_hex => $salt_hex,
+ %$option,
+ stretch_count => 10);
+ ok $ppr;
+ foreach my $passphrase ($rightphrase, $rightphrase . 'blahblahblah') {
+ ok ($ppr->match($passphrase) xor $passphrase ne $rightphrase);
+ }
+ }
my %pprs;
my $i = 0;