
# 明/密文数据的转换与同步
开启全密态数据库的内存加密逃生通道功能，且将密钥传输到服务端内存后，支持明文数据和密文数据之间的互相转换和数据同步。可以通过ALTER TABLE语句进行明/密文数据的转换与同步，详细信息请参见《参考》中"SQL参考 \> SQL语法 \> A \> ALTER TABLE"章节。
明文数据与密文数据转换时，涉及修改列类型，需要重建数据，会对操作的表加长时间的排他锁，阻塞对该表的并行读写操作，阻塞时长与数据量成正比，影响用户业务运行。相比离线方式，使用在线方式进行明密文转换时，会减少加排他锁的时间，减少对业务运行的影响。
#### 约束
- 明/密文数据间的转换前需要开启内存解密模式开关，配置密钥信息并创建密钥，建立安全通道并将密钥传输到服务端内存。
- 修改明/密文数据的字段类型后，其他数据库连接需要刷新客户端缓存或者重连，否则会触发报错信息。
- 不支持对加密列直接使用ALTER TABLE语法进行密钥轮转。
- 进行明/密文字段迁移时，明文表不能包含check约束，仅支持check(column is not null)语法。
- 进行明/密文字段迁移时，明文表不能包含外键。
- ALTER TABLE语法其他约束《参考》中"SQL参考 \> SQL语法 \> A \> ALTER TABLE"章节。

- 在线方式进行明/密文数据的转换与同步约束：
  - 在线从明文字段到密文字段的数据同步，不支持指定在线DDL中指定多并行追增线程数。
  
  - 在线DDL进行明/密文字段的数据同步转换时，需预留足够的磁盘空间，即剩余空间是所操作的表（及其上索引等附属品）总和。
  
  - 若DDL语句包含本特性支持操作（包含在线修改列类型/精度/长度缩减、表级在线移动表空间、在线加非空/检查/主键/唯一约束、在线VACUUM FULL）以外的操作，DDL默认以非在线方式执行。
  
  - 长事务可能会阻塞在线DDL，建议避免在长事务存在时进行在线DDL。在线DDL执行后期将阻塞DQL操作，对并行的DML操作进行报错。
  
  - 在线DDL不支持在事务内执行，不支持在存储过程中执行。
  
  - 在线DDL执行期间，应尽量避免与DDL并行执行，否则有一定概率出现死锁、报错等；若出现此类情况，可以重新连接客户端进行重试。
  
  - 在线DDL执行过程中会生成名为online$$ddl$$\[（表所在的模式+表名）的hash值\]的schema。
    - Schema的属主默认为初始用户，不建议在线DDL执行中操作此schema，可能会导致中止在线DDL并产生未知错误。
    
    - 用户不可创建online$$为前缀的schema，会有报错提示。若已存在同名schema冲突，在线DDL将不会生效，DDL默认以非在线方式执行，并提示当前DDL语句是因为schema名冲突不可在线。
     
  
  - 在线DDL进行明密文字段数据同步转换时，若操作失败，表上可能会有明显存储空间膨胀。
  
  - 在线DDL执行过程中发生严重故障，如FATAL、PANIC、数据库故障和节点断连等故障时，创建的临时模式和表的追加状态可能残留，需要手动清理和恢复，通过online_ddl_cleanup()进行手动清理（参见《参考》中"SQL参考 \> 函数和操作符 \> 其他系统函数"章节）。如不及时清理，可能导致reloption中online_ddl_status、online_ddl_internal_status、online_ddl_start_ctid_internal、online_ddl_end_ctid_internal、online_ddl_toast_end_ctid_internal、online_ddl_toast_index_end_ctid_internal、online_ddl_toast_oid、online_ddl_lock_tag残留，进而影响依赖于以上一系列的操作。不建议在线DDL正常执行过程中进行online_ddl_cleanup()清理操作，可能会导致中止在线DDL或产生未知错误。
  
  - 在线DDL进行明/密文字段数据同步转换时，若使用MODIFY \[ COLUMN \] column_name data_type语法且子句包含CHARSET、COLLATE、FIRST、AFTER等任何一个关键字或column_constraint（参见《参考》中"SQL参考 \> SQL语法 \> A \> ALTER TABLE"章节），在线DDL将不会生效，DDL默认以非在线方式执行。
  
  - 在线DDL进行明密文字段数据同步转换时，若使用CHANGE \[ COLUMN \] column_name new_column_name data_type语法（参见《参考》中"SQL参考 \> SQL语法 \> A \> ALTER TABLE"章节），在线DDL将不会生效，DDL默认以非在线方式执行。
   
 
