{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      :  Crypto.Ecdsa.Signature
-- Copyright   :  Alexander Krupenkin 2018
-- License     :  BSD3
--
-- Maintainer  :  mail@akru.me
-- Stability   :  experimental
-- Portability :  portable
--
-- Recoverable ECC signature support.
--

module Crypto.Ecdsa.Signature
    (
      sign
    , pack
    , unpack
    ) where

import           Control.Monad               (when)
import           Crypto.Hash                 (SHA256)
import           Crypto.Number.Generate      (generateBetween)
import           Crypto.Number.ModArithmetic (inverse)
import           Crypto.Number.Serialize     (i2osp, os2ip)
import           Crypto.PubKey.ECC.ECDSA     (PrivateKey (..))
import           Crypto.PubKey.ECC.Prim      (pointMul)
import           Crypto.PubKey.ECC.Types     (CurveCommon (ecc_g, ecc_n),
                                              Point (..), common_curve)
import           Crypto.Random               (MonadRandom, withDRG)
import           Crypto.Random.HmacDrbg      (HmacDrbg, initialize)
import           Data.Bits                   (xor, (.|.))
import           Data.ByteArray              (ByteArray, ByteArrayAccess, Bytes,
                                              convert, singleton, takeView,
                                              view)
import qualified Data.ByteArray              as BA (unpack)
import           Data.Word                   (Word8)

import           Crypto.Ecdsa.Utils          (exportKey)

-- | Sign arbitrary data by given private key.
--
-- /WARNING:/ Vulnerable to timing attacks.
sign :: ByteArrayAccess bin
     => PrivateKey
     -> bin
     -> (Integer, Integer, Word8)
sign pk bin = fst $ withDRG hmac_drbg $ ecsign pk (os2ip truncated)
  where
    hmac_drbg :: HmacDrbg SHA256
    hmac_drbg = initialize $ exportKey pk <> truncated
    truncated = convert $ takeView bin 32 :: Bytes

ecsign :: MonadRandom m
       => PrivateKey
       -> Integer
       -> m (Integer, Integer, Word8)
ecsign pk@(PrivateKey curve d) z = do
    k <- generateBetween 0 (n - 1)
    case trySign k of
        Nothing  -> ecsign pk z
        Just rsv -> return rsv
  where
    n = ecc_n (common_curve curve)
    g = ecc_g (common_curve curve)
    recoveryParam x y r = fromIntegral $
        fromEnum (odd y) .|. if x /= r then 2 else 0
    trySign k = do
        (kpX, kpY) <- case pointMul curve k g of
            PointO    -> Nothing
            Point x y -> return (x, y)
        let r = kpX `mod` n
        kInv <- inverse k n
        let s = kInv * (z + r * d) `mod` n
        when (r == 0 || s == 0) Nothing
        -- Recovery param
        let v = recoveryParam kpX kpY r
        -- Use complement of s if it > n / 2
        let (s', v') | s > n `div` 2 = (n - s, v `xor` 1)
                     | otherwise = (s, v)
        return $ (r, s', v' + 27)

-- | Unpack recoverable signature from byte array.
--
-- Input array should have 65 byte length.
unpack :: ByteArrayAccess rsv => rsv -> (Integer, Integer, Word8)
unpack vrs = (r, s, v)
  where
    r = os2ip (view vrs 1 33)
    s = os2ip (view vrs 33 65)
    v = head (BA.unpack vrs)

-- | Pack recoverable signature as byte array (65 byte length).
pack :: ByteArray rsv => (Integer, Integer, Word8) -> rsv
pack (r, s, v) = i2osp r <> i2osp s <> singleton v
