授权管理

使用全密态数据库实例时,不同用户之间天然存在密文数据隔离。本文介绍如何通过授权管理实现不同用户之间的数据融合计算,以及允许数据库管理员(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);否则,相同的序列号将会被覆盖,从而导致部分数据可计算而部分数据计算出错。