Blame Crypto/OTP.hs

Packit 141393
{-# LANGUAGE ScopedTypeVariables #-}
Packit 141393
Packit 141393
-- | One-time password implementation as defined by the
Packit 141393
-- <http://tools.ietf.org/html/rfc4226 HOTP> and <http://tools.ietf.org/html/rfc6238 TOTP>
Packit 141393
-- specifications.
Packit 141393
--
Packit 141393
-- Both implementations use a shared key between the client and the server. HOTP passwords
Packit 141393
-- are based on a synchronized counter. TOTP passwords use the same approach but calculate
Packit 141393
-- the counter as a number of time steps from the Unix epoch to the current time, thus
Packit 141393
-- requiring that both client and server have synchronized clocks.
Packit 141393
--
Packit 141393
-- Probably the best-known use of TOTP is in Google's 2-factor authentication.
Packit 141393
--
Packit 141393
-- The TOTP API doesn't depend on any particular time package, so the user needs to supply
Packit 141393
-- the current @OTPTime@ value, based on the system time. For example, using the @hourglass@
Packit 141393
-- package, you could create a @getOTPTime@ function:
Packit 141393
--
Packit 141393
-- >>> import Time.System
Packit 141393
-- >>> import Time.Types
Packit 141393
-- >>>
Packit 141393
-- >>> let getOTPTime = timeCurrent >>= \(Elapsed t) -> return (fromIntegral t :: OTPTime)
Packit 141393
--
Packit 141393
-- Or if you prefer, the @time@ package could be used:
Packit 141393
--
Packit 141393
-- >>> import Data.Time.Clock.POSIX
Packit 141393
-- >>>
Packit 141393
-- >>> let getOTPTime = getPOSIXTime >>= \t -> return (floor t :: OTPTime)
Packit 141393
--
Packit 141393
Packit 141393
module Crypto.OTP
Packit 141393
    ( OTP
Packit 141393
    , OTPDigits (..)
Packit 141393
    , OTPTime
Packit 141393
    , hotp
Packit 141393
    , resynchronize
Packit 141393
    , totp
Packit 141393
    , totpVerify
Packit 141393
    , TOTPParams
Packit 141393
    , ClockSkew (..)
Packit 141393
    , defaultTOTPParams
Packit 141393
    , mkTOTPParams
Packit 141393
    )
Packit 141393
where
Packit 141393
Packit 141393
import           Data.Bits (shiftL, shiftR, (.&.), (.|.))
Packit 141393
import           Data.ByteArray.Mapping (fromW64BE)
Packit 141393
import           Data.List (elemIndex)
Packit 141393
import           Data.Word
Packit 141393
import           Foreign.Storable (poke)
Packit 141393
import           Control.Monad (unless)
Packit 141393
import           Crypto.Hash (HashAlgorithm, SHA1(..))
Packit 141393
import           Crypto.MAC.HMAC
Packit 141393
import           Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, Bytes)
Packit 141393
import qualified Crypto.Internal.ByteArray as B
Packit 141393
Packit 141393
Packit 141393
-- | A one-time password which is a sequence of 4 to 9 digits.
Packit 141393
type OTP = Word32
Packit 141393
Packit 141393
-- | The strength of the calculated HOTP value, namely
Packit 141393
-- the number of digits (between 4 and 9) in the extracted value.
Packit 141393
data OTPDigits = OTP4 | OTP5 | OTP6 | OTP7 | OTP8 | OTP9 deriving (Show)
Packit 141393
Packit 141393
-- | An integral time value in seconds.
Packit 141393
type OTPTime = Word64
Packit 141393
Packit 141393
hotp :: forall hash key. (HashAlgorithm hash, ByteArrayAccess key)
Packit 141393
    => hash
Packit 141393
    -> OTPDigits
Packit 141393
    -- ^ Number of digits in the HOTP value extracted from the calculated HMAC
Packit 141393
    -> key
Packit 141393
    -- ^ Shared secret between the client and server
Packit 141393
    -> Word64
Packit 141393
    -- ^ Counter value synchronized between the client and server
Packit 141393
    -> OTP
Packit 141393
    -- ^ The HOTP value
Packit 141393
hotp _ d k c = dt `mod` digitsPower d
Packit 141393
  where
Packit 141393
    mac = hmac k (fromW64BE c :: Bytes) :: HMAC hash
Packit 141393
    offset = fromIntegral (B.index mac (B.length mac - 1) .&. 0xf)
Packit 141393
    dt = (fromIntegral (B.index mac offset       .&. 0x7f) `shiftL` 24) .|.
Packit 141393
         (fromIntegral (B.index mac (offset + 1) .&. 0xff) `shiftL` 16) .|.
Packit 141393
         (fromIntegral (B.index mac (offset + 2) .&. 0xff) `shiftL`  8) .|.
Packit 141393
         fromIntegral  (B.index mac (offset + 3) .&. 0xff)
Packit 141393
Packit 141393
-- | Attempt to resynchronize the server's counter value
Packit 141393
-- with the client, given a sequence of HOTP values.
Packit 141393
resynchronize :: (HashAlgorithm hash, ByteArrayAccess key)
Packit 141393
    => hash
Packit 141393
    -> OTPDigits
Packit 141393
    -> Word16
