全部產品
Search
文件中心

ApsaraDB RDS:授權管理

更新時間:Oct 24, 2024

使用全密態資料庫執行個體時,不同使用者之間天然存在密文資料隔離。本文介紹如何通過授權管理實現不同使用者之間的資料融合計算,以及允許資料庫管理員(DBA)進行高危資料操作。

功能介紹

全密態資料庫中,使用者資料使用各自的資料密鑰進行加密,不同使用者之間的密文資料天然存在隔離。若需要使用多方使用者的資料進行聯集查詢,則需要由資料所有者對查詢方進行授權,以允許其訪問該資料所有者的密文資料。該過程稱為多使用者授權管理。

在全密態資料庫中,通過行為控制列表(BCL,Behavior Control List)實現多使用者授權訪問功能。通過BCL授權管理,查詢方(Subject)能夠在未瞭解授權方(Issuer,即資料所有者)資料內容的情況下完成資料聯集查詢。授權方無需擔心己方資料的泄露,同時可以通過靈活地簽發BCL授權(同意授權/撤銷授權)來限制查詢方的行為,從而及時有效地避免資料被非預期使用。

特別地,當使用者需要資料庫管理員(DBA)在全密態資料庫中執行高風險操作(如建立使用者密鑰或進行明密文轉換)時,可以通過簽發BCL授權,授權資料庫管理員(DBA)在使用者帳號下進行相關操作。在這種情況下,查詢方和授權方為同一資料庫使用者。

前提條件

  • 已開通全密態資料庫。具體操作,請參見開通全密態資料庫

    說明

    RDS PostgreSQL執行個體的核心小版本大於等於20230830,如需升級核心小版本,請參見升級核心小版本

  • 已分別為授權方和查詢方建立帳號。具體操作,請參見建立帳號

  • 已定義敏感性資料。具體操作,請參見定義敏感性資料

  • 已使用EncDB SDK的方式,在授權方和查詢方各自完成了一次資料庫連接,並分別為二者構建了相應的測試資料。具體操作,請參見EncDB SDK用戶端使用說明

步驟一:初始化公開金鑰和私密金鑰

  1. 分別擷取授權方和查詢方的公開金鑰與私密金鑰。

    1. 安裝OpenSSL工具。

      本文以OpenSSL開源密碼工具擷取公私密金鑰為例。如果您使用Linux系統,系統會內建OpenSSL工具,無需安裝。如果您使用Windows系統,請擷取OpenSSL軟體包並安裝。

      說明

      如果RDS PostgreSQL執行個體規格為全密態基礎版(非Intel SGX安全增強型規格的RDS PostgreSQL執行個體),需要產生基於SM2演算法的公開金鑰和私密金鑰,請執行openssl ecparam -list_curves命令,確認當前OpenSSL版本是否支援SM2密碼編譯演算法。如果返回結果中包含SM2,則表示支援。否則需要升級OpenSSL到支援SM2的版本(1.1.1及以上版本)。

    2. 根據全密態資料庫規格,擷取授權方和查詢方的公開金鑰與私密金鑰。

      產生的公開金鑰和私密金鑰檔案分別為pub_key.pempri_key_pkcs8.pem

      全密態硬體加固版(Intel SGX)

      全密態硬體加固版(Intel SGX),是指Intel SGX 安全增強型規格的RDS PostgreSQL執行個體。詳細的規格清單請參見RDS PostgreSQL主執行個體規格列表

      擷取公開金鑰與私密金鑰的方法:

      1. 使用OpenSSL產生RSA私密金鑰

        openssl genpkey -algorithm RSA -out pri_key_pkcs8.pem -pkeyopt rsa_keygen_bits:3072
        說明

        3072:密鑰長度。可根據實際情況調整為其他符合安全標準的密鑰長度。

      2. 使用OpenSSL產生RSA公開金鑰

        openssl rsa -in pri_key_pkcs8.pem -pubout -out pub_key.pem

      全密態基礎版

      全密態基礎版,是指非Intel SGX安全增強型規格的RDS PostgreSQL執行個體。詳細的規格清單請參見RDS PostgreSQL主執行個體規格列表

      擷取公開金鑰與私密金鑰的方法:

      1. 使用OpenSSL產生SM2私密金鑰

        # 產生PKCS#1私密金鑰
        openssl ecparam -out ec_param.pem -name SM2 -param_enc explicit -genkey
        # 轉換成PKCS#8私密金鑰
        openssl pkcs8 -topk8 -inform PEM -in ec_param.pem -outform pem -nocrypt -out pri_key_pkcs8.pem
      2. 使用OpenSSL產生SM2公開金鑰

        openssl ec -in ec_param.pem -pubout -out pub_key.pem
  2. 分別初始化授權方和查詢方的公開金鑰和私密金鑰。

    分別在授權方和查詢方各自的業務代碼中,初始化授權方和查詢方的公開金鑰及私密金鑰。

    說明

    使用OpenSSL產生的PEM檔案在內容上會自動添加分行符號(newline)。在某些編輯器中,可能會對其顯示進行最佳化,從而導致在手動拷貝時出現漏掉分行符號的情況。建議通過程式碼讀取檔案內容,以確保其準確性。

    //參與方使用者私密金鑰
    String userPrkPemString = readPemFile("path/to/pri_key_pkcs8.pem");
    //參與方使用者公開金鑰
    String userPukPemString = readPemFile("path/to/pub_key.pem");
    //初始化,只需要初始化執行一次,無需重複執行
    KeyManager km = sdk.getKeyManager();
    km.registerCertificate(userPrkPemString, userPukPemString);

