Skip to content

How to read encrypted data and the problem of MARS

Following on from my previous post I’ve created a procedure to read encrypted data using the same principles: Executing the procedure in the context of a least privilege user and elevating the permissions using code signing as follows.

create procedure sales_api.usp_getCCTrans
with
    execute as 'lpu_code_context'
as
begin
    begin try
        open symmetric key
            cc_key
        decryption by
            asymmetric key encryption_root

        select
            name_on_card,
            amount,
            occurred,
            cast(DecryptByKey( card_number) as varchar(max))
        from
            sales_data.cc_trans

        close symmetric key cc_key
    end try
    begin catch
        if (exists (select * from sys.openkeys where key_name = 'cc_key'))
        begin
            close symmetric key cc_key
        end

        print 'ERROR: ' + cast(ERROR_NUMBER() as varchar(50)) + ' - ' + ERROR_MESSAGE() 

    end catch
end
go

-- we know we need to so sign the proc
add signature to
    sales_api.usp_getCCTrans
by
    certificate internal_code
go

-- grant the app_user permission to exec the proc
grant execute on object::sales_api.usp_getCCTrans to app_user
go This works, and does not leak open symmetric keys can be seen with a simple test. 

execute as user ='app_user'
exec sales_api.usp_getCCTransrevert
select * from sys.openkeys

This works well if the procedure is called from TSQL code; execution context is elevated, the necessary key is opened, a result set is created for the caller and the elevated context is removed. However if this procedure is called from .net code using the Sql Native Client its possible for the client to execute TSQL code *while* the result set is being returned to the caller through the expediency of Multiple Active Result Sets. MARS allows a client to iterate over a result set that has been partially delivered to the client and make and another call to the db on the same connection.

The second call will have the original execution context, the one used in the connection string, and not the elevated context the first call is now running it. It will though share the same session context as the first call, meaning session level data such as the open symmetric keys are available. To test this out I filled my original table with around 32k rows to make sure there was enough records to force multiple packets to be returned and ran the following c#.

static void Main()
{
    //get a connection with MARS enabled
    SqlConnection con = new SqlConnection(
        "server=(local);database=secure;uid=app_user;pwd=pword;MultipleActiveResultSets=true");

    SqlCommand cmdGet = new SqlCommand("sales_api.usp_getCCTrans", con);
    con.Open();
    SqlDataReader rdrGet = cmdGet.ExecuteReader();

    while (rdrGet.Read())
    {
        //we have one value from the result set, test another call to the server
        SqlCommand cmdInner = new SqlCommand("select user_name() as u", con);
        cmdInner.CommandType = CommandType.Text;

        //user name is app_user
        Console.WriteLine("Inner call user name is: " + (string)cmdInner.ExecuteScalar());

        //check when keys are open
        // the cc_key is
        cmdInner.CommandText = "select key_name from sys.openkeys";
        SqlDataReader rdrInner = cmdInner.ExecuteReader();
        while (rdrInner.Read())
        {
            Console.WriteLine("Key " + rdrInner.GetString(0) + " is open");
        }
        rdrInner.Close();

        //an open leaked key, lets try to use it
        // fails the user app_user does not have permission to select from the table
        cmdInner.CommandText = "select cast(DecryptByKey( card_number) as varchar(max)) from sales_data.cc_trans";
        rdrInner = cmdInner.ExecuteReader();
        while (rdrInner.Read())
        {
            Console.WriteLine("got card number = " + rdrInner.GetString(0));
            break;
        }
        rdrInner.Close();
        break;
    }
    rdrGet.Close();
    Console.WriteLine("Press return");
    Console.ReadLine();
}

By using a MARS connection the client app is able to view which keys are open and decrypt any data it has access to. In this case the limited permissions of the app_user account meant the select statement was stopped, but this clearly is a problem. If the account used by the application has permission to select from the table it can decrypt the data. Grant the following permission to test this:

grant select on object::sales_data.cc_trans to app_user

But there is another way to decrypt data, rather than manually opening the asymmetric key the *DecryptByKeyAutoAsymKey* function can be used which will open the asymmetric key, perform the decryption and close the key. I modified the get query above to use the Auto function and removed the explicit open and close. Calling the procedure in both TSQL (as above) and C# using MARS there were no leaked keys. My assumption here is that the function is called once for each row and has to decrypt the asymmetric key and open the symmetric key for each call, which should incur a small performance penalty when compared to holding the keys open. This can be observed by examining the “Estimated CPU Cost” of the Compute Scalar operation in the Execution Plan, in my tests the auto method was roughly double that of the manual.

So I take two things away from this. First always have the application connect with a user account that only has permission to call procedures and second consider using the auto functions to decrypt data possibly after testing performance.

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*