Always Encrypted

Always Encryptedで暗号化されたカラムを復号化して表示してみた

Always Encryptedで暗号化したカラムは、Always Encryptedにより暗号化されたカラムを復号化できる設定を追加すると共に、Azure Key Vaultに対する認証を行うための設定を追加することで、復号化した値を取得することができる。

今回は、Always Encryptedで暗号化したカラムを復号化し表示してみたので、そのサンプルプログラムを共有する。

前提条件

下記記事に従って、dbo.USER_PASSテーブルのカラム「pass_encrypted」を暗号化していること。

SQL DatabaseのカラムをSSMSを使ってAlways Encryptedで暗号化してみたAlways Encryptedを利用すると、SQL DatabaseやSQL Serverのデータベースに格納された、クレジットカード...

また、Azure Portal上に、以下の記事に従ってKey Vaultとサービスプリンシパルを作成済であること。

Azure Key Vaultを作成しKey Vaultのシークレットを取得してみた(ソースコード以外編)Azure Key Vaultを利用すると、パスワード等の機密情報へのアクセスをAzure Portal上のみに制限することができる。今...

なお、サービスプリンシパルの、Key Vaultのアクセスポリシーが以下のようになっていること。
前提条件_1

前提条件_2

さらに、下記記事の実装が完了していること。

Azure App ServiceからAzure FunctionsにPost送信してみた(ソースコード編)今回も引き続き、Azure App ServiceからPost通信によってAzure Functionsを呼び出す処理の実装について述べ...



作成したサンプルプログラム(App Service側)の内容

作成したサンプルプログラム(App Service側)の構成は以下の通り。
サンプルプログラムの構成(AppService)
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

コントローラクラスの内容は以下の通りで、後述のgetUserPass関数を呼び出すように修正している。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;

@Controller
public class DemoController {

    /** RestTemplateオブジェクト */
    @Autowired
    private RestTemplate restTemplate;

    /** ObjectMapperオブジェクト */
    @Autowired
    private ObjectMapper objectMapper;

    /** application.propertiesからdemoAzureFunc.urlBaseの値を取得 */
    @Value("${demoAzureFunc.urlBase}")
    private String demoAzureFuncBase;

    /**
     * メイン画面を初期表示する.
     * @param model Modelオブジェクト
     * @return メイン画面
     */
    @GetMapping("/")
    public String index(Model model) {
        return "main";
    }