步驟二:授權多使用者訪問

  1. 定義授權內容。

    String bclBodyJsonString = """{
      "version": 1,
      "serial_num": "a121bac0-5cb3-4463-a2b2-1155ff29f4c8",
      "issuer_pukid": "p81x+WqYb7BR0yP0LK0qiEaxgLDqwuIjfJhgC0mMJcE=",
      "subject_pukid": "qIPPfgTJEEG/9WkjP0E5LLAijZ14h/Qgb2EfmBZCWSo=",
      "validity": {
        "not_after": "20250820111111+0800",
        "not_before": "20240820111111+0800"
      },
      "policies": {
        "issuer_dek_group": [
          {
            "min": 1,
            "max": 100000,
            "groupid": "5bc60759-5b05-45ec-afc1-ffca1229e554"
          }
        ],
        "result_dek": "SUBJECT",
        "subject_dek_group": [
          {
            "min": 1,
            "max": 100000,
            "groupid": "53413af9-f90a-48a9-93b6-49847861b823"
          }
        ],
        "operation": [
          "*"
        ],
        "postproc": "NULL",
        "preproc": "NULL"
      }
    }
      """;

    參數說明

    參數

    取值範例

    說明

    version

    1

    版本號碼,當前固定為1。

    serial_num

    "a121bac0-5cb3-4463-a2b2-1155ff29f4c8"

    序號,UUID格式,自訂,全域唯一。

    說明

    可使用UUID產生工具產生序號。

    issuer_pukid

    "p81x+WqYb7BR0yP0LK0qiEaxgLDqwuIjfJhgC0mMJcE="

    授權方公開金鑰摘要。基於產生的公開金鑰檔案(pub_key.pem),擷取方法:

    • 全密態硬體加固版(Intel SGX)

      openssl sha256 -binary pub_key.pem | openssl base64

    • 全密態基礎版

      openssl sm3 -binary pub_key.pem | openssl base64

    subject_pukid

    "qIPPfgTJEEG/9WkjP0E5LLAijZ14h/Qgb2EfmBZCWSo="

    查詢方使用者公開金鑰摘要,配置方法與issuer_pukid相同。

    validity

    { "not_before": "20240820111111+0800", "not_after": "20250820111111+0800"}

    授權有效期間。需使用GeneralizedTime時間格式。

    • "not_before":有效期間開始時間。

    • "not_after":有效期間結束時間。

    policies

    issuer_dek_group

    [ { "min": 1, "max": 100000, "groupid": "5bc60759-5b05-45ec-afc1-ffca1229e554" }]

    授權方允許使用的資料密鑰(DEK)分組。

    • "groupid":DEK分組ID。

    • "min":分組內授權的最小DEK ID。

    • "max":分組內授權的最大DEK ID。

    說明

    給定groupid下,DEK ID取值嚴格單調遞增。

    groupid擷取方法:

    SELECT encdb_get_cc_entry_by_name(encdb.keyname_generate('<user_name>', '<database_name>', '<schema_name>', '<table_name>', '<column_name>'));

    說明
    • <user_name>為授權方的使用者名稱。

    • <table_name>為授權訪問的授權方目標表名稱。

    • <column_name>為授權訪問的授權方目標列名稱。

    subject_dek_group

    [ { "min": 1, "max": 100000, "groupid": "53413af9-f90a-48a9-93b6-49847861b823" }]

    查詢方允許使用的資料密鑰(DEK)分組。

    • "groupid":DEK分組ID。

    • "min":分組內授權的最小DEK ID。

    • "max":分組內授權的最大DEK ID。

    說明

    給定groupid下,DEK ID取值嚴格單調遞增。

    擷取groupid方法:

    SELECT encdb_get_cc_entry_by_name(encdb.keyname_generate('<user_name>', '<database_name>', '<schema_name>', '<table_name>', '<column_name>'));

    說明
    • <user_name>為查詢方的使用者名稱。

    • <table_name>為授權訪問的查詢方目標表名稱。

    • <column_name>為授權訪問的查詢方目標列名稱。

    result_dek

    "SUBJECT"

    計算結果使用的DEK加密方式。

    • "SUBJECT":使用當前計算中查詢方的DEK加密。

    • "ISSUER":使用當前計算中授權方的DEK加密。

    • DEK ID:使用指定DEK ID的DEK加密。

      重要

      如果使用DEK ID,直接在此參數配置DEK ID的內容,無需添加雙引號。

    operation

    [ "*"]

    允許的計算操作。

    • "encrypt":加密。

    • "decrypt":解密。

    • "cmp":比較。

    • "*":授權所有操作。

    postproc

    "NULL"

    計算前需要的前置預先處理操作。當前固定為NULL,表示無前置預先處理操作。

    preproc

    "NULL"

    計算後需要的後置預先處理操作。當前固定為NULL,表示無後置預先處理操作。

  2. 授權多使用者訪問。

    • 授權方使用己方公開金鑰和私密金鑰,簽發BCL,授權查詢方訪問目標列的資料。

      boolean isIssuer = true;
      bclBodyJsonString = km.issueBCL(bclBodyJsonString, userPukPemString, userPrkPemString, isIssuer);
    • 查詢方使用己方公開金鑰和私密金鑰,簽發BCL,訪問已授權的資料。

      boolean isIssuer = false;
      bclBodyJsonString = km.issueBCL(bclBodyJsonString, userPukPemString, userPrkPemString, isIssuer);

