| 1 | package Authen::Passphrase::Scrypt; |
| 2 | |
| 3 | use 5.014000; |
| 4 | use strict; |
| 5 | use warnings; |
| 6 | use Carp; |
| 7 | |
| 8 | use parent qw/Exporter Authen::Passphrase Class::Accessor::Fast/; |
| 9 | |
| 10 | our @EXPORT = qw/crypto_scrypt/; |
| 11 | our @EXPORT_OK = @EXPORT; |
| 12 | our $VERSION = '0.001001'; |
| 13 | |
| 14 | use Data::Entropy::Algorithms qw/rand_bits/; |
| 15 | use Digest::SHA qw/sha256 hmac_sha256/; |
| 16 | use MIME::Base64; |
| 17 | |
| 18 | require XSLoader; |
| 19 | XSLoader::load('Authen::Passphrase::Scrypt', $VERSION); |
| 20 | |
| 21 | __PACKAGE__->mk_accessors(qw/data logN r p salt hmac passphrase/); |
| 22 | |
| 23 | sub compute_hash { |
| 24 | my ($self, $passphrase) = @_; |
| 25 | crypto_scrypt ($passphrase, $self->salt, (1 << $self->logN), $self->r, $self->p, 64); |
| 26 | } |
| 27 | |
| 28 | sub truncated_sha256 { |
| 29 | my $sha = sha256 shift; |
| 30 | substr $sha, 0, 16 |
| 31 | } |
| 32 | |
| 33 | sub truncate_hash { |
| 34 | substr shift, 32 |
| 35 | } |
| 36 | |
| 37 | sub new { |
| 38 | my ($class, @args) = @_; |
| 39 | my $self = $class->SUPER::new(@args); |
| 40 | |
| 41 | $self->logN(14) unless defined $self->logN; |
| 42 | $self->r(16) unless defined $self->r; |
| 43 | $self->p(1) unless defined $self->p; |
| 44 | croak "passphrase not set" unless defined $self->passphrase; |
| 45 | $self->salt(rand_bits 256) unless $self->salt; |
| 46 | |
| 47 | my $data = "scrypt\x00" . pack 'CNNa32', |
| 48 | $self->logN, $self->r, $self->p, $self->salt; |
| 49 | $data .= truncated_sha256 $data; |
| 50 | $self->data($data); |
| 51 | $self->hmac(hmac_sha256 $self->data, truncate_hash $self->compute_hash($self->passphrase)); |
| 52 | $self |
| 53 | } |
| 54 | |
| 55 | sub from_rfc2307 { |
| 56 | my ($class, $rfc2307) = @_; |
| 57 | croak "Invalid Scrypt RFC2307" unless $rfc2307 =~ m,^{SCRYPT}([A-Za-z0-9+/]{128})$,; |
| 58 | my $data = decode_base64 $1; |
| 59 | my ($scrypt, $logN, $r, $p, $salt, $cksum, $hmac) = |
| 60 | unpack 'Z7CNNa32a16a32', $data; |
| 61 | croak 'Invalid Scrypt hash: should start with "scrypt"' unless $scrypt eq 'scrypt'; |
| 62 | croak 'Invalid Scrypt hash: bad checksum', unless $cksum eq truncated_sha256 (substr $data, 0, 48); |
| 63 | $class->SUPER::new({data => (substr $data, 0, 64), logN => $logN, r => $r, p => $p, salt => $salt, hmac => $hmac}); |
| 64 | } |
| 65 | |
| 66 | sub match { |
| 67 | my ($self, $passphrase) = @_; |
| 68 | my $correct_hmac = hmac_sha256 $self->data, truncate_hash $self->compute_hash($passphrase); |
| 69 | $self->hmac eq $correct_hmac |
| 70 | } |
| 71 | |
| 72 | sub as_rfc2307 { |
| 73 | my ($self) = @_; |
| 74 | '{SCRYPT}' . encode_base64 ($self->data . $self->hmac, '') |
| 75 | } |
| 76 | |
| 77 | sub from_crypt { |
| 78 | croak __PACKAGE__ ." does not support crypt strings, use from_rfc2307 instead"; |
| 79 | } |
| 80 | |
| 81 | sub as_crypt { |
| 82 | croak __PACKAGE__ ." does not support crypt strings, use as_rfc2307 instead"; |
| 83 | } |
| 84 | |
| 85 | 1; |
| 86 | __END__ |
| 87 | |
| 88 | =encoding utf-8 |
| 89 | |
| 90 | =head1 NAME |
| 91 | |
| 92 | Authen::Passphrase::Scrypt - passphrases using Tarsnap's scrypt algorithm |
| 93 | |
| 94 | =head1 SYNOPSIS |
| 95 | |
| 96 | use Authen::Passphrase::Scrypt; |
| 97 | |
| 98 | # Hash a password |
| 99 | my $sc = Authen::Passphrase::Scrypt->new({ |
| 100 | passphrase => 'correcthorsebatterystaple' |
| 101 | }); |
| 102 | my $hash = $sc->as_rfc2307; |
| 103 | say "The given password hashes to $hash"; |
| 104 | |
| 105 | # Verify a password |
| 106 | $sc = Authen::Passphrase::Scrypt->from_rfc2307($hash); |
| 107 | say 'The password was "correcthorsebatterystaple"' |
| 108 | if $sc->match('correcthorsebatterystaple'); |
| 109 | say 'The password was "xkcd"' if $sc->match('xkcd'); |
| 110 | |
| 111 | # Advanced hashing |
| 112 | my $sc = Authen::Passphrase::Scrypt->new({ |
| 113 | passphrase => 'xkcd', |
| 114 | logN => 14, # General work factor |
| 115 | r => 16, # Memory work factor |
| 116 | p => 1, # CPU (parallellism) work factor |
| 117 | salt => 'SodiumChloride && sODIUMcHLORIDE', # Must be 32 bytes |
| 118 | }); |
| 119 | say 'The given password now hashes to ', $sc->as_rfc2307; |
| 120 | |
| 121 | =head1 DESCRIPTION |
| 122 | |
| 123 | B<This is experimental code, DO NOT USE in security-critical software>. |
| 124 | |
| 125 | Scrypt is a key derivation function that was originally developed for |
| 126 | use in the Tarsnap online backup system and is designed to be far more |
| 127 | secure against hardware brute-force attacks than alternative functions |
| 128 | such as PBKDF2 or bcrypt. |
| 129 | |
| 130 | Authen::Passphrase::Scrypt is a module for hashing and verifying |
| 131 | passphrases using scrypt. It offers the same interface as |
| 132 | L<Authen::Passphrase>. It is not however possible to use this module |
| 133 | from within L<Authen::Passphrase>. The methods are: |
| 134 | |
| 135 | =over |
| 136 | |
| 137 | =item Authen::Passphrase::Scrypt->B<new>(I<\%args>) |
| 138 | |
| 139 | Creates a new L<Authen::Passphrase::Scrypt> from a given passphrase |
| 140 | and parameters. Use this to hash a passphrase. The arguments are: |
| 141 | |
| 142 | =over |
| 143 | |
| 144 | =item B<passphrase> |
| 145 | |
| 146 | The passphrase. Mandatory. |
| 147 | |
| 148 | =item B<logN> |
| 149 | |
| 150 | The general work factor (affects both CPU and memory cost). Defaults to 14 |
| 151 | |
| 152 | =item B<r> |
| 153 | |
| 154 | The blocksize (affects memory cost). Defaults to 16. |
| 155 | |
| 156 | =item B<p> |
| 157 | |
| 158 | The parallelization factor (affects CPU cost). Defaults to 1. |
| 159 | |
| 160 | =item B<salt> |
| 161 | |
| 162 | A 32-byte string used as a salt. By default it is randomly generated |
| 163 | using L<Data::Entropy>. |
| 164 | |
| 165 | =back |
| 166 | |
| 167 | All of the parameters change the result of the hash. They are all |
| 168 | stored in the hash, so there is no need to store them separately (or |
| 169 | provide them to the hash verification methods). |
| 170 | |
| 171 | It is normally sufficient to only use the B<logN> parameter to control |
| 172 | the speed of scrypt. B<r> and B<p> are intended to be used only for |
| 173 | fine-tuning: if scrypt uses too much memory but not enough CPU, |
| 174 | decrease logN and increase p; if scrypt uses too much CPU but not |
| 175 | enough memory, decrease logN and increase r. |
| 176 | |
| 177 | =item $sc->B<as_rfc2307> |
| 178 | |
| 179 | Returns the hash of the passphrase, in RFC2307 format. This is |
| 180 | "{SCRYPT}" followed by the base64-encoded 96-byte result described here: L<https://security.stackexchange.com/questions/88678/why-does-node-js-scrypt-function-use-hmac-this-way/91050> |
| 181 | |
| 182 | =item Authen::Passphrase::Scrypt->B<from_rfc2307>(I<$rfc2307>) |
| 183 | |
| 184 | Creates a new L<Authen::Passphrase::Scrypt> from a hash in RFC2307 |
| 185 | format. Use this to verify if a passphrase matches a hash. |
| 186 | |
| 187 | =item $sc->B<match>(I<$passphrase>) |
| 188 | |
| 189 | Returns true if the given passphrase matches the hash, false |
| 190 | otherwise. |
| 191 | |
| 192 | =item Authen::Passphrase::Scrypt->from_crypt |
| 193 | =item $sc->as_crypt |
| 194 | |
| 195 | These functions both croak. They are provided for compatibility with |
| 196 | the Authen::Passphrase interface. |
| 197 | |
| 198 | =back |
| 199 | |
| 200 | =head1 SEE ALSO |
| 201 | |
| 202 | L<Authen::Passphrase>, |
| 203 | L<https://www.tarsnap.com/scrypt.html>, |
| 204 | L<https://www.npmjs.com/package/scrypt> |
| 205 | |
| 206 | =head1 AUTHOR |
| 207 | |
| 208 | Marius Gavrilescu, E<lt>marius@ieval.roE<gt> |
| 209 | |
| 210 | =head1 COPYRIGHT AND LICENSE |
| 211 | |
| 212 | Copyright (C) 2017 by Marius Gavrilescu |
| 213 | |
| 214 | This library is free software; you can redistribute it and/or modify |
| 215 | it under the same terms as Perl itself, either Perl version 5.24.1 or, |
| 216 | at your option, any later version of Perl 5 you may have available. |
| 217 | |
| 218 | |
| 219 | =cut |