diff --git a/hie.yaml b/hie.yaml index 4b69878..624a6d1 100644 --- a/hie.yaml +++ b/hie.yaml @@ -16,10 +16,27 @@ cradle: - path: "./password-instances/src" component: "password-instances:lib" - - path: "./password-instances/test/doctest" - component: "password-instances:test:doctests" - - path: "./password-instances/test/tasty" - component: "password-instances:test:password-instances-tasty" + + - path: "./password-aeson/src" + component: "password-aeson:lib" + - path: "./password-aeson/test/doctest" + component: "password-aeson:test:doctests" + - path: "./password-aeson/test/tasty" + component: "password-aeson:test:password-aeson-tasty" + + - path: "./password-http-api-data/src" + component: "password-http-api-data:lib" + - path: "./password-http-api-data/test/doctest" + component: "password-http-api-data:test:doctests" + - path: "./password-http-api-data/test/tasty" + component: "password-http-api-data:test:password-http-api-data-tasty" + + - path: "./password-persistent/src" + component: "password-persistent:lib" + - path: "./password-persistent/test/doctest" + component: "password-persistent:test:doctests" + - path: "./password-persistent/test/tasty" + component: "password-persistent:test:password-persistent-tasty" - path: "./password-cli/app" component: "password-cli:exe:password-cli" diff --git a/password-aeson/ChangeLog.md b/password-aeson/ChangeLog.md new file mode 100644 index 0000000..c7815f6 --- /dev/null +++ b/password-aeson/ChangeLog.md @@ -0,0 +1,5 @@ +# Changelog for `password-aeson` + +## 0.1.0.0 + +- Split from `password-instances`. diff --git a/password-aeson/LICENSE b/password-aeson/LICENSE new file mode 100644 index 0000000..750d621 --- /dev/null +++ b/password-aeson/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Dennis Gosnell, 2019; Felix Paulusma, 2020 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * 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 +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/password-aeson/README.md b/password-aeson/README.md new file mode 100644 index 0000000..710a3b6 --- /dev/null +++ b/password-aeson/README.md @@ -0,0 +1,11 @@ +# password-aeson + +[![Build Status](https://github.com/cdepillabout/password/workflows/password/badge.svg)](http://github.com/cdepillabout/password) +[![Hackage](https://img.shields.io/hackage/v/password-aeson.svg)](https://hackage.haskell.org/package/password-aeson) +[![Stackage LTS](http://stackage.org/package/password-aeson/badge/lts)](http://stackage.org/lts/package/password-aeson) +[![Stackage Nightly](http://stackage.org/package/password-aeson/badge/nightly)](http://stackage.org/nightly/package/password-aeson) +[![BSD3 license](https://img.shields.io/badge/license-BSD3-blue.svg)](./LICENSE) + +This package provides `aeson` typeclass instances for the plain-text password +and hashed password datatypes from the +[password](https://hackage.haskell.org/package/password) package. diff --git a/password-aeson/Setup.hs b/password-aeson/Setup.hs new file mode 100644 index 0000000..8ec54a0 --- /dev/null +++ b/password-aeson/Setup.hs @@ -0,0 +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 diff --git a/password-aeson/password-aeson.cabal b/password-aeson/password-aeson.cabal new file mode 100644 index 0000000..ca70457 --- /dev/null +++ b/password-aeson/password-aeson.cabal @@ -0,0 +1,88 @@ +cabal-version: 1.12 + +name: password-aeson +version: 0.1.0.0 +category: Security +synopsis: aeson typeclass instances for password package +description: A library providing typeclass instances for aeson for the types from the password package. +homepage: https://github.com/cdepillabout/password/tree/master/password-aeson#readme +bug-reports: https://github.com/cdepillabout/password/issues +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: Custom +extra-source-files: + README.md + ChangeLog.md + +source-repository head + type: git + location: https://github.com/cdepillabout/password + +custom-setup + setup-depends: + base + , Cabal + , cabal-doctest >=1.0.6 && <1.1 + +library + hs-source-dirs: + src + exposed-modules: + Data.Password.Aeson + other-modules: + Paths_password_aeson + build-depends: + base >= 4.9 && < 5 + , aeson >= 0.2 + , password-types < 2 + , text + ghc-options: + -Wall + default-language: + Haskell2010 + +test-suite doctests + type: + exitcode-stdio-1.0 + hs-source-dirs: + test/doctest + main-is: + doctest.hs + ghc-options: + -threaded -rtsopts -with-rtsopts=-N + build-depends: + base >=4.9 && <5 + , base-compat + , doctest + , password + , password-aeson + , QuickCheck + , quickcheck-instances + , template-haskell + default-language: + Haskell2010 + +test-suite password-aeson-tasty + type: + exitcode-stdio-1.0 + hs-source-dirs: + test/tasty + main-is: + Spec.hs + ghc-options: + -threaded -rtsopts -with-rtsopts=-N + build-depends: + base >=4.9 && <5 + , password-aeson + , password-types + , aeson + , quickcheck-instances + , tasty + , tasty-hunit + , tasty-quickcheck + , text + default-language: + Haskell2010 diff --git a/password-aeson/src/Data/Password/Aeson.hs b/password-aeson/src/Data/Password/Aeson.hs new file mode 100644 index 0000000..b1ad192 --- /dev/null +++ b/password-aeson/src/Data/Password/Aeson.hs @@ -0,0 +1,83 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +{-| +Module : Data.Password.Aeson +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 additional typeclass instances +for 'Password' and 'PasswordHash'. + +See the "Data.Password.Types" module for more information. +-} + +module Data.Password.Aeson + ( FromJSON (..), + ToJSON (..), + ExposedPassword (..), + ) where + +import Data.Aeson (FromJSON(..), ToJSON(..)) +import Data.Password.Types +import GHC.TypeLits (TypeError, ErrorMessage(..)) + +-- $setup +-- >>> :set -XOverloadedStrings +-- >>> :set -XDataKinds +-- +-- Import needed functions. +-- +-- >>> import Data.Aeson (decode) +-- >>> import Data.Password.Bcrypt (Salt(..), hashPasswordWithSalt, unsafeShowPassword) + +-- | This instance allows a 'Password' to be created from a JSON blob. +-- +-- >>> let maybePassword = decode "\"foobar\"" :: Maybe Password +-- >>> fmap unsafeShowPassword maybePassword +-- Just "foobar" +-- +-- There is no instance for 'ToJSON' for 'Password' because we don't want to +-- accidentally encode a plain-text 'Password' to JSON and send it to the end-user. +-- +-- Similarly, there is no 'ToJSON' and 'FromJSON' instance for 'PasswordHash' +-- because we don't want to accidentally send the password hash to the end +-- user. +instance FromJSON Password where + parseJSON = fmap mkPassword . parseJSON + +type ErrMsg = 'Text "Warning! Tried to convert plain-text Password to JSON!" + ':$$: 'Text " This is likely a security leak. Please make sure whether this was intended." + ':$$: 'Text " If this is intended, please use 'unsafeShowPassword' before converting to JSON" + ':$$: 'Text "" + +-- | Type error! Do not use 'toJSON' on a 'Password'! +instance TypeError ErrMsg => ToJSON Password where + toJSON = error "unreachable" + +-- | WARNING: DO NOT USE UNLESS ABSOLUTELY NECESSARY! +-- +-- Using this newtype will allow your plain text password to be turned into +-- JSON. Keep this type tightly bound to only the section where you want to +-- expose the `Password`, since it's easy for a bigger type that contains +-- this `ExposedPassword` to be logged or printed as JSON, and now you've +-- accidentally leaked passwords in your logs or database. +newtype ExposedPassword = ExposedPassword Password + deriving newtype (FromJSON) + +instance ToJSON ExposedPassword where + toJSON (ExposedPassword p) = toJSON $ unsafeShowPassword p + +deriving newtype instance FromJSON (PasswordHash a) + +deriving newtype instance ToJSON (PasswordHash a) diff --git a/password-instances/test/doctest/doctest.hs b/password-aeson/test/doctest/doctest.hs similarity index 100% rename from password-instances/test/doctest/doctest.hs rename to password-aeson/test/doctest/doctest.hs diff --git a/password-instances/test/tasty/Spec.hs b/password-aeson/test/tasty/Spec.hs similarity index 61% rename from password-instances/test/tasty/Spec.hs rename to password-aeson/test/tasty/Spec.hs index b7889c7..82217ae 100644 --- a/password-instances/test/tasty/Spec.hs +++ b/password-aeson/test/tasty/Spec.hs @@ -3,22 +3,18 @@ import Data.Aeson import Data.Aeson.Types (parseMaybe) import Data.Text (Text) -import Database.Persist.Class (PersistField(..)) import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck import Test.QuickCheck.Instances.Text () -import Web.HttpApiData (FromHttpApiData(..)) import Data.Password.Types (Password, PasswordHash(..), unsafeShowPassword) -import Data.Password.Instances() +import Data.Password.Aeson() main :: IO () main = defaultMain $ testGroup "Password Instances" [ aesonTest - , fromHttpApiDataTest - , persistTest ] data TestUser = TestUser { @@ -41,15 +37,3 @@ aesonTest = testCase "Password (Aeson)" $ [ "name" .= String "testname" , "password" .= String testPassword ] - -fromHttpApiDataTest :: TestTree -fromHttpApiDataTest = testCase "Password (FromHttpApiData)" $ - assertEqual "password doesn't match" (Right testPassword) $ - unsafeShowPassword <$> parseUrlPiece testPassword - where - testPassword = "passtest" - -persistTest :: TestTree -persistTest = testProperty "PasswordHash (PersistField)" $ \pass -> - let pwd = PasswordHash pass - in fromPersistValue (toPersistValue pwd) === Right pwd diff --git a/password-http-api-data/ChangeLog.md b/password-http-api-data/ChangeLog.md new file mode 100644 index 0000000..d1b5092 --- /dev/null +++ b/password-http-api-data/ChangeLog.md @@ -0,0 +1,5 @@ +# Changelog for `password-http-api-data` + +## 0.1.0.0 + +- Split from `password-instances`. diff --git a/password-http-api-data/LICENSE b/password-http-api-data/LICENSE new file mode 100644 index 0000000..750d621 --- /dev/null +++ b/password-http-api-data/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Dennis Gosnell, 2019; Felix Paulusma, 2020 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * 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 +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/password-http-api-data/README.md b/password-http-api-data/README.md new file mode 100644 index 0000000..c64892c --- /dev/null +++ b/password-http-api-data/README.md @@ -0,0 +1,11 @@ +# password-http-api-data + +[![Build Status](https://github.com/cdepillabout/password/workflows/password/badge.svg)](http://github.com/cdepillabout/password) +[![Hackage](https://img.shields.io/hackage/v/password-http-api-data.svg)](https://hackage.haskell.org/package/password-http-api-data) +[![Stackage LTS](http://stackage.org/package/password-http-api-data/badge/lts)](http://stackage.org/lts/package/password-http-api-data) +[![Stackage Nightly](http://stackage.org/package/password-http-api-data/badge/nightly)](http://stackage.org/nightly/package/password-http-api-data) +[![BSD3 license](https://img.shields.io/badge/license-BSD3-blue.svg)](./LICENSE) + +This package provides `http-api-data` typeclass instances for the plain-text password +and hashed password datatypes from the +[password](https://hackage.haskell.org/package/password) package. diff --git a/password-http-api-data/Setup.hs b/password-http-api-data/Setup.hs new file mode 100644 index 0000000..8ec54a0 --- /dev/null +++ b/password-http-api-data/Setup.hs @@ -0,0 +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 diff --git a/password-http-api-data/password-http-api-data.cabal b/password-http-api-data/password-http-api-data.cabal new file mode 100644 index 0000000..2599e40 --- /dev/null +++ b/password-http-api-data/password-http-api-data.cabal @@ -0,0 +1,87 @@ +cabal-version: 1.12 + +name: password-http-api-data +version: 0.1.0.0 +category: Security +synopsis: http-api-data typeclass instances for password package +description: A library providing typeclass instances for `http-api-data` for the types from the password package. +homepage: https://github.com/cdepillabout/password/tree/master/password-http-api-data#readme +bug-reports: https://github.com/cdepillabout/password/issues +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: Custom +extra-source-files: + README.md + ChangeLog.md + +source-repository head + type: git + location: https://github.com/cdepillabout/password + +custom-setup + setup-depends: + base + , Cabal + , cabal-doctest >=1.0.6 && <1.1 + +library + hs-source-dirs: + src + exposed-modules: + Data.Password.HttpApiData + other-modules: + Paths_password_http_api_data + build-depends: + base >= 4.9 && < 5 + , http-api-data + , password-types < 2 + , text + ghc-options: + -Wall + default-language: + Haskell2010 + +test-suite doctests + type: + exitcode-stdio-1.0 + hs-source-dirs: + test/doctest + main-is: + doctest.hs + ghc-options: + -threaded -rtsopts -with-rtsopts=-N + build-depends: + base >=4.9 && <5 + , base-compat + , doctest + , password + , password-http-api-data + , QuickCheck + , quickcheck-instances + , template-haskell + default-language: + Haskell2010 + +test-suite password-http-api-data-tasty + type: + exitcode-stdio-1.0 + hs-source-dirs: + test/tasty + main-is: + Spec.hs + ghc-options: + -threaded -rtsopts -with-rtsopts=-N + build-depends: + base >=4.9 && <5 + , password-http-api-data + , password-types + , http-api-data + , quickcheck-instances + , tasty + , tasty-hunit + , tasty-quickcheck + default-language: + Haskell2010 diff --git a/password-http-api-data/src/Data/Password/HttpApiData.hs b/password-http-api-data/src/Data/Password/HttpApiData.hs new file mode 100644 index 0000000..db8d3b0 --- /dev/null +++ b/password-http-api-data/src/Data/Password/HttpApiData.hs @@ -0,0 +1,54 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +{-| +Module : Data.Password.HttpApiData +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 `http-api-data` typeclass instances +for 'Password'. + +See the "Data.Password.Types" module for more information. +-} + +module Data.Password.HttpApiData () where + +import Data.Password.Types +import GHC.TypeLits (TypeError, ErrorMessage(..)) +import Web.HttpApiData (FromHttpApiData(..), ToHttpApiData(..)) + +-- $setup +-- >>> :set -XOverloadedStrings +-- >>> :set -XDataKinds +-- +-- Import needed functions. +-- +-- >>> import Data.Password.Bcrypt (Salt(..), hashPasswordWithSalt, unsafeShowPassword) +-- >>> import Web.HttpApiData (parseUrlPiece) + +type ErrMsg = 'Text "Warning! Tried to convert plain-text Password to HttpApiData!" + ':$$: 'Text " This is likely a security leak. Please make sure whether this was intended." + ':$$: 'Text " If this is intended, please use 'unsafeShowPassword' before converting to HttpApiData" + ':$$: 'Text "" + +-- | This instance allows a 'Password' to be created with functions like +-- 'Web.HttpApiData.parseUrlPiece' or 'Web.HttpApiData.parseQueryParam'. +-- +-- >>> let eitherPassword = parseUrlPiece "foobar" +-- >>> fmap unsafeShowPassword eitherPassword +-- Right "foobar" +instance FromHttpApiData Password where + parseUrlPiece = fmap mkPassword . parseUrlPiece + +-- | Type error! Do not transmit plain-text 'Password's over HTTP! +instance TypeError ErrMsg => ToHttpApiData Password where + toUrlPiece = error "unreachable" diff --git a/password-http-api-data/test/doctest/doctest.hs b/password-http-api-data/test/doctest/doctest.hs new file mode 100644 index 0000000..dfaed6a --- /dev/null +++ b/password-http-api-data/test/doctest/doctest.hs @@ -0,0 +1,14 @@ +module Main where + +import Build_doctests (flags, pkgs, module_sources) +-- import Data.Foldable (traverse_) +import System.Environment.Compat (unsetEnv) +import Test.DocTest (doctest) + +main :: IO () +main = do + -- traverse_ putStrLn args + unsetEnv "GHC_ENVIRONMENT" + doctest args + where + args = flags ++ pkgs ++ module_sources diff --git a/password-http-api-data/test/tasty/Spec.hs b/password-http-api-data/test/tasty/Spec.hs new file mode 100644 index 0000000..57f9419 --- /dev/null +++ b/password-http-api-data/test/tasty/Spec.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE OverloadedStrings #-} + +import Test.Tasty +import Test.Tasty.HUnit +import Test.Tasty.QuickCheck +import Test.QuickCheck.Instances.Text () +import Web.HttpApiData (FromHttpApiData(..)) + +import Data.Password.Types (Password, PasswordHash(..), unsafeShowPassword) +import Data.Password.HttpApiData() + + +main :: IO () +main = defaultMain $ testGroup "Password Instances" + [ fromHttpApiDataTest + ] + +fromHttpApiDataTest :: TestTree +fromHttpApiDataTest = testCase "Password (FromHttpApiData)" $ + assertEqual "password doesn't match" (Right testPassword) $ + unsafeShowPassword <$> parseUrlPiece testPassword + where + testPassword = "passtest" diff --git a/password-instances/Setup.hs b/password-instances/Setup.hs index 8ec54a0..00bfe1f 100644 --- a/password-instances/Setup.hs +++ b/password-instances/Setup.hs @@ -1,33 +1,4 @@ -{-# 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 diff --git a/password-instances/password-instances.cabal b/password-instances/password-instances.cabal index 5754957..daaaeb8 100644 --- a/password-instances/password-instances.cabal +++ b/password-instances/password-instances.cabal @@ -2,7 +2,7 @@ cabal-version: 1.12 name: password-instances version: 3.0.0.0 -category: Data +category: Security synopsis: typeclass instances for password package description: A library providing typeclass instances for common libraries for the types from the password package. homepage: https://github.com/cdepillabout/password/tree/master/password-instances#readme @@ -25,7 +25,6 @@ custom-setup setup-depends: base , Cabal - , cabal-doctest >=1.0.6 && <1.1 library hs-source-dirs: @@ -36,57 +35,10 @@ library Paths_password_instances build-depends: base >= 4.9 && < 5 - , aeson >= 0.2 - , http-api-data - , password-types < 2 - , persistent >= 1.2 - , text + , password-aeson + , password-http-api-data + , password-persistent ghc-options: -Wall default-language: Haskell2010 - -test-suite doctests - type: - exitcode-stdio-1.0 - hs-source-dirs: - test/doctest - main-is: - doctest.hs - ghc-options: - -threaded -rtsopts -with-rtsopts=-N - build-depends: - base >=4.9 && <5 - , base-compat - , doctest - , password - , password-instances - , QuickCheck - , quickcheck-instances - , template-haskell - default-language: - Haskell2010 - -test-suite password-instances-tasty - type: - exitcode-stdio-1.0 - hs-source-dirs: - test/tasty - main-is: - Spec.hs - ghc-options: - -threaded -rtsopts -with-rtsopts=-N - build-depends: - base >=4.9 && <5 - , password-instances - , password-types - , aeson - , http-api-data - , persistent - , quickcheck-instances - , tasty - , tasty-hunit - , tasty-quickcheck - , text - default-language: - Haskell2010 diff --git a/password-instances/src/Data/Password/Instances.hs b/password-instances/src/Data/Password/Instances.hs index cfee799..ebfb3ad 100644 --- a/password-instances/src/Data/Password/Instances.hs +++ b/password-instances/src/Data/Password/Instances.hs @@ -1,14 +1,4 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE UndecidableInstances #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# OPTIONS_GHC -Wno-dodgy-exports -Wno-unused-imports #-} {-| Module : Data.Password.Instances @@ -24,99 +14,8 @@ for 'Password' and 'PasswordHash'. See the "Data.Password.Types" module for more information. -} -module Data.Password.Instances () where +module Data.Password.Instances (module E) where -import Data.Aeson (FromJSON(..), ToJSON(..)) -import Data.Password.Types (Password, PasswordHash(..), mkPassword) -#if !MIN_VERSION_base(4,13,0) -import Data.Semigroup ((<>)) -#endif -import Data.Text (pack) -import Data.Text.Encoding as TE (decodeUtf8') -import Database.Persist (PersistValue(..)) -import Database.Persist.Class (PersistField(..)) -import Database.Persist.Sql (PersistFieldSql(..)) -import GHC.TypeLits (TypeError, ErrorMessage(..)) -import Web.HttpApiData (FromHttpApiData(..), ToHttpApiData(..)) - - --- $setup --- >>> :set -XOverloadedStrings --- >>> :set -XDataKinds --- --- Import needed functions. --- --- >>> import Data.Aeson (decode) --- >>> import Data.Password.Bcrypt (Salt(..), hashPasswordWithSalt, unsafeShowPassword) --- >>> import Database.Persist.Class (PersistField(toPersistValue)) --- >>> import Web.HttpApiData (parseUrlPiece) - --- | This instance allows a 'Password' to be created from a JSON blob. --- --- >>> let maybePassword = decode "\"foobar\"" :: Maybe Password --- >>> fmap unsafeShowPassword maybePassword --- Just "foobar" --- --- There is no instance for 'ToJSON' for 'Password' because we don't want to --- accidentally encode a plain-text 'Password' to JSON and send it to the end-user. --- --- Similarly, there is no 'ToJSON' and 'FromJSON' instance for 'PasswordHash' --- because we don't want to accidentally send the password hash to the end --- user. -instance FromJSON Password where - parseJSON = fmap mkPassword . parseJSON - -type ErrMsg e = 'Text "Warning! Tried to convert plain-text Password to " ':<>: 'Text e ':<>: 'Text "!" - ':$$: 'Text " This is likely a security leak. Please make sure whether this was intended." - ':$$: 'Text " If this is intended, please use 'unsafeShowPassword' before converting to " ':<>: 'Text e - ':$$: 'Text "" - --- | Type error! Do not use 'toJSON' on a 'Password'! -instance TypeError (ErrMsg "JSON") => ToJSON Password where - toJSON = error "unreachable" - --- | This instance allows a 'Password' to be created with functions like --- 'Web.HttpApiData.parseUrlPiece' or 'Web.HttpApiData.parseQueryParam'. --- --- >>> let eitherPassword = parseUrlPiece "foobar" --- >>> fmap unsafeShowPassword eitherPassword --- Right "foobar" -instance FromHttpApiData Password where - parseUrlPiece = fmap mkPassword . parseUrlPiece - --- | Type error! Do not transmit plain-text 'Password's over HTTP! -instance TypeError (ErrMsg "HttpApiData") => ToHttpApiData Password where - toUrlPiece = error "unreachable" - --- | This instance allows a 'PasswordHash' to be stored as a field in a database using --- "Database.Persist". --- --- >>> let salt = Salt "abcdefghijklmnop" --- >>> let pass = mkPassword "foobar" --- >>> let hashedPassword = hashPasswordWithSalt 10 salt pass --- >>> toPersistValue hashedPassword --- PersistText "$2b$10$WUHhXETkX0fnYkrqZU3ta.N8Utt4U77kW4RVbchzgvBvBBEEdCD/u" --- --- In the example above, the long 'PersistText' will be the value you store in --- the database. --- --- We don't provide an instance of 'PersistField' for 'Password', because we don't --- want to make it easy to store a plain-text password in the database. -instance PersistField (PasswordHash a) where - toPersistValue (PasswordHash hpw) = PersistText hpw - fromPersistValue = \case - PersistText txt -> Right $ PasswordHash txt - PersistByteString bs -> - either failed (Right . PasswordHash) $ TE.decodeUtf8' bs - _ -> Left "did not parse PasswordHash from PersistValue" - where - failed e = Left $ "Failed decoding PasswordHash to UTF8: " <> pack (show e) - --- | This instance allows a 'PasswordHash' to be stored as a field in an SQL --- database in "Database.Persist.Sql". -deriving newtype instance PersistFieldSql (PasswordHash a) - --- | Type error! Do not store plain-text 'Password's in your database! -instance TypeError (ErrMsg "PersistValue") => PersistField Password where - toPersistValue = error "unreachable" - fromPersistValue = error "unreachable" +import Data.Password.Aeson as E +import Data.Password.HttpApiData as E +import Data.Password.Persistent as E diff --git a/password-persistent/ChangeLog.md b/password-persistent/ChangeLog.md new file mode 100644 index 0000000..ccb88c0 --- /dev/null +++ b/password-persistent/ChangeLog.md @@ -0,0 +1,5 @@ +# Changelog for `password-persistent` + +## 0.1.0.0 + +- Split from `password-instances`. diff --git a/password-persistent/LICENSE b/password-persistent/LICENSE new file mode 100644 index 0000000..750d621 --- /dev/null +++ b/password-persistent/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Dennis Gosnell, 2019; Felix Paulusma, 2020 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * 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 +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/password-persistent/README.md b/password-persistent/README.md new file mode 100644 index 0000000..ba324b2 --- /dev/null +++ b/password-persistent/README.md @@ -0,0 +1,11 @@ +# password-persistent + +[![Build Status](https://github.com/cdepillabout/password/workflows/password/badge.svg)](http://github.com/cdepillabout/password) +[![Hackage](https://img.shields.io/hackage/v/password-persistent.svg)](https://hackage.haskell.org/package/password-persistent) +[![Stackage LTS](http://stackage.org/package/password-persistent/badge/lts)](http://stackage.org/lts/package/password-persistent) +[![Stackage Nightly](http://stackage.org/package/password-persistent/badge/nightly)](http://stackage.org/nightly/package/password-persistent) +[![BSD3 license](https://img.shields.io/badge/license-BSD3-blue.svg)](./LICENSE) + +This package provides `persistent` typeclass instances for the plain-text password +and hashed password datatypes from the +[password](https://hackage.haskell.org/package/password) package. diff --git a/password-persistent/Setup.hs b/password-persistent/Setup.hs new file mode 100644 index 0000000..8ec54a0 --- /dev/null +++ b/password-persistent/Setup.hs @@ -0,0 +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 diff --git a/password-persistent/password-persistent.cabal b/password-persistent/password-persistent.cabal new file mode 100644 index 0000000..51b7bad --- /dev/null +++ b/password-persistent/password-persistent.cabal @@ -0,0 +1,88 @@ +cabal-version: 1.12 + +name: password-persistent +version: 0.1.0.0 +category: Security +synopsis: persistent typeclass instances for password package +description: A library providing typeclass instances for `persistent` for the types from the password package. +homepage: https://github.com/cdepillabout/password/tree/master/password-persistent#readme +bug-reports: https://github.com/cdepillabout/password/issues +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: Custom +extra-source-files: + README.md + ChangeLog.md + +source-repository head + type: git + location: https://github.com/cdepillabout/password + +custom-setup + setup-depends: + base + , Cabal + , cabal-doctest >=1.0.6 && <1.1 + +library + hs-source-dirs: + src + exposed-modules: + Data.Password.Persistent + other-modules: + Paths_password_persistent + build-depends: + base >= 4.9 && < 5 + , password-types < 2 + , persistent >= 1.2 + , text + ghc-options: + -Wall + default-language: + Haskell2010 + +test-suite doctests + type: + exitcode-stdio-1.0 + hs-source-dirs: + test/doctest + main-is: + doctest.hs + ghc-options: + -threaded -rtsopts -with-rtsopts=-N + build-depends: + base >=4.9 && <5 + , base-compat + , doctest + , password + , password-persistent + , QuickCheck + , quickcheck-instances + , template-haskell + default-language: + Haskell2010 + +test-suite password-persistent-tasty + type: + exitcode-stdio-1.0 + hs-source-dirs: + test/tasty + main-is: + Spec.hs + ghc-options: + -threaded -rtsopts -with-rtsopts=-N + build-depends: + base >=4.9 && <5 + , password-persistent + , password-types + , persistent + , quickcheck-instances + , tasty + , tasty-hunit + , tasty-quickcheck + , text + default-language: + Haskell2010 diff --git a/password-persistent/src/Data/Password/Persistent.hs b/password-persistent/src/Data/Password/Persistent.hs new file mode 100644 index 0000000..f81674d --- /dev/null +++ b/password-persistent/src/Data/Password/Persistent.hs @@ -0,0 +1,86 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +{-| +Module : Data.Password.Persistent +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 `persistent` typeclass instances +for 'Password' and 'PasswordHash'. + +See the "Data.Password.Types" module for more information. +-} + +module Data.Password.Persistent () where + +import Data.Password.Types +#if !MIN_VERSION_base(4,13,0) +import Data.Semigroup ((<>)) +#endif +import Data.Text (pack) +import Data.Text.Encoding as TE (decodeUtf8') +import Database.Persist (PersistValue(..)) +import Database.Persist.Class (PersistField(..)) +import Database.Persist.Sql (PersistFieldSql(..)) +import GHC.TypeLits (TypeError, ErrorMessage(..)) + +-- $setup +-- >>> :set -XOverloadedStrings +-- >>> :set -XDataKinds +-- +-- Import needed functions. +-- +-- >>> import Data.Password.Bcrypt (Salt(..), hashPasswordWithSalt, unsafeShowPassword) +-- >>> import Data.Password.Types (mkPassword) +-- >>> import Database.Persist.Class (PersistField(toPersistValue)) + +type ErrMsg = 'Text "Warning! Tried to convert plain-text Password to PersistValue!" + ':$$: 'Text " This is likely a security leak. Please make sure whether this was intended." + ':$$: 'Text " If this is intended, please use 'unsafeShowPassword' before converting to PersistValue." + ':$$: 'Text "" + +-- | This instance allows a 'PasswordHash' to be stored as a field in a database using +-- "Database.Persist". +-- +-- >>> let salt = Salt "abcdefghijklmnop" +-- >>> let pass = mkPassword "foobar" +-- >>> let hashedPassword = hashPasswordWithSalt 10 salt pass +-- >>> toPersistValue hashedPassword +-- PersistText "$2b$10$WUHhXETkX0fnYkrqZU3ta.N8Utt4U77kW4RVbchzgvBvBBEEdCD/u" +-- +-- In the example above, the long 'PersistText' will be the value you store in +-- the database. +-- +-- We don't provide an instance of 'PersistField' for 'Password', because we don't +-- want to make it easy to store a plain-text password in the database. +instance PersistField (PasswordHash a) where + toPersistValue (PasswordHash hpw) = PersistText hpw + fromPersistValue = \case + PersistText txt -> Right $ PasswordHash txt + PersistByteString bs -> + either failed (Right . PasswordHash) $ TE.decodeUtf8' bs + _ -> Left "did not parse PasswordHash from PersistValue" + where + failed e = Left $ "Failed decoding PasswordHash to UTF8: " <> pack (show e) + +-- | This instance allows a 'PasswordHash' to be stored as a field in an SQL +-- database in "Database.Persist.Sql". +deriving newtype instance PersistFieldSql (PasswordHash a) + +-- | Type error! Do not store plain-text 'Password's in your database! +instance TypeError ErrMsg => PersistField Password where + toPersistValue = error "unreachable" + fromPersistValue = error "unreachable" diff --git a/password-persistent/test/doctest/doctest.hs b/password-persistent/test/doctest/doctest.hs new file mode 100644 index 0000000..dfaed6a --- /dev/null +++ b/password-persistent/test/doctest/doctest.hs @@ -0,0 +1,14 @@ +module Main where + +import Build_doctests (flags, pkgs, module_sources) +-- import Data.Foldable (traverse_) +import System.Environment.Compat (unsetEnv) +import Test.DocTest (doctest) + +main :: IO () +main = do + -- traverse_ putStrLn args + unsetEnv "GHC_ENVIRONMENT" + doctest args + where + args = flags ++ pkgs ++ module_sources diff --git a/password-persistent/test/tasty/Spec.hs b/password-persistent/test/tasty/Spec.hs new file mode 100644 index 0000000..a8e83de --- /dev/null +++ b/password-persistent/test/tasty/Spec.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE OverloadedStrings #-} + +import Data.Text (Text) +import Database.Persist.Class (PersistField(..)) +import Test.Tasty +import Test.Tasty.HUnit +import Test.Tasty.QuickCheck +import Test.QuickCheck.Instances.Text () + +import Data.Password.Types (Password, PasswordHash(..), unsafeShowPassword) +import Data.Password.Persistent() + + +main :: IO () +main = defaultMain $ testGroup "Password Instances" + [ persistTest + ] + +persistTest :: TestTree +persistTest = testProperty "PasswordHash (PersistField)" $ \pass -> + let pwd = PasswordHash pass + in fromPersistValue (toPersistValue pwd) === Right pwd diff --git a/password-types/password-types.cabal b/password-types/password-types.cabal index 5654817..78138b8 100644 --- a/password-types/password-types.cabal +++ b/password-types/password-types.cabal @@ -2,7 +2,7 @@ cabal-version: 1.12 name: password-types version: 1.0.0.0 -category: Data +category: Security synopsis: Types for handling passwords description: A library providing types for working with plain-text and hashed passwords. homepage: https://github.com/cdepillabout/password/tree/master/password-types#readme diff --git a/password/password.cabal b/password/password.cabal index 99b2070..4339303 100644 --- a/password/password.cabal +++ b/password/password.cabal @@ -2,7 +2,7 @@ cabal-version: 1.12 name: password version: 3.1.0.1 -category: Data +category: Security synopsis: Hashing and checking of passwords description: A library providing functionality for working with plain-text and hashed passwords diff --git a/stack.yaml b/stack.yaml index d863e94..b1e1183 100644 --- a/stack.yaml +++ b/stack.yaml @@ -34,8 +34,11 @@ resolver: lts-22.38 # - wai packages: - password + - password-aeson - password-cli + - password-http-api-data - password-instances + - password-persistent - password-types # Dependency packages to be pulled from upstream that are not in the resolver