步驟三:(可選)撤銷授權

如果授權方需要撤銷授權,則使用如下配置。

  1. 定義撤銷授權內容。

    String brlBodyJsonString = """{
      "version": 1,
      "pukid": "dYJ3Wfj/n0eZbuqgQjv8bnBdPXGyWGOlxE/uMy16NXo=",
      "this_update": "20220819111128+0800",
      "next_update": "20220919111128+0800",
      "revoked": [
        {
          "revocation_date": "20220819111128+0800",
          "serial_num": "a121bac0-5cb3-4463-a2b2-1155ff29f4c8"
        }
      ]
    }
     """;  

    參數說明

    參數

    取值範例

    說明

    version

    1

    版本號碼,當前固定為1。

    pukid

    "dYJ3Wfj/n0eZbuqgQjv8bnBdPXGyWGOlxE/uMy16NXo="

    授權方公開金鑰摘要。需配置為請求授權時BCL中的授權方公開金鑰摘要。配置方法與定義授權內容中相同。

    this_update

    "20220819111128+0800"

    本次撤銷列表更新時間,需使用GeneralizedTime時間格式。

    next_update

    "20220919111128+0800"

    下次撤銷列表更新時間,需使用GeneralizedTime時間格式。

    revoked

    revocation_date

    "20220819111128+0800"

    撤銷時間,需使用GeneralizedTime時間格式。

    serial_num

    "a121bac0-5cb3-4463-a2b2-1155ff29f4c8"

    序號,UUID格式,需配置為請求授權時BCL中的序號。

  2. 授權方簽發BCL撤銷授權。

    brlBodyJsonString = km.revokeBCL(brlBodyJsonString, userPukPemString, userPrkPemString);

應用情境樣本

樣本一:授權高危操作-明文和密文轉換

在存量明文資料庫需要啟用全密態資料庫功能,而又不希望進行資料移轉的情況下,可以通過在原資料庫上執行明密文類型的轉換,來高效地切換至密文模式(詳情請參見明文和密文的轉換)。出於安全性考慮,全密態資料庫預設不允許資料庫使用者(包括資料庫管理員)直接在全密態資料庫上對明密文進行原地轉換,這類操作被視為資料安全高危操作。

