Browse Source

Merge remote-tracking branch 'philonous/master'

master
Jon Kristensen 14 years ago
parent
commit
c5841a6325
  1. 2
      pontarius.cabal
  2. 4
      source/Network/Xmpp.hs
  3. 5
      source/Network/Xmpp/Monad.hs
  4. 50
      source/Network/Xmpp/Sasl.hs
  5. 171
      source/Network/Xmpp/Sasl/Common.hs
  6. 70
      source/Network/Xmpp/Sasl/DigestMD5.hs
  7. 52
      source/Network/Xmpp/Sasl/Plain.hs
  8. 68
      source/Network/Xmpp/Sasl/Sasl.hs
  9. 139
      source/Network/Xmpp/Sasl/Scram.hs
  10. 19
      source/Network/Xmpp/Sasl/Types.hs
  11. 17
      source/Network/Xmpp/Types.hs
  12. 10
      tests/Tests.hs

2
pontarius.cabal

@ -37,6 +37,7 @@ Library @@ -37,6 +37,7 @@ Library
, binary -any
, attoparsec -any
, crypto-api -any
, cryptohash -any
, text -any
, bytestring -any
, transformers -any
@ -62,6 +63,7 @@ Library @@ -62,6 +63,7 @@ Library
, Network.Xmpp.Sasl
, Network.Xmpp.Sasl.Plain
, Network.Xmpp.Sasl.DigestMD5
, Network.Xmpp.Sasl.Scram
, Network.Xmpp.Sasl.Types
, Network.Xmpp.Session
, Network.Xmpp.Stream

4
source/Network/Xmpp.hs

@ -157,6 +157,8 @@ import Network.Xmpp.Message @@ -157,6 +157,8 @@ import Network.Xmpp.Message
import Network.Xmpp.Monad
import Network.Xmpp.Presence
import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Scram
import Network.Xmpp.Sasl.Plain
import Network.Xmpp.Sasl.Types
import Network.Xmpp.Session
import Network.Xmpp.Stream
@ -177,7 +179,7 @@ auth :: Text.Text -- ^ The username @@ -177,7 +179,7 @@ auth :: Text.Text -- ^ The username
-- assign one
-> XmppConMonad (Either AuthError Text.Text)
auth username passwd resource = runErrorT $ do
ErrorT $ xmppSASL [DIGEST_MD5Credentials Nothing username passwd]
ErrorT $ xmppSasl username Nothing [scramSha1 $ return passwd]
res <- lift $ xmppBind resource
lift $ xmppStartSession
return res

5
source/Network/Xmpp/Monad.hs

@ -99,6 +99,7 @@ xmppNoConnection = XmppConnection @@ -99,6 +99,7 @@ xmppNoConnection = XmppConnection
, sFeatures = SF Nothing [] []
, sConnectionState = XmppConnectionClosed
, sHostname = Nothing
, sAuthzid = Nothing
, sUsername = Nothing
, sResource = Nothing
, sCloseConnection = return ()
@ -111,7 +112,6 @@ xmppNoConnection = XmppConnection @@ -111,7 +112,6 @@ xmppNoConnection = XmppConnection
-- updates the XmppConMonad XmppConnection state.
xmppRawConnect :: HostName -> Text -> XmppConMonad ()
xmppRawConnect host hostname = do
uname <- gets sUsername
con <- liftIO $ do
con <- connectTo host (PortNumber 5222)
hSetBuffering con NoBuffering
@ -126,7 +126,8 @@ xmppRawConnect host hostname = do @@ -126,7 +126,8 @@ xmppRawConnect host hostname = do
(SF Nothing [] [])
XmppConnectionPlain
(Just hostname)
uname
Nothing
Nothing
Nothing
(hClose con)
put st

50
source/Network/Xmpp/Sasl.hs

