packages feed

paseto-0.1.1.0: src/Crypto/Paseto/Keys/V3/Internal.hs

module Crypto.Paseto.Keys.V3.Internal
  ( isScalarValid
  , encodeScalar
  , ScalarDecodingError (..)
  , renderScalarDecodingError
  , decodeScalar

  , encodePointUncompressed
  , encodePointCompressed
  , UncompressedPointDecodingError (..)
  , renderUncompressedPointDecodingError
  , decodePointUncompressed
  , CompressedPointDecodingError (..)
  , renderCompressedPointDecodingError
  , decodePointCompressed
  , fromPrivateKey
  ) where

import Control.Monad ( when )
import qualified Crypto.Number.Basic as Crypto.Number
import qualified Crypto.Number.ModArithmetic as Crypto.Number
import qualified Crypto.Number.Serialize as Crypto.Number
import qualified Crypto.PubKey.ECC.ECDSA as ECC.ECDSA
import qualified Crypto.PubKey.ECC.Prim as ECC
import qualified Crypto.PubKey.ECC.Types as ECC
import Data.ByteArray ( ScrubbedBytes )
import qualified Data.ByteArray as BA
import Data.ByteString ( ByteString )
import qualified Data.ByteString as BS
import Data.Text ( Text )
import qualified Data.Text as T
import Data.Word ( Word8 )
import Prelude

curveOrderBytes :: ECC.Curve -> Int
curveOrderBytes curve =
  (Crypto.Number.numBits (ECC.ecc_n $ ECC.common_curve curve) + 7) `div` 8

-- | Whether an elliptic curve scalar value is valid.
isScalarValid :: ECC.Curve -> Integer -> Bool
isScalarValid curve s = s > 0 && s < n
  where
    n :: Integer
    n = (ECC.ecc_n $ ECC.common_curve curve)

-- | Encode an elliptic curve scalar value.
encodeScalar :: ECC.Curve -> Integer -> ScrubbedBytes
encodeScalar curve = Crypto.Number.i2ospOf_ (curveOrderBytes curve)

-- | Error decoding a scalar value.
data ScalarDecodingError
  = -- | Invalid scalar length.
    ScalarDecodingInvalidLengthError
      -- | Expected length
      !Int
      -- | Actual length
      !Int
  | -- | Decoded scalar is invalid for the curve.
    ScalarDecodingInvalidError
  deriving stock (Show, Eq)

-- | Render a 'ScalarDecodingError' as 'Text'.
renderScalarDecodingError :: ScalarDecodingError -> Text
renderScalarDecodingError err =
  case err of
    ScalarDecodingInvalidLengthError expected actual ->
      "Decoded scalar value is of length "
        <> T.pack (show actual)
        <> ", but was expected to be "
        <> T.pack (show expected)
        <> "."
    ScalarDecodingInvalidError -> "Decoded scalar value is invalid for the curve."

-- | Decode an elliptic curve scalar value.
decodeScalar :: ECC.Curve -> ScrubbedBytes -> Either ScalarDecodingError Integer
decodeScalar curve bs
  | expectedLen /= actualLen = Left (ScalarDecodingInvalidLengthError expectedLen actualLen)
  | otherwise =
      let s = Crypto.Number.os2ip bs
      in if isScalarValid curve s then Right s else Left ScalarDecodingInvalidError
  where
    expectedLen :: Int
    expectedLen = curveOrderBytes curve

    actualLen :: Int
    actualLen = BA.length bs

