unit IdSASLDigest; interface {$i IdCompilerDefines.inc} uses Classes, SysUtils, //here to facilitate inline expansion IdSASL, IdSASLUserPass, IdUserPassProvider, IdException; type TIdSASLDigest = class(TIdSASLUserPass) protected Fauthzid : String; public function StartAuthenticate(const AChallenge, AHost, AProtocolName:string) : String; override; function ContinueAuthenticate(const ALastResponse, AHost, AProtocolName : string): string; override; class function ServiceName: TIdSASLServiceName; override; function IsReadyToStart: Boolean; override; published property authzid : String read Fauthzid write Fauthzid; end; EIdSASLDigestException = class(EIdException); EIdSASLDigestChallException = class(EIdSASLDigestException); EIdSASLDigestChallNoAlgorithm = class(EIdSASLDigestChallException); EIdSASLDigestChallInvalidAlg = class(EIdSASLDigestChallException); EIdSASLDigestAuthConfNotSupported = class(EIdSASLDigestException); //done this way so we can use testboxes function CalcDigestResponse(const AUserName, APassword, ARealm, ANonce, ACNonce : String; const ANC : Integer; const AQop, ADigestURI : String; const AAuthzid : String = '') : String; implementation uses IdFIPS, IdGlobal, IdGlobalProtocols, IdHash, IdHashMessageDigest, IdResourceStringsProtocols; function NCToStr(const AValue : Integer):String; {$IFDEF USE_INLINE} inline; {$ENDIF} begin Result := IntToHex(AValue,8); end; function Unquote(var S: String): String; {$IFDEF USE_INLINE} inline; {$ENDIF} var I, Len: Integer; begin Len := Length(S); I := 2; // skip first quote while I <= Len do begin if S[I] = '"' then begin Break; end; if S[I] = '\' then begin Inc(I); end; Inc(I); end; Result := Copy(S, 2, I-2); S := Copy(S, I+1, MaxInt); end; // function HashResult(const AStr : String): TIdBytes; {$IFDEF USE_INLINE} inline; {$ENDIF} var LMD5: TIdHashMessageDigest5; begin LMD5 := TIdHashMessageDigest5.Create; try Result := LMD5.HashString(AStr); finally LMD5.Free; end; end; function HashResultAsHex(const ABytes : TIdBytes) : String; overload; {$IFDEF USE_INLINE} inline; {$ENDIF} var LMD5: TIdHashMessageDigest5; begin LMD5 := TIdHashMessageDigest5.Create; try Result := LowerCase(LMD5.HashBytesAsHex(ABytes)); finally LMD5.Free; end; end; function HashResultAsHex(const AStr : String) : String; overload; {$IFDEF USE_INLINE} inline; {$ENDIF} var LMD5: TIdHashMessageDigest5; begin LMD5 := TIdHashMessageDigest5.Create; try Result := LowerCase(LMD5.HashStringAsHex(AStr)); finally LMD5.Free; end; end; function CalcDigestResponse(const AUserName, APassword, ARealm, ANonce, ACNonce : String; const ANC : Integer; const AQop, ADigestURI : String; const AAuthzid : String = '') : String; var LA1 : TIdBytes; LA2: TIdBytes; LA1_P : TIdBytes; begin LA1_P := IdGlobal.ToBytes(':' + ANonce + ':' + ACNonce); LA1 := HashResult(AUserName + ':' + ARealm + ':' + APassword); IdGlobal.AppendBytes(LA1,LA1_P); If AAuthzid <> '' then begin IdGlobal.AppendBytes(LA1,IdGlobal.ToBytes(AAuthzid)); end; if AQop = 'auth' then begin LA2 := ToBytes('AUTHENTICATE:' + ADigestURI); end else if (AQop = 'auth-int') or (AQop = 'auth-conf') then begin LA2 := ToBytes('AUTHENTICATE:' + ADigestURI + ':00000000000000000000000000000000'); end else begin SetLength(LA2,0); end; Result := HashResultAsHex(HashResultAsHex(LA1) + ':' + ANonce + ':' + NCToStr(ANC) + ':' + ACNonce + ':' + AQop +':' + HashResultAsHex(LA2)); end; // { TIdSASLDigest } function TIdSASLDigest.ContinueAuthenticate(const ALastResponse, AHost, AProtocolName: string): string; begin Result := ''; end; function TIdSASLDigest.IsReadyToStart: Boolean; begin Result := not GetFIPSMode; end; class function TIdSASLDigest.ServiceName: TIdSASLServiceName; begin Result := 'DIGEST-MD5'; end; function TIdSASLDigest.StartAuthenticate(const AChallenge, AHost, AProtocolName: string): String; var LBuf : String; LChallange: TStringList; LReply : TStringList; Lqop : String; LstrCNonce : String; LstrResponse : String; LURL : String; LCharset : String; LQopOptions: TStrings; LAlgorithm : String; LNonce : String; LRealm: String; LName, LValue: String; begin LURL := AProtocolName+'/'+AHost; LReply := TStringList.Create; LChallange := TStringList.Create; LQopOptions:= TStringList.Create; try LBuf := AChallenge; while Length(LBuf) > 0 do begin LName := Trim(Fetch(LBuf, '=')); {do not localize} LBuf := TrimLeft(LBuf); if TextStartsWith(LBuf, '"') then begin {do not localize} LValue := Unquote(LBuf); {do not localize} Fetch(LBuf, ','); {do not localize} end else begin LValue := Trim(Fetch(LBuf, ',')); end; LChallange.Add(LName + '=' + LValue); LBuf := TrimLeft(LBuf); end; LQopOptions.CommaText := LChallange.Values['qop']; if LQopOptions.IndexOf('auth-int') > -1 then begin Lqop := 'auth-int'; end else begin Lqop := 'auth'; end; if LQopOptions.IndexOf('auth-conf') > -1 then begin if LQopOptions.IndexOf('auth') = -1 then begin raise EIdSASLDigestAuthConfNotSupported.Create(RSSASLDigestAuthConfNotSupported); end; end; LNonce := LChallange.Values['nonce']; LRealm := LChallange.Values['realm']; LAlgorithm := LChallange.Values['algorithm']; if LAlgorithm = '' then begin raise EIdSASLDigestChallNoAlgorithm.Create(RSSASLDigestMissingAlgorithm); end; { if LAlgorithm <> 'md5-sess' then begin raise EIdSASLDigestChallInvalidAlg.Create(RSSASLDigestInvalidAlgorithm); end; } //Commented out for case test mentioned in RFC 2831 LstrCNonce := HashResultAsHex(DateTimeToStr(Now)); LCharset := LChallange.Values['charset']; LstrResponse := CalcDigestResponse(GetUserName,Self.GetPassword,LRealm,LNonce,LstrCNonce, 1, Lqop, LURL,Fauthzid); // if LQopOptions.IndexOf('auth-conf') > -1 then begin // if LQopOptions.IndexOf('auth') = -1 then begin // raise EIdSASLDigestAuthConfNotSupported.Create(RSSASLDigestAuthConfNotSupported); // end; // end; if LCharset = '' then begin Result := ''; end else begin Result := 'charset='+LCharset+','; end; { #( username | realm | nonce | cnonce | nonce-count | qop | digest-uri | response | maxbuf | charset | cipher | authzid | auth-param ) } Result := Result + 'username="'+GetUsername+'"'+ ',realm="'+LRealm+'"'+ {Do not localize} ',nonce="'+ LNonce+'"'+ ',nc='+NCToStr(1)+ ',cnonce="'+LstrCNonce+'"'+ ',digest-uri="'+LURL+'"'+ ',response='+LstrResponse+ ',qop='+Lqop; finally FreeAndNil(LQopOptions); FreeAndNil(LChallange); FreeAndNil(LReply); end; end; end.