never executed always true always false
1 module PureClaw.Security.Vault.Passphrase
2 ( mkPassphraseVaultEncryptor
3 ) where
4
5 import Control.Concurrent.STM
6 import Control.Monad.Trans.Except (runExceptT)
7 import Crypto.Age (decrypt, encrypt)
8 import Crypto.Age.Identity (Identity (..), ScryptIdentity (..))
9 import Crypto.Age.Recipient (Recipients (..), ScryptRecipient (..))
10 import Crypto.Age.Scrypt (Passphrase (..), WorkFactor, bytesToSalt, mkWorkFactor)
11 import Crypto.Random (getRandomBytes)
12 import Data.ByteArray (convert)
13 import Data.ByteString (ByteString)
14 import Data.List.NonEmpty (NonEmpty (..))
15 import Data.Maybe (fromJust)
16 import Data.Text qualified as T
17
18 import PureClaw.Security.Vault.Age (VaultEncryptor (..), VaultError (..))
19
20 -- | scrypt work factor (N = 2^22), matching the age CLI default.
21 ageWorkFactor :: WorkFactor
22 ageWorkFactor = fromJust (mkWorkFactor 22)
23
24 -- | Convert a passphrase 'ByteString' to an age 'Passphrase'.
25 toAgePass :: ByteString -> Passphrase
26 toAgePass bs = Passphrase (convert bs)
27
28 -- | Create a passphrase-based vault encryptor using the age encryption format.
29 -- The resulting ciphertext is a standard age binary file, compatible with
30 -- @age -d --passphrase@.
31 -- The IO action is called at most once to obtain the passphrase, then cached.
32 mkPassphraseVaultEncryptor :: IO ByteString -> IO VaultEncryptor
33 mkPassphraseVaultEncryptor getPass = do
34 cache <- newTVarIO Nothing
35 let getOrPrompt = do
36 c <- readTVarIO cache
37 case c of
38 Just p -> pure p
39 Nothing -> do
40 p <- getPass
41 atomically (writeTVar cache (Just p))
42 pure p
43 pure VaultEncryptor
44 { _ve_encrypt = \plaintext -> do
45 passphrase <- getOrPrompt
46 saltBytes <- getRandomBytes 16
47 case bytesToSalt saltBytes of
48 Nothing -> pure (Left (VaultCorrupted "salt generation failed"))
49 Just salt -> do
50 let recipient = ScryptRecipient
51 { srPassphrase = toAgePass passphrase
52 , srSalt = salt
53 , srWorkFactor = ageWorkFactor
54 }
55 result <- runExceptT (encrypt (RecipientsScrypt recipient) plaintext)
56 case result of
57 Left err -> pure (Left (VaultCorrupted ("age encrypt: " <> T.pack (show err))))
58 Right ct -> pure (Right ct)
59 , _ve_decrypt = \ciphertext -> do
60 passphrase <- getOrPrompt
61 let identity = ScryptIdentity
62 { siPassphrase = toAgePass passphrase
63 , siMaxWorkFactor = ageWorkFactor
64 }
65 identities = IdentityScrypt identity :| []
66 case decrypt identities ciphertext of
67 Left _ -> pure (Left (VaultCorrupted "wrong passphrase"))
68 Right pt -> pure (Right pt)
69 }