password 0.1.0.1 → 1.0.0.0
raw patch · 4 files changed
+98/−26 lines, 4 filesdep +tastydep +tasty-quickcheckPVP ok
version bump matches the API change (PVP)
Dependencies added: tasty, tasty-quickcheck
API changes (from Hackage documentation)
- Data.Password: Pass :: Text -> Pass
- Data.Password: PassCheckSucc :: PassCheck
- Data.Password: [unPass] :: Pass -> Text
- Data.Password: instance GHC.Read.Read Data.Password.Pass
- Data.Password: newtype Pass
+ Data.Password: PassCheckSuccess :: PassCheck
+ Data.Password: data Pass
+ Data.Password: instance GHC.Show.Show Data.Password.Pass
+ Data.Password: mkPass :: Text -> Pass
- Data.Password: hashPassWithSalt :: Pass -> Salt -> PassHash
+ Data.Password: hashPassWithSalt :: Salt -> Pass -> PassHash
- Data.Password: newSalt :: IO Salt
+ Data.Password: newSalt :: MonadIO m => m Salt
Files
- ChangeLog.md +19/−0
- password.cabal +7/−1
- src/Data/Password.hs +46/−24
- test/tasty/Spec.hs +26/−1
ChangeLog.md view
@@ -1,5 +1,24 @@ # Changelog for password +## 1.0.0.0++- `hashPassWithSalt` has switched function arguments for better currying.+ [#6](https://github.com/cdepillabout/password/pull/6)+ Although be warned that multiple passwords+ [should not be hashed with the same salt](https://github.com/cdepillabout/password/pull/6#discussion_r370455681).+- Removed `Read` instance from `Pass` and added `Show` instance.+ [#6](https://github.com/cdepillabout/password/pull/6)+ See [#5](https://github.com/cdepillabout/password/issues/5#issuecomment-576958351)+ for justification of this.+- `newSalt` is now `MonadIO m` instead of `IO`.+ [#6](https://github.com/cdepillabout/password/pull/6)+- `PassCheckSucc` has been renamed to `PassCheckSuccess`.+ [#6](https://github.com/cdepillabout/password/pull/6)+- Hide data constructor from `Pass` and add the `mkPass` function to construct a `Pass`.+ [#6](https://github.com/cdepillabout/password/pull/6)+- Thanks to Felix Paulusma ([@Vlix](https://github.com/Vlix)) for the above+ changes!+ ## 0.1.0.1 - Small fix to make sure the doctests build with stack.
password.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12 name: password-version: 0.1.0.1+version: 1.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.@@ -54,6 +54,7 @@ , password , QuickCheck , quickcheck-instances+ , scrypt default-language: Haskell2010 @@ -71,5 +72,10 @@ build-depends: base >=4.7 && <5 , password+ , quickcheck-instances+ , scrypt+ , tasty+ , tasty-quickcheck+ , text default-language: Haskell2010
src/Data/Password.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-} {-| Module : Data.Password@@ -32,7 +31,8 @@ module Data.Password ( -- * Plaintext Password- Pass(..)+ Pass+ , mkPass -- * Hashed Password , PassHash(..) , Salt(..)@@ -52,9 +52,9 @@ import Control.Monad.IO.Class (MonadIO(liftIO)) import Crypto.Scrypt (EncryptedPass(..), Salt(..), defaultParams, encryptPass,- newSalt, verifyPass')+ verifyPass') import qualified Crypto.Scrypt as Scrypt-import Data.String (IsString)+import Data.String (IsString(..)) import Data.Text (Text, unpack) import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Text.Encoding.Error (lenientDecode)@@ -84,15 +84,33 @@ -- -- 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+-- You should be careful with 'Pass'. Make sure not to write it to logs or -- store it in a database.-newtype Pass = Pass- { unPass :: Text- } deriving (IsString, Read)+--+-- 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 $ Pass "foobar"+-- >>> unsafeShowPasswordText $ mkPass "foobar" -- "foobar" -- -- You should generally not use this function.@@ -114,7 +132,7 @@ -- | Convert an "Crypto.Scrypt".'Scrypt.Pass' to our 'Pass' type. ----- >>> passToScryptPass $ Pass "foobar"+-- >>> passToScryptPass "foobar" -- Pass {getPass = "foobar"} passToScryptPass :: Pass -> Scrypt.Pass passToScryptPass (Pass pass) = Scrypt.Pass $ encodeUtf8 pass@@ -165,12 +183,12 @@ -- | Just like 'hashPassWithSalt', but generate a new 'Salt' everytime with a -- call to 'newSalt'. ----- >>> hashPass (Pass "foobar")+-- >>> hashPass $ mkPass "foobar" -- PassHash {unPassHash = "14|8|1|...|..."} hashPass :: MonadIO m => Pass -> m PassHash hashPass pass = do salt <- liftIO newSalt- pure $ hashPassWithSalt pass salt+ pure $ hashPassWithSalt salt pass -- | Hash a password with the given 'Salt'. --@@ -180,7 +198,7 @@ -- The input 'Salt' and resulting 'PassHash' are both byte-64 encoded. -- -- >>> let salt = Salt "abcdefghijklmnopqrstuvwxyz012345"--- >>> hashPassWithSalt (Pass "foobar") salt+-- >>> 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@@ -188,16 +206,16 @@ -- generates a new 'Salt' everytime it is called.) -- -- This function uses the hash function from the scrypt package: 'encryptPass'.-hashPassWithSalt :: Pass -> Salt -> PassHash-hashPassWithSalt pass salt =+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- = PassCheckSucc- -- ^ The password check was successful. The plain-text password matches the+ = 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@@ -206,28 +224,32 @@ -- | Check a 'Pass' against a 'PassHash'. ----- Returns 'PassCheckSucc' on success.+-- Returns 'PassCheckSuccess' on success. -- -- >>> let salt = Salt "abcdefghijklmnopqrstuvwxyz012345"--- >>> let pass = Pass "foobar"--- >>> let passHash = hashPassWithSalt pass salt+-- >>> let pass = mkPass "foobar"+-- >>> let passHash = hashPassWithSalt salt pass -- >>> checkPass pass passHash--- PassCheckSucc+-- PassCheckSuccess -- -- Returns 'PassCheckFail' If an incorrect 'Pass' or 'PassHash' is used. ----- >>> let badpass = Pass "incorrect-password"+-- >>> 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 (Pass "foobar") salt in checkPass badpass correctPassHash == PassCheckFail+-- 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 PassCheckSucc+ then PassCheckSuccess else PassCheckFail++-- | Generate a random 32-byte salt.+newSalt :: MonadIO m => m Salt+newSalt = liftIO Scrypt.newSalt -- TODO: Create code for checking that plaintext passwords conform to some sort of -- password policy.
test/tasty/Spec.hs view
@@ -1,2 +1,27 @@+import Crypto.Scrypt (Salt(..))+import Data.Text.Encoding (encodeUtf8)+import Test.Tasty+import Test.Tasty.QuickCheck+import Test.QuickCheck.Instances.Text ()++import Data.Password+ main :: IO ()-main = putStrLn "Test suite not yet implemented"+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+ ]