shomei-core-0.2.0.0: test/Shomei/Mfa/Totp/AlgorithmSpec.hs
-- | RFC 6238 conformance for "Shomei.Mfa.Totp.Algorithm": the Appendix B vectors, the ±1 acceptance
-- window, the strictly-greater replay rule, and a Base32 round-trip.
module Shomei.Mfa.Totp.AlgorithmSpec (tests) where
import Data.Text (Text)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Shomei.Mfa.Totp.Algorithm
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))
-- | The RFC 6238 Appendix B secret: the 20 ASCII bytes @"12345678901234567890"@.
rfcSecret :: TotpSecret
rfcSecret = TotpSecret "12345678901234567890"
-- | The 8-digit code the RFC prescribes for a given Unix time.
vectorAt :: Integer -> Text -> TestTree
vectorAt t expected =
testCase ("matches RFC 6238 vector at t=" <> show t) $
totpCode 8 rfcSecret (totpCounter (posixSecondsToUTCTime (fromIntegral t))) @?= expected
tests :: TestTree
tests =
testGroup
"Shomei.Mfa.Totp.Algorithm"
[ vectorAt 59 "94287082",
vectorAt 1111111109 "07081804",
vectorAt 1234567890 "89005924",
vectorAt 2000000000 "69279037",
testCase "counter derivation floors to the 30-second step" $ do
totpCounter (posixSecondsToUTCTime 59) @?= 1
totpCounter (posixSecondsToUTCTime 1234567890) @?= 41152263,
testCase "6-digit code is the 8-digit value mod 10^6" $
-- 94287082 `mod` 1000000 == 287082
totpCode 6 rfcSecret 1 @?= "287082",
testCase "accepts a code for the current counter and returns it" $ do
let now = posixSecondsToUTCTime 1234567890
c = totpCounter now
verifyTotp rfcSecret Nothing now (totpCode 6 rfcSecret c) @?= Just c,
testCase "accepts the previous-step code within the window" $ do
let now = posixSecondsToUTCTime 1234567890
c = totpCounter now
verifyTotp rfcSecret Nothing now (totpCode 6 rfcSecret (c - 1)) @?= Just (c - 1),
testCase "rejects a code two steps in the past (outside the window)" $ do
let now = posixSecondsToUTCTime 1234567890
c = totpCounter now
verifyTotp rfcSecret Nothing now (totpCode 6 rfcSecret (c - 2)) @?= Nothing,
testCase "rejects a replayed counter (strictly-greater rule)" $ do
let now = posixSecondsToUTCTime 1234567890
c = totpCounter now
verifyTotp rfcSecret (Just c) now (totpCode 6 rfcSecret c) @?= Nothing,
testCase "rejects a wrong code" $ do
let now = posixSecondsToUTCTime 1234567890
verifyTotp rfcSecret Nothing now "000000" @?= Nothing,
testCase "Base32 of the RFC secret is the well-known value" $
secretToBase32 rfcSecret @?= "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
testCase "Base32 round-trips" $
assertBool "decode . encode == id" (base32ToSecret (secretToBase32 rfcSecret) == Right rfcSecret),
testCase "otpauth URI carries the Base32 secret and issuer" $
otpauthUri "shomei" "alice" rfcSecret
@?= "otpauth://totp/shomei:alice?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=shomei"
]