viernes, 6 de marzo de 2009

Transparent Data Encryption Parte 1 de 2

Recientemente me ha tocado estar involucrado en proyectos que requieren encripción de datos para cumplir con lineamientos internos o gubernamentales, por lo que se han decidido a implementar los productos ofrecidos por Oracle para su base de datos. Oracle ofrece el módulo de Advanced Security para la encripción de datos a nivel físico (TDE) y red (Network Encryption).

En esta primera parte, analizaremos la opción TDE para 10gr2, la cual permite realizar la encripción de datos a nivel columna. Sin embargo, esta opción cuenta con varias limitaciones y no permite ser usada en los siguientes casos:

■ Columnas con índices que no sean del tipo B-tree
■ Tipos de datos LONG, BLOB, CLOB

además de que pudiera causar serios problemas con el funcionamiento esperado de import/export normales así como en el desempeño de aplicaciones que utilicen búsquedas del tipo 'Range scan' a través de un índice.

Para este artículo analizaremos este último punto y veremos como podría nuestra aplicación verse afectada si utilizamos queries que accesen a los datos a través de un 'Range Scan' en un índice.


1) Comenzaremos por crear 2 tablas sencillas y del mismo formato, con la única diferencia de que una de ellas tendrá una de sus columnas encriptadas.

SQL> create table tabla_noenc(c1 number, c2 varchar2(10));
Table created.

SQL> insert into tabla_noenc values(1,'Juan');
1 row created.

SQL> insert into tabla_noenc values(1,'Jose');
1 row created.

SQL> insert into tabla_noenc values(1,'Martinez');
1 row created.

SQL> create table tabla_enc as select * from tabla_noenc;

SQL> alter table tabla_enc modify c2 encrypt using 'AES128' no salt;


2) Ahora crearemos un índice en cada una de las tablas para el campo c2 y obtendremos sus estadísticas.

SQL> create index in_tabla_noenc_01 on tabla_noenc (c2);
Index created.

SQL> create bitmap index in_tabla_enc_01 on tabla_enc (c2);
Index created.

SQL> exec dbms_stats.gather_table_stats (ownname => 'user',tabname => 'TABLA_NOENC',estimate_percent => 100,cascade => true);
PL/SQL procedure successfully completed.

SQL> exec dbms_stats.gather_table_stats (ownname => 'user',tabname => 'TABLA_ENC',estimate_percent => 100,cascade => true);
PL/SQL procedure successfully completed.


3) Ejecutamos una sencilla sentencia en la tabla sin columnas encriptadas que cuente el numero de registros y que accese a los datos a través del índice creado:

SQL> set autotrace on explain stat

SQL> set linesize 120

SQL> select count(1) from user.tabla_noenc where c2 like 'Ma%';
COUNT(1)
----------
1

Execution Plan
----------------------------------------------------------
Plan hash value: 5644505
---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 7 | 1 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 7 | | |
|* 2 | INDEX RANGE SCAN| IN_TABLA_NOENC_01 | 1 | 7 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("C2" LIKE 'Ma%')
filter("C2" LIKE 'Ma%')

Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
1 consistent gets
0 physical reads
0 redo size
411 bytes sent via SQL*Net to client
400 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

4) Ahora ejecutamos la misma sentencia en la tabla con la columna encriptada:

SQL> select count(1) from user.tabla_enc where c2 like 'Ma%';
COUNT(1)
----------
1

Execution Plan
----------------------------------------------------------
Plan hash value: 933087212
--------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 7 | 3 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 7 | | |
|* 2 | TABLE ACCESS FULL| TABLA_ENC | 1 | 7 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(INTERNAL_FUNCTION("C2") LIKE 'Ma%')

Statistics
----------------------------------------------------------
1 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
411 bytes sent via SQL*Net to client
400 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

Resultados
Como podemos observar en los planes de ejecución, la sentencia en el punto 3 utiliza el índice creado en el plan de ejecución, tiene un costo 1 y presenta 1 consistent get. Sin embargo, la sentencia en el punto 4 realiza un Full Table Scan de la tabla, presenta un costo 3 y 3 consistent gets.


¿Por qué sucede esto?
La respuesta es simple, esto sucedo porque los bloques de la tabla encriptada en el buffer cache son replicas de la tabla, por lo tanto la columkna c2 permanecerá encriptada en el buffer cache, lo cual producirá una degradación en el desempeño para aquellas sentencias que accesen a la información a través de un range scan del índice creado.

Conclusión
Si contamos con Oracle 10gr2, se debe evaluar con mucho cuidado que columnas son las que serán encriptadas. Sin embargo muchas organizaciones no tienen opción alguna y deben optar por un desempeño menor con tal de cumplir con las regulaciones establecidas.

En la siguiente parte de este artículo veremos como TDE 11gr1 ofrece una alternativa a este problema.

No hay comentarios: