|
Submitted 26 February 2005
When implementing an FTP server, it is important to keep the user accounts information secured. In most Delphi FTP Server components information about the user passwords comes as a clean text. The task of the encoding and encrypting of the password information should be solved by the end-user when implementing the FTP server application. This article demonstrated a sample of storing passwords as MD5 hash values.
As an example, let's take the TclFtpServer component from the Clever Internet Suite v 5.0 library. Also you can apply this approach to the TIdFTPServer component from Indy9 or Indy10 by http://www.indyproject.org/ . In case of using Indy10 the task of validating FTP passwords is simplified since this version of the TIdFTPServer supports the MD5 (and also MD4 and SHA1) OTP (One Time Password) authentication method.
The password hash can be calculated by using the standard Microsoft Crypto Api library. The sample code which implements the MD5 hashing algorithm can be seen below:
function GetPasswordHash(const APassword: string): string; var context: HCRYPTPROV; hash: HCRYPTHASH; data: PByte; hashSize, dwordSize: DWORD; begin if not CryptAcquireContext(@context, nil, nil, PROV_RSA_SCHANNEL, 0) then begin if not CryptAcquireContext(@context, nil, nil, PROV_RSA_SCHANNEL, CRYPT_NEWKEYSET) then raise Exception.CreateFmt('CryptAcquireContext error %d', [GetLastError()]); end; try if not CryptCreateHash(context, CALG_MD5, 0, 0, @hash) then raise Exception.CreateFmt('CryptCreateHash error %d', [GetLastError()]); if not CryptHashData(hash, Pointer(APassword), Length(APassword), 0) then raise Exception.CreateFmt('CryptHashData error %d', [GetLastError()]); data := nil; try dwordSize := SizeOf(DWORD); if not CryptGetHashParam(hash, HP_HASHSIZE, @hashSize, @dwordSize, 0) then raise Exception.CreateFmt('CryptGetHashParam error %d', [GetLastError()]); GetMem(data, hashSize); if not CryptGetHashParam(hash, HP_HASHVAL, data, @hashSize, 0) then raise Exception.CreateFmt('CryptGetHashParam error %d', [GetLastError()]); SetLength(Result, hashSize); system.Move(data^, Pointer(Result)^, hashSize); finally FreeMem(data); CryptDestroyHash(hash); end; finally CryptReleaseContext(context, 0); end; end; |
You can choose the used hash algorithm specifying the corresponding parameter for the CryptCreateHash CryptoApi function. The number of supported algorithms depends on the cryptographic provider being used. You can learn more about cryptographic providers and supported algorithms from the MSDN Microsoft Knowledge base.
Let's go ahead and consider a task of modifying user accounts at the FTP server side.

Using the Edit Box above you can create new users and also modify existing ones.
Please note, we cannot see the passwords for the users which have been created previously since their password information is represented by the cryptographic hash. This approach is implemented in most professional systems including MS Windows User accounts.
The code-behind for this dialog looks trivial except for two methods: loading and storing the account details.
Since we cannot see passwords for existing users, we must keep all password hashes, which have been entered previously:
procedure TfrmUsers.LoadData(Accounts: TclUserAccounts); var i: Integer; begin FAccounts.Assign(Accounts); for i := 0 to FAccounts.Count - 1 do begin FAccounts[i].Password := ''; end; FillListbox(0); end; |
This method copies the user accounts to the temporary accounts collection in order to edit it with the Edit Box. We must clean the password values for all users from temporary accounts collection for separating users which are already exist from newly created users.
procedure TfrmUsers.StoreData(Accounts: TclUserAccounts); var i: Integer; item: TclUserAccount; psw: string; begin for i := Accounts.Count - 1 downto 0 do begin if (FAccounts.AccountByUserName(Accounts[i].UserName) = nil) then begin Accounts.Delete(i); end; end; for i := 0 to FAccounts.Count - 1 do begin item := Accounts.AccountByUserName(FAccounts[i].UserName); if (item = nil) then begin item := Accounts.Add(); item.Assign(FAccounts[i]); psw := GetPasswordHash(item.Password); end else begin psw := item.Password; item.Assign(FAccounts[i]); if (item.Password <> '') then begin psw := GetPasswordHash(item.Password); end; end; item.Password := psw; end; end; |
This method synchronizes the user accounts from the temporary FAccounts collection with the ones available on the FTP server. We cannot call the Assign method for the entire collection. In such case we can lose the password hashes calculated during the previous session of editing FTP accounts.
When a FTP client is about to connect to the FTP server, it provides both the user name and the password information by using the 'USER' and 'PASS' commands correspondingly. In case of using the OTP approach for sending-receiving password values, the password value comes from a client as a calculated MD4, MD5 or SHA1 hash value. In such a case we do not need to do anything for preparing the retrieved password before validating it. But in case of sending passwords as a clean text, it is necessary to apply the same hash algorithm as the one used for storing the user accounts collection.
In case of using the TclFtpServer from Clever Internet Suite v 5.0, the default password validation algorithm can be overridden using the OnAuthenticate component event. The event handler for the OnAuthenticate component event looks like the following:
procedure TMainForm.clFtpServer1Authenticate(Sender: TObject; Account: TclUserAccount; const APassword: String; var IsAuthorized, Handled: Boolean); begin IsAuthorized := (GetPasswordHash(APassword) = Account.Password); Handled := True; end; |
A full source code of all classes described in this article can be downloaded at FtpServer.zip
This code is constantly refined and improved and your comments and suggestions are always welcome.
Please write us at info@clevercomponents.com
With best regards, Sergey Shirokov Clever Components team. www.clevercomponents.com
|