packages feed

wai-cryptocookie-0.1: lib/Wai/CryptoCookie/Encryption.hs

{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoFieldSelectors #-}

-- | You will need to import this module if you are planning to define an
-- 'Encryption' scheme other than the defaults provided by this library.
module Wai.CryptoCookie.Encryption
   ( Encryption (..)
   , autoKeyFileBase16
   , readKeyFileBase16
   , readKeyFile
   , writeKeyFile
   ) where

import Control.Exception qualified as Ex
import Control.Monad
import Control.Monad.IO.Class
import Crypto.Random qualified as C
import Data.Aeson qualified as Ae
import Data.Bits
import Data.ByteArray qualified as BA
import Data.ByteArray.Encoding qualified as BA
import Data.ByteArray.Sized qualified as BAS
import Data.ByteString.Lazy qualified as BL
import Data.Char qualified as Char
import Data.Kind (Type)
import Data.Text.Encoding qualified as T
import Data.Word
import GHC.TypeNats
import System.IO qualified as IO
import System.IO.Error qualified as IO

-- | Encryption method.
class (KnownNat (KeyLength e), Eq (Key e)) => Encryption (e :: k) where
   -- | Key used for encryption. You can obtain an initial random
   -- 'Key' using 'genKey'. As long as you have access to
   -- said 'Key', you will be able to decrypt data previously
   -- encrypted with it. For this reason, be sure to save and load the key
   -- using 'keyToBytes' and 'keyFromBytes'.
   data Key e :: Type

   -- | Statically known 'Key' length.
   type KeyLength e :: Natural

   -- | Encryption context used by 'encrypt'.
   data Encrypt e :: Type

   -- | Decryption context used by 'decrypt'.
   data Decrypt e :: Type

   -- | Generate a random encryption 'Key'.
   genKey :: (C.MonadRandom m) => m (Key e)

   -- | Load a 'Key' from its bytes representation, if possible.
   keyFromBytes :: (BA.ByteArrayAccess raw) => raw -> Either String (Key e)

   -- | Dump the bytes representation of a 'Key'.
   keyToBytes :: (BAS.ByteArrayN (KeyLength e) raw) => Key e -> raw

   -- | Generate initial 'Encrypt'ion and 'Decrypt'ion context for a 'Key'.
   --
   -- The 'Encrypt'ion context could carry for example the next
   -- __randomly generated nonce__ to use for 'encrypt'ion, the 'Key'
   -- itself or its derivative used during the actual 'encrypt'ion
   -- process, or a deterministic random number generator.
   --
   -- The 'Decrypt'ion context could carry for example the 'Key' itself or its
   -- derivative used during the 'decrypt'ion process.
   initial :: (C.MonadRandom m) => Key e -> m (Encrypt e, Decrypt e)

   -- | After each 'encrypt'ion, the 'Encrypt'ion context will be automatically
   -- 'advance'd through this function. For example, if your 'Encrypt'ion
   -- context carries a nonce or a deterministic random number generator,
   -- this is the place to update them.
   advance :: Encrypt e -> Encrypt e

   -- | Encrypt a plaintext message according to the 'Encrypt'ion context.
   encrypt :: Encrypt e -> BL.ByteString -> BL.ByteString

   -- | Decrypt a message according to the 'Decrypt'ion context.
   --
   -- The 'String' is for internal debugging purposes only.
   decrypt :: Decrypt e -> BL.ByteString -> Either String BL.ByteString

-- | If the 'FilePath' exists, then read the base-16 representation of
-- a 'Key' from it. Ignores trailing newlines.
--
-- Otherwise, generate a random new 'Key' and write its base-16 representation
-- in the 'FilePath'.
--
-- Finally, return the 'Key'.
autoKeyFileBase16
   :: forall e m
    . (Encryption e, MonadIO m)
   => FilePath
   -> m (Key e)
autoKeyFileBase16 path = liftIO do
   Ex.catchJust
      (guard . IO.isDoesNotExistError)
      (readKeyFileBase16 path)
      \_ -> do
         k0 <- genKey
         writeKeyFile (BA.convertToBase BA.Base16) path k0
         k1 <- readKeyFileBase16 path
         when (k0 /= k1) $ fail "autoKeyFile: no roundtrip"
         pure k1

-- | Read a base-16 encoded 'Key' from a file. Ignores trailing newlines.
readKeyFileBase16
   :: forall e m
    . (Encryption e, MonadIO m)
   => FilePath
   -> m (Key e)
readKeyFileBase16 = readKeyFile \a ->
   case BA.span (not . rn) a of
      (pre, pos)
         | BA.all rn pos -> BA.convertFromBase BA.Base16 pre
         | otherwise -> Left "invalid format"
  where
   _r :: Word8 = fromIntegral (Char.ord '\r')
   _n :: Word8 = fromIntegral (Char.ord '\n')
   rn :: Word8 -> Bool = \w -> w == _r || w == _n

-- | Read a 'Key' from a file.
readKeyFile
   :: forall e m
    . (Encryption e, MonadIO m)
   => (BA.ScrubbedBytes -> Either String BA.ScrubbedBytes)
   -- ^ Convert the raw content of the file into input suitable
   -- for 'keyFromBytes'.
   -> FilePath
   -> m (Key e)
readKeyFile g path = liftIO do
   IO.withFile path IO.ReadMode \h -> do
      flen :: Int <- do
         a <- IO.hFileSize h
         case toIntegralSized a of
            Just b | b > 0 -> pure b
            _ -> fail "readKeyFile: invalid key file size"
      (rlen, fraw) <- BA.allocRet flen \p -> IO.hGetBuf h p flen
      when (rlen /= flen) do
         -- This shouldn't happen, but we are being extra careful.
         fail "readKeyFile: could not read key file"
      case g fraw of
         Left e -> fail $ "readKeyFile: " <> e
         Right kraw -> case keyFromBytes kraw of
            Right key -> pure key
            Left err -> fail $ "readKeyFile: " <> err

-- | Save a key to a file.
writeKeyFile
   :: forall e m
    . (Encryption e, MonadIO m)
   => (BAS.SizedByteArray (KeyLength e) BA.ScrubbedBytes -> BA.ScrubbedBytes)
   -- ^ Convert the raw 'keyToBytes' bytes to file contents.
   -> FilePath
   -> Key e
   -> m ()
writeKeyFile g path key = liftIO do
   kout <- Ex.evaluate $ g $ keyToBytes key
   IO.withFile path IO.WriteMode \h ->
      BA.withByteArray kout \p ->
         IO.hPutBuf h p $ BA.length kout

-- | Base-16 encoded.
instance (Encryption e) => Ae.FromJSON (Key e) where
   parseJSON = Ae.withText "Key" \t ->
      -- Note that un-scrubbable bytes will continue to exist in @t@.
      case BA.convertFromBase BA.Base16 (T.encodeUtf8 t) of
         Right (kraw :: BA.ScrubbedBytes) ->
            case keyFromBytes kraw of
               Right key -> pure key
               Left err -> fail err
         _ -> fail "Invalid key"