@ -30,38 +30,42 @@ import Network.Xmpp.Pickle @@ -30,38 +30,42 @@ import Network.Xmpp.Pickle
import qualified System.Random as Random
import Network.Xmpp.Sasl.Sasl
import Network.Xmpp.Sasl.DigestMD5
import Network.Xmpp.Sasl.Plain
import Network.Xmpp.Sasl.Types
runSasl :: (Text.Text -> Text.Text -> Maybe Text.Text -> SaslM a)
-> Text.Text
-> Maybe Text.Text
-> XmppConMonad (Either AuthError a)
runSasl authAction authcid authzid = runErrorT $ do
hn <- gets sHostname
case hn of
Just hn' -> do
r <- authAction hn' authcid authzid
modify (\s -> s{ sUsername = Just authcid
, sAuthzid = authzid
})
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream
return r
Nothing -> throwError AuthConnectionError
-- Uses the first supported mechanism to authenticate, if any. Updates the
-- XmppConMonad state with non-password credentials and restarts the stream upon
-- success. This computation wraps an ErrorT computation, which means that
-- catchError can be used to catch any errors.
xmppSASL :: [SASLCredentials] -- ^ Acceptable authentication mechanisms and
-- their corresponding credentials
xmppSasl :: Text.Text
-> Maybe Text.Text
-> [SaslHandler] -- ^ Acceptable authentication
-- mechanisms and their corresponding
-- handlers
-> XmppConMonad (Either AuthError ())
xmppSASL creds = runErrorT $ do
xmppSasl authcid authzid handlers = do
-- Chooses the first mechanism that is acceptable by both the client and the
-- server.
mechanisms <- gets $ saslMechanisms . sFeatures
let cred = L.find (\cred -> credsToName cred `elem` mechanisms) creds
unless (isJust cred) (throwError $ AuthMechanismError mechanisms)
case fromJust cred of
DIGEST_MD5Credentials authzid authcid passwd -> ErrorT $ xmppDigestMD5
authzid
authcid
passwd
PLAINCredentials authzid authcid passwd -> ErrorT $ xmppPLAIN
authzid
authcid
passwd
_ -> error "xmppSASL: Mechanism not caught"
where
-- Converts the credentials to the appropriate mechanism name, corresponding to
-- the XMPP mechanism attribute.
credsToName :: SASLCredentials -> Text
credsToName (DIGEST_MD5Credentials _ _ _) = "DIGEST-MD5"
credsToName (PLAINCredentials _ _ _) = "PLAIN"
credsToName c = error $ "credsToName failed for " ++ (show c)
case (filter (\(name,_) -> name `elem` mechanisms)) handlers of
[] -> return . Left $ AuthNoAcceptableMechanism mechanisms
(_name, handler):_ -> runSasl handler authcid authzid

171
source/Network/Xmpp/Sasl/Common.hs

