{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} -- | Provides a data type ('SHA256') for efficient memory -- representation of a sha-256 hash value, together with helper -- functions for converting to and from that value. This module is -- intended to be imported qualified as @SHA256@. -- -- Some nomenclature: -- -- * Hashing calculates a new hash value from some input. @from@ takes a value that representats an existing hash. -- -- * Raw means a raw binary representation of the hash value, without any hex encoding. -- -- * Text always uses lower case hex encoding -- -- @since 0.1.0.0 module Pantry.SHA256 ( -- * Types SHA256 , SHA256Exception (..) -- * Hashing , hashFile , hashBytes , hashLazyBytes , sinkHash -- * Convert from a hash representation , fromHexText , fromHexBytes , fromDigest , fromRaw -- * Convert to a hash representation , toHexText , toHexBytes , toRaw ) where import RIO import Data.Aeson import Database.Persist.Sql import Pantry.Internal.StaticBytes import Conduit import qualified RIO.Text as T import qualified Crypto.Hash.Conduit as Hash (hashFile, sinkHash) import qualified Crypto.Hash as Hash (hash, hashlazy, Digest, SHA256) import qualified Data.ByteArray import qualified Data.ByteArray.Encoding as Mem -- | A SHA256 hash, stored in a static size for more efficient -- memory representation. -- -- @since 0.1.0.0 newtype SHA256 = SHA256 Bytes32 deriving (Generic, Eq, NFData, Data, Typeable, Ord, Hashable) -- | Exceptions which can occur in this module -- -- @since 0.1.0.0 data SHA256Exception = InvalidByteCount !ByteString !StaticBytesException | InvalidHexBytes !ByteString !Text deriving (Typeable) -- | Generate a 'SHA256' value by hashing the contents of a file. -- -- @since 0.1.0.0 hashFile :: MonadIO m => FilePath -> m SHA256 hashFile fp = fromDigest <$> Hash.hashFile fp -- | Generate a 'SHA256' value by hashing a @ByteString@. -- -- @since 0.1.0.0 hashBytes :: ByteString -> SHA256 hashBytes = fromDigest . Hash.hash -- | Generate a 'SHA256' value by hashing a lazy @ByteString@. -- -- @since 0.1.0.0 hashLazyBytes :: LByteString -> SHA256 hashLazyBytes = fromDigest . Hash.hashlazy -- | Generate a 'SHA256' value by hashing the contents of a stream. -- -- @since 0.1.0.0 sinkHash :: Monad m => ConduitT ByteString o m SHA256 sinkHash = fromDigest <$> Hash.sinkHash -- | Convert a base16-encoded 'Text' value containing a hash into a 'SHA256'. -- -- @since 0.1.0.0 fromHexText :: Text -> Either SHA256Exception SHA256 fromHexText = fromHexBytes . encodeUtf8 -- | Convert a base16-encoded 'ByteString' value containing a hash into a 'SHA256'. -- -- @since 0.1.0.0 fromHexBytes :: ByteString -> Either SHA256Exception SHA256 fromHexBytes hexBS = do mapLeft (InvalidHexBytes hexBS . T.pack) (Mem.convertFromBase Mem.Base16 hexBS) >>= fromRaw -- | Convert a 'Hash.Digest' into a 'SHA256' -- -- @since 0.1.0.0 fromDigest :: Hash.Digest Hash.SHA256 -> SHA256 fromDigest digest = case toStaticExact (Data.ByteArray.convert digest :: ByteString) of Left e -> error $ "Impossible failure in fromDigest: " ++ show (digest, e) Right x -> SHA256 x -- | Convert a raw representation of a hash into a 'SHA256'. -- -- @since 0.1.0.0 fromRaw :: ByteString -> Either SHA256Exception SHA256 fromRaw bs = either (Left . InvalidByteCount bs) (Right . SHA256) (toStaticExact bs) -- | Convert a 'SHA256' into a base16-encoded SHA256 hash. -- -- @since 0.1.0.0 toHexText :: SHA256 -> Text toHexText ss = case decodeUtf8' $ toHexBytes ss of Left e -> error $ "Impossible failure in staticSHA256ToText: " ++ show (ss, e) Right t -> t -- | Convert a 'SHA256' into a base16-encoded SHA256 hash. -- -- @since 0.1.0.0 toHexBytes :: SHA256 -> ByteString toHexBytes (SHA256 x) = Mem.convertToBase Mem.Base16 x -- | Convert a 'SHA256' into a raw binary representation. -- -- @since 0.1.0.0 toRaw :: SHA256 -> ByteString toRaw (SHA256 x) = Data.ByteArray.convert x -- Instances instance Show SHA256 where show s = "SHA256 " ++ show (toHexText s) instance PersistField SHA256 where toPersistValue = PersistByteString . toRaw fromPersistValue (PersistByteString bs) = case toStaticExact bs of Left e -> Left $ tshow e Right ss -> pure $ SHA256 ss fromPersistValue x = Left $ "Unexpected value: " <> tshow x instance PersistFieldSql SHA256 where sqlType _ = SqlBlob instance Display SHA256 where display = displayBytesUtf8 . toHexBytes instance ToJSON SHA256 where toJSON = toJSON . toHexText instance FromJSON SHA256 where parseJSON = withText "SHA256" $ \t -> case fromHexText t of Right x -> pure x Left e -> fail $ concat [ "Invalid SHA256 " , show t , ": " , show e ] instance Exception SHA256Exception instance Show SHA256Exception where show = T.unpack . utf8BuilderToText . display instance Display SHA256Exception where display (InvalidByteCount bs sbe) = "Invalid byte count creating a SHA256 from " <> displayShow bs <> ": " <> displayShow sbe display (InvalidHexBytes bs t) = "Invalid hex bytes creating a SHA256: " <> displayShow bs <> ": " <> display t