packages feed

crypto-srp-0.1.0.0: test/Crypto/SRPSpec.hs

{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}

module Crypto.SRPSpec
  ( spec
  )
where

import Crypto.SRP
  ( FromClient (..)
  , FromServer (..)
  , KnownAlgorithm (..)
  , Results (..)
  , bytesOf
  , calcResults
  , digestSize
  , fromBytes
  , hash
  , verifyServerProof
  )
import Crypto.SRP.Constants
  ( n1024Bits
  , n1536Bits
  , n2048Bits
  , n3072Bits
  , n4096Bits
  , n6144Bits
  , n8192Bits
  )
import Crypto.SRP.Hashing (calcClientX, calcK)
import Crypto.SRP.PrimeGroup
  ( PrimeGroup (..)
  , byteLength
  , hexLength
  , padAs
  , safePrimeFor
  )
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Char (ord)
import Data.Word (Word8)
import Fmt (build, fmt, hexF, (+|), (|+))
import Numeric (readHex)
import Numeric.Natural (Natural)
import Test.Hspec (Spec, context, describe, expectationFailure, it, shouldBe)
import Test.QuickCheck
  ( Property
  , chooseInteger
  , forAll
  )


spec :: Spec
spec = describe "module Crypto.SRP.Constants" $ do
  largeNumberSpec
  viaBytesSpec
  primeGroupSpec
  hashingSpec
  rfc5054Spec


viaBytes :: Natural -> Integer
viaBytes = fromBytes . bytesOf


max64Bit :: Integer
max64Bit = (2 ^ (63 :: Int)) - 1


prop_roundtripViaBytes :: Property
prop_roundtripViaBytes = forAll (chooseInteger (0, max64Bit)) $ \anInteger ->
  viaBytes (fromIntegral anInteger) == anInteger


viaBytesSpec :: Spec
viaBytesSpec = describe "roundtrip bytesOf then fromBytes" $ do
  context "for any 64-bit integer" $ do
    it "should succeed" prop_roundtripViaBytes


largeNumberSpec :: Spec
largeNumberSpec = describe "the fixed large numbers" $ do
  oneNumberSpec n1024Bits 1024
  oneNumberSpec n1536Bits 1536
  oneNumberSpec n2048Bits 2048
  oneNumberSpec n3072Bits 3072
  oneNumberSpec n4096Bits 4096
  oneNumberSpec n6144Bits 6144
  oneNumberSpec n8192Bits 8192


oneNumberSpec :: ByteString -> Int -> Spec
oneNumberSpec b bitSize = do
  context ("the ByteString representing the " +| bitSize |+ " bit number") $ do
    context "each byte" $ do
      it "should be a valid hexadecimal value" $ isAllHex b
    it "should roundtrip with its integer value" $ fromHexBS b == fromHexBS (bsShow (fromHexBS b))
    context "hexLength" $ do
      it "should be consistent with the number of bits" $ BS.length b == bitSize `div` 4


hashingSpec :: Spec
hashingSpec = describe "module Crypto.SRP.Hashing" $ do
  context "digestSize" $ do
    it "SHA1   is 20 bytes" $ digestSize SHA1 == 20
    it "SHA256 is 32 bytes" $ digestSize SHA256 == 32
    it "SHA384 is 48 bytes" $ digestSize SHA384 == 48
    it "SHA512 is 64 bytes" $ digestSize SHA512 == 64
  context "hash output length" $ do
    it "SHA1   produces digestSize bytes" $ BS.length (hash SHA1 "x") == fromIntegral (digestSize SHA1)
    it "SHA256 produces digestSize bytes" $ BS.length (hash SHA256 "x") == fromIntegral (digestSize SHA256)
    it "SHA384 produces digestSize bytes" $ BS.length (hash SHA384 "x") == fromIntegral (digestSize SHA384)
    it "SHA512 produces digestSize bytes" $ BS.length (hash SHA512 "x") == fromIntegral (digestSize SHA512)


