{-# LANGUAGE CPP #-}
module Crypto.PubKey.ECIES.Conduit
  ( encrypt
  , decrypt
  ) where

import           Control.Monad.Catch                  (MonadThrow, throwM)
import           Control.Monad.Trans.Class            (lift)
import qualified Crypto.Cipher.ChaCha                 as ChaCha
import qualified Crypto.Cipher.ChaChaPoly1305.Conduit as ChaCha
import qualified Crypto.ECC                           as ECC
import qualified Crypto.Error                         as CE
import           Crypto.Hash                          (SHA512 (..), hashWith)
import           Crypto.PubKey.ECIES                  (deriveDecrypt,
                                                       deriveEncrypt)
import           Crypto.Random                        (MonadRandom)
import qualified Data.ByteArray                       as BA
import           Data.ByteString                      (ByteString)
import qualified Data.ByteString                      as B
import qualified Data.ByteString.Lazy                 as BL
import           Data.Conduit                         (ConduitM, yield)
import qualified Data.Conduit.Binary                  as CB
import           Data.Proxy                           (Proxy (..))
import           System.IO.Unsafe                     (unsafePerformIO)

getNonceKey :: ECC.SharedSecret -> (ByteString, ByteString)
getNonceKey shared =
  let state1 = ChaCha.initializeSimple $ B.take 40 $ BA.convert $ hashWith SHA512 shared
      (nonce, state2) = ChaCha.generateSimple state1 12
      (key, _) = ChaCha.generateSimple state2 32
   in (nonce, key)

type Curve = ECC.Curve_P256R1

proxy :: Proxy Curve
proxy = Proxy

pointBinarySize :: Int
pointBinarySize = B.length $ ECC.encodePoint proxy point
  where
    point = unsafePerformIO (ECC.keypairGetPublic <$> ECC.curveGenerateKeyPair proxy)
{-# NOINLINE pointBinarySize #-}

throwOnFail :: MonadThrow m => CE.CryptoFailable a -> m a
throwOnFail (CE.CryptoPassed a) = pure a
throwOnFail (CE.CryptoFailed e) = throwM e


encrypt
  :: (MonadThrow m, MonadRandom m)
  => ECC.Point Curve
  -> ConduitM ByteString ByteString m ()
encrypt point = do
  (point', shared) <- lift (deriveEncryptCompat proxy point) >>= throwOnFail
  let (nonce, key) = getNonceKey shared
  yield $ ECC.encodePoint proxy point'
  ChaCha.encrypt nonce key
  where
#if MIN_VERSION_cryptonite(0,23,999)
    deriveEncryptCompat prx p = deriveEncrypt prx p
#else
    deriveEncryptCompat prx p = CE.CryptoPassed <$> deriveEncrypt prx p
#endif

decrypt
  :: (MonadThrow m)
  => ECC.Scalar Curve
  -> ConduitM ByteString ByteString m ()
decrypt scalar = do
  pointBS <- fmap BL.toStrict $ CB.take pointBinarySize
  point   <- throwOnFail (ECC.decodePoint proxy pointBS)
  shared  <- throwOnFail (deriveDecryptCompat proxy point scalar)
  let (_nonce, key) = getNonceKey shared
  ChaCha.decrypt key
  where
#if MIN_VERSION_cryptonite(0,23,999)
    deriveDecryptCompat prx p s = deriveDecrypt prx p s
#else
    deriveDecryptCompat prx p s = CE.CryptoPassed (deriveDecrypt prx p s)
#endif