@ -0,0 +1,171 @@ @@ -0,0 +1,171 @@
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Sasl.Common where
import Network.Xmpp.Types
import Control.Applicative ((<$>))
import Control.Monad.Error
import Control.Monad.State.Class
import qualified Data.Attoparsec.ByteString.Char8 as AP
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.Maybe (fromMaybe)
import Data.Maybe (maybeToList)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.XML.Pickle
import Data.XML.Types
import Data.Word (Word8)
import Network.Xmpp.Monad
import Network.Xmpp.Pickle
import Network.Xmpp.Sasl.Types
import qualified System.Random as Random
data SaslElement = SaslSuccess (Maybe Text.Text)
| SaslChallenge (Maybe Text.Text)
--makeNonce :: SaslM BS.ByteString
makeNonce :: IO BS.ByteString
makeNonce = do
g <- liftIO Random.newStdGen
return $ B64.encode . BS.pack . map toWord8 . take 15 $ Random.randoms g
where
toWord8 :: Int -> Word8
toWord8 x = fromIntegral x :: Word8
-- The <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> element, with an
-- optional round-trip value.
saslInitE :: Text.Text -> Maybe Text.Text -> Element
saslInitE mechanism rt =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}auth"
[("mechanism", [ContentText mechanism])]
(maybeToList $ NodeContent . ContentText <$> rt)
-- SASL response with text payload.
saslResponseE :: Maybe Text.Text -> Element
saslResponseE resp =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response"
[]
(maybeToList $ NodeContent . ContentText <$> resp)
xpSuccess :: PU [Node] (Maybe Text.Text)
xpSuccess = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}success"
(xpOption $ xpContent xpId)
-- Parses the incoming SASL data to a mapped list of pairs.
pairs :: BS.ByteString -> Either String Pairs
pairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do
AP.skipSpace
name <- AP.takeWhile1 (/= '=')
_ <- AP.char '='
quote <- ((AP.char '"' >> return True) `mplus` return False)
content <- AP.takeWhile1 (AP.notInClass [',', '"'])
when quote . void $ AP.char '"'
return (name, content)
-- Failure element pickler.
xpFailure :: PU [Node] SaslFailure
xpFailure = xpWrap
(\(txt, (failure, _, _)) -> SaslFailure failure txt)
(\(SaslFailure failure txt) -> (txt,(failure,(),())))
(xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}failure"
(xp2Tuple
(xpOption $ xpElem
"{urn:ietf:params:xml:ns:xmpp-sasl}text"
xpLangTag
(xpContent xpId))
(xpElemByNamespace
"urn:ietf:params:xml:ns:xmpp-sasl"
xpPrim
(xpUnit)
(xpUnit))))
-- Challenge element pickler.
xpChallenge :: PU [Node] (Maybe Text.Text)
xpChallenge = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge"
(xpOption $ xpContent xpId)
-- | pickler for SaslElement
xpSaslElement :: PU [Node] SaslElement
xpSaslElement = xpAlt saslSel
[ xpWrap SaslSuccess (\(SaslSuccess x) -> x) xpSuccess
, xpWrap SaslChallenge (\(SaslChallenge c) -> c) xpChallenge
]
where
saslSel (SaslSuccess _) = 0
saslSel (SaslChallenge _) = 1
-- | Add quotationmarks around a byte string
quote :: BS.ByteString -> BS.ByteString
quote x = BS.concat ["\"",x,"\""]
saslInit :: Text.Text -> Maybe BS.ByteString -> SaslM Bool
saslInit mechanism payload = lift . pushElement . saslInitE mechanism $
Text.decodeUtf8 . B64.encode <$> payload
-- | Pull the next element
pullSaslElement :: SaslM SaslElement
pullSaslElement = do
el <- lift $ pullPickle (xpEither xpFailure xpSaslElement)
case el of
Left e ->throwError $ AuthSaslFailure e
Right r -> return r
-- | Pull the next element, checking that it is a challenge
pullChallenge :: SaslM (Maybe BS.ByteString)
pullChallenge = do
e <- pullSaslElement
case e of
SaslChallenge Nothing -> return Nothing
SaslChallenge (Just scb64)
| Right sc <- B64.decode . Text.encodeUtf8 $ scb64
-> return $ Just sc
_ -> throwError AuthChallengeError
-- | Extract value from Just, failing with AuthChallengeError on Nothing
saslFromJust :: Maybe a -> SaslM a
saslFromJust Nothing = throwError $ AuthChallengeError
saslFromJust (Just d) = return d
-- | Pull the next element and check that it is success
pullSuccess :: SaslM (Maybe Text.Text)
pullSuccess = do
e <- pullSaslElement
case e of
SaslSuccess x -> return x
_ -> throwError $ AuthXmlError
-- | Pull the next element. When it's success, return it's payload.
-- If it's a challenge, send an empty response and pull success
pullFinalMessage :: SaslM (Maybe BS.ByteString)
pullFinalMessage = do
challenge2 <- pullSaslElement
case challenge2 of
SaslSuccess x -> decode x
SaslChallenge x -> do
_b <- respond Nothing
_s <- pullSuccess
decode x
where
decode Nothing = return Nothing
decode (Just d) = case B64.decode $ Text.encodeUtf8 d of
Left _e -> throwError $ AuthChallengeError
Right x -> return $ Just x
-- | Extract p=q pairs from a challenge
toPairs :: BS.ByteString -> SaslM Pairs
toPairs ctext = case pairs ctext of
Left _e -> throwError AuthChallengeError
Right r -> return r
-- | Send a SASL response element. The content will be base64-encoded for you
respond :: Maybe BS.ByteString -> SaslM Bool
respond = lift . pushElement . saslResponseE .
fmap (Text.decodeUtf8 . B64.encode)

