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.
 

228 lines
9.4 KiB

{-
Copyright © 2010-2011 Jon Kristensen.
This file is part of Pontarius XMPP.
Pontarius XMPP is free software: you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
Pontarius XMPP is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License along
with Pontarius XMPP. If not, see <http://www.gnu.org/licenses/>.
-}
{-# OPTIONS_HADDOCK hide #-}
-- TODO: Make it possible to include host.
-- TODO: Host is assumed to be ISO 8859-1; make list of assumptions.
-- TODO: Can it contain newline characters?
module Network.XMPP.SASL (replyToChallenge1) where
import Data.ByteString.Internal (c2w)
import Data.Char (isLatin1)
import Data.Digest.Pure.MD5
import qualified Data.Binary as DBi (Binary, encode)
import qualified Data.ByteString.Lazy as DBL (ByteString, append, pack,
fromChunks, toChunks, null)
import qualified Data.ByteString.Lazy.Char8 as DBLC (append, pack, unpack)
import qualified Data.List as DL
import Data.Text (empty, singleton)
import Text.StringPrep (StringPrepProfile (..), a1, b1, c12, c21, c22, c3, c4, c5, c6, c7, c8, c9, runStringPrep)
import Data.Ranges (inRanges, ranges)
data Challenge1Error = C1MultipleCriticalAttributes |
C1NotAllParametersPresent |
C1SomeParamtersPresentMoreThanOnce |
C1WrongRealm |
C1UnsupportedAlgorithm |
C1UnsupportedCharset |
C1UnsupportedQOP
deriving Show
-- Will produce a list of key-value pairs given a string in the format of
-- realm="somerealm",nonce="OA6MG9tEQGm2hh",qop="auth",charset=utf-8...
stringToList :: String -> [(String, String)]
stringToList "" = []
stringToList s' = let (next, rest) = break' s' ','
in break' next '=' : stringToList rest
where
-- Like break, but will remove the first char of the continuation, if
-- present.
break' :: String -> Char -> (String, String)
break' s' c = let (first, second) = break ((==) c) s'
in (first, removeCharIfPresent second c)
-- Removes the first character, if present; "=hello" with '=' becomes
-- "hello".
removeCharIfPresent :: String -> Char -> String
removeCharIfPresent [] _ = []
removeCharIfPresent (c:t) c' | c == c' = t
removeCharIfPresent s' c = s'
-- Counts the number of directives in the pair list.
countDirectives :: String -> [(String, String)] -> Int
countDirectives v l = DL.length $ filter (isEntry v) l
where
isEntry :: String -> (String, String) -> Bool
isEntry name (name', _) | name == name' = True
| otherwise = False
-- Returns the given directive in the list of pairs, or Nothing.
lookupDirective :: String -> [(String, String)] -> Maybe String
lookupDirective d [] = Nothing
lookupDirective d ((d', v):t) | d == d' = Just v
| otherwise = lookupDirective d t
-- Returns the given directive in the list of pairs, or the default value
-- otherwise.
lookupDirectiveWithDefault :: String -> [(String, String)] -> String -> String
lookupDirectiveWithDefault di l de
| lookup == Nothing = de
| otherwise = let Just r = lookup in r
where
lookup = lookupDirective di l
-- Takes a challenge string (which is not Base64-encoded), the host name of the
-- Jabber server, the Jabber user name (JID), the password and a random and
-- unique "cnonce" value and generates either an error or a response to that
-- challenge.
-- We have broken replyToChallenge1 for non-TLS authentication. In order to
-- change it back, just uncomment the lines relevant to the realm and match it
-- in the C1NotAllParametersSet case.
replyToChallenge1 :: String -> String -> String -> String -> String ->
Either String Challenge1Error
replyToChallenge1 s h u p c =
-- Remove all new line characters.
let list = stringToList $ filter (/= '\n') s
in -- Count that there are no more than one nonce or algorithm directives.
case countDirectives "nonce" list <= 1 &&
countDirectives "algorithm" list <= 1 of
True ->
let -- realm = lookupDirective "realm" list
nonce = lookupDirective "nonce" list
qop = lookupDirectiveWithDefault "qop" list "auth"
charset = lookupDirectiveWithDefault "charset" list "utf-8"
algorithm = lookupDirective "algorithm" list
-- Verify that all necessary directives has been set.
in case (nonce, qop, charset, algorithm) of
(Just nonce', qop', charset', Just algorithm') ->
-- Strip quotations of the directives that need it.
let -- realm'' = stripQuotations realm'
nonce'' = stripQuotations nonce'
qop'' = stripQuotations qop' -- It seems ejabberd gives us an errorous "auth" instead of auth
in
-- -- Verify that the realm is the same as the Jabber host.
-- case realm'' == h of
-- True ->
-- Verify that QOP is "auth", charset is "utf-8" and that
-- the algorithm is "md5-sess".
case qop'' == "auth" of
True ->
case charset' == "utf-8" of
True ->
case algorithm' == "md5-sess" of
True ->
-- All data is valid; generate the reply.
Left (reply nonce'' qop'')
-- Errors are caught and reported below.
False -> Right C1UnsupportedAlgorithm
False -> Right C1UnsupportedCharset
False -> Right C1UnsupportedQOP
-- False -> Right C1WrongRealm
_ -> Right C1NotAllParametersPresent
where
reply n q =
let -- We start with what's in RFC 2831 is referred to as "A1", a 16 octet
-- MD5 hash.
-- If the username or password values are in ISO-8859-1, we convert
-- them to ISO-8859-1 strings.
username = case all isLatin1 u of
True -> DBL.pack $ map c2w u
False -> DBLC.pack $ u
password = case all isLatin1 p of
True -> DBL.pack $ map c2w p
False -> DBLC.pack p
nc = "00000001"
digestUri = "xmpp/" ++ h
-- Build the "{ username-value, ":", realm-value, ":", passwd }"
-- bytestring, the rest of the bytestring and then join them.
a1a = DBi.encode $ md5 $ DBLC.append
(DBLC.append username (DBLC.pack (":" ++ h ++ ":")))
password
a1aDebug = "DBi.encode $ md5 $ " ++ (DBLC.unpack $ DBLC.append
(DBLC.append username (DBLC.pack (":" ++ h ++ ":")))
password)
a1b = DBLC.pack (":" ++ n ++ ":" ++ c)
a1 = DBLC.append a1a a1b
-- Generate the "A2" value.
a2 = DBLC.pack ("AUTHENTICATE:" ++ digestUri)
-- Produce the responseValue.
k = DBLC.pack (show $ md5 a1)
colon = DBLC.pack ":"
s0 = DBLC.pack (n ++ ":" ++ nc ++ ":" ++ c ++ ":" ++
q ++ ":")
s1 = DBLC.pack $ show $ md5 a2
s_ = DBLC.append s0 s1
-- append k:d and 16 octet hash it
kd = md5 (DBLC.append k (DBLC.append colon s_))
lol0 = DBLC.unpack s_
lol1 = show kd
response = show kd
in "username=\"" ++ u ++ "\",realm=\"" ++ h ++ "\",nonce=\"" ++ n ++
"\",cnonce=\"" ++ c ++ "\",nc=" ++ nc ++ ",digest-uri=\"" ++
digestUri ++ "\",qop=auth,response=" ++ response ++ ",charset=utf-8"
-- "\n\n" ++
-- "a1aDebug: " ++ a1aDebug ++ "\n" ++
-- "a1b: " ++ (DBLC.unpack a1b) ++ "\n" ++
-- "a1: " ++ (DBLC.unpack a1) ++ "\n" ++
-- "a2: " ++ (DBLC.unpack a2) ++ "\n" ++
-- "k: " ++ (DBLC.unpack k) ++ "\n" ++
-- "colon: " ++ (DBLC.unpack colon) ++ "\n" ++
-- "s0: " ++ (DBLC.unpack s0) ++ "\n" ++
-- "s1: " ++ (DBLC.unpack s1) ++ "\n" ++
-- "s_: " ++ (DBLC.unpack s_) ++ "\n"
-- Stripts the quotations around a string, if any; "\"hello\"" becomes "hello".
stripQuotations :: String -> String
stripQuotations "" = ""
stripQuotations s | (head s == '"') && (last s == '"') = tail $ init s
| otherwise = s
saslprepProfile :: StringPrepProfile
saslprepProfile = Profile { maps = [\ char -> if char `inRanges` (ranges c12) then singleton '\x0020' else empty, b1]
, shouldNormalize = True
, prohibited = [a1] ++ [c12, c21, c22, c3, c4, c5, c6, c7, c8, c9]
, shouldCheckBidi = True }