在明確資料安全風險的前提下,使用者可通過簽發自授權的BCL,授權使用者帳號進行明密文類型的原地轉換。在此過程中,查詢方和授權方均為同一使用者帳號,即:

  • 定義授權內容時,BCL授權中的issuer_pukidsubject_pukid相同、issuer_dek_groupsubject_dek_group相同。

    定義自授權內容

    String bclBodyJsonString = """{
      "version": 1,
      "serial_num": "fdbed057-7fe5-4c31-97ae-afdd615732fe",
      "issuer_pukid": "p81x+WqYb7BR0yP0LK0qiEaxgLDqwuIjfJhgC0mMJcE=",
      "subject_pukid": "p81x+WqYb7BR0yP0LK0qiEaxgLDqwuIjfJhgC0mMJcE=",
      "validity": {
        "not_after": "20250920111111+0800",
        "not_before": "20240920111111+0800"
      },
      "policies": {
        "issuer_dek_group": [
          {
            "min": 1,
            "max": 100000,
            "groupid": "93165651-609f-47db-91aa-c3a2ab7084c4"
          }
        ],
        "result_dek": "SUBJECT",
        "subject_dek_group": [
          {
            "min": 1,
            "max": 100000,
            "groupid": "93165651-609f-47db-91aa-c3a2ab7084c4"
          }
        ],
        "operation": [
          "*"
        ],
        "postproc": "NULL",
        "preproc": "NULL"
      }
    }
     """;
  • 初始化公開金鑰和私密金鑰時,授權方和查詢方使用同一對公開金鑰和私密金鑰,即授權方的userPukPemStringuserPrkPemString與查詢方的對應密鑰相同。

    自授權簽發BCL

    // 授權方和查詢方使用同一對公私密金鑰,簽發BCL
    bclBodyJsonString = km.issueBCL(bclBodyJsonString, userPukPemString, userPrkPemString, true); 
    bclBodyJsonString = km.issueBCL(bclBodyJsonString, userPukPemString, userPrkPemString, false);

完成自授權BCL簽發後,即可在被授權帳號下執行相應操作。完整範例程式碼如下。

授權高危操作:明文和密文轉換

package org.example;

import com.alibaba.encdb.EncdbSDK;
import com.alibaba.encdb.crypto.EncdbSDKBuilder;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        main.testSelfBcl();
    }

    private String readPemFile(String filename) throws IOException {
        return new String(Files.readAllBytes(Paths.get(filename)));
    }

    public void testSelfBcl() {
        // 串連參數請使用實際執行個體資訊及參數進行替換
        String hostname = "pgm-****.pg.rds.aliyuncs.com";
        String port = "5432";
        String dbname = "testdb";
        String username = "testdbuser";
        String password = "****";
        String mek = "00112233445566778899aabbccddeeff";  // 只是樣本,建議用更複雜的密鑰
        String prikeyFilename = "D:\\test\\Issuer\\pri_key_pkcs8.pem"; // 只是樣本,請使用實際路徑替換
        String pubkeyFilename = "D:\\test\\Issuer\\pub_key.pem"; // 只是樣本,請使用實際路徑替換
        try (Connection conn = DriverManager.getConnection("jdbc:postgresql://" + hostname + ":" + port + "/" + dbname + "?binaryTransfer=true", username, password)) {
            System.out.println("connect success");
            String tblname = "tbl_alter_" + conn.getMetaData().getUserName();
            // 建立表
            conn.createStatement().executeUpdate("DROP TABLE IF EXISTS " + tblname);
            conn.createStatement().executeUpdate("CREATE TABLE " + tblname + " (id int)");
            // 寫入明文資料
            try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO " + tblname + " VALUES(?)")) {
                stmt.setInt(1, 231);
                stmt.executeUpdate();
            }
            // (可選)清理歷史BCL授權記錄
            conn.createStatement().executeUpdate("DELETE FROM encdb.encdb_internal_bcl_table");
            conn.createStatement().executeUpdate("DELETE FROM encdb.encdb_internal_brl_map_table");
            conn.createStatement().executeUpdate("DELETE FROM encdb.encdb_internal_brl_table");
            // 初始化SDK,並分發MEK
            EncdbSDK sdk = EncdbSDKBuilder.newInstance()
                    .setDbConnection(conn)
                    .setMek(mek)
                    .build();
            System.out.println("init success");
            // 從檔案讀取公私密金鑰內容
            String userPrkPemString = readPemFile(prikeyFilename);
            String userPukPemString = readPemFile(pubkeyFilename);
            // 註冊公私密金鑰資訊
            sdk.getKeyManager().registerCertificate(userPrkPemString, userPukPemString);
            System.out.println("register certificate success");

            // 使用指定密鑰將明文列變更成密文列,例如通過encdb.dek_generate產生
            // 此處使用使用者的預設密鑰,實際可使用encdb.keyname_generate擷取目標keyname
            String keyname = "'|" + username + "|" + dbname + "|'";
            String alterStmString = "ALTER TABLE " + tblname + " ALTER COLUMN id SET DATA TYPE enc_int4 USING encdb.enc_int4_encrypt(id, " + keyname + ")";

            // 未授權前,執行明密文變更失敗
            try {
                conn.createStatement().executeUpdate(alterStmString);
            } catch (SQLException exception) {
                if (exception.getMessage().contains("fa030000") || exception.getMessage().contains("fa020000")) {
                    System.out.println("alter column to enc_int4 failed without authorized");
                } else {
                    throw exception; // 重新拋出其他異常
                }
            }
            // 執行多使用者授權
            String bclBodyJsonString = """
                    {
                        "version": 1,
                        "serial_num": "fdbed057-7fe5-4c31-97ae-afdd615732fe",
                        "issuer_pukid": "p81x+WqYb7BR0yP0LK0qiEaxgLDqwuIjfJhgC0mMJcE=",
                        "subject_pukid": "p81x+WqYb7BR0yP0LK0qiEaxgLDqwuIjfJhgC0mMJcE=",
                        "validity": {
                            "not_after": "20250920111111+0800",
                            "not_before": "20240920111111+0800"
                        },
                        "policies": {
                            "issuer_dek_group": [
                                {
                                    "min": 1,
                                    "max": 100000,
                                    "groupid": "93165651-609f-47db-91aa-c3a2ab7084c4"
                                }
                            ],
                            "result_dek": "SUBJECT",
                            "subject_dek_group": [
                                {
                                    "min": 1,
                                    "max": 100000,
                                    "groupid": "93165651-609f-47db-91aa-c3a2ab7084c4"
                                }
                            ],
                            "operation": [
                                "*"
                            ],
                            "postproc": "NULL",
                            "preproc": "NULL"
                        }
                    }
                    """;
            // 簽發授權
            {
                bclBodyJsonString = sdk.getKeyManager().issueBCL(bclBodyJsonString, userPukPemString, userPrkPemString, true);
                bclBodyJsonString = sdk.getKeyManager().issueBCL(bclBodyJsonString, userPukPemString, userPrkPemString, false);
                System.out.println("issue bcl success");
            }
            // 授權後,執行明密文變更成功
            conn.createStatement().executeUpdate(alterStmString);
            System.out.println("alter column to enc_int4 success after authorized");
        } catch (SQLException |
                 IOException e) {
            e.printStackTrace(); // 列印異常
        }
    }
}

