packages feed

crypto-srp-0.1.0.0: src/Crypto/SRP/PrimeGroup.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MultiWayIf #-}
{-# OPTIONS_HADDOCK prune #-}

{- |
Module      : Crypto.SRP.PrimeGroup
Copyright   : (c) 2025 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3

Provides the 'PrimeGroup' type representing the standard SRP prime groups
(1024–8192 bits) and the group-arithmetic operations used during the SRP
handshake: public key generation ('pubOf'), modular exponentiation
('modExpPrime'), and @'ByteString'@ encoding helpers.
-}
module Crypto.SRP.PrimeGroup
  ( -- * the PrimeGroups
    PrimeGroup (..)
  , generatorFor
  , safePrimeFor
  , asByteString
  , hexLength
  , byteLength
  , pubOf
  , padAs
  , primeMod
  , modExpPrime

    -- * SRP Integer \<=> ByteString interconversion
  , bytesOf
  , fromBytes
  )
where

import Crypto.SRP.Constants
  ( n1024Bits
  , n1536Bits
  , n2048Bits
  , n3072Bits
  , n4096Bits
  , n6144Bits
  , n8192Bits
  )
import Data.Bits (Bits (shiftR, testBit, (.&.)))
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Char (ord)
import Data.Word (Word8)
import Numeric.Natural (Natural)


-- | Represents the primeGroups used in SRP computations
data PrimeGroup
  = G1024
  | G1536
  | G2048
  | G3072
  | G4096
  | G6144
  | G8192
  deriving (Eq, Show)


-- | The generator for 'PrimeGroup'
generatorFor :: PrimeGroup -> Word8
generatorFor G1024 = 0x2
generatorFor G1536 = 0x2
generatorFor G2048 = 0x2
generatorFor G3072 = 0x5
generatorFor G4096 = 0x5
generatorFor G6144 = 0x5
generatorFor G8192 = 0x19


-- | The safe prime for 'PrimeGroup'
safePrimeFor :: PrimeGroup -> Integer
safePrimeFor G1024 = p1024
safePrimeFor G1536 = p1536
safePrimeFor G2048 = p2048
safePrimeFor G3072 = p3072
safePrimeFor G4096 = p4096
safePrimeFor G6144 = p6144
safePrimeFor G8192 = p8192


fromHexBS :: ByteString -> Integer
fromHexBS = BS.foldl' (\acc d -> acc * 16 + hexCharToInt d) 0


hexCharToInt :: Word8 -> Integer
hexCharToInt w =
  let wI = fromIntegral w :: Integer
      to0 = wI - fromIntegral (ord '0')
      toa = wI - fromIntegral (ord 'a')
      toA = wI - fromIntegral (ord 'A')
   in if
        | to0 >= 0 && to0 < 10 -> to0
        | toa >= 0 && toa < 6 -> toa + 10
        | toA >= 0 && toA < 6 -> toA + 10
        | otherwise -> error $ "fromHexBS: invalid hex byte " ++ show w


p1024, p1536, p2048, p3072, p4096, p6144, p8192 :: Integer
p1024 = fromHexBS n1024Bits
p1536 = fromHexBS n1536Bits
p2048 = fromHexBS n2048Bits
p3072 = fromHexBS n3072Bits
p4096 = fromHexBS n4096Bits
p6144 = fromHexBS n6144Bits
p8192 = fromHexBS n8192Bits


-- | A ByteString representing the safe prime in hexadecimal
asByteString :: PrimeGroup -> ByteString
asByteString G1024 = n1024Bits
asByteString G1536 = n1536Bits
asByteString G2048 = n2048Bits
asByteString G3072 = n3072Bits
asByteString G4096 = n4096Bits
asByteString G6144 = n6144Bits
asByteString G8192 = n8192Bits


-- | The number of hex characters in the representation of the safe prime
hexLength :: PrimeGroup -> Int
hexLength = BS.length . asByteString


-- | The number of bytes in the binary encoding of the safe prime
byteLength :: PrimeGroup -> Int
byteLength = (`div` 2) . hexLength


-- | Encode a @Natural@ number as a @ByteString@
bytesOf :: Natural -> ByteString
bytesOf 0 = BS.pack [0]
bytesOf n = BS.pack $ reverse (bytes n)
  where
    bytes 0 = []
    bytes x = fromIntegral (x .&. 0xFF) : bytes (shiftR x 8)


-- | Obtain an @Integer@ from its @ByteString@ encoding
fromBytes :: ByteString -> Integer
fromBytes = BS.foldl' (\acc b -> acc * 256 + fromIntegral b) 0


-- | Reduce an @Integer@ modulo the safe prime of a 'PrimeGroup'
primeMod :: Integer -> PrimeGroup -> Integer
primeMod num pg =
  let prime = safePrimeFor pg
   in num `mod` prime


{- | Pad a 'ByteString' to the binary byte length of the safe prime of a
'PrimeGroup', prepending zero bytes as needed.

Precondition: @'BS.length' bs <= 'byteLength' pg@. If @bs@ is longer than the
group's byte length, no truncation occurs — the input is returned unchanged.
-}
padAs :: ByteString -> PrimeGroup -> ByteString
padAs bs pg =
  let padLength = byteLength pg - BS.length bs
   in BS.replicate padLength 0 <> bs


{- | Generate the public version of a private ephemeral key

the private version of the key is expected to be a randomly generated integer
-}
pubOf :: Integer -> PrimeGroup -> Integer
pubOf priv pg = modExpPrime (fromIntegral (generatorFor pg)) priv pg


{- | Perform exponentiation modulus the large number in a 'PrimeGroup'

Example

  > modExpPrime base power G2048
-}
modExpPrime :: Integer -> Integer -> PrimeGroup -> Integer
modExpPrime base power pg = modExp base power (safePrimeFor pg)


modExp :: Integer -> Integer -> Integer -> Integer
modExp base expn m = go 1 (base `mod` m) expn
  where
    go !acc !b !e
      | e == 0 = acc
      | otherwise =
          let acc' = if testBit e 0 then (acc * b) `mod` m else acc
           in go acc' ((b * b) `mod` m) (shiftR e 1)