Search code examples
sql-serverasp-classicalways-encrypted

How to read SQL Always-encrypted column from classic ASP


I maintain a classic ASP app (yeah, I know, we're working on it) and need to access Always Encrypted columns in SQL 2017.

I've imported the cert and tested in SSMS and PowerShell and that much works. The best I've been able to do in ASP is to get the encrypted value as a byte array. I've tried more connection string combinations than I can remember. My ASP dev box is Windows 10; the data server is SQL 2017.

cnn = "Provider=MSOLEDBSQL; DataTypeCompatibility=80; " _
    & "DRIVER={ODBC Driver 17 for SQL Server}; " _
    & "SERVER=xxxx; UID=xxxxx; PWD=xxxxx; DATABASE=xxxx; " _
    & "ColumnEncryption=Enabled; Column Encryption Setting=Enabled;"
Set oDB = CreateObject( "ADODB.Connection" )
oDB.Open cnn
set oCmd = Server.CreateObject("ADODB.Command") 
oCmd.ActiveConnection = cnn
oCmd.CommandText = "SELECT top 10 ID, Enc FROM tbl1"
set rst = oCmd.Execute()

The code works without error but the encrypted column (Enc, varchar(50)) is returned as a byte array. I seem to be getting the encrypted value when I should get the plain text value. I've also tried calling a Stored Procedure with the same results. No filter in the query, so nothing to parameterize. Any ideas what to try next?


Answer:
1) Import the cert as the same user as the AppPool Identity for that web app.
2) Set Anon authorization for the web app to Application Pool Identity.
3) Use this connection string:

  cnn = "Provider=MSDASQL;" _
      & "Extended Properties=""Driver={ODBC Driver 17 for SQL Server};" _
      & "SERVER=***; UID=***; PWD=***;" _
      & "DATABASE=***;ColumnEncryption=Enabled;"" "

Solution

  • The new Microsoft OleDb Provider for SQL Server (MSOLEDBSQL) doesn't support AlwaysEncrypted (currently). You'll have to use ODBC, which means the OleDb provider should be the Microsoft OleDb Provider for ODBC (MSDASQL). So you can either configure a system DSN using Microsoft® ODBC Driver 17 for SQL Server with a connection string like:

    cnn = "Provider=MSDASQL;DSN=testdb;"
    

    or embed all the ODBC driver parameters in the "Extended Properties" of the MSDASQL connection string. Like

    cnn = "Provider=MSDASQL;Extended Properties=""Driver={ODBC Driver 17 for SQL Server};Server=localhost;Database=testdb;Trusted_Connection=yes;ColumnEncryption=Enabled;"" "
    

    Here's walkthrough, using first VBScript for testing before testing with ASP.

    Starting with:

    create database testdb
    go
    use testdb
    
    create table tbl1(id int, Enc varchar(200))
    
    insert into tbl1(id,enc) values (1,'Hello')
    

    Then running through the column encryption wizard in SSMS, which stores the certificate on for the current user, on the machine running SSMS:

    gif of the Column Encryption Wizard

    Then the listing of query.vbs:

    cnn = "Provider=MSDASQL;Extended Properties=""Driver={ODBC Driver 17 for SQL Server};Server=localhost;Database=testdb;Trusted_Connection=yes;ColumnEncryption=Enabled;"" "
    Set oDB = CreateObject( "ADODB.Connection" )
    oDB.Open cnn
    set oCmd = CreateObject("ADODB.Command") 
    oCmd.ActiveConnection = cnn
    oCmd.CommandText = "SELECT top 10 ID, Enc FROM tbl1"
    set rst = oCmd.Execute()
    rst.MoveFirst()
    msgbox( cstr(rst("Enc")) )
    

    Which can be run from the commandline with:

    cscript  .\query.vbs
    

    To do this from ASP you'll additionally have to place the certificate in the user certificate store of the IIS App Pool account, per the docs here. Note that the relative path to the certificate has to be the same for all users. You can't store it in the machine store on the IIS box if you initially configured it to be stored in the user's certificate store. SQL Server stores the key_path of the key and instructs the clients where to find the certificate, eg CurrentUser/my/388FF64065A96DCF0858D84A88E1ADB5A927DECE.

    So discover the key path of the Column Master Key

    select name, key_path from sys.column_master_keys
    

    Then export the certificate from the machine that has it:

     PS C:\Windows> $path = "cert:\CurrentUser\My\388FF64065A96DCF0858D84A88E1ADB5A927DECE"
     PS C:\Windows> $mypwd = ConvertTo-SecureString -String "xxxxxxx" -Force -AsPlainText
     PS C:\Windows> Export-PfxCertificate -Cert $path -FilePath c:\temp\myexport.pfx -ChainOption EndEntityCertOnly  -Password $mypwd
    

    Running as the app pool identity user on the IIS server, import it

    PS C:\WINDOWS> $mypwd = ConvertTo-SecureString -String "xxxxxxx" -Force -AsPlainText
    PS C:\WINDOWS> Import-PfxCertificate -FilePath C:\temp\myexport.pfx -CertStoreLocation Cert:\LocalMachine\My -Password $mypwd
    

    And if you're using Anonymous/Forms auth sure you've configured IIS Anonymous Auth to run under the App Pool identity, not the default IUSR.

    IIS Anonymous Authentication Edit Dialog

    Here's an ASP page to test with:

    <!DOCTYPE html>
    <html>
    <body>
    
    <p>Output :</p>
    
    <%
    
    Set objNetwork = CreateObject("Wscript.Network")
    Response.write("The current user is " & objNetwork.UserName)
    
    cnn = "Provider=MSDASQL;Extended Properties=""Driver={ODBC Driver 17 for SQL Server};Server=localhost;Database=testdb;Trusted_Connection=yes;ColumnEncryption=Enabled;"" "
    Set oDB = CreateObject( "ADODB.Connection" )
    oDB.Open cnn
    set oCmd = CreateObject("ADODB.Command") 
    oCmd.ActiveConnection = cnn
    oCmd.CommandText = "SELECT top 10 ID, Enc FROM tbl1"
    set rst = oCmd.Execute()
    rst.MoveFirst()
    Response.write(cstr(rst("Enc")) )
    
    %>
    
    </body>
    </html>