樣本二:多方資料聯集查詢

以資料平台公司的業務情境為例,該平台通過合法渠道,在獲得使用者授權後,收集使用者資訊,並產生使用者畫像表。經授權後,平台能夠將資料提供給第三方(例如保險公司),以便進行多方資料融合計算,從而實現聯合營銷。詳情請參見多方資料融合計算

以下為多方資料聯集查詢的完整範例程式碼。

多方資料聯集查詢

package org.example;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import com.alibaba.encdb.Cryptor;
import com.alibaba.encdb.EncdbSDK;
import com.alibaba.encdb.common.Constants.EncAlgo;
import com.alibaba.encdb.crypto.EncdbSDKBuilder;

public class BlcTest {
    private String readPemFile(String filename) throws IOException {
        return new String(Files.readAllBytes(Paths.get(filename)));
    }

    private void prepareData(Connection conn, EncdbSDK sdk) throws SQLException {
        int id = 1;
        String name = "name";
        int price = 1234;
        float miles = 12.34f;
        String secret = "aliyun";

        String tblname = "tbl_" + conn.getMetaData().getUserName();
        // 建立表
        conn.createStatement().executeUpdate("DROP TABLE IF EXISTS " + tblname);
        conn.createStatement().executeUpdate("CREATE TABLE " + tblname
                + " (id INTEGER, name VARCHAR, price enc_int4, miles enc_float4, secret enc_text, PRIMARY KEY (id))");

        // 寫入資料
        PreparedStatement stmt = conn.prepareStatement(
                "INSERT INTO " + tblname + " (id, name, price, miles, secret) VALUES(?,?,?,?,?)");
        stmt.setInt(1, id);
        stmt.setString(2, name);
        // 對資料進行加密並寫入
        Cryptor cryptor = sdk.getCryptor();
        stmt.setBytes(3, cryptor.encrypt(tblname, "price", price));
        stmt.setBytes(4, cryptor.encrypt(tblname, "miles", miles));
        stmt.setBytes(5, cryptor.encrypt(tblname, "secret", secret));
        stmt.execute();
    }