70
source/Network/Xmpp/Sasl/DigestMD5.hs

@ -17,7 +17,6 @@ import qualified Data.ByteString.Char8 as BS8 @@ -17,7 +17,6 @@ import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BL
import qualified Data.Digest.Pure.MD5 as MD5
import qualified Data.List as L
import Data.Word (Word8)
import qualified Data.Text as Text
import Data.Text (Text)
@ -34,68 +33,47 @@ import Network.Xmpp.Stream @@ -34,68 +33,47 @@ import Network.Xmpp.Stream
import Network.Xmpp.Types
import Network.Xmpp.Pickle
import qualified System.Random as Random
import Network.Xmpp.Sasl.Sasl
import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types
xmppDigestMD5 :: Maybe Text -- Authorization identity (authzid)
-> Text -- Authentication identity (authzid)
-> Text -- Password (authzid)
-> XmppConMonad (Either AuthError ())
xmppDigestMD5 authzid authcid passwd = runErrorT $ do
realm <- gets sHostname
case realm of
Just realm' -> do
ErrorT $ xmppDIGEST_MD5' realm'
hn <- gets sHostname
case hn of
Just hn' -> do
xmppDigestMD5' hn'
-- TODO: Save authzid
modify (\s -> s{sUsername = Just authcid})
Nothing -> throwError AuthConnectionError
where
xmppDIGEST_MD5' :: Text -- ^ SASL realm
-> XmppConMonad (Either AuthError ())
xmppDIGEST_MD5' realm = runErrorT $ do
-- Push element and receive the challenge (in XmppConMonad).
_ <- lift . pushElement $ saslInitE "DIGEST-MD5" Nothing -- TODO: Check boolean?
challenge' <- lift $ B64.decode . Text.encodeUtf8 <$>
pullPickle challengePickle
challenge <- case challenge' of
Left _e -> throwError AuthChallengeError
Right r -> return r
pairs <- case toPairs challenge of
Left _ -> throwError AuthChallengeError
Right p -> return p
g <- liftIO Random.newStdGen
_ <- lift . pushElement . -- TODO: Check boolean?
saslResponseE $ createResponse g realm pairs
challenge2 <- lift $ pullPickle (xpEither failurePickle challengePickle)
case challenge2 of
Left _x -> throwError AuthXmlError
Right _ -> return ()
lift $ pushElement saslResponse2E
e <- lift pullElement
case e of
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] ->
return ()
_ -> throwError AuthXmlError -- TODO: investigate
-- The SASL authentication has succeeded; the stream is restarted.
xmppDigestMD5' :: Text -> SaslM ()
xmppDigestMD5' hostname = do
-- Push element and receive the challenge (in SaslM).
_ <- saslInit "DIGEST-MD5" Nothing -- TODO: Check boolean?
pairs <- toPairs =<< saslFromJust =<< pullChallenge
cnonce <- liftIO $ makeNonce
_b <- respond . Just $ createResponse hostname pairs cnonce
challenge2 <- pullFinalMessage
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream
return ()
-- Produce the response to the challenge.
createResponse :: Random.RandomGen g
=> g
-> Text
-> [(BS8.ByteString, BS8.ByteString)] -- Pairs
-> Text
createResponse g hostname pairs = let
createResponse :: Text
-> Pairs
-> BS.ByteString -- nonce
-> BS.ByteString
createResponse hostname pairs cnonce = let
Just qop = L.lookup "qop" pairs
Just nonce = L.lookup "nonce" pairs
uname_ = Text.encodeUtf8 authcid
passwd_ = Text.encodeUtf8 passwd
-- Using Int instead of Word8 for random 1.0.0.0 (GHC 7) compatibility.
cnonce = BS.tail . BS.init .
B64.encode . BS.pack . map toWord8 .
take 8 $ Random.randoms g
nc = "00000001"
digestURI = "xmpp/" `BS.append` Text.encodeUtf8 hostname
digest = md5Digest
@ -120,11 +98,7 @@ xmppDigestMD5 authzid authcid passwd = runErrorT $ do @@ -120,11 +98,7 @@ xmppDigestMD5 authzid authcid passwd = runErrorT $ do
, ["response" , digest ]
, ["charset" , "utf-8" ]
]
in Text.decodeUtf8 $ B64.encode response
quote :: BS8.ByteString -> BS8.ByteString
quote x = BS.concat ["\"",x,"\""]
toWord8 :: Int -> Word8
toWord8 x = fromIntegral x :: Word8
in B64.encode response
hash :: [BS8.ByteString] -> BS8.ByteString
hash = BS8.pack . show
. (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":")

