never executed always true always false
    1 module PureClaw.Security.Crypto
    2   ( -- * Encryption / decryption (AES-256-CTR via crypton)
    3     encrypt
    4   , decrypt
    5     -- * Cryptographic randomness
    6   , getRandomBytes
    7   , generateToken
    8     -- * Constant-time comparison
    9   , constantTimeEq
   10     -- * Hashing
   11   , sha256Hash
   12     -- * Errors
   13   , CryptoError (..)
   14   ) where
   15 
   16 import Crypto.Cipher.AES (AES256)
   17 import Crypto.Cipher.Types (cipherInit, ctrCombine, makeIV)
   18 import Crypto.Error (CryptoFailable (..))
   19 import Crypto.Hash (Digest, SHA256, hash)
   20 import Crypto.Random qualified as CR
   21 import Data.ByteArray (constEq, convert)
   22 import Data.ByteString (ByteString)
   23 import Data.ByteString qualified as BS
   24 import Data.ByteString.Base16 qualified as B16
   25 import Data.Text (Text)
   26 import Data.Text.Encoding qualified as TE
   27 
   28 import PureClaw.Security.Secrets
   29 
   30 -- | Errors that can occur during cryptographic operations.
   31 data CryptoError
   32   = InvalidKeyLength
   33   | InvalidIV
   34   | CipherInitFailed
   35   deriving stock (Show, Eq)
   36 
   37 -- | Encrypt plaintext using AES-256-CTR.
   38 -- The IV is prepended to the ciphertext so it can be recovered for decryption.
   39 -- Requires a 32-byte SecretKey and returns Left on invalid key/IV.
   40 encrypt :: SecretKey -> ByteString -> IO (Either CryptoError ByteString)
   41 encrypt key plaintext = withSecretKey key $ \rawKey -> do
   42   if BS.length rawKey /= 32
   43     then pure (Left InvalidKeyLength)
   44     else do
   45       iv <- CR.getRandomBytes 16
   46       case initCipher rawKey of
   47         Left err -> pure (Left err)
   48         Right cipher ->
   49           case makeIV (iv :: ByteString) of
   50             Nothing -> pure (Left InvalidIV)
   51             Just aesIV -> do
   52               let ciphertext = ctrCombine cipher aesIV plaintext
   53               pure (Right (iv <> ciphertext))
   54 
   55 -- | Decrypt ciphertext produced by 'encrypt'.
   56 -- Expects the IV prepended to the ciphertext (first 16 bytes).
   57 decrypt :: SecretKey -> ByteString -> Either CryptoError ByteString
   58 decrypt key ciphertextWithIV = withSecretKey key $ \rawKey ->
   59   if BS.length rawKey /= 32
   60     then Left InvalidKeyLength
   61     else if BS.length ciphertextWithIV < 16
   62       then Left InvalidIV
   63       else
   64         let (iv, ciphertext) = BS.splitAt 16 ciphertextWithIV
   65         in case initCipher rawKey of
   66           Left err -> Left err
   67           Right cipher ->
   68             case makeIV (iv :: ByteString) of
   69               Nothing -> Left InvalidIV
   70               Just aesIV -> Right (ctrCombine cipher aesIV ciphertext)
   71 
   72 -- | Generate cryptographically secure random bytes.
   73 getRandomBytes :: Int -> IO ByteString
   74 getRandomBytes = CR.getRandomBytes
   75 
   76 -- | Generate a hex-encoded random token of the given byte length.
   77 -- The output text will be 2x the byte length (hex encoding).
   78 generateToken :: Int -> IO Text
   79 generateToken n = do
   80   bytes <- CR.getRandomBytes n
   81   pure (TE.decodeUtf8 (B16.encode bytes))
   82 
   83 -- | Constant-time equality comparison for ByteStrings.
   84 -- Prevents timing attacks when comparing secrets.
   85 constantTimeEq :: ByteString -> ByteString -> Bool
   86 constantTimeEq = constEq
   87 
   88 -- | SHA-256 hash, returned as hex-encoded ByteString.
   89 sha256Hash :: ByteString -> ByteString
   90 sha256Hash bs = B16.encode (convert digest)
   91   where
   92     digest = hash bs :: Digest SHA256
   93 
   94 -- Internal: initialize an AES256 cipher from raw key bytes.
   95 initCipher :: ByteString -> Either CryptoError AES256
   96 initCipher rawKey =
   97   case cipherInit rawKey of
   98     CryptoPassed c  -> Right c
   99     CryptoFailed _  -> Left CipherInitFailed