    /**
     * 暗号化されたパスワードを取得する.
     * @param model Modelオブジェクト
     * @return メイン画面
     */
    @PostMapping("/getUserPass")
    public String getUserPass(Model model) {
        // Azure FunctionsのgetUserPass関数を呼び出すためのヘッダー情報を設定する
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Azure FunctionsのgetUserPass関数を呼び出すための引数を設定する
        MultiValueMap<String, String> map 
            = new LinkedMultiValueMap<>();
        HttpEntity<MultiValueMap<String, String>> entity 
            = new HttpEntity<>(map, headers);

        // Azure FunctionsのgetUserPass関数を呼び出す
        ResponseEntity<String> response = restTemplate.exchange(
              demoAzureFuncBase + "getUserPass", HttpMethod.POST
            , entity, String.class);

        // Azure Functionsの呼出結果を、Modelオブジェクトに設定する
        try {
            GetUserPassResult getUserPassResult 
               = objectMapper.readValue(response.getBody(), GetUserPassResult.class);
            if (getUserPassResult != null 
                  && getUserPassResult.getUserPass() != null) {
                model.addAttribute("getUserPass", getUserPassResult.getUserPass());
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        return "main";
    }

}

また、先ほどのgetUserPass関数を呼び出した結果に関する、dbo.USER_PASSテーブルからの取得結果をクラスの内容は以下の通り。

package com.example.demo;

import lombok.Data;

@Data
public class GetUserPassResult {

    /** USER_PASSテーブルからの取得結果 */
    private UserPass userPass;
	
}
package com.example.demo;

import lombok.Data;

@Data
public class UserPass {

    /** ID */
    private String id;
	
    /** パスワード */
    private String pass;
	
    /** パスワード(暗号化後) */
    private String passEncrypted;
	
}

さらに、メイン画面の内容は以下の通りで、「getUserPassの値を取得」ボタン押下により、コントローラクラスの「getUserPass」メソッドを呼び出すようにしている。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>メイン画面</title>
</head>
<body>
	<form method="post" th:action="@{/getUserPass}">
	    <input type="submit" value="getUserPassの値を取得" /><br/><br/>
	    <div th:if="${getUserPass}">
	    	<span th:text="${getUserPass}">getUserPassで取得した値</span>
	    </div>
	</form>
</body>
</html>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/always-encrypted-decrypt/demoAzureApp



作成したサンプルプログラム(Azure Functions側)の内容

作成したサンプルプログラム(Azure Functions側)の構成は以下の通り。
サンプルプログラムの構成(AzureFunctions)
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

<2021年4月13日 追記>
spring-cloud-function-dependenciesのバージョンは、2021年3月16日にリリースしたバージョン3.1.2を利用すると、1つのAzure Functions内に複数のファンクションを含む場合の不具合が解消できている。


その場合、Handlerクラスの継承するクラスを「AzureSpringBootRequestHandler」クラスから「FunctionInvoker」クラスに変更する。


spring-cloud-function-dependenciesの3.1.2を利用した実装サンプルは、以下の記事を参照のこと。

spring-cloud-function-dependenciesのバージョンを最新(3.1.2)にしてみたこれまでこのブログで取り上げてきたAzure Functionsのサンプルプログラムでは、spring-cloud-function-d...

pom.xmlの追加内容は以下の通りで、Microsoft SQL Server 用 JDBC Driver(mssql-jdbc)のバージョンを「8.4.1.jre8」に変更し、必要なライブラリを追加している。

<!-- SQL Serverを利用するための設定 -->
<dependency>
    <groupId>com.microsoft.sqlserver</groupId>
    <artifactId>mssql-jdbc</artifactId>
    <version>8.4.1.jre8</version>
</dependency>
<dependency>
    <groupId>com.microsoft.azure</groupId>
    <artifactId>adal4j</artifactId>
    <version>1.6.5</version>
</dependency>
<dependency>
    <groupId>com.microsoft.rest</groupId>
    <artifactId>client-runtime</artifactId>
    <version>1.7.4</version>
</dependency>
<dependency>
    <groupId>com.microsoft.azure</groupId>
    <artifactId>azure-keyvault</artifactId>
    <version>1.2.4</version>
</dependency>

なお、Microsoft SQL Server 用 JDBC Driver(mssql-jdbc)に依存するライブラリについては、以下のサイトを参照のこと。
https://docs.microsoft.com/ja-jp/sql/connect/jdbc/feature-dependencies-of-microsoft-jdbc-driver-for-sql-server?view=sql-server-ver15

application.propertiesの内容は以下の通りで、DBのURLに、Always Encryptedで暗号化されたカラムの復号化を可能にする「columnEncryptionSetting=Enabled」という設定を追加すると共に、サービスプリンシパルを利用してKey Vaultにアクセスするための設定を追加している。

# DB接続設定
spring.datasource.url=jdbc:sqlserver://azure-db-purinit.database.windows.net:1433;database=azureSqlDatabase;columnEncryptionSetting=Enabled
#spring.datasource.url=jdbc:sqlserver://azure-db-purinit.database.windows.net:1433;database=azureSqlDatabase
spring.datasource.username=purinit@azure-db-purinit
spring.datasource.password=(DBのパスワード)
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver

# Key Vaultへの設定
keyVaultClientId=cdbb5434-e455-4885-b2e6-911f78c4264b
keyVaultClientKey=(サービスプリンシパルのパスワード)

なお、上記設定のkeyVaultClientId、keyVaultClientKeyには、以下のサービスプリンシパル作成時のコマンド実行時に出力されたappId、passwordの値を指定している。
サービスプリンシパルの作成

また、Azure Functionsのメインクラスは以下の通りで、オブジェクト生成後にKey Vaultの認証の接続設定を登録する処理を追加すると共に、後述のサービスクラスを呼び出しを追加している。

package com.example;

import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import com.example.model.GetUserPassParam;
import com.example.model.GetUserPassResult;
import com.example.service.GetUserPassService;
import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionAzureKeyVaultProvider;
import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionKeyStoreProvider;
import com.microsoft.sqlserver.jdbc.SQLServerConnection;

@SpringBootApplication
public class DemoAzureFunction {

    /** keyVaultのクライアントID */
    @Value("${keyVaultClientId}")
    private String keyVaultClientId;

    /** keyVaultのクライアントキー */
    @Value("${keyVaultClientKey}")
    private String keyVaultClientKey;
	
    /** Key Vaultの認証の接続設定が終わっているかどうかを判定するフラグ */
    private static boolean keyVaultProviderFlg = false;

    /** USER_PASSデータ取得サービスクラスのオブジェクト */
    @Autowired
    private GetUserPassService getUserPassService;

    public static void main(String[] args) throws Exception {
        SpringApplication.run(DemoAzureFunction.class, args);
    }

    /**
     * Key Vaultの認証の接続設定を登録する
     * @throws SQLException SQL例外
     */
    @PostConstruct
    public void postConstruct() throws SQLException {
      if (!keyVaultProviderFlg) {
         SQLServerColumnEncryptionAzureKeyVaultProvider akvProvider 
             = new SQLServerColumnEncryptionAzureKeyVaultProvider(
                         keyVaultClientId, keyVaultClientKey);
         Map<String, SQLServerColumnEncryptionKeyStoreProvider> keyStoreMap 
             = new HashMap<String, SQLServerColumnEncryptionKeyStoreProvider>();
         keyStoreMap.put(akvProvider.getName(), akvProvider);
         SQLServerConnection.registerColumnEncryptionKeyStoreProviders(keyStoreMap);
         keyVaultProviderFlg = true;
      }
    }

    /**
     * USER_PASSテーブルのデータを取得し結果を返却する関数
     * @return USER_PASSテーブルデータ取得サービスクラスの呼出結果
     */
    @Bean
    public Function<GetUserPassParam, GetUserPassResult> getUserPass() {
        return getUserPassParam 
                 -> getUserPassService.getUserPass(getUserPassParam);
    }
}

さらに、UserPassテーブルにアクセスするエンティティクラス・Mapperクラス・Mapper XMLの内容は以下の通りで、Always Encryptedを利用しない場合と同じ内容になっている。

package com.example.mybatis.model;

import lombok.Data;

@Data
public class UserPass {

    /** ID */
    private String id;
	
    /** パスワード */
    private String pass;
	
    /** パスワード(暗号化後) */
    private String passEncrypted;
	
}
package com.example.mybatis;

import org.apache.ibatis.annotations.Mapper;
import com.example.mybatis.model.UserPass;

@Mapper
public interface UserPassMapper {

    UserPass selectOne();
	
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatis.UserPassMapper">
    <resultMap id="userPassResultMap" type="com.example.mybatis.model.UserPass" >
        <result column="id" property="id" jdbcType="VARCHAR" />
        <result column="pass" property="pass" jdbcType="VARCHAR" />
        <result column="passEncrypted" property="passEncrypted" jdbcType="VARCHAR" />
    </resultMap>
    <select id="selectOne" resultMap="userPassResultMap">
        SELECT id, pass, pass_encrypted AS passEncrypted
        FROM user_pass
        WHERE id = 1
    </select>
</mapper>

また、UserPassテーブルにアクセスするサービスクラスの内容は、以下の通り。

package com.example.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.model.GetUserPassParam;
import com.example.model.GetUserPassResult;
import com.example.mybatis.UserPassMapper;

@Service
public class GetUserPassService {

    /** USER_PASSテーブルのデータを取得するMapperオブジェクト */
    @Autowired
    private UserPassMapper userPassMapper;

    /**
     * USER_PASSテーブルのデータを取得し結果を返却する
     * @param getUserPassParam 検索条件Param
     * @return 結果情報オブジェクト
     */
    public GetUserPassResult getUserPass(GetUserPassParam getUserPassParam) {
        GetUserPassResult getUserPassResult = new GetUserPassResult();
        getUserPassResult.setUserPass(userPassMapper.selectOne());
        return getUserPassResult;
    }

}

ハンドラークラスの内容は以下の通りで、Azure App Serviceのコントローラクラスから呼ばれるgetUserPass関数である。

package com.example;

import java.util.Optional;
import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler;
import com.example.model.GetUserPassParam;
import com.example.model.GetUserPassResult;
import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;

public class GetUserPassHandler 
       extends AzureSpringBootRequestHandler<GetUserPassParam, GetUserPassResult> {

    /**
     * HTTP要求に応じて、DemoAzureFunctionクラスのgetUserPassメソッドを呼び出し、
     * その戻り値をボディに設定したレスポンスを返す
     * @param request リクエストオブジェクト
     * @param context コンテキストオブジェクト
     * @return レスポンスオブジェクト
     */
    @FunctionName("getUserPass")
    public HttpResponseMessage execute(
        @HttpTrigger(name = "request", methods = HttpMethod.POST
           , authLevel = AuthorizationLevel.ANONYMOUS) 
                HttpRequestMessage<Optional<String>> request,
             ExecutionContext context) {

        // handleRequestメソッド内でDemoAzureFunctionクラスのgetUserPassメソッドを呼び出し、
        // その戻り値をボディに設定したレスポンスを、JSON形式で返す
        return request.createResponseBuilder(HttpStatus.OK)
            .body(handleRequest(new GetUserPassParam(), context))
            .header("Content-Type", "text/json").build();
    }
}

さらに、Azure FunctionsのメインクラスのgetUserPassメソッドの引数・戻り値は以下のクラスで定義している。

package com.example.model;

public class GetUserPassParam {

    @Override
    public String toString() {
        return "GetUserPassParam []";
    }

}
package com.example.model;

import com.example.mybatis.model.UserPass;
import lombok.Data;

@Data
public class GetUserPassResult {

    /** USER_PASSテーブルからの取得結果 */
    private UserPass userPass;
	
}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/always-encrypted-decrypt/demoAzureFunc



サンプルプログラムの実行結果

サンプルプログラムの実行結果は、以下の通り。

1) まずは、ローカル環境で、Always Encryptedによる暗号化を行わなかった場合の実行結果は、以下の通り。

App Service側のapplication.propertiesは以下の通り。
サンプルプログラムの実行結果_1_1

Azure Functions側のapplication.propertiesは以下の通り。
サンプルプログラムの実行結果_1_2

上記の状態で、ローカル環境でdemoAzureFuncアプリを「mvn azure-functions:run」コマンドで起動した後で、Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスし、「getUserPassの値を取得」ボタンを押下した実行結果は、以下の通り。
サンプルプログラムの実行結果_1_3

サンプルプログラムの実行結果_1_4

上記結果を確認すると、下記データがそのまま表示されることが確認できる。
サンプルプログラムの実行結果_1_5

2) 次に、ローカル環境で、Always Encryptedによる暗号化を行った場合の実行結果は、以下の通り。

App Service側のapplication.propertiesは以下の通り。
サンプルプログラムの実行結果_2_1

Azure Functions側のapplication.propertiesは以下の通り。
サンプルプログラムの実行結果_2_2

上記の状態で、ローカル環境でdemoAzureFuncアプリを「mvn azure-functions:run」コマンドで起動した後で、Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスし、「getUserPassの値を取得」ボタンを押下した実行結果は、以下の通り。
サンプルプログラムの実行結果_2_3

サンプルプログラムの実行結果_2_4

上記結果を確認すると、下記データが復号化され表示されることが確認できる。
サンプルプログラムの実行結果_2_5

3) さらに、Azure環境で、Always Encryptedによる暗号化を行った場合の実行結果は、以下の通り。

