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:
parent
88a2cd80f6
commit
47d202a90f
@ -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
|
module Crypto.OTP
|
||||||
( hotp
|
( hotp
|
||||||
, OTPDigits (..)
|
, OTPDigits (..)
|
||||||
, resynchronize
|
, resynchronize
|
||||||
, totp
|
, totp
|
||||||
|
, defaultTOTPParams
|
||||||
|
, mkTOTPParams
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -12,7 +26,8 @@ import Data.Time.Clock.POSIX
|
|||||||
import Data.List (elemIndex)
|
import Data.List (elemIndex)
|
||||||
import Data.Word
|
import Data.Word
|
||||||
import Foreign.Storable (pokeByteOff)
|
import Foreign.Storable (pokeByteOff)
|
||||||
import Crypto.Hash (HashAlgorithm, SHA1)
|
import Control.Monad (unless)
|
||||||
|
import Crypto.Hash (HashAlgorithm, SHA1(..))
|
||||||
import Crypto.MAC.HMAC
|
import Crypto.MAC.HMAC
|
||||||
import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, Bytes)
|
import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, Bytes)
|
||||||
import qualified Crypto.Internal.ByteArray as B
|
import qualified Crypto.Internal.ByteArray as B
|
||||||
@ -76,14 +91,28 @@ digitsPower OTP8 = 100000000
|
|||||||
digitsPower OTP9 = 1000000000
|
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
|
=> hash
|
||||||
-> Word32
|
|
||||||
-- ^ The time step parameter X
|
|
||||||
-> Word64
|
-> Word64
|
||||||
-- ^ The T0 parameter in seconds. This is the Unix time from which to start
|
-- ^ 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
|
-> 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
|
-> key
|
||||||
-- ^ The shared secret
|
-- ^ The shared secret
|
||||||
-> POSIXTime
|
-> POSIXTime
|
||||||
@ -91,10 +120,11 @@ totp :: (HashAlgorithm hash, ByteArrayAccess key)
|
|||||||
-- This is usually the current time as returned by @Data.Time.Clock.POSIX.getPOSIXTime@
|
-- This is usually the current time as returned by @Data.Time.Clock.POSIX.getPOSIXTime@
|
||||||
-> Word32
|
-> Word32
|
||||||
-- ^ The OTP value
|
-- ^ 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
|
where
|
||||||
t = floor ((now - fromIntegral t0) / fromIntegral x)
|
t = floor ((now - fromIntegral t0) / fromIntegral x)
|
||||||
|
|
||||||
|
|
||||||
-- TODO: Put this in memory package
|
-- TODO: Put this in memory package
|
||||||
fromW64BE :: (ByteArray ba) => Word64 -> ba
|
fromW64BE :: (ByteArray ba) => Word64 -> ba
|
||||||
fromW64BE n = B.allocAndFreeze 8 $ \p -> do
|
fromW64BE n = B.allocAndFreeze 8 $ \p -> do
|
||||||
|
|||||||
@ -56,9 +56,10 @@ makeTOTPKATs = concatMap makeTest (zip3 is times otps)
|
|||||||
|
|
||||||
times = map fst totpExpected
|
times = map fst totpExpected
|
||||||
otps = map snd totpExpected
|
otps = map snd totpExpected
|
||||||
|
Right params = mkTOTPParams SHA1 0 30 OTP8
|
||||||
|
|
||||||
makeTest (i, now, password) =
|
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
|
-- resynching with the expected value should just return the current counter + 1
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user