#### 语法格式
- 修改表字段的数据类型。将字段由明文类型改为密文类型，此场景下，执行该语句会进行整表重写。
  ```
  ALTER TABLE [ OFFLINE | [ ONLINE [ WITH ( { online_parameter = value } [, ... ] ) ] ] ] [ IF EXISTS ] table_name
      { ALTER [ COLUMN ] column_name [ SET DATA ] TYPE data_type [ ENCRYPTED WITH ( COLUMN_ENCRYPTION_KEY = column_encryption_key, ENCRYPTION_TYPE = encryption_type_value ) ] } [, ... ];
  ```
  

- 修改表已存在字段的定义（包括将明文类型改为密文类型，NULL约束等），将用新定义替换字段原定义。此语法只能在参数sql_compatibility='B'时使用。
  ```
  ALTER TABLE [ OFFLINE | [ ONLINE [ WITH ( { online_parameter = value } [, ... ] ) ] ] ] [ IF EXISTS ] table_name
      MODIFY [ COLUMN ]  column_name data_type [ ENCRYPTED WITH ( COLUMN_ENCRYPTION_KEY = column_encryption_key, ENCRYPTION_TYPE = encryption_type_value ) ];
  ```
  

- 修改表已存在字段的名称和定义（包括修改字段名称，将明文类型改为密文类型，NULL约束等），字段新名称不能是已有字段的名称。此语法只能在参数sql_compatibility='B'时使用。
  ```
  ALTER TABLE [ OFFLINE | [ ONLINE [ WITH ( { online_parameter = value } [, ... ] ) ] ] ] [ IF EXISTS ] table_name
      CHANGE [ COLUMN ] column_name new_column_name data_type [ ENCRYPTED WITH ( COLUMN_ENCRYPTION_KEY = column_encryption_key, ENCRYPTION_TYPE = encryption_type_value ) ];
  ```
  
 
#### 参数说明
- **online_parameter** **= value**
  当指定ONLINE关键字时，用于指定在线DDL选项。若未指定ONLINE关键字或不满足在线DDL的条件，该参数不生效。
  取值范围：
  - parallel_threads：表示开启并行时的线程数。取值范围：1-32之间的整数值，缺省时默认值为1。 修改明/密文字段类型时，不支持开启并行线程加速追增数据。此时仍然以并行线程数为1的方式执行，因此明/密文数据转换不需要设置该参数。
    
  
  - max_catchup_times：表示在线DDL的最大追增轮次，防止在线DDL执行时间过长。1-50之间的整数值，缺省时默认值为50。
   
  在线DDL会记录数据重建过程中的DML增量数据到临时表，并采用多轮次循环追增，将增量数据进行重建。为防止由于DML并行数据量过大，在线DDL无法完成追增的情况，用户可以设置该参数，以限制追增次数。
  
- **COLUMN_ENCRYPTION_KEY = column_encryption_key**
  为ENCRYPTED WITH约束中列加密密钥的名称。
  取值范围：字符串，要符合标识符命名规范。
  

- **ENCRYPTION_TYPE = encryption_type_value**
  为ENCRYPTED WITH约束中的加密类型，encryption_type_value的值为\[ DETERMINISTIC \| RANDOMIZED \]。
  
 
