packages feed

crypto-token-0.2.1: test/TokenSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module TokenSpec where

import Control.Concurrent
import qualified Control.Exception as E
import Crypto.Token
import Data.Bits (xor)
import qualified Data.ByteString as BS
import Test.Hspec

----------------------------------------------------------------

spec :: Spec
spec = do
    describe "crypto token" $ do
        it "encrypt & decrypt ByteString" $
            roundTrip defaultConfig
        it "lives as long as its lifetime, across rotations" $ do
            mgr <- spawnTokenManager $ defaultConfig{interval = 1, tokenLifetime = 2}
            threadDelay 900000
            -- made just before a rotation
            tf <- encryptToken mgr "Foo"
            threadDelay 1200000
            decryptToken mgr tf `shouldReturn` Just "Foo"
            tb <- encryptToken mgr "Bar"
            threadDelay 1300000
            decryptToken mgr tf `shouldReturn` Nothing
            decryptToken mgr tb `shouldReturn` Just "Bar"
            killTokenManager mgr

        it "refuses a tampered token" $ do
            mgr <- spawnTokenManager defaultConfig
            t <- encryptToken mgr "Foo"
            let flip1 i = BS.take i t <> BS.singleton (BS.index t i `xor` 1) <> BS.drop (i + 1) t
            -- in the header, the payload and the tag
            mapM_
                (\i -> decryptToken mgr (flip1 i) `shouldReturn` Nothing)
                [0, 20, BS.length t - 1]
            decryptToken mgr (BS.take 15 t) `shouldReturn` Nothing
            killTokenManager mgr
        it "gives unrelated headers to consecutive tokens" $ do
            mgr <- spawnTokenManager defaultConfig
            t1 <- encryptToken mgr "Foo"
            t2 <- encryptToken mgr "Foo"
            -- with a fixed mask only the counter's low byte would differ
            length (filter id $ BS.zipWith (/=) (BS.take 16 t1) (BS.take 16 t2))
                `shouldSatisfy` (> 4)
            killTokenManager mgr

    describe "configuration" $ do
        it "survives a lifetime shorter than the interval" $
            roundTrip defaultConfig{interval = 10, tokenLifetime = 1}
        it "survives more intervals than an index can name" $
            roundTrip defaultConfig{interval = 1, tokenLifetime = 100000}
        it "refuses a non-positive interval" $
            spawnTokenManager defaultConfig{interval = 0}
                `shouldThrow` (\(_ :: E.IOException) -> True)

roundTrip :: Config -> IO ()
roundTrip conf =
    E.bracket (spawnTokenManager conf) killTokenManager $ \mgr -> do
        t <- encryptToken mgr "Foo"
        decryptToken mgr t `shouldReturn` Just "Foo"