password 1.0.0.0 → 2.0.0.0
raw patch · 19 files changed
+1617/−292 lines, 19 filesdep +base-compatdep +base64dep +cryptonitedep ~basedep ~bytestringdep ~textbuild-type:Customsetup-changednew-uploaderPVP ok
version bump matches the API change (PVP)
Dependencies added: base-compat, base64, cryptonite, memory, template-haskell
Dependency ranges changed: base, bytestring, text
API changes (from Hackage documentation)
- Data.Password: PassCheckFail :: PassCheck
- Data.Password: PassCheckSuccess :: PassCheck
- Data.Password: PassHash :: Text -> PassHash
- Data.Password: [getSalt] :: Salt -> ByteString
- Data.Password: [unPassHash] :: PassHash -> Text
- Data.Password: checkPass :: Pass -> PassHash -> PassCheck
- Data.Password: data Pass
- Data.Password: data PassCheck
- Data.Password: hashPass :: MonadIO m => Pass -> m PassHash
- Data.Password: hashPassWithSalt :: Salt -> Pass -> PassHash
- Data.Password: instance Data.String.IsString Data.Password.Pass
- Data.Password: instance GHC.Classes.Eq Data.Password.PassCheck
- Data.Password: instance GHC.Classes.Eq Data.Password.PassHash
- Data.Password: instance GHC.Classes.Ord Data.Password.PassHash
- Data.Password: instance GHC.Read.Read Data.Password.PassCheck
- Data.Password: instance GHC.Read.Read Data.Password.PassHash
- Data.Password: instance GHC.Show.Show Data.Password.Pass
- Data.Password: instance GHC.Show.Show Data.Password.PassCheck
- Data.Password: instance GHC.Show.Show Data.Password.PassHash
- Data.Password: mkPass :: Text -> Pass
- Data.Password: newtype PassHash
- Data.Password: unsafeShowPasswordText :: Pass -> Text
+ Data.Password: PasswordCheckFail :: PasswordCheck
+ Data.Password: PasswordCheckSuccess :: PasswordCheck
+ Data.Password: PasswordHash :: Text -> PasswordHash a
+ Data.Password: [unPasswordHash] :: PasswordHash a -> Text
+ Data.Password: data Password
+ Data.Password: data PasswordCheck
+ Data.Password: mkPassword :: Text -> Password
+ Data.Password: newtype PasswordHash a
+ Data.Password.Argon2: Argon2Params :: Word32 -> Variant -> Version -> Word32 -> Word32 -> Word32 -> Word32 -> Argon2Params
+ Data.Password.Argon2: Argon2d :: Variant
+ Data.Password.Argon2: Argon2i :: Variant
+ Data.Password.Argon2: Argon2id :: Variant
+ Data.Password.Argon2: PasswordCheckFail :: PasswordCheck
+ Data.Password.Argon2: PasswordCheckSuccess :: PasswordCheck
+ Data.Password.Argon2: PasswordHash :: Text -> PasswordHash a
+ Data.Password.Argon2: Salt :: ByteString -> Salt a
+ Data.Password.Argon2: Version10 :: Version
+ Data.Password.Argon2: Version13 :: Version
+ Data.Password.Argon2: [argon2MemoryCost] :: Argon2Params -> Word32
+ Data.Password.Argon2: [argon2OutputLength] :: Argon2Params -> Word32
+ Data.Password.Argon2: [argon2Parallelism] :: Argon2Params -> Word32
+ Data.Password.Argon2: [argon2Salt] :: Argon2Params -> Word32
+ Data.Password.Argon2: [argon2TimeCost] :: Argon2Params -> Word32
+ Data.Password.Argon2: [argon2Variant] :: Argon2Params -> Variant
+ Data.Password.Argon2: [argon2Version] :: Argon2Params -> Version
+ Data.Password.Argon2: [unPasswordHash] :: PasswordHash a -> Text
+ Data.Password.Argon2: checkPassword :: Password -> PasswordHash Argon2 -> PasswordCheck
+ Data.Password.Argon2: data Argon2
+ Data.Password.Argon2: data Argon2Params
+ Data.Password.Argon2: data Password
+ Data.Password.Argon2: data PasswordCheck
+ Data.Password.Argon2: data Variant
+ Data.Password.Argon2: data Version
+ Data.Password.Argon2: defaultParams :: Argon2Params
+ Data.Password.Argon2: hashPassword :: MonadIO m => Password -> m (PasswordHash Argon2)
+ Data.Password.Argon2: hashPasswordWithParams :: MonadIO m => Argon2Params -> Password -> m (PasswordHash Argon2)
+ Data.Password.Argon2: hashPasswordWithSalt :: Argon2Params -> Salt Argon2 -> Password -> PasswordHash Argon2
+ Data.Password.Argon2: instance GHC.Classes.Eq Data.Password.Argon2.Argon2Params
+ Data.Password.Argon2: instance GHC.Show.Show Data.Password.Argon2.Argon2Params
+ Data.Password.Argon2: mkPassword :: Text -> Password
+ Data.Password.Argon2: newSalt :: MonadIO m => m (Salt Argon2)
+ Data.Password.Argon2: newtype PasswordHash a
+ Data.Password.Argon2: newtype Salt a
+ Data.Password.Argon2: unsafeShowPassword :: Password -> Text
+ Data.Password.Bcrypt: PasswordCheckFail :: PasswordCheck
+ Data.Password.Bcrypt: PasswordCheckSuccess :: PasswordCheck
+ Data.Password.Bcrypt: PasswordHash :: Text -> PasswordHash a
+ Data.Password.Bcrypt: Salt :: ByteString -> Salt a
+ Data.Password.Bcrypt: [unPasswordHash] :: PasswordHash a -> Text
+ Data.Password.Bcrypt: checkPassword :: Password -> PasswordHash Bcrypt -> PasswordCheck
+ Data.Password.Bcrypt: data Bcrypt
+ Data.Password.Bcrypt: data Password
+ Data.Password.Bcrypt: data PasswordCheck
+ Data.Password.Bcrypt: hashPassword :: MonadIO m => Password -> m (PasswordHash Bcrypt)
+ Data.Password.Bcrypt: hashPasswordWithParams :: MonadIO m => Int -> Password -> m (PasswordHash Bcrypt)
+ Data.Password.Bcrypt: hashPasswordWithSalt :: Int -> Salt Bcrypt -> Password -> PasswordHash Bcrypt
+ Data.Password.Bcrypt: mkPassword :: Text -> Password
+ Data.Password.Bcrypt: newSalt :: MonadIO m => m (Salt Bcrypt)
+ Data.Password.Bcrypt: newtype PasswordHash a
+ Data.Password.Bcrypt: newtype Salt a
+ Data.Password.Bcrypt: unsafeShowPassword :: Password -> Text
+ Data.Password.PBKDF2: PBKDF2Params :: Word32 -> PBKDF2Algorithm -> Word32 -> Word32 -> PBKDF2Params
+ Data.Password.PBKDF2: PBKDF2_MD5 :: PBKDF2Algorithm
+ Data.Password.PBKDF2: PBKDF2_SHA1 :: PBKDF2Algorithm
+ Data.Password.PBKDF2: PBKDF2_SHA256 :: PBKDF2Algorithm
+ Data.Password.PBKDF2: PBKDF2_SHA512 :: PBKDF2Algorithm
+ Data.Password.PBKDF2: PasswordCheckFail :: PasswordCheck
+ Data.Password.PBKDF2: PasswordCheckSuccess :: PasswordCheck
+ Data.Password.PBKDF2: PasswordHash :: Text -> PasswordHash a
+ Data.Password.PBKDF2: Salt :: ByteString -> Salt a
+ Data.Password.PBKDF2: [pbkdf2Algorithm] :: PBKDF2Params -> PBKDF2Algorithm
+ Data.Password.PBKDF2: [pbkdf2Iterations] :: PBKDF2Params -> Word32
+ Data.Password.PBKDF2: [pbkdf2OutputLength] :: PBKDF2Params -> Word32
+ Data.Password.PBKDF2: [pbkdf2Salt] :: PBKDF2Params -> Word32
+ Data.Password.PBKDF2: [unPasswordHash] :: PasswordHash a -> Text
+ Data.Password.PBKDF2: checkPassword :: Password -> PasswordHash PBKDF2 -> PasswordCheck
+ Data.Password.PBKDF2: data PBKDF2
+ Data.Password.PBKDF2: data PBKDF2Algorithm
+ Data.Password.PBKDF2: data PBKDF2Params
+ Data.Password.PBKDF2: data Password
+ Data.Password.PBKDF2: data PasswordCheck
+ Data.Password.PBKDF2: defaultParams :: PBKDF2Params
+ Data.Password.PBKDF2: hashPassword :: MonadIO m => Password -> m (PasswordHash PBKDF2)
+ Data.Password.PBKDF2: hashPasswordWithParams :: MonadIO m => PBKDF2Params -> Password -> m (PasswordHash PBKDF2)
+ Data.Password.PBKDF2: hashPasswordWithSalt :: PBKDF2Params -> Salt PBKDF2 -> Password -> PasswordHash PBKDF2
+ Data.Password.PBKDF2: instance GHC.Classes.Eq Data.Password.PBKDF2.PBKDF2Algorithm
+ Data.Password.PBKDF2: instance GHC.Classes.Eq Data.Password.PBKDF2.PBKDF2Params
+ Data.Password.PBKDF2: instance GHC.Show.Show Data.Password.PBKDF2.PBKDF2Algorithm
+ Data.Password.PBKDF2: instance GHC.Show.Show Data.Password.PBKDF2.PBKDF2Params
+ Data.Password.PBKDF2: mkPassword :: Text -> Password
+ Data.Password.PBKDF2: newSalt :: MonadIO m => m (Salt PBKDF2)
+ Data.Password.PBKDF2: newtype PasswordHash a
+ Data.Password.PBKDF2: newtype Salt a
+ Data.Password.PBKDF2: unsafeShowPassword :: Password -> Text
+ Data.Password.Scrypt: PasswordCheckFail :: PasswordCheck
+ Data.Password.Scrypt: PasswordCheckSuccess :: PasswordCheck
+ Data.Password.Scrypt: PasswordHash :: Text -> PasswordHash a
+ Data.Password.Scrypt: Salt :: ByteString -> Salt a
+ Data.Password.Scrypt: ScryptParams :: Word32 -> Word32 -> Word32 -> Word32 -> Word32 -> ScryptParams
+ Data.Password.Scrypt: [scryptBlockSize] :: ScryptParams -> Word32
+ Data.Password.Scrypt: [scryptOutputLength] :: ScryptParams -> Word32
+ Data.Password.Scrypt: [scryptParallelism] :: ScryptParams -> Word32
+ Data.Password.Scrypt: [scryptRounds] :: ScryptParams -> Word32
+ Data.Password.Scrypt: [scryptSalt] :: ScryptParams -> Word32
+ Data.Password.Scrypt: [unPasswordHash] :: PasswordHash a -> Text
+ Data.Password.Scrypt: checkPassword :: Password -> PasswordHash Scrypt -> PasswordCheck
+ Data.Password.Scrypt: data Password
+ Data.Password.Scrypt: data PasswordCheck
+ Data.Password.Scrypt: data Scrypt
+ Data.Password.Scrypt: data ScryptParams
+ Data.Password.Scrypt: defaultParams :: ScryptParams
+ Data.Password.Scrypt: hashPassword :: MonadIO m => Password -> m (PasswordHash Scrypt)
+ Data.Password.Scrypt: hashPasswordWithParams :: MonadIO m => ScryptParams -> Password -> m (PasswordHash Scrypt)
+ Data.Password.Scrypt: hashPasswordWithSalt :: ScryptParams -> Salt Scrypt -> Password -> PasswordHash Scrypt
+ Data.Password.Scrypt: instance GHC.Classes.Eq Data.Password.Scrypt.ScryptParams
+ Data.Password.Scrypt: instance GHC.Show.Show Data.Password.Scrypt.ScryptParams
+ Data.Password.Scrypt: mkPassword :: Text -> Password
+ Data.Password.Scrypt: newSalt :: MonadIO m => m (Salt Scrypt)
+ Data.Password.Scrypt: newtype PasswordHash a
+ Data.Password.Scrypt: newtype Salt a
+ Data.Password.Scrypt: unsafeShowPassword :: Password -> Text
- Data.Password: Salt :: ByteString -> Salt
+ Data.Password: Salt :: ByteString -> Salt a
- Data.Password: newSalt :: MonadIO m => m Salt
+ Data.Password: newSalt :: MonadIO m => Int -> m (Salt a)
- Data.Password: newtype Salt
+ Data.Password: newtype Salt a
- Data.Password: unsafeShowPassword :: Pass -> String
+ Data.Password: unsafeShowPassword :: Password -> Text
Files
- ChangeLog.md +16/−0
- LICENSE +4/−4
- README +20/−0
- README.md +0/−13
- Setup.hs +31/−0
- password.cabal +41/−20
- src/Data/Password.hs +45/−230
- src/Data/Password/Argon2.hs +336/−0
- src/Data/Password/Bcrypt.hs +181/−0
- src/Data/Password/Internal.hs +140/−0
- src/Data/Password/PBKDF2.hs +294/−0
- src/Data/Password/Scrypt.hs +262/−0
- test/doctest/doctest.hs +11/−4
- test/tasty/Argon2.hs +29/−0
- test/tasty/Bcrypt.hs +19/−0
- test/tasty/Internal.hs +69/−0
- test/tasty/PBKDF2.hs +67/−0
- test/tasty/Scrypt.hs +37/−0
- test/tasty/Spec.hs +15/−21
ChangeLog.md view
@@ -1,5 +1,21 @@ # Changelog for password +## 2.0.0.0++- Complete overhaul of the library to include hashing and checking+ passwords with not just `scrypt`, but also `PBKDF2`, `bcrypt` and+ `Argon2`.+ [#8](https://github.com/cdepillabout/password/pull/8)+- `cryptonite` is now used as a dependency, instead of the `scrypt` package.+ [#8](https://github.com/cdepillabout/password/pull/8)+- Done away with abbreviating "password" (`Pass/pass` -> `Password/password`)+ [#8](https://github.com/cdepillabout/password/pull/8)+- Removed `unsafeShowPasswordText` and changed `unsafeShowPassword` to be+ `Password -> Text`. (Anyone who needs it to be a `String` knows where to+ find `Data.Text.unpack`)+ [#8](https://github.com/cdepillabout/password/pull/8)+- GHC versions < 8.2 are no longer actively supported. (Tested to work for GHC 8.2.2)+ ## 1.0.0.0 - `hashPassWithSalt` has switched function arguments for better currying.
LICENSE view
@@ -1,4 +1,4 @@-Copyright Dennis Gosnell (c) 2019+Copyright (c) Dennis Gosnell, 2019; Felix Paulusma, 2020 All rights reserved. @@ -13,9 +13,9 @@ disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Dennis Gosnell nor the names of other- contributors may be used to endorse or promote products derived- from this software without specific prior written permission.+ * Neither the name of Dennis Gosnell, Felix Paulusma nor the names+ of other contributors may be used to endorse or promote products+ derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ README view
@@ -0,0 +1,20 @@+# password++[](http://travis-ci.org/cdepillabout/password)+[](https://hackage.haskell.org/package/password)+[](http://stackage.org/lts/package/password)+[](http://stackage.org/nightly/package/password)+[](./LICENSE)++This library provides datatypes and functions for working with passwords and+password hashes in Haskell.++Currently supports the following algorithms:++* `PBKDF2`+* `bcrypt`+* `scrypt`+* `Argon2`++Also, see the [password-instances](https://hackage.haskell.org/package/password-instances)+package for instances for common typeclasses.
− README.md
@@ -1,13 +0,0 @@-# password--[](http://travis-ci.org/cdepillabout/password)-[](https://hackage.haskell.org/package/password)-[](http://stackage.org/lts/package/password)-[](http://stackage.org/nightly/package/password)-[](./LICENSE)--This library provides datatypes and functions for working with passwords and-password hashes in Haskell.--Also, see the [password-instances](../password-instances) package for instances-for common typeclasses.
Setup.hs view
@@ -1,2 +1,33 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -Wall #-}+module Main (main) where++#ifndef MIN_VERSION_cabal_doctest+#define MIN_VERSION_cabal_doctest(x,y,z) 0+#endif++#if MIN_VERSION_cabal_doctest(1,0,0)++import Distribution.Extra.Doctest ( defaultMainWithDoctests )+main :: IO ()+main = defaultMainWithDoctests "doctests"++#else++#ifdef MIN_VERSION_Cabal+-- If the macro is defined, we have new cabal-install,+-- but for some reason we don't have cabal-doctest in package-db+--+-- Probably we are running cabal sdist, when otherwise using new-build+-- workflow+#warning You are configuring this package without cabal-doctest installed. \+ The doctests test-suite will not work as a result. \+ To fix this, install cabal-doctest before configuring.+#endif+ import Distribution.Simple++main :: IO () main = defaultMain++#endif
password.cabal view
@@ -1,22 +1,28 @@ cabal-version: 1.12 name: password-version: 1.0.0.0+version: 2.0.0.0 category: Data-synopsis: plain-text password and hashed password datatypes and functions-description: A library providing types for working with plain-text and hashed passwords, generally for web applications.+synopsis: Hashing and checking of passwords+description: A library providing functionality for working with plain-text and hashed passwords with different types of algorithms. homepage: https://github.com/cdepillabout/password/password#readme bug-reports: https://github.com/cdepillabout/password/issues-author: Dennis Gosnell-maintainer: cdep.illabout@gmail.com-copyright: Copyright (c) 2019 Dennis Gosnell+author: Dennis Gosnell, Felix Paulusma+maintainer: cdep.illabout@gmail.com, felix.paulusma@gmail.com+copyright: Copyright (c) Dennis Gosnell, 2019; Felix Paulusma, 2020 license: BSD3 license-file: LICENSE-build-type: Simple+build-type: Custom extra-source-files:- README.md+ README ChangeLog.md +custom-setup+ setup-depends:+ base+ , Cabal+ , cabal-doctest >=1.0.6 && <1.1+ source-repository head type: git location: https://github.com/cdepillabout/password@@ -26,18 +32,26 @@ src exposed-modules: Data.Password+ Data.Password.Argon2+ Data.Password.Bcrypt+ Data.Password.PBKDF2+ Data.Password.Scrypt other-modules: Paths_password+ Data.Password.Internal build-depends:- base >=4.7 && <5- , scrypt- , text+ base >= 4.9 && < 5+ , base64 >= 0.3 && < 0.5+ , bytestring >= 0.10.8.1 && < 0.11+ , cryptonite >= 0.15.1 && < 0.27+ , memory >= 0.14 && < 0.16+ , text >= 1.2.2 && < 1.3 ghc-options: -Wall default-language: Haskell2010 -test-suite password-doctest+test-suite doctests type: exitcode-stdio-1.0 hs-source-dirs:@@ -45,16 +59,15 @@ main-is: doctest.hs ghc-options:- -threaded -rtsopts -with-rtsopts=-N+ -threaded -O2 -rtsopts -with-rtsopts=-N build-depends:- base >=4.7 && <5- -- bytestring is used when actually running the doctests- , bytestring+ base >=4.9 && <5+ , base-compat , doctest , password , QuickCheck , quickcheck-instances- , scrypt+ , template-haskell default-language: Haskell2010 @@ -66,12 +79,20 @@ main-is: Spec.hs other-modules:- Paths_password+ Argon2+ , Bcrypt+ , Internal+ , PBKDF2+ , Scrypt+ , Paths_password ghc-options:- -threaded -rtsopts -with-rtsopts=-N+ -threaded -O2 -rtsopts -with-rtsopts=-N build-depends:- base >=4.7 && <5+ base >=4.9 && <5 , password+ , bytestring+ , cryptonite+ , memory , quickcheck-instances , scrypt , tasty
src/Data/Password.hs view
@@ -1,260 +1,75 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}- {-| Module : Data.Password-Copyright : (c) Dennis Gosnell, 2019+Copyright : (c) Dennis Gosnell, 2019; Felix Paulusma, 2020 License : BSD-style (see LICENSE file) Maintainer : cdep.illabout@gmail.com Stability : experimental Portability : POSIX -This module provides an easy way for interacting with passwords from Haskell.-It provides the types 'Pass' and 'PassHash', which correspond to plain-text and+This library provides an easy way for interacting with passwords from Haskell.+It provides the types 'Password' and 'PasswordHash', which correspond to plain-text and hashed passwords. -It also provides functions for hashing ('hashPass') and checking passwords-('checkPass').+== API -The real benefit of this module is that there is a corresponding+Every supported hashing algorithm has its own module (e.g. "Data.Password.Bcrypt")+which exports its own @hashPassword@ and @checkPassword@ functions, as well as all the+types and functions in this module. If you are not sure about the specifics of an+algorithm you want to use, you can rest assured that by using the @hashPassword@ function+of the respective algorithm you are not making any big mistakes, security-wise.++Of course, if you know what you're doing and you want more fine-grained control+over the hashing function, you can adjust it using the @hashPasswordWithParams@+function of the respective algorithm.++== Algorithms++Generally, the most "secure" algorithm is believed to be @"Argon2"@,+then @"Scrypt"@, then @"Bcrypt"@, and lastly @"PBKDF2"@. @"Bcrypt"@ and @"PBKDF2"@+are the most established algorithms, so they have been tried and tested, though+they both lack a memory cost, and therefore have a greater vulnerability to+specialized hardware attacks.++When choosing an algorithm, and you have no idea which to pick, just go for+@"Bcrypt"@ if your password does not need the highest security possible.+It's still a fine way for hashing passwords, and the cost is easily adjustable if needed.+If your needs do require stronger protection, you should find someone who can advise you+on this topic. (And if you're already knowledgeable enough, you know what to do)++== Special instances++The real benefit of this module is that there is an accompanying <http://hackage.haskell.org/package/password-instances password-instances>-module that provides canonical typeclass instances for-'Pass' and 'PassHash' for many common typeclasses, like+package that provides canonical typeclass instances for+'Password' and 'PasswordHash' for many common typeclasses, like <http://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#t:FromJSON FromJSON> from <http://hackage.haskell.org/package/aeson aeson>, <http://hackage.haskell.org/package/persistent/docs/Database-Persist-Class.html#t:PersistField PersistField> from <http://hackage.haskell.org/package/persistent persistent>, etc. -See the <http://hackage.haskell.org/package/password-instances password-instances> module for more information.+See the <http://hackage.haskell.org/package/password-instances password-instances> package for more information. -} -module Data.Password- (- -- * Plaintext Password- Pass- , mkPass- -- * Hashed Password- , PassHash(..)+module Data.Password (+ -- * Plain-text Password+ Password+ , mkPassword+ -- * Password Hashing+ , PasswordHash(..)+ , PasswordCheck(..) , Salt(..)- -- * Functions for Hashing Plaintext Passwords- , hashPass- , hashPassWithSalt , newSalt- -- * Functions for Checking Plaintext Passwords Against Hashed Passwords- , checkPass- , PassCheck(..)- -- * Unsafe Debugging Functions for Showing a Password+ -- * Unsafe debugging function to show a Password , unsafeShowPassword- , unsafeShowPasswordText- , -- * Setup for doctests.- -- $setup ) where -import Control.Monad.IO.Class (MonadIO(liftIO))-import Crypto.Scrypt (EncryptedPass(..), Salt(..), defaultParams, encryptPass,- verifyPass')-import qualified Crypto.Scrypt as Scrypt-import Data.String (IsString(..))-import Data.Text (Text, unpack)-import Data.Text.Encoding (decodeUtf8With, encodeUtf8)-import Data.Text.Encoding.Error (lenientDecode)---- $setup--- >>> :set -XOverloadedStrings------ Import needed libraries.------ >>> import Data.ByteString (pack)--- >>> import Test.QuickCheck (Arbitrary(arbitrary), Blind(Blind), vector)--- >>> import Test.QuickCheck.Instances.ByteString ()--- >>> import Test.QuickCheck.Instances.Text ()------ 'Arbitrary' instances for types exported from this library.------ >>> instance Arbitrary Salt where arbitrary = Salt . pack <$> vector 32--- >>> instance Arbitrary Pass where arbitrary = fmap Pass arbitrary--- >>> instance Arbitrary PassHash where arbitrary = hashPassWithSalt <$> arbitrary <*> arbitrary------ 'Arbitrary' instances for types exported from "Crypto.Scrypt".------ >>> instance Arbitrary Scrypt.Pass where arbitrary = fmap Scrypt.Pass arbitrary--- >>> instance Arbitrary EncryptedPass where arbitrary = encryptPass defaultParams <$> arbitrary <*> arbitrary---- | A plain-text password.------ This represents a plain-text password that has /NOT/ been hashed.------ You should be careful with 'Pass'. Make sure not to write it to logs or--- store it in a database.------ You can construct a 'Pass' by using the 'mkPass' function or as literal strings together with the--- OverloadedStrings pragma (or manually, by using 'fromString' on a 'String').--- Alternatively, you could also use some of the instances in the @password-instances@ library.-newtype Pass = Pass Text- deriving (IsString)---- | CAREFUL: 'Show'-ing a 'Pass' will always print @"**PASSWORD**"@------ >>> show $ mkPass "hello"--- "**PASSWORD**"------ @since 1.0.0.0-instance Show Pass where- show _ = "**PASSWORD**"---- | Construct a 'Pass'------ @since 1.0.0.0-mkPass :: Text -> Pass-mkPass = Pass---- | This is an unsafe function that shows a password in plain-text.------ >>> unsafeShowPasswordText $ mkPass "foobar"--- "foobar"------ You should generally not use this function.-unsafeShowPassword :: Pass -> String-unsafeShowPassword = unpack . unsafeShowPasswordText---- | This is like 'unsafeShowPassword' but produces a 'Text' instead of a--- 'String'.-unsafeShowPasswordText :: Pass -> Text-unsafeShowPasswordText (Pass pass) = pass---- | A hashed password.------ This represents a password that has been put through a hashing function.--- The hashed password can be stored in a database.-newtype PassHash = PassHash- { unPassHash :: Text- } deriving (Eq, Ord, Read, Show)---- | Convert an "Crypto.Scrypt".'Scrypt.Pass' to our 'Pass' type.------ >>> passToScryptPass "foobar"--- Pass {getPass = "foobar"}-passToScryptPass :: Pass -> Scrypt.Pass-passToScryptPass (Pass pass) = Scrypt.Pass $ encodeUtf8 pass---- Convert our 'Pass' type to an "Crypto.Scrypt".'Script.Pass'.------ Opposite of 'passToScryptPass'.------ >>> scryptPassToPass $ Scrypt.Pass "foobar"--- Pass {getPass = "foobar"}--- scryptPassToPass :: Scrypt.Pass -> Pass--- scryptPassToPass (Scrypt.Pass pass) = Pass $ decodeUtf8With lenientDecode pass---- | Convert an "Crypto.Scrypt".'EncryptedPass' to our 'PassHash' type.------ This is the opposite of 'passHashToScryptEncryptedPass'.------ >>> let salt = Salt "abcdefghijklmnopqrstuvwxyz012345"--- >>> let pass = Scrypt.Pass "foobar"--- >>> let encryptedPass = encryptPass defaultParams salt pass--- >>> encryptedPass--- EncryptedPass {getEncryptedPass = "14|8|1|YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU=|nENDaqWBmPKapAqQ3//H0iBImweGjoTqn5SvBS8Mc9FPFbzq6w65maYPZaO+SPamVZRXQjARQ8Y+5rhuDhjIhw=="}--- >>> scryptEncryptedPassToPassHash encryptedPass--- PassHash {unPassHash = "14|8|1|YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU=|nENDaqWBmPKapAqQ3//H0iBImweGjoTqn5SvBS8Mc9FPFbzq6w65maYPZaO+SPamVZRXQjARQ8Y+5rhuDhjIhw=="}------ prop> scryptEncryptedPassToPassHash (passHashToScryptEncryptedPass passHash) == passHash-scryptEncryptedPassToPassHash :: EncryptedPass -> PassHash-scryptEncryptedPassToPassHash (EncryptedPass encPass) =- PassHash $ decodeUtf8With lenientDecode encPass---- | Convert out 'PassHash' type to "Crypto.Scrypt".'EncryptedPass'.------ This is the opposite of 'scryptEncryptedPassToPassHash'.------ >>> let salt = Salt "abcdefghijklmnopqrstuvwxyz012345"--- >>> let pass = Scrypt.Pass "foobar"--- >>> let encryptedPass = encryptPass defaultParams salt pass--- >>> encryptedPass--- EncryptedPass {getEncryptedPass = "14|8|1|YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU=|nENDaqWBmPKapAqQ3//H0iBImweGjoTqn5SvBS8Mc9FPFbzq6w65maYPZaO+SPamVZRXQjARQ8Y+5rhuDhjIhw=="}--- >>> scryptEncryptedPassToPassHash encryptedPass--- PassHash {unPassHash = "14|8|1|YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU=|nENDaqWBmPKapAqQ3//H0iBImweGjoTqn5SvBS8Mc9FPFbzq6w65maYPZaO+SPamVZRXQjARQ8Y+5rhuDhjIhw=="}------ prop> passHashToScryptEncryptedPass (scryptEncryptedPassToPassHash encPass) == encPass-passHashToScryptEncryptedPass :: PassHash -> EncryptedPass-passHashToScryptEncryptedPass (PassHash passHash) =- EncryptedPass $ encodeUtf8 passHash---- | Just like 'hashPassWithSalt', but generate a new 'Salt' everytime with a--- call to 'newSalt'.------ >>> hashPass $ mkPass "foobar"--- PassHash {unPassHash = "14|8|1|...|..."}-hashPass :: MonadIO m => Pass -> m PassHash-hashPass pass = do- salt <- liftIO newSalt- pure $ hashPassWithSalt salt pass---- | Hash a password with the given 'Salt'.------ The resulting 'PassHash' has the parameters used to hash it, as well as the--- 'Salt' appended to it, separated by @|@.------ The input 'Salt' and resulting 'PassHash' are both byte-64 encoded.------ >>> let salt = Salt "abcdefghijklmnopqrstuvwxyz012345"--- >>> hashPassWithSalt salt (mkPass "foobar")--- PassHash {unPassHash = "14|8|1|YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU=|nENDaqWBmPKapAqQ3//H0iBImweGjoTqn5SvBS8Mc9FPFbzq6w65maYPZaO+SPamVZRXQjARQ8Y+5rhuDhjIhw=="}------ (Note that we use an explicit 'Salt' in the example above. This is so that the--- example is reproducible, but in general you should use 'hashPass'. 'hashPass'--- generates a new 'Salt' everytime it is called.)------ This function uses the hash function from the scrypt package: 'encryptPass'.-hashPassWithSalt :: Salt -> Pass -> PassHash-hashPassWithSalt salt pass =- scryptEncryptedPassToPassHash $- encryptPass defaultParams salt (passToScryptPass pass)---- | The result of a checking a password against a hashed version. This is--- returned by 'checkPass'.-data PassCheck- = PassCheckSuccess- -- ^ The password check was successful. The plain-text password matches the- -- hashed password.- | PassCheckFail- -- ^ The password check failed. The plain-text password does not match the- -- hashed password.- deriving (Eq, Read, Show)---- | Check a 'Pass' against a 'PassHash'.------ Returns 'PassCheckSuccess' on success.------ >>> let salt = Salt "abcdefghijklmnopqrstuvwxyz012345"--- >>> let pass = mkPass "foobar"--- >>> let passHash = hashPassWithSalt salt pass--- >>> checkPass pass passHash--- PassCheckSuccess------ Returns 'PassCheckFail' If an incorrect 'Pass' or 'PassHash' is used.------ >>> let badpass = mkPass "incorrect-password"--- >>> checkPass badpass passHash--- PassCheckFail------ This should always fail if an incorrect password is given.------ prop> \(Blind badpass) -> let correctPassHash = hashPassWithSalt salt "foobar" in checkPass badpass correctPassHash == PassCheckFail-checkPass :: Pass -> PassHash -> PassCheck-checkPass pass passHash =- if verifyPass' (passToScryptPass pass) (passHashToScryptEncryptedPass passHash)- then PassCheckSuccess- else PassCheckFail---- | Generate a random 32-byte salt.-newSalt :: MonadIO m => m Salt-newSalt = liftIO Scrypt.newSalt+import Data.Password.Internal --- TODO: Create code for checking that plaintext passwords conform to some sort of+-- TODO: Create code for checking that plain-text passwords conform to some sort of -- password policy. --- data PassPolicy = PassPolicy+-- data PasswordPolicy = PasswordPolicy -- { passPolicyLength :: Int -- , passPolicyCharReqs :: [PolicyCharReq] -- , passPolicyCharSet :: PolicyCharSet
+ src/Data/Password/Argon2.hs view
@@ -0,0 +1,336 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ExplicitForAll #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-|+Module : Data.Password.Argon2+Copyright : (c) Felix Paulusma, 2020+License : BSD-style (see LICENSE file)+Maintainer : cdep.illabout@gmail.com+Stability : experimental+Portability : POSIX++= Argon2++@Argon2@ is probably the newest password algorithm out there. Argon2 was+selected as the winner of the Password Hashing Competition in July 2015.++It has three variants, namely 'Argon2d', 'Argon2i' and 'Argon2id'. These protect+against GPU cracking attacks, side-channel attacks, and both, respectively.++All three modes allow specification by three parameters that control:++* execution time+* memory required+* degree of parallelism++== Other algorithms++In comparison to other algorithms, Argon2 is the least "battle-tested",+being the newest algorithm out there.++It is, however, recommended over @"Scrypt"@ most of the time,+and it also seems like it might become the go-to password algorithm if no+vulnarabilities are discovered within the next couple of years.+-}++-- I think the portability is broadened to+-- whatever, now that we use cryptonite... I think+module Data.Password.Argon2 (+ -- Algorithm+ Argon2+ -- * Plain-text Password+ , Password+ , mkPassword+ -- * Hash Passwords (Argon2)+ , hashPassword+ , PasswordHash(..)+ -- * Verify Passwords (Argon2)+ , checkPassword+ , PasswordCheck(..)+ -- * Hashing Manually (Argon2)+ , hashPasswordWithParams+ , defaultParams+ , Argon2Params(..)+ , Argon2.Variant(..)+ , Argon2.Version(..)+ -- ** Hashing with salt (DISADVISED)+ --+ -- | Hashing with a set 'Salt' is almost never what you want+ -- to do. Use 'hashPassword' or 'hashPasswordWithParams' to have+ -- automatic generation of randomized salts.+ , hashPasswordWithSalt+ , newSalt+ , Salt(..)+ -- * Unsafe debugging function to show a Password+ , unsafeShowPassword+ , -- * Setup for doctests.+ -- $setup+ ) where++import Control.Monad (guard)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Crypto.Error (throwCryptoError)+import Crypto.KDF.Argon2 as Argon2+import Data.ByteArray (Bytes, convert)+import Data.ByteString (ByteString)+import Data.ByteString.Base64 (encodeBase64)+import qualified Data.ByteString.Char8 as C8 (length)+import Data.Maybe (fromMaybe)+#if! MIN_VERSION_base(4,13,0)+import Data.Semigroup ((<>))+#endif+import Data.Text (Text)+import qualified Data.Text as T (intercalate, length, split, splitAt)+import Data.Word (Word32)++import Data.Password (+ PasswordCheck(..)+ , PasswordHash(..)+ , Salt(..)+ , mkPassword+ , unsafeShowPassword+ )+import Data.Password.Internal (Password(..), from64, readT, showT, toBytes)+import qualified Data.Password.Internal (newSalt)+++-- | Phantom type for __Argon2__+--+-- @since 2.0.0.0+data Argon2++-- $setup+-- >>> :set -XFlexibleInstances+-- >>> :set -XOverloadedStrings+--+-- Import needed libraries.+--+-- >>> import Data.Password+-- >>> import Data.ByteString (pack)+-- >>> import Test.QuickCheck (Arbitrary(arbitrary), Blind(Blind), vector)+-- >>> import Test.QuickCheck.Instances.Text ()+--+-- >>> instance Arbitrary (Salt a) where arbitrary = Salt . pack <$> vector 16+-- >>> instance Arbitrary Password where arbitrary = fmap Password arbitrary+-- >>> let testParams = defaultParams {argon2TimeCost = 1}+-- >>> let salt = Salt "abcdefghijklmnop"++-- -- >>> instance Arbitrary (PasswordHash Argon2) where arbitrary = hashPasswordWithSalt testParams <$> arbitrary <*> arbitrary++-- | Hash the 'Password' using the 'Argon2' hash algorithm+--+-- >>> hashPassword $ mkPassword "foobar"+-- PasswordHash {unPasswordHash = "$argon2id$v=19$m=65536,t=2,p=1$...$..."}+hashPassword :: MonadIO m => Password -> m (PasswordHash Argon2)+hashPassword = hashPasswordWithParams defaultParams++-- | Parameters used in the 'Argon2' hashing algorithm.+--+-- @since 2.0.0.0+data Argon2Params = Argon2Params {+ argon2Salt :: Word32,+ -- ^ Bytes to randomly generate as a unique salt, default is __16__+ --+ -- Limits are min: @8@, and max: @(2 ^ 32) - 1@+ argon2Variant :: Argon2.Variant,+ -- ^ Which variant of Argon2 to use, default is __'Argon2id'__+ argon2Version :: Argon2.Version,+ -- ^ Which version of Argon2 to use, default is __'Version13'__+ argon2MemoryCost :: Word32,+ -- ^ Memory cost, given in /kibibytes/, default is __65536__ (i.e. 64MB)+ --+ -- Limits are min: @8 * 'argon2Parallelism'@, and max is addressing+ -- space / 2, or @(2 ^ 32) - 1@, whichever is lower.+ argon2TimeCost :: Word32,+ -- ^ Amount of computation realized, default is __2__+ --+ -- Limits are min: @1@, and max: @(2 ^ 32) - 1@+ argon2Parallelism :: Word32,+ -- ^ Parallelism factor, default is __1__+ --+ -- Limits are min: @1@, and max: @(2 ^ 24) - 1@+ argon2OutputLength :: Word32+ -- ^ Output key length in bytes, default is __32__+ --+ -- Limits are min: @4@, and max: @(2 ^ 32) - 1@+} deriving (Eq, Show)++-- | Default parameters for the 'Argon2' algorithm.+--+-- >>> defaultParams+-- Argon2Params {argon2Salt = 16, argon2Variant = Argon2id, argon2Version = Version13, argon2MemoryCost = 65536, argon2TimeCost = 2, argon2Parallelism = 1, argon2OutputLength = 32}+--+-- @since 2.0.0.0+defaultParams :: Argon2Params+defaultParams = Argon2Params {+ argon2Salt = 16,+ argon2Variant = Argon2id,+ argon2Version = Version13,+ argon2MemoryCost = 2 ^ (16 :: Int),+ argon2TimeCost = 2,+ argon2Parallelism = 1,+ argon2OutputLength = 32+}++-- | Hash a password with the given 'Argon2Params' and also with the given 'Salt'+-- instead of a random generated salt using 'argon2Salt' from 'Argon2Params'. (cf. 'hashPasswordWithParams')+-- Using 'hashPasswordWithSalt' is strongly __disadvised__ and 'hashPasswordWithParams' should be used instead.+-- /Never use a static salt in production applications!/+--+-- __N.B.__: The salt HAS to be 8 bytes or more, or this function will throw an error!+--+-- >>> let salt = Salt "abcdefghijklmnop"+-- >>> hashPasswordWithSalt defaultParams salt (mkPassword "foobar")+-- PasswordHash {unPasswordHash = "$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA==$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o="}+--+-- (Note that we use an explicit 'Salt' in the example above. This is so that the+-- example is reproducible, but in general you should use 'hashPassword'. 'hashPassword'+-- generates a new 'Salt' everytime it is called.)+hashPasswordWithSalt :: Argon2Params -> Salt Argon2 -> Password -> PasswordHash Argon2+hashPasswordWithSalt params@Argon2Params{..} s@(Salt salt) pass =+ PasswordHash . mappend "$argon2" $ T.intercalate "$"+ [ variantToLetter argon2Variant+ , "v=" <> versionToNum argon2Version+ , parameters+ , encodeBase64 salt+ , encodeBase64 key+ ]+ where+ parameters = T.intercalate ","+ [ "m=" <> showT argon2MemoryCost+ , "t=" <> showT argon2TimeCost+ , "p=" <> showT argon2Parallelism+ ]+ key = hashPasswordWithSalt' params s pass++-- | Only for internal use+hashPasswordWithSalt' :: Argon2Params -> Salt Argon2 -> Password -> ByteString+hashPasswordWithSalt' Argon2Params{..} (Salt salt) (Password pass) =+ convert (argon2Hash :: Bytes)+ where+ argon2Hash = throwCryptoError $+ Argon2.hash options (toBytes pass) (convert salt :: Bytes) $ fromIntegral argon2OutputLength+ options = Argon2.Options {+ iterations = argon2TimeCost,+ memory = argon2MemoryCost,+ parallelism = argon2Parallelism,+ variant = argon2Variant,+ version = argon2Version+ }++-- | Hash a password using the 'Argon2' algorithm with the given 'Argon2Params'.+--+-- __N.B.__: If you have any doubt in your knowledge of cryptography and/or the+-- 'Argon2' algorithm, please just use 'hashPassword'.+--+-- Advice to set the parameters:+--+-- * Figure out how many threads you can use, choose "parallelism" accordingly.+-- * Figure out how much memory you can use, choose "memory cost" accordingly.+-- * Decide on the maximum time @x@ you can spend on it, choose the largest+-- "time cost" such that it takes less than @x@ with your system and other+-- parameter choices.+--+-- @since 2.0.0.0+hashPasswordWithParams :: MonadIO m => Argon2Params -> Password -> m (PasswordHash Argon2)+hashPasswordWithParams params pass = liftIO $ do+ salt <- Data.Password.Internal.newSalt . fromIntegral $ argon2Salt params+ return $ hashPasswordWithSalt params salt pass++-- TODO: Parse different kinds of hashes, not only the ones from this library.+-- e.g. hashes that miss the first $, or have 'argon2$' in front of the 'argon2id' part.++-- | Check a 'Password' against a 'PasswordHash' 'Argon2'.+--+-- Returns 'PasswordCheckSuccess' on success.+--+-- >>> let pass = mkPassword "foobar"+-- >>> passHash <- hashPassword pass+-- >>> checkPassword pass passHash+-- PasswordCheckSuccess+--+-- Returns 'PasswordCheckFail' if an incorrect 'Password' or 'PasswordHash' 'Argon2' is used.+--+-- >>> let badpass = mkPassword "incorrect-password"+-- >>> checkPassword badpass passHash+-- PasswordCheckFail+--+-- This should always fail if an incorrect password is given.+--+-- prop> \(Blind badpass) -> let correctPasswordHash = hashPasswordWithSalt testParams salt "foobar" in checkPassword badpass correctPasswordHash == PasswordCheckFail+checkPassword :: Password -> PasswordHash Argon2 -> PasswordCheck+checkPassword pass (PasswordHash passHash) =+ fromMaybe PasswordCheckFail $ do+ let paramList = T.split (== '$') passHash+ guard $ length paramList == 6+ let [ _,+ variantT,+ versionT,+ parametersT,+ salt64,+ hashedKey64 ] = paramList+ argon2Variant <- parseVariant variantT+ argon2Version <- parseVersion versionT+ (argon2MemoryCost, argon2TimeCost, argon2Parallelism) <- parseParameters parametersT+ salt <- from64 salt64+ hashedKey <- from64 hashedKey64+ let argon2OutputLength = fromIntegral $ C8.length hashedKey -- only here because of warnings+ producedKey = hashPasswordWithSalt' Argon2Params{..} (Salt salt) pass+ guard $ hashedKey == producedKey+ return PasswordCheckSuccess+ where+ argon2Salt = 16 -- only here because of warnings+ parseVariant = splitMaybe "argon2" letterToVariant+ parseVersion = splitMaybe "v=" numToVersion+ parseParameters params = do+ let ps = T.split (== ',') params+ guard $ length ps == 3+ go ps (Nothing, Nothing, Nothing)+ where+ go [] (Just m, Just t, Just p) = Just (m, t, p)+ go [] _ = Nothing+ go (x:xs) (m, t, p) =+ case T.splitAt 2 x of+ ("m=", i) -> go xs (readT i, t, p)+ ("t=", i) -> go xs (m, readT i, p)+ ("p=", i) -> go xs (m, t, readT i)+ _ -> Nothing+ splitMaybe :: Text -> (Text -> Maybe a) -> Text -> Maybe a+ splitMaybe match f t =+ case T.splitAt (T.length match) t of+ (m, x) | m == match -> f x+ _ -> Nothing++-- | Generate a random 16-byte @Argon2@ salt+--+-- @since 2.0.0.0+newSalt :: MonadIO m => m (Salt Argon2)+newSalt = Data.Password.Internal.newSalt 16++-- | Makes a letter out of the variant+variantToLetter :: Argon2.Variant -> Text+variantToLetter = \case+ Argon2i -> "i"+ Argon2d -> "d"+ Argon2id -> "id"++-- | Parses the variant parameter in the encoded hash+letterToVariant :: Text -> Maybe Argon2.Variant+letterToVariant = \case+ "i" -> Just Argon2i+ "d" -> Just Argon2d+ "id" -> Just Argon2id+ _ -> Nothing++-- | Parses the "v=" parameter in the encoded hash+numToVersion :: Text -> Maybe Argon2.Version+numToVersion "16" = Just Argon2.Version10+numToVersion "19" = Just Argon2.Version13+numToVersion _ = Nothing++-- | Makes number for the "v=" parameter in the encoded hash+versionToNum :: Argon2.Version -> Text+versionToNum Version10 = "16"+versionToNum Version13 = "19"
+ src/Data/Password/Bcrypt.hs view
@@ -0,0 +1,181 @@+{-|+Module : Data.Password.Bcrypt+Copyright : (c) Felix Paulusma, 2020+License : BSD-style (see LICENSE file)+Maintainer : cdep.illabout@gmail.com+Stability : experimental+Portability : POSIX++= bcrypt++The @bcrypt@ algorithm is a popular way of hashing passwords.+It is based on the Blowfish cipher and fairly straightfoward+in its usage. It has a cost parameter that, when increased,+slows down the hashing speed.++It is a straightforward and easy way to get decent protection+on passwords, it is also been around long enough to be battle-tested+and generally considered to provide a good amount of security.++== Other algorithms++@bcrypt@, together with @"PBKDF2"@, are only computationally intensive.+And to protect from specialized hardware, new algorithms have been+developed that are also resource intensive, like @"Scrypt"@ and+@"Argon2"@. Not having high resource demands, means an attacker with+specialized software could take less time to brute-force a password,+though with the default cost (10) and a decently long password,+the amount of time to brute-force would still be significant.++This the algorithm to use if you're not sure about your needs, but+just want a decent, proven way to encrypt your passwords.+-}++module Data.Password.Bcrypt (+ -- * Algorithm+ Bcrypt+ -- * Plain-text Password+ , Password+ , mkPassword+ -- * Hash Passwords (bcrypt)+ , hashPassword+ , PasswordHash(..)+ -- * Verify Passwords (bcrypt)+ , checkPassword+ , PasswordCheck(..)+ -- * Hashing Manually (bcrypt)+ , hashPasswordWithParams+ -- ** Hashing with salt (DISADVISED)+ --+ -- | Hashing with a set 'Salt' is almost never what you want+ -- to do. Use 'hashPassword' or 'hashPasswordWithParams' to have+ -- automatic generation of randomized salts.+ , hashPasswordWithSalt+ , newSalt+ , Salt(..)+ -- * Unsafe debugging function to show a Password+ , unsafeShowPassword+ , -- * Setup for doctests.+ -- $setup+ ) where++import Control.Monad.IO.Class (MonadIO(liftIO))+import Crypto.KDF.BCrypt as Bcrypt hiding (hashPassword)+import Data.ByteArray (Bytes, convert)++import Data.Password (+ PasswordCheck(..)+ , PasswordHash(..)+ , Salt(..)+ , mkPassword+ , unsafeShowPassword+ )+import Data.Password.Internal (Password(..), fromBytes, toBytes)+import qualified Data.Password.Internal (newSalt)+++-- | Phantom type for __bcrypt__+--+-- @since 2.0.0.0+data Bcrypt++-- $setup+-- >>> :set -XFlexibleInstances+-- >>> :set -XOverloadedStrings+--+-- Import needed libraries.+--+-- >>> import Data.Password+-- >>> import Data.ByteString (pack)+-- >>> import Test.QuickCheck (Arbitrary(arbitrary), Blind(Blind), vector)+-- >>> import Test.QuickCheck.Instances.Text ()+--+-- >>> instance Arbitrary (Salt a) where arbitrary = Salt . pack <$> vector 16+-- >>> instance Arbitrary Password where arbitrary = fmap Password arbitrary+-- >>> let salt = Salt "abcdefghijklmnop"++-- -- >>> instance Arbitrary (PasswordHash Bcrypt) where arbitrary = hashPasswordWithSalt 8 <$> arbitrary <*> arbitrary++-- | Hash the 'Password' using the /bcrypt/ hash algorithm.+--+-- __N.B.__: @bcrypt@ has a limit of 72 bytes as input, so anything longer than that+-- will be cut off at the 72 byte point and thus any password that is 72 bytes+-- or longer will match as long as the first 72 bytes are the same.+--+-- >>> hashPassword $ mkPassword "foobar"+-- PasswordHash {unPasswordHash = "$2b$10$..."}+hashPassword :: MonadIO m => Password -> m (PasswordHash Bcrypt)+hashPassword = hashPasswordWithParams 10++-- | Hash a password with the given cost and also with the given 'Salt'+-- instead of generating a random salt. Using 'hashPasswordWithSalt' is strongly __disadvised__,+-- and 'hashPasswordWithParams' should be used instead. /Never use a static salt/+-- /in production applications!/+--+-- __N.B.__: The salt HAS to be 16 bytes or this function will throw an error!+--+-- >>> let salt = Salt "abcdefghijklmnop"+-- >>> hashPasswordWithSalt 10 salt (mkPassword "foobar")+-- PasswordHash {unPasswordHash = "$2b$10$WUHhXETkX0fnYkrqZU3ta.N8Utt4U77kW4RVbchzgvBvBBEEdCD/u"}+--+-- (Note that we use an explicit 'Salt' in the example above. This is so that the+-- example is reproducible, but in general you should use 'hashPassword'. 'hashPassword'+-- (and 'hashPasswordWithParams') generates a new 'Salt' everytime it is called.)+hashPasswordWithSalt+ :: Int -- ^ The cost parameter. Should be between 4 and 31 (inclusive). Values which lie outside this range will be adjusted accordingly.+ -> Salt Bcrypt -- ^ The salt. MUST be 16 bytes in length or an error will be raised.+ -> Password -- ^ The password to be hashed.+ -> PasswordHash Bcrypt -- ^ The bcrypt hash in standard format.+hashPasswordWithSalt cost (Salt salt) (Password pass) =+ let hash = Bcrypt.bcrypt cost (convert salt :: Bytes) (toBytes pass)+ in PasswordHash $ fromBytes hash++-- | Hash a password using the /bcrypt/ algorithm with the given cost.+--+-- The higher the cost, the longer 'hashPassword' and 'checkPassword' will take to run,+-- thus increasing the security, but taking longer and taking up more resources.+-- The optimal cost for generic user logins would be one that would take between+-- 0.05 - 0.5 seconds to check on the machine that will run it.+--+-- __N.B.__: It is advised to use 'hashPassword' if you're unsure about the+-- implications that changing the cost brings with it.+--+-- @since 2.0.0.0+hashPasswordWithParams+ :: MonadIO m+ => Int -- ^ The cost parameter. Should be between 4 and 31 (inclusive). Values which lie outside this range will be adjusted accordingly.+ -> Password -- ^ The password to be hashed.+ -> m (PasswordHash Bcrypt) -- ^ The bcrypt hash in standard format.+hashPasswordWithParams cost pass = liftIO $ do+ salt <- newSalt+ return $ hashPasswordWithSalt cost salt pass++-- | Check a 'Password' against a 'PasswordHash' 'Bcrypt'.+--+-- Returns 'PasswordCheckSuccess' on success.+--+-- >>> let pass = mkPassword "foobar"+-- >>> passHash <- hashPassword pass+-- >>> checkPassword pass passHash+-- PasswordCheckSuccess+--+-- Returns 'PasswordCheckFail' if an incorrect 'Password' or 'PasswordHash' 'Bcrypt' is used.+--+-- >>> let badpass = mkPassword "incorrect-password"+-- >>> checkPassword badpass passHash+-- PasswordCheckFail+--+-- This should always fail if an incorrect password is given.+--+-- prop> \(Blind badpass) -> let correctPasswordHash = hashPasswordWithSalt 8 salt "foobar" in checkPassword badpass correctPasswordHash == PasswordCheckFail+checkPassword :: Password -> PasswordHash Bcrypt -> PasswordCheck+checkPassword (Password pass) (PasswordHash passHash) =+ if Bcrypt.validatePassword (toBytes pass) (toBytes passHash)+ then PasswordCheckSuccess+ else PasswordCheckFail++-- | Generate a random 16-byte @bcrypt@ salt+--+-- @since 2.0.0.0+newSalt :: MonadIO m => m (Salt Bcrypt)+newSalt = Data.Password.Internal.newSalt 16
+ src/Data/Password/Internal.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE ExplicitForAll #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-|+Module : Data.Password.Internal+Copyright : (c) Dennis Gosnell, 2019; Felix Paulusma, 2020+License : BSD-style (see LICENSE file)+Maintainer : cdep.illabout@gmail.com+Stability : experimental+Portability : POSIX+-}++module Data.Password.Internal (+ -- * Global types+ Password(..)+ , mkPassword+ , PasswordHash(..)+ , PasswordCheck(..)+ , Salt(..)+ , newSalt+ -- * Unsafe function+ , unsafeShowPassword+ -- * Utility+ , toBytes+ , fromBytes+ , from64+ , readT+ , showT+ ) where++import Control.Monad.IO.Class (MonadIO(liftIO))+import Crypto.Random (getRandomBytes)+import Data.ByteArray (Bytes, convert)+import Data.ByteString (ByteString)+import Data.ByteString.Base64 (decodeBase64)+import Data.String (IsString(..))+import Data.Text as T (Text, pack, unpack)+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Text.Read (readMaybe)+++-- | A plain-text password.+--+-- This represents a plain-text password that has /NOT/ been hashed.+--+-- You should be careful with 'Password'. Make sure not to write it to logs or+-- store it in a database.+--+-- You can construct a 'Password' by using the 'mkPassword' function or as literal+-- strings together with the OverloadedStrings pragma (or manually, by using+-- 'fromString' on a 'String'). Alternatively, you could also use some of the+-- instances in the <http://hackage.haskell.org/package/password-instances password-instances>+-- library.+newtype Password = Password Text+ deriving (IsString)++-- | CAREFUL: 'Show'-ing a 'Password' will always print @"**PASSWORD**"@+--+-- >>> show ("hello" :: Password)+-- "**PASSWORD**"+--+-- @since 1.0.0.0+instance Show Password where+ show _ = "**PASSWORD**"++-- | Construct a 'Password'+--+-- @since 1.0.0.0+mkPassword :: Text -> Password+mkPassword = Password+{-# INLINE mkPassword #-}++-- | A salt used by a hashing algorithm.+--+-- @since 2.0.0.0+newtype Salt a = Salt ByteString+ deriving (Eq, Show)++-- | Generate a random x-byte-long salt.+--+-- @since 2.0.0.0+newSalt :: MonadIO m => Int -> m (Salt a)+newSalt i = liftIO $ Salt <$> getRandomBytes i+{-# INLINE newSalt #-}++-- | This is an unsafe function that shows a password in plain-text.+--+-- >>> unsafeShowPassword ("foobar" :: Password)+-- "foobar"+--+-- You should generally not use this function.+unsafeShowPassword :: Password -> Text+unsafeShowPassword (Password pass) = pass+{-# INLINE unsafeShowPassword #-}++-- | A hashed password.+--+-- This represents a password that has been put through a hashing function.+-- The hashed password can be stored in a database.+newtype PasswordHash a = PasswordHash+ { unPasswordHash :: Text+ } deriving (Eq, Ord, Read, Show)++-- | The result of checking a password against a hashed version. This is+-- returned by the @checkPassword@ functions.+data PasswordCheck+ = PasswordCheckSuccess+ -- ^ The password check was successful. The plain-text password matches the+ -- hashed password.+ | PasswordCheckFail+ -- ^ The password check failed. The plain-text password does not match the+ -- hashed password.+ deriving (Eq, Read, Show)++-- | Converting 'Text' to 'Bytes'+toBytes :: Text -> Bytes+toBytes = convert . encodeUtf8+{-# INLINE toBytes #-}++-- | Converting 'Bytes' to 'Text'+fromBytes :: Bytes -> Text+fromBytes = decodeUtf8 . convert+{-# INLINE fromBytes #-}++-- | Decodes a base64 'Text' to a regular 'ByteString' (if possible)+from64 :: Text -> Maybe ByteString+from64 = toMaybe . decodeBase64 . encodeUtf8+ where+ toMaybe = either (const Nothing) Just+{-# INLINE from64 #-}++-- | Same as 'read' but works on 'Text'+readT :: forall a. Read a => Text -> Maybe a+readT = readMaybe . T.unpack+{-# INLINE readT #-}++-- | Same as 'show' but works on 'Text'+showT :: forall a. Show a => a -> Text+showT = T.pack . show+{-# INLINE showT #-}+
+ src/Data/Password/PBKDF2.hs view
@@ -0,0 +1,294 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-|+Module : Data.Password.PBKDF2+Copyright : (c) Felix Paulusma, 2020+License : BSD-style (see LICENSE file)+Maintainer : cdep.illabout@gmail.com+Stability : experimental+Portability : POSIX++= PBKDF2++The PBKDF2 algorithm is one of the oldest and most solid password+algorithms out there. It has also, however, been shown to be+the least secure out of all major password algorithms. The main+reason for this is that is doesn't make use of any memory cost+or other method of making it difficult for specialized hardware+attacks, like GPU cracking attacks.++It is still, however used all over the world, since it has been+shown to be a very reliable way to encrypt passwords. Though it is+most definitely better than trying to develop a password algorithm+on your own, or god-forbid, not using /any/ encryption on your stored+passwords.++== Other algorithms++Seeing as PBKDF2 is shown to be very weak in terms of protection+against GPU cracking attacks, it is generally advised to go with+@"Bcrypt"@, if not @"Scrypt"@ or @"Argon2"@. When unsure, @"Bcrypt"@+would probably be the safest option, as it has no memory cost which+could become a problem if not properly calibrated to the machine+doing the password verifications.+-}++module Data.Password.PBKDF2 (+ -- Algorithm+ PBKDF2+ -- * Plain-text Password+ , Password+ , mkPassword+ -- * Hash Passwords (PBKDF2)+ , hashPassword+ , PasswordHash(..)+ -- * Verify Passwords (PBKDF2)+ , checkPassword+ , PasswordCheck(..)+ -- * Hashing Manually (PBKDF2)+ , hashPasswordWithParams+ , defaultParams+ , PBKDF2Params(..)+ , PBKDF2Algorithm(..)+ -- ** Hashing with salt (DISADVISED)+ --+ -- | Hashing with a set 'Salt' is almost never what you want+ -- to do. Use 'hashPassword' or 'hashPasswordWithParams' to have+ -- automatic generation of randomized salts.+ , hashPasswordWithSalt+ , newSalt+ , Salt(..)+ -- * Unsafe debugging function to show a Password+ , unsafeShowPassword+ , -- * Setup for doctests.+ -- $setup+ ) where++import Control.Monad (guard)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Crypto.Hash.Algorithms as Crypto (MD5(..))+import Crypto.KDF.PBKDF2 as PBKDF2+import Data.ByteArray (ByteArray, ByteArrayAccess, Bytes, convert)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Base64 as Base64+import qualified Data.ByteString.Char8 as C8 (length)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T (intercalate, pack, split, stripPrefix)+import Data.Word (Word32)++import Data.Password (+ PasswordCheck(..)+ , PasswordHash(..)+ , Salt(..)+ , mkPassword+ , unsafeShowPassword+ )+import Data.Password.Internal (Password(..), from64, readT, toBytes)+import qualified Data.Password.Internal (newSalt)+++-- | Phantom type for __PBKDF2__+--+-- @since 2.0.0.0+data PBKDF2++-- $setup+-- >>> :set -XFlexibleInstances+-- >>> :set -XOverloadedStrings+--+-- Import needed libraries.+--+-- >>> import Data.Password+-- >>> import Data.ByteString (pack)+-- >>> import Test.QuickCheck (Arbitrary(arbitrary), Blind(Blind), vector)+-- >>> import Test.QuickCheck.Instances.Text ()+--+-- >>> instance Arbitrary (Salt a) where arbitrary = Salt . pack <$> vector 16+-- >>> instance Arbitrary Password where arbitrary = fmap Password arbitrary+-- >>> let testParams = defaultParams{ pbkdf2Iterations = 5000 }+-- >>> let salt = Salt "abcdefghijklmnop"++-- -- >>> instance Arbitrary (PasswordHash PBKDF2) where arbitrary = hashPasswordWithSalt defaultParams <$> arbitrary <*> arbitrary++-- | Hash the 'Password' using the 'PBKDF2' hash algorithm+--+-- >>> hashPassword $ mkPassword "foobar"+-- PasswordHash {unPasswordHash = "sha512:25000:...:..."}+hashPassword :: MonadIO m => Password -> m (PasswordHash PBKDF2)+hashPassword = hashPasswordWithParams defaultParams++-- TODO: Add way to parse the following:+-- $pbkdf2-md5$29000$...$...+-- $pbkdf2$25000$...$... (SHA1)+-- $pbkdf2-sha256$29000$x9h7j/Ge8x6DMEao1VqrdQ$kra3R1wEnY8mPdDWOpTqOTINaAmZvRMcYd8u5OBQP9A+-- $pbkdf2-sha512$25000$LyWE0HrP2RsjZCxlDGFMKQ$1vC5Ohk2mCS9b6akqsEfgeb4l74SF8XjH.SljXf3dMLHdlY1GK9ojcCKts6/asR4aPqBmk74nCDddU3tvSCJvw++-- | Parameters used in the 'PBKDF2' hashing algorithm.+--+-- @since 2.0.0.0+data PBKDF2Params = PBKDF2Params {+ pbkdf2Salt :: Word32,+ -- ^ Bytes to randomly generate as a unique salt, default is __16__+ pbkdf2Algorithm :: PBKDF2Algorithm,+ -- ^ Which algorithm to use for hashing, default is __'PBKDF2_SHA512'__+ pbkdf2Iterations :: Word32,+ -- ^ Rounds to hash, default is __25,000__+ pbkdf2OutputLength :: Word32+ -- ^ Output key length in bytes, default is __64__+ --+ -- Limits are min: 1, max: /the amount of entropy of the hashing algorithm/.+ -- This is limited automatically to __16, 20, 32, 64__+ -- for __MD5, SHA1, SHA256, SHA512__, respectively.+} deriving (Eq, Show)++-- | Default parameters for the 'PBKDF2' algorithm.+--+-- >>> defaultParams+-- PBKDF2Params {pbkdf2Salt = 16, pbkdf2Algorithm = PBKDF2_SHA512, pbkdf2Iterations = 25000, pbkdf2OutputLength = 64}+--+-- @since 2.0.0.0+defaultParams :: PBKDF2Params+defaultParams = PBKDF2Params {+ pbkdf2Salt = 16,+ pbkdf2Algorithm = PBKDF2_SHA512,+ pbkdf2Iterations = 25 * 1000,+ pbkdf2OutputLength = 64+}++-- | Hash a password with the given 'PBKDF2Params' and also with the given 'Salt'+-- instead of a randomly generated salt using 'pbkdf2Salt' from 'PBKDF2Params'. (cf. 'hashPasswordWithParams')+-- Using 'hashPasswordWithSalt' is strongly __disadvised__ and 'hashPasswordWithParams' should be used instead.+-- /Never use a static salt in production applications!/+--+-- >>> let salt = Salt "abcdefghijklmnop"+-- >>> hashPasswordWithSalt defaultParams salt (mkPassword "foobar")+-- PasswordHash {unPasswordHash = "sha512:25000:YWJjZGVmZ2hpamtsbW5vcA==:JRElYYrOMe9OIV4LDxaLTgO9ho8fFBVofXoQcdngi7AcuH6Amvmlj2B0y6y1UtQciXXBepSCS+rpy8/vDDQvoA=="}+--+-- (Note that we use an explicit 'Salt' in the example above. This is so that the+-- example is reproducible, but in general you should use 'hashPassword'. 'hashPassword'+-- (and 'hashPasswordWithParams') generates a new 'Salt' everytime it is called.)+hashPasswordWithSalt :: PBKDF2Params -> Salt PBKDF2 -> Password -> PasswordHash PBKDF2+hashPasswordWithSalt params@PBKDF2Params{..} s@(Salt salt) pass =+ PasswordHash $ T.intercalate ":"+ [ algToText pbkdf2Algorithm+ , T.pack $ show pbkdf2Iterations+ , b64 salt+ , b64 key+ ]+ where+ b64 = Base64.encodeBase64+ key = hashPasswordWithSalt' params s pass++-- | Only for internal use+hashPasswordWithSalt' :: PBKDF2Params -> Salt PBKDF2 -> Password -> ByteString+hashPasswordWithSalt' PBKDF2Params{..} (Salt salt) (Password pass) =+ convert (pbkdf2Hash :: Bytes)+ where+ pbkdf2Hash = algToFunc pbkdf2Algorithm params (toBytes pass) (convert salt :: Bytes)+ params = PBKDF2.Parameters {+ PBKDF2.iterCounts = fromIntegral pbkdf2Iterations,+ PBKDF2.outputLength = fromIntegral $ maxOutputLength pbkdf2Algorithm pbkdf2OutputLength+ }++-- | Hash a password using the 'PBKDF2' algorithm with the given 'PBKDF2Params'.+--+-- __N.B.__: If you have any doubt in your knowledge of cryptography and/or the+-- 'PBKDF2' algorithm, please just use 'hashPassword'.+--+-- @since 2.0.0.0+hashPasswordWithParams :: MonadIO m => PBKDF2Params -> Password -> m (PasswordHash PBKDF2)+hashPasswordWithParams params pass = liftIO $ do+ salt <- Data.Password.Internal.newSalt . fromIntegral $ pbkdf2Salt params+ return $ hashPasswordWithSalt params salt pass++-- | Check a 'Password' against a 'PasswordHash' 'PBKDF2'.+--+-- Returns 'PasswordCheckSuccess' on success.+--+-- >>> let pass = mkPassword "foobar"+-- >>> passHash <- hashPassword pass+-- >>> checkPassword pass passHash+-- PasswordCheckSuccess+--+-- Returns 'PasswordCheckFail' if an incorrect 'Password' or 'PasswordHash' 'PBKDF2' is used.+--+-- >>> let badpass = mkPassword "incorrect-password"+-- >>> checkPassword badpass passHash+-- PasswordCheckFail+--+-- This should always fail if an incorrect password is given.+--+-- prop> \(Blind badpass) -> let correctPasswordHash = hashPasswordWithSalt testParams salt "foobar" in checkPassword badpass correctPasswordHash == PasswordCheckFail+checkPassword :: Password -> PasswordHash PBKDF2 -> PasswordCheck+checkPassword pass (PasswordHash passHash) =+ fromMaybe PasswordCheckFail $ do+ -- This step makes it possible to also check the following format:+ -- "pbkdf2:sha256:150000:etc.etc."+ let passHash' = fromMaybe passHash $ "pbkdf2:" `T.stripPrefix` passHash+ paramList = T.split (== ':') passHash'+ guard $ length paramList == 4+ let [ algT,+ iterationsT,+ salt64,+ hashedKey64 ] = paramList+ pbkdf2Algorithm <- textToAlg algT+ pbkdf2Iterations <- readT iterationsT+ salt <- from64 salt64+ hashedKey <- from64 hashedKey64+ let pbkdf2OutputLength = fromIntegral $ C8.length hashedKey+ producedKey = hashPasswordWithSalt' PBKDF2Params{..} (Salt salt) pass+ guard $ hashedKey == producedKey+ return PasswordCheckSuccess+ where+ pbkdf2Salt = 16+++-- | Type of algorithm to use for hashing PBKDF2 passwords.+--+-- N.B.: 'PBKDF2_MD5' and 'PBKDF2_SHA1' are not considered very secure.+data PBKDF2Algorithm =+ PBKDF2_MD5+ | PBKDF2_SHA1+ | PBKDF2_SHA256+ | PBKDF2_SHA512+ deriving (Eq, Show)++-- | Depending on the given algorithm limits the output length.+maxOutputLength :: PBKDF2Algorithm -> Word32 -> Word32+maxOutputLength = min . \case+ PBKDF2_MD5 -> 16+ PBKDF2_SHA1 -> 20+ PBKDF2_SHA256 -> 32+ PBKDF2_SHA512 -> 64++algToText :: PBKDF2Algorithm -> Text+algToText = \case+ PBKDF2_MD5 -> "md5"+ PBKDF2_SHA1 -> "sha1"+ PBKDF2_SHA256 -> "sha256"+ PBKDF2_SHA512 -> "sha512"++textToAlg :: Text -> Maybe PBKDF2Algorithm+textToAlg = \case+ "md5" -> Just PBKDF2_MD5+ "sha1" -> Just PBKDF2_SHA1+ "sha256" -> Just PBKDF2_SHA256+ "sha512" -> Just PBKDF2_SHA512+ _ -> Nothing++-- Which function to use, based on the given algorithm+algToFunc :: (ByteArrayAccess password, ByteArrayAccess salt, ByteArray hash)+ => PBKDF2Algorithm -> PBKDF2.Parameters -> password -> salt -> hash+algToFunc = \case+ PBKDF2_MD5 -> PBKDF2.generate (PBKDF2.prfHMAC Crypto.MD5)+ PBKDF2_SHA1 -> PBKDF2.fastPBKDF2_SHA1+ PBKDF2_SHA256 -> PBKDF2.fastPBKDF2_SHA256+ PBKDF2_SHA512 -> PBKDF2.fastPBKDF2_SHA512++-- | Generate a random 16-byte @PBKDF2@ salt+--+-- @since 2.0.0.0+newSalt :: MonadIO m => m (Salt PBKDF2)+newSalt = Data.Password.Internal.newSalt 16
+ src/Data/Password/Scrypt.hs view
@@ -0,0 +1,262 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-|+Module : Data.Password.Scrypt+Copyright : (c) Dennis Gosnell, 2019; Felix Paulusma, 2020+License : BSD-style (see LICENSE file)+Maintainer : cdep.illabout@gmail.com+Stability : experimental+Portability : POSIX++= scrypt++The @scrypt@ algorithm is a fairly new one. First published+in 2009, but published by the IETF in 2016 as <https://tools.ietf.org/html/rfc7914 RFC 7914>.+Originally used for the Tarsnap backup service, it is+designed to be costly by requiring large amounts of memory.++== Other algorithms++@scrypt@ does increase the memory requirement in contrast to+@"Bcrypt"@ and @"PBKDF2"@, but it turns out it is not as optimal+as it could be, and thus others have set out to search for other+algorithms that do fulfill on their promises. @"Argon2"@ seems+to be the winner in that search.++That is not to say using @scrypt@ somehow means your passwords+won't be properly protected. The cryptography is sound and+thus is fine for protection against brute-force attacks.+Because of the memory cost, it is generally advised to use+@"Bcrypt"@ if you're not sure this might be a+problem on your system.+-}++module Data.Password.Scrypt (+ -- * Algorithm+ Scrypt+ -- * Plain-text Password+ , Password+ , mkPassword+ -- * Hash Passwords (scrypt)+ , hashPassword+ , PasswordHash(..)+ -- * Verify Passwords (scrypt)+ , checkPassword+ , PasswordCheck(..)+ -- * Hashing Manually (scrypt)+ , hashPasswordWithParams+ , defaultParams+ , ScryptParams(..)+ -- ** Hashing with salt (DISADVISED)+ --+ -- | Hashing with a set 'Salt' is almost never what you want+ -- to do. Use 'hashPassword' or 'hashPasswordWithParams' to have+ -- automatic generation of randomized salts.+ , hashPasswordWithSalt+ , newSalt+ , Salt(..)+ -- * Unsafe debugging function to show a Password+ , unsafeShowPassword+ , -- * Setup for doctests.+ -- $setup+ ) where++import Control.Monad (guard)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Crypto.KDF.Scrypt as Scrypt+import Data.ByteArray (Bytes, convert)+import Data.ByteString (ByteString)+import Data.ByteString.Base64 (encodeBase64)+import qualified Data.ByteString.Char8 as C8 (length)+import Data.Maybe (fromMaybe)+import qualified Data.Text as T (intercalate, split)+import Data.Word (Word32)++import Data.Password (+ PasswordCheck(..)+ , PasswordHash(..)+ , Salt(..)+ , mkPassword+ , unsafeShowPassword+ )+import Data.Password.Internal (Password(..), from64, readT, showT, toBytes)+import qualified Data.Password.Internal (newSalt)++-- | Phantom type for __Argon2__+--+-- @since 2.0.0.0+data Scrypt++-- $setup+-- >>> :set -XFlexibleInstances+-- >>> :set -XOverloadedStrings+--+-- Import needed libraries.+--+-- >>> import Data.Password+-- >>> import Data.ByteString (pack)+-- >>> import Test.QuickCheck (Arbitrary(arbitrary), Blind(Blind), vector)+-- >>> import Test.QuickCheck.Instances.Text ()+--+-- >>> instance Arbitrary (Salt a) where arbitrary = Salt . pack <$> vector 32+-- >>> instance Arbitrary Password where arbitrary = fmap Password arbitrary+-- >>> let salt = Salt "abcdefghijklmnopqrstuvwxyz012345"+-- >>> let testParams = defaultParams {scryptRounds = 10}++-- -- >>> instance Arbitrary (PasswordHash Scrypt) where arbitrary = hashPasswordWithSalt testParams <$> arbitrary <*> arbitrary++-- | Hash the 'Password' using the 'Scrypt' hash algorithm+--+-- >>> hashPassword $ mkPassword "foobar"+-- PasswordHash {unPasswordHash = "14|8|1|...|..."}+hashPassword :: MonadIO m => Password -> m (PasswordHash Scrypt)+hashPassword = hashPasswordWithParams defaultParams++-- TODO: Add way to parse the following. From [https://hashcat.net/wiki/doku.php?id=example_hashes]+-- SCRYPT:1024:1:1:MDIwMzMwNTQwNDQyNQ==:5FW+zWivLxgCWj7qLiQbeC8zaNQ+qdO0NUinvqyFcfo=++-- | Parameters used in the 'Scrypt' hashing algorithm.+--+-- @since 2.0.0.0+data ScryptParams = ScryptParams {+ scryptSalt :: Word32,+ -- ^ Bytes to randomly generate as a unique salt, default is __32__+ scryptRounds :: Word32,+ -- ^ log2(N) rounds to hash, default is __14__ (i.e. 2^14 rounds)+ scryptBlockSize :: Word32,+ -- ^ Block size, default is __8__+ --+ -- Limits are min: @1@, and max: @scryptBlockSize * scryptParallelism < 2 ^ 30@+ scryptParallelism :: Word32,+ -- ^ Parallelism factor, default is __1__+ --+ -- Limits are min: @0@, and max: @scryptBlockSize * scryptParallelism < 2 ^ 30@+ scryptOutputLength :: Word32+ -- ^ Output key length in bytes, default is __64__+} deriving (Eq, Show)++-- | Default parameters for the 'Scrypt' algorithm.+--+-- >>> defaultParams+-- ScryptParams {scryptSalt = 32, scryptRounds = 14, scryptBlockSize = 8, scryptParallelism = 1, scryptOutputLength = 64}+--+-- @since 2.0.0.0+defaultParams :: ScryptParams+defaultParams = ScryptParams {+ scryptSalt = 32,+ scryptRounds = 14,+ scryptBlockSize = 8,+ scryptParallelism = 1,+ scryptOutputLength = 64+}++-- | Hash a password with the given 'ScryptParams' and also with the given 'Salt'+-- instead of a randomly generated salt using 'scryptSalt' from 'ScryptParams'.+-- Using 'hashPasswordWithSalt' is strongly __disadvised__ and 'hashPasswordWithParams'+-- should be used instead. /Never use a static salt in production applications!/+--+-- The resulting 'PasswordHash' has the parameters used to hash it, as well as the+-- 'Salt' appended to it, separated by @|@.+--+-- The input 'Salt' and resulting 'PasswordHash' are both base64 encoded.+--+-- >>> let salt = Salt "abcdefghijklmnopqrstuvwxyz012345"+-- >>> hashPasswordWithSalt defaultParams salt (mkPassword "foobar")+-- PasswordHash {unPasswordHash = "14|8|1|YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU=|nENDaqWBmPKapAqQ3//H0iBImweGjoTqn5SvBS8Mc9FPFbzq6w65maYPZaO+SPamVZRXQjARQ8Y+5rhuDhjIhw=="}+--+-- (Note that we use an explicit 'Salt' in the example above. This is so that the+-- example is reproducible, but in general you should use 'hashPassword'. 'hashPassword'+-- generates a new 'Salt' everytime it is called.)+hashPasswordWithSalt :: ScryptParams -> Salt Scrypt -> Password -> PasswordHash Scrypt+hashPasswordWithSalt params@ScryptParams{..} s@(Salt salt) pass =+ PasswordHash $ T.intercalate "|"+ [ showT scryptRounds+ , showT scryptBlockSize+ , showT scryptParallelism+ , encodeBase64 salt+ , encodeBase64 key+ ]+ where+ key = hashPasswordWithSalt' params s pass++-- | Only for internal use+hashPasswordWithSalt' :: ScryptParams -> Salt Scrypt -> Password -> ByteString+hashPasswordWithSalt' ScryptParams{..} (Salt salt) (Password pass) =+ convert (scryptHash :: Bytes)+ where+ scryptHash = Scrypt.generate params (toBytes pass) (convert salt :: Bytes)+ params = Scrypt.Parameters {+ n = 2 ^ scryptRounds,+ r = fromIntegral scryptBlockSize,+ p = fromIntegral scryptParallelism,+ outputLength = fromIntegral scryptOutputLength+ }+++-- | Hash a password using the 'Scrypt' algorithm with the given 'ScryptParams'.+--+-- __N.B.__: If you have any doubt in your knowledge of cryptography and/or the+-- 'Scrypt' algorithm, please just use 'hashPassword'.+--+-- Advice for setting the parameters:+--+-- * Memory used is about: @(2 ^ 'scryptRounds') * 'scryptBlockSize' * 128@+-- * Increasing 'scryptBlockSize' and 'scryptRounds' will increase CPU time+-- and memory used.+-- * Increasing 'scryptParallelism' will increase CPU time. (since this+-- implementation, like most, runs the 'scryptParallelism' parameter in+-- sequence, not in parallel)+--+-- @since 2.0.0.0+hashPasswordWithParams :: MonadIO m => ScryptParams -> Password -> m (PasswordHash Scrypt)+hashPasswordWithParams params pass = liftIO $ do+ salt <- Data.Password.Internal.newSalt saltLength+ return $ hashPasswordWithSalt params salt pass+ where+ saltLength = fromIntegral $ scryptSalt params++-- | Check a 'Password' against a 'PasswordHash' 'Scrypt'.+--+-- Returns 'PasswordCheckSuccess' on success.+--+-- >>> let pass = mkPassword "foobar"+-- >>> passHash <- hashPassword pass+-- >>> checkPassword pass passHash+-- PasswordCheckSuccess+--+-- Returns 'PasswordCheckFail' if an incorrect 'Password' or 'PasswordHash' 'Scrypt' is used.+--+-- >>> let badpass = mkPassword "incorrect-password"+-- >>> checkPassword badpass passHash+-- PasswordCheckFail+--+-- This should always fail if an incorrect password is given.+--+-- prop> \(Blind badpass) -> let correctPasswordHash = hashPasswordWithSalt testParams salt "foobar" in checkPassword badpass correctPasswordHash == PasswordCheckFail+checkPassword :: Password -> PasswordHash Scrypt -> PasswordCheck+checkPassword pass (PasswordHash passHash) =+ fromMaybe PasswordCheckFail $ do+ let paramList = T.split (== '|') passHash+ guard $ length paramList == 5+ let [ scryptRoundsT,+ scryptBlockSizeT,+ scryptParallelismT,+ salt64,+ hashedKey64 ] = paramList+ scryptRounds <- readT scryptRoundsT+ scryptBlockSize <- readT scryptBlockSizeT+ scryptParallelism <- readT scryptParallelismT+ salt <- from64 salt64+ hashedKey <- from64 hashedKey64+ let scryptOutputLength = fromIntegral $ C8.length hashedKey+ producedKey = hashPasswordWithSalt' ScryptParams{..} (Salt salt) pass+ guard $ hashedKey == producedKey+ return PasswordCheckSuccess+ where+ scryptSalt = 32 -- only here because of warnings++-- | Generate a random 32-byte @scrypt@ salt+--+-- @since 2.0.0.0+newSalt :: MonadIO m => m (Salt Scrypt)+newSalt = Data.Password.Internal.newSalt 32
test/doctest/doctest.hs view
@@ -1,7 +1,14 @@--module Main (main) where+module Main where -import Test.DocTest+import Build_doctests (flags, pkgs, module_sources)+-- import Data.Foldable (traverse_)+import System.Environment.Compat (unsetEnv)+import Test.DocTest (doctest) main :: IO ()-main = doctest ["src"]+main = do+ -- traverse_ putStrLn args+ unsetEnv "GHC_ENVIRONMENT"+ doctest args+ where+ args = flags ++ pkgs ++ module_sources
+ test/tasty/Argon2.hs view
@@ -0,0 +1,29 @@+module Argon2 where++import Test.Tasty+import Test.QuickCheck.Instances.Text ()++import Data.Password.Argon2++import Internal+++testArgon2 :: TestTree+testArgon2 = testGroup "Argon2"+ [ testCorrectPassword "Argon2 (hashPassword)" hashFast checkPassword+ , testIncorrectPassword "Argon2 (hashPassword) fail" hashFast checkPassword+ , testWithSalt "Argon2 (hashPasswordWithSalt)"+ (hashPasswordWithSalt fastParams)+ checkPassword+ , testWithParams "Argon2 (Argon2i)" $ fastParams{ argon2Variant = Argon2i }+ , testWithParams "Argon2 (Argon2d)" $ fastParams{ argon2Variant = Argon2d }+ ]+ where+ testWithParams s params =+ testWithSalt s (hashPasswordWithSalt params) checkPassword+ hashFast = hashPasswordWithParams fastParams+ fastParams =+ defaultParams{+ argon2MemoryCost = 2 ^ (8 :: Int),+ argon2TimeCost = 1+ }
+ test/tasty/Bcrypt.hs view
@@ -0,0 +1,19 @@+module Bcrypt where++import Data.Text (Text)+import Test.Tasty+import Test.Tasty.QuickCheck+import Test.QuickCheck.Instances.Text ()++import Data.Password+import Data.Password.Bcrypt++import Internal (testCorrectPassword, testIncorrectPassword_, testWithSalt)+++testBcrypt :: TestTree+testBcrypt = testGroup "bcrypt"+ [ testCorrectPassword "Bcrypt (hashPassword)" (hashPasswordWithParams 4) checkPassword+ , testIncorrectPassword_ "Bcrypt (hashPassword) fail" (hashPasswordWithParams 4) checkPassword+ , testWithSalt "Bcrypt (hashPasswordWithSalt)" (hashPasswordWithSalt 4) checkPassword+ ]
+ test/tasty/Internal.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-orphans #-}+module Internal where++import Data.ByteArray (pack)+import Data.Text (Text)+import Test.Tasty+import Test.Tasty.QuickCheck+import Test.QuickCheck.Instances.Text ()++import Data.Password+++testCorrectPassword :: String+ -> (Password -> IO (PasswordHash a))+ -> (Password -> PasswordHash a -> PasswordCheck)+ -> TestTree+testCorrectPassword s hashF checkF = testProperty s $+ \pass -> run10 $ do+ let pw = mkPassword pass+ hpw <- hashF pw+ return $ checkF pw hpw === PasswordCheckSuccess++testIncorrectPassword :: String+ -> (Password -> IO (PasswordHash a))+ -> (Password -> PasswordHash a -> PasswordCheck)+ -> TestTree+testIncorrectPassword s hashF checkF =+ testProperty s $ testIncorrectPassword' hashF checkF++-- Similar to 'testIncorrectPassword', but exempts the comparison of+-- "" and "\NUL", since 'bcrypt' and 'PBKDF2' match those as well.+testIncorrectPassword_ :: String+ -> (Password -> IO (PasswordHash a))+ -> (Password -> PasswordHash a -> PasswordCheck)+ -> TestTree+testIncorrectPassword_ s hashF checkF =+ testProperty s $ \pass pass2 ->+ not (all isEmpty [pass, pass2]) ==>+ testIncorrectPassword' hashF checkF pass pass2+ where+ isEmpty c = c `elem` ["", "\NUL"]++testIncorrectPassword' :: (Password -> IO (PasswordHash a))+ -> (Password -> PasswordHash a -> PasswordCheck)+ -> Text -> Text -> Property+testIncorrectPassword' hashF checkF pass pass2 = run10 $ do+ let pw = mkPassword pass+ pw2 = mkPassword pass2+ result = if pass == pass2 then PasswordCheckSuccess+ else PasswordCheckFail+ hpw <- hashF pw+ return $ checkF pw2 hpw === result++testWithSalt :: String+ -> (Salt a -> Password -> PasswordHash a)+ -> (Password -> PasswordHash a -> PasswordCheck)+ -> TestTree+testWithSalt s hashWithSalt checkF = testProperty s $+ \pass salt -> withMaxSuccess 10 $+ let pw = mkPassword pass+ hpw = hashWithSalt salt pw+ in checkF pw hpw === PasswordCheckSuccess++run10 :: Testable prop => IO prop -> Property+run10 = withMaxSuccess 10 . ioProperty++instance Arbitrary (Salt a) where+ arbitrary = Salt . pack <$> vector 16
+ test/tasty/PBKDF2.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+module PBKDF2 where++import Crypto.Hash.Algorithms as Crypto (HashAlgorithm, SHA1(..), SHA256(..), SHA512(..))+import Crypto.KDF.PBKDF2 as PBKDF2+import Data.ByteString (ByteString)+#if !MIN_VERSION_base(4,13,0)+import Data.Semigroup ((<>))+#endif+import Test.Tasty+import Test.Tasty.QuickCheck+import Test.QuickCheck.Instances.ByteString ()+import Test.QuickCheck.Instances.Text ()++import Data.Password.PBKDF2++import Internal++_10k :: Num a => a+_10k = 10 * 1000++testPBKDF2 :: TestTree+testPBKDF2 = testGroup "PBKDF2"+ [ testIt "PBKDF2 (hashPassword)" $ _10k defaultParams -- This is PBKDF2_SHA512+ , testIncorrectPassword_+ "PBKDF2 (hashPassword) fail"+ (hashPasswordWithParams $ _10k defaultParams)+ checkPassword+ , testWithSalt+ "PBKDF2 (hashPasswordWithSalt)"+ (hashPasswordWithSalt $ _10k defaultParams)+ checkPassword+ , testIt "PBKDF2 (md5)" (defaultParams{ pbkdf2Algorithm = PBKDF2_MD5, pbkdf2Iterations = 5000 })+ , testIt "PBKDF2 (sha1)" (_10k defaultParams{ pbkdf2Algorithm = PBKDF2_SHA1 })+ , testIt "PBKDF2 (sha256)" (_10k defaultParams{ pbkdf2Algorithm = PBKDF2_SHA256 })+ , testFast Crypto.SHA1 20 PBKDF2.fastPBKDF2_SHA1+ , testFast Crypto.SHA256 32 PBKDF2.fastPBKDF2_SHA256+ , testFast Crypto.SHA512 64 PBKDF2.fastPBKDF2_SHA512+ -- Check to see if a hash with "pbkdf2:" prefixed also works+ , testCorrectPassword+ "PBKDF2 (pbkdf2:sha-...)"+ (hashPasswordWithParams $ _10k defaultParams)+ (\pass (PasswordHash hash) -> checkPassword pass . PasswordHash $ "pbkdf2:" <> hash)+ ]+ where+ testIt s params = testCorrectPassword s (hashPasswordWithParams params) checkPassword+ _10k params = params{ pbkdf2Iterations = 10 * 1000 }++testFast :: (HashAlgorithm a, Show a)+ => a+ -> Int+ -> (Parameters -> ByteString -> ByteString -> ByteString)+ -> TestTree+testFast alg i f = testProperty s $ \pass salt -> run10 $+ return $ f params pass salt ===+ PBKDF2.generate (PBKDF2.prfHMAC alg) params pass salt+ where+ params = cryptoParams i+ s = sAlg ++ " HMAC == fast_" ++ sAlg+ sAlg = show alg++cryptoParams :: Int -> PBKDF2.Parameters+cryptoParams i = PBKDF2.Parameters {+ PBKDF2.iterCounts = 5000,+ PBKDF2.outputLength = i+ }
+ test/tasty/Scrypt.hs view
@@ -0,0 +1,37 @@+module Scrypt where++import Data.Maybe (fromJust)+import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import Test.Tasty+import Test.Tasty.QuickCheck+import Test.QuickCheck.Instances.Text ()++import qualified Crypto.Scrypt as Scrypt+import Data.Password+import Data.Password.Scrypt++import Internal+++testScrypt :: TestTree+testScrypt = testGroup "scrypt"+ [ testCorrectPassword "Scrypt (hashPassword)" hash8Rounds checkPassword+ , testIncorrectPassword "Scrypt (hashPassword) fail" hash8Rounds checkPassword+ , testWithSalt "Scrypt (hashPasswordWithSalt)"+ (hashPasswordWithSalt defaultParams{ scryptRounds = 8 })+ checkPassword+ , testProperty "scrypt <-> cryptonite" $ withMaxSuccess 10 checkScrypt+ ]+ where+ hash8Rounds = hashPasswordWithParams defaultParams{ scryptRounds = 8 }++checkScrypt :: Text -> Property+checkScrypt pass = ioProperty $ do+ s@(Scrypt.Salt salt) <- Scrypt.newSalt+ let params = fromJust $ Scrypt.scryptParams 8 8 1+ Scrypt.EncryptedPass scryptHash =+ Scrypt.encryptPass params s $ Scrypt.Pass $ encodeUtf8 pass+ PasswordHash ourHash =+ hashPasswordWithSalt defaultParams{ scryptRounds = 8 } (Salt salt) $ mkPassword pass+ return $ scryptHash === encodeUtf8 ourHash
test/tasty/Spec.hs view
@@ -1,27 +1,21 @@-import Crypto.Scrypt (Salt(..))-import Data.Text.Encoding (encodeUtf8) import Test.Tasty import Test.Tasty.QuickCheck-import Test.QuickCheck.Instances.Text ()+import Test.Tasty.Runners (NumThreads(..)) import Data.Password +import Argon2+import Bcrypt+import PBKDF2+import Scrypt+ main :: IO ()-main = defaultMain $ testGroup "Password"- [ testProperty "Pass" $ \pass ->- unsafeShowPasswordText (mkPass pass) === pass- , testProperty "Scrypt (hashPass)" $ \pass -> ioProperty $ do- let pw = mkPass pass- hpw <- hashPass pw- return $ checkPass pw hpw === PassCheckSuccess- , testProperty "Scrypt (hashPass) fail" $ \pass pass2 -> ioProperty $ do- let pw = mkPass pass- pw2 = mkPass pass2- result = if pass == pass2 then PassCheckSuccess else PassCheckFail- hpw <- hashPass pw- return $ checkPass pw2 hpw === result- , testProperty "Scrypt (hashPassWithSalt)" $ \pass salt ->- let pw = mkPass pass- hpw = hashPassWithSalt (Salt $ encodeUtf8 salt) pw- in checkPass pw hpw === PassCheckSuccess- ]+main = defaultMain $ localOption (NumThreads 1) $+ testGroup "Password"+ [ testProperty "Password" $ \pass ->+ unsafeShowPassword (mkPassword pass) === pass+ , testArgon2+ , testBcrypt+ , testPBKDF2+ , testScrypt+ ]