IdUserAccountsOTP.pas 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. {
  2. $Project$
  3. $Workfile$
  4. $Revision$
  5. $DateUTC$
  6. $Id$
  7. This file is part of the Indy (Internet Direct) project, and is offered
  8. under the dual-licensing agreement described on the Indy website.
  9. (http://www.indyproject.org/)
  10. Copyright:
  11. (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
  12. }
  13. {
  14. $Log$
  15. }
  16. {
  17. Rev 1.2 7/6/2004 4:53:52 PM DSiders
  18. Corrected spelling of Challenge in properties, methods, types.
  19. Rev 1.1 4/12/2003 10:24:10 PM GGrieve
  20. Fix to Compile
  21. Rev 1.0 11/13/2002 08:04:22 AM JPMugaas
  22. }
  23. {*******************************************************}
  24. { }
  25. { Indy OTP User Account Manager }
  26. { }
  27. { Copyright (C) 2000 Winshoes Working Group }
  28. { Original author J. Peter Mugaas }
  29. { 2002-Nov-2 }
  30. { Based on RFC 2289 }
  31. { }
  32. {*******************************************************}
  33. {
  34. Note: One vulnerability in OTP is a race condition where
  35. a user connects to a server, gets a Challenge, then a hacker
  36. connects to the system and then the hacker guesses the OTP password.
  37. To prevent this, servers should not allow a user to connect to the server
  38. during the authentication process.
  39. }
  40. {2002-Nov-3 J. Peter Mugaas
  41. -Renamed units and classes from SKey to OTP. SKey is a
  42. trademark of BellCore. One Time Only (OTP is a more accurate description anyway.
  43. -Made properties less prone to entry errors
  44. -Now disregards white space with OTP
  45. -Will now accept the OTP Password as Hexidecimal
  46. -Will now accept the OTP Password in either lower or uppercase }
  47. unit IdUserAccountsOTP;
  48. interface
  49. {$i IdCompilerDefines.inc}
  50. uses
  51. Classes,
  52. IdBaseComponent,
  53. IdComponent,
  54. IdException,
  55. IdUserAccounts,
  56. IdGlobal, SysUtils;
  57. const
  58. DEF_MAXCount = 900;
  59. type
  60. TIdOTPUserManager = class;
  61. TIdOTPUserAccounts = class;
  62. TIdOTPPassword = (IdPW_NoEncryption, IdPW_OTP_MD4, IdPW_OTP_MD5, IdPW_OTP_SHA1);
  63. TIdOTPUserAccount = class(TIdUserAccount)
  64. protected
  65. FPasswordType : TIdOTPPassword;
  66. FCurrentCount : LongWord;
  67. FSeed : String;
  68. FAuthenticating : Boolean;
  69. FNoReenter : TCriticalSection;
  70. procedure SetSeed(const AValue : String);
  71. procedure SetPassword(const AValue: String); override;
  72. public
  73. constructor Create(Collection: TCollection); override;
  74. destructor Destroy; override;
  75. function CheckPassword(const APassword: String): Boolean; override;
  76. published
  77. property CurrentCount : LongWord read FCurrentCount write FCurrentCount;
  78. property Seed : String read FSeed write SetSeed;
  79. property PasswordType : TIdOTPPassword read FPasswordType write FPasswordType;
  80. property Authenticating : Boolean read FAuthenticating write FAuthenticating;
  81. end;
  82. TIdOTPUserAccounts = class(TOwnedCollection)
  83. protected
  84. //
  85. function GetAccount(const AIndex: Integer): TIdOTPUserAccount;
  86. function GetByUsername(const AUsername: String): TIdOTPUserAccount;
  87. procedure SetAccount(const AIndex: Integer; AAccountValue: TIdOTPUserAccount);
  88. public
  89. function Add: TIdOTPUserAccount; reintroduce;
  90. constructor Create(AOwner: TIdOTPUserManager); reintroduce;
  91. //
  92. property UserNames[const AUserName: String]: TIdOTPUserAccount read GetByUsername; default;
  93. property Items[const AIndex: Integer]: TIdOTPUserAccount read GetAccount write SetAccount;
  94. end;//TIdOTPUserAccounts
  95. TIdOTPUserManager = class(TIdCustomUserManager)
  96. protected
  97. FMaxCount : LongWord;
  98. FAccounts : TIdOTPUserAccounts;
  99. FDefaultPassword : String;
  100. procedure DoAuthentication(const AUsername: String; var VPassword: String;
  101. var VUserHandle: TIdUserHandle; var VUserAccess: TIdUserAccess); override;
  102. procedure SetMaxCount(const AValue: LongWord);
  103. procedure SetDefaultPassword(const AValue : String);
  104. public
  105. constructor Create(AOwner: TComponent); override;
  106. destructor Destroy; override;
  107. procedure UserDisconnected(const AUser : String); override;
  108. function SendsChallange : Boolean; override;
  109. property Accounts : TIdOTPUserAccounts read FAccounts;
  110. published
  111. property DefaultPassword : String read FDefaultPassword write SetDefaultPassword;
  112. property MaxCount : LongWord read FMaxCount write SetMaxCount default DEF_MAXCount;
  113. end;
  114. EIdOTPException = class(EIdException);
  115. EIdOTPInvalidSeed = class(EIdOTPException);
  116. EIdOTPInvalidCount = class(EIdOTPException);
  117. EIdOTPInvalidPassword = class(EIdOTPException);
  118. function GenerateSeed : String;
  119. implementation
  120. uses
  121. IdOTPCalculator;
  122. resourcestring
  123. RSOTP_Challenge = 'Response to %s required for OTP.';
  124. RSOTP_SeedBadFormat = 'The seed must be alphanumeric and it must at least 1 character but no more than 16 characters.';
  125. RSOTP_InvalidCount = 'The count must be greater than 1.';
  126. RSOTP_InvalidPassword = 'The password must be longer than 10 characters but no more than 63 characters.';
  127. //This must be longer than 9 characters but no more than 63 characters in length
  128. RSOTP_DefaultPassword = 'PleaseChangeMeNow';
  129. const
  130. CharMap = 'abcdefghijklmnopqrstuvwxyz1234567890'; {Do not Localize}
  131. function GetRandomString(NumChar: LongWord): string;
  132. var
  133. i: Integer;
  134. MaxChar: LongWord;
  135. begin
  136. randomize;
  137. MaxChar := Length(CharMap) - 1;
  138. for i := 1 to NumChar do begin
  139. // Add one because CharMap is 1-based
  140. Result := Result + CharMap[Random(maxChar) + 1];
  141. end;
  142. end;
  143. function IsValidPassword(const AValue : String): Boolean;
  144. begin
  145. Result := (Length(AValue) > 9) and (Length(AValue) < 64);
  146. end;
  147. function IsValidSeed(const ASeed : String) : Boolean;
  148. var
  149. i : Integer;
  150. begin
  151. Result := (ASeed <> '') and (Length(ASeed) < 17);
  152. if Result then begin
  153. for i := 1 to Length(ASeed) do begin
  154. if not CharIsInSet(ASeed, i, CharMap) then begin
  155. Result := False;
  156. Break;
  157. end;
  158. end;
  159. end;
  160. end;
  161. function GenerateSeed : String;
  162. begin
  163. Randomize;
  164. Result := GetRandomString(Random(15)+1);
  165. end;
  166. function LowStripWhiteSpace(const AString : String): String;
  167. var
  168. i : Integer;
  169. begin
  170. Result := '';
  171. for i := 1 to Length(AString) do begin
  172. if not (AString[i] in LWS) then begin
  173. Result := Result + LowerCase(AString[i]);
  174. end;
  175. end;
  176. end;
  177. { TIdOTPUserManager }
  178. function TIdOTPUserManager.ChallengeUser(var VIsSafe: Boolean; const AUserName: String): String;
  179. var
  180. LUser : TIdOTPUserAccount;
  181. begin
  182. Result := '';
  183. LUser := FAccounts.UserNames[AUserName];
  184. if (not Assigned(LUser)) or (LUser.PasswordType = IdPW_NoEncryption) then begin
  185. Exit;
  186. end;
  187. VIsSafe := not LUser.Authenticating;
  188. if VIsSafe then begin
  189. //Note that we want to block any attempts to access the server after the challanage
  190. //is given. This is required to prevent a race condition that a hacker can
  191. //exploit.
  192. LUser.FNoReenter.Acquire;
  193. try
  194. LUser.Authenticating := True;
  195. Result := 'otp-'; {Do not translate}
  196. case LUser.PasswordType of
  197. IdPW_OTP_MD4 : Result := Result + 'md4 '; {Do not translate}
  198. IdPW_OTP_MD5 : Result := Result + 'md5 '; {Do not translate}
  199. IdPW_OTP_SHA1 : Result := Result + 'sha1 '; {Do not translate}
  200. end;
  201. Result := Result + IntToStr(LUser.CurrentCount) + ' ' + LUser.Seed;
  202. Result := IndyFormat(RSOTP_Challenge, [Result]);
  203. finally
  204. LUser.FNoReenter.Release;
  205. end;
  206. end;
  207. end;
  208. constructor TIdOTPUserManager.Create(AOwner: TComponent);
  209. begin
  210. inherited Create(AOwner);
  211. FAccounts := TIdOTPUserAccounts.Create(Self);
  212. FMaxCount := DEF_MAXCount;
  213. FDefaultPassword := RSOTP_DefaultPassword;
  214. end;
  215. destructor TIdOTPUserManager.Destroy;
  216. begin
  217. FreeAndNil(FAccounts);
  218. inherited Destroy;;
  219. end;
  220. procedure TIdOTPUserManager.DoAuthentication(const AUsername: String;
  221. var VPassword: String; var VUserHandle: TIdUserHandle; var VUserAccess: TIdUserAccess);
  222. var
  223. LUser: TIdUserAccount;
  224. begin
  225. inherited DoAuthentication(AUsername, VPassword, VUserHandle, VUserAccess);
  226. VUserHandle := IdUserHandleNone;
  227. VUserAccess := IdUserAccessDenied;
  228. LUser := FAccounts[AUsername];
  229. if Assigned(LUser) then begin
  230. if LUser.CheckPassword(VPassword) then begin
  231. VUserHandle := LUser.ID;
  232. VUserAccess := LUser.Access;
  233. end;
  234. end;
  235. end;
  236. procedure TIdOTPUserManager.LoadUserAccounts(const AIniFile: String);
  237. begin
  238. end;
  239. procedure TIdOTPUserManager.SaveUserAccounts(const AIniFile: String);
  240. begin
  241. end;
  242. procedure TIdOTPUserManager.SetDefaultPassword(const AValue: String);
  243. begin
  244. if not IsValidPassword(AValue) then begin
  245. raise EIdOTPInvalidPassword.Create(RSOTP_InvalidPassword);
  246. end;
  247. FDefaultPassword := AValue;
  248. end;
  249. procedure TIdOTPUserManager.SetMaxCount(const AValue: LongWord);
  250. begin
  251. if AValue <= 1 then begin
  252. raise EIdOTPInvalidCount.Create(RSOTP_InvalidCount);
  253. end;
  254. FMaxCount := AValue;
  255. end;
  256. procedure TIdOTPUserManager.UserDisconnected(const AUser: String);
  257. var
  258. LUser : TIdOTPUserAccount;
  259. begin
  260. inherited UserDisconnected(AUser);
  261. LUser := FAccounts.UserNames[AUserName];
  262. if Assigned(LUser) then begin
  263. LUser.Authenticating := False;
  264. end;
  265. end;
  266. { TIdOTPUserAccounts }
  267. function TIdOTPUserAccounts.Add: TIdOTPUserAccount;
  268. begin
  269. Result := inherited Add as TIdOTPUserAccount;
  270. Result.Seed := GenerateSeed;
  271. Result.CurrentCount := TIdOTPUserManager(GetOwner).MaxCount;
  272. Result.Password := TIdOTPUserManager(GetOwner).DefaultPassword;
  273. end;
  274. constructor TIdOTPUserAccounts.Create(AOwner: TIdOTPUserManager);
  275. begin
  276. inherited Create(AOwner, TIdOTPUserAccount);
  277. end;
  278. function TIdOTPUserAccounts.GetAccount(const AIndex: Integer): TIdOTPUserAccount;
  279. begin
  280. Result := TIdOTPUserAccount(inherited Items[AIndex]);
  281. end;
  282. function TIdOTPUserAccounts.GetByUsername(const AUsername: String): TIdOTPUserAccount;
  283. var
  284. i: Integer;
  285. begin
  286. Result := nil;
  287. for i := 0 to Count - 1 do begin
  288. if AUsername = Items[i].UserName then begin
  289. Result := Items[i];
  290. Break;
  291. end;
  292. end;
  293. end;
  294. procedure TIdOTPUserAccounts.SetAccount(const AIndex: Integer; AAccountValue: TIdOTPUserAccount);
  295. begin
  296. inherited SetItem(AIndex, AAccountValue);
  297. end;
  298. { TIdOTPUserAccount }
  299. function TIdOTPUserAccount.CheckPassword(const APassword: String): Boolean;
  300. var
  301. LWordOTP : String;
  302. LHashSum : Int64;
  303. LRecPass : String;
  304. LHexOTP : String;
  305. begin
  306. LHexOTP := '';
  307. LRecPass := APassword;
  308. case FPasswordType of
  309. IdPW_NoEncryption :
  310. begin
  311. LWordOTP := Password;
  312. end;
  313. IdPW_OTP_MD4 :
  314. begin
  315. LRecPass := LowStripWhiteSpace(APassword);
  316. LHashSum := TIdOTPCalculator.GenerateKeyMD4(FSeed, Password, FCurrentCount);
  317. LWordOTP := LowStripWhiteSpace(TIdOTPCalculator.ToSixWordFormat(LHashSum));
  318. LHexOTP := LowStripWhiteSpace(TIdOTPCalculator.ToHex(LHashSum));
  319. end;
  320. IdPW_OTP_MD5 :
  321. begin
  322. LRecPass := LowStripWhiteSpace(APassword);
  323. LHashSum := TIdOTPCalculator.GenerateKeyMD5(FSeed, Password, FCurrentCount);
  324. LWordOTP := LowStripWhiteSpace(TIdOTPCalculator.ToSixWordFormat(LHashSum));
  325. LHexOTP := LowStripWhiteSpace(TIdOTPCalculator.ToHex(LHashSum));
  326. end;
  327. IdPW_OTP_SHA1 :
  328. begin
  329. LRecPass := LowStripWhiteSpace(APassword);
  330. LHashSum := TIdOTPCalculator.GenerateKeySHA1(FSeed, Password, FCurrentCount);
  331. LWordOTP := LowStripWhiteSpace(TIdOTPCalculator.ToSixWordFormat(LHashSum));
  332. LHexOTP := LowStripWhiteSpace(TIdOTPCalculator.ToHex(LHashSum));
  333. end;
  334. end;
  335. Result := (LRecPass = LWordOTP);
  336. if (not Result) and (LHexOTP <> '') then begin
  337. Result := (LRecPass = LHexOTP);
  338. end;
  339. if Result then begin
  340. FNoReenter.Acquire;
  341. try
  342. if CurrentCount = 0 then begin
  343. Seed := GenerateSeed;
  344. end else begin
  345. Dec(FCurrentCount);
  346. end;
  347. Authenticating := False;
  348. finally
  349. FNoReenter.Release;
  350. end;
  351. end;
  352. end;
  353. constructor TIdOTPUserAccount.Create(Collection: TIdCollection);
  354. begin
  355. inherited Create(Collection);
  356. FNoReenter := TCriticalSection.Create;
  357. end;
  358. destructor TIdOTPUserAccount.Destroy;
  359. begin
  360. FreeAndNil(FNoReenter);
  361. inherited Destroy;
  362. end;
  363. procedure TIdOTPUserAccount.SetPassword(const AValue: String);
  364. begin
  365. if not IsValidPassword(AValue) then begin
  366. raise EIdOTPInvalidPassword.Create(RSOTP_InvalidPassword);
  367. end;
  368. inherited SetPassword(AValue);
  369. end;
  370. procedure TIdOTPUserAccount.SetSeed(const AValue: String);
  371. begin
  372. if not IsValidSeed(LowerCase(AValue)) then begin
  373. raise EIdOTPInvalidSeed.Create(RSOTP_SeedBadFormat);
  374. end;
  375. FSeed := LowerCase(AValue);
  376. FCurrentCount := TIdOTPUserManager(TIdOTPUserAccounts(Collection).GetOwner).MaxCount;
  377. end;
  378. function TIdOTPUserAccount.SendsChallange : Boolean;
  379. begin
  380. Result := True;
  381. end;
  382. end.