From fb9b185e972593e66027eeef7f3c38ce9b361a02 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Sun, 5 Oct 2025 00:49:07 +0200 Subject: [PATCH 1/8] Split password-instances into multiple packages (#1) --- hie.yaml | 25 +++- password-aeson/ChangeLog.md | 5 + password-aeson/LICENSE | 30 +++++ password-aeson/README.md | 11 ++ password-aeson/Setup.hs | 33 ++++++ password-aeson/password-aeson.cabal | 88 ++++++++++++++ password-aeson/src/Data/Password/Aeson.hs | 59 ++++++++++ .../test/doctest/doctest.hs | 0 .../test/tasty/Spec.hs | 18 +-- password-http-api-data/ChangeLog.md | 5 + password-http-api-data/LICENSE | 30 +++++ password-http-api-data/README.md | 11 ++ password-http-api-data/Setup.hs | 33 ++++++ .../password-http-api-data.cabal | 87 ++++++++++++++ .../src/Data/Password/HttpApiData.hs | 54 +++++++++ .../test/doctest/doctest.hs | 14 +++ password-http-api-data/test/tasty/Spec.hs | 23 ++++ password-instances/Setup.hs | 29 ----- password-instances/password-instances.cabal | 54 +-------- .../src/Data/Password/Instances.hs | 111 +----------------- password-persistent/ChangeLog.md | 5 + password-persistent/LICENSE | 30 +++++ password-persistent/README.md | 11 ++ password-persistent/Setup.hs | 33 ++++++ password-persistent/password-persistent.cabal | 88 ++++++++++++++ .../src/Data/Password/Persistent.hs | 86 ++++++++++++++ password-persistent/test/doctest/doctest.hs | 14 +++ password-persistent/test/tasty/Spec.hs | 22 ++++ stack.yaml | 3 + 29 files changed, 805 insertions(+), 207 deletions(-) create mode 100644 password-aeson/ChangeLog.md create mode 100644 password-aeson/LICENSE create mode 100644 password-aeson/README.md create mode 100644 password-aeson/Setup.hs create mode 100644 password-aeson/password-aeson.cabal create mode 100644 password-aeson/src/Data/Password/Aeson.hs rename {password-instances => password-aeson}/test/doctest/doctest.hs (100%) rename {password-instances => password-aeson}/test/tasty/Spec.hs (61%) create mode 100644 password-http-api-data/ChangeLog.md create mode 100644 password-http-api-data/LICENSE create mode 100644 password-http-api-data/README.md create mode 100644 password-http-api-data/Setup.hs create mode 100644 password-http-api-data/password-http-api-data.cabal create mode 100644 password-http-api-data/src/Data/Password/HttpApiData.hs create mode 100644 password-http-api-data/test/doctest/doctest.hs create mode 100644 password-http-api-data/test/tasty/Spec.hs create mode 100644 password-persistent/ChangeLog.md create mode 100644 password-persistent/LICENSE create mode 100644 password-persistent/README.md create mode 100644 password-persistent/Setup.hs create mode 100644 password-persistent/password-persistent.cabal create mode 100644 password-persistent/src/Data/Password/Persistent.hs create mode 100644 password-persistent/test/doctest/doctest.hs create mode 100644 password-persistent/test/tasty/Spec.hs 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..68cad00 --- /dev/null +++ b/password-aeson/src/Data/Password/Aeson.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# 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 () where + +import Data.Aeson (FromJSON(..), ToJSON(..)) +import Data.Password.Types (Password, mkPassword) +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" 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..f7c7ad4 --- /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' and 'PasswordHash'. + +See the "Data.Password.Types" module for more information. +-} + +module Data.Password.HttpApiData () where + +import Data.Password.Types (Password, mkPassword) +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..a5cfd0b 100644 --- a/password-instances/password-instances.cabal +++ b/password-instances/password-instances.cabal @@ -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..afd78e5 --- /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 (Password, PasswordHash(..)) +#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/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 From b16b4ee2c5023af40712f29555068c485e7daa30 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Tue, 7 Oct 2025 20:57:34 +0200 Subject: [PATCH 2/8] Add `ExposedPassword` in `password-aeson` --- password-aeson/src/Data/Password/Aeson.hs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/password-aeson/src/Data/Password/Aeson.hs b/password-aeson/src/Data/Password/Aeson.hs index 68cad00..244930d 100644 --- a/password-aeson/src/Data/Password/Aeson.hs +++ b/password-aeson/src/Data/Password/Aeson.hs @@ -1,4 +1,6 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeOperators #-} @@ -19,10 +21,14 @@ for 'Password' and 'PasswordHash'. See the "Data.Password.Types" module for more information. -} -module Data.Password.Aeson () where +module Data.Password.Aeson + ( FromJSON (..), + ToJSON (..), + ExposedPassword (..), + ) where import Data.Aeson (FromJSON(..), ToJSON(..)) -import Data.Password.Types (Password, mkPassword) +import Data.Password.Types (Password, mkPassword, unsafeShowPassword) import GHC.TypeLits (TypeError, ErrorMessage(..)) -- $setup @@ -57,3 +63,16 @@ type ErrMsg = 'Text "Warning! Tried to convert plain-text Password to JSON!" -- | 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 From 08ba3b3f08b31bb68abc24bcfc85242b9f365ff7 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Tue, 7 Oct 2025 20:58:25 +0200 Subject: [PATCH 3/8] Migrate packages `category` to `Security` --- password-instances/password-instances.cabal | 2 +- password-types/password-types.cabal | 2 +- password/password.cabal | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/password-instances/password-instances.cabal b/password-instances/password-instances.cabal index a5cfd0b..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 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 From d2277fe5932fe9cd070524d5ec61d54cf7d813fc Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Sat, 11 Oct 2025 22:43:50 +0200 Subject: [PATCH 4/8] Add `Password`/`PasswordHash` orphan `instance`s --- password-aeson/src/Data/Password/Aeson.hs | 7 ++++++- password-http-api-data/src/Data/Password/HttpApiData.hs | 4 ++-- password-persistent/src/Data/Password/Persistent.hs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/password-aeson/src/Data/Password/Aeson.hs b/password-aeson/src/Data/Password/Aeson.hs index 244930d..b1ad192 100644 --- a/password-aeson/src/Data/Password/Aeson.hs +++ b/password-aeson/src/Data/Password/Aeson.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} @@ -28,7 +29,7 @@ module Data.Password.Aeson ) where import Data.Aeson (FromJSON(..), ToJSON(..)) -import Data.Password.Types (Password, mkPassword, unsafeShowPassword) +import Data.Password.Types import GHC.TypeLits (TypeError, ErrorMessage(..)) -- $setup @@ -76,3 +77,7 @@ newtype ExposedPassword = ExposedPassword Password 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-http-api-data/src/Data/Password/HttpApiData.hs b/password-http-api-data/src/Data/Password/HttpApiData.hs index f7c7ad4..db8d3b0 100644 --- a/password-http-api-data/src/Data/Password/HttpApiData.hs +++ b/password-http-api-data/src/Data/Password/HttpApiData.hs @@ -15,14 +15,14 @@ Stability : experimental Portability : POSIX This module provides `http-api-data` typeclass instances -for 'Password' and 'PasswordHash'. +for 'Password'. See the "Data.Password.Types" module for more information. -} module Data.Password.HttpApiData () where -import Data.Password.Types (Password, mkPassword) +import Data.Password.Types import GHC.TypeLits (TypeError, ErrorMessage(..)) import Web.HttpApiData (FromHttpApiData(..), ToHttpApiData(..)) diff --git a/password-persistent/src/Data/Password/Persistent.hs b/password-persistent/src/Data/Password/Persistent.hs index afd78e5..f81674d 100644 --- a/password-persistent/src/Data/Password/Persistent.hs +++ b/password-persistent/src/Data/Password/Persistent.hs @@ -26,7 +26,7 @@ See the "Data.Password.Types" module for more information. module Data.Password.Persistent () where -import Data.Password.Types (Password, PasswordHash(..)) +import Data.Password.Types #if !MIN_VERSION_base(4,13,0) import Data.Semigroup ((<>)) #endif From 5142f5e5d5dfcc3f35ce1068ce1c382f1bce6140 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 17 Mar 2026 11:47:23 +0100 Subject: [PATCH 5/8] removed unused pragmas and packages --- password-aeson/password-aeson.cabal | 6 +++--- password-aeson/src/Data/Password/Aeson.hs | 3 --- password-http-api-data/password-http-api-data.cabal | 6 +++--- password-http-api-data/src/Data/Password/HttpApiData.hs | 4 ---- password-persistent/password-persistent.cabal | 7 +++---- password-persistent/src/Data/Password/Persistent.hs | 4 ++-- 6 files changed, 11 insertions(+), 19 deletions(-) diff --git a/password-aeson/password-aeson.cabal b/password-aeson/password-aeson.cabal index ca70457..40b1663 100644 --- a/password-aeson/password-aeson.cabal +++ b/password-aeson/password-aeson.cabal @@ -32,8 +32,8 @@ library src exposed-modules: Data.Password.Aeson - other-modules: - Paths_password_aeson +-- other-modules: +-- Paths_password_aeson build-depends: base >= 4.9 && < 5 , aeson >= 0.2 @@ -81,7 +81,7 @@ test-suite password-aeson-tasty , aeson , quickcheck-instances , tasty - , tasty-hunit + -- , tasty-hunit , tasty-quickcheck , text default-language: diff --git a/password-aeson/src/Data/Password/Aeson.hs b/password-aeson/src/Data/Password/Aeson.hs index b1ad192..5a614de 100644 --- a/password-aeson/src/Data/Password/Aeson.hs +++ b/password-aeson/src/Data/Password/Aeson.hs @@ -1,11 +1,8 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-| diff --git a/password-http-api-data/password-http-api-data.cabal b/password-http-api-data/password-http-api-data.cabal index 2599e40..d8bb414 100644 --- a/password-http-api-data/password-http-api-data.cabal +++ b/password-http-api-data/password-http-api-data.cabal @@ -32,8 +32,8 @@ library src exposed-modules: Data.Password.HttpApiData - other-modules: - Paths_password_http_api_data +-- other-modules: +-- Paths_password_http_api_data build-depends: base >= 4.9 && < 5 , http-api-data @@ -81,7 +81,7 @@ test-suite password-http-api-data-tasty , http-api-data , quickcheck-instances , tasty - , tasty-hunit + -- , 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 index db8d3b0..630fc6f 100644 --- a/password-http-api-data/src/Data/Password/HttpApiData.hs +++ b/password-http-api-data/src/Data/Password/HttpApiData.hs @@ -1,9 +1,5 @@ {-# LANGUAGE DataKinds #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-| diff --git a/password-persistent/password-persistent.cabal b/password-persistent/password-persistent.cabal index 51b7bad..102a073 100644 --- a/password-persistent/password-persistent.cabal +++ b/password-persistent/password-persistent.cabal @@ -32,8 +32,8 @@ library src exposed-modules: Data.Password.Persistent - other-modules: - Paths_password_persistent +-- other-modules: +-- Paths_password_persistent build-depends: base >= 4.9 && < 5 , password-types < 2 @@ -81,8 +81,7 @@ test-suite password-persistent-tasty , persistent , quickcheck-instances , tasty - , tasty-hunit + -- , 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 index f81674d..7ae2e62 100644 --- a/password-persistent/src/Data/Password/Persistent.hs +++ b/password-persistent/src/Data/Password/Persistent.hs @@ -4,10 +4,10 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} +-- {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE UndecidableInstances #-} +-- {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-| From d333c19815e79ce287587ac87c6a6e3c1dc254ca Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 17 Mar 2026 11:48:28 +0100 Subject: [PATCH 6/8] cleaned up and made imports more explicit, and transformed some unit tests into property tests --- password-aeson/test/tasty/Spec.hs | 27 +++++++++---------- password-http-api-data/test/tasty/Spec.hs | 21 +++++++-------- .../src/Data/Password/Persistent.hs | 2 +- password-persistent/test/tasty/Spec.hs | 10 +++---- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/password-aeson/test/tasty/Spec.hs b/password-aeson/test/tasty/Spec.hs index 82217ae..0aea410 100644 --- a/password-aeson/test/tasty/Spec.hs +++ b/password-aeson/test/tasty/Spec.hs @@ -3,13 +3,12 @@ import Data.Aeson import Data.Aeson.Types (parseMaybe) import Data.Text (Text) -import Test.Tasty -import Test.Tasty.HUnit -import Test.Tasty.QuickCheck +import Test.Tasty (TestTree, defaultMain, testGroup) +import Test.Tasty.QuickCheck (testProperty, (===)) import Test.QuickCheck.Instances.Text () -import Data.Password.Types (Password, PasswordHash(..), unsafeShowPassword) -import Data.Password.Aeson() +import Data.Password.Types (Password, unsafeShowPassword) +import Data.Password.Aeson () main :: IO () @@ -26,14 +25,14 @@ instance FromJSON TestUser where parseJSON = withObject "TestUser" $ \o -> TestUser <$> o .: "name" <*> o .: "password" - aesonTest :: TestTree -aesonTest = testCase "Password (Aeson)" $ - assertEqual "password doesn't match" (Just testPassword) $ - unsafeShowPassword . password <$> parseMaybe parseJSON testUser +aesonTest = + testProperty "Password (Aeson)" $ \pwd -> + Just pwd === (unsafeShowPassword . password <$> parseIt pwd) where - testPassword = "testpass" - testUser = object - [ "name" .= String "testname" - , "password" .= String testPassword - ] + parseIt pwd = + parseMaybe parseJSON $ + object + [ "name" .= String "testname" + , "password" .= String pwd + ] diff --git a/password-http-api-data/test/tasty/Spec.hs b/password-http-api-data/test/tasty/Spec.hs index 57f9419..e31edee 100644 --- a/password-http-api-data/test/tasty/Spec.hs +++ b/password-http-api-data/test/tasty/Spec.hs @@ -1,13 +1,12 @@ {-# LANGUAGE OverloadedStrings #-} -import Test.Tasty -import Test.Tasty.HUnit -import Test.Tasty.QuickCheck -import Test.QuickCheck.Instances.Text () -import Web.HttpApiData (FromHttpApiData(..)) +import Test.Tasty (TestTree, defaultMain, testGroup) +import Test.Tasty.QuickCheck (testProperty, (===)) +import Test.QuickCheck.Instances () +import Web.HttpApiData (FromHttpApiData (..)) -import Data.Password.Types (Password, PasswordHash(..), unsafeShowPassword) -import Data.Password.HttpApiData() +import Data.Password.Types (unsafeShowPassword) +import Data.Password.HttpApiData () main :: IO () @@ -16,8 +15,6 @@ main = defaultMain $ testGroup "Password Instances" ] fromHttpApiDataTest :: TestTree -fromHttpApiDataTest = testCase "Password (FromHttpApiData)" $ - assertEqual "password doesn't match" (Right testPassword) $ - unsafeShowPassword <$> parseUrlPiece testPassword - where - testPassword = "passtest" +fromHttpApiDataTest = + testProperty "Password (FromHttpApiData)" $ \pw -> + (Right pw) === (unsafeShowPassword <$> parseUrlPiece pw) diff --git a/password-persistent/src/Data/Password/Persistent.hs b/password-persistent/src/Data/Password/Persistent.hs index 7ae2e62..c4dccf8 100644 --- a/password-persistent/src/Data/Password/Persistent.hs +++ b/password-persistent/src/Data/Password/Persistent.hs @@ -72,7 +72,7 @@ instance PersistField (PasswordHash a) where PersistText txt -> Right $ PasswordHash txt PersistByteString bs -> either failed (Right . PasswordHash) $ TE.decodeUtf8' bs - _ -> Left "did not parse PasswordHash from PersistValue" + _ -> Left "could not parse PasswordHash from PersistValue" where failed e = Left $ "Failed decoding PasswordHash to UTF8: " <> pack (show e) diff --git a/password-persistent/test/tasty/Spec.hs b/password-persistent/test/tasty/Spec.hs index a8e83de..36571e5 100644 --- a/password-persistent/test/tasty/Spec.hs +++ b/password-persistent/test/tasty/Spec.hs @@ -1,13 +1,11 @@ {-# LANGUAGE OverloadedStrings #-} -import Data.Text (Text) -import Database.Persist.Class (PersistField(..)) -import Test.Tasty -import Test.Tasty.HUnit -import Test.Tasty.QuickCheck +import Database.Persist.Class (PersistField (..)) +import Test.Tasty (TestTree, defaultMain, testGroup) +import Test.Tasty.QuickCheck (testProperty, (===)) import Test.QuickCheck.Instances.Text () -import Data.Password.Types (Password, PasswordHash(..), unsafeShowPassword) +import Data.Password.Types (PasswordHash (..)) import Data.Password.Persistent() From ec6196607c84d313ad156af418c35bfc2a52af97 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 17 Mar 2026 11:49:10 +0100 Subject: [PATCH 7/8] password-aeson: added extra documentation about the 'ExposedPassword' newtype, and removed re-exporting the 'From-/ToJSON' classes --- password-aeson/src/Data/Password/Aeson.hs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/password-aeson/src/Data/Password/Aeson.hs b/password-aeson/src/Data/Password/Aeson.hs index 5a614de..05adc05 100644 --- a/password-aeson/src/Data/Password/Aeson.hs +++ b/password-aeson/src/Data/Password/Aeson.hs @@ -14,16 +14,14 @@ Stability : experimental Portability : POSIX This module provides additional typeclass instances -for 'Password' and 'PasswordHash'. +for 'Password' and 'PasswordHash', along with the +'ExposedPassword' newtype if you /absolutely have to/ +convert a plain text password into JSON. See the "Data.Password.Types" module for more information. -} -module Data.Password.Aeson - ( FromJSON (..), - ToJSON (..), - ExposedPassword (..), - ) where +module Data.Password.Aeson (ExposedPassword (..)) where import Data.Aeson (FromJSON(..), ToJSON(..)) import Data.Password.Types From 7f03e71db83ccc4cf7f7231175db119bc7bd8854 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 17 Mar 2026 11:54:12 +0100 Subject: [PATCH 8/8] password-persistent: actually remove unused pragmas --- password-persistent/src/Data/Password/Persistent.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/password-persistent/src/Data/Password/Persistent.hs b/password-persistent/src/Data/Password/Persistent.hs index c4dccf8..429db93 100644 --- a/password-persistent/src/Data/Password/Persistent.hs +++ b/password-persistent/src/Data/Password/Persistent.hs @@ -4,10 +4,8 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} --- {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeOperators #-} --- {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-|