    private void validateResult(ResultSet rs, EncdbSDK sdk) throws SQLException {
        int id = 1;
        String name = "name";
        int price = 1234;
        float miles = 12.34f;
        String secret = "aliyun";

        Cryptor cryptor = sdk.getCryptor();
        while (rs.next()) {
            int idRs = rs.getInt(1);
            Assertions.assertEquals(id, idRs);
            String nameRs = rs.getString(2);
            Assertions.assertEquals(name, nameRs);
            int priceRs = cryptor.decryptInt(rs.getBytes(3));
            Assertions.assertEquals(price, priceRs);
            float milesRs = cryptor.decryptFloat(rs.getBytes(4));
            Assertions.assertEquals(miles, milesRs, 0.000001f);
            String secretRs = cryptor.decryptString(rs.getBytes(5));
            Assertions.assertEquals(secret, secretRs);
        }
    }

    private void simpleQuery(Connection conn, EncdbSDK sdk) throws SQLException {
        String tblname = "tbl_" + conn.getMetaData().getUserName();
        String sqlCmd = "SELECT * FROM " + tblname + " WHERE  price > ?";
        PreparedStatement stmt = conn.prepareStatement(sqlCmd);
        // 加密查詢內容
        Cryptor cryptor = sdk.getCryptor();
        stmt.setBytes(1, cryptor.encrypt(tblname, "price", 100));
        ResultSet rs = stmt.executeQuery();

        validateResult(rs, sdk);
    }

    private void subjectJoinQuery(Connection subjectConn, EncdbSDK subjectSdk, Connection issuerConn)
            throws SQLException {
        String issuerTblname = "tbl_" + issuerConn.getMetaData().getUserName();
        String subjectTblname = "tbl_" + subjectConn.getMetaData().getUserName();

        // 以 Subject 身份發起 JOIN 查詢 Issuer 表中資料
        String sqlCmd = "SELECT subject.id as id, subject.name as name, subject.price as price, subject.miles as miles, subject.secret as secret "
                + "FROM " + issuerTblname + " issuer, " + subjectTblname + " subject "
                + "WHERE issuer.price = subject.price and subject.price > ?";

        Connection conn = subjectConn;
        EncdbSDK sdk = subjectSdk;
        PreparedStatement stmt = conn.prepareStatement(sqlCmd);
        // 加密查詢內容
        Cryptor cryptor = sdk.getCryptor();
        stmt.setBytes(1, cryptor.encrypt(subjectTblname, "price", 100));
        ResultSet rs = stmt.executeQuery();

        validateResult(rs, sdk);
    }

