incorporate Dancer::Plugin::Passphrase to avoid broken installs
This commit is contained in:
@@ -10,7 +10,7 @@ use base 'Dancer::Plugin::Auth::Extensible::Provider::Base';
|
||||
|
||||
use Dancer ':syntax';
|
||||
use Dancer::Plugin::DBIC;
|
||||
use Dancer::Plugin::Passphrase;
|
||||
use App::Netdisco::Web::Plugin::Passphrase;
|
||||
use Digest::MD5;
|
||||
|
||||
sub authenticate_user {
|
||||
@@ -78,7 +78,6 @@ sub match_with_local_pass {
|
||||
return unless $password and $user->$password_column;
|
||||
|
||||
if ($user->$password_column !~ m/^{[A-Z]+}/) {
|
||||
debug 'authN: using legacy MD5';
|
||||
my $sum = Digest::MD5::md5_hex($password);
|
||||
|
||||
if ($sum eq $user->$password_column) {
|
||||
@@ -93,7 +92,6 @@ sub match_with_local_pass {
|
||||
}
|
||||
}
|
||||
else {
|
||||
debug 'authN: using Passphrase';
|
||||
return passphrase($password)->matches($user->$password_column);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use Dancer ':syntax';
|
||||
use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
use Dancer::Plugin::Auth::Extensible;
|
||||
use Dancer::Plugin::Passphrase;
|
||||
use App::Netdisco::Web::Plugin::Passphrase;
|
||||
|
||||
use App::Netdisco::Web::Plugin;
|
||||
use Digest::MD5 ();
|
||||
|
||||
753
Netdisco/lib/App/Netdisco/Web/Plugin/Passphrase.pm
Normal file
753
Netdisco/lib/App/Netdisco/Web/Plugin/Passphrase.pm
Normal file
@@ -0,0 +1,753 @@
|
||||
package App::Netdisco::Web::Plugin::Passphrase;
|
||||
|
||||
# ABSTRACT: Passphrases and Passwords as objects for Dancer
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Dancer::Plugin::Passphrase - Passphrases and Passwords as objects for Dancer
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
This plugin manages the hashing of passwords for Dancer apps, allowing
|
||||
developers to follow cryptography best practices without having to
|
||||
become a cryptography expert.
|
||||
|
||||
It uses the bcrypt algorithm as the default, while also supporting any
|
||||
hashing function provided by L<Digest>
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
package MyWebService;
|
||||
use Dancer ':syntax';
|
||||
use Dancer::Plugin::Passphrase;
|
||||
|
||||
post '/login' => sub {
|
||||
my $phrase = passphrase( param('my password') )->generate;
|
||||
|
||||
# $phrase is now an object that contains RFC 2307 representation
|
||||
# of the hashed passphrase, along with the salt, and other metadata
|
||||
|
||||
# You should store $phrase->rfc2307() for use later
|
||||
};
|
||||
|
||||
get '/protected' => sub {
|
||||
# Retrieve $stored_rfc_2307_string, like we created above.
|
||||
# IT MUST be a valid RFC 2307 string
|
||||
|
||||
if ( passphrase( param('my password') )->matches( $stored_rfc_2307 ) ) {
|
||||
# Passphrase matches!
|
||||
}
|
||||
};
|
||||
|
||||
get '/generate_new_password' => sub {
|
||||
return passphrase->generate_random;
|
||||
};
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use feature 'switch';
|
||||
|
||||
use Dancer::Plugin;
|
||||
|
||||
use Carp qw(carp croak);
|
||||
use Data::Entropy::Algorithms qw(rand_bits rand_int);
|
||||
use Digest;
|
||||
use MIME::Base64 qw(decode_base64 encode_base64);
|
||||
use Scalar::Util qw(blessed);
|
||||
|
||||
our $VERSION = '2.0.0';
|
||||
|
||||
# Auto stringifies and returns the RFC 2307 representation
|
||||
# of the object unless we are calling a method on it
|
||||
use overload (
|
||||
'""' => sub {
|
||||
if (blessed($_[0]) && $_[0]->isa('App::Netdisco::Web::Plugin::Passphrase')) {
|
||||
$_[0]->rfc2307();
|
||||
}
|
||||
},
|
||||
fallback => 1,
|
||||
);
|
||||
|
||||
register passphrase => \&passphrase;
|
||||
|
||||
|
||||
=head1 KEYWORDS
|
||||
|
||||
=head2 passphrase
|
||||
|
||||
Given a plaintext password, it returns a Dancer::Plugin::Passphrase
|
||||
object that you can generate a new hash from, or match against a stored hash.
|
||||
|
||||
=cut
|
||||
|
||||
sub passphrase {
|
||||
# Dancer 2 keywords receive a reference to the DSL object as a first param.
|
||||
# We don't need it, so get rid of it, and just get the plaintext
|
||||
shift if blessed($_[0]) && $_[0]->isa('Dancer::Core::DSL');
|
||||
|
||||
my $plaintext = $_[0];
|
||||
|
||||
return bless {
|
||||
plaintext => $plaintext
|
||||
}, 'App::Netdisco::Web::Plugin::Passphrase';
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head1 MAIN METHODS
|
||||
|
||||
=head2 generate
|
||||
|
||||
Generates an RFC 2307 representation of the hashed passphrase
|
||||
that is suitable for storage in a database.
|
||||
|
||||
my $pass = passphrase('my passphrase')->generate;
|
||||
|
||||
You should store C<$phrase->rfc_2307()> in your database. For convenience
|
||||
the object will automagically return the RFC 2307 representation when no
|
||||
method is called on it.
|
||||
|
||||
Accepts a hashref of options to specify what kind of hash should be
|
||||
generated. All options settable in the config file are valid.
|
||||
|
||||
If you specify only the algorithm, the default settings for that algorithm will be used.
|
||||
|
||||
A cryptographically random salt is used if salt is not defined.
|
||||
Only if you specify the empty string will an empty salt be used
|
||||
This is not recommended, and should only be used to upgrade old insecure hashes
|
||||
|
||||
my $phrase = passphrase('my password')->generate({
|
||||
algorithm => '', # What algorithm is used to generate the hash
|
||||
cost => '', # Cost / Work Factor if using bcrypt
|
||||
salt => '', # Manually specify salt if using a salted digest
|
||||
});
|
||||
|
||||
=cut
|
||||
|
||||
sub generate {
|
||||
my ($self, $options) = @_;
|
||||
|
||||
$self->_get_settings($options);
|
||||
$self->_calculate_hash;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub generate_hash {
|
||||
carp "generate_hash method is deprecated";
|
||||
return shift->generate();
|
||||
}
|
||||
|
||||
|
||||
=head2 matches
|
||||
|
||||
Matches a plaintext password against a stored hash.
|
||||
Returns 1 if the hash of the password matches the stored hash.
|
||||
Returns undef if they don't match or if there was an error
|
||||
Fail-Secure, rather than Fail-Safe.
|
||||
|
||||
passphrase('my password')->matches($stored_rfc_2307_string);
|
||||
|
||||
$stored_rfc_2307_string B<MUST> be a valid RFC 2307 string,
|
||||
as created by L<generate()|/"passphrase__generate">
|
||||
|
||||
An RFC 2307 string is made up of a scheme identifier, followed by a
|
||||
base64 encoded string. The base64 encoded string should contain
|
||||
the password hash and the salt concatenated together - in that order.
|
||||
|
||||
'{'.$scheme.'}'.encode_base64($hash . $salt, '');
|
||||
|
||||
Where C<$scheme> can be any of the following and their unsalted variants,
|
||||
which have the leading S removed. CRYPT will be Bcrypt.
|
||||
|
||||
SMD5 SSHA SSHA224 SSHA256 SSHA384 SSHA512 CRYPT
|
||||
|
||||
A complete RFC2307 string looks like this:
|
||||
|
||||
{SSHA}K3LAbIjRL5CpLzOlm3/HzS3qt/hUaGVTYWx0
|
||||
|
||||
This is the format created by L<generate()|/"passphrase__generate">
|
||||
|
||||
=cut
|
||||
|
||||
sub matches {
|
||||
my ($self, $stored_hash) = @_;
|
||||
|
||||
# Force auto stringification in case we were passed an object.
|
||||
($stored_hash) = ($stored_hash =~ m/(.*)/s);
|
||||
|
||||
my $new_hash = $self->_extract_settings($stored_hash)->_calculate_hash->rfc2307;
|
||||
|
||||
return ($new_hash eq $stored_hash) ? 1 : undef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 generate_random
|
||||
|
||||
Generates and returns any number of cryptographically random
|
||||
characters from the url-safe base64 charater set.
|
||||
|
||||
my $rand_pass = passphrase->generate_random;
|
||||
|
||||
The passwords generated are suitable for use as
|
||||
temporary passwords or one-time authentication tokens.
|
||||
|
||||
You can configure the length and the character set
|
||||
used by passing a hashref of options.
|
||||
|
||||
my $rand_pass = passphrase->generate_random({
|
||||
length => 32,
|
||||
charset => ['a'..'z', 'A'..'Z'],
|
||||
});
|
||||
|
||||
=cut
|
||||
|
||||
sub generate_random {
|
||||
my ($self, $options) = @_;
|
||||
|
||||
# Default is 16 URL-safe base64 chars. Supported everywhere and a reasonable length
|
||||
my $length = $options->{length} || 16;
|
||||
my $charset = $options->{charset} || ['a'..'z', 'A'..'Z', '0'..'9', '-', '_'];
|
||||
|
||||
return join '', map { @$charset[rand_int scalar @$charset] } 1..$length;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head1 ADDITIONAL METHODS
|
||||
|
||||
The methods are only applicable once you have called C<generate>
|
||||
|
||||
passphrase( 'my password' )->generate->rfc2307; # CORRECT
|
||||
|
||||
passphrase( 'my password' )->rfc2307; # INCORRECT, Returns undef
|
||||
|
||||
|
||||
=head2 rfc2307
|
||||
|
||||
Returns the rfc2307 representation from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
passphrase('my password')->generate->rfc2307;
|
||||
|
||||
=cut
|
||||
|
||||
sub rfc2307 {
|
||||
return shift->{rfc2307} || undef;
|
||||
}
|
||||
|
||||
sub as_rfc2307 {
|
||||
carp "as_rfc2307 method is deprecated";
|
||||
return shift->rfc2307();
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 scheme
|
||||
|
||||
Returns the scheme name from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
This is the scheme name as used in the RFC 2307 representation
|
||||
|
||||
passphrase('my password')->generate->scheme;
|
||||
|
||||
The scheme name can be any of the following, and will always be capitalized
|
||||
|
||||
SMD5 SSHA SSHA224 SSHA256 SSHA384 SSHA512 CRYPT
|
||||
MD5 SHA SHA224 SHA256 SHA384 SHA512
|
||||
|
||||
=cut
|
||||
|
||||
sub scheme {
|
||||
return shift->{scheme} || undef;
|
||||
}
|
||||
|
||||
|
||||
=head2 algorithm
|
||||
|
||||
Returns the algorithm name from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
The algorithm name can be anything that is accepted by C<Digest->new($alg)>
|
||||
This includes any modules in the C<Digest::> Namespace
|
||||
|
||||
passphrase('my password')->generate->algorithm;
|
||||
|
||||
=cut
|
||||
|
||||
sub algorithm {
|
||||
return shift->{algorithm} || undef;
|
||||
}
|
||||
|
||||
|
||||
=head2 cost
|
||||
|
||||
Returns the bcrypt cost from a C<Dancer::Plugin::Passphrase> object.
|
||||
Only works when using the bcrypt algorithm, returns undef for other algorithms
|
||||
|
||||
passphrase('my password')->generate->cost;
|
||||
|
||||
=cut
|
||||
|
||||
sub cost {
|
||||
return shift->{cost} || undef;
|
||||
}
|
||||
|
||||
|
||||
=head2 salt_raw
|
||||
|
||||
Returns the raw salt from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
passphrase('my password')->generate->salt_raw;
|
||||
|
||||
Can be defined, but false - The empty string is technically a valid salt.
|
||||
|
||||
Returns C<undef> if there is no salt.
|
||||
|
||||
=cut
|
||||
|
||||
sub salt_raw {
|
||||
return shift->{salt} // undef;
|
||||
}
|
||||
|
||||
sub raw_salt {
|
||||
carp "raw_salt method is deprecated";
|
||||
return shift->salt_raw();
|
||||
}
|
||||
|
||||
=head2 hash_raw
|
||||
|
||||
Returns the raw hash from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
passphrase('my password')->generate->hash_raw;
|
||||
|
||||
=cut
|
||||
|
||||
sub hash_raw {
|
||||
return shift->{hash} || undef;
|
||||
}
|
||||
|
||||
sub raw_hash {
|
||||
carp "raw_hash method is deprecated";
|
||||
return shift->hash_raw();
|
||||
}
|
||||
|
||||
|
||||
=head2 salt_hex
|
||||
|
||||
Returns the hex-encoded salt from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
Can be defined, but false - The empty string is technically a valid salt.
|
||||
Returns C<undef> if there is no salt.
|
||||
|
||||
passphrase('my password')->generate->salt_hex;
|
||||
|
||||
=cut
|
||||
|
||||
sub salt_hex {
|
||||
return unpack("H*", shift->{salt}) // undef;
|
||||
}
|
||||
|
||||
|
||||
=head2 hash_hex
|
||||
|
||||
Returns the hex-encoded hash from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
passphrase('my password')->generate->hash_hex;
|
||||
|
||||
=cut
|
||||
|
||||
sub hash_hex {
|
||||
return unpack("H*", shift->{hash}) || undef;
|
||||
}
|
||||
|
||||
|
||||
=head2 salt_base64
|
||||
|
||||
Returns the base64 encoded salt from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
Can be defined, but false - The empty string is technically a valid salt.
|
||||
Returns C<undef> if there is no salt.
|
||||
|
||||
passphrase('my password')->generate->salt_base64;
|
||||
|
||||
=cut
|
||||
|
||||
sub salt_base64 {
|
||||
return encode_base64(shift->{salt}, '') // undef;
|
||||
}
|
||||
|
||||
|
||||
=head2 hash_base64
|
||||
|
||||
Returns the base64 encoded hash from a C<Dancer::Plugin::Passphrase> object.
|
||||
|
||||
passphrase('my password')->generate->hash_base64;
|
||||
|
||||
=cut
|
||||
|
||||
sub hash_base64 {
|
||||
return encode_base64(shift->{hash}, '') || undef;
|
||||
}
|
||||
|
||||
=head2 plaintext
|
||||
|
||||
Returns the plaintext password as originally supplied to the L<passphrase> keyword.
|
||||
|
||||
passphrase('my password')->generate->plaintext;
|
||||
|
||||
=cut
|
||||
|
||||
sub plaintext {
|
||||
return shift->{plaintext} || undef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Actual generation of the hash, using the provided settings
|
||||
sub _calculate_hash {
|
||||
my $self = shift;
|
||||
|
||||
my $hasher = Digest->new( $self->algorithm );
|
||||
|
||||
given ($self->algorithm) {
|
||||
when ('Bcrypt') {
|
||||
$hasher->add($self->{plaintext});
|
||||
$hasher->salt($self->salt_raw);
|
||||
$hasher->cost($self->cost);
|
||||
|
||||
$self->{hash} = $hasher->digest;
|
||||
$self->{rfc2307}
|
||||
= '{CRYPT}$'
|
||||
. $self->{type} . '$'
|
||||
. $self->cost . '$'
|
||||
. _en_bcrypt_base64($self->salt_raw)
|
||||
. _en_bcrypt_base64($self->hash_raw);
|
||||
}
|
||||
default {
|
||||
$hasher->add($self->{plaintext});
|
||||
$hasher->add($self->{salt});
|
||||
|
||||
$self->{hash} = $hasher->digest;
|
||||
$self->{rfc2307}
|
||||
= '{' . $self->{scheme} . '}'
|
||||
. encode_base64($self->hash_raw . $self->salt_raw, '');
|
||||
}
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
||||
# Extracts the settings from an RFC 2307 string
|
||||
sub _extract_settings {
|
||||
my ($self, $rfc2307_string) = @_;
|
||||
|
||||
my ($scheme, $settings) = ($rfc2307_string =~ m/^{(\w+)}(.*)/s);
|
||||
|
||||
unless ($scheme && $settings) {
|
||||
croak "An RFC 2307 compliant string must be passed to matches()";
|
||||
}
|
||||
|
||||
if ($scheme eq 'CRYPT'){
|
||||
given ($settings) {
|
||||
when (/^\$2(?:a|x|y)\$/) {
|
||||
$scheme = 'Bcrypt';
|
||||
$settings =~ m{\A\$(2a|2x|2y)\$([0-9]{2})\$([./A-Za-z0-9]{22})}x;
|
||||
|
||||
($self->{type}, $self->{cost}, $self->{salt}) = ($1, $2, _de_bcrypt_base64($3));
|
||||
}
|
||||
default { croak "Unknown CRYPT format: $_"; }
|
||||
}
|
||||
}
|
||||
|
||||
my $scheme_meta = {
|
||||
'MD5' => { algorithm => 'MD5', octets => 128 / 8 },
|
||||
'SMD5' => { algorithm => 'MD5', octets => 128 / 8 },
|
||||
'SHA' => { algorithm => 'SHA-1', octets => 160 / 8 },
|
||||
'SSHA' => { algorithm => 'SHA-1', octets => 160 / 8 },
|
||||
'SHA224' => { algorithm => 'SHA-224', octets => 224 / 8 },
|
||||
'SSHA224' => { algorithm => 'SHA-224', octets => 224 / 8 },
|
||||
'SHA256' => { algorithm => 'SHA-256', octets => 256 / 8 },
|
||||
'SSHA256' => { algorithm => 'SHA-256', octets => 256 / 8 },
|
||||
'SHA384' => { algorithm => 'SHA-384', octets => 384 / 8 },
|
||||
'SSHA384' => { algorithm => 'SHA-384', octets => 384 / 8 },
|
||||
'SHA512' => { algorithm => 'SHA-512', octets => 512 / 8 },
|
||||
'SSHA512' => { algorithm => 'SHA-512', octets => 512 / 8 },
|
||||
'Bcrypt' => { algorithm => 'Bcrypt', octets => 128 / 8 },
|
||||
};
|
||||
|
||||
$self->{scheme} = $scheme;
|
||||
$self->{algorithm} = $scheme_meta->{$scheme}->{algorithm};
|
||||
|
||||
if (!defined $self->{salt}) {
|
||||
$self->{salt} = substr(decode_base64($settings), $scheme_meta->{$scheme}->{octets});
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
||||
# Gets the settings from config.yml, and merges them with any custom
|
||||
# settings given to the constructor
|
||||
sub _get_settings {
|
||||
my ($self, $options) = @_;
|
||||
|
||||
$self->{algorithm} = $options->{algorithm} ||
|
||||
plugin_setting->{algorithm} ||
|
||||
'Bcrypt';
|
||||
|
||||
my $plugin_setting = plugin_setting->{$self->algorithm};
|
||||
|
||||
# Specify empty string to get an unsalted hash
|
||||
# Leaving it undefs results in 128 random bits being used as salt
|
||||
# bcrypt requires this amount, and is reasonable for other algorithms
|
||||
$self->{salt} = $options->{salt} //
|
||||
$plugin_setting->{salt} //
|
||||
rand_bits(128);
|
||||
|
||||
# RFC 2307 scheme is based on the algorithm, with a prefixed 'S' for salted
|
||||
$self->{scheme} = join '', $self->algorithm =~ /[\w]+/g;
|
||||
$self->{scheme} = 'S'.$self->{scheme} if $self->{salt};
|
||||
|
||||
given ($self->{scheme}) {
|
||||
when ('SHA1') { $self->{scheme} = 'SHA'; }
|
||||
when ('SSHA1') { $self->{scheme} = 'SSHA'; }
|
||||
}
|
||||
|
||||
# Bcrypt requires a cost parameter
|
||||
if ($self->algorithm eq 'Bcrypt') {
|
||||
$self->{scheme} = 'CRYPT';
|
||||
$self->{type} = '2a';
|
||||
$self->{cost} = $options->{cost} ||
|
||||
$plugin_setting->{cost} ||
|
||||
4;
|
||||
|
||||
$self->{cost} = 31 if $self->cost > 31;
|
||||
$self->{cost} = sprintf("%02d", $self->cost);
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
||||
# From Crypt::Eksblowfish::Bcrypt.
|
||||
# Bcrypt uses it's own variation on base64
|
||||
sub _en_bcrypt_base64 {
|
||||
my ($octets) = @_;
|
||||
my $text = encode_base64($octets, '');
|
||||
$text =~ tr{A-Za-z0-9+/=}{./A-Za-z0-9}d;
|
||||
return $text;
|
||||
}
|
||||
|
||||
|
||||
# And the decoder of bcrypt's custom base64
|
||||
sub _de_bcrypt_base64 {
|
||||
my ($text) = @_;
|
||||
$text =~ tr{./A-Za-z0-9}{A-Za-z0-9+/};
|
||||
$text .= "=" x (3 - (length($text) + 3) % 4);
|
||||
return decode_base64($text);
|
||||
}
|
||||
|
||||
|
||||
register_plugin for_versions => [ 1, 2 ];
|
||||
|
||||
1;
|
||||
|
||||
|
||||
=head1 MORE INFORMATION
|
||||
|
||||
=head2 Purpose
|
||||
|
||||
The aim of this module is to help you store new passwords in a secure manner,
|
||||
whilst still being able to verify and upgrade older passwords.
|
||||
|
||||
Cryptography is a vast and complex field. Many people try to roll their own
|
||||
methods for securing user data, but succeed only in coming up with
|
||||
a system that has little real security.
|
||||
|
||||
This plugin provides a simple way of managing that complexity, allowing
|
||||
developers to follow crypto best practice without having to become an expert.
|
||||
|
||||
|
||||
=head2 Rationale
|
||||
|
||||
The module defaults to hashing passwords using the bcrypt algorithm, returning them
|
||||
in RFC 2307 format.
|
||||
|
||||
RFC 2307 describes an encoding system for passphrase hashes, as used in the "userPassword"
|
||||
attribute in LDAP databases. It encodes hashes as ASCII text, and supports several
|
||||
passphrase schemes by starting the encoding with an alphanumeric scheme identifier enclosed
|
||||
in braces.
|
||||
|
||||
RFC 2307 only specifies the C<MD5>, and C<SHA> schemes - however in real-world usage,
|
||||
schemes that are salted are widely supported, and are thus provided by this module.
|
||||
|
||||
Bcrypt is an adaptive hashing algorithm that is designed to resist brute
|
||||
force attacks by including a cost (aka work factor). This cost increases
|
||||
the computational effort it takes to compute the hash.
|
||||
|
||||
SHA and MD5 are designed to be fast, and modern machines compute a billion
|
||||
hashes a second. With computers getting faster every day, brute forcing
|
||||
SHA hashes is a very real problem that cannot be easily solved.
|
||||
|
||||
Increasing the cost of generating a bcrypt hash is a trivial way to make
|
||||
brute forcing ineffective. With a low cost setting, bcrypt is just as secure
|
||||
as a more traditional SHA+salt scheme, and just as fast. Increasing the cost
|
||||
as computers become more powerful keeps you one step ahead
|
||||
|
||||
For a more detailed description of why bcrypt is preferred, see this article:
|
||||
L<http://codahale.com/how-to-safely-store-a-password/>
|
||||
|
||||
|
||||
=head2 Configuration
|
||||
|
||||
In your applications config file, you can set the default hashing algorithm,
|
||||
and the default settings for every supported algorithm. Calls to
|
||||
L<generate()|/"passphrase__generate"> will use the default settings
|
||||
for that algorithm specified in here.
|
||||
|
||||
You can override these defaults when you call L<generate()|/"passphrase__generate">.
|
||||
|
||||
If you do no configuration at all, the default is to bcrypt with a cost of 4, and
|
||||
a strong psuedo-random salt.
|
||||
|
||||
plugins:
|
||||
Passphrase:
|
||||
default: Bcrypt
|
||||
|
||||
Bcrypt:
|
||||
cost: 8
|
||||
|
||||
|
||||
=head2 Storage in a database
|
||||
|
||||
You should be storing the RFC 2307 string in your database, it's the easiest way
|
||||
to use this module. You could store the C<raw_salt>, C<raw_hash>, and C<scheme>
|
||||
separately, but this strongly discouraged. RFC 2307 strings are specifically
|
||||
designed for storing hashed passwords, and should be used wherever possible.
|
||||
|
||||
The length of the string produced by L<generate()|/"passphrase__generate"> can
|
||||
vary dependent on your settings. Below is a table of the lengths generated
|
||||
using default settings.
|
||||
|
||||
You will need to make sure your database columns are at least this long.
|
||||
If the string gets truncated, the password can I<never> be validated.
|
||||
|
||||
ALGORITHM LENGTH EXAMPLE RFC 2307 STRING
|
||||
|
||||
Bcrypt 68 {CRYPT}$2a$04$MjkMhQxasFQod1qq56DXCOvWu6YTWk9X.EZGnmSSIbbtyEBIAixbS
|
||||
SHA-512 118 {SSHA512}lZG4dZ5EU6dPEbJ1kBPPzEcupFloFSIJjiXCwMVxJXOy/x5qhBA5XH8FiUWj7u59onQxa97xYdqje/fwY5TDUcW1Urplf3KHMo9NO8KO47o=
|
||||
SHA-384 98 {SSHA384}SqZF5YYyk4NdjIM8YgQVfRieXDxNG0dKH4XBcM40Eblm+ribCzdyf0JV7i2xJvVHZsFSQNcuZPKtiTMzDyOU+w==
|
||||
SHA-256 74 {SSHA256}xsJHNzPlNCpOZ41OkTfQOU35ZY+nRyZFaM8lHg5U2pc0xT3DKNlGW2UTY0NPYsxU
|
||||
SHA-224 70 {SSHA224}FTHNkvKOdyX1d6f45iKLVxpaXZiHel8pfilUT1dIZ5u+WIUyhDGxLnx72X0=
|
||||
SHA-1 55 {SSHA}Qsaao/Xi/bYTRMQnpHuD3y5nj02wbdcw5Cek2y2nLs3pIlPh
|
||||
MD5 51 {SMD5}bgfLiUQWgzUm36+nBhFx62bi0xdwTp+UpEeNKDxSLfM=
|
||||
|
||||
=head2 Common Mistakes
|
||||
|
||||
Common mistakes people make when creating their own solution. If any of these
|
||||
seem familiar, you should probably be using this module
|
||||
|
||||
=over
|
||||
|
||||
=item Passwords are stored as plain text for a reason
|
||||
|
||||
There is never a valid reason to store a password as plain text.
|
||||
Passwords should be reset and not emailed to customers when they forget.
|
||||
Support people should be able to login as a user without knowing the users password.
|
||||
No-one except the user should know the password - that is the point of authentication.
|
||||
|
||||
=item No-one will ever guess our super secret algorithm!
|
||||
|
||||
Unless you're a cryptography expert with many years spent studying
|
||||
super-complex maths, your algorithm is almost certainly not as secure
|
||||
as you think. Just because it's hard for you to break doesn't mean
|
||||
it's difficult for a computer.
|
||||
|
||||
=item Our application-wide salt is "Sup3r_S3cret_L0ng_Word" - No-one will ever guess that.
|
||||
|
||||
This is common misunderstanding of what a salt is meant to do. The purpose of a
|
||||
salt is to make sure the same password doesn't always generate the same hash.
|
||||
A fresh salt needs to be created each time you hash a password. It isn't meant
|
||||
to be a secret key.
|
||||
|
||||
=item We generate our random salt using C<rand>.
|
||||
|
||||
C<rand> isn't actually random, it's a non-unform pseudo-random number generator,
|
||||
and not suitable for cryptographic applications. Whilst this module also defaults to
|
||||
a PRNG, it is better than the one provided by C<rand>. Using a true RNG is a config
|
||||
option away, but is not the default as it it could potentially block output if the
|
||||
system does not have enough entropy to generate a truly random number
|
||||
|
||||
=item We use C<md5(pass.salt)>, and the salt is from C</dev/random>
|
||||
|
||||
MD5 has been broken for many years. Commodity hardware can find a
|
||||
hash collision in seconds, meaning an attacker can easily generate
|
||||
the correct MD5 hash without using the correct password.
|
||||
|
||||
=item We use C<sha(pass.salt)>, and the salt is from C</dev/random>
|
||||
|
||||
SHA isn't quite as broken as MD5, but it shares the same theoretical
|
||||
weaknesses. Even without hash collisions, it is vulnerable to brute forcing.
|
||||
Modern hardware is so powerful it can try around a billion hashes a second.
|
||||
That means every 7 chracter password in the range [A-Za-z0-9] can be cracked
|
||||
in one hour on your average desktop computer.
|
||||
|
||||
=item If the only way to break the hash is to brute-force it, it's secure enough
|
||||
|
||||
It is unlikely that your database will be hacked and your hashes brute forced.
|
||||
However, in the event that it does happen, or SHA512 is broken, using this module
|
||||
gives you an easy way to change to a different algorithm, while still allowing
|
||||
you to validate old passphrases
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head1 KNOWN ISSUES
|
||||
|
||||
If you see errors like this
|
||||
|
||||
Wide character in subroutine entry
|
||||
|
||||
or
|
||||
|
||||
Input must contain only octets
|
||||
|
||||
The C<MD5>, C<bcrypt>, and C<SHA> algorithms can't handle chracters with an ordinal
|
||||
value above 255, producing errors like this if they encounter them.
|
||||
It is not possible for this plugin to automagically work out the correct
|
||||
encoding for a given string.
|
||||
|
||||
If you see errors like this, then you probably need to use the L<Encode> module
|
||||
to encode your text as UTF-8 (or whatever encoding it is) before giving it
|
||||
to C<passphrase>.
|
||||
|
||||
Text encoding is a bag of hurt, and errors like this are probably indicitive
|
||||
of deeper problems within your app's code.
|
||||
|
||||
You will save yourself a lot of trouble if you read up on the
|
||||
L<Encode> module sooner rather than later.
|
||||
|
||||
For further reading on UTF-8, unicode, and text encoding in perl,
|
||||
see L<http://training.perl.com/OSCON2011/index.html>
|
||||
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Dancer>, L<Digest>, L<Crypt::Eksblowfish::Bcrypt>, L<Dancer::Plugin::Bcrypt>
|
||||
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
James Aitken <jaitken@cpan.org>
|
||||
|
||||
|
||||
=head1 COPYRIGHT AND LICENSE
|
||||
|
||||
This software is copyright (c) 2012 by James Aitken.
|
||||
|
||||
This is free software; you can redistribute it and/or modify it under
|
||||
the same terms as the Perl 5 programming language system itself.
|
||||
|
||||
=cut
|
||||
Reference in New Issue
Block a user