-- | Encode an elliptic curve point into its uncompressed binary format as
-- defined by [SEC 1](https://www.secg.org/sec1-v2.pdf) and
-- [RFC 5480 section 2.2](https://datatracker.ietf.org/doc/html/rfc5480#section-2.2).
--
-- Note that this function will only accept a point on an elliptic curve over
-- 𝔽p (i.e. 'ECC.CurvePrime').
encodePointUncompressed :: ECC.CurvePrime -> ECC.Point -> ByteString
encodePointUncompressed curvePrime point
  | ECC.isPointValid curve point =
      case point of
        ECC.Point x y -> do
          let size = ECC.curveSizeBits (ECC.CurveFP curvePrime) `div` 8
          BS.concat
            [ BS.singleton 0x04
            , Crypto.Number.i2ospOf_ size x
            , Crypto.Number.i2ospOf_ size y
            ]
        ECC.PointO -> error "encodePointUncompressed: cannot encode point at infinity"
  | otherwise = error "encodePointUncompressed: point is invalid"
  where
    curve :: ECC.Curve
    curve = ECC.CurveFP curvePrime

-- | Encode an elliptic curve point into its compressed binary format as
-- defined by [SEC 1](https://www.secg.org/sec1-v2.pdf).
--
-- Note that this function will only accept a point on an elliptic curve over
-- 𝔽p (i.e. 'ECC.CurvePrime').
--
-- Adapted from
-- [cryptonite issue #302](https://github.com/haskell-crypto/cryptonite/issues/302#issue-531003322).
encodePointCompressed :: ECC.CurvePrime -> ECC.Point -> ByteString
encodePointCompressed curvePrime point
  | ECC.isPointValid curve point =
      case point of
        -- We are using `i2ospOf_` because `curveSizeBits` ensures that
        -- the number won't have more than that many bytes.
        ECC.Point x y -> prefix y <> Crypto.Number.i2ospOf_ (ECC.curveSizeBits curve `div` 8) x
        ECC.PointO -> error "encodePointCompressed: cannot encode point at infinity"
  | otherwise = error "encodePointCompressed: point is invalid"
  where
    prefix :: Integer -> ByteString
    prefix y
      | odd y = BS.singleton 0x03
      | otherwise = BS.singleton 0x02

    curve :: ECC.Curve
    curve = ECC.CurveFP curvePrime

-- | Error decoding an uncompressed elliptic curve point.
data UncompressedPointDecodingError
  = -- | Prefix is not the expected value (@0x04@).
    UncompressedPointDecodingInvalidPrefixError
      -- | Invalid prefix which was encountered.
      !Word8
  | -- | Length of the provided point is invalid.
    UncompressedPointDecodingInvalidLengthError
      -- | Expected length
      !Int
      -- | Actual length
      !Int
  | -- | Point is invalid for the curve.
    UncompressedPointDecodingInvalidPointError !ECC.Point
  deriving stock (Show, Eq)

-- | Render an 'UncompressedPointDecodingError' as 'Text'.
renderUncompressedPointDecodingError :: UncompressedPointDecodingError -> Text
renderUncompressedPointDecodingError err =
  case err of
    UncompressedPointDecodingInvalidPrefixError invalidPrefix ->
      "Expected prefix "
        <> T.pack (show (0x04 :: Word8))
        <> " for uncompressed point, but encountered "
        <> T.pack (show invalidPrefix)
        <> "."
    UncompressedPointDecodingInvalidLengthError expected actual ->
      "Decoded point length is expected to be "
        <> T.pack (show expected)
        <> ", but it was "
        <> T.pack (show actual)
        <> "."
    UncompressedPointDecodingInvalidPointError _ ->
      "Decoded point is invalid for the curve."

-- | Decode an elliptic curve point from its uncompressed binary format as
-- defined by [SEC 1](https://www.secg.org/sec1-v2.pdf) and
-- [RFC 5480 section 2.2](https://datatracker.ietf.org/doc/html/rfc5480#section-2.2).
--
-- Note that this function will only decode a point on an elliptic curve over
-- 𝔽p (i.e. 'ECC.CurvePrime').
decodePointUncompressed :: ECC.CurvePrime -> ByteString -> Either UncompressedPointDecodingError ECC.Point
decodePointUncompressed curvePrime bs = do
  let expectedPointLen :: Int
      expectedPointLen = 1 + ((ECC.curveSizeBits (ECC.CurveFP curvePrime) `div` 8) * 2)

      actualPointLen :: Int
      actualPointLen = BS.length bs

  when
    (expectedPointLen /= actualPointLen)
    (Left $ UncompressedPointDecodingInvalidLengthError expectedPointLen actualPointLen)

  case BS.uncons bs of
    Nothing -> Left (UncompressedPointDecodingInvalidLengthError expectedPointLen 0)
    Just (prefix, rest)
      | prefix == 0x04 ->
          let (xBs, yBs) = BS.splitAt actualPointLen rest
              x = Crypto.Number.os2ip xBs
              y = Crypto.Number.os2ip yBs
              point = ECC.Point x y
          in if ECC.isPointValid (ECC.CurveFP curvePrime) point
            then Right point
            else Left (UncompressedPointDecodingInvalidPointError point)
      | otherwise -> Left (UncompressedPointDecodingInvalidPrefixError prefix)

-- | Error decoding a compressed elliptic curve point.
data CompressedPointDecodingError
  = -- | Prefix is not either of the expected values (@0x02@ or @0x03@).
    CompressedPointDecodingInvalidPrefixError
      -- | Invalid prefix which was encountered.
      !Word8
  | -- | Length of the provided compressed point is invalid.
    CompressedPointDecodingInvalidLengthError
      -- | Expected length
      !Int
      -- | Actual length
      !Int
  | -- | Failed to find the modular square root of a value.
    CompressedPointDecodingModularSquareRootError
  | -- | Point is invalid for the curve.
    CompressedPointDecodingInvalidPointError !ECC.Point
  deriving stock (Show, Eq)

-- | Render an 'CompressedPointDecodingError' as 'Text'.
renderCompressedPointDecodingError :: CompressedPointDecodingError -> Text
renderCompressedPointDecodingError err =
  case err of
    CompressedPointDecodingInvalidPrefixError invalidPrefix ->
      "Expected prefix of either "
        <> T.pack (show (0x02 :: Word8))
        <> " or "
        <> T.pack (show (0x03 :: Word8))
        <> " for compressed point, but encountered "
        <> T.pack (show invalidPrefix)
        <> "."
    CompressedPointDecodingInvalidLengthError expected actual ->
      "Decoded point length is expected to be "
        <> T.pack (show expected)
        <> ", but it was "
        <> T.pack (show actual)
        <> "."
    CompressedPointDecodingModularSquareRootError ->
      "Failed to recover the y-coordinate from the compressed point."
    CompressedPointDecodingInvalidPointError _ ->
      "Decoded point is invalid for the curve."

data EvenOrOddY
  = EvenY
  | OddY

toEvenOrOddY :: Word8 -> Maybe EvenOrOddY
toEvenOrOddY 0x02 = Just EvenY
toEvenOrOddY 0x03 = Just OddY
toEvenOrOddY _ = Nothing

-- | Decode an elliptic curve point from its compressed binary format as
-- defined by [SEC 1](https://www.secg.org/sec1-v2.pdf) and
-- [RFC 5480 section 2.2](https://datatracker.ietf.org/doc/html/rfc5480#section-2.2).
--
-- Note that this function will only decode a point on an elliptic curve over
-- 𝔽p (i.e. 'ECC.CurvePrime').
--
-- Thanks to
-- [cryptonite PR #303](https://github.com/haskell-crypto/cryptonite/pull/303),
-- there's a function that we can use to compute a square root modulo a prime
-- number ('Crypto.Number.squareRoot').
decodePointCompressed :: ECC.CurvePrime -> ByteString -> Either CompressedPointDecodingError ECC.Point
decodePointCompressed curvePrime@(ECC.CurvePrime p curveCommon) bs = do
  let expectedCompressedPointLen :: Int
      expectedCompressedPointLen = 1 + (ECC.curveSizeBits (ECC.CurveFP curvePrime) `div` 8)

      actualCompressedPointLen :: Int
      actualCompressedPointLen = BS.length bs

  when
    (expectedCompressedPointLen /= actualCompressedPointLen)
    (Left $ CompressedPointDecodingInvalidLengthError expectedCompressedPointLen actualCompressedPointLen)

  case BS.uncons bs of
    Just (prefix, rest) ->
      case toEvenOrOddY prefix of
        Nothing -> Left (CompressedPointDecodingInvalidPrefixError prefix)
        Just evenOrOddY -> do
          let x :: Integer
              x = Crypto.Number.os2ip rest

              b :: Integer
              b = ECC.ecc_b curveCommon

          y <-
            case Crypto.Number.squareRoot p ((x ^ (3 :: Integer)) - (x * 3) + b) of
              Nothing -> Left CompressedPointDecodingModularSquareRootError
              Just y' ->
                case (evenOrOddY, odd y') of
                  (EvenY, True) -> Right (p - y')
                  (OddY, False) -> Right (p - y')
                  _ -> Right y'

          let point :: ECC.Point
              point = ECC.Point x y
          if ECC.isPointValid (ECC.CurveFP curvePrime) point
            then Right point
            else Left (CompressedPointDecodingInvalidPointError point)
    Nothing ->
      -- This should be impossible since we checked the length beforehand.
      Left (CompressedPointDecodingInvalidLengthError expectedCompressedPointLen 0)

-- | Construct the 'ECC.ECDSA.PublicKey' which corresponds to a given
-- 'ECC.ECDSA.PrivateKey'.
fromPrivateKey :: ECC.ECDSA.PrivateKey -> ECC.ECDSA.PublicKey
fromPrivateKey (ECC.ECDSA.PrivateKey curve d) =
  ECC.ECDSA.PublicKey curve (ECC.pointBaseMul curve d)