clientsession 0.8.1 → 0.9.3.0
raw patch · 8 files changed
Files
- ChangeLog.md +13/−0
- LICENSE +17/−22
- README.md +6/−0
- bin/generate.hs +9/−0
- clientsession.cabal +22/−10
- src/System/LookupEnv.hs +6/−0
- src/Web/ClientSession.hs +114/−64
- tests/runtests.hs +8/−0
+ ChangeLog.md view
@@ -0,0 +1,13 @@+# ChangeLog for clientsession++## 0.9.3.0++* Migrate to crypton from cryptonite.++## 0.9.2.0++* Migrate crypto-aes and cprng-aes to cryptonite. [#36](https://github.com/yesodweb/clientsession/pull/36)++## 0.9.1.2++* Clarify that we're using MIT license
LICENSE view
@@ -1,25 +1,20 @@-The following license covers this documentation, and the source code, except-where otherwise indicated.--Copyright 2008, Michael Snoyman. All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions are met:+Copyright (c) 2008 Michael Snoyman, http://www.yesodweb.com/ -* Redistributions of source code must retain the above copyright notice, this- list of conditions and the following disclaimer.+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: -* Redistributions in binary form must reproduce the above copyright notice,- this list of conditions and the following disclaimer in the documentation- and/or other materials provided with the distribution.+The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR-IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO-EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT-NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,-OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE-OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF-ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+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.
+ README.md view
@@ -0,0 +1,6 @@+## clientsession++Securely store session data in a client-side cookie.++Achieves security through AES-CTR encryption and Skein-MAC-512-256+authentication. Uses Base64 encoding to avoid any issues with characters.
+ bin/generate.hs view
@@ -0,0 +1,9 @@+module Main where++import Data.Maybe (fromMaybe, listToMaybe)+import Control.Monad (void)+import System.Environment (getArgs)+import Web.ClientSession (randomKeyEnv)++main :: IO ()+main = void $ randomKeyEnv . fromMaybe "SESSION_KEY" . listToMaybe =<< getArgs
clientsession.cabal view
@@ -1,6 +1,6 @@ name: clientsession-version: 0.8.1-license: BSD3+version: 0.9.3.0+license: MIT license-file: LICENSE author: Michael Snoyman <michael@snoyman.com>, Felipe Lessa <felipe.lessa@gmail.com> maintainer: Michael Snoyman <michael@snoyman.com>@@ -10,33 +10,45 @@ encoding to avoid any issues with characters. category: Web stability: stable-cabal-version: >= 1.8+cabal-version: >= 1.10 build-type: Simple homepage: http://github.com/yesodweb/clientsession/tree/master-data-files: bench.hs-extra-source-files: tests/runtests.hs+extra-source-files: tests/runtests.hs bench.hs ChangeLog.md README.md flag test description: Build the executable to run unit tests default: False +executable clientsession-generate+ default-language: Haskell2010+ main-is: generate.hs+ build-depends: base+ , clientsession+ ghc-options: -Wall+ hs-source-dirs: bin+ library- build-depends: base >=4 && < 5+ default-language: Haskell2010+ build-depends: base >= 4.8 && < 5+ -- https://github.com/yesodweb/clientsession/commit/1221230770feff60f77ff676d52fc464cb77b2d9#r122087962+ -- Data.Bifunctor entered base in 4.8 , bytestring >= 0.9 , cereal >= 0.3 , directory >= 1 , tagged >= 0.1 , crypto-api >= 0.8- , skein >= 0.1 && < 0.2+ , skein == 1.0.* , base64-bytestring >= 0.1.1.1 , entropy >= 0.2.1- , cprng-aes >= 0.2- , cipher-aes >= 0.1.7+ , crypton >= 1.0+ , setenv exposed-modules: Web.ClientSession+ other-modules: System.LookupEnv ghc-options: -Wall hs-source-dirs: src test-suite runtests+ default-language: Haskell2010 type: exitcode-stdio-1.0 build-depends: base , bytestring >= 0.9@@ -54,4 +66,4 @@ source-repository head type: git- location: git://github.com/yesodweb/clientsession.git+ location: https://github.com/yesodweb/clientsession.git
+ src/System/LookupEnv.hs view
@@ -0,0 +1,6 @@+module System.LookupEnv (lookupEnv) where++import System.Environment (getEnvironment)++lookupEnv :: String -> IO (Maybe String)+lookupEnv envVar = fmap (lookup envVar) $ getEnvironment
src/Web/ClientSession.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE CPP #-}+{-# LANGUAGE PackageImports #-} --------------------------------------------------------- -- -- |@@ -44,10 +46,12 @@ , randomIV , mkIV , getKey+ , getKeyEnv , defaultKeyFile , getDefaultKey , initKey , randomKey+ , randomKeyEnv -- * Actual encryption/decryption , encrypt , encryptIO@@ -58,7 +62,19 @@ import Control.Applicative ((<$>)) import Control.Concurrent (forkIO) import Control.Monad (guard, when)+import Data.Bifunctor (first) import Data.Function (on)++#if MIN_VERSION_base(4,7,0)+import System.Environment (lookupEnv, setEnv)+#elif MIN_VERSION_base(4,6,0)+import System.Environment (lookupEnv)+import System.SetEnv (setEnv)+#else+import System.LookupEnv (lookupEnv)+import System.SetEnv (setEnv)+#endif+ import System.IO.Unsafe (unsafePerformIO) import qualified Data.IORef as I @@ -67,6 +83,7 @@ -- from bytestring import qualified Data.ByteString as S+import qualified Data.ByteString.Char8 as C import qualified Data.ByteString.Base64 as B -- from cereal@@ -77,11 +94,12 @@ -- from crypto-api import Crypto.Classes (constTimeEq)-import Crypto.Random (genSeedLength, reseed)-import Crypto.Types (ByteLength) --- from cipher-aes+-- from crypton import qualified Crypto.Cipher.AES as A+import Crypto.Cipher.Types(Cipher(..),BlockCipher(..),makeIV)+import Crypto.Error (eitherCryptoError)+import "crypton" Crypto.Random (ChaChaDRG,drgNew,randomBytesGenerate) -- from skein import Crypto.Skein (skeinMAC', Skein_512_256)@@ -89,18 +107,16 @@ -- from entropy import System.Entropy (getEntropy) --- from cprng-aes-import Crypto.Random.AESCtr (AESRNG, makeSystem, genRandomBytes) - -- | The keys used to store the cookies. We have an AES key used -- to encrypt the cookie and a Skein-MAC-512-256 key used verify--- the authencity and integrity of the cookie. The AES key needs--- to have exactly 32 bytes (256 bits) while Skein-MAC-512-256--- should have 64 bytes (512 bits).+-- the authencity and integrity of the cookie. The AES key must+-- have exactly 32 bytes (256 bits) while Skein-MAC-512-256 must+-- have 64 bytes (512 bits). -- -- See also 'getDefaultKey' and 'initKey'.-data Key = Key { aesKey :: !A.Key+data Key = Key { aesKey ::+ !A.AES256 -- ^ AES key with 32 bytes. , macKey :: !(S.ByteString -> Skein_512_256) -- ^ Skein-MAC key. Instead of storing the key@@ -120,15 +136,15 @@ instance Show Key where show _ = "<Web.ClientSession.Key>" --- | The initialization vector used by AES. Should be exactly 16+-- | The initialization vector used by AES. Must be exactly 16 -- bytes long.-newtype IV = IV A.IV+newtype IV = IV S.ByteString unsafeMkIV :: S.ByteString -> IV-unsafeMkIV bs = (IV (A.IV bs))+unsafeMkIV bs = (IV bs) unIV :: IV -> S.ByteString-unIV (IV (A.IV bs)) = bs+unIV (IV bs) = bs instance Eq IV where (==) = (==) `on` unIV@@ -155,9 +171,9 @@ | otherwise = Nothing -- | Randomly construct a fresh initialization vector. You--- /should not/ reuse initialization vectors.+-- /MUST NOT/ reuse initialization vectors. randomIV :: IO IV-randomIV = aesRNG+randomIV = chaChaRNG -- | The default key file. defaultKeyFile :: FilePath@@ -184,6 +200,22 @@ S.writeFile keyFile bs return key' +-- | Get the key from the named environment variable+--+-- Assumes the value is a Base64-encoded string. If the variable is not set, a+-- random key will be generated, set in the environment, and the Base64-encoded+-- version printed on @/dev/stdout@.+getKeyEnv :: String -- ^ Name of the environment variable+ -> IO Key -- ^ The actual key.+getKeyEnv envVar = do+ mvalue <- lookupEnv envVar+ case mvalue of+ Just value -> either (const newKey) return $ initKey =<< decode value+ Nothing -> newKey+ where+ decode = B.decode . C.pack+ newKey = randomKeyEnv envVar+ -- | Generate a random 'Key'. Besides the 'Key', the -- 'ByteString' passed to 'initKey' is returned so that it can be -- saved for later use.@@ -194,18 +226,42 @@ Left e -> error $ "Web.ClientSession.randomKey: never here, " ++ e Right key -> return (bs, key) +-- | Generate a random 'Key', set a Base64-encoded version of it in the given+-- environment variable, then return it. Also prints the generated string to+-- @/dev/stdout@.+randomKeyEnv :: String -> IO Key+randomKeyEnv envVar = do+ (bs, key) <- randomKey+ let encoded = C.unpack $ B.encode bs+ setEnv envVar encoded+ putStrLn $ envVar ++ "=" ++ encoded+ return key+ -- | Initializes a 'Key' from a random 'S.ByteString'. Fails if -- there isn't exactly 96 bytes (256 bits for AES and 512 bits -- for Skein-MAC-512-512).+--+-- Note that the input string is assumed to be uniformly chosen+-- from the set of all 96-byte strings. In other words, each+-- byte should be chosen from the set of all byte values (0-255)+-- with the same probability.+--+-- In particular, this function does not do any kind of key+-- stretching. You should never feed it a password, for example.+--+-- It's /highly/ recommended to feed @initKey@ only with values+-- generated by 'randomKey', unless you really know what you're+-- doing. initKey :: S.ByteString -> Either String Key initKey bs | S.length bs /= 96 = Left $ "Web.ClientSession.initKey: length of " ++ show (S.length bs) ++ " /= 96."-initKey bs = Right $ Key { aesKey = A.initKey preAesKey- , macKey = skeinMAC' preMacKey- , keyRaw = bs- }- where- (preMacKey, preAesKey) = S.splitAt 64 bs+initKey bs = do+ let (preMacKey, preAesKey) = S.splitAt 64 bs+ aesKey <- first show $ eitherCryptoError (cipherInit preAesKey)+ Right $ Key { aesKey+ , macKey = skeinMAC' preMacKey+ , keyRaw = bs+ } -- | Same as 'encrypt', however randomly generates the -- initialization vector for you.@@ -222,12 +278,14 @@ -> S.ByteString -- ^ Serialized cookie data. -> S.ByteString -- ^ Encoded cookie data to be given to -- the client browser.-encrypt key (IV (A.IV iv)) x = B.encode final- where- encrypted = A.encryptCTR (aesKey key) (A.IV iv) x- toBeAuthed = iv `S.append` encrypted- auth = macKey key toBeAuthed- final = encode auth `S.append` toBeAuthed+encrypt key (IV b) x = case makeIV b of+ Nothing -> error "Web.ClientSession.encrypt: Failed to makeIV"+ Just iv -> B.encode final+ where+ encrypted = ctrCombine (aesKey key) iv x+ toBeAuthed = b `S.append` encrypted+ auth = macKey key toBeAuthed+ final = encode auth `S.append` toBeAuthed -- | Decode (Base64), verify the integrity and authenticity -- (Skein-MAC-512-256) and decrypt (AES-CTR) the given encoded@@ -243,61 +301,53 @@ auth' = macKey key toBeAuthed guard (encode auth' `constTimeEq` auth) let (iv, encrypted) = S.splitAt 16 toBeAuthed- return $! A.decryptCTR (aesKey key) (A.IV iv) encrypted+ iv' <- makeIV iv+ return $! ctrCombine (aesKey key) iv' encrypted +-- [from when the code used cprng-aes.AESRNG] -- Significantly more efficient random IV generation. Initial -- benchmarks placed it at 6.06 us versus 1.69 ms for -- Crypto.Modes.getIVIO, since it does not require /dev/urandom -- I/O for every call. -data AESState =- ASt {-# UNPACK #-} !AESRNG -- Our CPRNG using AES on CTR mode- {-# UNPACK #-} !Int -- How many IVs were generated with this- -- AESRNG. Used to control reseeding.+-- [now with crypton.ChaChaDRG]+-- I haven't run any benchmark; this conversion is a case of “code+-- that doesn't crash trumps performance.” +data ChaChaState =+ CCSt {-# UNPACK #-} !ChaChaDRG -- Our CPRNG using ChaCha+ {-# UNPACK #-} !Int -- How many IVs were generated with this+ -- CPRNG. Used to control reseeding.+ -- | Construct initial state of the CPRNG.-aesSeed :: IO AESState-aesSeed = do- rng <- makeSystem- return $! ASt rng 0+chaChaSeed :: IO ChaChaState+chaChaSeed = do+ drg <- drgNew+ return $! CCSt drg 0 -- | Reseed the CPRNG with new entropy from the system pool.-aesReseed :: IO ()-aesReseed = do- let len :: Tagged AESRNG ByteLength- len = genSeedLength- ent <- getEntropy (untag len)- I.atomicModifyIORef aesRef $- \(ASt rng _) ->- case reseed ent rng of- Right rng' -> (ASt rng' 0, ())- Left _ -> (ASt rng 0, ())- -- Use the old RNG, but force a reseed- -- after another 'threshold' uses of it.- -- In theory, we will never reach this- -- branch, but if we do, we're safe.+chaChaReseed :: IO ()+chaChaReseed = do+ drg' <- drgNew+ I.writeIORef chaChaRef $ CCSt drg' 0 -- | 'IORef' that keeps the current state of the CPRNG. Yep, -- global state. Used in thread-safe was only, though.-aesRef :: I.IORef AESState-aesRef = unsafePerformIO $ aesSeed >>= I.newIORef-{-# NOINLINE aesRef #-}+chaChaRef :: I.IORef ChaChaState+chaChaRef = unsafePerformIO $ chaChaSeed >>= I.newIORef+{-# NOINLINE chaChaRef #-} -- | Construct a new 16-byte IV using our CPRNG. Forks another -- thread to reseed the CPRNG should its usage count reach a -- hardcoded threshold.-aesRNG :: IO IV-aesRNG = do+chaChaRNG :: IO IV+chaChaRNG = do (bs, count) <-- I.atomicModifyIORef aesRef $ \(ASt rng count) ->-#if MIN_VERSION_cprng_aes(0, 3, 2)- let (bs', rng') = genRandomBytes 16 rng-#else- let (bs', rng') = genRandomBytes rng 16-#endif- in (ASt rng' (succ count), (bs', count))- when (count == threshold) $ void $ forkIO aesReseed+ I.atomicModifyIORef chaChaRef $ \(CCSt drg count) ->+ let (bs', drg') = randomBytesGenerate 16 drg+ in (CCSt drg' (succ count), (bs', count))+ when (count == threshold) $ void $ forkIO chaChaReseed return $! unsafeMkIV bs where void f = f >> return ()
tests/runtests.hs view
@@ -22,6 +22,7 @@ main :: IO () main = hspec $ describe "client session" $ do it "encrypt/decrypt success" $ property propEncDec+ it "encrypt/decrypt success (environment key)" $ property propEncDecEnv it "encrypt/decrypt failure" $ property propEncDecFailure it "AES encrypt/decrypt success" $ property propAES it "AES encryption changes bs" $ property propAESChanges@@ -32,6 +33,13 @@ propEncDec :: S.ByteString -> Bool propEncDec bs = unsafePerformIO $ do key <- getDefaultKey+ s <- encryptIO key bs+ let bs' = decrypt key s+ return $ Just bs == bs'++propEncDecEnv :: S.ByteString -> Bool+propEncDecEnv bs = unsafePerformIO $ do+ key <- getKeyEnv "SESSION_KEY" s <- encryptIO key bs let bs' = decrypt key s return $ Just bs == bs'