文章
Oracle+PHP 简明手册 |
|
如何使用 PHP 在 Oracle 中加密数据 通过加密数据提高 Oracle 驱动的 PHP 应用程序的安全性。
2005 年 12 月发表 虽然存储和检索数据的过程是任何动态 Web 站点的核心,但对于专业的应用程序则需要投入比这些简单的事务更多的考虑和努力。数据存储的一个关键但容易被忽略的方面是使用加密来包含敏感信息 — 从口令到信用卡、社会保障号码等。因此,了解 Oracle 中提供了哪些加密方法(通过 PHP 接口)以及如何充分使用这些方法对所有 Web 开发人员来说都非常重要。 但它没有提供什么加密妙方。而是提供了许多选项,每种选项都有不同的安全级别、编程复杂度和性能开销。(通常在安全性和性能之间存在着反向关系)。在了解细节之前,您应认识到安全性并不只存在两个极端 — “安全”或“不安全” — 相反它是一个连续的分布,每个应用程序应当根据需求和上下文要求选择其中的一个级别。 在这篇方法文档中,将简要介绍一些必备的工具,还会提供一些具体的代码来提高数据的安全性。 背景 首先,我们来介绍一些基本术语和概念。加密的过程是获取输入,用一个特定的密钥通过一种算法来处理它,然后返回加密的输出。算法的复杂度和密钥决定了特定加密的安全程度。您经常听到以 位 来描述加密:128 位、256 位等等。简而言之,使用了越多的位,算法也就越安全。 两种最基本的加密方法是 散列法 和 消息认证码(MAC,发音为 "mack")。这两种方法都不加密或存储数据自身,而是存储数据经过数学计算后的表示。下面是它们的工作原理:
下一个级别就是真正的加密,它会改变数据本身。加密比散列法更复杂并且更具安全风险(因为您存储了某种形式的原始数据)。加密算法有很多种;数字加密标准 (DES) 是早期的加密算法之一,它只有 64 位,因此相对容易破解。几年前 DES 得到了改进,并重新发布为 Triple DES,也称为 DES3。最新且最好的方法是高级加密标准 (AES)。 与 MAC 类似,真正的加密使用密钥(一个字符串)。密钥应当为 RAW 数据类型,密钥中的字符数必须对应加密的位数。例如,128 位的 AES 使用 16 个字符的密钥(128 位除以每个字节 8 位等于 16 个字符)。因为加密数据与解密数据要使用完全相同的密钥,所以保护密钥是一个重要的安全性问题。 对于 Oracle,散列法是通过相应命名的 HASH() 函数实现的;MAC 则通过 MAC() 来计算;加密自然使用 ENCRYPT();解密使用 DECRYPT()。这些都定义在 dbms_obfuscation_toolkit (DOTK) 中。如果您在使用 Oracle 数据库 10g 或更高版本,那么 dbms_obfuscation_toolkit 已被 dbms_crypto 程序包所代替,它包含改进的特性,能够使用 BLOB 和 CLOB 数据类型,并支持(DOTK 不支持的)128 位、192 位或 256 位的 AES。要使用以下示例,需要安装这个程序包,并且您必须以在 dbms_crypto 上拥有 EXECUTE 权限的用户身份与 Oracle 连接。 加密示例 这篇方法文档中的示例代码基于一个非常简单的表,可以使用以下 SQL 命令来创建它:
CREATE TABLE security (
data RAW(2000)
)
以下步骤将向您展示使用 Oracle 存储和检索该表中的加密数据所需的具体操作。由于重点是介绍函数加密,因此出于简洁性考虑,忽略了相关的一些概念(如错误管理和数据验证)。请查看 Oracle 文档获取更多详细信息。 第 1 步:使用 PL/SQL 创建加密/解密函数 我的第一个建议是使 Oracle 做尽可能多的工作。对于初学者,我强烈推荐定义处理加密的函数或存储过程。这么做有两个主要的好处:
清单 1 包含了这里将由 PHP 脚本使用的四个函数的定义。要在 Oracle 中创建它们,使用 PHP 脚本或 SQL*Plus(登录时采用和您使用 PHP 脚本时相同的用户身份)来运行清单 1 代码。这些函数都有注释,并使用相关函数的每个后续步骤中进行了详细说明。
/* Hash calculating function.
* Takes a string of data as its argument.
* Returns a hash in raw format.
*/
CREATE OR REPLACE FUNCTION return_hash(data IN VARCHAR) RETURN RAW IS
BEGIN
/* Call HASH() function, which takes data in RAW format.
* Must use STRING_TO_RAW() to convert data.
* HASH_SH1 or HASH_MD5 are constants representing types of hashes to calculate.
*/
RETURN DBMS_CRYPTO.HASH(UTL_I18N.STRING_TO_RAW (data, 'AL32UTF8'), DBMS_CRYPTO.HASH_SH1);
END;
/
/* MAC calculating function.
* Takes a string of data as its argument.
* Returns a hash in raw format.
*/
CREATE OR REPLACE FUNCTION return_mac(data IN VARCHAR) RETURN RAW IS
BEGIN
/* Call MAC() function, which takes data in RAW format and key in RAW format.
* Must use STRING_TO_RAW() to convert data.
* HMAC_SH1 or HMAC_MD5 are constants representing types of hashes to calculate.
* Use 16-character key with 128-bit encryption.
* Function usage:
* DBMS_CRYPTO.MAC(data AS RAW, HASH TYPE, key AS RAW)
*/
return DBMS_CRYPTO.MAC(UTL_I18N.STRING_TO_RAW (data, 'AL32UTF8'),
DBMS_CRYPTO.HMAC_SH1, UTL_I18N.STRING_TO_RAW ('A1B2C3D4E5F6G7H8', 'AL32UTF8'));
END;
/
/* Encryption function.
* Takes a string of data as its argument.
* Returns data in encrypted RAW format.
*/
CREATE OR REPLACE FUNCTION return_encrypted_data(data IN VARCHAR) RETURN RAW IS
/* Define variables.
* key is the encryption key.
* encryption_mode identifies encryption algorithm and bit level
* as well as and how chaining and padding are handled.
*/
key VARCHAR(16) := 'A1B2C3D4E5F6G7H8';
encryption_mode NUMBER := DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5;
BEGIN
/* Call ENCRYPT() function, which takes data in RAW format and key in RAW format.
* Must use STRING_TO_RAW() to convert data to RAW format.
* Use 16-character key with 128-bit encryption.
* Function usage:
* DBMS_CRYPTO.ENCRYPT(data AS RAW, ENCRYPTION TYPE, key AS RAW)
*/
RETURN DBMS_CRYPTO.ENCRYPT(UTL_I18N.STRING_TO_RAW (data, 'AL32UTF8'),
encryption_mode, UTL_I18N.STRING_TO_RAW(key, 'AL32UTF8') );
END;
/
/* Decryption function.
* Takes raw, encrypted data as its argument.
* Returns unencrypted data in string format.
*/
CREATE OR REPLACE FUNCTION return_decrypted_data(data IN RAW) RETURN VARCHAR IS
/* Define variables.
* key is the encryption key.
* encryption_mode identifies encryption algorithm and bit level
* as well as and how chaining and padding are handled.
*/
key VARCHAR(16) := 'A1B2C3D4E5F6G7H8';
encryption_mode NUMBER := DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5;
BEGIN
/* Call DECRYPT() function, which takes data in RAW format and key in RAW format.
* Must use STRING_TO_RAW() to convert data to raw.
* Must use CAST_TO_VARCHAR2() to convert RAW data to string.
* Use 16-character key with 128-bit encryption.
* Function usage:
* DBMS_CRYPTO.DECRYPT(data AS RAW, ENCRYPTION TYPE, key AS RAW)
*/
RETURN UTL_RAW.CAST_TO_VARCHAR2(DBMS_CRYPTO.DECRYPT
(data, encryption_mode, UTL_I18N.STRING_TO_RAW(key, 'AL32UTF8')));
END;
/
第 2 步:将散列数据插入到数据库中 当您定义了执行加密的函数时,从 PHP 脚本中将加密数据存储在 Oracle 中将相当容易。 清单 2 包含了一个将散列数据插入 security 表中的简单 PHP 脚本。要使用的函数被定义为:
CREATE OR REPLACE FUNCTION return_hash(data IN VARCHAR) RETURN RAW IS
BEGIN
RETURN DBMS_CRYPTO.HASH(UTL_I18N.STRING_TO_RAW (data, 'AL32UTF8'), DBMS_CRYPTO.HASH_SH1);
END;
该函数以一个字符串作为其唯一参数,并以 RAW 格式返回该字符串的一个散列版本。HASH() 函数自身有两个参数:散列化的字符串(RAW 格式)和一个指示要计算的散列类型的常量。UTL_I18N.STRING_TO_RAW() 函数(作为向 HASH() 函数提供的第一个参数的一部分调用)将把输入的字符串转换成 RAW 版本,而 HASH_SH1 是散列类型 (SHA-1)。假定您定义了这个函数,那么您可以在查询中使用它:
INSERT INTO security (data) VALUES (return_hash('$data'))
将这个查询放到一个插入记录的标准 PHP 脚本(清单 2)的上下文中就可以了!使用 $data 的不同值运行 PHP 脚本几次。(PHP 脚本假定您在运行 PHP 5 或更高版本,并且您已经正确设置了 Web 服务器的环境变量。)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-15" <//>
<title>Storing Hashed Values with PHP</title>
</head>
<body>
<h3>Storing Hashed Values</h3>
<?php # Listing 2
// Assumes that the environmental variables have been properly set.
// Data to be stored.
$data = 'This is the data.';
echo "<p>Data being handled:<b>$data</b></p>\n";
// Connect to Oracle.
// Assumes you are using PHP 5, see the PHP manual for PHP 4 examples.
$c = oci_pconnect ('scott', 'tiger', $sid) OR die
('Unable to connect to the database.ERROR:<pre>' . print_r(oci_error(),1) .'</pre></body></html>');
// Define the query.
$q = "INSERT INTO security (data) VALUES (return_hash('$data'))";
// Parse the query.
$s = oci_parse($c, $q);
// Execute the query.
oci_execute ($s);
// Report on the results.
$num = oci_num_rows($s);
if ($num > 0) {
echo '<p>The insert query worked!</p>';
} else {
echo '<p>The insert query DID NOT work!</p>';
}
// Close the connection.
oci_close($c);
// Query to confirm the results:
// SELECT data FROM security
// SELECT * FROM security WHERE data=return_hash('$data')
?>
</body>
</html>
然后在 SQL*Plus 中运行 SELECT 查询查看结果(图 1)。不过请记住,使用散列法是没有办法再查看原始数据的。
第 3 步:创建数据的 MAC 版本 这时,从 PHP 的角度看,将任何类型的加密数据插入到表中实质上都是一样的 — 只是调用适当的 PL/SQL 函数的问题。要用 MAC 值填充 security 表,可以使用 return_mac() 函数:
CREATE OR REPLACE FUNCTION return_mac(data IN VARCHAR) RETURN RAW IS
BEGIN
RETURN DBMS_CRYPTO.MAC(UTL_I18N.STRING_TO_RAW (data, 'AL32UTF8'),
DBMS_CRYPTO.HMAC_SH1, UTL_I18N.STRING_TO_RAW ('A1B2C3D4E5F6G7H8', 'AL32UTF8'));
END;
同样,该函数以一个字符串作为其唯一参数,并以 RAW 格式返回该字符串的一个散列版本。这次将使用 MAC() 函数来执行加密。它有三个参数:散列化的字符串(RAW 格式)、一个指示要计算的散列类型的常量(HMAC_SH1,代表 SHA-1)和一个密钥。由于格式也是 RAW,因此必须将 UTL_I18N.STRING_TO_RAW() 函数应用到要加密的数据和密钥上。 正如我先前提到的,密钥的长度应当与加密的类型匹配 — 例如,一个 16 字符的密钥对应 128 位加密。密钥是由编程人员创建并硬编码到 PL/SQL 函数中去的(正如我在这篇方法文档中所演示的那样)。很明显应当保持它的机密性。 要将 MAC 值插入到表中,使用以下查询:
INSERT INTO security (data) VALUES (return_mac('$data'))
查看清单 3,获知执行该查询的 PHP 脚本:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-15" <//>
<title>Storing MAC Values with PHP</title>
</head>
<body>
<h3>Storing MAC Values</h3>
<?php
// Assumes that the environmental variables have been properly set.
// Data to be stored.
$data = 'This is the data.';
echo "<p>Data being handled:<b>$data</b></p>\n";
// Connect to Oracle.
// Assumes you are using PHP 5, see the PHP manual for PHP 4 examples.
$c = oci_pconnect ('scott', 'tiger', $sid) OR die
('Unable to connect to the database.ERROR:<pre>' . print_r(oci_error(),1) .'</pre></body></html>');
// Define the query.
$q = "INSERT INTO security (data) VALUES (return_mac('$data'))";
// Parse the query.
$s = oci_parse($c, $q);
// Execute the query.
oci_execute ($s);
// Report on the results.
$num = oci_num_rows($s);
if ($num > 0) {
echo '<p>The insert query worked!</p>';
} else {
echo '<p>The insert query DID NOT work!</p>';
}
// Close the connection.
oci_close($c);
// Query to confirm the results:
// SELECT data FROM security
// SELECT * FROM security WHERE data=return_mac('$data')
?>
</body>
</html>
第 4 步:使用 AES 加密
您将实践的下一件事是如何在 PHP 脚本中使用可恢复的加密方法 — 具体来说就是如何存储能够以非加密形式检索的加密值。为此提供的相关用户定义的函数是 return_encrypted_data:
CREATE OR REPLACE FUNCTION return_encrypted_data(data IN VARCHAR) RETURN RAW IS
key VARCHAR(16) := 'A1B2C3D4E5F6G7H8';
encryption_mode NUMBER := DBMS_CRYPTO.ENCRYPT_AES128 +
DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5;
BEGIN
RETURN DBMS_CRYPTO.ENCRYPT(UTL_I18N.STRING_TO_RAW (data, 'AL32UTF8'),
encryption_mode, UTL_I18N.STRING_TO_RAW(key, 'AL32UTF8') );
END;
该函数也以一个字符串作为其唯一参数,并以 RAW 格式返回该字符串的一个加密版本。不过,其内部工作方式要更复杂一些。使用 Oracle 的 ENCRYPT() 函数需要一些更复杂的语法,因此我将从定义两个变量开始,这两个变量将存储您可能在以后希望修改的特定值。查看 ENCRYPT() 函数本身,注意它的第一个参数是要加密的数据 — 当然是 RAW 格式。与 MAC() 函数一样,ENCRYPT() 使用一个密钥作为其第三个参数,该密钥将拥有与加密类型对应的适当长度,当然它也是 RAW 格式。 这里的第二个参数非常重要,它指定应当执行加密的具体类型。您可以通过添加特定的常量作为数字型值(存储在 encryption_mode 变量中)设置这个参数。这里,加密模式是 128 位 AES (ENCRYPT_AES128)— 使用 CHAIN_CBC 链接和 PAD_PKCS5 扩展。(关于“链接”和“扩展”的解释,请阅读本方法文档的结论中参考的文章。) 虽然这个函数可能看起来很复杂,但在查询中使用它仍然非常简单:
INSERT INTO security (data) VALUES (return_encrypted_data('$data'))
清单 4 包含了使用该查询的 PHP 脚本。您应当清空 security 表 (TRUNCATE TABLE security),然后使用不同的数据运行这个脚本几次来重新填充数据库。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-15" <//>
<title>Storing Encrypted Values with PHP</title>
</head>
<body>
<h3>Storing Encrypted Values</h3>
<?php
// Assumes that the environmental variables have been properly set.
// Data to be stored.
$data = 'This is the data.';
echo "<p>Data being handled:<b>$data</b></p>\n";
// Connect to Oracle.
// Assumes you are using PHP 5, see the PHP manual for PHP 4 examples.
$c = oci_pconnect ('scott', 'tiger', $sid) OR die
('Unable to connect to the database.Error:<pre>' . print_r(oci_error(),1) .'</pre></body></html>');
// Define the query.
$q = "INSERT INTO security (data) VALUES (return_encrypted_data('$data'))";
// Parse the query.
$s = oci_parse($c, $q);
// Execute the query.
oci_execute ($s);
// Report on the results.
$num = oci_num_rows($s);
if ($num > 0) {
echo '<p>The insert query worked!</p>';
} else {
echo '<p>The insert query DID NOT work!</p>';
}
// Close the connection.
oci_close($c);
// Query to confirm the results:
// SELECT data FROM security
// SELECT * FROM security WHERE data=return_encrypted_data('$data')
?>
</body>
</html>
在 SQL*Plus 中运行 SELECT 查询查看结果(图 1)。
第 5 步:使用 AES 解密 最后一步是以非加密的形式检索加密数据。为此提供的相应用户自定义的函数是 return_decrypted_data:
CREATE OR REPLACE FUNCTION return_decrypted_data(data IN RAW) RETURN VARCHAR IS
key VARCHAR(16) := 'A1B2C3D4E5F6G7H8';
encryption_mode NUMBER := DBMS_CRYPTO.ENCRYPT_AES128 +
DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5;
BEGIN
RETURN UTL_RAW.CAST_TO_VARCHAR2(DBMS_CRYPTO.DECRYPT
(data, encryption_mode, UTL_I18N.STRING_TO_RAW(key, 'AL32UTF8')));
END;
该函数的运行稍有不同,它使用 RAW 数据作为其唯一参数并返回一个字符串。该函数调用 Oracle 的 DECRYPT() 函数,后者使用 RAW 数据作为其第一个参数(要解密的数据),解密模式作为其第二个参数,密钥作为其第三个参数。第二个和第三个参数的值必须与最早用来加密数据的参数值相同。 除了这种相反的行为之外,该函数可以和其他函数一样容易地在查询中使用。自然,您将在 SELECT 查询中使用这个函数:
SELECT return_decrypted_data(data) AS data FROM security
清单 5 从一个 PHP 脚本中运行这个查询,并使用一个循环打印检索到的记录。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-15" <//>
<title>Retrieving Encrypted Values with PHP</title>
</head>
<body>
<h3>Retrieving Encrypted Values</h3>
<?php
// Assumes that the environmental variables have been properly set.
// Data to be stored.
$data = 'This is the data.';
echo "<p>Data being handled:<b>$data</b></p>\n";
// Connect to Oracle.
// Assumes you are using PHP 5, see the PHP manual for PHP 4 examples.
$c = oci_pconnect ('scott', 'tiger', $sid) OR die
('Unable to connect to the database.Error:<pre>' . print_r(oci_error(),1) .'</pre></body></html>');
// Define the query.
$q = "SELECT return_decrypted_data(data) AS data FROM security";
// Parse the query.
oci_parse($c, $q);
// Execute the query.
oci_execute ($s);
// Fetch and print the returned results.
while (oci_fetch($s)) {
echo "<p>" . oci_result($s,'DATA') ."</p>\n";
}
// Close the connection.
oci_close($c);
// Query to confirm the results:
// SELECT data FROM security
// SELECT * FROM security WHERE data=return_mac('$data')
?>
</body>
</html>
结论 正如您所看到的那样,使用 PHP 和 Oracle 执行加密不会很复杂。技巧是定义完成主要工作的 PL/SQL 函数。这种方法不仅使得 PHP 编码变得更容易,还且更加安全,这是因为使用的特定加密类型和密钥都被隐藏在 Oracle 内部。您可以通过为函数提供假脚本名称进一步隐藏其加密过程。 关于更多详细信息以及关于链接和扩展等的信息,请搜索 Oracle ACE 和安全专家 Arup Nanda 的文章。他在 "Protect from Prying Eyes:Encryption in Oracle 10g " 中对加密的讨论非常精辟。在那篇文章中,Nanda 还讨论了您应当考虑的不同密钥管理理论,这是因为保护密钥是加密数据之安全性的关键。 关于 Oracle 中非 PHP 的加密,请阅读关于在 Oracle 数据库 10g 第 2 版中提供的 特性的信息。该技术可以快速自动地为您加密和解密数据(基于权限),同时将密钥安全地存储在服务器上的一个钱包文件中。 当然,对于数据库而言,最重要的安全性措施是正确管理数据库:限制访问,为用户设立相应的权限等。Oracle 数据库 10g 甚至添加了列级访问权限,它可以和已经提供的行级访问权限一起使用。
Larry Ullman 是 DMC Insights Inc.(该公司专门从事信息技术)的 Digital Media Technology 的主管和首席 Web 开发人员。Larry 居住在华盛顿特区周边,他还撰写了几本关于 PHP、SQL、Web 开发和其他计算机技术的书籍。 |