#### 示例
- 使用gsql进行明密文数据迁移。
  ```
  --解压GaussDB-Kernel_数据库版本号_操作系统版本号_64bit_Gsql.tar.gz安装包，找到脚本gsql_env.sh。
  --通过脚本，自动配置环境变量。
  source gsql_env.sh
  -- 1） 初始化环境。
  --连接数据库，创建B兼容性数据库db1，创建新用户例如alice。
  gsql -p 端口号 -d postgres -r
  gaussdb=# CREATE DATABASE db1 with DBCOMPATIBILITY = 'B';
  CREATE DATABASE
  gaussdb=# CREATE USER alice PASSWORD '*******';
  CREATE ROLE
  gaussdb=# \q
  --连接数据库
  gsql -p 端口号 -d db1 -r
  db1=# GRANT USAGE,CREATE ON SCHEMA PUBLIC TO alice;
  GRANT
  gsql -p 端口号 -d db1 -U alice -r
  --创建明文表并插入数据
  db1=> CREATE TABLE ce_t1 (c1 int, c2 int default 4, c3 int);
  CREATE TABLE
  db1=> INSERT INTO ce_t1 VALUES (1,1,1),(2,2,2),(3,3,3);
  INSERT 0 3
  db1=> \d ce_t1
       Table "public.ce_t1"
   Column |  Type   | Modifiers
  --------+---------+-----------
   c1     | integer |
   c2     | integer | default 4
   c3     | integer |
  -- 2）开始进行明/密文数据转换。
  --连接数据库,使用C3开启内存加解密模式
  gsql -p 端口号 -d db1 -U alice -r -C3
  --设置用户密码或派生密钥，gsql可以通过password=stdin或key_token=stdin进行交互式输入。 
  --此处可以使用其他密钥管理方式，比如kms，具体参考使用gsql操作内存解密逃生通道章节
  --设置用户密码，密码长度不小于8，包含大、小写字母、数字、符号的其中3种。
  db1=> \key_info keyType=user_token,password=*********
  --或者直接通过对接满足安全强度的密钥（二选一即可），此处密钥为16进制编码。
  db1=> \key_info keyType=user_token,key_token=********************************
  --关联主密钥。
  db1=> CREATE CLIENT MASTER KEY cmk1 WITH ( KEY_STORE = user_token , ALGORITHM = AES_256_GCM);
  CREATE CLIENT MASTER KEY
  --创建列密钥
  db1=> CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (CLIENT_MASTER_KEY = cmk1, ALGORITHM = AES_256_GCM);
  CREATE COLUMN ENCRYPTION KEY
  --传输密钥
  db1=> \st
  Token cache enabled in Trusted Domain.
  -- 修改列类型为密文类型并进行数据同步，ALTER TABLE ALTER TYPE修改列类型
  db1=> ALTER TABLE ONLINE ce_t1 ALTER COLUMN c2 TYPE int ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = cek1, ENCRYPTION_TYPE = DETERMINISTIC);
  ALTER TABLE
  db1=> \d ce_t1
               Table "public.ce_t1"
   Column |  Type   |         Modifiers
  --------+---------+---------------------------
   c1     | integer |
   c2     | integer | default 4::int4 encrypted
   c3     | integer |
  db1=> SELECT * FROM ce_t1;
   c1 | c2 | c3
  ----+----+----
    1 |  1 |  1
    2 |  2 |  2
    3 |  3 |  3
  (3 rows)
  -- 3）清理环境
  db1=> DROP TABLE ce_t1;
  DROP TABLE
  --删除列密钥
  db1=> DROP COLUMN ENCRYPTION KEY cek1;
  DROP COLUMN ENCRYPTION KEY
  --删除主密钥。
  db1=> DROP CLIENT MASTER KEY cmk1;
  DROP CLIENT MASTER KEY
  db1=> \q
  --连接数据库，删除用户alice，删除数据库db1。
  gsql -p 端口号 -d postgres -r
  gaussdb=# DROP DATABASE db1;
  DROP DATABASE
  gaussdb=# DROP USER alice;
  DROP ROLE
  ```
  
 
