What protection scheme for my passwords? - delphi

I'm developing a software (for personal use) with Delphi.
But I have a problem, this is it :
-> There's a main password to access a some other password file.
-> When storing these password, i use as key the main password. I think, it's ok.
-> But how protect the main password, and allow modification of it ???
If I use a constant key (so stored in the code, in binary), It can be disassembled !
So, I'm crazy or there's a way to make this possible : Protect main password and derived passwords.
(Main password (choosen by user) -> use it as key when encrypting user data (other password and usernames related).
Thank you for your helps.
Excuse my bad english.

i'd like to suggest turning the problem on its head. Your Windows account is already protected with a password. The Win32 API provides a mechanism where you can have Windows encrypt data with your Windows password.
This means that your data is as secure as your Windows password; and you don't need to memorize a second password.
The Windows function CredWrite and CredRead allow storing and saving of credentials; of which i just happen to have a handy wrapper function already for storing credentials:
function CredWriteGenericCredentials(const Target, Username, Password: WideString): Boolean;
var
PersistType: DWORD;
Credentials: CREDENTIALW;
hr: DWORD;
s: string;
begin
if not CredGetMaxPersistType(CRED_TYPE_GENERIC, {var}PersistType) then
begin
Result := False;
Exit;
end;
ZeroMemory(#Credentials, SizeOf(Credentials));
Credentials.TargetName := PWideChar(Target); //cannot be longer than CRED_MAX_GENERIC_TARGET_NAME_LENGTH (32767) characters. Recommended format "Company_Target"
Credentials.Type_ := CRED_TYPE_GENERIC;
Credentials.UserName := PWideChar(Username);
Credentials.Persist := PersistType; //CRED_PERSIST_ENTERPRISE; //local machine and roaming
Credentials.CredentialBlob := PByte(Password);
Credentials.CredentialBlobSize := 2*(Length(Password)); //By convention no trailing null. Cannot be longer than CRED_MAX_CREDENTIAL_BLOB_SIZE (512) bytes
Credentials.UserName := PWideChar(Username);
Result := CredWriteW(Credentials, 0);
if not Result then
begin
hr := GetLastError;
case hr of
CredUI.ERROR_NO_SUCH_LOGON_SESSION: s := 'The logon session does not exist or there is no credential set associated with this logon session. Network logon sessions do not have an associated credential set. (ERROR_NO_SUCH_LOGON_SESSION)';
CredUI.ERROR_INVALID_PARAMETER: s := 'Certain fields cannot be changed in an existing credential. This error is returned if a field does not match the value in a protected field of the existing credential. (ERROR_INVALID_PARAMETER)';
CredUI.ERROR_INVALID_FLAGS: s := 'A value that is not valid was specified for the Flags parameter. (ERROR_INVALID_FLAGS)';
ERROR_BAD_USERNAME: s := 'The UserName member of the passed in Credential structure is not valid. For a description of valid user name syntax, see the definition of that member. (ERROR_BAD_USERNAME)';
ERROR_NOT_FOUND: s := 'CRED_PRESERVE_CREDENTIAL_BLOB was specified and there is no existing credential by the same TargetName and Type. (ERROR_NOT_FOUND)';
// SCARD_E_NO_READERS_AVAILABLE: raise Exception.Create('The CRED_TYPE_CERTIFICATE credential being written requires the smart card reader to be available. (SCARD_E_NO_READERS_AVAILABLE)');
// SCARD_E_NO_SMARTCARD: raise Exception.Create('A CRED_TYPE_CERTIFICATE credential being written requires the smart card to be inserted. (SCARD_E_NO_SMARTCARD)');
// SCARD_W_REMOVED_CARD: raise Exception.Create('A CRED_TYPE_CERTIFICATE credential being written requires the smart card to be inserted. (SCARD_W_REMOVED_CARD)');
// SCARD_W_WRONG_CHV: raise Exception.Create('The wrong PIN was supplied for the CRED_TYPE_CERTIFICATE credential being written. (SCARD_W_WRONG_CHV)');
else
s := SysErrorMessage(hr)+' (0x'+IntToHex(hr, 8)+')';
end;
OutputDebugString(PChar(s));
end;
end;
And a wrapper function to read credentials:
function CredReadGenericCredentials(const Target: WideString; var Username, Password: WideString): Boolean;
var
Credential: PCREDENTIALW;
begin
Credential := nil;
if CredReadW(Target, CRED_TYPE_GENERIC, 0, Credential) then
begin
try
username := Credential.UserName;
password := WideCharToWideString(PWideChar(Credential.CredentialBlob), Credential.CredentialBlobSize); //By convention blobs that contain strings do not have a trailing NULL.
finally
CredFree(Credential);
end;
Result := True;
end
else
Result := False;
end;
It should be noted that CredRead and CredWrite are themselves functions that turn around and use CryptProtectData and CryptUnprotectData.
These functions let you take some arbitrary blob, and encrypt it with the user account's password1, and then hand you back the encrypted blob. You can then store that blob wherever you like (e.g. registry or file).
Later you can have the blob decrypted, and can only be decrypted by the user who originally encrypted it.
This lets you have your dream of forcing you to deal with another password, but uses Windows to protect it.
"MyPassword04" --> CryptProtectData() --> "TXlQYXNzd29yZDA0"
You can store your encrypted password anywhere you like. Then later:
"TXlQYXNzd29yZDA0" --> CryptUnprotectData() --> "MyPassword04"
The suggestion i'm making is the ability to abandon passwords; leveraging the security of your own account.
Just a suggestion; you're free to consider, and reject, it.
Update
Additional helper functions.
Convert a PWideChar to a WideString (if there's a built-in (Delphi 5) function for it, i've never found it):
function WideCharToWideString(Source: PWideChar; SourceLen: Integer): WideString;
begin
if (SourceLen <= 0) then
begin
Result := '';
Exit;
end;
SetLength(Result, SourceLen div 2);
Move(Source^, Result[1], SourceLen);
end;
There are different "scopes" that you'r allowed to store credentails in:
CRED_PERSIST_NONE: No credential can be stored. This value will be returned if the credential type is not supported or has been disabled by policy.
CRED_PERSIST_SESSION: Only a session-specific credential can be stored.
CRED_PERSIST_LOCAL_MACHINE: Session-specific and computer-specific credentials can be stored.
Windows XP: This credential cannot be stored for sessions in which the profile is not loaded.
CRED_PERSIST_ENTERPRISE: Any credential can be stored.
Windows XP: This credential cannot be stored for sessions in which the profile is not loaded.
This function returns the highest supported persistence type for a given credential type (e.g. "generic" credentails). It's needed when you call CredWrite that you don't try to persist it in a location that isn't supported (i.e. in the domain when there is no domain):
type
TCredGetSessionTypes = function(MaximumPersistCount: DWORD; MaximumPersist: LPDWORD): BOOL; stdcall;
function CredGetMaxPersistType(CredType: DWORD; var MaxCredPersistType: DWORD): Boolean;
const
CRED_TYPE_MAXIMUM = 5;
var
_CredGetSessionTypes: TCredGetSessionTypes;
MaximumPersist: array[0..CRED_TYPE_MAXIMUM-1] of DWORD;
begin
_CredGetSessionTypes := GetProcedureAddress(advapi32, 'CredGetSessionTypes');
if Assigned(_CredGetSessionTypes) then
begin
Result := _CredGetSessionTypes(CRED_TYPE_MAXIMUM, PDWORD(#MaximumPersist[0]));
if Result then
MaxCredPersistType := MaximumPersist[CredType]
else
MaxCredPersistType := 0;
end
else
begin
SetLastError(ERROR_INVALID_FUNCTION);
Result := False;
MaxCredPersistType := 0;
end;
end;
Note: Any code is released into the public domain. No attribution required.

Encode your password file with the main password. Don't store that password anywhere; simply query it before decrypting the password file. If someone enters a wrong password, the password file will be scrambled.

What you could probably do is use a one-way hash for all passwords, without the need for a master password at all.
The nice thing with a hash is that it may be readable by everyone, they aren't any the smarter, since the only way to break a hashed password is a brute-force attack. Which is all the more time consuming that the hash is "large".
Of course, this won't hold if passwords stored are easily discoverable by a dictionary attack, but then is your master password secure?

Check out how password safe http://passwordsafe.sourceforge.net/ solves the problem.
Assemble the list-of-passwords-to-be-encrypted.
Generate two random 128 bit numbers using a secure random generator. Use the first as HMAC key for later authentication and integrity checks. Use the second as AES-CBC key for encrypting the list-of-passwords-to-be-encrypted. Append the HMAC output to the end of the encrypted list.
Generate a third random number. Use this as salt together with the password for deriving a key encryption key using a PBKDF. Use the key encryption key for encrypting the two random keys in step 2.
Optionally, generate a password verifier by hashing your password a sufficiently large number of times.
The final file should have the following layout, formatting omitted
[salt][password verifier][encrypted encryption key][encrypted hmac key][encrypted password list][hmac value]

You may be interested in our new SmartUtils Password SDK: http://sutils.com/index.php/smartutils-password-sdk It allows to store passwords with related info like URLs, usernames etc. in AES-256 encrypted database file. A master password may be encrypted using DPAPI in one line of code.

Related

Check if user authentication in Active DIrectory

i would like to know whether a user inputs the correct combination of Domain, User and Password for his Active Directory user.
I tried to make a very simple program that is not able to connect but by reading the error message i can know if the user/password is correct.
This is trick based (the logic is on reading the Exception message), anyway i testd this prototype on 2 servers and i noticed that the excpetion messages change from server to server so this is not reliable.
uses adshlp, ActiveDs_TLB;
// 3 TEdit and a TButton
procedure TForm4.Button1Click(Sender: TObject);
Var
aUser : IAdsUser;
pDomain, pUser, pPassword : string;
myResult : HRESULT;
Counter: integer;
begin
pDomain := edtDomain.Text;
pUser:= edtUser.Text;
pPassword := edtPwd.Text;
Counter := GetTickCount;
Try
myResult := ADsOpenObject(Format('LDAP://%s',[pDomain]),Format('%s\%s',[pDomain,pUser]),pPassword,
ADS_READONLY_SERVER,
IAdsUser,aUser);
except
On E : EOleException do
begin
if (GetTickCount - Counter > 3000) then ShowMessage ('Problem with connection') else
if Pos('password',E.Message) > 0 then ShowMessage ('wrong username or password') else
if Pos('server',E.Message) > 0 then ShowMessage ('Connected') else
ShowMessage('Unhandled case');
memLog.Lines.Add(E.Message);
end;
end
end;
The reason why i set "Connected" if the message contain "server" is that on my
local machine (on my company ldap server in fact) in case all is fine (domain, user and password) the server replies "The server requires a safer authentication", so the "server" word is in there, while in other cases it says "wrong user or password". SInce this must work on itlian and english servers i set "server" and "pasword" as reliable words. Anyway i tested on another server that gives differente errors.
I started from a reply to this question to do the above.
How can i check if the user set the correct password or not in a more reliable way using a similar technique?
UPDATE (found solution)
Thanks to the replies i managed to write this function that does what i need. It seems quite reliable up to now, I write here to share, hoping it can help others:
// This function returns True if the provided parameters are correct
// login credentials for a user in the specified Domain
// From empirical tests it seems reliable
function UserCanLogin(aDomain, aUser, aPassword: string): Boolean;
var
hToken: THandle;
begin
Result := False;
if (LogonUser(pChar(aUser), pChar(aDomain), pChar(aPassword), LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT, hToken)) then
begin
CloseHandle(hToken);
Result := True;
end;
end;
You need to check the error code that ADsOpenObject returns. do not base your error checking on the returned exception messages.
if the function succeeded it will return S_OK, otherwise you need to refer to ADSI Error Codes, specifically, the LDAP error codes for ADSI
When an LDAP server generates an error and passes the error to the
client, the error is then translated into a string by the LDAP client.
This method is similar to Win32 error codes for ADSI. In this example,
the client error code is the WIN32 error 0x80072020.
To Determine the LDAP error codes for ADSI
Drop the 8007 from the WIN32 error code. In the example, the remaining hex value is 2020.
Convert the remaining hex value to a decimal value. In the example, the remaining hex value 2020 converts to the decimal value
8224.
Search in the WinError.h file for the definition of the decimal value. In the example, 8224L corresponds to the error
ERROR_DS_OPERATIONS_ERROR.
Replace the prefix ERROR_DS with LDAP_. In the example, the new definition is LDAP_OPERATIONS_ERROR.
Search in the Winldap.h file for the value of the LDAP error definition. In the example, the value of LDAP_OPERATIONS_ERROR in
the Winldap.h file is 0x01.
For a 0x8007052e result (0x052e = 1326) for example you will get ERROR_LOGON_FAILURE
From your comment:
Since the function always raises an exception i am not able to read
the code
You are getting an EOleException because your ADsOpenObject function is defined with safecall calling convention. while other implementations might be using stdcall. when using safecall Delphi will raise an EOleException and the HResult will be reflected in the EOleException.ErrorCode, otherwise (stdcall) will not raise an exception and the HResult will be returned by the ADsOpenObject function.
i would like to know whether a user inputs the correct combination of
Domain, User and Password for his Active Directory user.
You can use LogonUser function to validate user login e.g. :
if (LogonUser(pChar(_Username), pChar(_ADServer), pChar(_Password), LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT, hToken)) then
begin
CloseHandle(hToken);
//...
//DoSomething
end
else raise Exception.Create(SysErrorMessage(GetLastError));
Note: You must use LogonUser within a domain machine to be able to use the domain login or the function will always return The user name or password is incorrect
The alternative is using TLDAPSend e.g. :
function _IsAuthenticated(const lpszUsername, lpszDomain, lpszPassword: string): Boolean;
var
LDAP : TLDAPSend;
begin
Result := False;
if ( (Length(lpszUsername) = 0) or (Length(lpszPassword) = 0) )then Exit;
LDAP := TLDAPSend.Create;
try
LDAP.TargetHost := lpszDomain;
LDAP.TargetPort := '389';
....
LDAP.UserName := lpszUsername + #64 + lpszDomain;;
LDAP.Password := lpszPassword;
Result := LDAP.Login;
finally
LDAP.Free;
end;
end;
How can i check if the user set the correct password or not in a more
reliable way using a similar technique?
Try to use FormatMessage function
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_FROM_SYSTEM,
nil,
myResult,
LANG_ENGLISH or SUBLANG_ENGLISH_US,
lpMsg,
0,
nil);
MessageBox(0, lpMsg, 'Msg', 0);
I get (HRESULT) 0x8007052e (2147943726) "unknown user name or bad password" when I use a wrong password. And there is no EOleException, try:
hr := ADsOpenObject('LDAP://'+ ADomain + '/OU=Domain Controllers,' + APath,
AUser, APwd,
ADS_SECURE_AUTHENTICATION or ADS_READONLY_SERVER,
IID_IADs, pObject);
if (hr=HRESULT(2147943726)) then ShowMessage ('wrong username or password')

In a textfile, How to express if ( Line 1's answer = Line 2's answer ) then

I just started Delphi.
I'm making a simple login form, and the text file (UserInfo.txt) is set up like this
I just want Delphi to run through the TextFile to look for the string, then if that string equals edtUseranme.Text then it must check if that string is equal to the string on the second line.
If it equals then it can continue to the next Form
Here is my code:
for I := 0 to Eof(tFile) do
begin
Readln(tFile, sLine);
Inc(I);
if ReadLn(tFile) = edtUsername.Text then
begin
if edtUsername.Text = then
begin
frmMain.Show;
frmLogin.Hide;
end
else
begin
ShowMessage('INPUT INVALID (Try again)');
end;
end;
end;
At that second if statement, I don't know what to put after =.
Like Ken White suggested, you could redesign your text file and then use a TStringList to parse it. That would certainly make it easier to work with the data. However, if you want to stick with the code you already have, then you could do something more like this instead:
var
tFile: TextFile;
//Declare variables for temporally storing read username and password
sUser, sPass, sDivider: string;
Valid: Boolean;
...
Valid := False;
//use while loop to read every line till the end of file
while not Eof(tFile) do
begin
//Read username into sUser variable
Readln(tFile, sUser);
//Read password into sPass variable
Readln(tFile, sPass);
//Read divider into sDivider variable
Readln(tFile, sDivider);
//Use if clause to perform multiconditional logical comparison to see if
//read username and password equals to those specified in suitable edit
//boxes
//NOTE: Each separate logical condition must be in its own brackets
if (sUser = edtUsername.Text) and (sPass = edtPassword.Text) then
begin
//If conditions are met we set local variable Valid to true
Valid := True;
//and then break the loop
Break;
end;
end;
//If local variable valid was set to True show the main form
if Valid then
begin
frmMain.Show;
frmLogin.Hide;
end else
//else we show message about invalid input entered
ShowMessage('INPUT INVALID (Try again)');
Your file format is poorly designed for what you're wanting to do, I'm afraid.
You'd be better off using a more standard format (realizing, of course, that it's totally insecure to store names and passwords as plain text). Delphi makes it easy to work with name/value pairs using TStringList with text like this:
Name=Value
This would mean your text file would contain the username=password type lines:
Bob=Password
Sue=Bambi
You then use TStringList to load the file and read the value associated with the name, like this:
var
SL: TStringList;
UserName, Password: string;
begin
UserName := Edit1.Text; // Your username control
Password := Edit2.Text;
// You should check to make sure that both the username and password
// were entered here before proceeding. I'll leave that to you to do.
SL := TStringList.Create;
try
SL.LoadFromFile('C:\Temp\UserInfo.txt'); // Replace with your filename
// The next line looks for a name in the stringlist equal to the
// entered username, and if it's found compares its value to the
// password that was provided.
//
// You'll need to handle making sure that you do a case-sensitive
// (or non-sensitive, depending on your needs) comparison is done
// if needed.
if SL.Values[UserName] = Password then
// Login succeeded
else
// Login failed.
finally
SL.Free;
end;
end;

Encryption of a password

i've managed to do a simple encryption of a password entered using the following code which then displays the encrypted password in a labels caption,
procedure TfrmLogin.edtAddPasswordClick(Sender: TObject);
var
NormalPassword, EncryptedPassword: string;
PasswordChar: Char;
EncryptedCharValue: string;
CharPtr: Integer;
Ptr, n: Integer;
begin
NormalPassword := Edit1.text;
EncryptedPassword := '';
for CharPtr := 1 to Length(NormalPassword) do
begin
PasswordChar := NormalPassword[CharPtr];
EncryptedCharValue := IntToStr (Ord(PasswordChar) * 5 + 14);
EncryptedPassword := EncryptedPassword + EncryptedCharValue;
Label1.Caption := EncryptedPassword;
end;
end;
The problem is that i would like to convert the encrypted password displayed in label1.caption back into its original form on the click of another button and i can't work out how this could be done. any suggestions?
Instead of create your own algorithm to hash( or encrypt) a password, try using a well tested and reliable algorithm like SHA1, MD5, and so on.
Back to your question to convert the encrypted value to the original, all you must do is reverse your algorithm , try this sample.
var
NormalPassword, EncryptedPassword: String;
PasswordChar : char;
EncryptedCharValue : String;
CharPtr : Integer;
begin
NormalPassword :='';
EncryptedPassword := Label1.Caption; //here is stored the encrypted password
CharPtr := 1;
while CharPtr< length(EncryptedPassword) do
Begin
EncryptedCharValue:=Copy(EncryptedPassword, CharPtr, 3);
Inc(CharPtr, 3);
PasswordChar := Chr((StrToint(EncryptedCharValue)-14) div 5);
NormalPassword :=NormalPassword+ PasswordChar;
end;
Label2.Caption := NormalPassword;
end;
I know this is for homework and the idea is to get the reversing code, and others are providing you too much detail for that purpose, but I need to give this as an answer because its concept is too important to say in a note:
If you are truly talking about a password, then you must not make the password reversable. Users expect their passwords to be safe and secure and non-reversable.
If the reason why you want to do this is because you want to send them their password if they forget it, then the answer is don't.
When a person loses or forgets their password, you should not provide it back to them, because that proves it is insecure. Instead, the proper thing to do is, after ensuring they are the user who signed up (via email address or other means), then allow them to enter a new password of their choice.
If you're determined to do it the way you've described, you could make it reversible by changing the line:
EncryptedCharValue := IntToStr (Ord(PasswordChar) * 5 + 14);
to
EncryptedCharValue := format('%.4d', [Ord(PasswordChar) * 5 + 14]);
That would allow you later to pull the string apart in four-character chunks, subtract 14, divide by 5, and turn it back into a character.
I stand by my earlier comment though - if you actually have a use-case that requires reversible security, use a stronger algorithm, for example as discussed in this question.
[Edit: four chars is clearly more robust]
You function is a very simple hashing algorithm that cannot be reversed. It does not make sense to store passwords in a way that can be reversed, since it does not add any additional layer of security (except you use a complicated scheme based on asymmetric crypto)
simple hash algorithm :
How do I hash a string with Delphi?
hash with secret key algorithm(CRAM,HMac):
HMAC-SHA256 in Delphi

How can my program detect whether it's running on a particular domain?

I have the need to restrict specific functions of an application based on the location of the currently logged in user. As I have to implement this logic in Delphi, I'd prefer not to go overboard with full active directory/LDAP queries.
My curent thought is to utilize DsGetDcName, and use the GUID returned in the DOMAIN_CONTROLLER_INFO structure and compare it to a hard coded constant. It seems to reason that a domain GUID would only change if the domain is recreated, so this would provide functionality that I desire with limited overhead. My only concern is that I can't find any documentation on MSDN confirming my assumption.
type
EAccessDenied = Exception;
EInvalidOwner = Exception;
EInsufficientBuffer = Exception;
ELibraryNotFound = Exception;
NET_API_STATUS = Integer;
TDomainControllerInfoA = record
DomainControllerName: LPSTR;
DomainControllerAddress: LPSTR;
DomainControllerAddressType: ULONG;
DomainGuid: TGUID;
DomainName: LPSTR;
DnsForestName: LPSTR;
Flags: ULONG;
DcSiteName: LPSTR;
ClientSiteName: LPSTR;
end;
PDomainControllerInfoA = ^TDomainControllerInfoA;
const
NERR_Success = 0;
procedure NetCheck(ErrCode: NET_API_STATUS);
begin
if ErrCode <> NERR_Success then
begin
case ErrCode of
ERROR_ACCESS_DENIED:
raise EAccessDenied.Create('Access is Denied');
ERROR_INVALID_OWNER:
raise EInvalidOwner.Create('Cannot assign the owner of this object.');
ERROR_INSUFFICIENT_BUFFER:
raise EInsufficientBuffer.Create('Buffer passed was too small');
else
raise Exception.Create('Error Code: ' + IntToStr(ErrCode) + #13 +
SysErrorMessage(ErrCode));
end;
end;
end;
function IsInternalDomain: Boolean;
var
NTNetDsGetDcName: function(ComputerName, DomainName: PChar; DomainGuid: PGUID; SiteName: PChar; Flags: ULONG; var DomainControllerInfo: PDomainControllerInfoA): NET_API_STATUS; stdcall;
NTNetApiBufferFree: function (lpBuffer: Pointer): NET_API_STATUS; stdcall;
LibHandle: THandle;
DomainControllerInfo: PDomainControllerInfoA;
ErrMode: Word;
const
NTlib = 'NETAPI32.DLL';
DS_IS_FLAT_NAME = $00010000;
DS_RETURN_DNS_NAME = $40000000;
INTERNAL_DOMAIN_GUID: TGUID = '{????????-????-????-????-????????????}';
begin
if Win32Platform = VER_PLATFORM_WIN32_NT then
begin
ErrMode := SetErrorMode(SEM_NOOPENFILEERRORBOX);
LibHandle := LoadLibrary(NTlib);
SetErrorMode(ErrMode);
if LibHandle = 0 then
raise ELibraryNotFound.Create('Unable to map library: ' + NTlib);
try
#NTNetDsGetDcName := GetProcAddress(Libhandle, 'DsGetDcNameA');
#NTNetApiBufferFree := GetProcAddress(Libhandle,'NetApiBufferFree');
try
NetCheck(NTNetDsGetDcName(nil, nil, nil, nil, DS_IS_FLAT_NAME or DS_RETURN_DNS_NAME, DomainControllerInfo));
Result := (DomainControllerInfo.DomainName = 'foo.com') and (CompareMem(#DomainControllerInfo.DomainGuid,#INTERNAL_DOMAIN_GUID, SizeOf(TGuid)));//WideCharToString(pDomain);
finally
NetCheck(NTNetApiBufferFree(DomainControllerInfo));
end;
finally
FreeLibrary(LibHandle);
end;
end
else
Result := False;
end;
Added a related question on ServerFault as suggested.
Found another interesting read on Technet which also seems to hint at me being right, but isn't specifically scoped at domain SID's.
Create a service account on the domain;
Get the GUID of the service account and encrypt it and save it somewhere (registry) maybe as part of enterprise install process to validate a license agreement.
On startup of the client app query for the Domain Service Account GUID and validate it with the saved GUID.
Or create your own enterprise 'key' server.
Doing an LDAP query is easier than doing all the domain controller crap.
If I correct understand your requirement the best API in your case is GetUserNameEx. You can choose the value of NameFormat parameter of the type EXTENDED_NAME_FORMAT which you can better verify. Another function GetComputerNameEx is helpful if you want additionally verify the information about the computer where the program is running.
I have the need to restrict specific
functions of an application based on
the location of the currently logged
in user
If you are trying to find out the location of the currently logged in user, you shouldn't be using DsGetDcName.
Your computer can be joined to domainA. Your logon user can be from domainB. Calling DsGetDcName on your computer doesn't give you domainB GUID but it will give you domainA GUID
Therefore, I think you should use LookupAccountName instead. The LookupAccountName gives you the currently logged in user's SID. Then, you can extract the domain SID from the user SID. That domain SID is really the domain where this user coming from. For the details of how to extract a domain SID from a user SID, please check here
Regarding to your original question about the uniqueness of the domain GUID, I am sorry that I don't have answer on it. AFAIK, there is no tool available allowing you to change the domain SID nor the GUID. I am not sure how hard to hack into it and change it.

Checking for Administrator user login in non-English installations of windows

I have some small questions...I have a program that stores a list of users in a database and compares on program startup if the user is in the list or is an administrator before letting them use it. At the moment, the way I'm using to check if the user is an administrator is simply by comparing the username to a string constant called 'ADMINISTRATOR'. Will this work on a non-Engish system? I.E. does Windows use a language specific version of 'administrator'? Or maybe is there an enumerated version of the Admin user that I can use to check with instead of my 'ADMINISTRATOR' string? (you know, just like how Windows folders are enumerated). I'm using Delphi 2009 by the way. Thanks in advance!
No, don't do it that way. It will surely break. You could get a list of all the groups the user is a member of and check if one of the SIDs is S-1-5-32-544, which is the SID of the Administrators group. There is a list of well known SIDs. There is also an SID for the original administrator account.
Here is the list:
http://support.microsoft.com/kb/243330
NEWS
In 2010, #ChristianWimmer criticized my coding style. Now, two years afterward, I have to use the function again in my program. So, I decided to improve the coding style of the function.
Overview
I pick out a small portion of my private library for your convenience. To test whether user account of the access token is a member of the local administrator's group, pass WinBuiltinAdministratorsSid of JwaWinNT to eWellKnownSidType parameter. Note, it requires JEDI API Libray because Delphi Windows.pas unit didn't define CreateWellKnownSid().
Implementation
//------------------------------------------------------------------------------
// Purpose: Tests whether user account of the access token is a member of the
// specified well known group, and report its elevation type.
// Parameter:
// hToken [in,opt]
// A handle to an access token having TOKEN_QUERY and TOKEN_DUPLICATE
// access. If hToken is 0: if it is an impersonation token, the access token
// of the calling thread is used; otherwise, the access token associated
// with the process is used.
// eWellKnownSidType [in]
// Member of the WELL_KNOWN_SID_TYPE enumeration that specifies what Sid the
// function will identify.
// pDomainSid [in,opt]
// A pointer to a SID that identifies the domain to use when identifying the
// Sid. Pass nil to use the local computer.
// peElevType [out,opt]
// A pointer to a variable that receives the following elevation type of the
// access token:
// - TokenElevationTypeDefault: The access token does not have a linked
// token. This value is reported under Windows prior to Windows Vista.
// - TokenElevationTypeFull: The access token is an elevated token.
// - TokenElevationTypeLimited: The access token is a limited token.
// Return value:
// - True if user account of the access token is a member of the well known
// group specified in eWellKnownSidType parameter.
// - False, otherwise. To get error information, call GetLastError().
// Remarks:
// To test whether user account of the access token is a member of local
// administrators group, pass JwaWinNT.WinBuiltinAdministratorsSid to
// eWellKnownSidType parameter.
// References:
// - How To Determine Whether a Thread Is Running in User Context of
// Local Administrator Account [MSDN]
//------------------------------------------------------------------------------
function Inu_IsMemberOfWellKnownGroup(const hToken: Windows.THandle;
const eWellKnownSidType: JwaWinNT.WELL_KNOWN_SID_TYPE;
const pDomainSid: JwaWinNT.PSID=nil;
peElevType: PTokenElevationType=nil): Boolean;
var
hAccessToken: Windows.THandle;
rOSVerInfo: Windows.OSVERSIONINFO;
eTET: Windows.TTokenElevationType;
iReturnLen: Windows.DWORD;
hTokenToCheck: Windows.THandle;
iSidLen: Windows.DWORD;
pGroupSid: JwaWinNT.PSID;
bMemberOfWellKnownGroup: Windows.BOOL;
begin
Result := False;
hAccessToken := 0;
hTokenToCheck := 0;
pGroupSid := nil;
try
if hToken = 0 then begin // If the caller doesn't supply a token handle,
// Get the calling thread's access token
if not Windows.OpenThreadToken(Windows.GetCurrentThread(),
Windows.TOKEN_QUERY or Windows.TOKEN_DUPLICATE,
True, hAccessToken) then begin
if Windows.GetLastError() <> Windows.ERROR_NO_TOKEN then
Exit();
// If no thread token exists, retry against process token
if not Windows.OpenProcessToken(Windows.GetCurrentProcess(),
Windows.TOKEN_QUERY or Windows.TOKEN_DUPLICATE, hAccessToken) then
Exit();
end;
end
else // If the caller supplies a token handle,
hAccessToken := hToken;
// Determine whether the system is running Windows Vista or later because
// because they support linked tokens, previous versions don't.
rOSVerInfo.dwOSVersionInfoSize := SizeOf(Windows.OSVERSIONINFO);
if not Windows.GetVersionEx(rOSVerInfo) then
Exit();
if rOSVerInfo.dwMajorVersion >= 6 then begin
// Retrieve information about the elevation level of the access token
if not Windows.GetTokenInformation(hAccessToken,
Windows.TokenElevationType, #eTET,
SizeOf(Windows.TTokenElevationType), iReturnLen) then
Exit();
// If the access token is a limited token, retrieve the linked token
// information from the access token.
if eTET = Windows.TokenElevationTypeLimited then begin
if not Windows.GetTokenInformation(hAccessToken,
Windows.TokenLinkedToken, #hTokenToCheck,
SizeOf(Windows.TTokenLinkedToken), iReturnLen) then
Exit();
end;
// Report the elevation type if it is wanted
if Assigned(peElevType) then
peElevType^ := eTET;
end
else begin // if rOSVerInfo.dwMajorVersion < 6
// There is no concept of elevation prior to Windows Vista
if Assigned(peElevType) then
peElevType^ := Windows.TokenElevationTypeDefault;
end;
// CheckTokenMembership() requires an impersonation token. If we just got a
// linked token, it is already an impersonation token. Otherwise, duplicate
// the original as an impersonation token for CheckTokenMembership().
if (hTokenToCheck = 0) and (not Windows.DuplicateToken(hAccessToken,
Windows.SecurityIdentification, #hTokenToCheck)) then
Exit();
// Allocate enough memory for the longest possible Sid
iSidLen := JwaWinNT.SECURITY_MAX_SID_SIZE;
pGroupSid := JwaWinNT.PSid(Windows.LocalAlloc(Windows.LMEM_FIXED, iSidLen));
if not Assigned(pGroupSid) then
Exit();
// Create a Sid for the predefined alias as specified in eWellKnownSidType
if not JwaWinBase.CreateWellKnownSid(eWellKnownSidType, pDomainSid,
pGroupSid, iSidLen) then
Exit();
// Now, check presence of the created Sid in the user and group Sids of the
// access token. In other words, it determines whether the user is a member
// of the well known group specified in eWellKnownSidType parameter.
if not JwaWinBase.CheckTokenMembership(hTokenToCheck, pGroupSid,
bMemberOfWellKnownGroup) then
Exit();
Result := bMemberOfWellKnownGroup;
finally
// Close the access token handle
if hAccessToken <> 0 then
Windows.CloseHandle(hAccessToken);
// Close the new duplicate token handle if exists
if (hTokenToCheck <> 0) then
Windows.CloseHandle(hTokenToCheck);
// Free the allocated memory for the Sid created by CreateWellKnownSid()
if Assigned(pGroupSid) then
Windows.LocalFree(Windows.HLOCAL(pGroupSid));
end;
end; // endfunction Inu_IsMemberOfWellKnownGroup
//==============================================================================
This is an excerpt from JwsclToken.pas from the JEDI API&WSCL.
Both functions do the same check but in different ways. You see how little code is used? The same code in plain WinAPI would be at least 5 times bigger.
Of course you can just call these functions from the unit itself. No need to copy here!
function JwCheckAdministratorAccess: boolean;
var
SD: TJwSecurityDescriptor;
begin
if not Assigned(JwAdministratorsSID) then
JwInitWellKnownSIDs;
SD := TJwSecurityDescriptor.Create;
try
SD.PrimaryGroup := JwNullSID;
SD.Owner := JwAdministratorsSID;
SD.OwnDACL := True;
SD.DACL.Add(TJwDiscretionaryAccessControlEntryAllow.Create(nil,
[], STANDARD_RIGHTS_ALL, JwAdministratorsSID, False));
Result := TJwSecureGeneralObject.AccessCheck(SD, nil,
STANDARD_RIGHTS_ALL, TJwSecurityGenericMapping);
finally
FreeAndNil(SD);
end;
end;
function JwIsMemberOfAdministratorsGroup: boolean;
var
Token: TJwSecurityToken;
begin
Token := TJwSecurityToken.CreateTokenEffective(TOKEN_READ or
TOKEN_DUPLICATE);
try
Token.ConvertToImpersonatedToken(SecurityImpersonation, MAXIMUM_ALLOWED);
Result := Token.CheckTokenMembership(JwAdministratorsSID)
finally
FreeAndNil(Token);
end;
end;
It varies from windows version to windows version... in pre-vista... administrator username is in the primary windows language... for example, in spanish it is Administrador.
In post-vista, there's no administrator user. You shall store and check for user privileges.
I found this IsAdmin function and you may find it useful too...
There is the CreateWellKnownSid function.
But explicit check for admin account may be not a good idea. Just do the operation and ask for elevation, if you got 'access denied' error.

Resources