Add TOTParams data type

Reduce the arguments to the totp function (most people will use defaults)
and allows validation of the time step value.

Added a top-level module overview.
This commit is contained in:
Luke Taylor 2015-12-28 17:23:26 +00:00
parent 88a2cd80f6
commit 47d202a90f
2 changed files with 38 additions and 7 deletions

View File

@ -1,9 +1,23 @@
-- | One-time password implementation as defined by the
-- <http://tools.ietf.org/html/rfc4226 HOTP> and <http://tools.ietf.org/html/rfc6238 TOTP>
-- specifications.
--
-- Both implementations use a shared key between the client and the server. HOTP passwords
-- are based on a synchronized counter. TOTP passwords use the same approach but calculate
-- the counter as a number of time steps from the Unix epoch to the current time, thus
-- requiring that both client and server have synchronized clocks.
--
-- Probably the best-known use of TOTP is in Google's 2-factor authentication.
--
module Crypto.OTP
( hotp
, OTPDigits (..)
, resynchronize
, totp
, defaultTOTPParams
, mkTOTPParams
)
where
@ -12,7 +26,8 @@ import Data.Time.Clock.POSIX
import Data.List (elemIndex)
import Data.Word
import Foreign.Storable (pokeByteOff)
import Crypto.Hash (HashAlgorithm, SHA1)
import Control.Monad (unless)
import Crypto.Hash (HashAlgorithm, SHA1(..))
import Crypto.MAC.HMAC
import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, Bytes)
import qualified Crypto.Internal.ByteArray as B
@ -76,14 +91,28 @@ digitsPower OTP8 = 100000000
digitsPower OTP9 = 1000000000
totp :: (HashAlgorithm hash, ByteArrayAccess key)
data TOTPParams h = TP !h !Word64 !Word32 !OTPDigits
defaultTOTPParams :: TOTPParams SHA1
defaultTOTPParams = TP SHA1 0 30 OTP6
mkTOTPParams :: (HashAlgorithm hash)
=> hash
-> Word32
-- ^ The time step parameter X
-> Word64
-- ^ The T0 parameter in seconds. This is the Unix time from which to start
-- counting steps (usually zero)
-- counting steps (default 0). Must be before the current time.
-> Word32
-- ^ The time step parameter X in seconds (default 30)
-> OTPDigits
-- ^ Number of required digits in the OTP (default 6)
-> Either String (TOTPParams hash)
mkTOTPParams h t0 x d = do
unless (x > 0) (Left "Time step must be greater than zero")
unless (x <= 300) (Left "Time step cannot be greater than 300 seconds")
return (TP h t0 x d)
totp :: (HashAlgorithm hash, ByteArrayAccess key)
=> TOTPParams hash
-> key
-- ^ The shared secret
-> POSIXTime
@ -91,10 +120,11 @@ totp :: (HashAlgorithm hash, ByteArrayAccess key)
-- This is usually the current time as returned by @Data.Time.Clock.POSIX.getPOSIXTime@
-> Word32
-- ^ The OTP value
totp h x t0 d k now = hotp d k t
totp (TP h t0 x d) k now = hotp d k t
where
t = floor ((now - fromIntegral t0) / fromIntegral x)
-- TODO: Put this in memory package
fromW64BE :: (ByteArray ba) => Word64 -> ba
fromW64BE n = B.allocAndFreeze 8 $ \p -> do

View File

@ -56,9 +56,10 @@ makeTOTPKATs = concatMap makeTest (zip3 is times otps)
times = map fst totpExpected
otps = map snd totpExpected
Right params = mkTOTPParams SHA1 0 30 OTP8
makeTest (i, now, password) =
[ testCase (show i) (assertEqual "" password (totp SHA1 30 0 OTP8 otpKey (fromIntegral now)))
[ testCase (show i) (assertEqual "" password (totp params otpKey (fromIntegral now)))
]
-- resynching with the expected value should just return the current counter + 1