Packit 141393
    -- ^ The look-ahead window parameter. Up to this many values will
Packit 141393
    -- be calculated and checked against the value(s) submitted by the client
Packit 141393
    -> key
Packit 141393
    -- ^ The shared secret
Packit 141393
    -> Word64
Packit 141393
    -- ^ The current server counter value
Packit 141393
    -> (OTP, [OTP])
Packit 141393
    -- ^ The first OTP submitted by the client and a list of additional
Packit 141393
    -- sequential OTPs (which may be empty)
Packit 141393
    -> Maybe Word64
Packit 141393
    -- ^ The new counter value, synchronized with the client's current counter
Packit 141393
    -- or Nothing if the submitted OTP values didn't match anywhere within the window
Packit 141393
resynchronize h d s k c (p1, extras) = do
Packit 141393
    offBy <- fmap fromIntegral (elemIndex p1 range)
Packit 141393
    checkExtraOtps (c + offBy + 1) extras
Packit 141393
  where
Packit 141393
    checkExtraOtps ctr [] = Just ctr
Packit 141393
    checkExtraOtps ctr (p:ps)
Packit 141393
        | hotp h d k ctr /= p = Nothing
Packit 141393
        | otherwise           = checkExtraOtps (ctr + 1) ps
Packit 141393
Packit 141393
    range = map (hotp h d k)[c..c + fromIntegral s]
Packit 141393
Packit 141393
digitsPower :: OTPDigits -> Word32
Packit 141393
digitsPower OTP4 = 10000
Packit 141393
digitsPower OTP5 = 100000
Packit 141393
digitsPower OTP6 = 1000000
Packit 141393
digitsPower OTP7 = 10000000
Packit 141393
digitsPower OTP8 = 100000000
Packit 141393
digitsPower OTP9 = 1000000000
Packit 141393
Packit 141393
Packit 141393
data TOTPParams h = TP !h !OTPTime !Word16 !OTPDigits !ClockSkew deriving (Show)
Packit 141393
Packit 141393
data ClockSkew = NoSkew | OneStep | TwoSteps | ThreeSteps | FourSteps deriving (Enum, Show)
Packit 141393
Packit 141393
-- | The default TOTP configuration.
Packit 141393
defaultTOTPParams :: TOTPParams SHA1
Packit 141393
defaultTOTPParams = TP SHA1 0 30 OTP6 TwoSteps
Packit 141393
Packit 141393
-- | Create a TOTP configuration with customized parameters.
Packit 141393
mkTOTPParams :: (HashAlgorithm hash)
Packit 141393
    => hash
Packit 141393
    -> OTPTime
Packit 141393
    -- ^ The T0 parameter in seconds. This is the Unix time from which to start
Packit 141393
    -- counting steps (default 0). Must be before the current time.
Packit 141393
    -> Word16
Packit 141393
    -- ^ The time step parameter X in seconds (default 30, maximum allowed 300)
Packit 141393
    -> OTPDigits
Packit 141393
    -- ^ Number of required digits in the OTP (default 6)
Packit 141393
    -> ClockSkew
Packit 141393
    -- ^ The number of time steps to check either side of the current value
Packit 141393
    -- to allow for clock skew between client and server and or delay in
Packit 141393
    -- submitting the value. The default is two time steps.
Packit 141393
    -> Either String (TOTPParams hash)
Packit 141393
mkTOTPParams h t0 x d skew = do
Packit 141393
    unless (x > 0) (Left "Time step must be greater than zero")
Packit 141393
    unless (x <= 300) (Left "Time step cannot be greater than 300 seconds")
Packit 141393
    return (TP h t0 x d skew)
Packit 141393
Packit 141393
-- | Calculate a totp value for the given time.
Packit 141393
totp :: (HashAlgorithm hash, ByteArrayAccess key)
Packit 141393
    => TOTPParams hash
Packit 141393
    -> key
Packit 141393
    -- ^ The shared secret
Packit 141393
    -> OTPTime
Packit 141393
    -- ^ The time for which the OTP should be calculated.
Packit 141393
    -- This is usually the current time as returned by @Data.Time.Clock.POSIX.getPOSIXTime@
Packit 141393
    -> OTP
Packit 141393
totp (TP h t0 x d _) k now = hotp h d k (timeToCounter now t0 x)
Packit 141393
Packit 141393
-- | Check a supplied TOTP value is valid for the given time,
Packit 141393
-- within the window defined by the skew parameter.
Packit 141393
totpVerify :: (HashAlgorithm hash, ByteArrayAccess key)
Packit 141393
    => TOTPParams hash
Packit 141393
    -> key
Packit 141393
    -> OTPTime
Packit 141393
    -> OTP
Packit 141393
    -> Bool
Packit 141393
totpVerify (TP h t0 x d skew) k now otp = otp `elem` map (hotp h d k) (range window [])
Packit 141393
  where
Packit 141393
    t = timeToCounter now t0 x
Packit 141393
    window = fromIntegral (fromEnum skew)
Packit 141393
    range 0 acc = t : acc
Packit 141393
    range n acc = range (n-1) ((t-n) : (t+n) : acc)
Packit 141393
Packit 141393
timeToCounter :: Word64 -> Word64 -> Word16 -> Word64
Packit 141393
timeToCounter now t0 x = (now - t0) `div` fromIntegral x