之前已经介绍了SQL语句经过mysql-proxy的lua脚本与C++库交互的过程。在CryptDB的处理中,总体分为两个阶段:rewrite与next。本文介绍在rewrite和next这两个阶段中比较重要的两个类层次:handler以及executor。
SQL改写方式与query恢复介绍 首先考虑如何对SQL语句进行加密。CryptDB不直接处理字符串,而是借用了MySQL5.5版本的parser先对原始SQL语句进行解析,解析完以后获得一个LEX类型结构,是一个MySQL定义的类。 在加密阶段,对LEX内部的各个成员进行加密,并获得一个加密的LEX结构。 在恢复阶段,则需要将这个加密的LEX结构恢复成字符串类型,从而获得加密的SQL语句。
举例来说,对于一个SQL语句SELECT id from student , id 需要被加密。而这个语句解析成LEX 结构以后,id是在item_list成员里面,具体如下:
1 2 3 lex->select_lex.item_list
对这个item_list遍历,可以得到每个被选择的列对应的结构,是一个Item_field 类型。其内部就包含了filed的名字,也就是id。加密过程,就是把这个内部的成员修改成自己想要的加密列的名字。完成LEX结构的加密以后,需要从LEX结构恢复成string类型的SQL语句,相关代码位于parser/stringify.hh 。比如针对上面的SELECT语句,已经得到了加密以后的LEX结构,要重新得到字符串类型的SQL语句,可以通过如下的代码进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 //代码来源 parser/stringfy.hh //参数lex是加密以后的lex static inline std::ostream& operator<<(std::ostream &out, LEX &lex){ String s; switch (lex.sql_command) { //对于select语句,直接调用一次函数就可以恢复SQL语句 case SQLCOM_SELECT: //string类型的结果保存在ostream类型的变量out里面 out << lex.unit; break; } .... } //该函数完成lex.unit到字符串的转化。 static inline std::ostream& operator<<(std::ostream &out, SELECT_LEX_UNIT &select_lex_unit){ String s; select_lex_unit.print(&s, QT_ORDINARY); return out << s; }
所以, 加密过程其实就是SQL语句的解析,以及解析以后的类的成员的修改。要理解完整的SQL解析,加密,以及LEX结构恢复成字符串的代码流程,就需要了解MySQL的parser的内部类的含义,这些在后续的文章中会逐步介绍。
SQL解析与分类处理 在mysqlproxy/ConnectWrapper.cc的rewrite函数中完成了基本的SQL加密操作。其内部执行SQL加密的入口是:Rewriter::rewrite 函数。该函数的作用是获得一个QueryRewrite类,这个类包含了改写以后的SQL语句,以及数据解密所需要的元信息。其部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 QueryRewrite Rewriter::rewrite(const std::string &q, const SchemaInfo &schema, const std::string &default_db, const ProxyState &ps) { //辅助信息 Analysis analysis(default_db, schema, ps.getMasterKey(), ps.defaultSecurityRating()); //SQL解析,加密,获得executor类型,在next阶段使用 AbstractQueryExecutor *const executor = Rewriter::dispatchOnLex(analysis, q); if (!executor) { return QueryRewrite(true, analysis.rmeta, analysis.kill_zone, new NoOpExecutor()); } //QueryRewrite类包含所有必要信息 return QueryRewrite(true, analysis.rmeta, analysis.kill_zone, executor); }
首先是调用Rewriter::dispatchOnLex 函数, 获得executor, 然后返回QueryRewrite类。其中executor中就保存了加密以后的SQL语句。在dispatchOnLex函数中,首先调用MySQL的parer对原始的SQL语句进行解析,获得LEX结构, 然后根据lex.sql_command把SQL语句分为三类:noRewrite,ddl以及dml。不同类型的sql语句分配不同的handler类型进行处理,并返回executor类型,具体如下:
对于noRewrite类型, 直接返回SimpleExecutor类型,其作用是直接返回明文的SQL语句,不进行任何的SQL改写操作。
对于DML以及DDL,则分别由不同的handler类型对SQL语句进行处理,并且返回对应的executor结构。
其简化的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 AbstractQueryExecutor * Rewriter::dispatchOnLex(Analysis &a, const std::string &query) { //使用MySQL的parser进行SQL解析,获得LEX类型 LEX *const lex = query_parse(query); //三种不同的处理 if(noRewrite(*lex)){ return new SimpleExecutor(); }else if(dml_dispatcher->canDo(lex)){ const SQLHandler &handler = dml_dispatcher->dispatch(lex); AbstractQueryExecutor executor = handler.transformLex(lex); return executor; }else if(ddl_dispatcher->canDo(lex)){ const SQLHandler &handler = ddl_dispatcher->dispatch(lex); AbstractQueryExecutor executor = handler.transformLex(lex); return executor; } }
可以看到,这里的加密处理涉及到dispatcher,handler,以及execotor。Dispatcher根据lex的结构,为SQL语句分配handler,不同类型的SQL语句有不同的handler,用来处理sql语句的加密。加密以后的结果则放置在executor中。dispatcher分配handler是基于map的查找来做的,其map初始化的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 SQLDispatcher *buildDMLDispatcher(){ DMLHandler *h; SQLDispatcher *const dispatcher = new SQLDispatcher(); h = new InsertHandler(); dispatcher->addHandler(SQLCOM_INSERT, h); h = new InsertHandler(); dispatcher->addHandler(SQLCOM_REPLACE, h); h = new UpdateHandler; dispatcher->addHandler(SQLCOM_UPDATE, h); h = new DeleteHandler; dispatcher->addHandler(SQLCOM_DELETE, h); h = new MultiDeleteHandler; dispatcher->addHandler(SQLCOM_DELETE_MULTI, h); h = new SelectHandler; dispatcher->addHandler(SQLCOM_SELECT, h); h = new SetHandler; dispatcher->addHandler(SQLCOM_SET_OPTION, h); h = new ShowTablesHandlers; dispatcher->addHandler(SQLCOM_SHOW_TABLES, h); h = new ShowCreateTableHandler; dispatcher->addHandler(SQLCOM_SHOW_CREATE,h); return dispatcher; } SQLDispatcher *buildDDLDispatcher(){ DDLHandler *h; SQLDispatcher *dispatcher = new SQLDispatcher(); h = new CreateTableHandler(); dispatcher->addHandler(SQLCOM_CREATE_TABLE, h); h = new AlterTableHandler(); dispatcher->addHandler(SQLCOM_ALTER_TABLE, h); h = new DropTableHandler(); dispatcher->addHandler(SQLCOM_DROP_TABLE, h); h = new CreateDBHandler(); dispatcher->addHandler(SQLCOM_CREATE_DB, h); h = new ChangeDBHandler(); dispatcher->addHandler(SQLCOM_CHANGE_DB, h); h = new DropDBHandler(); dispatcher->addHandler(SQLCOM_DROP_DB, h); h = new LockTablesHandler(); dispatcher->addHandler(SQLCOM_LOCK_TABLES, h); h = new CreateIndexHandler(); dispatcher->addHandler(SQLCOM_CREATE_INDEX, h); return dispatcher; } //初始化两个全局的dispatcher,内部通过map来保存handler结构 const std::unique_ptr<SQLDispatcher> Rewriter::dml_dispatcher = std::unique_ptr<SQLDispatcher>(buildDMLDispatcher()); const std::unique_ptr<SQLDispatcher> Rewriter::ddl_dispatcher = std::unique_ptr<SQLDispatcher>(buildDDLDispatcher());
在实际的代码中,上面的handler处理过程里还包含通过异常处理来调整洋葱层次的代码,这里暂时不做介绍。下面主要关注hander类以及executor类的组织方式以及特点。
三类handler 我们已经知道了通过不同的handler类,可以处理SQL语句,完成加密的操作。这里,首先给出Handler类的层次结构。
此外,对于ALTER TABLE语句,还有额外的subhandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 AlterDispatcher *buildAlterSubDispatcher() { AlterDispatcher *dispatcher = new AlterDispatcher(); AlterSubHandler *h; h = new AddColumnSubHandler(); dispatcher->addHandler(ALTER_ADD_COLUMN, h); h = new DropColumnSubHandler(); dispatcher->addHandler(ALTER_DROP_COLUMN, h); h = new ChangeColumnSubHandler(); dispatcher->addHandler(ALTER_CHANGE_COLUMN, h); h = new ForeignKeySubHandler(); dispatcher->addHandler(ALTER_FOREIGN_KEY, h); h = new AddIndexSubHandler(); dispatcher->addHandler(ALTER_ADD_INDEX, h); h = new DropIndexSubHandler(); dispatcher->addHandler(ALTER_DROP_INDEX, h); h = new DisableOrEnableKeys(); dispatcher->addHandler(ALTER_KEYS_ONOFF, h); return dispatcher; }
可以看到,不同类型的语句,有不同的handler来处理。这是因为,不同语句的处理流程是不一样的,我们分别来看如下三种handler的特点。
dml handler: dml系列的handler类定义了如下的函数,其中gather和rewrite函数用于加密处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 class DMLHandler : public SQLHandler { public: virtual AbstractQueryExecutor * transformLex(Analysis &a, LEX *lex) const; private: virtual void gather(Analysis &a, LEX *lex) const = 0; virtual AbstractQueryExecutor * rewrite(Analysis &a, LEX *lex) const = 0; protected: DMLHandler() {;} virtual ~DMLHandler() {;} };
每个dml系列的handler都实现了自己的gather和rewrite函数,在gather阶段,对于每个要改写的单元,保存一个rewrite plain,里面记录了这个基本单元可以用什么方式来进行加密。然后在rewrite阶段,使用这些rewrite plain,对解析以后的Lex结构中的基本单元做加密,从而完成加密功能。
ddl handler: ddl 系列的handler类定义了如下的函数,用于加密处理。
1 2 3 4 virtual AbstractQueryExecutor * rewriteAndUpdate(LEX *lex) const = 0;
ddl系列类型的典型处理流程包含了两部分,一是对SQL语句进行加密,二是以delta类来基于数据库的变化,这个delta在next阶段会写入到本地的embedded数据库中。例如CREATE TABLE语句,delta需要记录添加的表的名字, 表有多少列,每列分别采用什么样的加密算法。这些功能全都实现在rewriteAndUpdate函数中了。对于DML来说,由于不会对表结构产生影响,就不需要delta做记录。
Alter table handler 对于ALTER TABLE语句,由于其可能包含多个不同的子命令,所以创建了很多的不同的subhandler来进行处理,其调用subhandler的相关代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 virtual AbstractQueryExecutor * rewriteAndUpdate(Analysis &a, LEX *lex, const Preamble &pre) const { //获取多个subhandler const std::vector<AlterSubHandler *> &handlers = sub_dispatcher->dispatch(lex); assert(handlers.size() > 0); // 使用多个subhandler对LEX进行加密处理 LEX *new_lex ; for (auto it : handlers) { new_lex = it->transformLex(a, new_lex); } }
可以看到,对于一个ALTER TABLE语句,存在多个subhandler的情况。这个信息保存在解析出来LEX结构的alter_info成员里面,通过位操作的方式,获得多个subhandler,分别进行加密处理。 部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 //flags的可能值如下 lex->alter_info.flags; #define ALTER_ADD_COLUMN (1L << 0) #define ALTER_DROP_COLUMN (1L << 1) #define ALTER_CHANGE_COLUMN (1L << 2) #define ALTER_ADD_INDEX (1L << 3) #define ALTER_DROP_INDEX (1L << 4) #define ALTER_RENAME (1L << 5) #define ALTER_ORDER (1L << 6) #define ALTER_OPTIONS (1L << 7) #define ALTER_CHANGE_COLUMN_DEFAULT (1L << 8) #define ALTER_KEYS_ONOFF (1L << 9) #define ALTER_CONVERT (1L << 10) #define ALTER_RECREATE (1L << 11) #define ALTER_ADD_PARTITION (1L << 12) #define ALTER_DROP_PARTITION (1L << 13) #define ALTER_COALESCE_PARTITION (1L << 14) #define ALTER_REORGANIZE_PARTITION (1L << 15) #define ALTER_PARTITION (1L << 16) #define ALTER_ADMIN_PARTITION (1L << 17) #define ALTER_TABLE_REORG (1L << 18) #define ALTER_REBUILD_PARTITION (1L << 19) #define ALTER_ALL_PARTITION (1L << 20) #define ALTER_REMOVE_PARTITIONING (1L << 21) #define ALTER_FOREIGN_KEY (1L << 22) #define ALTER_TRUNCATE_PARTITION (1L << 23)
从handler到executor executor 类型包含了很多的功能,其类型层次结构如下图所示。
其中主要的函数是:nextImpl 。该函数一般基于boost 的coroutine机制来实现,将一个完整的功能分成几个部分。该函数在mysqlproxy/ConnectWrapper.cc 中的next函数里被调用。举例来说,对于SELECT语句,在mysqlproxy/ConnectWrapper.cc的rewrite阶段,得到的是DMLQueryExecutor 。在mysqlproxy/ConnectWrapper.cc的next函数中,调用这个DMLQueryExecutor 的nextImpl函数。第一次进入该函数的时候,返回加密以后的SQL。这个SQL传递给lua脚本,然后转发给MySQL处理并且获得加密以后的结果。在第二次进入nextImpl 函数的时候,会对返回的加密结果进行解密,并将解密的结果返回给lua脚本,转发给客户端。这样,executor的函数执行,就可以和“CryptDB代码分析1-lua与加密库” 中的介绍联系起来了。
一个非常简单的例子 介绍完了原理,现在给出一个非常简单的SQL语句加密的例子,将之前介绍的内容串联起来。我们考虑SHOW TABLES 这个命令,该命令会获取当前db总的表名,由于表名是加密过的,所以mysql-proxy还是对表名进行解密才可以将结果返回给客户端。
首先,客户端发送SHOW TABLES 命令的时候,被mysql-proxy接收到,并且调用mysqlproxy/ConnectWrapper.cc中的rewrite函数,其内部进入Rewriter::dispatchOnLex函数,首先调用parser进行解析,获得了LEX类型。然后进行判断。
1 2 3 4 5 6 7 8 9 10 if (noRewrite(*lex)) { } else if (dml_dispatcher->canDo(lex)) { const SQLHandler &handler = dml_dispatcher->dispatch(lex); AbstractQueryExecutor * executor = handler.transformLex(a, lex); } else if (ddl_dispatcher->canDo(lex)) { }
由于是dml语句,所以进入了第二个分支,得到了一个DMLHandler,并且调用transformLex函数。在transformlext函数内部,包含了gather和rewrite两个函数,首先获得rewrite plain,然后根据rewrite plain多lex的内部成员进行改写,最后返回的executor,这里的executor类型是ShowTableExecutor。
1 2 3 4 5 6 7 8 9 10 class ShowTablesHandlers : public DMLHandler { virtual void gather(Analysis &a, LEX *const lex) const { } virtual AbstractQueryExecutor *rewrite(Analysis &a, LEX *lex) const { return new ShowTablesExecutor(); } };
这样,rewrite阶段完成。在ShowTableExecutor中保存了加密以后的SQL语句。需要主要的是,由于show tables语句比较简单,没有加密,gather阶段没有获取任何的rewrite plain,rewrite阶段自然也就没有多lex的内部成员进行修改,所以加密以后的SQL语句依然是SHOW TABLES 。
然后到了next阶段,需要执行ShowTableExecutor中的nextImpl函数了,其实现如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ShowTablesExecutor:: nextImpl(const ResType &res, const NextParams &nparams) { reenter(this->corot) { //返回加密以后的SQL语句,在这里是 SHOW TABLES yield return CR_QUERY_AGAIN(nparams.original_query); yield { const std::shared_ptr<const SchemaInfo> &schema = nparams.ps.getSchemaInfo(); const DatabaseMeta *const dm = schema->getChild(IdentityMetaKey(nparams.default_db)); TEST_ErrPkt(dm, "failed to find the database '" + nparams.default_db + "'"); std::vector<std::vector<Item *> > new_rows; for (const auto &it : res.rows) { assert(1 == it.size()); for (const auto &table : dm->getChildren()) { assert(table.second); if (table.second->getAnonTableName() == ItemToString(*it.front())) { const IdentityMetaKey &plain_table_name = dm->getKey(*table.second.get()); new_rows.push_back(std::vector<Item *> {make_item_string(plain_table_name.getValue())}); } } } //返回加密以后的表名 return CR_RESULTS(ResType(res, new_rows)); } } }
第一次调用的时候,返回了加密以后的命令,进入之前介绍的mysqlproxy/ConnectWrapper.cc中的next函数中的QUERY_COME_AGAIN分支 ,这个SQL语句传递给lua脚本,并转发给MySQL执行,然后返回加密的结果给mysql-proxy。之后会再次调用next函数,进入到ShowTablesExecutor::nextImpl函数,执行第二个return,返回解密以后的表名,并且进入RESULTS分支,这次就可以将解密以后的表名字返回给客户端,这样该SQL语句的执行就结束了。关于带rewrite plain的操作过程,涉及到更多复杂的处理,将在后续文章中给出。
总结 之前已经介绍过,在CryptDB的模型下,一个SQL语句通过mysqlproxy的lua脚本进行处理,其主要的处理函数是rewrite和next。rewrite被调用一次,用于SQL语句的加密,next则会被多次调用,和mysql-proxy交互,来完成数据解密等功能。本文首先介绍了rewrite阶段。 该阶段通过SQL解析获得LEX结构进行加密,然后将LEX结构恢复成字符串。由于不同类型的SQL有不同的加密方法,需要使用dispatcher类为其分配不同的handler。rewrite阶段结束以后,得到了executor类,其中就包含了加密以后的SQL以及其他相关的信息。executor类型的nextImpl函数在next阶段被调用,分阶段完成返回加密的SQL,数据解密等功能。
相关文献 正在开发新功能的CryptDB分支: https://github.com/yiwenshao/Practical-Cryptdb https://yiwenshao.github.io/2018/02/26/CryptDB代码分析1-lua与加密库/