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