435 lines
13 KiB
Plaintext
435 lines
13 KiB
Plaintext
{
|
|
$Project$
|
|
$Workfile$
|
|
$Revision$
|
|
$DateUTC$
|
|
$Id$
|
|
|
|
This file is part of the Indy (Internet Direct) project, and is offered
|
|
under the dual-licensing agreement described on the Indy website.
|
|
(http://www.indyproject.org/)
|
|
|
|
Copyright:
|
|
(c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
|
|
}
|
|
{
|
|
$Log$
|
|
}
|
|
{
|
|
Rev 1.2 7/6/2004 4:53:52 PM DSiders
|
|
Corrected spelling of Challenge in properties, methods, types.
|
|
|
|
Rev 1.1 4/12/2003 10:24:10 PM GGrieve
|
|
Fix to Compile
|
|
|
|
Rev 1.0 11/13/2002 08:04:22 AM JPMugaas
|
|
}
|
|
{*******************************************************}
|
|
{ }
|
|
{ Indy OTP User Account Manager }
|
|
{ }
|
|
{ Copyright (C) 2000 Winshoes Working Group }
|
|
{ Original author J. Peter Mugaas }
|
|
{ 2002-Nov-2 }
|
|
{ Based on RFC 2289 }
|
|
{ }
|
|
{*******************************************************}
|
|
{
|
|
Note: One vulnerability in OTP is a race condition where
|
|
a user connects to a server, gets a Challenge, then a hacker
|
|
connects to the system and then the hacker guesses the OTP password.
|
|
To prevent this, servers should not allow a user to connect to the server
|
|
during the authentication process.
|
|
}
|
|
{2002-Nov-3 J. Peter Mugaas
|
|
-Renamed units and classes from SKey to OTP. SKey is a
|
|
trademark of BellCore. One Time Only (OTP is a more accurate description anyway.
|
|
-Made properties less prone to entry errors
|
|
-Now disregards white space with OTP
|
|
-Will now accept the OTP Password as Hexidecimal
|
|
-Will now accept the OTP Password in either lower or uppercase }
|
|
|
|
unit IdUserAccountsOTP;
|
|
|
|
interface
|
|
|
|
{$i IdCompilerDefines.inc}
|
|
|
|
uses
|
|
Classes,
|
|
IdBaseComponent,
|
|
IdComponent,
|
|
IdException,
|
|
IdUserAccounts,
|
|
IdGlobal, SysUtils;
|
|
|
|
const
|
|
DEF_MAXCount = 900;
|
|
|
|
type
|
|
TIdOTPUserManager = class;
|
|
TIdOTPUserAccounts = class;
|
|
|
|
TIdOTPPassword = (IdPW_NoEncryption, IdPW_OTP_MD4, IdPW_OTP_MD5, IdPW_OTP_SHA1);
|
|
|
|
TIdOTPUserAccount = class(TIdUserAccount)
|
|
protected
|
|
FPasswordType : TIdOTPPassword;
|
|
FCurrentCount : LongWord;
|
|
FSeed : String;
|
|
FAuthenticating : Boolean;
|
|
FNoReenter : TCriticalSection;
|
|
procedure SetSeed(const AValue : String);
|
|
procedure SetPassword(const AValue: String); override;
|
|
public
|
|
constructor Create(Collection: TCollection); override;
|
|
destructor Destroy; override;
|
|
function CheckPassword(const APassword: String): Boolean; override;
|
|
published
|
|
property CurrentCount : LongWord read FCurrentCount write FCurrentCount;
|
|
property Seed : String read FSeed write SetSeed;
|
|
property PasswordType : TIdOTPPassword read FPasswordType write FPasswordType;
|
|
property Authenticating : Boolean read FAuthenticating write FAuthenticating;
|
|
end;
|
|
|
|
TIdOTPUserAccounts = class(TOwnedCollection)
|
|
protected
|
|
//
|
|
function GetAccount(const AIndex: Integer): TIdOTPUserAccount;
|
|
function GetByUsername(const AUsername: String): TIdOTPUserAccount;
|
|
procedure SetAccount(const AIndex: Integer; AAccountValue: TIdOTPUserAccount);
|
|
public
|
|
function Add: TIdOTPUserAccount; reintroduce;
|
|
constructor Create(AOwner: TIdOTPUserManager); reintroduce;
|
|
//
|
|
property UserNames[const AUserName: String]: TIdOTPUserAccount read GetByUsername; default;
|
|
property Items[const AIndex: Integer]: TIdOTPUserAccount read GetAccount write SetAccount;
|
|
end;//TIdOTPUserAccounts
|
|
|
|
TIdOTPUserManager = class(TIdCustomUserManager)
|
|
protected
|
|
FMaxCount : LongWord;
|
|
FAccounts : TIdOTPUserAccounts;
|
|
FDefaultPassword : String;
|
|
procedure DoAuthentication(const AUsername: String; var VPassword: String;
|
|
var VUserHandle: TIdUserHandle; var VUserAccess: TIdUserAccess); override;
|
|
procedure SetMaxCount(const AValue: LongWord);
|
|
procedure SetDefaultPassword(const AValue : String);
|
|
public
|
|
constructor Create(AOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
procedure UserDisconnected(const AUser : String); override;
|
|
function SendsChallange : Boolean; override;
|
|
property Accounts : TIdOTPUserAccounts read FAccounts;
|
|
|
|
published
|
|
property DefaultPassword : String read FDefaultPassword write SetDefaultPassword;
|
|
property MaxCount : LongWord read FMaxCount write SetMaxCount default DEF_MAXCount;
|
|
end;
|
|
|
|
EIdOTPException = class(EIdException);
|
|
EIdOTPInvalidSeed = class(EIdOTPException);
|
|
EIdOTPInvalidCount = class(EIdOTPException);
|
|
EIdOTPInvalidPassword = class(EIdOTPException);
|
|
|
|
function GenerateSeed : String;
|
|
|
|
implementation
|
|
|
|
uses
|
|
IdOTPCalculator;
|
|
|
|
resourcestring
|
|
RSOTP_Challenge = 'Response to %s required for OTP.';
|
|
RSOTP_SeedBadFormat = 'The seed must be alphanumeric and it must at least 1 character but no more than 16 characters.';
|
|
RSOTP_InvalidCount = 'The count must be greater than 1.';
|
|
RSOTP_InvalidPassword = 'The password must be longer than 10 characters but no more than 63 characters.';
|
|
//This must be longer than 9 characters but no more than 63 characters in length
|
|
RSOTP_DefaultPassword = 'PleaseChangeMeNow';
|
|
|
|
const
|
|
CharMap = 'abcdefghijklmnopqrstuvwxyz1234567890'; {Do not Localize}
|
|
|
|
function GetRandomString(NumChar: LongWord): string;
|
|
var
|
|
i: Integer;
|
|
MaxChar: LongWord;
|
|
begin
|
|
randomize;
|
|
MaxChar := Length(CharMap) - 1;
|
|
for i := 1 to NumChar do begin
|
|
// Add one because CharMap is 1-based
|
|
Result := Result + CharMap[Random(maxChar) + 1];
|
|
end;
|
|
end;
|
|
|
|
function IsValidPassword(const AValue : String): Boolean;
|
|
begin
|
|
Result := (Length(AValue) > 9) and (Length(AValue) < 64);
|
|
end;
|
|
|
|
function IsValidSeed(const ASeed : String) : Boolean;
|
|
var
|
|
i : Integer;
|
|
begin
|
|
Result := (ASeed <> '') and (Length(ASeed) < 17);
|
|
if Result then begin
|
|
for i := 1 to Length(ASeed) do begin
|
|
if not CharIsInSet(ASeed, i, CharMap) then begin
|
|
Result := False;
|
|
Break;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function GenerateSeed : String;
|
|
begin
|
|
Randomize;
|
|
Result := GetRandomString(Random(15)+1);
|
|
end;
|
|
|
|
function LowStripWhiteSpace(const AString : String): String;
|
|
var
|
|
i : Integer;
|
|
begin
|
|
Result := '';
|
|
for i := 1 to Length(AString) do begin
|
|
if not (AString[i] in LWS) then begin
|
|
Result := Result + LowerCase(AString[i]);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ TIdOTPUserManager }
|
|
|
|
function TIdOTPUserManager.ChallengeUser(var VIsSafe: Boolean; const AUserName: String): String;
|
|
var
|
|
LUser : TIdOTPUserAccount;
|
|
begin
|
|
Result := '';
|
|
LUser := FAccounts.UserNames[AUserName];
|
|
if (not Assigned(LUser)) or (LUser.PasswordType = IdPW_NoEncryption) then begin
|
|
Exit;
|
|
end;
|
|
VIsSafe := not LUser.Authenticating;
|
|
if VIsSafe then begin
|
|
//Note that we want to block any attempts to access the server after the challanage
|
|
//is given. This is required to prevent a race condition that a hacker can
|
|
//exploit.
|
|
LUser.FNoReenter.Acquire;
|
|
try
|
|
LUser.Authenticating := True;
|
|
Result := 'otp-'; {Do not translate}
|
|
case LUser.PasswordType of
|
|
IdPW_OTP_MD4 : Result := Result + 'md4 '; {Do not translate}
|
|
IdPW_OTP_MD5 : Result := Result + 'md5 '; {Do not translate}
|
|
IdPW_OTP_SHA1 : Result := Result + 'sha1 '; {Do not translate}
|
|
end;
|
|
Result := Result + IntToStr(LUser.CurrentCount) + ' ' + LUser.Seed;
|
|
Result := IndyFormat(RSOTP_Challenge, [Result]);
|
|
finally
|
|
LUser.FNoReenter.Release;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
constructor TIdOTPUserManager.Create(AOwner: TComponent);
|
|
begin
|
|
inherited Create(AOwner);
|
|
FAccounts := TIdOTPUserAccounts.Create(Self);
|
|
FMaxCount := DEF_MAXCount;
|
|
FDefaultPassword := RSOTP_DefaultPassword;
|
|
end;
|
|
|
|
destructor TIdOTPUserManager.Destroy;
|
|
begin
|
|
FreeAndNil(FAccounts);
|
|
inherited Destroy;;
|
|
end;
|
|
|
|
procedure TIdOTPUserManager.DoAuthentication(const AUsername: String;
|
|
var VPassword: String; var VUserHandle: TIdUserHandle; var VUserAccess: TIdUserAccess);
|
|
var
|
|
LUser: TIdUserAccount;
|
|
begin
|
|
inherited DoAuthentication(AUsername, VPassword, VUserHandle, VUserAccess);
|
|
VUserHandle := IdUserHandleNone;
|
|
VUserAccess := IdUserAccessDenied;
|
|
LUser := FAccounts[AUsername];
|
|
if Assigned(LUser) then begin
|
|
if LUser.CheckPassword(VPassword) then begin
|
|
VUserHandle := LUser.ID;
|
|
VUserAccess := LUser.Access;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TIdOTPUserManager.LoadUserAccounts(const AIniFile: String);
|
|
begin
|
|
|
|
end;
|
|
|
|
procedure TIdOTPUserManager.SaveUserAccounts(const AIniFile: String);
|
|
begin
|
|
|
|
end;
|
|
|
|
procedure TIdOTPUserManager.SetDefaultPassword(const AValue: String);
|
|
begin
|
|
if not IsValidPassword(AValue) then begin
|
|
raise EIdOTPInvalidPassword.Create(RSOTP_InvalidPassword);
|
|
end;
|
|
FDefaultPassword := AValue;
|
|
end;
|
|
|
|
procedure TIdOTPUserManager.SetMaxCount(const AValue: LongWord);
|
|
begin
|
|
if AValue <= 1 then begin
|
|
raise EIdOTPInvalidCount.Create(RSOTP_InvalidCount);
|
|
end;
|
|
FMaxCount := AValue;
|
|
end;
|
|
|
|
procedure TIdOTPUserManager.UserDisconnected(const AUser: String);
|
|
var
|
|
LUser : TIdOTPUserAccount;
|
|
begin
|
|
inherited UserDisconnected(AUser);
|
|
LUser := FAccounts.UserNames[AUserName];
|
|
if Assigned(LUser) then begin
|
|
LUser.Authenticating := False;
|
|
end;
|
|
end;
|
|
|
|
{ TIdOTPUserAccounts }
|
|
|
|
function TIdOTPUserAccounts.Add: TIdOTPUserAccount;
|
|
begin
|
|
Result := inherited Add as TIdOTPUserAccount;
|
|
Result.Seed := GenerateSeed;
|
|
Result.CurrentCount := TIdOTPUserManager(GetOwner).MaxCount;
|
|
Result.Password := TIdOTPUserManager(GetOwner).DefaultPassword;
|
|
end;
|
|
|
|
constructor TIdOTPUserAccounts.Create(AOwner: TIdOTPUserManager);
|
|
begin
|
|
inherited Create(AOwner, TIdOTPUserAccount);
|
|
end;
|
|
|
|
function TIdOTPUserAccounts.GetAccount(const AIndex: Integer): TIdOTPUserAccount;
|
|
begin
|
|
Result := TIdOTPUserAccount(inherited Items[AIndex]);
|
|
end;
|
|
|
|
function TIdOTPUserAccounts.GetByUsername(const AUsername: String): TIdOTPUserAccount;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
Result := nil;
|
|
for i := 0 to Count - 1 do begin
|
|
if AUsername = Items[i].UserName then begin
|
|
Result := Items[i];
|
|
Break;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TIdOTPUserAccounts.SetAccount(const AIndex: Integer; AAccountValue: TIdOTPUserAccount);
|
|
begin
|
|
inherited SetItem(AIndex, AAccountValue);
|
|
end;
|
|
|
|
{ TIdOTPUserAccount }
|
|
|
|
function TIdOTPUserAccount.CheckPassword(const APassword: String): Boolean;
|
|
var
|
|
LWordOTP : String;
|
|
LHashSum : Int64;
|
|
LRecPass : String;
|
|
LHexOTP : String;
|
|
begin
|
|
LHexOTP := '';
|
|
LRecPass := APassword;
|
|
case FPasswordType of
|
|
IdPW_NoEncryption :
|
|
begin
|
|
LWordOTP := Password;
|
|
end;
|
|
IdPW_OTP_MD4 :
|
|
begin
|
|
LRecPass := LowStripWhiteSpace(APassword);
|
|
LHashSum := TIdOTPCalculator.GenerateKeyMD4(FSeed, Password, FCurrentCount);
|
|
LWordOTP := LowStripWhiteSpace(TIdOTPCalculator.ToSixWordFormat(LHashSum));
|
|
LHexOTP := LowStripWhiteSpace(TIdOTPCalculator.ToHex(LHashSum));
|
|
end;
|
|
IdPW_OTP_MD5 :
|
|
begin
|
|
LRecPass := LowStripWhiteSpace(APassword);
|
|
LHashSum := TIdOTPCalculator.GenerateKeyMD5(FSeed, Password, FCurrentCount);
|
|
LWordOTP := LowStripWhiteSpace(TIdOTPCalculator.ToSixWordFormat(LHashSum));
|
|
LHexOTP := LowStripWhiteSpace(TIdOTPCalculator.ToHex(LHashSum));
|
|
end;
|
|
IdPW_OTP_SHA1 :
|
|
begin
|
|
LRecPass := LowStripWhiteSpace(APassword);
|
|
LHashSum := TIdOTPCalculator.GenerateKeySHA1(FSeed, Password, FCurrentCount);
|
|
LWordOTP := LowStripWhiteSpace(TIdOTPCalculator.ToSixWordFormat(LHashSum));
|
|
LHexOTP := LowStripWhiteSpace(TIdOTPCalculator.ToHex(LHashSum));
|
|
end;
|
|
end;
|
|
Result := (LRecPass = LWordOTP);
|
|
if (not Result) and (LHexOTP <> '') then begin
|
|
Result := (LRecPass = LHexOTP);
|
|
end;
|
|
if Result then begin
|
|
FNoReenter.Acquire;
|
|
try
|
|
if CurrentCount = 0 then begin
|
|
Seed := GenerateSeed;
|
|
end else begin
|
|
Dec(FCurrentCount);
|
|
end;
|
|
Authenticating := False;
|
|
finally
|
|
FNoReenter.Release;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
constructor TIdOTPUserAccount.Create(Collection: TIdCollection);
|
|
begin
|
|
inherited Create(Collection);
|
|
FNoReenter := TCriticalSection.Create;
|
|
end;
|
|
|
|
destructor TIdOTPUserAccount.Destroy;
|
|
begin
|
|
FreeAndNil(FNoReenter);
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TIdOTPUserAccount.SetPassword(const AValue: String);
|
|
begin
|
|
if not IsValidPassword(AValue) then begin
|
|
raise EIdOTPInvalidPassword.Create(RSOTP_InvalidPassword);
|
|
end;
|
|
inherited SetPassword(AValue);
|
|
end;
|
|
|
|
procedure TIdOTPUserAccount.SetSeed(const AValue: String);
|
|
begin
|
|
if not IsValidSeed(LowerCase(AValue)) then begin
|
|
raise EIdOTPInvalidSeed.Create(RSOTP_SeedBadFormat);
|
|
end;
|
|
FSeed := LowerCase(AValue);
|
|
FCurrentCount := TIdOTPUserManager(TIdOTPUserAccounts(Collection).GetOwner).MaxCount;
|
|
end;
|
|
|
|
function TIdOTPUserAccount.SendsChallange : Boolean;
|
|
begin
|
|
Result := True;
|
|
end;
|
|
|
|
end.
|