never executed always true always false
    1 module PureClaw.Security.Vault.Passphrase
    2   ( mkPassphraseVaultEncryptor
    3   ) where
    4 
    5 import Control.Concurrent.STM
    6 import Crypto.KDF.PBKDF2 (Parameters (..), fastPBKDF2_SHA256)
    7 import Crypto.Random (getRandomBytes)
    8 import Data.ByteString (ByteString)
    9 import Data.ByteString qualified as BS
   10 
   11 import PureClaw.Security.Crypto (decrypt, encrypt)
   12 import PureClaw.Security.Secrets (mkSecretKey)
   13 import PureClaw.Security.Vault.Age (VaultEncryptor (..), VaultError (..))
   14 
   15 -- Magic header identifying passphrase-encrypted vault files.
   16 magicHeader :: ByteString
   17 magicHeader = "PCLAWPW1"
   18 
   19 -- Magic prefix prepended to plaintext before encryption for passphrase verification.
   20 checkMagic :: ByteString
   21 checkMagic = "PCLAWCHK"
   22 
   23 saltLen :: Int
   24 saltLen = 32
   25 
   26 pbkdf2Params :: Parameters
   27 pbkdf2Params = Parameters { iterCounts = 100_000, outputLength = 32 }
   28 
   29 -- | Derive a 32-byte encryption key from a passphrase and salt using PBKDF2-SHA256.
   30 deriveKey :: ByteString -> ByteString -> ByteString
   31 deriveKey = fastPBKDF2_SHA256 pbkdf2Params
   32 
   33 -- | Create a passphrase-based vault encryptor.
   34 -- The IO action is called at most once to obtain the passphrase, then cached.
   35 mkPassphraseVaultEncryptor :: IO ByteString -> IO VaultEncryptor
   36 mkPassphraseVaultEncryptor getPass = do
   37   cache <- newTVarIO Nothing
   38   let getOrPrompt = do
   39         c <- readTVarIO cache
   40         case c of
   41           Just p  -> pure p
   42           Nothing -> do
   43             p <- getPass
   44             atomically (writeTVar cache (Just p))
   45             pure p
   46   pure VaultEncryptor
   47     { _ve_encrypt = \plaintext -> do
   48         passphrase <- getOrPrompt
   49         salt <- getRandomBytes saltLen
   50         let key = deriveKey passphrase salt
   51         result <- encrypt (mkSecretKey key) (checkMagic <> plaintext)
   52         case result of
   53           Left  _          -> pure (Left (VaultCorrupted "encryption failed"))
   54           Right ciphertext -> pure (Right (magicHeader <> salt <> ciphertext))
   55     , _ve_decrypt = \ciphertext -> do
   56         passphrase <- getOrPrompt
   57         let (hdr, rest) = BS.splitAt (BS.length magicHeader) ciphertext
   58         if hdr /= magicHeader
   59           then pure (Left (VaultCorrupted "not a passphrase-encrypted vault"))
   60           else do
   61             let (salt, encrypted) = BS.splitAt saltLen rest
   62             if BS.length salt < saltLen
   63               then pure (Left (VaultCorrupted "truncated vault file"))
   64               else do
   65                 let key = deriveKey passphrase salt
   66                 case decrypt (mkSecretKey key) encrypted of
   67                   Left  _ -> pure (Left (VaultCorrupted "decryption failed"))
   68                   Right plain ->
   69                     if checkMagic `BS.isPrefixOf` plain
   70                       then pure (Right (BS.drop (BS.length checkMagic) plain))
   71                       else pure (Left (VaultCorrupted "wrong passphrase"))
   72     }