packages feed

crypto-token 0.0.2 → 0.1.0

raw patch · 5 files changed

+227/−148 lines, 5 filesdep +bytestringdep +crypto-tokendep +hspecsetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: bytestring, crypto-token, hspec, network-byte-order

API changes (from Hackage documentation)

- Crypto.Token: Config :: Int -> Int -> Config
- Crypto.Token: [interval] :: Config -> Int
- Crypto.Token: [maxEntries] :: Config -> Int
- Crypto.Token: instance Foreign.Storable.Storable Crypto.Token.Header
+ Crypto.Token: instance GHC.Classes.Eq Crypto.Token.Config
+ Crypto.Token: instance GHC.Show.Show Crypto.Token.Config
+ Crypto.Token: interval :: Config -> Int
+ Crypto.Token: tokenLifetime :: Config -> Int
- Crypto.Token: decryptToken :: (Storable a, ByteArray ba) => TokenManager -> ba -> IO (Maybe a)
+ Crypto.Token: decryptToken :: TokenManager -> ByteString -> IO (Maybe ByteString)
- Crypto.Token: encryptToken :: (Storable a, ByteArray ba) => TokenManager -> a -> IO ba
+ Crypto.Token: encryptToken :: TokenManager -> ByteString -> IO ByteString

Files

Crypto/Token.hs view
@@ -3,89 +3,102 @@ {-# LANGUAGE ScopedTypeVariables #-}  -- | Encrypted tokens/tickets to keep state in the client side.---   For security reasons, 'Storable' data types MUST be fixed-size---   when stored (i.e. serialized into the memory). module Crypto.Token (-  -- * Configuration-    Config(..)-  , defaultConfig-  -- * Token manager-  , TokenManager-  , spawnTokenManager-  , killTokenManager-  -- * Encryption and decryption-  , encryptToken-  , decryptToken-  ) where+    -- * Configuration+    Config,+    interval,+    tokenLifetime,+    defaultConfig, +    -- * Token manager+    TokenManager,+    spawnTokenManager,+    killTokenManager,++    -- * Encryption and decryption+    encryptToken,+    decryptToken,+) where+ import Control.Concurrent import Crypto.Cipher.AES (AES256)-import Crypto.Cipher.Types (AuthTag(..), AEADMode(..))+import Crypto.Cipher.Types (AEADMode (..), AuthTag (..)) import qualified Crypto.Cipher.Types as C import Crypto.Error (maybeCryptoError, throwCryptoError) import Crypto.Random (getRandomBytes) import Data.Array.IO import Data.Bits (xor)-import Data.ByteArray (ByteArray, Bytes) import qualified Data.ByteArray as BA+import qualified Data.ByteString as BS+import qualified Data.ByteString.Internal as BS import qualified Data.IORef as I-import Data.Int (Int64)-import Data.Word (Word16, Word64)+import Data.Word import Foreign.Ptr import Foreign.Storable+import Network.ByteOrder  ---------------------------------------------------------------- +type Index = Word16+type Counter = Word64+ -- | Configuration for token manager.-data Config = Config {-  -- | The interval to generate a new secret and remove the oldest one in minutes.-    interval   :: Int-  -- | Maximum size of secret entries. Minimum is 256 and maximum is 32767.-  , maxEntries :: Int-  }+data Config = Config+    { interval :: Int+    -- ^ The interval to generate a new secret and remove the oldest one in seconds.+    , tokenLifetime :: Int+    -- ^ The token lifetime, that is, tokens can be decrypted in this period.+    }+    deriving (Eq, Show) --- | Default configuration to update secrets in 30 minutes and keep them for 10 days.+-- | Default configuration to update secrets in 30 minutes (1,800 seconds) and token liefetime is 1 day (86,400 seconds)+--+-- >>> defaultConfig+-- Config {interval = 1800, maxEntries = 86400} defaultConfig :: Config-defaultConfig = Config {-    interval = 30-  , maxEntries = 480-  }+defaultConfig =+    Config+        { interval = 1800+        , tokenLifetime = 86400+        }  ----------------------------------------------------------------  -- fixme: mask+ -- | The abstract data type for token manager.-data TokenManager = TokenManager {-    secrets :: IOArray Int Secret-  , currentIndex :: I.IORef Int-  , headerMask :: Header-  , threadId :: ThreadId-  }+data TokenManager = TokenManager+    { headerMask :: Header+    , getEncryptSecret :: IO (Secret, Index)+    , getDecryptSecret :: Index -> IO Secret+    , threadId :: ThreadId+    }  -- | Spawning a token manager. spawnTokenManager :: Config -> IO TokenManager spawnTokenManager Config{..} = do     emp <- emptySecret-    let lim = (max 256 (min maxEntries 32767)) - 1-    arr <- newArray (0, lim) emp-    update arr 0+    let lim = fromIntegral (tokenLifetime `div` interval)+    arr <- newArray (0, lim - 1) emp+    ent <- generateSecret+    writeArray arr 0 ent     ref <- I.newIORef 0     tid <- forkIO $ loop arr ref     msk <- newHeaderMask-    return $ TokenManager arr ref msk tid+    return $ TokenManager msk (readCurrentSecret arr ref) (readSecret arr) tid   where-    update :: IOArray Int Secret -> Int -> IO ()-    update arr idx = do-        ent <- generateSecret-        writeArray arr idx ent     loop arr ref = do-        threadDelay (interval * 60 * 1000000)+        threadDelay (interval * 1000000)+        update arr ref+        loop arr ref+    update :: IOArray Index Secret -> I.IORef Index -> IO ()+    update arr ref = do         idx0 <- I.readIORef ref         (_, n) <- getBounds arr         let idx = (idx0 + 1) `mod` (n + 1)-        update arr idx+        sec <- generateSecret+        writeArray arr idx sec         I.writeIORef ref idx-        loop arr ref  -- | Killing a token manager. killTokenManager :: TokenManager -> IO ()@@ -93,32 +106,40 @@  ---------------------------------------------------------------- -getSecret :: TokenManager -> Int -> IO Secret-getSecret TokenManager{..} idx0 = do+readSecret :: IOArray Index Secret -> Index -> IO Secret+readSecret secrets idx0 = do     (_, n) <- getBounds secrets     let idx = idx0 `mod` (n + 1)     readArray secrets idx +readCurrentSecret :: IOArray Index Secret -> I.IORef Index -> IO (Secret, Index)+readCurrentSecret arr ref = do+    idx <- I.readIORef ref+    sec <- readSecret arr idx+    return (sec, idx)+ ---------------------------------------------------------------- -data Secret = Secret {-    secretIV      :: Bytes-  , secretKey     :: Bytes-  , secretCounter :: I.IORef Int64-  }+data Secret = Secret+    { secretIV :: ByteString+    , secretKey :: ByteString+    , secretCounter :: I.IORef Counter+    }  emptySecret :: IO Secret-emptySecret = Secret BA.empty BA.empty <$> I.newIORef 0+emptySecret = Secret BS.empty BS.empty <$> I.newIORef 0  generateSecret :: IO Secret-generateSecret = Secret <$> genIV-                        <*> genKey-                        <*> I.newIORef 0+generateSecret =+    Secret+        <$> genIV+        <*> genKey+        <*> I.newIORef 0 -genKey :: IO Bytes+genKey :: IO ByteString genKey = getRandomBytes keyLength -genIV :: IO Bytes+genIV :: IO ByteString genIV = getRandomBytes ivLength  ----------------------------------------------------------------@@ -140,119 +161,126 @@  ---------------------------------------------------------------- -data Header = Header {-    headerIndex   :: Word16-  , headerCounter :: Word64-  }+data Header = Header+    { headerIndex :: Index+    , headerCounter :: Counter+    } -instance Storable Header where-    sizeOf _    = indexLength + counterLength-    alignment _ = indexLength -- fixme-    peek p = do-        i <- peek $ castPtr p-        c <- peek (castPtr p `plusPtr` indexLength)-        return $ Header i c-    poke p (Header i c) = do-        poke (castPtr p) i-        poke (castPtr p `plusPtr` indexLength) c+encodeHeader :: Header -> IO ByteString+encodeHeader Header{..} = withWriteBuffer (indexLength + counterLength) $ \wbuf -> do+    write16 wbuf headerIndex+    write64 wbuf headerCounter +decodeHeader :: ByteString -> IO Header+decodeHeader bs = withReadBuffer bs $ \rbuf ->+    Header <$> read16 rbuf <*> read64 rbuf+ newHeaderMask :: IO Header newHeaderMask = do-    bin <- getRandomBytes (indexLength + counterLength) :: IO Bytes-    BA.withByteArray bin peek+    bin <- getRandomBytes (indexLength + counterLength) :: IO ByteString+    decodeHeader bin  ----------------------------------------------------------------  xorHeader :: Header -> Header -> Header-xorHeader x y = Header {-    headerIndex = headerIndex x `xor` headerIndex y-  , headerCounter = headerCounter x `xor` headerCounter y-  }+xorHeader x y =+    Header+        { headerIndex = headerIndex x `xor` headerIndex y+        , headerCounter = headerCounter x `xor` headerCounter y+        } -addHeader :: ByteArray ba => TokenManager -> Int -> Int64 -> ba -> IO ba+addHeader :: TokenManager -> Index -> Counter -> ByteString -> IO ByteString addHeader TokenManager{..} idx counter cipher = do-    let hdr = Header (fromIntegral idx) (fromIntegral counter)+    let hdr = Header idx counter         mskhdr = headerMask `xorHeader` hdr-    hdrbin <- BA.create (sizeOf mskhdr) $ \ptr -> poke ptr mskhdr-    return (hdrbin `BA.append` cipher)+    hdrbin <- encodeHeader mskhdr+    return (hdrbin `BS.append` cipher) -delHeader :: ByteArray ba => TokenManager -> ba -> IO (Maybe (Int, Int64, ba))+delHeader+    :: TokenManager -> ByteString -> IO (Maybe (Index, Counter, ByteString)) delHeader TokenManager{..} token-  | BA.length token < minlen = return Nothing-  | otherwise = do-        let (hdrbin, cipher) = BA.splitAt minlen token-        mskhdr <- BA.withByteArray hdrbin peek+    | BS.length token < minlen = return Nothing+    | otherwise = do+        let (hdrbin, cipher) = BS.splitAt minlen token+        mskhdr <- decodeHeader hdrbin         let hdr = headerMask `xorHeader` mskhdr-            idx = fromIntegral $ headerIndex hdr-            counter = fromIntegral $ headerCounter hdr+            idx = headerIndex hdr+            counter = headerCounter hdr         return $ Just (idx, counter, cipher)   where     minlen = indexLength + counterLength  -- | Encrypting a target value to get a token.-encryptToken :: (Storable a, ByteArray ba)-             => TokenManager -> a -> IO ba+encryptToken+    :: TokenManager+    -> ByteString+    -> IO ByteString encryptToken mgr x = do-    idx <- I.readIORef $ currentIndex mgr-    secret <- getSecret mgr idx+    (secret, idx) <- getEncryptSecret mgr     (counter, cipher) <- encrypt secret x     addHeader mgr idx counter cipher -encrypt :: (Storable a, ByteArray ba)-        => Secret -> a -> IO (Int64, ba)-encrypt secret x = do-    counter <- I.atomicModifyIORef' (secretCounter secret) (\i -> (i+1, i))-    plain <- BA.create (sizeOf x) $ \ptr -> poke ptr x+encrypt+    :: Secret+    -> ByteString+    -> IO (Counter, ByteString)+encrypt secret plain = do+    counter <- I.atomicModifyIORef' (secretCounter secret) (\i -> (i + 1, i))     nonce <- makeNonce counter $ secretIV secret     let cipher = aes256gcmEncrypt plain (secretKey secret) nonce     return (counter, cipher)  -- | Decrypting a token to get a target value.-decryptToken :: (Storable a, ByteArray ba)-             => TokenManager -> ba -> IO (Maybe a)+decryptToken+    :: TokenManager+    -> ByteString+    -> IO (Maybe ByteString) decryptToken mgr token = do     mx <- delHeader mgr token     case mx of-      Nothing -> return Nothing-      Just (idx, counter, cipher) -> do-          secret <- getSecret mgr idx-          decrypt secret counter cipher+        Nothing -> return Nothing+        Just (idx, counter, cipher) -> do+            secret <- getDecryptSecret mgr idx+            decrypt secret counter cipher -decrypt :: forall a ba . (Storable a, ByteArray ba)-        => Secret -> Int64 -> ba -> IO (Maybe a)+decrypt+    :: Secret+    -> Counter+    -> ByteString+    -> IO (Maybe ByteString) decrypt secret counter cipher = do     nonce <- makeNonce counter $ secretIV secret-    let mplain = aes256gcmDecrypt cipher (secretKey secret) nonce-        expect = sizeOf (undefined :: a)-    case mplain of-      Just plain-        | BA.length plain == expect -> Just <$> BA.withByteArray plain peek-      _                             -> return Nothing-+    return $ aes256gcmDecrypt cipher (secretKey secret) nonce -makeNonce :: forall ba . ByteArray ba => Int64 -> ba -> IO ba+makeNonce :: Counter -> ByteString -> IO ByteString makeNonce counter iv = do-    cv <- BA.create ivLength $ \ptr -> poke ptr counter-    return $ iv `BA.xor` (cv :: ba)+    cv <- BS.create ivLength $ \ptr -> poke (castPtr ptr) counter+    return $ iv `BA.xor` cv  ---------------------------------------------------------------- -constantAdditionalData :: Bytes-constantAdditionalData = BA.empty+constantAdditionalData :: ByteString+constantAdditionalData = BS.empty -aes256gcmEncrypt :: ByteArray ba-                 => ba -> Bytes -> Bytes -> ba-aes256gcmEncrypt plain key nonce = cipher `BA.append` BA.convert tag+aes256gcmEncrypt+    :: ByteString+    -> ByteString+    -> ByteString+    -> ByteString+aes256gcmEncrypt plain key nonce = cipher `BS.append` (BA.convert tag)   where     conn = throwCryptoError (C.cipherInit key) :: AES256     aeadIni = throwCryptoError $ C.aeadInit AEAD_GCM conn nonce     (AuthTag tag, cipher) = C.aeadSimpleEncrypt aeadIni constantAdditionalData plain tagLength -aes256gcmDecrypt :: ByteArray ba-                 => ba -> Bytes -> Bytes -> Maybe ba+aes256gcmDecrypt+    :: ByteString+    -> ByteString+    -> ByteString+    -> Maybe ByteString aes256gcmDecrypt ctexttag key nonce = do-    aes  <- maybeCryptoError $ C.cipherInit key :: Maybe AES256+    aes <- maybeCryptoError $ C.cipherInit key :: Maybe AES256     aead <- maybeCryptoError $ C.aeadInit AEAD_GCM aes nonce-    let (ctext, tag) = BA.splitAt (BA.length ctexttag - tagLength) ctexttag+    let (ctext, tag) = BS.splitAt (BS.length ctexttag - tagLength) ctexttag         authtag = AuthTag $ BA.convert tag     C.aeadSimpleDecrypt aead constantAdditionalData ctext authtag
Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
crypto-token.cabal view
@@ -1,26 +1,47 @@-name:                crypto-token-version:             0.0.2-synopsis:            crypto tokens-description:         Encrypted tokens/tickets to keep state in the client side.-license:             BSD3-license-file:        LICENSE-author:              Kazu Yamamoto <kazu@iij.ad.jp>-maintainer:          Kazu Yamamoto <kazu@iij.ad.jp>-category:            Cryptography, Network-cabal-version:       >= 1.10-build-type:          Simple+cabal-version: >=1.10+name:          crypto-token+version:       0.1.0+license:       BSD3+license-file:  LICENSE+maintainer:    Kazu Yamamoto <kazu@iij.ad.jp>+author:        Kazu Yamamoto <kazu@iij.ad.jp>+synopsis:      crypto tokens+description:   Encrypted tokens/tickets to keep state in the client side.+category:      Cryptography, Network+build-type:    Simple +source-repository head+    type:     git+    location: git://github.com/kazu-yamamoto/crypto-token+ library-  default-language:    Haskell2010-  ghc-options:         -Wall-  exposed-modules:     Crypto.Token-  build-depends:       base >= 4.9 && < 5-                     , array-                     , crypton-                     , memory-  if impl(ghc >= 8)-    default-extensions:  Strict StrictData+    exposed-modules:  Crypto.Token+    default-language: Haskell2010+    ghc-options:      -Wall+    build-depends:+        base >=4.9 && <5,+        array,+        bytestring,+        crypton,+        memory,+        network-byte-order -source-repository head-  type:                 git-  location:             git://github.com/kazu-yamamoto/crypto-token+    if impl(ghc >=8)+        default-extensions: Strict StrictData++test-suite spec+    type:               exitcode-stdio-1.0+    main-is:            Spec.hs+    build-tool-depends: hspec-discover:hspec-discover+    hs-source-dirs:     test+    other-modules:+        TokenSpec++    default-language:   Haskell2010+    default-extensions: Strict StrictData+    ghc-options:        -Wall -threaded -rtsopts+    build-depends:+        base >=4.9 && <5,+        bytestring,+        crypto-token,+        hspec
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/TokenSpec.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedStrings #-}++module TokenSpec where++import Control.Concurrent+import Crypto.Token+import Test.Hspec++----------------------------------------------------------------++spec :: Spec+spec = do+    describe "crypto token" $ do+        it "encrypt & decrypt ByteString" $ do+            mgr <- spawnTokenManager $ defaultConfig{interval = 1, tokenLifetime = 3}+            tf <- encryptToken mgr "Foo"+            threadDelay 1100000+            decryptToken mgr tf `shouldReturn` Just "Foo"+            tb <- encryptToken mgr "Bar"+            threadDelay 1100000+            decryptToken mgr tf `shouldReturn` Just "Foo"+            decryptToken mgr tb `shouldReturn` Just "Bar"+            threadDelay 1100000+            decryptToken mgr tf `shouldReturn` Nothing+            decryptToken mgr tb `shouldReturn` Just "Bar"+            threadDelay 1100000+            decryptToken mgr tf `shouldReturn` Nothing+            decryptToken mgr tb `shouldReturn` Nothing