You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
6.7 KiB

{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Sasl.Mechanisms.Scram
where
import Control.Applicative ((<$>))
import Control.Monad.Error
import Control.Monad.State.Strict
import qualified Crypto.Classes as Crypto
import qualified Crypto.HMAC as Crypto
import qualified Crypto.Hash.CryptoAPI as Crypto
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 as BS8 (unpack)
import Data.List (foldl1', genericTake)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types
import Network.Xmpp.Types
-- | A nicer name for undefined, for use as a dummy token to determin
-- the hash function to use
hashToken :: (Crypto.Hash ctx hash) => hash
hashToken = undefined
-- | Salted Challenge Response Authentication Mechanism (SCRAM) SASL
-- mechanism according to RFC 5802.
--
-- This implementation is independent and polymorphic in the used hash function.
scram :: (Crypto.Hash ctx hash)
=> hash -- ^ Dummy argument to determine the hash to use; you
-- can safely pass undefined or a 'hashToken' to it
-> Text.Text -- ^ Authentication ID (user name)
-> Maybe Text.Text -- ^ Authorization ID
-> Text.Text -- ^ Password
-> ErrorT AuthFailure (StateT StreamState IO) ()
scram hToken authcid authzid password = do
(ac, az, pw) <- prepCredentials authcid authzid password
scramhelper ac az pw
where
scramhelper authcid' authzid' pwd = do
cnonce <- liftIO makeNonce
_ <- saslInit "SCRAM-SHA-1" (Just $ cFirstMessage cnonce)
sFirstMessage <- saslFromJust =<< pullChallenge
prs <- toPairs sFirstMessage
(nonce, salt, ic) <- fromPairs prs cnonce
let (cfm, v) = cFinalMessageAndVerifier nonce salt ic sFirstMessage cnonce
_ <- respond $ Just cfm
finalPairs <- toPairs =<< saslFromJust =<< pullFinalMessage
Clean up and additionally document AuthFailure (and XmppFailure) types As mentioned in a previous patch, the `AuthFailure' type signals a (non-fatal) SASL error condition. This is now reflected in the documentation. I went through the different constructors for the type, looking at how they were produced (thrown) and whether or not that information were useful for the application using Pontarius XMPP. To begin, I conclude that `AuthStreamFailure' is only used internally. It will probably be removed when the internal type signatures of the Sasl package are changed to conform with the rest of the `Error' computations of Pontarius XMPP. `AuthFailure' is not thrown as far as I can see, but is only used for the Error instance. `AuthNoAcceptableMechanism' is thrown by `xmppSasl' when none of the mechanisms offered by the server is specified as acceptable by the client. It wraps the mechanisms offered. I consider this information useful for client developers, and will therefor keep this constructor. `AuthSaslFailure' wraps a `SaslFailure' (from Types.hs) and is only thrown when `pullSaslElement' unpickles a SASL failure. This, together with `AuthNoAcceptableMechanism' above, could be considered the `normal' ways of which SASL might be failing. `AuthStringPrepFailure' is thrown if `prepCredentials' fails to stringprep-verify the credentials. This might be interesting for the client developer. As I think that `AuthIllegalCredentials' is more understandable, I have changed the name to that. `AuthNoStream' is thrown by `xmppSasl' when the stream state is `Closed'. This is the result of a client program error/bug. This patch removes this constructor and modifies the behaviour of xmppSasl to throw an `XmppFailure' instead. `AuthChallengeFailure' is thrown if `fromPairs' fails (in Scram.hs), if a challenge element could not be pulled (in Common.hs), by `saslFromJust' if a `Nothing' value is encountered (in Common.hs), in `pullFinalMessage' (`decode') if the success payload could not be decoded (in Common.hs), or if `toPairs' (in Common.hs) can not extract the pairs. Furthermore, `AuthServerAuthFailure' is thrown if there is no `v' value in the final message of the SCRAM handler. Finally, `AuthXmlFailure' is thrown when `pullSuccess' find something other than a success element (and, I'm guessing, a `SaslFailure' element). This can only happen if there is a bug in Pontarius XMPP or the server. The way I see it, all these failures are abnormal and are of no interest from the client application itself. I suggest that these events are logged instead, and that we signal any of these conditions with a new `AuthOtherFailure' constructor. I suggest that we remove the `AuthFailure' constructor, and use the `AuthOtherFailure' for the `Error' instance. The `AuthFailure' type and all its constructors are now documented. I also made some minor documentation enhancements to the `XmppFailure' type.
13 years ago
unless (lookup "v" finalPairs == Just v) $ throwError AuthOtherFailure -- TODO: Log
return ()
where
-- We need to jump through some hoops to get a polymorphic solution
encode :: Crypto.Hash ctx hash => hash -> hash -> BS.ByteString
encode _hashtoken = Crypto.encode
hash :: BS.ByteString -> BS.ByteString
hash str = encode hToken $ Crypto.hash' str
hmac :: BS.ByteString -> BS.ByteString -> BS.ByteString
hmac key str = encode hToken $ Crypto.hmac' (Crypto.MacKey key) str
authzid'' :: Maybe BS.ByteString
authzid'' = (\z -> "a=" +++ Text.encodeUtf8 z) <$> authzid'
gs2CbindFlag :: BS.ByteString
gs2CbindFlag = "n" -- we don't support channel binding yet
gs2Header :: BS.ByteString
gs2Header = merge $ [ gs2CbindFlag
, maybe "" id authzid''
, ""
]
-- cbindData :: BS.ByteString
-- cbindData = "" -- we don't support channel binding yet
cFirstMessageBare :: BS.ByteString -> BS.ByteString
cFirstMessageBare cnonce = merge [ "n=" +++ Text.encodeUtf8 authcid'
, "r=" +++ cnonce]
cFirstMessage :: BS.ByteString -> BS.ByteString
cFirstMessage cnonce = gs2Header +++ cFirstMessageBare cnonce
fromPairs :: Pairs
-> BS.ByteString
-> ErrorT AuthFailure (StateT StreamState IO) (BS.ByteString, BS.ByteString, Integer)
fromPairs prs cnonce | Just nonce <- lookup "r" prs
, cnonce `BS.isPrefixOf` nonce
, Just salt' <- lookup "s" prs
, Right salt <- B64.decode salt'
, Just ic <- lookup "i" prs
, [(i,"")] <- reads $ BS8.unpack ic
= return (nonce, salt, i)
Clean up and additionally document AuthFailure (and XmppFailure) types As mentioned in a previous patch, the `AuthFailure' type signals a (non-fatal) SASL error condition. This is now reflected in the documentation. I went through the different constructors for the type, looking at how they were produced (thrown) and whether or not that information were useful for the application using Pontarius XMPP. To begin, I conclude that `AuthStreamFailure' is only used internally. It will probably be removed when the internal type signatures of the Sasl package are changed to conform with the rest of the `Error' computations of Pontarius XMPP. `AuthFailure' is not thrown as far as I can see, but is only used for the Error instance. `AuthNoAcceptableMechanism' is thrown by `xmppSasl' when none of the mechanisms offered by the server is specified as acceptable by the client. It wraps the mechanisms offered. I consider this information useful for client developers, and will therefor keep this constructor. `AuthSaslFailure' wraps a `SaslFailure' (from Types.hs) and is only thrown when `pullSaslElement' unpickles a SASL failure. This, together with `AuthNoAcceptableMechanism' above, could be considered the `normal' ways of which SASL might be failing. `AuthStringPrepFailure' is thrown if `prepCredentials' fails to stringprep-verify the credentials. This might be interesting for the client developer. As I think that `AuthIllegalCredentials' is more understandable, I have changed the name to that. `AuthNoStream' is thrown by `xmppSasl' when the stream state is `Closed'. This is the result of a client program error/bug. This patch removes this constructor and modifies the behaviour of xmppSasl to throw an `XmppFailure' instead. `AuthChallengeFailure' is thrown if `fromPairs' fails (in Scram.hs), if a challenge element could not be pulled (in Common.hs), by `saslFromJust' if a `Nothing' value is encountered (in Common.hs), in `pullFinalMessage' (`decode') if the success payload could not be decoded (in Common.hs), or if `toPairs' (in Common.hs) can not extract the pairs. Furthermore, `AuthServerAuthFailure' is thrown if there is no `v' value in the final message of the SCRAM handler. Finally, `AuthXmlFailure' is thrown when `pullSuccess' find something other than a success element (and, I'm guessing, a `SaslFailure' element). This can only happen if there is a bug in Pontarius XMPP or the server. The way I see it, all these failures are abnormal and are of no interest from the client application itself. I suggest that these events are logged instead, and that we signal any of these conditions with a new `AuthOtherFailure' constructor. I suggest that we remove the `AuthFailure' constructor, and use the `AuthOtherFailure' for the `Error' instance. The `AuthFailure' type and all its constructors are now documented. I also made some minor documentation enhancements to the `XmppFailure' type.
13 years ago
fromPairs _ _ = throwError $ AuthOtherFailure -- TODO: Log
cFinalMessageAndVerifier :: BS.ByteString
-> BS.ByteString
-> Integer
-> BS.ByteString
-> BS.ByteString
-> (BS.ByteString, BS.ByteString)
cFinalMessageAndVerifier nonce salt ic sfm cnonce
= (merge [ cFinalMessageWOProof
, "p=" +++ B64.encode clientProof
]
, B64.encode serverSignature
)
where
cFinalMessageWOProof :: BS.ByteString
cFinalMessageWOProof = merge [ "c=" +++ B64.encode gs2Header
, "r=" +++ nonce]
saltedPassword :: BS.ByteString
saltedPassword = hi (Text.encodeUtf8 pwd) salt ic
clientKey :: BS.ByteString
clientKey = hmac saltedPassword "Client Key"
storedKey :: BS.ByteString
storedKey = hash clientKey
authMessage :: BS.ByteString
authMessage = merge [ cFirstMessageBare cnonce
, sfm
, cFinalMessageWOProof
]
clientSignature :: BS.ByteString
clientSignature = hmac storedKey authMessage
clientProof :: BS.ByteString
clientProof = clientKey `xorBS` clientSignature
serverKey :: BS.ByteString
serverKey = hmac saltedPassword "Server Key"
serverSignature :: BS.ByteString
serverSignature = hmac serverKey authMessage
-- helper
hi :: BS.ByteString -> BS.ByteString -> Integer -> BS.ByteString
hi str slt ic' = foldl1' xorBS (genericTake ic' us)
where
u1 = hmac str (slt +++ (BS.pack [0,0,0,1]))
us = iterate (hmac str) u1
scramSha1 :: Text.Text -- ^ username
-> Maybe Text.Text -- ^ authorization ID
-> Text.Text -- ^ password
-> SaslHandler
scramSha1 authcid authzid passwd =
( "SCRAM-SHA-1"
, do
r <- runErrorT $ scram (hashToken :: Crypto.SHA1) authcid authzid passwd
case r of
Left (AuthStreamFailure e) -> return $ Left e
Left e -> return $ Right $ Just e
Right () -> return $ Right Nothing
)