rails-session 0.1.0.0 → 0.1.1.0
raw patch · 4 files changed
+124/−44 lines, 4 filesdep +filepathdep +transformersPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: filepath, transformers
API changes (from Hackage documentation)
- Web.Rails.Session: instance Eq Cookie
- Web.Rails.Session: instance Eq DecryptedData
- Web.Rails.Session: instance Eq EncryptedData
- Web.Rails.Session: instance Eq InitVector
- Web.Rails.Session: instance Eq Salt
- Web.Rails.Session: instance Eq SecretKey
- Web.Rails.Session: instance Eq SecretKeyBase
- Web.Rails.Session: instance Ord Cookie
- Web.Rails.Session: instance Ord DecryptedData
- Web.Rails.Session: instance Ord EncryptedData
- Web.Rails.Session: instance Ord InitVector
- Web.Rails.Session: instance Ord Salt
- Web.Rails.Session: instance Ord SecretKey
- Web.Rails.Session: instance Ord SecretKeyBase
- Web.Rails.Session: instance Show Cookie
- Web.Rails.Session: instance Show DecryptedData
- Web.Rails.Session: instance Show EncryptedData
- Web.Rails.Session: instance Show InitVector
- Web.Rails.Session: instance Show Salt
- Web.Rails.Session: instance Show SecretKey
- Web.Rails.Session: instance Show SecretKeyBase
+ Web.Rails.Session: csrfToken :: RubyObject -> Maybe ByteString
+ Web.Rails.Session: data DecryptedData
+ Web.Rails.Session: instance GHC.Classes.Eq Web.Rails.Session.Cookie
+ Web.Rails.Session: instance GHC.Classes.Eq Web.Rails.Session.DecryptedData
+ Web.Rails.Session: instance GHC.Classes.Eq Web.Rails.Session.EncryptedData
+ Web.Rails.Session: instance GHC.Classes.Eq Web.Rails.Session.InitVector
+ Web.Rails.Session: instance GHC.Classes.Eq Web.Rails.Session.Salt
+ Web.Rails.Session: instance GHC.Classes.Eq Web.Rails.Session.SecretKey
+ Web.Rails.Session: instance GHC.Classes.Eq Web.Rails.Session.SecretKeyBase
+ Web.Rails.Session: instance GHC.Classes.Ord Web.Rails.Session.Cookie
+ Web.Rails.Session: instance GHC.Classes.Ord Web.Rails.Session.DecryptedData
+ Web.Rails.Session: instance GHC.Classes.Ord Web.Rails.Session.EncryptedData
+ Web.Rails.Session: instance GHC.Classes.Ord Web.Rails.Session.InitVector
+ Web.Rails.Session: instance GHC.Classes.Ord Web.Rails.Session.Salt
+ Web.Rails.Session: instance GHC.Classes.Ord Web.Rails.Session.SecretKey
+ Web.Rails.Session: instance GHC.Classes.Ord Web.Rails.Session.SecretKeyBase
+ Web.Rails.Session: instance GHC.Show.Show Web.Rails.Session.Cookie
+ Web.Rails.Session: instance GHC.Show.Show Web.Rails.Session.DecryptedData
+ Web.Rails.Session: instance GHC.Show.Show Web.Rails.Session.EncryptedData
+ Web.Rails.Session: instance GHC.Show.Show Web.Rails.Session.InitVector
+ Web.Rails.Session: instance GHC.Show.Show Web.Rails.Session.Salt
+ Web.Rails.Session: instance GHC.Show.Show Web.Rails.Session.SecretKey
+ Web.Rails.Session: instance GHC.Show.Show Web.Rails.Session.SecretKeyBase
+ Web.Rails.Session: sessionId :: RubyObject -> Maybe ByteString
+ Web.Rails.Session: unwrapDecryptedData :: DecryptedData -> ByteString
Files
- CHANGELOG.md +10/−0
- rails-session.cabal +4/−5
- src/Web/Rails/Session.hs +49/−13
- test/Spec.hs +61/−26
+ CHANGELOG.md view
@@ -0,0 +1,10 @@+## 0.1.1.0++- Export DecryptedData type.+- Add function to unwrap DecryptedData.+- Add two additional helper functions.+- Improve documentation.++## 0.1.0.0++- Initial release.
rails-session.cabal view
@@ -1,5 +1,5 @@ name: rails-session-version: 0.1.0.0+version: 0.1.1.0 synopsis: Decrypt Ruby on Rails sessions in Haskell description: Please see README.md homepage: http://github.com/iconnect/rails-session#readme@@ -11,6 +11,7 @@ category: Web build-type: Simple cabal-version: >=1.10+extra-source-files: CHANGELOG.md Source-repository head type: git@@ -38,14 +39,12 @@ main-is: Spec.hs build-depends: base , bytestring+ , filepath , rails-session , ruby-marshal , tasty , tasty-hspec+ , transformers , vector ghc-options: -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010--source-repository head- type: git- location: https://github.com/iconnect/haskell-oss/rails-interop
src/Web/Rails/Session.hs view
@@ -10,6 +10,8 @@ -- * Decrypting , decrypt -- * Utilities+ , csrfToken+ , sessionId , lookupString , lookupFixnum -- * Lifting weaker types into stronger types@@ -19,6 +21,8 @@ , mkSalt , SecretKeyBase , mkSecretKeyBase+ , DecryptedData+ , unwrapDecryptedData ) where import Control.Applicative ((<$>))@@ -43,49 +47,68 @@ -- TYPES +-- | Wrapper around data after it has been decrypted. newtype DecryptedData = DecryptedData ByteString deriving (Show, Ord, Eq) +-- | Wrapper around data before it has been decrypted. newtype EncryptedData = EncryptedData ByteString deriving (Show, Ord, Eq) +-- | Wrapper around initialisation vector. newtype InitVector = InitVector ByteString deriving (Show, Ord, Eq) +-- | Wrapper around raw cookie. newtype Cookie = Cookie ByteString deriving (Show, Ord, Eq) +-- | Wrapper around salt. newtype Salt = Salt ByteString deriving (Show, Ord, Eq) +-- | Wrapper around secret. newtype SecretKey = SecretKey ByteString deriving (Show, Ord, Eq) +-- | Wrapper around secret key base. newtype SecretKeyBase = SecretKeyBase ByteString deriving (Show, Ord, Eq) -- SMART CONSTRUCTORS +-- | Lift a cookie into a richer type. mkCookie :: ByteString -> Cookie mkCookie = Cookie +-- | Lift salt into a richer type. mkSalt :: ByteString -> Salt mkSalt = Salt +-- | Lifts secret into a richer type. mkSecretKeyBase :: ByteString -> SecretKeyBase mkSecretKeyBase = SecretKeyBase +-- SMART DESTRUCTORS++unwrapDecryptedData :: DecryptedData -> ByteString+unwrapDecryptedData (DecryptedData deData) =+ deData+ -- EXPORTS -- | Decode a cookie encrypted by Rails.-decode :: Maybe Salt -> SecretKeyBase -> Cookie -> Maybe RubyObject+decode :: Maybe Salt+ -> SecretKeyBase+ -> Cookie+ -> Maybe RubyObject decode mbSalt secretKeyBase cookie = either (const Nothing) Just (decodeEither mbSalt secretKeyBase cookie) @@ -108,26 +131,39 @@ -> Cookie -> Either String DecryptedData decrypt mbSalt secretKeyBase cookie =- let salt = fromMaybe (Salt "encrypted cookie") mbSalt+ let salt = fromMaybe defaultSalt mbSalt (SecretKey secret) = generateSecret salt secretKeyBase- key = BS.take 32 secret (EncryptedData encData, InitVector initVec) = prepare cookie in case makeIV initVec of Nothing -> Left $! "Failed to build init. vector for: " <> show initVec- Just initVec' ->+ Just initVec' -> do+ let key = BS.take 32 secret case (cipherInit key :: CryptoFailable AES256) of CryptoFailed errorMessage -> Left (show errorMessage) CryptoPassed cipher -> Right . DecryptedData $! cbcDecrypt cipher initVec' encData+ where+ defaultSalt :: Salt+ defaultSalt = Salt "encrypted cookie" +-- UTIL++-- | Helper function for looking up the csrf token in a cooie.+csrfToken :: RubyObject -> Maybe ByteString+csrfToken = lookupString "_csrf_token" US_ASCII++-- | Helper function for looking up the session id in a cookie.+sessionId :: RubyObject -> Maybe ByteString+sessionId = lookupString "session_id" UTF_8+ -- | Lookup integer for a given key. lookupFixnum :: ByteString -> RubyStringEncoding -> RubyObject -> Maybe Int-lookupFixnum k enc m =- case lookup (RIVar (RString k, enc)) m of- Just (RFixnum v) ->- Just v+lookupFixnum key enc rubyObject =+ case lookup (RIVar (RString key, enc)) rubyObject of+ Just (RFixnum val) ->+ Just val _ -> Nothing @@ -136,14 +172,14 @@ -> RubyStringEncoding -> RubyObject -> Maybe ByteString-lookupString k enc m =- case lookup (RIVar (RString k, enc)) m of- Just (RIVar (RString v, _)) ->- Just v+lookupString key enc rubyObject =+ case lookup (RIVar (RString key, enc)) rubyObject of+ Just (RIVar (RString val, _)) ->+ Just val _ -> Nothing --- UTIL+-- PRIVATE -- | Generate secret key using same cryptographic routines as Rails. generateSecret :: Salt -> SecretKeyBase -> SecretKey
test/Spec.hs view
@@ -1,21 +1,28 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} -import Data.ByteString (ByteString)-import Data.Either (isRight)-import Data.Ruby.Marshal hiding (decodeEither)-import Data.Vector (fromList)-import Test.Tasty (defaultMain, testGroup)-import Test.Tasty.Hspec (describe, it, shouldBe, shouldSatisfy, testSpec, Spec)-import Web.Rails.Session+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.Either (isRight)+import Data.Monoid ((<>))+import Data.Ruby.Marshal hiding (decodeEither)+import Data.Vector (fromList)+import System.IO.Unsafe (unsafePerformIO)+import Test.Tasty (defaultMain, testGroup)+import Test.Tasty.Hspec (describe, it, shouldBe, shouldSatisfy, testSpec, Spec)+import Web.Rails.Session +-- SPECS+ main :: IO () main = do- x <- testSpec "Web.Rails.Session" specs- defaultMain (testGroup "All the Specs" [x])+ rails4 <- testSpec "Web.Rails.Session: Rails4" $ specsFor Rails4+ defaultMain (testGroup "All the Specs" [rails4]) -specs :: Spec-specs = do+specsFor :: Rails -> Spec+specsFor rails = do+ let cookie = unsafeReadCookie rails+ describe "decode" $ do it "should be a Right(..)" $ do let result = decodeEither Nothing secret cookie@@ -32,20 +39,34 @@ let result = decrypt Nothing secret cookie result `shouldSatisfy` isRight + describe "csrfToken" $ do+ it "should look up the '_csrf_token'" $ do+ case decodeEither Nothing secret cookie of+ Left _ -> error "lookup failed"+ Right result ->+ csrfToken result `shouldBe` Just csrfTokenVal++ describe "sessionId" $ do+ it "should look up the 'session_id'" $ do+ case decodeEither Nothing secret cookie of+ Left _ -> error "lookup failed"+ Right result ->+ sessionId result `shouldBe` Just sessionIdVal+ describe "lookupString" $ do it "should look up the '_csrf_token'" $ do case decodeEither Nothing secret cookie of Left _ -> error "lookup failed" Right result -> lookupString "_csrf_token" US_ASCII result `shouldBe`- Just csrfToken+ Just csrfTokenVal it "should look up the 'session_id'" $ do case decodeEither Nothing secret cookie of Left _ -> error "lookup failed" Right result -> lookupString "session_id" UTF_8 result `shouldBe`- Just sessionId+ Just sessionIdVal it "should look up the 'token'" $ do case decodeEither Nothing secret cookie of@@ -54,6 +75,27 @@ lookupString "token" US_ASCII result `shouldBe` Just authToken +-- EXAMPLES++example :: FilePath -> IO (Maybe ByteString)+example path = do+ rawCookie <- BS.readFile path+ let appSecret = mkSecretKeyBase "development_secret_token"+ let cookie = mkCookie rawCookie+ case decodeEither Nothing appSecret cookie of+ Left _ ->+ pure Nothing+ Right rubyObject ->+ pure $ sessionId rubyObject++-- UTIL++data Rails = Rails4 deriving (Show)++unsafeReadCookie :: Rails -> Cookie+unsafeReadCookie rails = unsafePerformIO $+ BS.readFile ("test/" <> (show rails)) >>= pure . mkCookie+ -- CONFIG secret :: SecretKeyBase@@ -64,28 +106,21 @@ authToken :: ByteString authToken = "GT1EYH9X8OXYqup4HwnQIvfnh59TqNys1IvukVXpXR8=" -sessionId :: ByteString-sessionId = "912a0abcf3d64e0d9d2bdb601b9e8224"--csrfToken :: ByteString-csrfToken = "c9kRzi8L7oj2MPI/QlqYpQ79WR6YfKTDob6PGl9V2pg="---- SESSIONS+sessionIdVal :: ByteString+sessionIdVal = "912a0abcf3d64e0d9d2bdb601b9e8224" -cookie :: Cookie-cookie =- mkCookie $- "T3NpYnRsWGJsZ25qL0R4MFlZdE9wZVBrZXc3VnFHMHhYMFgvemlVUjlTekZKbERXbVd2Mkwra0xxTmNOYnZaWktBQWo3T25RV3VPRkR6RU9BQ3dub2FSWExlZmZ1RUxVWG9vZjRTbWphRzBFSW5nMVJOSklPTVRPRGxKN0tvdGxhZzVlZktLTmhxc0V1a1FCWHpwNVJGTjdON0JCbjZQWHM0R1M1SWIzeUkzalhVNWdVbnd2Z2E5RlBMSXcvR0tNSHVOZ0NiK1RBTzVxcCtMK3hJa1daYW13allNb1NyN3pOUTFBQ0tRcFNEdUVJelVMZE8rVXhwM3RhcHFONWRwby9kRStNNkRUVXNQTDNKcjF0ZWpGTUE9PS0tb3NGeEJiZ1dLeFFKcFV0WXgyUytnZz09--e996601dbf39ff5ffbf6e2f23f6ddf78c5179baa"+csrfTokenVal :: ByteString+csrfTokenVal = "c9kRzi8L7oj2MPI/QlqYpQ79WR6YfKTDob6PGl9V2pg=" rubySession :: RubyObject rubySession = RHash (fromList [ ( RIVar (RString "session_id", UTF_8)- , RIVar (RString sessionId, UTF_8))+ , RIVar (RString sessionIdVal, UTF_8)) , ( RIVar (RString "_csrf_token", US_ASCII) , RIVar- (RString csrfToken, US_ASCII))+ (RString csrfTokenVal, US_ASCII)) , ( RIVar (RString "token", US_ASCII) , RIVar (RString authToken, UTF_8)) ])