    @Test
    public void testBcl() throws SQLException, IOException {
        // 網域名稱(hostname)、連接埠(port)、資料庫執行個體名(dbname)等串連資訊需要更新為您的執行個體資訊
        String hostname = "pgm-****.pg.rds.aliyuncs.com";
        String port = "5432";
        String dbname = "testdb";

        // Issuer 資訊,即授權方
        Connection issuerConn;
        EncdbSDK issuerSdk;
        String issuerPriKeyPemString;
        String issuerPubKeyPemString;
        // Subject 資訊,即申請方
        Connection subjectConn;
        EncdbSDK subjectSdk;
        String subjectPriKeyPemString;
        String subjectPubKeyPemString;

        // 初始化資料庫連接,並完成MEK分發。請參考 EncDB SDK
        {
            // 初始化 Issuer 資訊
            {
                // 使用者名稱(username)、密碼(password)等串連資訊需要更新為您的執行個體資訊
                String username = "testdbuser";
                String password = "****";
                String mek = "00112233445566778899aabbccddeeff"; // 只是樣本,建議用更複雜的密鑰

                // 建立資料庫連接
                String dbUrl = String.format("jdbc:postgresql://%s:%s/%s?binaryTransfer=true", hostname, port, dbname);
                issuerConn = DriverManager.getConnection(dbUrl, username, password);
                System.out.println("issuer connect success");

                // 初始化SDK,並分發MEK,請參考 EncDB SDK
                issuerSdk = EncdbSDKBuilder.newInstance()
                        .setDbConnection(issuerConn)
                        .setMek(mek)
                        .setEncAlgo(EncAlgo.SM4_128_CBC)
                        .build();
                System.out.println("issuer init success");
            }
            // 初始化 Subject 資訊
            {
                // 使用者名稱(username)、密碼(password)等串連資訊需要更新為您的執行個體資訊
                String username = "testdbuser02";
                String password = "****";
                String mek = "ffeeddccbbaa99887766554433221100"; // 只是樣本,建議用更複雜的密鑰

                // 建立資料庫連接
                String dbUrl = String.format("jdbc:postgresql://%s:%s/%s?binaryTransfer=true", hostname, port, dbname);
                subjectConn = DriverManager.getConnection(dbUrl, username, password);
                System.out.println("subject connect success");

                // 初始化SDK,並分發MEK,請參考 EncDB SDK
                subjectSdk = EncdbSDKBuilder.newInstance()
                        .setDbConnection(subjectConn)
                        .setMek(mek)
                        .setEncAlgo(EncAlgo.SM4_128_CBC)
                        .build();
                System.out.println("subject init success");
            }

            // 插入測試資料,並驗證資料所有者可正確訪問並查看密文欄位解密後內容
            {
                // Issuer 寫入資料並驗證
                {
                    Connection tmpConnection = issuerConn;
                    EncdbSDK tmpSdk = issuerSdk;

                    // 以 Issuer 身份,準備測試資料,其中部分欄位為加密欄位
                    prepareData(tmpConnection, tmpSdk);
                    System.out.println("issuer prepare data success");

                    // 以 Issuer 身份查詢資料,可以正常訪問並查看密文欄位解密後內容
                    Assertions.assertDoesNotThrow(() -> {
                        simpleQuery(tmpConnection, tmpSdk);
                    });
                    System.out.println("issuer query own data success");
                }
                // Subject 寫入資料並驗證
                {
                    Connection tmpConnection = subjectConn;
                    EncdbSDK tmpSdk = subjectSdk;

                    // 以 Subject 身份,準備測試資料,其中部分欄位為加密欄位
                    prepareData(tmpConnection, tmpSdk);
                    System.out.println("subject prepare data success");

                    // 以 Subject 身份查詢資料,可以正常訪問並查看密文欄位解密後內容
                    Assertions.assertDoesNotThrow(() -> {
                        simpleQuery(tmpConnection, tmpSdk);
                    });
                    System.out.println("subject query own data success");
                }
                // 授予資料表存取權限,但並非資料內容存取權限
                {
                    String tblname = "tbl_" + issuerConn.getMetaData().getUserName();
                    issuerConn.createStatement().execute(
                            "GRANT SELECT ON TABLE " + tblname + " TO " + subjectConn.getMetaData().getUserName());

                    // 清理歷史BCL授權記錄
                    subjectConn.createStatement().execute("DELETE FROM encdb.encdb_internal_bcl_table");
                    subjectConn.createStatement().execute("DELETE FROM encdb.encdb_internal_brl_map_table");
                    subjectConn.createStatement().execute("DELETE FROM encdb.encdb_internal_brl_table");
                }
            }
        }

        // 註冊 Subject 和 Issuer 公開金鑰和私密金鑰。僅需初始化首次一次性操作,不需要重複執行
        {
            // Issuer 註冊公私密金鑰
            {
                // 從檔案讀取公私密金鑰內容
                String prikeyFilename = "D:\\test\\Issuer\\pri_key_pkcs8.pem"; // 只是樣本,請使用實際路徑替換
                String pubkeyFilename = "D:\\test\\Issuer\\pub_key.pem"; // 只是樣本,請使用實際路徑替換
                issuerPriKeyPemString = readPemFile(prikeyFilename);
                issuerPubKeyPemString = readPemFile(pubkeyFilename);
                // 註冊公私密金鑰資訊
                issuerSdk.getKeyManager().registerCertificate(issuerPriKeyPemString, issuerPubKeyPemString);
                System.out.println("issuer register certificate success");

            }

            // Subject 註冊公私密金鑰
            {
                // 從檔案讀取公私密金鑰內容
                String prikeyFilename = "D:\\test\\Subject\\pri_key_pkcs8.pem"; // 只是樣本,請使用實際路徑替換
                String pubkeyFilename = "D:\\test\\Subject\\pub_key.pem"; // 只是樣本,請使用實際路徑替換
                subjectPriKeyPemString = readPemFile(prikeyFilename);
                subjectPubKeyPemString = readPemFile(pubkeyFilename);
                // 註冊公私密金鑰資訊
                subjectSdk.getKeyManager().registerCertificate(subjectPriKeyPemString, subjectPubKeyPemString);
                System.out.println("subject register certificate success");
            }
        }

        // 未多使用者授權前, Subject 嘗試訪問 Issuer 寫入的資料,報錯提示拒絕訪問
        {
            Connection tmpConnection = subjectConn;
            EncdbSDK tmpSdk = subjectSdk;
            Connection tmpIssuerConn = issuerConn;
            SQLException exception = Assertions.assertThrows(SQLException.class, () -> {
                subjectJoinQuery(tmpConnection, tmpSdk, tmpIssuerConn);
            });
            Assertions.assertEquals("ERROR: encdb_get_hash_from_bytea: errno:fa030000", exception.getMessage());
            System.out.println("subject query issuer data failed without authorized");
        }

        // 執行多使用者授權。請參考文檔中對步驟的描述,確保授權資訊正確
        {
            // 授權 Subject 訪問 Issuer 指定列的密鑰,其中,Subject 和 Issuer 的密鑰分別在
            // `issuer_dek_group`、`subject_dek_group` 指定
            String bclBodyJsonString = """
                    {
                        "version": 1,
                        "serial_num": "a121bac0-5cb3-4463-a2b2-1155ff29f4c8",
                        "issuer_pukid": "p81x+WqYb7BR0yP0LK0qiEaxgLDqwuIjfJhgC0mMJcE=",
                        "subject_pukid": "qIPPfgTJEEG/9WkjP0E5LLAijZ14h/Qgb2EfmBZCWSo=",
                        "validity": {
                            "not_after": "20250820111111+0800",
                            "not_before": "20240820111111+0800"
                        },
                        "policies": {
                            "issuer_dek_group": [
                                {
                                    "min": 1,
                                    "max": 100000,
                                    "groupid": "e4a92b05-f64d-4665-aadd-cd1336d0c0cc"
                                },
                                {
                                    "min": 1,
                                    "max": 100000,
                                    "groupid": "1010b43b-50da-4473-81ac-ce84657eb4f9"
                                },
                                {
                                    "min": 1,
                                    "max": 100000,
                                    "groupid": "b99e70cf-c6b0-444f-a404-81a721e38734"
                                }
                            ],
                            "result_dek": "SUBJECT",
                            "subject_dek_group": [
                                {
                                    "min": 1,
                                    "max": 100000,
                                    "groupid": "d1cbe5a6-49f0-42e0-ba07-572e2a5e2f5f"
                                },
                                {
                                    "min": 1,
                                    "max": 100000,
                                    "groupid": "9c772fa3-4712-4034-9447-0f1e9e18fbeb"
                                },
                                {
                                    "min": 1,
                                    "max": 100000,
                                    "groupid": "e2541c31-891e-4f8d-8389-09c5631f1e66"
                                }
                            ],
                            "operation": [
                                "*"
                            ],
                            "postproc": "NULL",
                            "preproc": "NULL"
                        }
                    }
                    """;

            // 以下兩個步驟先後順序可調整,即:1)Issuer 主動授權,或者 2)Subject 申請授權,均可
            // Issuer 簽發授權,允許 Subject 訪問被 BCL 中指定密鑰DEK加密的資料
            {
                bclBodyJsonString = issuerSdk.getKeyManager().issueBCL(bclBodyJsonString, issuerPubKeyPemString,
                        issuerPriKeyPemString, true);
                System.out.println("issuer issue bcl success");
            }
            // Subject 簽發請求,申請訪問被 BCL 中指定密鑰DEK加密的資料
            {
                bclBodyJsonString = subjectSdk.getKeyManager().issueBCL(bclBodyJsonString, subjectPubKeyPemString,
                        subjectPriKeyPemString, false);
                System.out.println("subject issue bcl success");
            }
        }

        // 多使用者授權後, Subject 嘗試訪問 Issuer 寫入的資料,成功訪問並查看解密後內容
        {
            Connection tmpConnection = subjectConn;
            EncdbSDK tmpSdk = subjectSdk;
            Connection tmpIssuerConn = issuerConn;
            Assertions.assertDoesNotThrow(() -> {
                subjectJoinQuery(tmpConnection, tmpSdk, tmpIssuerConn);
            });
            System.out.println("subject query issuer data success after authorized");
        }
    }
}

常見問題

  • Q:如何處理報錯:ERROR: permission denied for table <table name>

    A:該錯誤表明使用者缺乏對該表的存取權限,需要由表的所有者或管理員執行GRANT SELECT ON <table> TO <user>;命令以授權存取權限。

  • Q:如何驗證授權是否成功?

    A:完成授權操作後,可在查詢方(Subject)帳號下,簡單查詢授權方(Issuer)的資料,如果查詢成功,則表明授權已成功;若查詢失敗,則返回錯誤碼fa030000(未找到授權記錄)或者錯誤碼fa020000(拒絕訪問)。若為授權高危操作,也可通過授權後執行對應高危操作是否成功來驗證,操作成功則說明授權成功。

  • Q:如何對多列密鑰授權?

    A:對於同一組的查詢方(SUBJECT)和授權方(ISSUER),建議將所需的多列密鑰在一個BCL中統一授權,即在issuer_dek_group和subject_dek_group欄位中添加多列密鑰資訊。

    若需對每個密鑰進行單獨授權,則必須為不同的BCL分配不同的序號(serial_num);否則,相同的序號將會被覆蓋,從而導致部分資料可計算而部分資料計算出錯。