-- RFC 5054 Appendix B test vectors (G1024 / SHA1).
-- Private ephemeral values a and b are fixed so the exchange is deterministic.
rfc5054Spec :: Spec
rfc5054Spec = describe "RFC 5054 Appendix B" $ do
  let salt = hexBS "BEB25379D1A8581EB5A727673A2441EE"
      aPriv = fromHexBS "60975527035CF2AD1989806F0407210BC81EDC04E2762A56AFD529DDDA2D4393"
      aPub =
        hexBS
          "61d5e490f6f1b79547b0704c436f523dd0e560f0c64115bb72557ec44352e8903211c04692272d8b2d1a5358a2cf1b6e0bfcf99f921530ec8e39356179eae45e42ba92aeaced825171e1e8b9af6d9c03e1327f44be087ef06530e69f66615261eef54073ca11cf5858f0edfdfe15efeab349ef5d76988a3672fac47b0769447b"
      bPub =
        hexBS
          "bd0c61512c692c0cb6d041fa01bb152d4916a1e77af46ae105393011baf38964dc46a0670dd125b95a981652236f99d9b681cbf87837ec996c6da04453728610d0c6ddb58b318885d7d82c7f8deb75ce7bd4fbaa37089e6f9c6059f388838e7a00030b331eb76840910440b1b27aaeaeeb4012b7d7665238a8e3fb004b117b58"
      fc =
        FromClient
          { fcUser = "alice"
          , fcPassword = "password123"
          , fcPrivateNumber = aPriv
          , fcPublicBytes = aPub
          }
      fs =
        FromServer
          { fsPublicBytes = bPub
          , fsSalt = salt
          , fsPrimeGroup = G1024
          , fsKnownAlgorithm = SHA1
          }
      expectedK = hexBS "017eefa1cefc5c2e626e21598987f31e0f1b11bb"
      expectedM1 = hexBS "62c71b289cb22a034b405667e1541202ce5d8e03"
      expectedM2 = hexBS "b475d7f2d75ce9537748005483e5d326048b59e9"
      expectedMultiplierK = hexBS "7556AA045AEF2CDD07ABAF0F665C3E818913186F"
      expectedX = hexBS "94B7555AABE9127CC58CCF4993DB6CF84D16C124"

  it "calcResults produces the correct session key and proofs" $
    case calcResults () fc fs of
      Nothing -> expectationFailure "calcResults returned Nothing"
      Just results -> do
        rKey results `shouldBe` expectedK
        rClientProof results `shouldBe` expectedM1
        rServerProof results `shouldBe` expectedM2

  it "verifyServerProof returns True for the correct M2" $
    verifyServerProof () expectedM2 fc fs `shouldBe` True

  it "verifyServerProof returns False for a wrong M2" $
    verifyServerProof () (BS.replicate (fromIntegral (digestSize SHA1)) 0) fc fs `shouldBe` False

  it "calcResults returns Nothing when B is a multiple of N" $
    let fsInvalid = fs {fsPublicBytes = bytesOf (fromIntegral (safePrimeFor G1024))}
     in case calcResults () fc fsInvalid of
          Nothing -> pure ()
          Just _ -> expectationFailure "expected Nothing for B ≡ 0 (mod N)"

  it "calcK produces the RFC 5054 Appendix B multiplier k" $
    calcK SHA1 G1024 `shouldBe` expectedMultiplierK

  it "calcClientX produces the RFC 5054 Appendix B x for alice/password123" $
    calcClientX ("alice", "password123") salt SHA1 `shouldBe` expectedX


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


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


hexBS :: String -> ByteString
hexBS = BS.pack . go
  where
    go [] = []
    go (a : b : rest) = case readHex [a, b] of
      [(v, "")] -> v : go rest
      _ -> error $ "hexBS: invalid pair: " ++ [a, b]
    go [_] = error "hexBS: odd-length string"


isHexChar :: Word8 -> Bool
isHexChar w = w - ordAlt '0' < 10 || w - ordAlt 'A' < 6 || w - ordAlt 'a' < 6


ordAlt :: Char -> Word8
ordAlt = fromIntegral . ord


isAllHex :: ByteString -> Bool
isAllHex b =
  let
    checkWord _ignored False = False
    checkWord nextChar True = isHexChar nextChar
   in
    BS.foldr checkWord True b


bsShow :: Integer -> ByteString
bsShow = fmt . build . hexF


primeGroupSpec :: Spec
primeGroupSpec = describe "module Crypto.SRP.PrimeGroup" $ do
  it "byteLength is hexLength `div` 2" $
    byteLength G2048 == hexLength G2048 `div` 2
  it "padAs pads a short ByteString to the binary byte length of N" $
    BS.length (BS.empty `padAs` G2048) == byteLength G2048
  it "padAs returns an oversized ByteString unchanged" $
    let oversized = BS.replicate (byteLength G2048 + 1) 0
     in BS.length (oversized `padAs` G2048) == byteLength G2048 + 1