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 +12/−0
- LICENSE +30/−0
- README.lhs +68/−0
- Setup.hs +2/−0
- haveibeenpwned.cabal +51/−0
- src/HaveIBeenPwned.hs +120/−0
+ 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">+======================+[](https://haskell.org) [](https://hackage.haskell.org/package/haveibeenpwned) [](https://matrix.hackage.haskell.org/#/package/haveibeenpwned) [](https://github.com/obsidiansystems/haveibeenpwned/actions) [](https://travis-ci.org/obsidiansystems/haveibeenpwned) [](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