52
source/Network/Xmpp/Sasl/Plain.hs

@ -40,34 +40,38 @@ import Network.Xmpp.Pickle @@ -40,34 +40,38 @@ import Network.Xmpp.Pickle
import qualified System.Random as Random
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import qualified Data.Text as Text
import Network.Xmpp.Sasl.Sasl
import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types
xmppPLAIN :: Maybe T.Text
-> T.Text
-> T.Text
-> XmppConMonad (Either AuthError ())
xmppPLAIN authzid authcid passwd = runErrorT $ do
_ <- lift . pushElement $ saslInitE "PLAIN" $ -- TODO: Check boolean?
Just $ Text.decodeUtf8 $ B64.encode $ Text.encodeUtf8 $ plainMessage authzid authcid passwd
lift $ pushElement saslResponse2E
e <- lift pullElement
case e of
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] ->
return ()
_ -> throwError AuthXmlError -- TODO: investigate
-- The SASL authentication has succeeded; the stream is restarted.
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream
-- TODO: stringprep
xmppPlain :: SaslM Text.Text
-> a
-> Text.Text
-> Maybe Text.Text
-> SaslM ()
xmppPlain pw _hostname authcid authzid = do
passwd <- pw
_ <- saslInit "PLAIN" ( Just $ plainMessage authzid authcid passwd)
_ <- pullSuccess
return ()
where
-- Converts an optional authorization identity, an authentication identity,
-- and a password to a \NUL-separated PLAIN message.
plainMessage :: Maybe T.Text -- Authorization identity (authzid)
-> T.Text -- Authentication identity (authcid)
-> T.Text -- Password
-> T.Text -- The PLAIN message
plainMessage authzid authcid passwd =
let authzid' = fromMaybe "" authzid in
T.concat [authzid', "\NUL", authcid, "\NUL", passwd]
plainMessage :: Maybe Text.Text -- Authorization identity (authzid)
-> Text.Text -- Authentication identity (authcid)
-> Text.Text -- Password
-> BS.ByteString -- The PLAIN message
plainMessage authzid authcid passwd = BS.concat $
[ authzid'
, "\NUL"
, Text.encodeUtf8 $ authcid
, "\NUL"
, Text.encodeUtf8 $ passwd
]
where
authzid' = maybe "" Text.encodeUtf8 authzid
plain :: SaslM Text.Text -> SaslHandler
plain passwd = ("PLAIN", xmppPlain passwd)

68
source/Network/Xmpp/Sasl/Sasl.hs

@ -1,68 +0,0 @@ @@ -1,68 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Sasl.Sasl where
import Network.Xmpp.Types
import Control.Monad.Error
import Data.Text
import qualified Data.Attoparsec.ByteString.Char8 as AP
import Data.XML.Pickle
import Data.XML.Types
import qualified Data.ByteString as BS
import Data.Maybe (fromMaybe)
import Network.Xmpp.Pickle
-- The <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> element, with an
-- optional round-trip value.
saslInitE :: Text -> Maybe Text -> Element
saslInitE mechanism rt =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}auth"
[("mechanism", [ContentText mechanism])]
[NodeContent $ ContentText $ fromMaybe "" rt]
-- SASL response with text payload.
saslResponseE :: Text -> Element
saslResponseE resp =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response"
[]
[NodeContent $ ContentText resp]
-- SASL response without payload.
saslResponse2E :: Element
saslResponse2E =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response"
[]
[]
-- Parses the incoming SASL data to a mapped list of pairs.
toPairs :: BS.ByteString -> Either String [(BS.ByteString, BS.ByteString)]
toPairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do
AP.skipSpace
name <- AP.takeWhile1 (/= '=')
_ <- AP.char '='
quote <- ((AP.char '"' >> return True) `mplus` return False)
content <- AP.takeWhile1 (AP.notInClass [',', '"'])
when quote . void $ AP.char '"'
return (name, content)
-- Failure element pickler.
failurePickle :: PU [Node] SaslFailure
failurePickle = xpWrap
(\(txt, (failure, _, _)) -> SaslFailure failure txt)
(\(SaslFailure failure txt) -> (txt,(failure,(),())))
(xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}failure"
(xp2Tuple
(xpOption $ xpElem
"{urn:ietf:params:xml:ns:xmpp-sasl}text"
xpLangTag
(xpContent xpId))
(xpElemByNamespace
"urn:ietf:params:xml:ns:xmpp-sasl"
xpPrim
(xpUnit)
(xpUnit))))
-- Challenge element pickler.
challengePickle :: PU [Node] Text
challengePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge"
(xpIsolate $ xpContent xpId)