App Service側のapplication.propertiesは以下の通り。
サンプルプログラムの実行結果_3_1

Azure Functions側のapplication.propertiesは以下の通り。
サンプルプログラムの実行結果_3_2

上記状態で、「mvn azure-functions:deploy」コマンドによって、Azure Functions上にサンプルプログラムをデプロイする。
サンプルプログラムの実行結果_3_3

なお、Azure Functionsにデプロイする過程は、以下の記事の「Azure FunctionsへのSpring Bootを利用したJavaアプリケーションのデプロイ」を参照のこと。

Azure Functions上でSpring Bootを利用したJavaアプリケーションを作成してみた前回は、Azure Potal上でAzure Functionsを作成してみたが、今回は、前回作成したAzure FunctionsにS...

また、 「mvn azure-webapp:deploy」コマンドによって、Azure App Service上にサンプルプログラムをデプロイする。
サンプルプログラムの実行結果_3_4

なお、Azure App Serviceにデプロイする過程は、以下の記事の「App ServiceへのSpring Bootを利用したJavaアプリケーションのデプロイ」を参照のこと。

Azure App Service上でSpring Bootを利用したJavaアプリケーションを作成してみた前回は、Azure Potal上でApp Serviceを作成してみたが、今回は、前回作成したApp ServiceにSpring Bo...

その後、Azure App ServiceのURL「https://azureappdemoservice.azurewebsites.net/」とアクセスした場合の実行結果は、以下の通り。
サンプルプログラムの実行結果_3_5

なお、上記URLは、下記Azure App ServiceのURLから確認できる。
サンプルプログラムの実行結果_3_6

その後、「getUserPassの値を取得」ボタンを押下すると、以下の画面が表示され、Always Encryptedで暗号化したカラムが復号化し表示されることが確認できる。
サンプルプログラムの実行結果_3_7

サンプルプログラムの実行結果_3_8

要点まとめ

  • Always Encryptedで暗号化したカラムを復号化するには、JDBC接続文字列に「columnEncryptionSetting=Enabled」を追加すると共に、サービスプリンシパルを経由してAzure Key Vaultに対する認証を行うための設定を追加すればよい。