From 47d202a90ffd35fcbb2e191199715c17710838c4 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Mon, 28 Dec 2015 17:23:26 +0000 Subject: [PATCH] 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. --- Crypto/OTP.hs | 42 ++++++++++++++++++++++++++++++++++++------ tests/KAT_OTP.hs | 3 ++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Crypto/OTP.hs b/Crypto/OTP.hs index b302fbd..ceb38fb 100644 --- a/Crypto/OTP.hs +++ b/Crypto/OTP.hs @@ -1,9 +1,23 @@ +-- | One-time password implementation as defined by the +-- and +-- 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 diff --git a/tests/KAT_OTP.hs b/tests/KAT_OTP.hs index 24baf9c..5033279 100644 --- a/tests/KAT_OTP.hs +++ b/tests/KAT_OTP.hs @@ -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