139
source/Network/Xmpp/Sasl/Scram.hs

@ -0,0 +1,139 @@ @@ -0,0 +1,139 @@
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Sasl.Scram where
import Control.Applicative((<$>))
import Control.Monad.Error
import Control.Monad.Trans (liftIO)
import qualified Crypto.Classes as Crypto
import qualified Crypto.HMAC as Crypto
import qualified Crypto.Hash.SHA1 as Crypto
import Data.Binary(Binary,encode)
import Data.Bits
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 as BS8 (unpack)
import qualified Data.ByteString.Lazy as LBS
import Data.List (foldl1')
import qualified Data.Binary.Builder as Build
import Data.Maybe (maybeToList)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Word(Word8)
import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types
-- | Bit-wise xor of byte strings
xorBS :: BS.ByteString -> BS.ByteString -> BS.ByteString
xorBS x y = BS.pack $ BS.zipWith xor x y
-- | Join byte strings with ","
merge :: [BS.ByteString] -> BS.ByteString
merge = BS.intercalate ","
-- | Infix concatenation of byte strings
(+++) :: BS.ByteString -> BS.ByteString -> BS.ByteString
(+++) = BS.append
-- | 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 (username)
-> Maybe Text.Text -- ^ authorization ID
-> Text.Text -- ^ password
-> SaslM ()
scram hashToken authcid authzid' password = do
cnonce <- liftIO $ makeNonce
saslInit "SCRAM-SHA-1" (Just $ cFirstMessage cnonce)
liftIO $ putStrLn "pulling challenge"
sFirstMessage <- saslFromJust =<< pullChallenge
liftIO $ putStrLn "pulled challenge"
pairs <- toPairs sFirstMessage
(nonce, salt, ic) <- fromPairs pairs cnonce
let (cfm, v) = cFinalMessageAndVerifier nonce salt ic sFirstMessage cnonce
respond $ Just cfm
finalPairs <- toPairs =<< saslFromJust =<< pullFinalMessage
unless (lookup "v" finalPairs == Just v) $ throwError AuthServerAuthError
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 str = encode hashToken $ Crypto.hash' str
hmac key str = encode hashToken $ Crypto.hmac' (Crypto.MacKey key) str
authzid = (\z -> "a=" +++ normalize z) <$> authzid'
gs2CbindFlag = "n" -- we don't support channel binding yet
gs2Header = merge $ [ gs2CbindFlag
, maybe "" id authzid
, ""
]
cbindData = "" -- we don't support channel binding yet
cFirstMessageBare cnonce = merge [ "n=" +++ normalize authcid
, "r=" +++ cnonce]
cFirstMessage cnonce = gs2Header +++ cFirstMessageBare cnonce
fromPairs :: Pairs
-> BS.ByteString
-> SaslM (BS.ByteString, BS.ByteString, Int)
fromPairs pairs cnonce | Just nonce <- lookup "r" pairs
, cnonce `BS.isPrefixOf` nonce
, Just salt' <- lookup "s" pairs
, Right salt <- B64.decode salt'
, Just ic <- lookup "i" pairs
, [(i,"")] <- reads $ BS8.unpack ic
= return (nonce, salt, i :: Int)
fromPairs _ _ = throwError $ AuthChallengeError
cFinalMessageAndVerifier nonce salt ic sfm cnonce
= (merge [ cFinalMessageWOProof
, "p=" +++ B64.encode clientProof
]
, B64.encode serverSignature
)
where
cFinalMessageWOProof = merge [ "c=" +++ B64.encode gs2Header
, "r=" +++ nonce]
saltedPassword = hi (normalize password) salt ic
clientKey = hmac saltedPassword "Client Key"
storedKey = hash clientKey
authMessage = merge [ cFirstMessageBare cnonce
, sfm
, cFinalMessageWOProof
]
clientSignature = hmac storedKey authMessage
clientProof = clientKey `xorBS` clientSignature
serverKey = hmac saltedPassword "Server Key"
serverSignature = hmac serverKey authMessage
-- helper
hi str salt ic = foldl1' xorBS (take ic us)
where
u1 = hmac str (salt +++ (BS.pack [0,0,0,1]))
us = iterate (hmac str) u1
normalize = Text.encodeUtf8 . id -- TODO: stringprep
base64 = B64.encode
-- | 'scram' spezialised to the SHA-1 hash function, packaged as a SaslHandler
scramSha1 :: SaslM Text.Text -> SaslHandler
scramSha1 passwd = ("SCRAM-SHA-1"
, \_hostname authcid authzid -> do
pw <- passwd
scram (hashToken :: Crypto.SHA1) authcid authzid pw
)

19
source/Network/Xmpp/Sasl/Types.hs

@ -1,16 +1,31 @@ @@ -1,16 +1,31 @@
module Network.Xmpp.Sasl.Types where
import Control.Monad.Error
import Data.Text
import Control.Monad.State.Strict
import Data.ByteString(ByteString)
import qualified Data.Text as Text
import Network.Xmpp.Types
data AuthError = AuthXmlError
| AuthMechanismError [Text] -- ^ Wraps mechanisms offered
| AuthNoAcceptableMechanism [Text.Text] -- ^ Wraps mechanisms
-- offered
| AuthChallengeError
| AuthServerAuthError -- ^ The server failed to authenticate
-- himself
| AuthStreamError StreamError -- ^ Stream error on stream restart
| AuthConnectionError -- ^ No host name set in state
| AuthError -- General instance used for the Error instance
| AuthSaslFailure SaslFailure -- ^ defined SASL error condition
deriving Show
instance Error AuthError where
noMsg = AuthError
type SaslM a = ErrorT AuthError (StateT XmppConnection IO) a
type Pairs = [(ByteString, ByteString)]
type SaslHandler = (Text.Text, Text.Text
-> Text.Text
-> Maybe Text.Text
-> SaslM ())

17
source/Network/Xmpp/Types.hs

@ -22,8 +22,8 @@ module Network.Xmpp.Types @@ -22,8 +22,8 @@ module Network.Xmpp.Types
, PresenceType(..)
, SaslError(..)
, SaslFailure(..)
, SASLMechanism (..)
, SASLCredentials (..)
, SaslMechanism (..)
, SaslCredentials (..)
, ServerFeatures(..)
, Stanza(..)
, StanzaError(..)
@ -402,18 +402,18 @@ instance Read StanzaErrorCondition where @@ -402,18 +402,18 @@ instance Read StanzaErrorCondition where
-- OTHER STUFF
-- =============================================================================
data SASLCredentials = DIGEST_MD5Credentials (Maybe Text) Text Text
| PLAINCredentials (Maybe Text) Text Text
data SaslCredentials = DigestMD5Credentials (Maybe Text) Text Text
| PlainCredentials (Maybe Text) Text Text
instance Show SASLCredentials where
show (DIGEST_MD5Credentials authzid authcid _) = "DIGEST_MD5Credentials " ++
instance Show SaslCredentials where
show (DigestMD5Credentials authzid authcid _) = "DIGEST_MD5Credentials " ++
(Text.unpack $ fromMaybe "" authzid) ++ " " ++ (Text.unpack authcid) ++
" (password hidden)"
show (PLAINCredentials authzid authcid _) = "PLAINCredentials " ++
show (PlainCredentials authzid authcid _) = "PLAINCredentials " ++
(Text.unpack $ fromMaybe "" authzid) ++ " " ++ (Text.unpack authcid) ++
" (password hidden)"
data SASLMechanism = DIGEST_MD5 deriving Show
data SaslMechanism = DigestMD5 deriving Show
data SaslFailure = SaslFailure { saslFailureCondition :: SaslError
, saslFailureText :: Maybe ( Maybe LangTag
@ -655,6 +655,7 @@ data XmppConnection = XmppConnection @@ -655,6 +655,7 @@ data XmppConnection = XmppConnection
, sConnectionState :: XmppConnectionState
, sHostname :: Maybe Text
, sUsername :: Maybe Text
, sAuthzid :: Maybe Text
, sResource :: Maybe Text
, sCloseConnection :: IO ()
-- TODO: add default Language

10
tests/Tests.hs

@ -12,9 +12,9 @@ import qualified Data.Text as Text @@ -12,9 +12,9 @@ import qualified Data.Text as Text
import Data.XML.Pickle
import Data.XML.Types
import Network.XMPP
import Network.XMPP.IM.Presence
import Network.XMPP.Pickle
import Network.Xmpp
import Network.Xmpp.IM.Presence
import Network.Xmpp.Pickle
import System.Environment
import Text.XML.Stream.Elements
@ -29,7 +29,7 @@ supervisor :: JID @@ -29,7 +29,7 @@ supervisor :: JID
supervisor = read "uart14@species64739.dyndns.org"
attXmpp :: STM a -> XMPP a
attXmpp :: STM a -> Xmpp a
attXmpp = liftIO . atomically
testNS :: Text
@ -70,7 +70,7 @@ iqResponder = do @@ -70,7 +70,7 @@ iqResponder = do
liftIO $ threadDelay 1000000
endSession
autoAccept :: XMPP ()
autoAccept :: Xmpp ()
autoAccept = forever $ do
st <- waitForPresence isPresenceSubscribe
sendPresence $ presenceSubscribed (fromJust $ presenceFrom st)

Loading…
Cancel
Save