- 使用JDBC进行明密文数据迁移。
  使用JDBC操作密态数据库具体操作指导请参见[使用JDBC操作密态数据库](https://support.huaweicloud.com/centralized-devg-v10-gaussdb/gaussdb-42-0432.html)和[使用JDBC驱动操作内存解密逃生通道](https://support.huaweicloud.com/centralized-devg-v10-gaussdb/gaussdb-42-0440.html)，此处仅作为数据转换示例。
  ```
  // 以下用例以gscejdbc.jar驱动为例，如果使用其他驱动包，仅需修改驱动类名和数据库连接的url前缀。
  // gsjdbc4.jar: 主类名为“org.postgresql.Driver”，数据库连接的url前缀为“jdbc:postgresql”。
  // opengaussjdbc.jar：主类名为“com.huawei.opengauss.jdbc.Driver”，数据库连接的url前缀为“jdbc:opengauss”。
  // gscejdbc.jar：主类名为“com.huawei.gaussdb.jdbc.Driver”，数据库连接的url前缀为“jdbc:gaussdb”
  // gaussdbjdbc.jar：主类名为“com.huawei.gaussdb.jdbc.Driver”，数据库连接的url前缀为“jdbc:gaussdb”。
  public static void main(String[] args) {
      // 驱动类。
      String driver = "com.huawei.gaussdb.jdbc.Driver";
      // 数据库连接描述符。enable_ce=3表示开启内存加解密模式。
      String sourceURL = "jdbc:gaussdb://127.0.0.1:8000/postgres?enable_ce=3";
      // 在环境变量USER、PASSWORD分别配置用户名密码。
      String username = System.getenv("USER");
      String passwd = System.getenv("PASSWORD");
      Connection conn = null;
      try {
          // 加载驱动
          Class.forName(driver);
          Properties properties = new Properties();
          properties.setProperty("user", username);
          properties.setProperty("password", password);
          // 此处使用user_token用户口令的密钥管理方式作为示例。
          // 使用其他密钥管理方式，比如kms，具体参考设置密态等值查询->使用JDBC操作密态数据库章节
          // 设置用户口令进行派生主密钥，口令长度不小于8，包含大、小写字母、数字、符号的其中3种。
          properties.setProperty("key_info", "keyType=user_token,password=*********");
          // 创建连接
          conn = DriverManager.getConnection(sourceURL, properties);
          System.out.println("Connection succeed!");
          // 创建语句对象
          Statement stmt = conn.createStatement();
          // 定义客户端主密钥：cmk1为主密钥名字，可自行取名,此处使用user_token用户口令的密钥管理方式作为示例
          int rc = stmt.executeUpdate("CREATE CLIENT MASTER KEY cmk1 WITH ( KEY_STORE = user_token, ALGORITHM = AES_256_GCM);");
          /* 解释：执行本语法是将主密钥的相关信息存储在数据库中，后续访问主密钥时，会通过用户设置口令和数据库存储的盐值迭代派生客户端主密钥 */
          // 定义列加密密钥
          int rc2 = stmt.executeUpdate("CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (CLIENT_MASTER_KEY = cmk1, ALGORITHM  = AES_256_GCM);");
          // 创建明文表
          int rc3 = stmt.executeUpdate("CREATE TABLE creditcard_info (id_number int, name varchar(50),credit_card varchar(19));");
          // 插入数据
          int rc4 = stmt.executeUpdate("INSERT INTO creditcard_info VALUES (1,'joe','6217986500001288393');");
          // 传输密钥到服务端内存
          conn.setClientInfo("send_token", null);
          // 修改列类型为密文类型并进行数据同步，ALTER TABLE ALTER TYPE修改列类型
          int rc5 = stmt.executeUpdate("ALTER TABLE ONLINE creditcard_info ALTER COLUMN name TYPE varchar(50) ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = cek1, ENCRYPTION_TYPE = DETERMINISTIC);");
          // 查询加密表
          ResultSet rs = null;
          rs = stmt.executeQuery("select * from creditcard_info where name = 'joe';");
          // 删除加密表
          int rc6 = stmt.executeUpdate("DROP TABLE IF EXISTS creditcard_info;");
          // 删除列加密密钥
          int rc7 = stmt.executeUpdate("DROP COLUMN ENCRYPTION KEY IF EXISTS cek1;");
          // 删除客户端主密钥
          int rc8 = stmt.executeUpdate("DROP CLIENT MASTER KEY IF EXISTS cmk1;");
          // 关闭语句对象
          stmt.close();
          // 关闭连接
          conn.close();
      } catch (Exception e) {
          e.printStackTrace();
          return;
      }
  }
  ```
  
 
