packages feed

haveibeenpwned (empty) → 0.2.0.0

raw patch · 6 files changed

+283/−0 lines, 6 filesdep +basedep +bytestringdep +cryptonitesetup-changed

Dependencies added: base, bytestring, cryptonite, data-default, haveibeenpwned, http-client, http-client-tls, http-types, monad-logger, mtl, safe, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,12 @@+# Revision history for haveibeenpwned++## 0.2.0.0++* Breaking change in order to make the API and the implementation more secure.+  * There is a new HaveIBeenPwnedResult_Secure constructor which signals that the given password was not found in any database.+  * The `HaveIBeenPwnedResult_Disclosed` constructor has been renamed to `HaveIBeenPwnedResult_Pwned`, as its behaviour changed. (Valid passwords are no longer signalled by this constructor.)+* Also internally, a "not found in database" is no longer represented as a disclosed count of zero. This improves security in the case of an incorrect database entry, having a disclosed count of 0, which would make this library report that password as "secure", although it actually has been leaked.++## 0.1.0.0++* Initial release
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2019, Obsidian Systems++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 Obsidian Systems 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.
+ README.lhs view
@@ -0,0 +1,68 @@+haveibeenpwned+<img align="right" src="https://obsidian.systems/static/images/ObsidianSystemsLogo.svg">+======================+[![Haskell](https://img.shields.io/badge/language-Haskell-orange.svg)](https://haskell.org) [![Hackage](https://img.shields.io/hackage/v/haveibeenpwned.svg)](https://hackage.haskell.org/package/haveibeenpwned) [![Hackage CI](https://matrix.hackage.haskell.org/api/v2/packages/haveibeenpwned/badge)](https://matrix.hackage.haskell.org/#/package/haveibeenpwned)   [![Github CI](https://github.com/obsidiansystems/haveibeenpwned/workflows/github-action/badge.svg)](https://github.com/obsidiansystems/haveibeenpwned/actions) [![travis-ci](https://api.travis-ci.org/obsidiansystems/haveibeenpwned.svg?branch=develop)](https://travis-ci.org/obsidiansystems/haveibeenpwned) [![BSD3 License](https://img.shields.io/badge/license-BSD3-blue.svg)](https://github.com/obsidiansystems/haveibeenpwned/blob/master/LICENSE)++A [haskell](https://haskell.org) library for checking passwords against the+[haveibeenpwned.com](https://haveibeenpwned.com) database.++By means of this library you can do some basic strength check on new user+passwords. Common weak passwords like many plain English words or also many+stronger passwords which happen to have been leaked will likely be found in the+database and can thus be rejected.++Example+-------++The example below can be built and run using `cabal build exe:readme` or `cabal+repl exe:readme`.++```haskell ++> {-# LANGUAGE OverloadedStrings #-}+>+> import Control.Monad.IO.Class (liftIO)+> import Control.Monad.Logger (runStdoutLoggingT)+> import Control.Exception (bracket_)+> import Data.Text as T (pack)+> import Network.HTTP.Client (newManager)+> import Network.HTTP.Client.TLS (tlsManagerSettings)+> import System.IO (hFlush, stdout, hGetEcho, stdin, hSetEcho)+>+> import HaveIBeenPwned+>+> -- | A really simple demo of the hibp functionality. Asks the user to enter+> -- a password and then uses the hibp api to check whether that password has+> -- been pwned.+> consoleHaveIBeenPwned :: IO ()+> consoleHaveIBeenPwned = do+>   runStdoutLoggingT $ do+>     mgr <- liftIO $ newManager tlsManagerSettings+>     p <- liftIO $ getPassword+>     let hibpEnv = HaveIBeenPwnedConfig mgr "https://api.pwnedpasswords.com/range"+>     p' <- flip runPwnedT hibpEnv $ haveIBeenPwned $ T.pack p+>     liftIO $ case p' of+>       HaveIBeenPwnedResult_Secure ->+>         putStrLn "Your password does not appear in any known breaches.  Practice good password hygene."+>       HaveIBeenPwnedResult_Pwned p'' ->+>         putStrLn $ "You have been pwned! Your password has appeared in breaches " ++ show p'' ++ " times."+>       HaveIBeenPwnedResult_Error ->+>         putStrLn "Network Error, try again later"+>+> getPassword :: IO String+> getPassword = do+>   putStr "Password: "+>   hFlush stdout+>   password <- withEcho False getLine+>   putChar '\n'+>   return password+>+> withEcho :: Bool -> IO a -> IO a+> withEcho echo action = do+>   old <- hGetEcho stdin+>   bracket_ (hSetEcho stdin echo) (hSetEcho stdin old) action+>+> main :: IO ()+> main = consoleHaveIBeenPwned++```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ haveibeenpwned.cabal view
@@ -0,0 +1,51 @@+cabal-version:      2.2+name:               haveibeenpwned+version:            0.2.0.0+synopsis:           Library for checking for weak/compromised passwords.+description:+  This library uses the haveibeenpwned database to check for weak or compromised passwords.++bug-reports:        https://github.com/obsidiansystems/haveibeenpwned/issues+license:            BSD-3-Clause+license-file:       LICENSE+author:             Obsidian Systems LLC+maintainer:         maintainer@obsidian.systems+copyright:          2019 Obsidian Systems LLC+category:           Web+extra-source-files: CHANGELOG.md+tested-with:        GHC ==8.6.5 || ==8.8.4++library+  exposed-modules:  HaveIBeenPwned+  build-depends:+    , base             >=4.11.0   && <4.15+    , bytestring       ^>=0.10.8+    , cryptonite       >=0.24     && <0.28+    , data-default     ^>=0.7.1+    , http-client      >=0.5.13.1 && <0.7+    , http-types       ^>=0.12.1+    , monad-logger     ^>=0.3.29+    , mtl              ^>=2.2.2+    , safe             ^>=0.3.17+    , text             ^>=1.2.3++  ghc-options:      -Wall+  hs-source-dirs:   src+  default-language: Haskell2010++executable readme+  build-depends:+    , base+    , haveibeenpwned+    , http-client+    , http-client-tls  ^>=0.3.5+    , monad-logger+    , text++  default-language: Haskell2010+  main-is:          README.lhs+  ghc-options:      -Wall -optL -q++source-repository head+  type:     git+  location: https://github.com/obsidiansystems/haveibeenpwned
+ src/HaveIBeenPwned.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Query haveibeenpwned database to check basic password strength in a secure way.+--+--   By checking new user passwords against a database of leaked passwords you+--   get some means for rejecting very weak or just leaked passwords.+module HaveIBeenPwned where++import "cryptonite" Crypto.Hash+import Control.Exception+import Control.Monad.Logger+import Control.Monad.Reader+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as LBS+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8)+import qualified Data.Text.Lazy as LT+import qualified Data.Text.Lazy.Encoding+import Network.HTTP.Client+import Network.HTTP.Types.Status (Status(..))+import Safe (readMay)++data HaveIBeenPwnedConfig = HaveIBeenPwnedConfig+  { _haveIBeenPwnedConfig_manager :: Manager+  , _haveIBeenPwnedConfig_apihost :: Text+  }++-- | Result of a password check.+--+--   It is either considered secure, insecure or we can't say because of an+--   error.+data HaveIBeenPwnedResult =+    HaveIBeenPwnedResult_Secure+    -- ^ We could not find the password in any database, thus it is considered+    -- "secure" as far as this library is concerned.+  | HaveIBeenPwnedResult_Pwned Int+    -- ^ How many times the password was found in public places. Usually this+    -- will be a value greater than 0, but in any case if you hit this+    -- constructor you must assume tha password has been leaked.+  | HaveIBeenPwnedResult_Error+    -- ^ The check failed for some reason. We can't say anything about the+    -- password quality.+  deriving (Eq, Ord, Show)++class Monad m => MonadPwned m where+  -- | Returns the number of disclosures the supplied password has been seen in.+  --+  -- If this is not zero, do not use the supplied password, it is known to hackers.+  -- If it *is* zero, it might still not be safe, only that if it is+  -- compromised, that is not yet known.+  --+  -- https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange+  haveIBeenPwned :: Text -> m HaveIBeenPwnedResult++newtype PwnedT m a = PwnedT { unPwnedT :: ReaderT HaveIBeenPwnedConfig m a }+  deriving (Functor, Applicative, Monad , MonadIO, MonadLogger+    , MonadTrans+    )++runPwnedT :: PwnedT m a -> HaveIBeenPwnedConfig -> m a+runPwnedT (PwnedT (ReaderT f)) = f++mapPwnedT :: (m a -> n b) -> PwnedT m a -> PwnedT n b+mapPwnedT f = PwnedT . mapReaderT f . unPwnedT++instance MonadReader r m => MonadReader r (PwnedT m) where+  ask = lift ask+  local = mapPwnedT . local+  reader = lift . reader++instance (MonadLogger m, MonadIO m) => MonadPwned (PwnedT m) where+ haveIBeenPwned password = do+  let (pfx, rest) = passwdDigest password+  cfg <- PwnedT ask+  let request = parseRequest_ $ T.unpack $ T.concat [_haveIBeenPwnedConfig_apihost cfg, "/", pfx]+  result' <- liftIO $ try $ httpLbs request (_haveIBeenPwnedConfig_manager cfg)+  case result' of+    Left err -> do+      $(logError) $ T.pack $ show @ HttpException $ err+      return HaveIBeenPwnedResult_Error+    Right result -> case responseStatus result of+      Status 200 _ -> do+        let r = parseHIBPResponse (responseBody result) rest+        case r of+          HaveIBeenPwnedResult_Error ->+            $(logError) $ "Parsing number of occurrences failed. (Not an Int)."+          _ -> pure ()+        pure r+      Status code phrase -> do+        $(logError) $ T.pack $ show $ Status code phrase+        return HaveIBeenPwnedResult_Error+++-- | Get the sha1 digest for the supplied password, split into two parts, to agree with the+--   hibp api.+passwdDigest :: Text -> (Text, Text)+passwdDigest passwd = (T.take 5 digest, T.drop 5 digest)+  where digest = T.toUpper $ T.pack $ show $ sha1 $ encodeUtf8 passwd+        sha1 :: ByteString -> Digest SHA1+        sha1 = hash++-- | The hibp response is a line separated list of colon separated hash+-- *suffixes* and a number indicating the number of times that password(hash)+-- has been seen in known publicly disclosed leaks+parseHIBPResponse :: LBS.ByteString -> Text -> HaveIBeenPwnedResult+parseHIBPResponse response suffix =+  let+    digests :: [(LT.Text, Maybe Int)]+    digests = fmap (fmap (readMay . LT.unpack . LT.drop 1) . LT.breakOn ":") $ LT.lines $ Data.Text.Lazy.Encoding.decodeUtf8 response+  in case filter ((LT.fromStrict suffix ==) . fst) digests of+    ((_,n):_) -> maybe HaveIBeenPwnedResult_Error HaveIBeenPwnedResult_Pwned n+    [] -> HaveIBeenPwnedResult_Secure