OTP 0.0.0.1 → 0.1.0.0
raw patch · 10 files changed
+584/−116 lines, 10 filesdep +OTPdep +SHAdep +bytestringdep −Cryptodep ~basedep ~timesetup-changednew-uploader
Dependencies added: OTP, SHA, bytestring, cryptohash-sha1, cryptohash-sha256, cryptohash-sha512, tasty, tasty-hunit
Dependencies removed: Crypto
Dependency ranges changed: base, time
Files
- CHANGELOG.md +9/−0
- Data/OTP.hs +0/−39
- LICENSE +19/−17
- OTP.cabal +85/−35
- README.md +69/−0
- Setup.hs +2/−2
- Test/opt.hs +0/−23
- src/Data/OTP.hs +286/−0
- src/HashImpl.hs +39/−0
- test/Test.hs +75/−0
+ CHANGELOG.md view
@@ -0,0 +1,9 @@+## 0.1.0.0++* Major API rewrite ([#3](https://github.com/hvr/OTP/pull/3))+* Add support for SHA256 and SHA512 besides SHA-1 ([#1](https://github.com/hvr/OTP/pull/1))+* Provide support via cabal flag for using pure Haskell [SHA](http://hackage.haskell.org/package/SHA) implementation++## 0.0.0.1++Initial version of `OTP`
− Data/OTP.hs
@@ -1,39 +0,0 @@--- |Implements HMAC-Based One-Time Password Algorithm as defined in RFC 4226 and --- Time-Based One-Time Password Algorithm as defined in RFC 6238. -module Data.OTP (hotp, totp) where - -import Codec.Utils -import Data.Bits -import Data.HMAC -import Data.Time.Clock -import Data.Time.Clock.POSIX - --- | Compute an HOTP using secret key and counter value. -hotp - :: [Octet] -- ^ Secret key - -> Int -- ^ Counter value - -> Int -- ^ Number of digits in password - -> Int -- ^ HOTP -hotp key count digit' = truncate_hotp (hmac_sha1 key count') digit' - where truncate_hotp hmac_result digit' = snum `mod` (10 ^ digit') - where snum = fromTwosComp sbits - sbits = dt hmac_result - dt hmac_r = [(head p) .&. 0x7F] ++ (tail p) - where offsetBits = (hmacr !! 19) .&. 0xF - offset = fromTwosComp [offsetBits] - p = take 4 (drop offset hmacr) - hmacr = pad hmac_r - where pad xs = if length xs < 20 then pad (0 : xs) else xs - count' = pad (toTwosComp count) - where pad xs = if length xs < 8 then pad (0 : xs) else xs - --- | Compute an TOTP using secret key and time. -totp - :: [Octet] -- ^ Secret key - -> UTCTime -- ^ Time - -> Int -- ^ Number of digits in password - -> Int -- ^ Period - -> Int -- ^ TOTP -totp key time digit' period = hotp key timeCounter digit' - where timePOSIX = utcTimeToPOSIXSeconds time - timeCounter = (floor timePOSIX) `div` period
LICENSE view
@@ -1,18 +1,20 @@-Copyright (c) 2012 Artem Leshchev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +Copyright (c) 2012 Artem Leshchev+ 2016 Aleksey Uimanov,+ 2018 Herbert Valerio Riedel++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished+to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
OTP.cabal view
@@ -1,35 +1,85 @@-name: OTP --- PVP summary: +-+------- breaking API changes --- | | +----- non-breaking API additions --- | | | +--- code changes with no API change -version: 0.0.0.1 -synopsis: HMAC-Based and Time-Based One-Time Passwords -description: Implements HMAC-Based One-Time Password Algorithm as defined in RFC 4226 and Time-Based One-Time Password Algorithm as defined in RFC 6238. -license: MIT -license-file: LICENSE -copyright: (c) 2012 Artem Leshchev -author: Artem Leshchev -maintainer: matshch@gmail.com -homepage: https://github.com/matshch/OTP -bug-reports: https://github.com/matshch/OTP/issues -category: Cryptography -build-type: Simple -cabal-version: >=1.8 - -library - exposed-modules: Data.OTP - build-depends: base >= 3 && < 5, Crypto >= 4, time >= 1.1 - -Test-Suite tests - type: exitcode-stdio-1.0 - main-is: Test/opt.hs - build-depends: base >= 3 && < 5, Crypto >= 4, time >= 1.1 - -source-repository head - type: git - location: git://github.com/matshch/OTP.git - -source-repository this - type: git - location: git://github.com/matshch/OTP.git - tag: v0.0.0.1+cabal-version: 1.12+name: OTP+version: 0.1.0.0+synopsis: HMAC-Based and Time-Based One-Time Passwords (HOTP & TOTP)++description: Implements the /HMAC-Based One-Time Password Algorithm/ (HOTP) as+ defined in [RFC 4226](https://tools.ietf.org/html/rfc4226)+ and the /Time-Based One-Time Password Algorithm/ (TOTP) as defined+ in [RFC 6238](https://tools.ietf.org/html/rfc6238).+ .+ The TOTP and HOTP algorithms are commonly used to implement two-step verification (2FA)+ (e.g. by [Google Authenticator](https://en.wikipedia.org/wiki/Google_Authenticator) ).+ .+ See module "Data.OTP" for API documentation.++license: MIT+license-file: LICENSE+copyright: 2012 Artem Leshchev,+ 2016 Aleksey Uimanov,+ 2018 Herbert Valerio Riedel+author: Artem Leshchev,+ Aleksey Uimanov,+ Herbert Valerio Riedel+maintainer: hvr@gnu.org+homepage: https://github.com/hvr/OTP+bug-reports: https://github.com/hvr/OTP/issues+category: Cryptography+build-type: Simple+tested-with: GHC==8.4.3, GHC==8.2.2, GHC==8.0.2, GHC==7.10.3, GHC==7.8.4, GHC==7.6.3, GHC==7.4.2++extra-source-files:+ CHANGELOG.md+ README.md++source-repository head+ type: git+ location: https://github.com/hvr/OTP.git++flag pure-sha+ manual: True+ default: False+ description: Use pure Haskell [SHA](http://hackage.haskell.org/package/SHA) implementation instead of @cryptohash-sha1\/256\/512@++library+ hs-source-dirs: src+ exposed-modules: Data.OTP+ other-modules: HashImpl++ default-language: Haskell2010+ other-extensions: CPP++ if flag(pure-sha)+ build-depends: SHA >= 1.6.4.4 && < 1.7+ , bytestring >= 0.10.0.0 && < 0.11+ , base >= 4.3.0.0 && < 4.12+ , time >= 1.2 && < 1.9+ else+ build-depends: cryptohash-sha1 >= 0.11.100.1 && < 0.12+ , cryptohash-sha256 >= 0.11.100.1 && < 0.12+ , cryptohash-sha512 >= 0.11.100.1 && < 0.12+ , bytestring >= 0.9.2.0 && < 0.11+ , base >= 4.5.0.0 && < 4.12+ , time >= 1.4 && < 1.9++ ghc-options: -Wall+++test-suite tests+ type: exitcode-stdio-1.0++ hs-source-dirs: test+ main-is: Test.hs++ default-language: Haskell2010+ other-extensions: OverloadedStrings++ build-depends: base+ , bytestring+ , OTP+ , time+ -- dependency constraints not inherited via 'OTP' component+ , tasty == 1.1.*+ , tasty-hunit == 0.10.*++ ghc-options: -Wall
+ README.md view
@@ -0,0 +1,69 @@+# `OTP`: HMAC-Based and Time-Based One-Time Passwords (HOTP & TOTP) [](https://hackage.haskell.org/package/OTP) [](https://travis-ci.org/haskell-hvr/OTP)++**Please refer to the [package description](https://hackage.haskell.org/package/OTP#description) for an overview of `OTP`.**++## Usage examples++### Generating one-time passwords++If you need to generate HOTP password described in RFC4226, then use++```haskell+>>> hotp SHA1 "1234" 100 6+317569++>>> hotp SHA512 "1234" 100 6+134131+```++Or++```haskell+>>> totp SHA1 "1234" (read "2010-10-10 00:01:00 UTC") 30 8+43388892+```++to generate TOTP password described in RFC6238.++### Checking one-time passwords++```haskell+hotpCheck :: HashAlgorithm -- ^ Hashing algorithm+ -> Secret -- ^ Shared secret+ -> (Word8, Word8) -- ^ how much counters to take lower and higher than ideal+ -> Word64 -- ^ ideal (expected) counter value+ -> Word8 -- ^ Number of digits in password+ -> Word32 -- ^ Password entered by user+ -> Bool -- ^ True if password acceptable+```++```haskell+>>> hotpCheck SHA1 "1234" (0,0) 10 6 50897+True++>>> hotpCheck SHA1 "1234" (0,0) 9 6 50897+False++>>> hotpCheck SHA1 "1234" (0,1) 9 6 50897+True+```++Here almost the same aguments as for `hotp` function, but there is+also `(0, 0)` tuple. This tuple describes range of counters to check+in case of desynchronisation of counters between client and+server. I.e. if you specify `(1, 1)` and ideal counter will be `10`+then function will check passwords for `[9, 10, 11]` list of+counters.++Here is the same for TOTP:++```haskell+>>> totpCheck SHA1 "1234" (0, 0) (read "2010-10-10 00:00:00 UTC") 30 6 778374+True++>>> totpCheck SHA1 "1234" (0, 0) (read "2010-10-10 00:00:30 UTC") 30 6 778374+False++>>> totpCheck SHA1 "1234" (1, 0) (read "2010-10-10 00:00:30 UTC") 30 6 778374+True+```
Setup.hs view
@@ -1,2 +1,2 @@-import Distribution.Simple -main = defaultMain +import Distribution.Simple+main = defaultMain
− Test/opt.hs
@@ -1,23 +0,0 @@-module Main where - -import Codec.Utils -import Data.OTP -import Data.Time -import System.Exit (exitFailure) - -secret_otp :: [Octet] -secret_otp = [49,50,51,52,53,54,55,56,57,48,49,50,51,52,53,54,55,56,57,48] - -hotp_t = zipWith (\x y -> (hotp secret_otp x 6) == y) [0..9] - [755224, 287082, 359152, 969429, 338314, 254676, 287922, 162583, 399871, 520489] - -totp_t = zipWith (\x y -> (totp secret_otp x 8 30) == y) (map (read) ["1970-01-01 00:00:59 UTC", - "2005-03-18 01:58:29 UTC", "2005-03-18 01:58:31 UTC", "2009-02-13 23:31:30 UTC", - "2033-05-18 03:33:20 UTC"]) - [94287082, 07081804, 14050471, 89005924, 69279037] -tests = hotp_t ++ totp_t - -main = if (and tests) then putStrLn "Tests succeful!" - else do - putStrLn "Tests falls!" - exitFailure
+ src/Data/OTP.hs view
@@ -0,0 +1,286 @@+-- | SPDX-License-Identifier: MIT+--+-- Implements the /HMAC-Based One-Time Password Algorithm/ (HOTP) as+-- defined in [RFC 4226](https://tools.ietf.org/html/rfc4226)+-- and the /Time-Based One-Time Password Algorithm/ (TOTP) as defined+-- in [RFC 6238](https://tools.ietf.org/html/rfc6238).+--+-- Many operations in this module take or return a 'Word32' OTP value+-- (whose most significant bit is always 0) which is truncated modulo+-- @10^digits@ according to the 'Word8' /digits/+-- parameter. Consequently, passing a value above 10 won't produce+-- more than 10 digits and will effectively return the raw+-- non-truncated 31-bit OTP value.+--+-- @since 0.1.0.0+module Data.OTP+ ( -- * HOTP+ hotp+ , hotpCheck+ -- * TOTP+ , totp+ , totpCheck+ -- * Auxiliary+ , totpCounter+ , counterRange+ , totpCounterRange++ , HashAlgorithm(..)+ , Secret+ ) where++import Data.Bits+import qualified Data.ByteString as BS+import Data.Time+import Data.Time.Clock.POSIX+import Data.Word++import HashImpl++{- | Compute /HMAC-Based One-Time Password/ using secret key and counter value.++>>> hotp SHA1 "1234" 100 6+317569++>>> hotp SHA512 "1234" 100 6+134131++>>> hotp SHA512 "1234" 100 8+55134131++-}++hotp+ :: HashAlgorithm -- ^ Hashing algorithm+ -> Secret -- ^ Shared secret+ -> Word64 -- ^ Counter value+ -> Word8 -- ^ Number of base10 digits in HOTP value+ -> Word32 -- ^ HOTP value+hotp alg key cnt digits+ | digits >= 10 = snum+ | otherwise = snum `rem` (10 ^ digits)+ where+ -- C+ msg = bsFromW64 cnt++ -- Snum = StToNum(Sbits)+ -- Sbits = DT(HS)+ -- HS = HMAC(K,C)+ snum = trunc $ hmac alg key msg++ -- DT(HS)+ trunc :: BS.ByteString -> Word32+ trunc b = case bsToW32 rb of+ Left e -> error e+ Right res -> res .&. (0x80000000 - 1) -- reset highest bit+ where+ offset = BS.last b .&. 15 -- take low 4 bits of last byte+ rb = BS.take 4 $ BS.drop (fromIntegral offset) b -- resulting 4 byte value++ -- StToNum(Sbits)+ bsToW32 :: BS.ByteString -> Either String Word32+ bsToW32 bs = case BS.unpack bs of+ [ b0, b1, b2, b3 ] -> Right $! (((((fI b0 `shiftL` 8) .|. fI b1) `shiftL` 8) .|. fI b2) `shiftL` 8) .|. fI b3+ _ -> Left "bsToW32: the impossible happened"+ where+ fI = fromIntegral++ bsFromW64 :: Word64 -> BS.ByteString+ bsFromW64 w = BS.pack [ b j | j <- [ 7, 6 .. 0 ] ]+ where+ b j = fromIntegral (w `shiftR` (j*8))++{- | Check presented password against a valid range.++>>> hotp SHA1 "1234" 10 6+50897++>>> hotpCheck SHA1 "1234" (0,0) 10 6 50897+True++>>> hotpCheck SHA1 "1234" (0,0) 9 6 50897+False++>>> hotpCheck SHA1 "1234" (0,1) 9 6 50897+True++>>> hotpCheck SHA1 "1234" (1,0) 11 6 50897+True++>>> hotpCheck SHA1 "1234" (2,2) 8 6 50897+True++>>> hotpCheck SHA1 "1234" (2,2) 7 6 50897+False++>>> hotpCheck SHA1 "1234" (2,2) 12 6 50897+True++>>> hotpCheck SHA1 "1234" (2,2) 13 6 50897+False++-}++hotpCheck+ :: HashAlgorithm -- ^ Hash algorithm to use+ -> Secret -- ^ Shared secret+ -> (Word8, Word8) -- ^ Valid counter range, before and after ideal+ -> Word64 -- ^ Ideal (expected) counter value+ -> Word8 -- ^ Number of base10 digits in a password+ -> Word32 -- ^ Password (i.e. HOTP value) entered by user+ -> Bool -- ^ True if password is valid+hotpCheck alg secr rng cnt len pass =+ let counters = counterRange rng cnt+ passwds = map (\c -> hotp alg secr c len) counters+ in any (pass ==) passwds++{- | Compute a /Time-Based One-Time Password/ using secret key and time.++>>> totp SHA1 "1234" (read "2010-10-10 00:01:00 UTC") 30 6+388892++>>> totp SHA1 "1234" (read "2010-10-10 00:01:00 UTC") 30 8+43388892++>>> totp SHA1 "1234" (read "2010-10-10 00:01:15 UTC") 30 8+43388892++>>> totp SHA1 "1234" (read "2010-10-10 00:01:31 UTC") 30 8+39110359++-}++totp+ :: HashAlgorithm -- ^ Hash algorithm to use+ -> Secret -- ^ Shared secret+ -> UTCTime -- ^ Time of TOTP+ -> Word64 -- ^ Time range in seconds+ -> Word8 -- ^ Number of base10 digits in TOTP value+ -> Word32 -- ^ TOTP value+totp alg secr time period len =+ hotp alg secr (totpCounter time period) len++{- | Check presented password against time periods.++>>> totp SHA1 "1234" (read "2010-10-10 00:00:00 UTC") 30 6+778374++>>> totpCheck SHA1 "1234" (0, 0) (read "2010-10-10 00:00:00 UTC") 30 6 778374+True++>>> totpCheck SHA1 "1234" (0, 0) (read "2010-10-10 00:00:30 UTC") 30 6 778374+False++>>> totpCheck SHA1 "1234" (1, 0) (read "2010-10-10 00:00:30 UTC") 30 6 778374+True++>>> totpCheck SHA1 "1234" (1, 0) (read "2010-10-10 00:01:00 UTC") 30 6 778374+False++>>> totpCheck SHA1 "1234" (2, 0) (read "2010-10-10 00:01:00 UTC") 30 6 778374+True+-}++totpCheck+ :: HashAlgorithm -- ^ Hash algorithm to use+ -> Secret -- ^ Shared secret+ -> (Word8, Word8) -- ^ Valid counter range, before and after ideal+ -> UTCTime -- ^ Time of TOTP+ -> Word64 -- ^ Time range in seconds+ -> Word8 -- ^ Number of base10 digits in a password+ -> Word32 -- ^ Password given by user+ -> Bool -- ^ True if password is valid+totpCheck alg secr rng time period len pass =+ let counters = totpCounterRange rng time period+ passwds = map (\c -> hotp alg secr c len) counters+ in any (pass ==) passwds+++{- | Calculate HOTP counter using time. Starting time (T0+according to RFC6238) is 0 (begining of UNIX epoch)++>>> totpCounter (read "2010-10-10 00:00:00 UTC") 30+42888960++>>> totpCounter (read "2010-10-10 00:00:30 UTC") 30+42888961++>>> totpCounter (read "2010-10-10 00:01:00 UTC") 30+42888962++-}++totpCounter+ :: UTCTime -- ^ Time of totp+ -> Word64 -- ^ Time range in seconds+ -> Word64 -- ^ Resulting counter+totpCounter time period =+ let timePOSIX = floor $ utcTimeToPOSIXSeconds time+ in timePOSIX `div` period++{- | Make a sequence of acceptable counters, protected from+arithmetic overflow.++>>> counterRange (0, 0) 9000+[9000]++>>> counterRange (1, 0) 9000+[8999,9000]++>>> length $ counterRange (5000, 0) 9000+501++>>> length $ counterRange (5000, 5000) 9000+1000++>>> counterRange (2, 2) maxBound+[18446744073709551613,18446744073709551614,18446744073709551615]++>>> counterRange (2, 2) minBound+[0,1,2]++>>> counterRange (2, 2) (maxBound `div` 2)+[9223372036854775805,9223372036854775806,9223372036854775807,9223372036854775808,9223372036854775809]++>>> counterRange (5, 5) 9000+[8995,8996,8997,8998,8999,9000,9001,9002,9003,9004,9005]++RFC recommends avoiding excessively large values for counter ranges.+-}++counterRange+ :: (Word8, Word8) -- ^ Number of counters before and after ideal+ -> Word64 -- ^ Ideal counter value+ -> [Word64]+counterRange (tolow, tohigh) ideal = [l..h]+ where+ l' = ideal - fromIntegral tolow+ l | l' <= ideal = l'+ | otherwise = 0++ h' = ideal + fromIntegral tohigh+ h | ideal <= h' = h'+ | otherwise = maxBound++{- | Make a sequence of acceptable periods.++>>> totpCounterRange (0, 0) (read "2010-10-10 00:01:00 UTC") 30+[42888962]++>>> totpCounterRange (2, 0) (read "2010-10-10 00:01:00 UTC") 30+[42888960,42888961,42888962]++>>> totpCounterRange (0, 2) (read "2010-10-10 00:01:00 UTC") 30+[42888962,42888963,42888964]++>>> totpCounterRange (2, 2) (read "2010-10-10 00:01:00 UTC") 30+[42888960,42888961,42888962,42888963,42888964]++-}++totpCounterRange :: (Word8, Word8)+ -> UTCTime+ -> Word64+ -> [Word64]+totpCounterRange rng time period =+ counterRange rng $ totpCounter time period
+ src/HashImpl.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE CPP #-}++-- | SPDX-License-Identifier: MIT+--+-- @since 0.1.0.0+module HashImpl where++#if defined(MIN_VERSION_SHA)+import qualified Data.ByteString.Lazy as BS.L+import qualified Data.Digest.Pure.SHA as SHA+#else+import qualified Crypto.Hash.SHA1 as SHA1+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Crypto.Hash.SHA512 as SHA512+#endif++import qualified Data.ByteString as BS+++-- | Shared secret encoded as raw octets+type Secret = BS.ByteString++-- | Hash algorithm used for HOTP\/TOTP computations+data HashAlgorithm = SHA1+ | SHA256+ | SHA512+ deriving (Eq,Show)++hmac :: HashAlgorithm -> Secret -> BS.ByteString -> BS.ByteString+hmac alg key msg = case alg of+#if defined(MIN_VERSION_SHA)+ SHA1 -> BS.L.toStrict (SHA.bytestringDigest (SHA.hmacSha1 (BS.L.fromStrict key) (BS.L.fromStrict msg)))+ SHA256 -> BS.L.toStrict (SHA.bytestringDigest (SHA.hmacSha256 (BS.L.fromStrict key) (BS.L.fromStrict msg)))+ SHA512 -> BS.L.toStrict (SHA.bytestringDigest (SHA.hmacSha512 (BS.L.fromStrict key) (BS.L.fromStrict msg)))+#else+ SHA1 -> SHA1.hmac key msg+ SHA256 -> SHA256.hmac key msg+ SHA512 -> SHA512.hmac key msg+#endif
+ test/Test.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BC+import Data.Time+import Data.Word+import Test.Tasty+import Test.Tasty.HUnit++-- IUT+import Data.OTP+++hotpSecret :: ByteString+hotpSecret = "12345678901234567890"++testHotp :: Word64 -> Word32 -> TestTree+testHotp key result = testCase (show result) $ do+ let h = hotp SHA1 hotpSecret key 6+ result @=? h++hotpResults :: [Word32]+hotpResults =+ [ 755224, 287082, 359152+ , 969429, 338314, 254676+ , 287922, 162583, 399871+ , 520489+ ]+++testTotp :: (ByteString, UTCTime, HashAlgorithm, Word32) -> TestTree+testTotp (secr, key, alg, result) =+ testCase (show alg ++ " => " ++ show result) $ do+ let t = totp alg secr key 30 8+ result @=? t++sha1Secr :: ByteString+sha1Secr = BC.pack $ take 20 $ cycle "12345678901234567890"++sha256Secr :: ByteString+sha256Secr = BC.pack $ take 32 $ cycle "12345678901234567890"++sha512Secr :: ByteString+sha512Secr = BC.pack $ take 64 $ cycle "12345678901234567890"++totpData :: [(ByteString, UTCTime, HashAlgorithm, Word32)]+totpData =+ [ (sha1Secr, read "1970-01-01 00:00:59 UTC", SHA1, 94287082)+ , (sha256Secr, read "1970-01-01 00:00:59 UTC", SHA256, 46119246)+ , (sha512Secr, read "1970-01-01 00:00:59 UTC", SHA512, 90693936)+ , (sha1Secr, read "2005-03-18 01:58:29 UTC", SHA1, 07081804)+ , (sha256Secr, read "2005-03-18 01:58:29 UTC", SHA256, 68084774)+ , (sha512Secr, read "2005-03-18 01:58:29 UTC", SHA512, 25091201)+ , (sha1Secr, read "2005-03-18 01:58:31 UTC", SHA1, 14050471)+ , (sha256Secr, read "2005-03-18 01:58:31 UTC", SHA256, 67062674)+ , (sha512Secr, read "2005-03-18 01:58:31 UTC", SHA512, 99943326)+ , (sha1Secr, read "2009-02-13 23:31:30 UTC", SHA1, 89005924)+ , (sha256Secr, read "2009-02-13 23:31:30 UTC", SHA256, 91819424)+ , (sha512Secr, read "2009-02-13 23:31:30 UTC", SHA512, 93441116)+ , (sha1Secr, read "2033-05-18 03:33:20 UTC", SHA1, 69279037)+ , (sha256Secr, read "2033-05-18 03:33:20 UTC", SHA256, 90698825)+ , (sha512Secr, read "2033-05-18 03:33:20 UTC", SHA512, 38618901)+ , (sha1Secr, read "2603-10-11 11:33:20 UTC", SHA1, 65353130)+ , (sha256Secr, read "2603-10-11 11:33:20 UTC", SHA256, 77737706)+ , (sha512Secr, read "2603-10-11 11:33:20 UTC", SHA512, 47863826)+ ]+++main :: IO ()+main = defaultMain $ testGroup "test vectors"+ [ testGroup "hotp" $ map (uncurry testHotp) $ zip [0..] hotpResults+ , testGroup "totp" $ map testTotp totpData+ ]