August 2, 2021

Hide user data using impersonation



This article is based on a question seen on StackOverflow: File permission to a specific application.
(https://stackoverflow.com/questions/68457656/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 (https://github.com/MHumm/DelphiEncryptionCompendium).

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 https://github.com/fpiette/Delphi-ImpersonateUser

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” https://stackoverflow.com/questions/how-to-ask

 

 

 

No comments: