RAD Studio Hide user data using impersonation

ADSoft

Местный
Регистрация
1 Окт 2007
Сообщения
223
Реакции
86
Credits
1,098
This article is based on a question seen on StackOverflow: File permission to a specific application.
(Для просмотра ссылки Войди или Зарегистрируйся).

In that question a developer ask how he can hide a data file so that the user cannot see it and yet have his application access the file.

A developer noted in a comment “If you don't want the configuration file to be readable you should encrypt the data and decrypt it as part of your program “. Another one said: “Windows security is user-based, not application-based. If the file is accessible to a given user, and that same user runs both your app and Notepad, then both apps will have access to the file “.

Sure, encryption is a valid solution to the problem. There are a lot of encryption/decryption libraries available for Delphi, including free one such as Delphi Encryption Compendium available for free on GitHub (Для просмотра ссылки Войди или Зарегистрируйся).

I wanted to develop an alternate and probably easier method to access a data file from an application and yet prohibit the user access the file with another application and even prevent the user to see the file exists!

The key to this solution is to make the application logon as another user and store the file in that other user profile.

Sound complex? Maybe but actually, it is very simple. There are only two system calls required:
  • LogonUser to provide credential for that user authentication (Username, domain and password) and get a handle.
  • ImpersonateLoggedOnUser to ask Windows to momentarily forget about the current user and act as the newly logged on user.
Later, to return to the normal situation, that is real logged on user, two other calls are required:
  • RevertToSelf
  • CloseHandle.
I have encapsulated the above sequences in a simple class with two methods: Logon and Logoff.
Код:
type
    TImpersonateUser = class(TComponent)
    protected
        FUserToken : THandle;
        FErrorCode : DWORD;
    public
        destructor Destroy; override;
        function  Logon(const UserName : String;
                        const Domain   : String;
                        const Password : String) : Boolean;
        procedure Logoff();
        property ErrorCode : DWORD read FErrorCode;
    end;
The implementation is straightforward:
Код:
destructor TImpersonateUser.Destroy;
begin
    if FUserToken <> 0 then begin
        CloseHandle(FUserToken);
        FUserToken := 0;
    end;
    inherited Destroy;
end;

procedure TImpersonateUser.Logoff;
begin
    if FUserToken <> 0 then begin
        RevertToSelf();   // Revert to our user
        CloseHandle(FUserToken);
        FUserToken := 0;
    end;
end;

function TImpersonateUser.Logon(
    const UserName : String;
    const Domain   : String;
    const Password : String): Boolean;
var
    LoggedOn : Boolean;
begin
    Result := FALSE;
    if FUserToken <> 0 then
        Logoff();

    if UserName = '' then begin // Must at least provide a user name
        FErrorCode := ERROR_BAD_ARGUMENTS;
        Exit;
    end;

    if Domain <> '' then
        LoggedOn := LogonUser(PChar(UserName),
                              PChar(Domain),
                              PChar(Password),
                              LOGON32_LOGON_INTERACTIVE,
                              LOGON32_PROVIDER_DEFAULT,
                              FUserToken)
    else
        LoggedOn := LogonUser(PChar(UserName),
                              PChar(Domain),
                              PChar(Password),
                              LOGON32_LOGON_NEW_CREDENTIALS,
                              LOGON32_PROVIDER_WINNT50,
                              FUserToken);
    if not LoggedOn then begin
        FErrorCode := GetLastError();
        Exit;
    end;

    if not ImpersonateLoggedOnUser(FUserToken) then begin
        FErrorCode := GetLastError();
        Exit;
    end;

    FErrorCode := ERROR_SUCCESS;
    Result     := TRUE;
end;
The sequences of operations in TImpersonateUser are limited to the thread issuing the calls. You may have – for example – the main thread acting as the current user and simultaneously have another thread acting as another user account of your choice. Of course, you need the credentials for that user account.

The use case for the developer who posted the question I mentioned in the beginning is to create a new Windows account for the sole purpose of storing the configuration files for all other users. The configuration files could be stored in a subdirectory of the Documents folder. One subdirectory for each other user using the application.

To further show how simple it is, I created a new TStream derived class that can directly access a file in another user account. I named this class TImpersonateFileStream. It derives from TFileStream, overriding the constructor to first impersonate the user (logon), then call the inherited TFileStream constructor and then immediately revert back (logoff) to the current Windows User. The stream remains opened with the access right of the given user event after logoff because the permissions are defined by the file handle when opened and persists during the live time of the file handle.

With TImpersonateFileStream you code exactly like with a TFileStream beside the 3 additional arguments of the constructor (Usercode, domain and password). If logon fails, you’ll get an exception.

The interface part is like this:
Код:
type
    TImpersonateFileStream = class(TFileStream)
    protected
        FImpersonate   : TImpersonateUser;
    public
        constructor Create(const AFileName  : String;
                           const AMode      : Word;
                           const AUserName  : String;
                           const ADomain    : String;
                           const APassword  : String); overload;
        destructor Destroy; override;
    end;
and the implementation is very simple:
Код:
constructor TImpersonateFileStream.Create(
    const AFileName  : String;
    const AMode      : Word;
    const AUserName  : String;
    const ADomain    : String;
    const APassword  : String);
begin
    // If no user name given, behave like a TFileStream (No cross account)
    if AUserName = '' then begin
        inherited Create(AFileName, AMode);
        Exit;
    end;

    // A username is given, try to logon the user before opening the file
    // and logoff once the file is opened (The file will be accessed as
    // the user used to logon, even after logoff).
    FImpersonate := TImpersonateUser.Create(nil);
    if not FImpersonate.Logon(AUserName, ADomain, APassword) then
         raise EImpersonateFileStream.CreateFmt('Logon error %d. %s.',
                             [FImpersonate.ErrorCode,
                              SysErrorMessage(FImpersonate.ErrorCode)]);
    try
        inherited Create(AFileName, AMode);
    finally
        FImpersonate.Logoff;
    end;
end;

destructor TImpersonateFileStream.Destroy;
begin
    FreeAndNil(FImpersonate);
    inherited Destroy;
end;
I made a demo with edit box to specify the user account credentials and the file, and a few buttons to call various TStream methods.

It is easy to write code similar to TImpersonateFileStream.Create, to do other operation like DeleteFile or RenameFile. Just put that code between logon and logoff as is TImpersonateFileStream.Create.

Full source code for the article, including demo, can be found on GitHub at Для просмотра ссылки Войди или Зарегистрируйся

If you need help with this code, please use StackOverflow.com to ask for your question, be sure to use the tag #delphi so that I receive a notification. I’ll try to answer your question. Before asking, please review “How to Ask” Для просмотра ссылки Войди или Зарегистрируйся