Oracle 和 Java 安全专家级教程(二)
六、秘密密码加密
我所说的秘密密码加密(password encryption)也被称为共享秘密密码加密和基于密码的加密(PBE)。基本上,这个想法是两方都知道一个密码短语(或密码——我会交替使用这两个词),并且他们都使用这个密码来加密消息或数据。收件人使用相同的密码来解密邮件和数据。没有其他人可以解密数据,因为密码是一个秘密,只有双方共享。
密码加密对我们有好处,原因有几个,主要是它可以用于加密更大的数据块。我们将使用美国数据加密标准(DES)和密码块链接(CBC ),自动将任何大小的数据分成适当的块进行加密;解密后,将结果组合回原始数据。
密码加密的另一个好处是没有公钥,也就是说,没有人知道我们用来加密数据的密钥,假设我们已经充分保护了密码。另一种方法是在客户机和服务器上都使用公钥加密,每一方都用另一方的公钥加密数据,只有接收者才能读取。为了增加保证,让每个人首先用自己的私钥加密他们的消息,然后用另一个人的公钥。仔细想想,你会发现不仅收件人可以解密信息,而且预期的发件人也可以发送信息。(添加一个像 VeriSign 这样的可信证书颁发机构(CA ),您就有了身份保证——每个人都是他们所说的那个人。)然而,我们将仅通过我们的秘密密码加密获得这些好处中的大部分。
我们将在 Oracle 服务器上创建一个密码,并秘密地将其传递回客户端。我们将通过使用客户端的 RSA 公钥对密码进行加密来使其保密。为此,客户机已经将公钥工件(模数和指数)传递给了 Oracle 服务器。只有客户端可以使用私钥解密密码短语。
除了 RSA 公钥加密之外,使用密码加密的最后一个好处是,任何攻击者都必须攻击这两种协议才能拦截我们的数据。
接近
当你通读本章时,你会想要打开参考文件,以跟随完整的代码清单。首先,我们将讨论我们为秘密密码加密实现的 Java 代码,因为我们将在 Java 中构建加密密钥并进行加密。然而,在本章结束之前,我们不会编译和运行 Java 代码。然后我们将分两个阶段运行它:第一个阶段将在客户端计算机上用 Java 进行加密和解密;第二阶段将完成客户端计算机和 Oracle 数据库之间的密码加密密钥交换,并演示客户端/服务器加密/解密。
在本章末尾进入测试阶段之前,我们还将讨论这个过程所需的 Oracle SQL 代码。正如我们讨论的那样,您可以随意执行 SQL 代码来创建我们在 Oracle 数据库上需要的结构,或者您可以在我们进行第二阶段测试之前执行所有的 SQL 代码。
Java 密码加密代码
我们将返回来添加代码并编辑我们在上一章中介绍的OracleJavaSecure类。这个类将构成我们在客户端和 Oracle 数据库上的所有安全流程的核心。随着我们在这一部分的进展,打开 OracleJavaSecure.java并参考完整的代码清单,你会受益匪浅。我们将替换 Oracle 数据库中的OracleJavaSecure类,并在我们的客户端计算机上编译和运行更新后的代码,直到本章结束并进行一些测试。
共享秘密密码钥匙的神器
有几个基于 DES 密码的加密工件必须在客户机和服务器之间共享,以便在每一端都有相同的加密密钥和Cipher。首先,有一个密码短语,只有参与加密对话的双方知道。
还有另外两个工件必须共享,但是在特定的上下文中通常被视为常量。这些是 salt 和迭代计数。将这两个参数固定为常数是一个主要缺点。供应商通常会混淆(隐藏)他们的代码,以隐藏这些值。任何能够窃取 salt 和迭代次数的黑客在解密你的数据上都有优势。
我们的计划是为每个会话生成三个不同的工件。我们将使用SecureRandom实例来生成随机迭代计数和随机 salt。我们还将从随机可接受的字符中生成最大长度的密码短语。
生成密码和工件
既然我们在这个主题上,让我们继续下去,看看我们如何生成这些工件。我们在makeSessionSecretDESPassPhrase()方法中完成,其代码如清单 6-1 所示。
***清单 6-1。*生成 DES 密码神器makeSessionSecretDESPassPhrase()
` private static SecureRandom random = new SecureRandom();
private static final int SALT_LENGTH = 8;
private static int maxPassPhraseBytes;
private static char[] sessionSecretDESPassPhraseChars = null;
private static byte[] salt;
private static int iterationCount;
private static void makeSessionSecretDESPassPhrase() {
// Pass Phrase, Buffer size is limited by RSACipher class (on Oracle JVM)
// Max size of data to encrypt is equal to the key bytes minus padding
// (key.bitlength/8)-PAD_PKCS1_LENGTH (11 Bytes)
maxPassPhraseBytes = ( keyLengthRSA/8 ) - 11;
sessionSecretDESPassPhraseChars = new char[maxPassPhraseBytes];
for( int i = 0; i < maxPassPhraseBytes; i++ ) {
// I want printable ASCII characters for PassPhrase
sessionSecretDESPassPhraseChars[i] =
( char )( **random.**nextInt( 126 - 32 ) + 32 ); }
// Appreciate the power of random
iterationCount = random.nextInt( 10 ) + 15;
salt = new byte[SALT_LENGTH];
for( int i = 0; i < SALT_LENGTH; i++ ) {
salt[i] = ( byte )**random.**nextInt( 256 );
}
}`
注意你可以在文件 *第六章/orajavsec/Oracle javasecure . Java .*中找到这段代码
计算密码的大小
在清单 6-1 中你会注意到的第一件事是密码短语的最大长度的定义。它是根据 RSA 密钥的大小计算的。
maxPassPhraseBytes = ( keyLengthRSA/8 ) - 11;
我们将密码长度基于 RSA 密钥大小的原因是,我们将使用公钥加密我们的密码,而 RSA 只能加密小于其自身密钥的字节数组,减去填充。
我们可以让maxPassPhraseBytes成为一个常数,但是如果我们不知道它的来源,我们可能会尝试更大的东西(好吧,我试过了)。此外,我们可能会在某个时候增加 RSA 密钥的大小,这将自动转换为更长的密码长度。
尊重随机的力量
在我们的方法中,如清单 6-1 所示,我们实例化了一个大小为maxPassPhraseBytes的字节数组,并在for循环中用随机字符填充它。请注意随机字符的参数。我们希望我们的密码短语由 ASCII 范围 32 到 126 的可打印字符组成。
我们将迭代次数设置为 15 到 25 之间的一个随机数。
我们将用 0 到 256 之间的随机字节填充 salt 字节数组。我们的 salt 字节数组大小固定为八个字节。我们将SALT_LENGTH声明为常量(static final)。
我们基于密码的加密的所有这些构件将为每个 Oracle 会话生成,并传递回客户端。在传输之前,将使用 RSA 公钥对它们进行加密。客户端将使用私钥解密它们。然后,有了这些工件,客户机将创建一个相同的密码密钥,用于发送和接收加密数据。
初始化静态类成员
我们将把两个静态类成员的初始化从它们之前在方法中的位置移到类体中。参见清单 6-2 。我们统一了创建这些组件的过程,而不是两次单独调用makeCryptUtilities()方法(一次在客户端,另一次在服务器端)。我们将在定义时实例化SecureRandom,并且我们将在静态初始化块中实例化cipherRSA(根据需要,捕捉异常)。
***清单 6-2。*静态类成员
` private static SecureRandom random = new SecureRandom(); private static Cipher cipherRSA; static { try { cipherRSA = Cipher.getInstance( "RSA" ); } catch( Exception x ) {} }
private static Cipher cipherDES; private static SecretKey sessionSecretDESKey = null; private static AlgorithmParameterSpec paramSpec;
private static String sessionSecretDESAlgorithm = "PBEWithSHA1AndDESede";`
除了上一节描述的基于 DES 密码的加密密钥的构件之外,我们将声明密钥本身和我们将使用的 DES Cipher。我们还将为 DES 密钥构建和使用一个AlgorithmParameterSpec成员。
评估 Java 1.5 基于密码的加密漏洞
我们可以选择使用什么算法来加密我们的秘密密码。美国(DES)不错,但三重 DES (3DES 或 DESede)更强,AES 更是如此;SHA1 是比 MD5 更好的加密散列。所以我们想使用完全由PBEWithSHA1AndDESede表示的基于密码的加密算法,我们指定它如清单 6-2 所示。
但是等等,有个问题。在 Java Runtime Edition 版中,有一个 bug。这个链接有报道:[bugs.sun.com/bugdatabase/view_bug.do?bug_id=6332761](http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6332761)。
Oracle JVM 是基于 JRE 1.5 的,所以它显示了这个错误。当我们生成指定我们的首选算法的密钥时,密钥生成器将返回一个类型为PBEWithMD5AndDES的较弱的密钥。
编码自动升级:协商算法
程序员的工作是调试问题,比如我们遇到的上一节中描述的 bug,并架设跨越障碍的桥梁。我们将有一个方法向我们展示实际使用中的算法;这将显示错误。此外,我们不会假设我们指定的就是我们得到的——我们会将实际的算法返回给客户端,并使用实际的算法构建密码密钥的副本。
这种方法的好处是,我们将协商一个通用算法,并继续指定一个更强的加密算法。继续指定更强的算法将使我们的代码倾向于在 Oracle JVM 中可用时使用更强的算法。
所以我们将指定PBEWithSHA1AndDESede,但此时我们将使用PBEWithMD5AndDES。当 Oracle 下一次升级 Oracle JVM 时,我们准备使用更强的算法。这两种算法都使用 CBC 作为它们的模式,所以它们对我们正在做的事情同样适用。
生成密码密钥
现在我们已经拥有了秘密密码密钥的所有构件,让我们来构建密钥。在清单 6-3 中,我们有一个创建密钥的方法makeSessionSecretDESKey()。该方法的第一步是检查是否已经生成了密钥(passphrase char array 是否为空),如果还没有生成,我们调用前面描述的方法makeSessionSecretDESPassPhrase()来构建工件。
***清单 6-3。*生成密码密钥,makeSessionSecretDESKey()
private static void makeSessionSecretDESKey() throws Exception { // DES Pass Phrase is generated on server and passed to client if( null == sessionSecretDESPassPhraseChars ) makeSessionSecretDESPassPhrase(); paramSpec = new PBEParameterSpec( salt, iterationCount ); KeySpec keySpec = new PBEKeySpec( sessionSecretDESPassPhraseChars, salt, iterationCount ); // Try with recommended algorithm sessionSecretDESKey = SecretKeyFactory.getInstance( sessionSecretDESAlgorithm ).generateSecret( keySpec ); // See what algorithm used sessionSecretDESAlgorithm = sessionSecretDESKey.getAlgorithm(); cipherDES = Cipher.getInstance( sessionSecretDESKey.getAlgorithm() ); }
使用我们的秘密密码密匙工件,我们首先实例化paramSpec,它是一个static类成员。我们在多个方法中使用了那个paramSpec成员,所以我们将它创建为一个静态类成员。它将在将来 Java 存储过程从同一个 Oracle 会话调用静态方法时可用。
我们还实例化了一个KeySpec类,它是方法的本地类,只在这里使用。keySpec成员被SecretKeyFactory用来生成sessionSecretDESAlgorithm成员中描述的算法类型的密钥。这种算法类型会受到 bug 的影响,所以我们实际上会得到一个 Oracle JVM 版本支持的算法类型的键。之后,我们通过调用sessionSecretDESKey.getAlgorithm()得到实际的算法。我们还获得了基于该算法的 DES Cipher的一个实例。
请记住,我们已经将 RSA 公钥从客户端传递给了 Oracle。现在,在 Oracle 数据库上,我们正在构建我们的秘密密码密钥,我们将使用客户机公钥对其工件进行加密,并将其传递回客户机。我们还会把实际的算法回传到客户端,所以我们在客户端建立一个通用的算法。
用公开的 RSA 密钥加密
在上一章中,我们已经看到了这段代码。我们使用我们的服务器构建的 RSA 公钥副本来加密(Oracle 上的)明文数据。然而,以前这是一个公共方法,我们直接调用它来演示 RSA 公钥加密。现在我们使用它作为一个实用方法来加密我们的秘密密码密钥的工件,以发送到客户端,所以我们已经使它成为私有的。看看清单 6-4 中的代码。
***清单 6-4。*用 RSA 公钥加密,getRSACryptData()
private static final RAW getRSACryptData( String extRSAPubMod, String extRSAPubExp, **String clearText** ) throws Exception { byte[] clearBytes = clearText.getBytes(); return getRSACryptData( extRSAPubMod, extRSAPubExp, clearBytes ); }
private static final RAW getRSACryptData( String extRSAPubMod, String extRSAPubExp, **byte[] clearBytes** ) throws Exception { if( ( null == extRSAPubKey ) || ( !saveExtRSAPubMod.equals( extRSAPubMod ) ) ) makeExtRSAPubKey( extRSAPubMod, extRSAPubExp ); cipherRSA.init( Cipher.ENCRYPT_MODE, extRSAPubKey, random ); return new RAW( cipherRSA.doFinal( clearBytes ) ); }
还要注意,我们现在有两个同名的方法:getRSACryptData()。然而,通过采用不同的参数,它们是不同的:一个采用String形式的明文;另一个是字节数组形式。据说这些方法有不同的签名。当我们有多个同名但不同签名的方法时,我们称这个方法为重载。
第一种方法获取明文String并创建一个字节数组。然后为了避免重复任何代码,第一个方法简单地调用第二个方法并返回第二个方法返回的内容:加密的文本。我们将多次调用这些方法来加密我们的秘密密码密钥的所有工件。
向客户端返回秘密密码密钥工件
你会看到以下四种方法之间有如此多的相似之处,以至于我们将只描述每个方面一次。这四种方法返回密码短语、算法、salt 和迭代次数,当它们组合在一起时,可以用来生成 DES 加密密码密钥。
这些方法会从 Java 存储过程中调用,所以分别是public和static。注意,这些方法将 RSA 公钥工件作为参数,它们将使用这些参数来加密秘密密码密钥工件。这些方法将关键工件作为RAW数据类型返回,因此我们保持了 Oracle 数据库和客户机之间的数据保真度。让我们看看清单 6-5 中的第一个。它返回密码短语。
***清单 6-5。*加密口令,getCryptSessionSecretDESPassPhrase()
public static final RAW getCryptSessionSecretDESPassPhrase( String extRSAPubMod, String extRSAPubExp ) { RAW rtrnRAW = new RAW( "getCryptSessionSecretDESPassPhrase() failed".getBytes() ); try { if( null == sessionSecretDESKey ) makeSessionSecretDESKey(); byte[] sessionSecretDESPassPhraseBytes = charArrayToByteArray( sessionSecretDESPassPhraseChars ); rtrnRAW = getRSACryptData( extRSAPubMod, extRSAPubExp, sessionSecretDESPassPhraseBytes ); } catch( Exception x ) { **java.io.CharArrayWriter errorText =** **new java.io.CharArrayWriter( 32767 );** **x.printStackTrace( new java.io.PrintWriter( errorText ) );** **rtrnRAW = new RAW( errorText.toString().getBytes() );** } return rtrnRAW;
}
我们已经讨论了很多Exception处理。这个方法通常被称为当前四人组中的第一个,包括更多的错误报告。因为我们正在返回一个RAW,所以我们最多可以返回 32,767 字节的数据。在catch块中,我们在内存数组中实例化了一个大小为 32,767 个字符的CharArrayWriter。然后通过堆栈调用,我们实例化一个PrintWriter,它指向CharArrayWriter。然后我们将Exception 的钉痕打印到那个PrintWriter上。结果,我们在一个 char 数组编写器errorText中得到堆栈跟踪。我们调用errorText.toString().getBytes()来获取堆栈跟踪的字节数组,然后从中实例化一个RAW,我们可以将它传递回客户端。
在客户端,如果我们在解密从返回的RAW中寻找的密码短语时遇到困难,我们可以将RAW读取为String,并查看Exception堆栈跟踪。在客户机/服务器环境中,当您希望看到服务器看到的错误,而不仅仅是客户机上的错误时,这是一种方便的故障排除实践。
清单 6-6 展示了这一堆方法中的第二个。这个函数返回 Oracle 数据库中使用的实际算法的名称。在try块中,您将看到所有这些方法共有的一个特性。我们测试一下sessionSecretDESKey是否是null,如果是,我们调用makeSessionSecretDESKey()(前面描述过)来创建秘密的密码密钥。
***清单 6-6。*加密算法名称,getCryptSessionSecretDESAlgorithm ()
public static final RAW getCryptSessionSecretDESAlgorithm( String extRSAPubMod, String extRSAPubExp ) { RAW rtrnRAW = new RAW( "getCryptSessionSecretDESAlgorithm() failed"**.getBytes()** ); try { **if( null == sessionSecretDESKey ) makeSessionSecretDESKey();** rtrnRAW = **getRSACryptData**( extRSAPubMod, extRSAPubExp, sessionSecretDESAlgorithm ); } catch( Exception x ) {} return rtrnRAW; }
在try块中的最后一个公共调用是对前面描述的getRSACryptData()方法的调用)来加密秘密密码密钥工件;在这种情况下,是算法名。这会生成一个包含加密工件的RAW数据类型,它将被返回给客户端。
请注意调用异常时返回的内容。我们仍然返回rtrnRAW,但是它的值是字符串"getCryptSessionSecretDESAlgorithm() failed"的字节。将此报告给客户有助于故障排除。另外,请注意我们是如何获得那个String的字节的——我们将引号之间的值视为已经是一个 String对象,调用getBytes()方法。这是允许的吗?没错。
这组中的最后两个方法,如清单 6-7 和清单 6-8 所示,将 salt 和迭代计数作为加密的RAW数据类型返回。
***清单 6-7。*加密盐,getCryptSessionSecretDESSalt ()
public static final RAW getCryptSessionSecretDESSalt( String extRSAPubMod, String extRSAPubExp ) { RAW rtrnRAW = new RAW( "getCryptSessionSecretDESSalt() failed".getBytes() ); try { if( null == sessionSecretDESKey ) makeSessionSecretDESKey();
rtrnRAW = getRSACryptData( extRSAPubMod, extRSAPubExp, salt ); } catch( Exception x ) {} return rtrnRAW; }
***清单 6-8。*加密计数,getCryptSessionSecretDESIterationCount ()
public static final RAW getCryptSessionSecretDESIterationCount( String extRSAPubMod, String extRSAPubExp ) { RAW rtrnRAW = new RAW( "getCryptSessionSecretDESIterationCount() failed".getBytes() ); try { if( null == sessionSecretDESKey ) makeSessionSecretDESKey(); **byte[] sessionSecretDESIterationCountBytes =** **{ ( byte )iterationCount };** rtrnRAW = getRSACryptData( extRSAPubMod, extRSAPubExp, sessionSecretDESIterationCountBytes ); } catch( Exception x ) {} return rtrnRAW; }
返回加密迭代计数的最后一个方法创建了一个字节数组byte。数组中唯一的byte是迭代计数int,转换为byte。这样,我们可以调用相同的方法将迭代计数加密为一个字节数组。当我们在客户端解密时,我们将不得不逆转这个过程。
使用我们的秘密密码加密数据
我们将在客户机和 Oracle 数据库上调用相同的方法,使用密码密钥加密数据。清单 6-9 中的语法现在应该很熟悉了。该方法获取一个String明文数据,并返回一个RAW加密数据。注意,使用sessionSecretDESKey初始化Cipher与我们使用 RSA 密钥的方式非常相似,除了我们还提供了paramSpec。
***清单 6-9。*用密码加密数据,getCryptData()
public static final RAW getCryptData( String clearData ) { if( null == clearData ) return null; RAW rtrnRAW = new RAW( "getCryptData() failed".getBytes() ); try { if( null == sessionSecretDESKey ) makeSessionSecretDESKey(); **cipherDES.init( Cipher.ENCRYPT_MODE, sessionSecretDESKey, paramSpec );** rtrnRAW = new RAW( cipherDES.doFinal( clearData.getBytes() ) ); } catch( Exception x ) {} return rtrnRAW; }
像我们之前描述的加密和返回我们的秘密密码密钥工件的方法一样,这个方法测试sessionSecretDESKey是否已经被实例化,如果没有,尝试创建它。这在 Oracle 数据库上是一种很好的做法,但是在客户机上就太放肆了(在客户机上,我们不希望生成秘密的口令密钥。)开发人员必须理解,客户端必须首先从 Oracle 数据库获得密码密钥,然后才可以使用它来加密数据,以便插入或更新到 Oracle。如果开发人员没有遵循这个指导方针,不会有任何伤害,但是他们的代码将不会工作。
Oracle 秘密密码加密结构
在上一章中,我们创建了一个 Oracle 函数和一个过程来演示我们的 RSA 公钥加密在客户机/服务器环境中的使用。现在,我们将创建一个包含多个函数和过程(包括 Java 存储过程)的 Oracle 包,来处理我们的秘密密码加密。
该包将被放置在应用安全模式中。作为appsec用户,首先使用以下命令将您的角色设置为非默认角色appsec_role:
SET ROLE appsec_role;
注意你可以在名为 Chapter6/AppSec.sql 的文件中找到以下命令的脚本。
当您阅读本节时,您可以跟随参考的代码文件,并在上下文中查看本文中讨论的代码。此外,在阅读 Oracle 结构时,您可以执行代码来创建 Oracle 结构。我们将在本章末尾运行测试时使用这些结构。
打包获取秘密密码工件和加密数据
Oracle 数据库中的包是一组可以配置为一组的函数和过程。访问包中的函数和过程的权限是通过授予包的可执行权限来授予的。到第七章时,我们将了解 Oracle 包的另一个好处:我们可以定义新的数据类型,并在 Oracle 包中使用它们。
Oracle 包有两部分,规范和主体。规范为每个过程或函数提供了签名,但是实际的代码只包含在主体中。包的这种两部分身份允许 PL/SQL 程序员共享功能(规范)而不共享代码(主体)。你这样做可能是为了职责分离、通过混淆实现安全性,或者仅仅是为了保护你的知识产权。c 程序员会认为这种方法类似于每个代码文件都有一个头文件。
应用安全包规范
Oracle 包规范仅定义函数和过程,列出函数的预期参数和返回类型。清单 6-10 显示了app_sec_pkg包规范。作为appsec用户执行此操作。
***清单 6-10。*密码加密包规范
`CREATE OR REPLACE PACKAGE appsec.app_sec_pkg IS
-- For Chapter 6 testing only - move to app in later versions of this package PROCEDURE p_get_shared_passphrase(
ext_modulus VARCHAR2,
ext_exponent VARCHAR2,
secret_pass_salt OUT RAW,
secret_pass_count OUT RAW,
secret_pass_algorithm OUT RAW,
secret_pass OUT RAW,
m_err_no OUT NUMBER,
m_err_txt OUT VARCHAR2 );
-- For Chapter 6 testing only - remove in later versions of this package PROCEDURE p_get_des_crypt_test_data( ext_modulus VARCHAR2, ext_exponent VARCHAR2, secret_pass_salt OUT RAW, secret_pass_count OUT RAW, secret_pass_algorithm OUT RAW, secret_pass OUT RAW, m_err_no OUT NUMBER, m_err_txt OUT VARCHAR2, test_data VARCHAR2, crypt_data OUT RAW );
FUNCTION f_get_crypt_secret_pass( ext_modulus VARCHAR2, ext_exponent VARCHAR2 ) RETURN RAW;
FUNCTION f_get_crypt_secret_algorithm( ext_modulus VARCHAR2, ext_exponent VARCHAR2 ) RETURN RAW;
FUNCTION f_get_crypt_secret_salt( ext_modulus VARCHAR2, ext_exponent VARCHAR2 ) RETURN RAW;
FUNCTION f_get_crypt_secret_count( ext_modulus VARCHAR2, ext_exponent VARCHAR2 ) RETURN RAW;
FUNCTION f_get_crypt_data( clear_text VARCHAR2 ) RETURN RAW;
FUNCTION f_get_decrypt_data( crypt_data RAW ) RETURN VARCHAR2;
-- For Chapter 6 testing only - remove in later versions of this package FUNCTION f_show_algorithm RETURN VARCHAR2;
END app_sec_pkg; /`
参见封装规范后半部分的功能列表。我们有一个函数返回每个秘密密码密钥工件的加密数据:f_get_crypt_secret_pass、f_get_crypt_secret_ algorithm、f_get_crypt_secret_ salt和f_get_crypt_secret_ count。我们也有函数来加密明文并返回一个加密的RAW、f_get_crypt_data,以及解密一个RAW并返回明文、f_get_decrypt_data(我们还没有看到该过程的 Java 部分)。
在我们的函数上面,我们指定了两个过程:p_get_shared_passphrase和p_get_des_crypt_test_data。这些过程中的每一个都将客户机 RSA 公钥模数和指数作为输入参数,并将秘密密码密钥工件作为OUT参数返回。我们正在处理错误,就像我们在前一章中描述的那样,使用OUT参数作为错误号和错误文本。此外,p_get_des_crypt_test_data过程接受一个明文输入参数,并返回一个加密的RAW作为附加的OUT参数。
这两个过程仅用于本章中的测试,并将在以后的章节中从包中移除。最后一个功能f_show_algorithm,也仅用于本章中的测试,稍后将被删除。
应用安全包主体:函数
我们的程序包规范中的函数和过程定义必须在我们的程序包主体中精确复制。主体不仅包含定义,还包含过程和函数的代码。此时您可以执行此程序包体;该包将在 Oracle 数据库中创建。
这里有一个来自清单主体的示例函数供我们考虑,在清单 6-11 中。我们将 RSA 公钥模数和指数传递给函数f_get_crypt_secret_pass。它将它们传递给名为getCryptSessionSecretDESPassPhrase()的 Java 方法(如上所述)。这个 Java 方法返回一个用 RSA 公钥加密的RAW,即秘密密码密匙 passphrase。该函数返回 Java 方法返回给它的原始值。
***清单 6-11。*返回密码短语的功能
FUNCTION f_get_crypt_secret_pass( ext_modulus VARCHAR2, ext_exponent VARCHAR2 ) **RETURN RAW** AS LANGUAGE JAVA NAME 'orajavsec.OracleJavaSecure.getCryptSessionSecretDESPassPhrase( java.lang.String, java.lang.String ) **return oracle.sql.RAW';**
采用相同的方法来获得秘密密码密钥的每个工件,并获得加密和解密的数据。我们还有一个函数,它没有输入参数,返回算法字符串作为一个OUT参数。那个函数f_show_algorithm,只是为了本章的测试。
应用安全包主体:过程
包主体中的过程是我们主要工作的焦点。这些程序如清单 6-12 和清单 6-13 所示。请注意,这两个程序仅用于本章中的测试,在以后的章节中将被删除和替换。我们要看的第一个过程是p_get_shared_passphrase,它将一个秘密的密码密钥返回给客户机。如果这个键还不存在,当我们调用f_get_crypt_secret_salt时,它将被创建。请记住,我们的加密密钥是特定于 Oracle 会话的,因此我们需要保持会话打开,以便使用秘密密码密钥进行加密。
***清单 6-12。*获取共享密码密钥的程序,p_get_shared_passphrase
PROCEDURE p_get_shared_passphrase( ext_modulus VARCHAR2, ext_exponent VARCHAR2, secret_pass_salt OUT RAW,
secret_pass_count OUT RAW, secret_pass_algorithm OUT RAW, secret_pass OUT RAW, m_err_no OUT NUMBER, m_err_txt OUT VARCHAR2 ) IS BEGIN m_err_no := 0; secret_pass_salt := **f_get_crypt_secret_salt**( ext_modulus, ext_exponent ); secret_pass_count := f_get_crypt_secret_count( ext_modulus, ext_exponent ); secret_pass := f_get_crypt_secret_pass( ext_modulus, ext_exponent ); secret_pass_algorithm := f_get_crypt_secret_algorithm( ext_modulus, ext_exponent ); EXCEPTION WHEN OTHERS THEN m_err_no := SQLCODE; m_err_txt := SQLERRM; END p_get_shared_passphrase;
观察清单 6-12 中的p_get_shared_passphraseOracle 程序中的IN和OUT参数列表。我们提供 RSA 公钥的模数和指数,然后得到秘密密码密钥的伪像、错误号和文本。每个工件都是通过调用一个 Oracle 函数获得的,如下所示:
secret_pass := f_get_crypt_secret_pass( ext_modulus, ext_exponent );
像f_get_crypt_secret_pass一样,从p_get_shared_passphrase调用的每个函数都是 Java 存储过程。我们在清单 6-11 中看到了其中一个函数的代码。Java 存储过程的所有实质性工作都是在 Oracle 数据库上用我们的 Java 代码OracleJavaSecure完成的
我们在这里要看的第二个程序是p_get_des_crypt_test_data。除了p_get_des_crypt_test_data有两个额外的参数,一个IN和一个OUT之外,它与p_get_shared_passphrase非常相似,如清单 6-13 所示。这些参数将用于向 Oracle 数据库提交明文,并以加密形式返回该文本—用秘密口令密钥加密。
***清单 6-13。*获取加密数据的程序,p_get_des_crypt_test_data
PROCEDURE p_get_des_crypt_test_data( ext_modulus VARCHAR2, ext_exponent VARCHAR2, secret_pass_salt OUT RAW, secret_pass_count OUT RAW, secret_pass_algorithm OUT RAW, secret_pass OUT RAW, m_err_no OUT NUMBER, m_err_txt OUT VARCHAR2, **test_data VARCHAR2,** **crypt_data OUT RAW )** IS BEGIN m_err_no := 0; secret_pass_salt := f_get_crypt_secret_salt( ext_modulus, ext_exponent ); secret_pass_count := f_get_crypt_secret_count( ext_modulus, ext_exponent ); secret_pass := f_get_crypt_secret_pass( ext_modulus, ext_exponent ); secret_pass_algorithm := f_get_crypt_secret_algorithm( ext_modulus, ext_exponent ); **crypt_data := f_get_crypt_data( test_data );** EXCEPTION WHEN OTHERS THEN m_err_no := SQLCODE; m_err_txt := SQLERRM; END p_get_des_crypt_test_data;
我们将明文test_data从客户端发送到 Oracle,这个过程通过调用f_get_crypt_data函数在加密后返回crypt_data。该函数也是一个 Java 存储过程。
用于秘密密码解密的 Java 方法
一旦我们调用了appsec过程来将 DES 秘密密码密钥工件和加密数据返回给客户机,我们需要
1)用 RSA 私钥解密工件
2)生成 DES 加密口令密钥
3)用秘密口令密钥解密数据
作为一条规则,我试图限制我要求开发人员完成工作的步骤数量。当开发人员可以调用一个方法来完成其他调用时,为什么要让他们调用三个方法呢?应用开发人员的目标是解密数据,因此我们为他们提供了一种方法来完成这项工作。
注你可以在文件chapter 6/orajavsec/Oracle javasecure . Java中找到这段代码。
使用密码密钥解密数据
在客户端应用调用了p_get_des_crypt_test_data过程之后,我们让他们调用清单 6-14 中所示的方法getDecryptData()。
***清单 6-14。*建立秘密密码并解密数据,getDecryptData()
public static final String getDecryptData( RAW cryptData, RAW cryptSecretDESPassPhrase, RAW cryptSecretDESAlgorithm, RAW cryptSecretDESSalt, RAW cryptSecretDESIterationCount ) { String rtrnString = "getDecryptData() A failed"; try { if( ( null == sessionSecretDESKey ) || **testAsClientAndServer** ) { decryptSessionSecretDESPassPhrase( cryptSecretDESPassPhrase, cryptSecretDESAlgorithm, cryptSecretDESSalt, cryptSecretDESIterationCount ); makeSessionSecretDESKey(); } rtrnString = **getDecryptData**( cryptData ); } catch( Exception x ) { x.printStackTrace(); } return rtrnString; }
我们在try块中做的第一件事是测试sessionSecretDESKey是否已经被实例化。如果没有,那么我们调用两个方法:decryptSessionSecretDESPassPhrase()(在下一节讨论)和makeSessionSecretDESKey()。我们在本章前面讨论了makeSessionSecretDESKey()——它与我们最初在 Oracle 数据库上调用的构建密码密钥的方法相同。我们在客户端再次调用它来构建一个相同的密钥。
当我们测试我们是否已经有了sessionSecretDESKey,我们也测试了boolean testAsClientAndServer。除非我们从main()方法测试OracleJavaSecure类,否则testAsClientAndServer boolean总是false。在main()中,当我们将此boolean设置为true时,我们可以在测试的不同阶段用来自 Oracle 数据库的密钥替换本地生成的 DES 加密密码密钥。我们将在本章稍后检查main()方法的代码。
getDecryptData()方法重载了一个版本,该版本假设已经构建了秘密密码密钥并进行解密。它接受一个RAW并将明文作为一个String返回。第一个getDecryptData()方法(如上所示)调用第二个getDecryptData()方法,参见清单 6-15 。
***清单 6-15。*用已有的密码解密数据,getDecryptData()
public static final String getDecryptData( RAW cryptData ) { if( null == cryptData ) return null; String rtrnString = "getDecryptData() B failed"; try { cipherDES.init( Cipher.DECRYPT_MODE, sessionSecretDESKey, paramSpec ); rtrnString = new String( cipherDES.doFinal( cryptData.getBytes() ) ); } catch( Exception x ) { //x.printStackTrace(); //rtrnString = x.toString(); } return rtrnString; }
这个相同的第二个getDecryptData()方法也被调用来解密 Oracle 数据库上的数据,用于来自客户端的加密数据插入和更新。在 Oracle 数据库中,我们大概知道我们已经有了 DES 秘密密码密钥。
使用 RSA 私钥解密 DES 密码
decryptSessionSecretDESPassPhrase()方法使用客户机的 RSA 私钥来解密服务器 DES 秘密密码密钥的所有工件。代码显示在清单 6-16 中。
***清单 6-16。*解密秘密密码钥匙神器,decryptSessionSecretDESPassPhrase()
private static void decryptSessionSecretDESPassPhrase( RAW cryptSecretDESPassPhrase, RAW cryptSecretDESAlgorithm, RAW cryptSecretDESSalt, RAW cryptSecretDESIterationCount ) throws Exception
{ cipherRSA.init( Cipher.DECRYPT_MODE, locRSAPrivKey ); **byte[] cryptBytes;** cryptBytes = cryptSecretDESPassPhrase**.getBytes();** sessionSecretDESPassPhraseChars = **byteArrayToCharArray**( cipherRSA.doFinal( cryptBytes ) ); cryptBytes = cryptSecretDESAlgorithm.getBytes(); sessionSecretDESAlgorithm = **new String**( cipherRSA.doFinal( cryptBytes ) ); cryptBytes = cryptSecretDESSalt.getBytes(); salt = cipherRSA.doFinal( cryptBytes ); cryptBytes = cryptSecretDESIterationCount.getBytes(); iterationCount = cipherRSA.doFinal( cryptBytes )**[0];** //System.out.println( "\n" + new String( sessionSecretDESPassPhraseChars ) ); //System.out.println( sessionSecretDESAlgorithm ); //System.out.println( new String( salt ) ); //System.out.println( iterationCount ); }
对于每个工件,我们将加密的RAW转换成一个字节数组,并将其传递给cipherRSA成员进行解密。我们使用从Cipher返回的字节数组,用适当的数据类型填充静态类成员。通过调用byteArrayToCharArray()方法将值保存为salt的字节数组,通过实例化new String()将值保存为sessionSecretDESPassPhraseChars的字符数组,将值保存为sessionSecretDESAlgorithm的String,并将单个byte自动转换为iterationCount的int。
如果您有兴趣在这些特定于会话的随机工件和协商的算法到达客户端时观察它们,您可以在方法末尾取消对System.out.println()调用的注释。然而,您应该只是暂时这样做:在下一章的代码中已经删除了System.out.println()调用。
数组转换的辅助方法
在前面代码的两个地方,我们调用了一些我们在OracleJavaSecure代码中定义的辅助数组转换方法,如清单 6-17 所示。一个是字节数组,将其转换为字符数组。另一个则相反。我们在得到sessionSecretDESPassPhraseChars时从decryptSessionSecretDESPassPhrase()(见清单 6-16 调用byteArrayToCharArray(),在加密sessionSecretDESPassPhraseChars时从getCryptSessionSecretDESPassPhrase()(见清单 6-5 )调用charArrayToByteArray()。
***清单 6-17。*数组转换方法,byteArrayToCharArray()和charArrayToByteArray()
` static char[] byteArrayToCharArray( byte[] bytes ) { char[] rtrnArray = new char[bytes.length]; for ( int i = 0; i < bytes.length; i++ ) { rtrnArray[i] = ( char )bytes[i]; } return rtrnArray; }
static byte[] charArrayToByteArray( char[] chars ) {
byte[] rtrnArray = new byte[chars.length]; for ( int i = 0; i < chars.length; i++ ) {
rtrnArray[i] = ( byte )chars[i];
}
return rtrnArray;
}`
我们通常可以通过强制转换来来回转换这些数组类型,如下例所示。然而,我们需要意识到缩小和扩大转换的含义。我们必须将数组中的char值限制为标准 ASCII 字符,而不是 16 位 Unicode 字符,以便在转换中不丢失信息。
byte[] bAr = new byte[10]; char[] cAr = (char[])bAr; bAr = (byte[])cAr;
JDeveloper IDE(可能还有其他地方)不支持这种数组转换,所以我们将依赖于我们的辅助方法。JDeveloper 很好,因为它是免费的,而且它是为使用 Oracle 数据库而高度定制的;它比任何其他 IDE 更好地处理 Oracle 视图。您可以在 Oracle 公司网站上找到 JDeveloper,网址为[www.oracle.com](http://www.oracle.com)。
您可能想知道为什么我们将密码短语维护为一个 char 数组。这是我们构建PBEKeySpec时需要的格式。
用于显示实际算法的方法
清单 6-18 展示了showAlgorithm()方法。这实际上是重复的功能。看一下decryptSessionSecretDESPassPhrase()方法的代码(如前所示),您会看到我们将sessionSecretDESAlgorithm从 Oracle 数据库发送到客户端的String,我们可以简单地打印出来。
直接从 Oracle 数据库中选择(通过函数f_show_algorithm)的唯一额外保证是在传输过程中不会混淆。我们已经在app_sec_pkg中构建了调用该方法返回算法名称的函数。我们也可以从客户端调用这个方法(在调用服务器之前)并比较使用的算法。
***清单 6-18。*显示正在使用的密码算法名称,showAlgorithm()
public static final String showAlgorithm() { String rtrnString = "showAlgorithm failed"; try { rtrnString = sessionSecretDESKey.getAlgorithm(); } catch( Exception x ) { rtrnString = x.toString(); } finally { return rtrnString; } }
这是一个临时的测试方法,我们将在后面的章节中从代码中删除它和调用它的 Oracle 函数。
仅在客户端测试 DES 加密
我们将再次通过从OracleJavaSecure的main()方法中调用我们的方法来进行我们的仅客户端测试。main()第一部分的代码如清单 6-19 中的所示。从获取我们的客户机公钥模数和指数开始,在这个过程中生成 RSA 公钥/私钥对(如果不存在的话)。
***清单 6-19。*仅用于客户端测试的代码,来自main()
` String clientPubModulus = getLocRSAPubMod(); String clientPubExponent = getLocRSAPubExp();
// Emulates server actions RAW mCryptSessionSecretDESPassPhrase = getCryptSessionSecretDESPassPhrase( clientPubModulus, clientPubExponent ); RAW mCryptSessionSecretDESSalt = getCryptSessionSecretDESSalt( clientPubModulus, clientPubExponent ); RAW mCryptSessionSecretDESAlgorithm = getCryptSessionSecretDESAlgorithm( clientPubModulus, clientPubExponent ); RAW mCryptSessionSecretDESIterationCount = getCryptSessionSecretDESIterationCount( clientPubModulus, clientPubExponent ); RAW cryptData = getCryptData( "Monday" );
testAsClientAndServer = true;
// As client System.out.println( getDecryptData( cryptData, mCryptSessionSecretDESPassPhrase, mCryptSessionSecretDESAlgorithm, mCryptSessionSecretDESSalt, mCryptSessionSecretDESIterationCount ) ); System.out.println( showAlgorithm() );`
接下来,我们模拟服务器,接收模数和指数,从getCryptSessionSecretDESPassPhrase()和其他方法获得 DES 秘密密码密钥工件。在该过程中,从工件构建公钥的副本,生成 DES 秘密密码密钥,并且使用 RSA 公钥加密每个秘密密码密钥工件。
我们还模拟服务器,用秘密密码密钥加密一些数据:
RAW cryptData = getCryptData( "Monday" );
我们测试的下一步假设我们回到了客户端,并且已经收到了所有加密的秘密密码密钥工件,我们从这些工件中生成了一个秘密密码密钥的副本。为此,我们将testAsClientAndServer设置为true,以覆盖我们刚刚在模拟服务器时创建的秘密密码密钥(即使密钥是相同的):
testAsClientAndServer = true;
现在再次作为客户端,我们用所有的 DES 秘密密码密钥工件和加密数据调用getDecryptData()。这将基于工件创建一个新的 DES 密钥,然后使用该密钥解密数据。我们将打印出解密的数据,这些数据应该与我们之前加密的数据相同。此外,我们将打印出用于加密密码的 DES 算法名称。
运行代码
我们假设您按照上一章中的步骤运行了代码。本章将使用相同的程序。在命令提示符下,将目录切换到第六章。使用以下命令编译代码:
javac orajavsec/OracleJavaSecure.java
如果您有任何问题,请参考第三章中关于在命令提示符下编译和设置您的环境CLASSPATH以包含 ojdbc6.jar 的说明。然后使用以下命令运行同一目录中的代码:
java orajavsec.OracleJavaSecure.OracleJavaSecure
观察结果
前面部分中发出的命令将会打印出以下两行:
Monday PBEWithSHA1AndDESede
在模拟服务器时,我们使用 DES 加密密码密钥对字符串“Monday”进行了加密,并将加密的数据以及使用客户端 RSA 公钥加密的加密密码密钥工件传递回客户端。回到客户端,我们使用工件构建了一个复制的 DES 密钥,并解密了加密的数据。我们打印出解密后的数据,在命令提示符下看到了“星期一”。然后我们打印了协商好的算法。如果您在工作站上使用的是 JDK 1.6 或更高版本,您将会看到PBEWithSHA1AndDESede;但是,如果你使用的是 JDK 1.5,你会看到PBEWithMD5AndDES。
编码测试客户端/服务器秘密密码加密
下一行位于我们在 OracleJavaSecure.java 的 Java 代码的顶部。取消对它的注释,并将整个代码复制到您的 Oracle 客户端。
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/OracleJavaSecure" AS
为了安全起见,向下滚动到类主体,确保在连接字符串中没有有效的密码。如果是这样,请在 Oracle 中执行该命令之前,从连接字符串中删除密码。
private static String appsecConnString = "jdbc:oracle:thin:AppSec/**password**@localhost:1521:Orcl";
在您的 Oracle 客户端(如 SQL*Plus)中执行脚本,将 Java 代码加载到 Oracle 数据库中。正如我们所看到的,这个命令将 Java 代码加载到 Oracle 数据库中并进行编译。
设置测试服务器和客户端的代码
为了在您的客户机上编译和执行OracleJavaSecure,我们取消注释以在 Oracle 数据库上运行的第一行需要被注释:
//CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/OracleJavaSecure" AS
向下滚动到类主体,并在连接字符串中设置密码。还要更正连接字符串的任何其他地址和名称。
private static String appsecConnString = "jdbc:oracle:thin:AppSec/**password**@localhost:1521:Orcl";
还将testingOnServer boolean设置为true:
private static boolean testingOnServer = **true;**
保存文件。
在本章的前面,您可能已经在 Oracle 上执行了app_sec_pkg包规范和主体。如果您还没有这样做,现在就做吧。这将创建我们需要做秘密密码加密的 Oracle 结构。
考虑 main()方法的服务器部分
这次当我们运行OracleJavaSecure的main()方法时,我们将通过testingOnServer测试,因此我们将执行main()的剩余部分,如清单 6-20 所示。我们声明了几个成员变量来保存从 Oracle、errNo和errMsg返回的错误号和错误消息。
因为我们是从客户端运行的(不是在 Oracle 数据库上),所以我们需要加载特定于 Oracle 的驱动程序(假设我们可能没有使用 JDK 1.6 或更高版本)。我们将设置 Oracle 连接以供使用:注意,我们将作为appsec用户进行连接。
我们将使用特定于 Oracle 的OracleCallableStatement,它允许我们从 Oracle 检索OUT参数,并传输特定于 Oracle 的数据类型。
***清单 6-20。*客户端/服务器测试代码,来自main()
if( testingOnServer ) { int **errNo;** String **errMsg;** // Since not on the Server, must load Oracle-specific Driver Class.forName( "oracle.jdbc.driver.OracleDriver" ); // This will set the static member "conn" to a new Connection conn = DriverManager.**getConnection**( appsecConnString ); **OracleCallableStatement stmt;**
从 Oracle 获取 DES 加密口令
在清单 6-21 的中,我们的第一个过程调用是对p_get_shared_passphrase。这将简单地测试我们的客户机和 Oracle 之间的 RSA 和 DES 密钥的交换。我们将我们的 RSA 公钥模数和指数传递给这个过程,作为回报,我们得到了由 Oracle 数据库使用公钥加密的 DES 秘密密码密钥工件。注意,我们注册了OUT参数,并设置或setNull了我们所有的参数。
***清单 6-21。*从main() 获取共享密码
stmt = ( OracleCallableStatement )conn.prepareCall( "CALL app_sec_pkg**.p_get_shared_passphrase**(?,?,?,?,?,?,?,?)" ); ` stmt**.registerOutParameter**( 3, OracleTypes.RAW );
stmt.registerOutParameter( 4, OracleTypes.RAW );
stmt.registerOutParameter( 5, OracleTypes.RAW );
stmt.registerOutParameter( 6, OracleTypes.RAW );
stmt.registerOutParameter( 7, OracleTypes.NUMBER );
stmt.registerOutParameter( 8, OracleTypes.VARCHAR );
stmt.setString( 1, clientPubModulus );
stmt.setString( 2, clientPubExponent );
stmt.setNull( 3, OracleTypes.RAW );
stmt.setNull( 4, OracleTypes.RAW );
stmt.setNull( 5, OracleTypes.RAW );
stmt.setNull( 6, OracleTypes.RAW );
stmt.setInt( 7, 0 );
stmt.setNull( 8, OracleTypes.VARCHAR );
stmt.executeUpdate();
errNo = stmt.getInt( 7 ); if( errNo != 0 ) { errMsg = stmt.getString( 8 ); System.out.println( "Oracle error " + errNo + ", " + errMsg ); System.out.println( (stmt.getRAW( 3 )).toString() ); } else { mCryptSessionSecretDESSalt = stmt.getRAW( 3 ); mCryptSessionSecretDESIterationCount = stmt.getRAW( 4 ); mCryptSessionSecretDESAlgorithm = stmt.getRAW( 5 ); mCryptSessionSecretDESPassPhrase = stmt.getRAW( 6 ); } if( null != stmt ) stmt.close();`
在执行该语句后,我们得到从 Oracle 数据库返回的错误号,errNo,并确保它为 0。如果没有,我们报告错误。
如果没有错误,那么我们作为客户端应用,设置一个与每个加密工件相等的方法成员,比如mCryptSessionSecretDESSalt。现在,我们拥有了构建 Oracle 数据库上生成的密码密钥的精确副本所需的一切。至此,我们已经完全交换了钥匙;但是,我们还没有在客户机上建立 DES 加密口令密钥的副本。在这段代码的后面,我们将使用我们的私钥解密每个工件,然后构建秘密的密码密钥,并使用它与 Oracle 交换加密的数据。
因为我们已经完成了这个语句,所以我们关闭它。这是一个一致性和勤奋的问题,在我们结束声明之前,我们保证它不是null。实际上,如果stmt是null,我们会在第一个registerOutParameter()调用的早期代码中抛出一个异常。
查看基于密码加密的协商算法
我们调用函数,f_show_ algorithm 来显示 Oracle 数据库选择的算法名(参见清单 6-22)。因为 Oracle 11g JVM 是基于标准 JVM 版本 1.5 的,所以我们将看到 Oracle 数据库在这方面表现出了前面提到的错误,并选择了PBEWithMD5AndDES作为协议,而不是我们所要求的PBEWithSHA1AndDESede。这将作为与客户端协商的通用算法。
***清单 6-22。*显示算法,来自main()
stmt = ( OracleCallableStatement )conn.prepareCall( "{**?** = call app_sec_pkg.**f_show_algorithm**}" ); stmt**.registerOutParameter( 1**, OracleTypes.VARCHAR ); stmt.executeUpdate(); **System.out.println( stmt.getString(1) );** if( null != stmt ) stmt.close();
这里要注意的是调用 Oracle 函数的语法,而不是过程。该函数总是返回值。“领头的?= "代表返回值,我们的语句参数(1)就是那个值。准备好的可调用语句中的每个问号都是一个参数,无论它位于过程名或函数名之前还是之后。参数总是从左到右编号,1(一)是第一个。
此函数调用中使用的格式(带有左花括号和右花括号)称为 SQL92 语法。这是对 1992 年采用的 SQL 国际标准的引用。另一种可用于调用存储过程和函数的语法形式是 PL/SQL 块语法(带有begin和end语句)。我发现,对于 Oracle 驱动程序的旧版本(例如 jdbc14.jar )(虽然没有 SQL92 旧), PL/SQL 块语法可以工作,而 SQL92 语法则不行。以下是来自清单 6-22 中的相同prepareCall()方法调用的 PL/SQL 块语法:
stmt = ( OracleCallableStatement )conn.prepareCall( "begin ? = call app_sec_pkg.f_show_algorithm; end;" );
调用 Oracle 数据库获取加密数据
接下来,我们要演示如何从 Oracle 数据库取回加密的数据,并在客户机上使用 DES 加密密码密钥的本地副本对其进行解密。在清单 6-23 的中,你可以看到我们称这个过程为p_get_des_crypt_test_data。同样,我们传递我们的公钥工件,并检索加密的秘密密码密钥工件。因为这个过程是我们将在本书的剩余部分看到重复的,我将称之为“密钥交换”。我们刚刚从另一个检索秘密密码密钥工件的过程中返回,所以我们不需要再次设置我们的方法成员——这些行被注释掉了。请注意,所有这些调用都发生在同一个 Oracle 会话中,因此使用现有的密钥—没有额外的密钥生成。
***清单 6-23。*获取 DES 的地穴测试数据,来自main()
stmt = ( OracleCallableStatement )conn.prepareCall( "CALL app_sec_pkg**.p_get_des_crypt_test_data**(?,?,?,?,?,?,?,?,?,?)" ); stmt.registerOutParameter( 3, OracleTypes.RAW ); stmt.registerOutParameter( 4, OracleTypes.RAW ); stmt.registerOutParameter( 5, OracleTypes.RAW ); stmt.registerOutParameter( 6, OracleTypes.RAW ); stmt.registerOutParameter( 7, OracleTypes.NUMBER ); stmt.registerOutParameter( 8, OracleTypes.VARCHAR ); stmt.registerOutParameter( 10, OracleTypes.RAW ); stmt.setString( 1, clientPubModulus ); stmt.setString( 2, clientPubExponent ); stmt.setString( 9, **"Tuesday"** ); stmt.setNull( 3, OracleTypes.RAW ); stmt.setNull( 4, OracleTypes.RAW ); stmt.setNull( 5, OracleTypes.RAW ); stmt.setNull( 6, OracleTypes.RAW ); stmt.setInt( 7, 0 ); stmt.setNull( 8, OracleTypes.VARCHAR ); stmt.setNull( 10, OracleTypes.RAW ); stmt.executeUpdate(); errNo = stmt.getInt( 7 ); if( errNo != 0 ) { errMsg = stmt.getString( 8 ); System.out.println( "Oracle error " + errNo + ", " + errMsg ); System.out.println( (stmt.getRAW( 10 )).toString() ); } else { **//mCryptSessionSecretDESSalt** = stmt.getRAW( 3 ); //mCryptSessionSecretDESIterationCount = stmt.getRAW( 4 ); //mCryptSessionSecretDESAlgorithm = stmt.getRAW( 5 ); //mCryptSessionSecretDESPassPhrase = stmt.getRAW( 6 ); **cryptData = stmt.getRAW( 10 );** **System.out.println( getDecryptData( cryptData,** mCryptSessionSecretDESPassPhrase, mCryptSessionSecretDESAlgorithm, mCryptSessionSecretDESSalt, mCryptSessionSecretDESIterationCount ) ); } if( null != stmt ) stmt.close();
除了密钥交换,我们还将字符串“Tuesday”发送给p_get_des_crypt_test_data过程,以便在 Oracle 数据库上使用秘密密码密钥进行加密。因此,在执行了Statement之后,我们检索加密的数据,然后通过在客户端本地调用getDecryptData()来解密数据,并打印出解密后的String。注意,getDecryptData()方法获取所有加密的秘密密码密钥工件。
如果秘密密码密钥还没有建立,那么getDecryptData()调用makeSessionSecretDESKey()。当我们调用getDecryptData()时,我们传递足够的参数,即秘密密码密钥的工件,来构建密钥;但是,如果已经建成,我们就不重复这项工作。我们可能会多次调用getDecryptData()来获得多条加密数据,但是构建秘密密码密钥的工作将只进行一次。
测试 Oracle 数据库加密和本地解密数据
清单 6-24 中的下一个测试更加简洁,尽管不太现实。我们将调用临时函数f_get_crypt_data来获取代表明文String“星期三”的加密数据。我们将从语句中取回加密的数据RAW,并在本地调用getDecryptData()方法来解密它,打印结果。
***清单 6-24。*获取地穴数据,来自main()
stmt = ( OracleCallableStatement )conn.prepareCall( "{? = call app_sec_pkg**.f_get_crypt_data**(?) }" ); stmt.registerOutParameter( 1, OracleTypes.RAW ); stmt.setString( 2, **"Wednesday"** ); stmt.executeUpdate(); **cryptData = stmt.getRAW( 1 );**
**System.out.println( getDecryptData( cryptData,** mCryptSessionSecretDESPassPhrase, mCryptSessionSecretDESAlgorithm, mCryptSessionSecretDESSalt, mCryptSessionSecretDESIterationCount ) ); if( null != stmt ) stmt.close();
通常我们不会调用 Oracle 数据库来加密来自客户端的明文数据,然后在客户端解密它以获得明文。然而,这正是我们在这里所做的。
向 Oracle 发送加密数据
在清单 6-25 中的最后一个测试中,我们将在客户端加密明文数据“星期四”。我们用 DES 秘密密码密钥的副本加密它,基于我们从 Oracle 得到的工件。然后我们通过调用f_get_decrypt_data函数将加密的数据提交给 Oracle 数据库。Oracle 数据库将使用原始密码密钥解密数据,然后我们的客户机将读取作为参数 1 返回的明文String。我们打印结果并关闭Statement。
***清单 6-25。*获取解密数据,来自main()
**cryptData = getCryptData( "Thursday" );** stmt = ( OracleCallableStatement )conn.prepareCall( "{? = call app_sec_pkg**.f_get_decrypt_data(?)** }" ); stmt.registerOutParameter( 1, OracleTypes.VARCHAR ); **stmt.setRAW( 2, cryptData );** stmt.executeUpdate(); **System.out.println( stmt.getString( 1 ) );** if( null != stmt ) stmt.close();
这是另一个不太可能的场景,我们调用一个 Oracle 函数来解密数据以便在客户机上使用。不要担心向客户端暴露不需要的功能;我们将继续解决这个问题。事实上,我们已经有了!我们的客户端应用不会作为appsec用户连接,只有appsec能够执行app_sec_pkg包中的过程和功能。
测试我们的安全客户端/服务器数据传输
在命令提示符下,将目录切换到第六章。使用以下命令编译代码:
javac orajavsec/OracleJavaSecure.java
同样,如果你有任何问题,参考第三章关于在命令提示符下编译和设置你的环境CLASSPATH包括 ojdbc6.jar 的指导。然后使用以下命令运行同一目录中的代码:
java orajavsec.OracleJavaSecure
结果将打印以下六行:
Monday PBEWithSHA1AndDESede PBEWithMD5AndDES Tuesday Wednesday
Thursday
前两行显示了我们在本章前面执行的相同的仅客户端测试。之后,因为testingOnServer是true,main()方法继续从 Oracle 获取 DES secret 密码密钥。然后我们调用f_show_algorithm并在命令提示符下显示协商好的算法,很可能是PBEWithMD5AndDES。
之后,我们通过p_get_des_crypt_test_data过程将字符串“星期二”发送到 Oracle 数据库进行加密。我们读取加密的数据,用秘密密码密钥的副本解密,并显示“星期二”。
我们也直接调用函数f_get_crypt_data,传递给它明文字符串“星期三”。我们读取函数返回的加密数据,用秘密密码密钥的副本再次解密,并显示“星期三”。到目前为止,很明显,我们的 Oracle 连接会话正在为多个查询保留和重用相同的 RSA 和 DES 密钥。
最后,我们在客户机上加密字符串“Thursday”,并将其提交给 Oracle 函数f_get_decrypt_data。Oracle 数据库使用原始密码密钥解密数据。我们读取返回的明文字符串并打印出来,“星期四。”
章节回顾
我们发现自己正处于另一章的末尾。回顾过去,让我们看看我们走过了哪些路。
- 我们学习了 DES 秘密密码加密。特别是,我们了解了组成秘密密码密钥的各种工件:通行短语、salt、迭代计数和算法。我们还设计了一种方法来观察、报告和解决 JCE 的一个 bug,甚至编码来适应最终的 JVM 升级,而不会有这个 bug。
- 我们发现了如何在 Oracle 上生成密码密钥,进行密钥交换,然后在客户机上构建一个相同的密钥。使用相同的密钥(共享密码),我们可以交换加密数据。我们还看到了如何使用 RSA 公钥来加密 DES 密钥,这样我们就可以在 Oracle 数据库和客户机之间交换它,但仍然保持它的保密性。
- 我们广泛使用了
SecureRandom类,以便为每个会话生成一个随机的密码短语、salt 和迭代计数。 - 我们研究了 Oracle 包,这是我们在组织和安全性方面所依赖的东西。
图 6-1 和 6-2 展示了我们在本章中介绍的秘密密码加密过程。这些过程将贯穿本书的其余部分。在图 6-1 的顶部,你会看到我们指的是图 5-1 中的**【A】**——在那里我们看到了在客户端生成 RSA 公钥/私钥对的标准流程。当我们调用p_get_shared_pass_phrase时,我们将公钥指数和模数传递给 Oracle 数据库。您可以在图 6-1 中看到,在最右边,一个等价的公钥建立在 Oracle 数据库上,由标记为 RSA 的密钥图像表示。最右边的另一个密钥图像,标记为 DES,描述了我们在 Oracle 数据库上创建的共享密码密钥。
共享密码密钥的每个工件都在 Oracle 数据库上用公钥加密,并返回给客户机。每个加密的工件都放在p_get_shared_pass_phrase过程的一个OUT参数中。我们已经概述了整个过程,并在图 6-1 中将其标记为**【B】**区块。这是我们交换共享密码密钥的标准过程,我们将在以后的图中引用它。
在图 6-1 的底部,我们说明了客户端如何调用方法来解密秘密密码密钥工件并构建一个等价的秘密密码密钥。底部的密钥图像代表客户端上的密码密钥。
***图 6-1。*密钥交换
在图 6-2 中,您可以看到在客户端和 Oracle 数据库之间交换加密数据的示例流程图。在图 6-2 的前三分之一,您可以看到在 Oracle 数据库上进行密钥交换和数据加密的过程。加密的数据在p_get_des_crypt_test_data过程的OUT参数中返回给客户端。这里我们指的是图 6-1 的【B】块,在这里我们看到了在 Oracle 数据库上构建并返回给客户端的密码密钥。在图 6-2 中,除了建立密码密钥,我们还使用该密钥对以加密形式返回给客户端的数据进行加密。
在图 6-2 的中间部分,通过调用getDecryptData()方法(两个同名方法的版本 A ,我们看到了客户端如何解密来自 Oracle 数据库的数据的图示。我们概述了图 6-2 的这一部分,并将其标记为块**【C】**——它说明了解密数据的标准过程。您可以看到,在解密过程中,在客户机上建立了一个等价的加密密码密钥,标记为 DES(根据需要)。
图 6-2 的最后一部分,在底部显示了这个过程的镜像。在这个例子中,客户机使用已经构建好的等价密码密钥来加密数据。然后通过调用f_get_decrypt_data函数将加密的数据发送到 Oracle 数据库。然后,Oracle 数据库使用原始密码密钥解密数据。在我们的示例代码中,解密后的明文数据被返回给客户端进行显示。
***图 6-2。*加密数据交换示例
七、传输中的数据加密
在第六章中,我们为 Oracle 数据库和 Java 客户端之间的数据加密奠定了基础。我们证明了我们可以安全地交换密钥,然后来回发送加密数据,成功地在接收方和 Oracle 数据库中解密数据。
在本章中,我们将完成加密的基础,我们将继续在应用安全中构建加密,appsec Oracle 模式。然后,我们将扮演应用开发人员的角色,使用appsec结构来保护对我们数据的访问。具体来说,我们将保护对HR示例模式中数据的访问。
考虑最后一章,观察我们作为appsec用户构建和测试我们的应用安全结构和代码。并不打算让每个应用都作为appsec运行。相反,我们将允许每个需要我们安全性的应用执行我们的安全功能,我们将在本章中演示这一点。
此外,我们需要为开发人员提供对 Java 结构的访问,以包含在他们的桌面应用中。我们将在本章末尾讨论这一点。
安全管理员活动
我们的安全管理员secadm需要提供更多的权限。有些权限是系统特权,有些是授予appsec和HR模式中的包的。
注你可以在名为chapter 7/sec ADM . SQL的文件中找到以下命令的脚本。
以 SECADM 用户身份连接到 Oracle 数据库,并获得安全应用角色secadm_role。
EXECUTE sys.p_check_secadm_access;
我们将在appsec模式中为记录错误创建一个表。我们还将创建一个与该表相关联的触发器。我们的触发器就像一个在特定事件发生时运行的过程——在我们的例子中,当一条记录被插入到我们的表中时,我们的触发器就会运行。
我们希望有一个应用错误的中央表,因为错误消息可能会返回给几十或几百个应用。作为应用安全管理员,我们如何从所有这些来源获得报告?如果我们的应用开发人员是认真的,他们会让我们知道他们看到了什么问题,但我们不指望这种情况会发生。我们将从我们的远程监听站——错误表——监控错误。
授予应用安全用户更多的系统权限
为了成功设置错误日志表,我们需要允许appsec将数据存储在表空间中。默认的表空间是“USERS”,这就足够了。我们需要指定appsec可以使用多少空间,一个配额。我们将开始允许两兆字节的空间。以appsec的身份执行以下操作:
ALTER USER appsec DEFAULT TABLESPACE USERS QUOTA 2M ON USERS;
此外,为了让appsec能够创建触发器,我们需要授予CREATE TRIGGER系统特权。我们将把它授予她的非默认角色(她只是偶尔需要它):
GRANT CREATE TRIGGER TO appsec_role;
允许用户在其他模式下执行包
我们希望HR执行appsec安全结构。我们希望创建一个角色,我们可以向其授予对包的执行权限,然后将该角色授予任何需要它的人。然而,让我们来看看为什么这种方法并不总是有效。请考虑以下不应该执行的语句:
--CREATE ROLE appsec_user_role NOT IDENTIFIED; --GRANT EXECUTE ON appsec.app_sec_pkg TO appsec_user_role; --GRANT appsec_user_role TO hr;
具体来说,当过程、函数和包调用其他模式中的过程、函数和包时,这里说明的方法不起作用。为了超前一点,我们将在HR模式中创建我们希望用来执行app_sec_pkg包的过程(你知道,我们希望HR调用函数来加密数据)。
问题是HR过程、函数和包不能从角色获得特权。这是一个限制(基于依赖模型),旨在防止每次我们注销或设置角色时HR过程失效。我们通过将对app_sec_pkg包的执行权直接授予HR用户来弥补这个限制。例如,执行以下代码:
GRANT EXECUTE ON appsec.app_sec_pkg TO hr;
与此形成鲜明对比的是,我们的应用用户appusr和其他应用用户将根据需要直接调用HR模式中的过程、函数和包。我们不想象appusr从他们自己的过程调用我们的过程。因此,我们可以将对我们的HR安全包hr_sec_pkg的访问权授予appusr所拥有的角色hrview_role。下面是 GRANT 语句,我们将在创建hr.hr_sec_pkg包之后执行它:
--GRANT EXECUTE ON hr.hr_sec_pkg TO hrview_role;
稍后我们将执行一个名为 HR.sql 的脚本,该脚本创建 hr.hr_sec_pkg 包,并执行前面的 GRANT 语句。
应用安全用户活动
我们将创建一个错误日志表和一个插入触发器,我们还将添加一些程序来记录到app_sec_pkg包中。
注意你可以在名为chapter 7/appsec . SQL的文件中找到以下命令的脚本。
以appsec用户身份连接到 Oracle 数据库,并将您的角色设置为非默认角色appsec_role:
SET ROLE appsec_role;
创建错误记录表
接下来,创建一个错误日志记录表。您可能希望发挥 DBA 的技能,或者让 DBA 帮助您定义该表,设置其性能参数并估计初始存储和增长计划,这些都没有在此处定义。执行清单 7-1 中的代码,使用默认值创建表格。
***清单 7-1。*创建应用安全错误日志表,t_appsec_errors
CREATE TABLE appsec.t_appsec_errors ( err_no NUMBER, err_txt VARCHAR2(2000), msg_txt VARCHAR2(4000) **DEFAULT** NULL, update_ts DATE **DEFAULT** SYSDATE );
我们将捕获 Oracle 错误号err_no和文本err_txt,并为自己提供另一个字段msg_txt,用于提供有用的信息(例如,方法名或堆栈跟踪)。我们还将捕获错误的时间,update_ts,这在两个方面有所帮助:首先,我们想知道事情是什么时候发生的,或者正在发生什么;第二,当日志记录太旧而不再有用时,我们希望将其丢弃。
注意,清单中最后两个列定义使用关键字DEFAULT指定默认值。要插入记录,只需插入前两个字段。第三个默认为NULL,第四个默认为 Oracle 数据库上的当前日期和时间SYSDATE。事实上,我们不想在UPDATE_TS中插入日期。我们希望接受默认设置。因为appsec是唯一一个将在该表中输入数据的人(这是一个用于应用安全的错误日志,不是用于一般用途),我们不需要实施默认的UPDATE_TS。
为了按日期完成记录的排序和选择,我们将在UPDATE_TS列建立一个索引。执行清单 7-2 中的代码来创建索引。
***清单 7-2。*为应用安全错误日志表的索引,t_appsec_errors
CREATE INDEX i_appsec_errors00 ON appsec.t_appsec_errors ( update_ts );
索引一旦创建,就会在您插入或更新行时自动维护。此外,当您从表中选择记录时,会自动使用它们。使用哪个索引是 Oracle 数据库做出的逻辑选择,如果需要的话,可以用提示覆盖。需要注意的一点是,没有提到索引前导列的选择查询不会直接从索引中受益。例如,如果我们从t_appsec_errors表中选择所有行,其中msg_txt列包含字符串“Exception”,我们就不会直接使用我们刚刚创建的索引。如果我们在列(update_ts, msg_txt)上创建一个索引,这个索引也不会直接有利于我们的查询。当我们从msg_txt中进行选择时,为了获得索引的直接好处,我们希望创建一个索引,将msg_txt作为第一列,例如(msg_txt, update_ts)。
Oracle 数据库优化器实际上可以通过执行跳过扫描来使用任何索引来提高查询的性能。跳过扫描可以提高简单查询的性能,潜在地减少对全表扫描的依赖(全表扫描会导致非常差的性能)。还可以使用 skip scan 提示显式命名要使用的索引。从跳过扫描提示中获得最佳性能可能需要一些反复试验和一些工程设计。下面是一个摘自 Oracle 文档的示例。带有加号和提示名称的注释部分( / / )作为对优化器的提示。
SELECT **/*+ INDEX_SS(e emp_name_ix) */** last_name FROM employees e WHERE first_name = 'Steven';
注意 skip scan 提示,INDEX_SS指示优化器使用索引emp_name_ix进行查询。即使我们选择了first_name列为‘史蒂文’的记录,我们也要求受益于last_name、first_name的索引。为了更好地理解跳过搜索优化,我建议做一个互联网搜索的例子。
您可能希望根据不同的列对t_appsec_errors表进行排序或选择,但是除非有一个频繁的查询需要对该列进行排序,否则您不需要索引。因为这个表实际上只用于错误后的故障排除,所以我们不期望有其他索引—我们将总是选择最近的记录(基于update_ts)。
我们将为准备授予 select 权限的表创建一个视图;但是,我们目前还没有想到任何可能需要查看它的人。也许将来我们会有一个精明的应用开发者,他想帮助调试他的应用对appsec包的使用。我们可以安排她从我们的视野中选择。执行以下操作:
CREATE OR REPLACE VIEW appsec.v_appsec_errors AS SELECT * FROM appsec.t_appsec_errors;
创建一个表格来管理我们的错误日志表
请记住,我们只给应用安全性用户appsec在USERS表空间中提供了两兆字节的空间。创建一个表,尤其是一个日志表,而不提供任何定期清理和维护,即使不是疏忽,也是不体谅人的。
现在告诉你一个秘密:我们正在制造一个机器人。不是用来取咖啡的机械装置,而是帮助我们管理错误日志表的软件哨兵,尤其是在我们不注意的时候。我们将用一个触发器自动删除表中的旧记录,每当我们向表中插入一条记录时,该触发器就会运行。
棘手的是,管理我们的表涉及到一些工作,所以我们希望最小化管理任务发生的频率。事实上,我们只想每天管理一次错误日志表。我们也不介意知道表最后一次被管理是什么时候。实现这两个目标的最好方法是,每当管理错误日志表时,创建另一个表来存储日期。执行清单 7-3 中的代码,创建错误日志维护表。
***清单 7-3。*创建应用安全错误日志表和索引,t_appsec_errors_maint
CREATE TABLE appsec.t_appsec_errors_maint ( update_ts DATE DEFAULT SYSDATE ); CREATE UNIQUE INDEX i_appsec_errors_maint00 ON appsec.t_appsec_errors_maint ( update_ts );
同样,按日期选择对性能很重要,所以我们将在UPDATE_TS列(唯一的一列)上创建一个索引。
这一次,我们使它成为一个UNIQUE索引,这意味着我们将只有一个带有特定时间戳的条目。在我们的错误日志表上,索引不是UNIQUE,因为我们可能同时在表中有多个错误和条目。
t_appsec_errors_maint表仅供内部使用,所以我们不会创建视图,也不会在表上授予特权。
创建错误日志管理程序
我们的表管理任务是由触发器发起的,但是在定义触发器之前,我们需要定义完成管理任务的过程。我们的管理过程将被命名为p_appsec_errors_janitor,它没有参数。
我们希望它独立运行;所以我们用修饰语PRAGMA AUTONOMOUS_TRANSACTION来定义。这允许过程执行插入、删除和提交更改,即使调用该事务的程序没有提交。如果没有这个修饰符,如果我们在这里发出一个 commit,我们就要求 Oracle 数据库提交我们在当前会话中所做的每个更新、插入或删除。当我们处理一个错误时,除了在日志中插入错误消息和清除旧条目之外,我们特别希望避免提交任何东西。执行清单 7-4 中的脚本来创建程序。
***清单 7-4。*管理错误日志表的程序,p_appsec_errors_janitor
CREATE OR REPLACE PROCEDURE appsec.p_appsec_errors_janitor AS PRAGMA AUTONOMOUS_TRANSACTION; m_err_no NUMBER; m_err_txt VARCHAR2(2000); BEGIN INSERT INTO t_appsec_errors_maint ( update_ts ) VALUES ( SYSDATE ); COMMIT; -- Remove error log entries over 45 days old DELETE FROM t_appsec_errors WHERE update_ts < ( SYSDATE - 45 ); COMMIT; INSERT INTO t_appsec_errors ( err_no, err_txt, msg_txt ) VALUES ( 0, 'No Error', 'Success managing log file by Janitor' ); COMMIT; EXCEPTION WHEN OTHERS THEN m_err_no := SQLCODE; m_err_txt := SQLERRM; INSERT INTO t_appsec_errors ( err_no, err_txt, msg_txt ) VALUES ( m_err_no, m_err_txt, 'Error managing log file by Janitor' ); COMMIT; END; /
我们的第一步(在BEGIN头之后)是在我们的管理表中插入当前日期。这将防止其他人也试图管理错误日志表。我们提交它,这在这里是允许的,因为我们是一个自治的事务。
第二步是删除错误日志中超过 45 天的记录。注意,我们做了一些涉及SYSDATE的日期运算,在这里是SYSDATE - 45,相当于 45 天前。我们将在触发器中使用类似的日期算法。我们也提交这一删除。
在BEGIN标题下的最后一件事是在错误日志中插入一个“成功”消息,并提交它。为什么不呢?那似乎是个好地方。
正如我们到目前为止看到的其他过程一样,我们将捕捉错误。在这种情况下,我们将把错误插入到我们的错误日志表中(为什么不呢?将我们所有的故障排除信息放在一个地方会很好。)并提交。
创建触发器以维护错误日志表
我们上面定义的维护过程将在您每次调用它时工作,无论您如何调用它。你可以雇人每天手动运行一次这个过程。Oracle 数据库有一个调度器(DBMS_SCHEDULER PL/SQL包或旧的DBMS_JOB PL/SQL包),您可以使用它每天运行一次。
相反,我们将通过添加一个触发器来使表自治。触发器与过程有很多相似之处,所以它与我们一直在讨论的语法是一致的。执行清单 7-5 中的代码,在t_appsec_errors表上创建并启用一个触发器,该触发器将在每行(日志条目)插入表中后运行。
***清单 7-5。*在错误日志表上插入触发器,t_appsec_errors_iar
CREATE OR REPLACE TRIGGER appsec.t_appsec_errors_iar **AFTER INSERT** ON t_appsec_errors FOR EACH ROW DECLARE m_log_maint_dt DATE; BEGIN **SELECT MAX( update_ts ) INTO m_log_maint_dt** FROM t_appsec_errors_maint; -- Whenever T_APPSEC_ERRORS_MAINT is empty, M_LOG_MAINT_DT is null IF( ( m_log_maint_dt IS NULL ) OR ( m_log_maint_dt < ( SYSDATE - 1 ) ) ) THEN p_appsec_errors_janitor; END IF; END; / ALTER TRIGGER appsec.t_appsec_errors_iar ENABLE;
此触发器在每次插入后运行,AFTER INSERT;然而,我们只希望我们的过程每天运行一次。为了实现这一点,我们从t_appsec_errors_maint表中获取我们的过程最后一次运行的MAX( update_ts ),并将该日期存储在m_log_maint_dt中。(注意这个SELECT INTO语法的例子——选择一个变量的值。)然后我们检查m_log_maint_dt是否为NULL(每当t_appsec_errors_maint表为空时)或者m_log_maint_dt是否早于 24 小时前(< SYSDATE – 1)。如果是,那么我们运行我们的过程,p_appsec_errors_janitor。
测试触发器
当您作为appsec用户连接到 Oracle 数据库时,您可以测试触发器。首先执行下面几行来插入一个错误日志条目并提交它:
INSERT INTO appsec.v_appsec_errors (err_no, err_txt ) VALUES (1, 'DAVE' ); COMMIT;
注意,我们的自治过程只能处理独立存在的数据。我们的插入和更新不会独立存在于数据库中,直到我们COMMIT数据。
还要注意,我们依赖于默认值msg_txt和update_ts——这些列不是我们的 insert 语句的一部分。
查询我们的每个表、错误日志和维护记录,观察我们之前的插入是否成功,以及看门人过程是否运行。这里有一个例子:
SELECT * FROM appsec.v_appsec_errors ORDER BY update_ts; SELECT * FROM appsec.t_appsec_errors_maint ORDER BY update_ts;
现在插入一个假装 60 天的错误日志条目(注意带SYSDATE的算术):
INSERT INTO appsec.v_appsec_errors (err_no, err_txt, msg_txt, update_ts) VALUES (2, 'DAVE', 'NONE', SYSDATE - 60 ); COMMIT;
再次查询我们的每个表,以确保我们的插入有效,并且我们的看门人过程没有再次运行(因为它已经在这一天运行过):
SELECT * FROM appsec.v_appsec_errors ORDER BY update_ts; SELECT * FROM appsec.t_appsec_errors_maint ORDER BY update_ts;
现在,将我们最后一次看门人维护运行日期的数据更改为昨天(实际上是 24 小时前),并确保更改有效(如果您在此表中有多个条目,此UPDATE将不起作用。本表中UPDATE_TS上的指数为UNIQUE指数):
UPDATE appsec.t_appsec_errors_maint SET update_ts = SYSDATE-1; COMMIT; SELECT * FROM appsec.t_appsec_errors_maint ORDER BY update_ts;
并从今天开始提交另一条记录(默认,SYSDATE):
INSERT INTO appsec.v_appsec_errors (err_no, err_txt ) VALUES (3, 'DAVE' ); COMMIT;
在这些测试中,最后一次查询我们的每个表,以确保我们的插入有效,我们的看门人过程再次运行,并且模拟旧记录(带有err_no = 2的记录)被删除:
SELECT * FROM appsec.v_appsec_errors ORDER BY update_ts; SELECT * FROM appsec.t_appsec_errors_maint ORDER BY update_ts;
更新应用安全包
在第六章的中,我们有两个过程(p_get_shared_passphrase和p_get_des_crypt_test_data)和一个函数(f_show_algorithm),我们称之为“临时的”。它们仅在第六章的中用于测试,我们将在本章的app_sec_pkg中删除它们。在app_sec_pkg中的功能的剩余部分将会保留并且没有被改变。查看文件,chapter 7/appsec . SQL查看完整清单。我们在本章中引入了一个新的程序:p_log_error。
创建错误日志记录过程
p_log_error程序采用一个NUMBER和一个或两个VARCHAR2(文本)参数。err_txt字段限制为 2,000 个字符,但一个VARCHAR2列最多可包含 4000 个字符;因此,如果需要,我们将m_err_txt参数截断为 2,000 个字符,以适合我们的err_txt列。
注意,这个过程(包)和被更新的表在appsec模式中,但是调用这个过程的可能是另一个模式中的应用(比如HR)。我们已经将对app_sec_pkg包的执行权授予了HR用户,我们需要将执行权授予任何其他需要我们的应用安全进程的应用用户。
如果你愿意,回想一下我们定义t_appsec_errors表的时候。回想一下,我们将msg_txt和update_ts列设置为可空,并使用默认值(NULL和SYSDATE)。这允许我们通过为前两列提供数据元素来插入数据。我们甚至可以在不提及最后两列的情况下插入数据。其实我们说过不想给update_ts插入一个值;而是允许 Oracle 数据库分配当前的默认值SYSDATE。
好了,现在我们正在创建一个程序(如清单 7-6 中的所示)供各种应用调用,以便将错误记录插入到我们的表中,并且该程序考虑了那些默认值。首先,过程不接受update_ts的值;相反,将使用默认的SYSDATE。第二,msg_txt的值有一个缺省值NULL,这样应用用户可以在有或者没有msg_txt值的情况下调用这个过程。
***清单 7-6。*插入日志条目的程序,p_log_error
PROCEDURE p_log_error( m_err_no NUMBER, m_err_txt VARCHAR2, m_msg_txt VARCHAR2 DEFAULT NULL ) IS l_err_txt VARCHAR2(2000); BEGIN l_err_txt := **RTRIM( SUBSTR( m_err_txt, 1, 2000 ) )**; INSERT INTO v_appsec_errors ( err_no, err_txt, msg_txt ) VALUES ( m_err_no, l_err_txt, m_msg_txt ); COMMIT; END p_log_error;
我们使用substring函数SUBSTR,只获取错误文本的前 2000 个字符。然后,我们使用右修剪功能,RTRIM删除任何空格在右端的剩余文本。如果m_err_txt为NULL,SUBSTR返回一个NULL,RTRIM返回一个NULL。
在p_log_error过程的最后,我们简单地将错误数据插入到我们的错误日志表和COMMIT中。
执行包规格和主体
执行名为chapter 7/appsec . SQL文件中的两个块来替换app_sec_pkg包规范和主体。您可以看到这两个块都是以命令CREATE OR REPLACE开始的。因为我们已经有了一个名为app_sec_pkg的包,这个命令将替换它。这个命令最大的优点是我们可以在运行的 Oracle 数据库上执行它,并且使用这个包的应用不会失败。也就是说,如果封装规格不需要改变。考虑另一个选项:如果我们不得不DROP然后分别CREATE这些结构,我们将不得不等待直到 Oracle 数据库离线,或者至少直到依赖的应用不运行;否则,在DROP和CREATE之间的过渡期间,应用将会失败。
传输中使用和测试加密的方法
我们的工作模型不会从OracleJavaSecure的main()方法进行测试;相反,我们将展示如何作为一个独立的应用使用我们的应用安全包app_sec_pkg的结构。我们将在OracleJavaSecure类中再添加两个方法:一个用于测试,resetKeys();另一个是让客户端准备加密数据以更新/插入 Oracle 数据库,makeDESKey()。
我们希望能够以最少的工作量从我们的客户端应用进行数据更新。最简单的工作需要以下步骤:
- 在客户端生成 RSA 密钥,并将公钥传递给 Oracle。
- 在 Oracle 数据库上生成 DES secret 密码密钥,用 RSA 公钥加密工件,并传递回客户端。
- 在客户端上构建 DES 密钥的副本。
- 用 DES 密钥加密数据并发送到 Oracle 数据库进行解密和更新。
我们已经演示了一个 Oracle 过程p_get_shared_passphrase,它允许我们将步骤 1 和 2 合并成一个步骤。但是,第 4 步需要第二条 Oracle 语句。因此,我们至少需要两次调用 Oracle 数据库来进行第一次更新。在同一个 Oracle 会话中,我们可以进行额外的更新,每次只需一个调用。我们只需要做一次组合步骤 1、2、3(密钥交换);然后在建立了键之后,我们可以使用现有的键进行尽可能多的更新和插入。
建立秘密口令密钥的方法
在第六章中,我们使用了p_get_shared_passphrase Oracle 过程将所有的 DES secret 密码密钥工件拿到客户端;然而,直到我们从 Oracle 数据库接收到想要在客户机上解密的加密数据,我们才构建了秘密密码密钥。
在本章中,即使没有数据要解密,我们也需要 DES 密钥。我们将在客户端进行数据加密,并将其作为独立任务发送到 Oracle 数据库。因此,我们需要一个 Java 方法来独立地构建密码密钥。清单 7-7 显示了该方法的代码。
***清单 7-7。*方法调用建立秘密密码的密钥,makeDESKey()
public static final void makeDESKey( RAW cryptSecretDESPassPhrase, RAW cryptSecretDESAlgorithm, RAW cryptSecretDESSalt, RAW cryptSecretDESIterationCount ) { try { decryptSessionSecretDESPassPhrase( cryptSecretDESPassPhrase, cryptSecretDESAlgorithm, cryptSecretDESSalt, cryptSecretDESIterationCount ); makeSessionSecretDESKey(); } catch( Exception x ) { x.printStackTrace(); } }
在try块中是我们之前的getDecryptData()方法的大部分主体,没有实际解密数据的调用。这给我们提供了一个机会来做一些重构,改进我们代码的设计。因为我们的新方法完成了我们在getDecryptData()中所做的大部分工作,所以让我们重写getDecryptData()来调用新方法,如清单 7-8 所示。
***清单 7-8。*用秘密密码密钥解密数据,getDecryptData()
public static final String getDecryptData( RAW cryptData, RAW cryptSecretDESPassPhrase, RAW cryptSecretDESAlgorithm, RAW cryptSecretDESSalt, RAW cryptSecretDESIterationCount ) { String rtrnString = "getDecryptData() A failed"; try { if( ( null == sessionSecretDESKey ) || testAsClientAndServer ) { **makeDESKey**( cryptSecretDESPassPhrase, cryptSecretDESAlgorithm, cryptSecretDESSalt, cryptSecretDESIterationCount ); } rtrnString = getDecryptData( cryptData ); } catch( Exception x ) { x.printStackTrace(); } return rtrnString; }
粗体文本,我们对makeDESKey()的调用,是我们之前已经移入makeDESKey()主体的代码。
重置所有键的临时方法
我们添加到OracleJavaSecure的第二个方法是resetKeys()。resetKeys()方法仅用于本章中的测试(不过,我们将在第十章中再次提到它)。稍后我们将描述几个测试场景,其中一个将模拟在客户机上启动一个新的连接/会话(通过运行这个方法)并尝试使用 Oracle 数据库上的现有键。这个场景将会失败,但是我们将进行测试来演示这个场景。
在resetKeys(),清单 7-9 中,我们将最初设置为null的静态成员设置回null。回想一下,它们是null,以便于为null测试那些变量和/或测试与其他成员的比较。我们需要最初将它们设置为null,以便编译通过“可能尚未初始化”错误消息。
我们还将sessionSecretDESAlgorithm的值重置为其预先协商的值。
***清单 7-9。*复位所有按键,resetKeys()
public static final void resetKeys() { locRSAPubMod = null; saveExtRSAPubMod = null; extRSAPubKey = null; sessionSecretDESPassPhraseChars = null; sessionSecretDESKey = null; sessionSecretDESAlgorithm = "PBEWithSHA1AndDESede"; }
将更新的 OracleJavaSecure 类加载到 Oracle 中
以应用安全用户appsec、非默认角色用户appsec_role的身份连接或保持连接到 Oracle 数据库,并将文件 第七章\ orajavsec \ Oracle javasecure . Java中的代码复制/粘贴到您的 Oracle 客户端。取消第一行的注释,然后运行脚本来替换 Oracle 数据库中的 Java 类。
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED appsec."orajavsec/OracleJavaSecure" AS
人力资源用户的安全结构
我们的应用加密工作模型将包括从HR模式中读取数据,敏感列在通过网络传输时被加密。部分责任落在了应用开发者身上,他们必须确保敏感数据只能以加密的形式提供给客户端。我们的应用安全模式appsec可以提供这些工具,但是我们的应用开发者,比如HR,需要实现它们。
先来探究一下HR是怎么加密他的数据的。然后,我们将看看应用安全管理器可以为所有应用开发人员提供什么样的模板来实现这一点。
探索支持人力资源任务的权限
Oracle 提供的HR是一个示例模式,已经拥有各种系统特权。HR拥有默认角色RESOURCE,并通过该角色拥有以下权限列表:
CREATE SEQUENCE, CREATE TRIGGER, CREATE CLUSTER, CREATE PROCEDURE, CREATE TYPE, CREATE OPERATOR, CREATE TABLE, CREATE INDEXTYPE
所有打算实现我们的应用安全性的应用模式都需要CREATE PROCEDURE系统特权。
回想一下,要访问应用安全结构,我们还需要授予每个应用模式执行app_sec_pkg包的对象特权,就像我们对HR所做的那样(已经作为secadm):
GRANT EXECUTE ON appsec.app_sec_pkg TO hr;
创建人力资源安全包
HR将拥有自己的过程和函数包,提供对HR表的访问,但仅以加密形式返回任何敏感列。让我们检查一下这个包,然后在最后创建它。
注你可以在名为 * Chapter7 /HR.sql* 的文件中找到以下命令的脚本。
CREATE OR REPLACE PACKAGE hr.hr_sec_pkg IS **TYPE RESULTSET_TYPE IS REF CURSOR;**
在我们的包的规范中,我们将定义一个TYPE。我们将其命名为RESULTSET_TYPE,它将代表一个CURSOR,在 Java 中也称为ResultSet。当我们调用过程来获取我们的加密的HR数据时,我们将从 Oracle 数据库返回一些OUT参数。正如我们已经看到的,许多OUT参数将是我们的秘密密码密钥的工件,其中一个也可能是RESULTSET_TYPE,它将保存多行加密数据。
从员工中选择敏感数据列
清单 7-10 中的代码是一个名为p_select_employees_sensitive的 Oracle 过程的主体。你应该非常熟悉这种格式。用于设置秘密密码密钥工件的参数列表和代码看起来就像我们之前看到的那样。我们有一个名为resultset_out的OUT参数,它将保存一个RESULTSET_TYPE(数据行):
***清单 7-10。*从雇员表中选择敏感数据的程序,p_select_employees_sensitive
PROCEDURE p_select_employees_sensitive( ext_modulus VARCHAR2, ext_exponent VARCHAR2, secret_pass_salt OUT RAW, secret_pass_count OUT RAW, secret_pass_algorithm OUT RAW, secret_pass OUT RAW, **resultset_out OUT RESULTSET_TYPE**, m_err_no OUT NUMBER, m_err_txt OUT VARCHAR2 ) IS BEGIN m_err_no := 0; secret_pass_salt := appsec.app_sec_pkg.f_get_crypt_secret_salt( ext_modulus, ext_exponent ); secret_pass_count := appsec.app_sec_pkg.f_get_crypt_secret_count( ext_modulus, ext_exponent ); secret_pass := appsec.app_sec_pkg.f_get_crypt_secret_pass( ext_modulus, ext_exponent ); secret_pass_algorithm := appsec.app_sec_pkg.f_get_crypt_secret_algorithm(ext_modulus, ext_exponent); ** OPEN resultset_out FOR SELECT** employee_id, first_name, last_name, email, phone_number, hire_date, job_id, appsec.app_sec_pkg.**f_get_crypt_data**( TO_CHAR( salary ) ), appsec.app_sec_pkg.**f_get_crypt_data**( TO_CHAR( commission_pct ) ), manager_id, department_id FROM employees; EXCEPTION WHEN OTHERS THEN m_err_no := SQLCODE; m_err_txt := SQLERRM; appsec.app_sec_pkg.p_log_error( m_err_no, m_err_txt, 'HR p_select_employees_sensitive' ); END p_select_employees_sensitive;
填充结果集 _ 类型
在p_select_employees_sensitive过程的中间,我们打开RESULTSET_TYPE从查询中收集一个CURSOR。注意,当我们返回到客户端时,我们实际上并没有传输所有的数据;相反,我们为客户端提供了一个CURSOR句柄,这样客户端就可以收集和处理数据行,一次一行。
我们使用的查询选择了EMPLOYEES表中的所有列。注意清单 7-10 中的,我们用这些调用加密了SALARY和COMMISSION_PCT:
appsec.app_sec_pkg.f_get_crypt_data( TO_CHAR( salary ) ), appsec.app_sec_pkg.f_get_crypt_data( TO_CHAR( commission_pct ) ),
我们的加密方法要求我们使用String传递数据进行加密。SALARY和COMMISSION_PCT都是数字列,所以我们先把它们转换成VARCHAR2,然后传递给我们的应用安全 Java 存储过程(函数)appsec.app_sec_pkg.f_get_crypt_data。
该函数返回一个保存加密数据的RAW类型。客户端将数据解密回明文String。并且我们会将数据转换回它原来的类型(Date,数字等)。),根据客户端的需要。
您可能会问,“但是我们不能加密非String数据吗?”答案是肯定的。实际上,我们可以加密任何可以表示为一个byte数组的东西,这实际上是任何东西,经过一些转换。然而,如果你能在屏幕上看到数据或者把它打印出来,那么你也可以把数据表示成一个String,当我们转换成Strings或者从Strings转换过来的时候,这通常会更清楚,而且通常情况下,我们最终无论如何都需要一个String。
注在本章结束时,你将有一个坚实的基础来扩展我们在这里建立的加密。您将能够扩展您所学的内容,以便加密对象或 BLOBS 或其他类型的数据。
记录错误信息
在过程的最后,我们捕获任何 Oracle 异常并记录错误。我们称我们的新程序为p_log_error。我们将在appsec模式中记录错误,这样我们的应用安全管理器就可以捕捉到appsec结构中的错误,并且可以帮助使用这些结构调试单个应用中的问题。我们不会孤立应用开发人员,但会在调试工作中提供帮助。
选择所有数据作为单个敏感字符串
HR实现了另一种方法,加密所有选择的数据,不是作为单独的列,而是作为每行一个长的连接的VARCHAR2。这个过程(清单 7-11 中的p_select_employees_secret)和我们上次看到的唯一区别是RESULTSET_OUT的定义。
***清单 7-11。*将所有数据加密为单个字符串的程序部分,p_select_employees_secret
OPEN resultset_out FOR **SELECT** ** appsec.app_sec_pkg.f_get_crypt_data(** TO_CHAR( employee_id ) ||', '|| first_name ||', '|| last_name ||', '|| email ||', '|| phone_number ||', '|| TO_CHAR( hire_date ) ||', '|| job_id ||', '|| TO_CHAR( salary ) ||', '|| TO_CHAR( commission_pct ) ||', '|| TO_CHAR( manager_id ) ||', '|| TO_CHAR( department_id ) ) FROM employees;
双管字符“||”是 Oracle 数据库中用来连接文本的符号。注意,我们为不属于VARCHAR2类型的列调用了TO_CHAR函数。在我们将所有这些列连接在一起之后,我们将得到的VARCHAR2传递给f_get_crypt_data函数进行加密,并在RESULTSET_OUT中为每行返回一个RAW。
请注意,在客户端,我们可能必须在解密数据后解析数据,以获得各个列。我们使用逗号作为列之间的分隔符,但是解析逗号假设数据中不存在逗号。每个应用都必须规划它们对我们的应用安全结构的使用,以及以适合其客户使用的形式提供其数据的最佳方法。对于不需要单个记录元素的客户机来说,这种串联格式可能更好。
为员工 ID 选择敏感数据
我们将探索从HR中选择加密数据的程序的另一个例子。这个 Oracle 过程p_select_employee_by_id_sens与前两个过程几乎相同,除了它也采用一个表示单个EMPLOYEE_ID的参数。如清单 7-12 中的所示。
***清单 7-12。*通过 ID 选择敏感数据的程序部分,p_select_employee_by_id_sens
m_employee_id employees.employee_id%TYPE ... OPEN resultset_out FOR SELECT employee_id, ... FROM employees **WHERE employee_id = m_employee_id;**
对resultset_out参数的查询选择了EMPLOYEE_ID等于该输入参数的数据。
这个过程应该只返回一行数据。
修改程序以获取共享密码
我们在第六章的中看到了p_get_shared_passphrase程序。在这一章中,我们用错误日志来修饰它。错误日志记录可以帮助应用安全性支持应用开发人员。
最大的变化是我们将app_sec_pkg包中的p_get_shared_passphrase放到我们自己的应用包hr_sec_pkg中。现在我们在hr_sec_pkg中有了它,这样我们的客户端应用(可能使用hr_view角色运行)就可以执行这个过程。我们允许HR执行app_sec_pkg结构,但是我们不允许hr_view这样做。因此,hr_view执行HR结构,HR结构执行appsec结构。
我们调用p_get_shared_passphrase并接着调用我们的新OracleJavaSecure.makeDESKey()方法来完成密钥交换并构建共享的秘密密码密钥。在尝试数据更新之前,我们必须这样做。
更新雇员中的敏感数据列
我们现在可以对数据实施加密更新了。我们将在包hr_sec_pkg中定义一个如清单 7-13 所示的过程p_update_employees_sensitive,以获取EMPLOYEES表中所有列的数据。对于敏感列,我们将提交封装加密数据的RAW类型。唯一的IN参数是表列数据,唯一的OUT参数是错误号和文本。注意这里缺少了什么——没有代表我们的加密密钥的参数。我们必须假设关键的交换已经发生。如果我们尚未在当前 Oracle 会话中交换密钥,则用户应用正在尝试在需要加密的字段中提交未加密的数据,或者他们正在使用不同会话中的密钥加密数据;Oracle 数据库将无法解密数据。
我们使用引用原始数据定义的锚定数据类型表单来定义参数类型。我们将这个数据类型声明锚定到前面的定义。例如,在该宣言中:
m_employee_id employees.employee_id%TYPE,
我们说m_employee_id参数与EMPLOYEES表中的EMPLOYEE_ID列是同一类型。我们将在适当的时候使用这种形式的“引用类型规范”来进一步建立我们正在接收的数据和它要到达的表之间的关系。这种做法至少有两个好处。第一,我们的过程将只接受适合于它将被插入或更新的字段的数据。这是对 SQL 注入攻击的进一步保护(见下一节的详细讨论)。锚定数据类型很好的第二个原因是,我们可以更改表中该列的定义,而不必同时更改这个过程。
***清单 7-13。*更新雇员表中的敏感数据,p_update_employees_sensitive
PROCEDURE p_update_employees_sensitive( m_employee_id employees.employee_id%TYPE, m_first_name employees.first_name%TYPE, m_last_name employees.last_name%TYPE, m_email employees.email%TYPE, m_phone_number employees.phone_number%TYPE, m_hire_date employees.hire_date%TYPE, m_job_id employees.job_id%TYPE, **crypt_salary RAW**, **crypt_commission_pct RAW**, m_manager_id employees.manager_id%TYPE, m_department_id employees.department_id%TYPE, m_err_no OUT NUMBER, m_err_txt OUT VARCHAR2 ) IS test_emp_ct NUMBER(6); v_salary VARCHAR2(15); -- Plenty of space, eventually a NUMBER v_commission_pct VARCHAR2(15); BEGIN m_err_no := 0; v_salary := appsec.app_sec_pkg.f_get_decrypt_data( crypt_salary ); v_commission_pct := appsec.app_sec_pkg.f_get_decrypt_data( crypt_commission_pct ); SELECT COUNT(*) INTO test_emp_ct FROM employees WHERE employee_id = m_employee_id; IF test_emp_ct = 0 THEN INSERT INTO employees (employees_seq.NEXTVAL, first_name, last_name, email, phone_number, hire_date, job_id, salary, commission_pct, manager_id, department_id) VALUES (m_employee_id, m_first_name, m_last_name, m_email, m_phone_number, m_hire_date, m_job_id, v_salary, v_commission_pct, m_manager_id, m_department_id); ELSE -- Comment update of certain values during testing - date constraint UPDATE employees SET first_name = m_first_name, last_name = m_last_name, email = m_email, phone_number = m_phone_number, -- Job History Constraint -- hire_date = m_hire_date, job_id = m_job_id, ` salary = v_salary, commission_pct = v_commission_pct,
manager_id = m_manager_id
-- Job History Constraint -- , department_id = m_department_id
WHERE employee_id = m_employee_id;
END IF;
EXCEPTION
WHEN OTHERS THEN
m_err_no := SQLCODE;
m_err_txt := SQLERRM;
appsec.app_sec_pkg.p_log_error( m_err_no, m_err_txt,
'HR p_update_employees_sensitive' );
END p_update_employees_sensitive;
END hr_sec_pkg; /`
程序变量和数据解密
我们不能像在前面的示例过程中使用OUT参数那样修改IN参数,但是我们希望捕获解密的输出,因此我们建立了两个过程变量:v_salary和v_commission_pct。我们还定义了一个名为test_emp_ct的数值过程变量:
test_emp_ct NUMBER(6); **v_salary** VARCHAR2(15); -- Plenty of space, eventually a NUMBER **v_commission_pct** VARCHAR2(15);
在BEGIN标题下,我们的过程体包括对f_get_decrypt_data Oracle 函数的两次调用,该函数将返回一个代表SALARY和COMMISSION_PCT的VARCHAR2数据类型。再次注意,此过程的使用假设您已经完成了密钥交换:
m_err_no := 0; v_salary := appsec.app_sec_pkg.**f_get_decrypt_data**( crypt_salary ); v_commission_pct := appsec.app_sec_pkg.**f_get_decrypt_data**( crypt_commission_pct );
插入或更新
我发现多功能过程通常是管理数据插入和更新的最佳选择。最多,我们给数据视图授予SELECT特权,给管理过程授予EXECUTE特权。我们定期传递一个事务代码(通常是 A、U 或 D ),它指示我们是否要插入(添加)、更新或删除一个记录。对于一个简单的双功能过程(插入或更新),我们不需要事务代码,但可以检查数据并 1)更新现有记录或 2)如果没有现有记录与键列匹配,则插入新记录。
在我们的管理过程主体中,我们将通过使用SELECT INTO语法来填充test_emp_ct,其中包含其EMPLOYEE_ID与被传入进行更新的m_employee_id相匹配的员工数量。不应该有超过一个,所以我们期望从计数中得到 0 或 1 的值。
SELECT COUNT(*) INTO test_emp_ct FROM employees WHERE employee_id = m_employee_id;
然后我们测试看test_emp_ct是否为 0——如果是,我们做一个INSERT;如果不是,一个UPDATE:
IF test_emp_ct = 0 THEN **INSERT** INTO employees ... ELSE **UPDATE** employees SET first_name = m_first_name, last_name = m_last_name, email = m_email, phone_number = m_phone_number, hire_date = m_hire_date, -- **Job History Constraint** -- job_id = m_job_id, salary = v_salary, commission_pct = v_commission_pct, manager_id = m_manager_id -- **Job History Constraint** -- , department_id = m_department_id WHERE employee_id = m_employee_id; END IF IF;
雇员表上的完整性约束
您将在前面的代码中看到,我们跳过了更新两列:JOB_ID和DEPARTMENT_ID。原因是在EMPLOYEES表上有一个现有的触发器,当EMPLOYEES记录中的这两列中的任何一列被更新时,该触发器会在JOB_HISTORY中插入一条记录。触发代码如清单 7-14 所示。
***清单 7-14。*对雇员表的现有完整性约束,HR.update_job_history
CREATE OR REPLACE TRIGGER HR.update_job_history AFTER **UPDATE OF job_id, department_id ON HR.EMPLOYEES** FOR EACH ROW BEGIN **add_job_history**(:old.employee_id, :old.hire_date, sysdate, :old.job_id, :old.department_id); END;
你可以在清单 7-14 的中看到触发器调用一个过程add_job_history。这个过程所做的只是将一条记录记录到JOB_HISTORY表中。然而,JOB_HISTORY表包含一个关于(EMPLOYEE_ID, 开始日期)的UNIQUE索引。
总结一下这个问题:如果您试图一天多次更新一个EMPLOYEES ' JOB_ID或DEPARTMENT_ID,它会失败,因为触发器不能在同一天为同一个用户在JOB_HISTORY表中插入另一个记录。这是一个商业规则,HR示例模式的开发人员通过一个UNIQUE索引来强制执行——雇员一天内不能换工作超过一次。
更新触发器语法
我想指出触发器语法的一个方面。你看到清单 7-14 中的前缀了吗?该前缀表示我们正在使用表中已经存在的值。因为这是一个 AFTER UPDATE 触发器,所以表中存在的值与我们在更新中提交的值相同。这在数据更新后运行。
通常,触发器可用于测试、过滤和操作提交到表中的数据,然后再进行存储。例如,如果我正在更新一个雇员的姓氏,我可能会说:
update employees set last_name = ‘coffin’ where employee_id = 700;
如果我们要求所有的last_name条目都是大写字母,这可能会是个问题!我们可以用一个BEFORE UPDATE OR INSERT触发器来捕捉并纠正这个问题。在我们的触发器中间,我们可能会说:
:new.last_name := upper(:new.last_name);
这将使我们提交给姓氏的新值大写。如果我们想抱怨用户试图用已经存在的相同姓氏更新姓氏,我们可以将大写的新值与旧值进行比较,如下所示:
IF :new.last_name = :old.last_name THEN Raise_Application_Error(-20000, 'Same last name as before!');
一个BEFORE UPDATE触发器可以访问数据库中的现有值(:old)和正在提交的新值(:new)。这种能力经常在触发器中使用。
避免 SQL 注入
如果计算机用户保存了提交数据的网页的 html 源,并且能够修改该网页以发送通常不被允许的数据,那么这将是交叉视线脚本的一个例子(用户自己的网页是一个站点,向另一个站点的 web 服务器提交数据。)例如,您可能有一个提交地址的邮政编码并且只允许数字数据的网页。我可能会恶意修改网页,使我的副本在邮政编码字段中提交一个 web 链接(URL)。对跨站点脚本的唯一真正预防是假设它总是会发生,并在服务器上采取措施来捕捉和处理它。
也许您的 web 页面向 Oracle 数据库提交数据,恶意用户修改了您的 web 页面的副本,以便在邮政编码字段中提交 Oracle SQL 或 PL/SQL 命令。黑客可能会将此代码放在字段“11111;delete from employees;--”中。如果您构建的动态查询只是将提交的数据嵌入到查询中,那么不用执行:
UPDATE EMPLOYEES SET ZIP=**11111** WHERE EMPLOYEE_ID=300;
您可以执行这组命令:
UPDATE EMPLOYEES SET ZIP=**11111;delete from employees;**-- WHERE EMPLOYEE_ID=300;
这相当于三行代码:对所有雇员的更新,删除雇员的所有记录,以及一个注释。这是 SQL 注入的一个例子。
典型的 SQL 注入攻击通过附加一个对所有数据都适用的 where 测试来修改 select 语句。例如,如果我接受用户输入姓氏来搜索雇员,并且用户键入“King”或“a”=“a”,我的动态 SQL 可能如下所示:
SELECT * FROM EMPLOYEES WHERE LAST_NAME=**'King' or 'a'='a';**
如果这是一个密码匹配 Oracle 数据库中存储的值的测试,那么 SQL 注入可能如下所示:
SELECT count(*) FROM EMPLOYEES WHERE LAST_NAME='King' and PASSWORD=**'whatever' or 'a'='a';**
select 语句将返回一个大于 0 的数字,即使用户不知道密码,他也可能获得访问权限。
在 Oracle 数据库中,您可以通过几种方法来防止 SQL 注入的发生。一种传统的方法是过滤传入的数据和/或对数据进行转义(使其成为单个字符的序列,而不是文本。)然而,更好的方法是始终使用参数化输入。我们用带参数的存储过程来实现这一点。我们不是在构建动态查询,而是将参数抽取到已经在 Oracle 数据库中暂存的 PL/SQL 中。数据库将变量绑定到我们的查询/更新框架。
我们还可以通过如下 Java 语句阻止客户端的 SQL 注入。userInputEmpID的值在问号(?).
String query = "SELECT * FROM EMPLOYEES WHERE EMPLOYEE_ID = **?** "; PreparedStatement pstmt = connection.prepareStatement( query ); pstmt.setString( 1, **userInputEmpID** );
让一个PreparedStatement接受我们的参数并用它填充查询,可以防止恶意代码被添加到查询中。再一次,PreparedStatement存放在 Oracle 数据库中,我们的参数在那里设置,Oracle 数据库将它们绑定到更新/查询。
如果您需要在应用(java 或其他)中放置 Oracle 数据库查询的代码,请使用PreparedStatement,如前所述,而不是将用户输入连接到查询字符串中。
证明存储过程中的 SQL 注入失败
我在hr_sec_pkg中增加了两个过程,它们将展示在存储过程中 SQL 注入的尝试。我不是来自密苏里州,但我来自“展示自我”的心态:信任,但要核实。让我们在对LAST_NAME进行选择查询时尝试一下 SQL 注入。在清单 7-15 、p_select_employee_by_ln_sens中部分显示的过程中,我们将传入第十个参数LAST_NAME,并修改我们在过程中的选择以使用它:
***清单 7-15。*按姓氏选择雇员数据并尝试 SQL 注入
PROCEDURE p_select_employee_by_ln_sens( ... **m_last_name** employees.last_name%TYPE ) IS BEGIN ... OPEN resultset_out FOR SELECT ... FROM employees **WHERE last_name = m_last_name;**
让我们看看是否可以通过将 SQL 注入包含在RAW中(就像我们对加密数据更新所做的那样)并将RAW转换为WHERE子句中的VARCHAR2,来偷偷加入一些。我们在清单 7-16 的中的一个名为p_select_employee_by_raw_sens的测试过程中完成了这个任务。
***清单 7-16。*通过原始值选择员工数据并尝试 SQL 注入
PROCEDURE p_select_employee_by_raw_sens( ... m_last_name RAW ) IS BEGIN ... OPEN resultset_out FOR SELECT ... FROM employees **WHERE last_name = UTL_RAW.CAST_TO_VARCHAR2( m_last_name )**;
你会很高兴地注意到,当我们测试这一点,这些企图逃避是不成功的。在这两种情况下,正如预期的那样,Oracle 数据库会说,“给我一些东西插入测试WHERE LAST_NAME = ?;”。
我们将为 Oracle 数据库提供这个字符串:“King”或“a”=“a”,人们可能会想象用单引号将它括起来:
WHERE LAST_NAME = 'King' or 'a'='a';
然而,Oracle 数据库将我们的字符串视为一个单个数据元素,并检查是否有人的LAST_NAME是(以转义形式):" King\ '或'a'='a "或" {King '或' a'='a} "。
执行人力资源包规范和主体
既然我们已经描述了hr_sec_pkg包中的过程,我们将继续执行包规范和包主体的CREATE语句。执行名为 Chapter7 /HR.sql 文件中的两个块,创建hr_sec_pkg包规范和主体。在您创建了hr_sec_pkg之后,您需要将包的 execute 权限授予hrview_role角色
GRANT EXECUTE ON hr.hr_sec_pkg TO hrview_role;
插入雇员记录:更新序列
为了让我们的示例代码能够工作,我们需要一个固定的EMPLOYEE_ID,数字 300 作为EMPLOYEES表中的记录。当最初安装样本EMPLOYEES表时,大约有 100 条记录,EMPLOYEE_ID从 100 到大约 200。一般来说,插入到EMPLOYEES表中会使用序列的下一个值,EMPLOYEES_SEQ,如下所示(暂时不要执行,仅供参考):
INSERT INTO employees (employee_id, first_name, last_name, email, phone_number, hire_date, job_id, salary, commission_pct, manager_id, department_id) VALUES (**employees_seq.NEXTVAL**, 'David', 'Coffin', 'DAVID.COFFIN', '800.555.1212', SYSDATE, 'SA_REP', 5000, 0.20, 147, 80);
每次调用SEQUENCE.NEXTVAL时,该值都会递增。要查看EMPLOYEES_SEQ的当前(下一个)值,执行以下命令:
SELECT last_number FROM user_sequences WHERE sequence_name='EMPLOYEES_SEQ';
注你可以在名为 * Chapter7 /HR.sql* 的文件中找到本节命令的脚本。
没有认可的方法来手动设置序列的LAST_NUMBER。但是,我们可以调整增量值来获得想要的效果。首先,确保上面命令中返回的电流LAST_NUMBER小于 300(我们的例子是EMPLOYEE_ID)。)如果不是,您可能需要替换一个比我们示例代码中的LAST_NUMBER大的数字,或者更新EMPLOYEE_ID 300 处的数据。
要设置在EMPLOYEE_ID = 300 处插入我们的示例EMPLOYEES记录,我们需要让EMPLOYEES_SEQ的LAST_NUMBER等于 300,我们将使用匿名(未命名)PL/SQL 块来完成。这不会保存到一个命名的存储过程中,而是执行一次来完成我们的计划。
注意我们使用
user_sequences视图中的LAST_NUMBER代替序列的当前值CURRVAL。我们这样做是因为我们在这个会话中可能没有CURRVAL。CURRVAL仅在我们在此会话中对序列执行NEXTVAL后存在。然后我们可以得到序列的当前值。
参见清单 7-17 。我们有一个NUMBER、offset,我们从序列中选择值(300–LAST_NUMBER)放入其中。例如,如果我们的LAST_NUMBER当前是 207,offset的值将是 300–207 或 93。我们将一个命令字符串alter_command连接到ALTER序列,将INCREMENT BY值设置为那个offset。我们将那个ALTER命令传递给EXECUTE IMMEDIATE。那么下一次我们调用EMPLOYEES_SEQ.NEXTVAL的时候,就会得到 207 + 93 = 300 的值。为了完成这个计划,我们将序列的INCREMENT BY值设置回 1。
此时*执行清单 7-17 中的所有命令。*您将创建我们的测试用户为employee_id = 300。请随意在最后的INSERT命令中插入您自己的个人数据。
***清单 7-17。*匿名 PL/SQL 块重置序列
`DECLARE offset NUMBER; alter_command VARCHAR2(100); new_last_number NUMBER; BEGIN SELECT (300 - last_number) INTO offset FROM user_sequences WHERE sequence_name='EMPLOYEES_SEQ';
alter_command := 'ALTER SEQUENCE employees_seq INCREMENT BY ' || TO_CHAR(offset) || ' MINVALUE 0'; EXECUTE IMMEDIATE alter_command;
SELECT employees_seq.NEXTVAL INTO new_last_number FROM DUAL; DBMS_OUTPUT.PUT_LINE( new_last_number );
EXECUTE IMMEDIATE 'ALTER SEQUENCE employees_seq INCREMENT BY 1'; END; /
SELECT last_number FROM user_sequences WHERE sequence_name='EMPLOYEES_SEQ';
INSERT INTO employees
(employee_id, first_name, last_name, email, phone_number, hire_date,
job_id, salary, commission_pct, manager_id, department_id)
VALUES
(employees_seq.NEXTVAL, 'David', 'Coffin', 'DAVID.COFFIN', '800.555.1212', SYSDATE, 'SA_REP', 5000, 0.20, 147, 80);
COMMIT;`
在不插入记录的情况下递增序列的一种强力方式是将SELECT SEQUENCE.NEXTVAL足够多次。您也可以将INCREMENT BY值设置为负数,以减少序列中LAST_NUMBER的值。
我们可以通过再次选择EMPLOYEES_SEQ来查看新的LAST_NUMBER设置。确保它是 300,然后插入我们的示例记录EMPLOYEE_ID = 300。终于COMMIT更新了。通过选择查看我们的新条目:
SELECT * FROM employees WHERE employee_id=300;
我应该提到在名为SECURE_DML的EMPLOYEES表上有一个现有的INSERT / UPDATE / DELETE触发器。该触发器限制将EMPLOYEES数据更改为工作日的上午 8 点到下午 6 点。这类似于我们在第二章中实施的限制。但是,默认情况下,此触发器是禁用的。
加密数据交换的演示和测试
我们将执行一个单独的 Java 类,TestOracleJavaSecure来模拟HR模式的客户端应用。我们的客户端应用将调用我们在hr_sec_pkg中定义的存储过程,进行一些查询和一些更新。
我们将只探索应用代码的几个大小片段。当我们浏览这一部分时,您应该打开 TestOracleJavaSecure.java 文件进行参考。
注你可以在文件chapter 7/testoraclejavasecure . Java中找到这段代码。
一些预备步骤
在我们开始大规模的个人演示和测试之前,我们想弄清楚我们的方向。接下来的几个小节为后面的演示和测试做好了准备。
main()方法和方法成员
TestOracleJavaSecure类的全部代码都驻留在main()方法中。所以,当我们从命令行用这个类调用 Java 时,我们简单地从上到下运行代码。
我们在main()方法中做的第一件事是建立一个 Oracle 连接,如清单 7-18 所示。编辑连接字符串,以使用您分配给应用用户appusr的密码,以及您的特定服务器名称和端口。
***清单 7-18。*测试传输中加密的代码开头,TestOracleJavaSecure类
public class TestOracleJavaSecure { public static void main( String[] args ) { Connection conn = null; try { private static String appusrConnString = "jdbc:oracle:thin:AppUsr/password@localhost:1521:Orcl"; Class.forName( "oracle.jdbc.driver.OracleDriver" ); conn = DriverManager.getConnection( appusrConnString );
准备加密
我们不需要OracleJavaSecure中的Connection,因为我们不会直接从那个类中调用 Oracle 数据库。客户端上OracleJavaSecure的唯一功能(在本章中)是建立密钥和加密/解密数据。参见清单 7-19 。
***清单 7-19。*准备加密
//OracleJavaSecure.setConnection( conn ); String locModulus = OracleJavaSecure.getLocRSAPubMod(); String locExponent = OracleJavaSecure.getLocRSAPubExp();
我们得到 RSA 密钥对,并得到公钥指数和模数以传递给 Oracle 数据库。
设置非默认角色
appusr用户有权限执行appsec.p_check_hrview_access程序(回头参考第二章,该程序将设置安全应用角色hrview_role。我们执行如清单 7-20 所示的程序。
***清单 7-20。*设置非默认角色
stmt = ( OracleCallableStatement )conn.prepareCall( "CALL appsec.**p_check_hrview_access()**" ); // Comment next line to see Exception when non-default role not set stmt.executeUpdate();
为了获得角色,我们需要执行语句。如果您想确保没有该角色的访问将会失败,请将该行注释为executeUpdate()并运行TestOracleJavaSecure。确保在运行测试之后取消注释该行,这样您就可以运行我们的主要测试。
重用可调用语句
由于OracleCallableStatement是实现Statement的接口,我们可以像普通的Statement一样使用它。可以反复使用常规的Statement来执行查询和更新。然而,以我的经验来看,如果您有来自您的OracleCallableStatement调用的过程的OUT参数,那么您不应该重用它——只需要获得一个新的OracleCallableStatement。
在清单 7-20 中设置我们角色的第一个调用,给我们留下了一个OracleCallableStatement,我们可以重用它来获得EMPLOYEES的非敏感视图中的行数。我们将通过两种方式对行进行计数,如清单 7-21 所示:一次是通过遍历所有行的ResultSet,递增我们的计数,cnt;一次通过选择所有行的count(*)。选择count(*)是一种更有效的方式:
***清单 7-21。*获得员工公共视图中的行数
` rset = stmt.executeQuery( "SELECT * FROM hr.v_employees_public" ); int cnt = 0; while( rset.next() ) cnt++; System.out.println( "Count data in V_EMPLOYEES_PUBLIC: " + cnt );
rset = stmt.executeQuery( "SELECT COUNT(*) FROM hr.v_employees_public" ); if( rset.next() ) cnt = rset.getInt(1); System.out.println( "Count data in V_EMPLOYEES_PUBLIC: " + cnt );
if( null != stmt ) stmt.close();`
从员工中选择加密数据
这里是我们从 Oracle 数据库中选择加密数据的教科书过程,清单 7-22 。它类似于我们在上一章中使用的测试程序。我们将公钥模数和指数传递给 Oracle 数据库,并接收回我们的 DES 加密密码密钥的加密工件。此外,我们还有一个类型为OracleTypes.CURSOR的OUT参数。我们将利用OracleTypes.CURSOR(Java 中的ResultSet)来读取我们的数据。
清单 7-22。 Java 代码从员工中选择敏感数据,来自p_select_employees_sensitive
`stmt = ( OracleCallableStatement )conn.prepareCall( "CALL hr.hr_sec_pkg.p_select_employees_sensitive(?,?,?,?,?,?,?,?,?)" ); stmt.registerOutParameter( 3, OracleTypes.RAW ); stmt.registerOutParameter( 4, OracleTypes.RAW ); stmt.registerOutParameter( 5, OracleTypes.RAW ); stmt.registerOutParameter( 6, OracleTypes.RAW ); stmt.registerOutParameter( 7, OracleTypes.CURSOR ); stmt.registerOutParameter( 8, OracleTypes.NUMBER ); stmt.registerOutParameter( 9, OracleTypes.VARCHAR ); stmt.setString( 1, locModulus ); stmt.setString( 2, locExponent ); stmt.setNull( 3, OracleTypes.RAW ); stmt.setNull( 4, OracleTypes.RAW ); stmt.setNull( 5, OracleTypes.RAW ); stmt.setNull( 6, OracleTypes.RAW ); // This must go without saying - unsupported type for setNull //stmt.setNull( 7, OracleTypes.CURSOR ); stmt.setInt( 8, 0 ); stmt.setNull( 9, OracleTypes.VARCHAR ); stmt.executeUpdate();
errNo = stmt.getInt( 8 );
if( errNo != 0 ) {
errMsg = stmt.getString( 9 );
System.out.println( "Oracle error 1) " + errNo + ", " + errMsg );
} else {
System.out.println( "Oracle success 1)" );
sessionSecretDESSalt = stmt.getRAW( 3 );
sessionSecretDESIterationCount = stmt.getRAW( 4 );
sessionSecretDESAlgorithm = stmt.getRAW( 5 );
sessionSecretDESPassPhrase = stmt.getRAW( 6 );
rs = ( OracleResultSet )stmt.getCursor( 7 );
//while( rs.next() ) {
// Only show first row
if( rs.next() ) {
System.out.print( rs.getString( 1 ) );
System.out.print( ", " );
System.out.print( rs.getString( 2 ) );
System.out.print( ", " );
System.out.print( rs.getString( 3 ) );
System.out.print( ", " );
System.out.print( rs.getString( 4 ) );
System.out.print( ", " );
System.out.print( rs.getString( 5 ) );
System.out.print( ", " );
System.out.print( rs.getString( 6 ) );
System.out.print( ", " );
System.out.print( rs.getString( 7 ) );
System.out.print( ", " );
System.out.print( OracleJavaSecure.getDecryptData(
rs.getRAW( 8 ), sessionSecretDESPassPhrase,
sessionSecretDESAlgorithm, sessionSecretDESSalt,
sessionSecretDESIterationCount ) );
if ( null != rs.getRAW( 8 ) )
System.out.print( " (" + rs.getRAW( 8 ).stringValue() +
")" );
System.out.print( ", " );
// Most initial commissions in database are null
System.out.print( OracleJavaSecure.getDecryptData(
rs.getRAW( 9 ), sessionSecretDESPassPhrase,
sessionSecretDESAlgorithm, sessionSecretDESSalt,
sessionSecretDESIterationCount ) );
if ( null != rs.getRAW( 9 ) )
System.out.print( " (" + rs.getRAW( 9 ).stringValue() +
")" );
System.out.print( ", " );
System.out.print( rs.getString( 10 ) );
System.out.print( ", " );
System.out.print( rs.getString( 11 ) );
System.out.print( "\n" );
}
}
if( null != rs ) rs.close();
if( null != stmt ) stmt.close();`
我们的错误处理与我们在上一章中所做的相同:我们通过两个OUT参数返回错误号和消息。如果没有错误,我们继续将密码密钥工件放入本地方法成员中,我们还将CURSOR放入ResultSet中,这样我们就可以遍历数据了。由于EMPLOYEES表中大约有 100 个条目,我们将只显示第一个条目(通过读取if块中的ResultSet.next()而不是while块中的】。
大多数列都是明文,所以我们只需将它们打印出来。但是SALARY和COMMISSION_PCT值是由 Oracle 数据库作为RAW加密数据交给我们的。(注意加密的SALARY在ResultSet的第 8 个位置位置,而ResultSet在Statement的第 7 个位置。那些元素的编号是独立的。)我们将把这些RAW值发送给getDecryptData()方法,同时发送的还有秘密密码密钥工件。如果我们还没有构建秘密密码密钥的本地副本,我们将在那里构建它;在任何情况下,我们都会解密数据,并将其作为String返回。我们也会把它打印出来。
仅出于演示目的,我们还将打印出实际的RAW(如果不是null),放在括号“()”内。如果我们想在这段代码的多次运行中跟踪它,我们会发现每次都不一样——因为在不同的 Oracle 会话中使用了不同的密码密钥。
在每次调用我们的存储过程进行数据加密时,我们将关闭OracleCallableStatement。我们也可以关闭ResultSet,尽管这是不必要的——当我们关闭Statement时,它本来就是关闭的,因为它是通过Statement获得的。然而,显式关闭每个ResultSet是一种好的做法,特别是因为重用一个Statement是一种常见的做法——在单个Statement的生命周期中,您可能会打开多个ResultSets,并且为了确保您正在释放 Oracle 资源,您应该在使用完每个ResultSet后关闭它。
选择加密字符串中的所有列
我们从 Oracle 数据库中选择加密数据的第二个示例过程p_select_employees_secret不是选择并加密单个列,而是选择所有列,并将它们连接成一个逗号分隔的要加密的VARCHAR2。在这种情况下,没有任何数据以明文形式发送。在客户端,如果需要单独的数据列,您将需要解析解密的字符串以获取单独的数据元素。
同样在这个例子中,我们使用逗号作为字段之间的分隔符。这假设数据中没有逗号——这通常不是一个有效的假设。您可以使用其他不太可能出现在数据中的分隔符,如插入符号(^)or 波浪号(~)。这只是一个示例,在为您的应用构建这样的过程之前,您需要评估您对数据的特定要求。
我们调用这个过程,解密数据并打印结果,如清单 7-23 中的部分所示。
***清单 7-23。*加密从员工中选择的所有数据,来自p_select_employees_secret
stmt = ( OracleCallableStatement )conn.prepareCall( "CALL hr.hr_sec_pkg.p_select_employees_secret(?,?,?,?,?,?,?,?,?)" ); ... if( rs.next() ) { System.out.**print**( OracleJavaSecure.**getDecryptData( rs.getRAW( 1 )**, sessionSecretDESPassPhrase, sessionSecretDESAlgorithm, sessionSecretDESSalt, sessionSecretDESIterationCount ) ); if( null != rs.getRAW( 1 ) ) System.out.**print**( " (" + **rs.getRAW( 1 ).stringValue()** + ")" ); System.out.print( "\n" );
同样,我们将加密的RAW的String值打印在数据旁边的括号中。在这种情况下,有一个单独的RAW代表整个串联数据行。
将加密数据发送到 Oracle 数据库进行插入/更新
我不会说下一个示例过程与我们前面的示例没有相似之处,但是您会注意到在两个方向上都没有密钥交换的痕迹。这是一个更新,为了在客户端加密数据,我们已经交换了密钥。清单 7-24 显示了调用我们的加密数据更新过程p_update_employees_sensitive的代码。
***清单 7-24。*更新员工中的敏感数据,调用p_update_employees_sensitive
` stmt = ( OracleCallableStatement )conn.prepareCall( "CALL hr.hr_sec_pkg.p_update_employees_sensitive(?,?,?,?,?,?,?,?,?,?,?,?,?)" ); stmt.registerOutParameter( 12, OracleTypes.NUMBER ); stmt.registerOutParameter( 13, OracleTypes.VARCHAR ); stmt.setInt( 1, 300 ); stmt.setString( 2, "David" ); stmt.setString( 3, "Coffin" ); stmt.setString( 4, "DAVID.COFFIN" ); stmt.setString( 5, "800.555.1212" ); stmt.setDate( 6, new Date( ( new java.util.Date() ).getTime() ) ); stmt.setString( 7, "SA_REP" ); // Note - may not have locModulus, locExponent, at this time! stmt.setRAW( 8, OracleJavaSecure.getCryptData( "9000.25" ) ); stmt.setRAW( 9, OracleJavaSecure.getCryptData( "0.15" ) ); stmt.setInt( 10, 147 ); stmt.setInt( 11, 80 ); stmt.setInt( 12, 0 ); stmt.setNull( 13, OracleTypes.VARCHAR ); stmt.executeUpdate();
errNo = stmt.getInt( 12 ); if( errNo != 0 ) { errMsg = stmt.getString( 13 ); System.out.println( "Oracle error 3) " + errNo + ", " + errMsg ); } else System.out.println( "Oracle success 3)" ); if( null != stmt ) stmt.close();`
对于我们的加密数据,我们首先调用getCryptData()方法。然后,我们在 Oracle 过程中设置一个RAW参数。当我们执行此语句时,明文和加密数据值都被发送到 Oracle 数据库进行插入或更新,这由过程决定。
两个日期类的故事
和我们之前的例子一样,在这里的清单 7-24 中,我们正在设置我们的参数,除了这一次,我们正在设置数据而不是关键工件。在我们的第六个参数中,我们实例化了一个java.util.Date 类。不带参数的构造函数创建一个带有当前日期和时间的Date。对于 Oracle 数据库,我们经常会使用java.sql.Date的实例(注意包 java.sql 而不是 java.util ),它没有这样的构造函数;然而,我们可以用来自java.util.Date.getTime()方法的 long(毫秒)来构造一个java.sql.Date。我们也可以使用与java.util.Date()构造函数相同的语句来实例化一个java.sql.Date对象,如下所示:
java.sql.Date( System.currentTimeMillis() );
对于我们在代码中使用的语法,我有一个观察。我们实例化的( new java.util.Date() )周围的括号允许我们直接访问它作为一个对象,访问它的getTime()方法,就像这样:
stmt.setDate( 6, **new Date( ( new java.util.Date() ).getTime())**);
好吧,那么为什么java.sql.Date没有一个Date()构造函数,它不是通过扩展java.util.Date继承了一个吗?哦,好问题!简单的回答是,构造函数不被认为是类的成员,所以它们不会被子类继承。然而,您可以通过调用super()方法作为任何子类构造函数的第一行来访问父构造函数。可以有多个具有不同签名的super()方法,每个方法对应父类中的一个构造函数。
我们将在第九章的中再次回到 java.sql 和Java . util的主题。我们需要交换日期的标准做法,我将在那里提出一个。
从雇员中选择单个行
也许我们不想选择整个表,而只想选择满足特定条件的记录。我们可以在查询中提供标准作为WHERE子句的一部分。在这个过程中,p_select_employee_by_id_sens,清单 7-25 ,我们提供了第十个参数,EMPLOYEE_ID。
***清单 7-25。*按 ID 选择,敏感数据来自员工,来自p_select_employee_by_id_sens
stmt = ( OracleCallableStatement )conn.prepareCall( "CALL hr.hr_sec_pkg.p_select_employee_by_id_sens(?,?,?,?,?,?,?,?,?,?)" ); ... **stmt.setInt( 10, 300 )**; // Employee ID 300 stmt.executeUpdate();
过程调用的其余部分与我们之前的查询示例相同,除了我们只期望返回一条或零条记录(EMPLOYEE_ID是该表的一个UNIQUE键)。)所以我们可以在一个if( rs.next() )块中处理ResultSet,而不是while( rs.next() )。
按姓氏选择雇员数据:尝试 SQL 注入
我们还可以查询满足其他一些标准的所有记录,这些标准可能不是UNIQUE。在部分清单 7-26 中,我们将展示LAST_NAME使用p_select_employee_by_ln_sens过程进行的查询EMPLOYEES的一个例子。有两个条目带有LAST_NAME“国王”。所以这将返回两行。
***清单 7-26。*按姓氏选择,敏感数据来自员工,来自p_select_employee_by_ln_sens
stmt = ( OracleCallableStatement )conn.prepareCall( "CALL hr.hr_sec_pkg.p_select_employee_by_ln_sens(?,?,?,?,?,?,?,?,?,?)" ); ... stmt.setString( 10, "King" ); // Employees Janette and Steven King ... while( rs.next() ) {
使用while块遍历ResultSet以查看所有返回的行。
我们可以尝试 SQL 注入,将之前的参数 10 设置替换为以下内容:
stmt.setString( 10, "King' or 'a'='a" );
我们将看到的是没有数据返回,因为没有EMPLOYEES有LAST_NAME{ King’或‘a’=‘a }。
通过 RAW 选择员工数据:尝试 SQL 注入
也许有人会想,如果我们把数据作为一个RAW返回,并且只在做出选择的时候把它转换成一个VARCHAR2,我们就可以突破并完成 SQL 注入(参考 Oracle 过程,p_select_employee_by_raw_sens,我们在前面和清单 7-16 中描述过它)。)这个过程调用,部分清单 7-27 ,尝试了那个策略。
***清单 7-27。*通过原始值选择,敏感数据来自员工,来自p_select_employee_by_raw_sens
stmt = ( OracleCallableStatement )conn.prepareCall( "CALL hr.hr_sec_pkg.p_select_employee_by_raw_sens(?,?,?,?,?,?,?,?,?,?)" ); ... stmt.setRAW( 10, new RAW("King' or 'a'='a".**getBytes()**) );
您将再次看到,我们在存储过程中对 SQL 注入的尝试失败了。与在动态 SQL 中嵌入用户提供的文本相反,传递参数似乎非常抵制 SQL 注入。
还要注意我们如何对待引号之间的值,就好像它已经是一个String对象,调用getBytes()方法。我们首先在《??》第六章中看到了这一点。
使用新的客户端密钥测试加密失败
也许你需要亲眼看看,也许不需要。在任何情况下,如果客户机上的键与 Oracle 数据库上的键不匹配,您都可以测试调用过程:它将失败。请注意,到目前为止,在 Java 代码的TestOracleJavaSecure.main()方法中,我们已经交换了密钥,这些密钥将继续工作。我们将在一瞬间移除或禁用客户端上的密钥。我们通过调用resetKeys()方法来做到这一点(参见清单 7-28 )。
***清单 7-28。*用混合密钥测试加密
OracleJavaSecure.resetKeys(); // Method for Chapter 7 testing only locModulus = OracleJavaSecure.getLocRSAPubMod(); locExponent = OracleJavaSecure.getLocRSAPubExp();
我们还通过调用getLocRSAPubMod()方法在客户端建立新的密钥,并获得公共模数和指数。如果我们不努力将这些公钥工件传递给 Oracle 数据库并检索新的密码密钥工件作为回报,那么更新(不交换密钥)将会失败。这就是我们将要测试的。
请注意,我们不太可能会意外重置我们的键,多次调用getLocRSAPubMod()方法或OracleJavaSecure的其他方法不会创建新的键(如果它们已经存在),而是会返回现有的键。这些键是static,所以只要 Java 虚拟机继续运行,它们就不会自己消失。所以,这个测试真的只是为了好玩:回答一个“如果”的问题。
新 Oracle 连接测试失败
我们可以演示的另一个测试是我们关闭 Oracle 数据库的Connection并建立一个新数据库的场景。服务器端密钥将会消失——客户端公钥和密码密钥都将消失。我们可以重置Connection并准备调用我们的加密程序,方法是将我们的安全应用角色设置为HRVIEW_ROLE,如清单 7-29 中的所示。
***清单 7-29。*重置连接并重置安全应用角色
if ( null != conn ) conn.close(); conn = DriverManager.getConnection( appusrConnString ); OracleJavaSecure.setConnection( conn ); stmt = ( OracleCallableStatement )conn.prepareCall( "CALL appsec.p_check_hrview_access()" ); stmt.executeUpdate();
同样,在进行加密的 Oracle 更新的情况下,我们将看到一个失败,因为 Oracle 数据库既没有公钥也没有密码密钥。这段代码最初是注释的。
一些结束语
我已经在前面的章节中介绍了所有的大型演示和测试。现在,轮到您修改测试并尝试自己的测试了。请随意评论和取消评论我们刚刚描述的那些测试代码部分。当您重新编译并运行TestOracleJavaSecure的这些部分时,您将看到一条关于尝试数据更新的消息,表明我们“在预期的地方失败了——没问题”
如果您需要关闭/打开您的连接
当我们选择加密数据时,重置您的 Oracle 连接会带来另一个潜在的问题,因为我们肯定会将我们的公钥传递给 Oracle 数据库,而数据库会构建密码密钥并将其交还。但是,我们仍然在客户机上保留一份旧的秘密密码密钥,解密数据时会遇到问题。在我们尝试解密数据之前,我们可以通过立即调用OracleJavaSecure.makeDESKey()方法,在客户机上获取并解密秘密的密码密钥工件之后进行补救。
我认为最好的经验法则,以及对代码审查清单的检查,是在查询和更新期间保持 Oracle 连接。如果有长时间的暂停,并且您需要关闭 Oracle 连接,那么当您打开一个新的连接时,通过p_get_shared_passphrase过程将您的公钥工件传递到 Oracle 数据库(如下所述),并通过调用makeDESKey()方法在客户机上构建一个替换的密码密钥。
运行无数据加密的基本密钥交换
当我们只想向 Oracle 数据库提交加密的数据更新时,或者我们想在进行任何选择之前做好更新准备时,我们需要确保我们事先已经交换了密钥。我们可以通过调用p_get_shared_passphrase过程来实现(在我们当前的设计中,这个过程必须包含在每个单独的应用包中,比如hr_sec_pkg)。清单 7-30 展示了从 Java 客户端进行基本密钥交换的基础。
***清单 7-30。*基本密钥交换
stmt = ( OracleCallableStatement )conn.prepareCall( "CALL hr.hr_sec_pkg.p_get_shared_passphrase(?,?,?,?,?,?,?,?)" ); ... OracleJavaSecure.makeDESKey( sessionSecretDESPassPhrase, sessionSecretDESAlgorithm, sessionSecretDESSalt, sessionSecretDESIterationCount );
我们将获得并解密我们的秘密密码密钥的每个工件,并将其传递给makeDESKey()方法。此时,我们已经完成了密钥交换,并准备好交换加密数据,并在客户端和 Oracle 数据库上进行解密。
在我们调用了p_get_shared_passphrase过程和OracleJavaSecure.makeDESKey()之后,TestOracleJavaSecure类将再次尝试进行加密数据更新,并将成功。
执行演示和测试
我们现在将运行我们的演示和测试。为此,我们将根据需要再次编辑我们的代码,然后编译并运行它。在命令提示符下,将目录更改为 第七章 。编辑TestOracleJavaSecure.java如果还没有,将appusr的正确密码和正确的主机和端口号放入 Oracle 连接字符串,靠近顶部。
private static String appusrConnString = "jdbc:oracle:thin:appusr/password@localhost:1521:Orcl";
用这些命令编译代码,或者只编译第二个命令,第二个命令会自动编译第一个命令(确保对OracleJavaSecure.java的第一行进行了注释,这一行指向CREATEOracle 数据库中的 Java 结构)。
javac orajavsec/OracleJavaSecure.java javac TestOracleJavaSecure.java
然后使用以下命令运行同一目录中的代码:
java TestOracleJavaSecure
观察结果
当您执行TestOracleJavaSecure(分布式的)时,前面列出的所有测试将从上到下直接运行。结果将如下所示:
Count data in V_EMPLOYEES_PUBLIC: 108 Count data in V_EMPLOYEES_PUBLIC: 108 Oracle success 1) 198, Donald, OConnell, DOCONNEL, 650.507.9833, 2007-06-21 00:00:00, SH_CLERK, 2600 (E27811A8C7C9D9F3), null, 124, 50 Oracle success 2) 198, Donald, OConnell, DOCONNEL, 650.507.9833, 21-JUN-07, SH_CLERK, 2600, , 124, 50 (F7EA4E97B2F39E036AF6E880B2E5CA3EB78332BF8CE82B7585A4CBC7B340FEBDE4862830927 D118D27A1DDE3304478D9A463EBA9BC78E3188217884D5F5EA92F54A6EA2FB62598D1419F003295D F1C076E48BC6D07058E3B) Oracle success 3) Oracle success 4) 300, David, Coffin, DAVID.COFFIN, 800.555.1212, 2010-08-30 00:00:00, SA_REP, 9000.25, .15, 147, 80 Oracle success 5) No data on failed SQL Injection Oracle success 6) No data on failed SQL Injection Failed where expected - OK. Need key exchange. Oracle success 8) Oracle success 9)
演示场景
这里用相对简单的英语列出了我们演示过的场景。有大量的代码来完成所有这些不同的场景。每个场景的代码与其他一些场景非常相似,只是针对特定的演示进行了修改。
- 我们查询了
EMPLOYEES表,得到了加密形式的SALARY和COMMISSION_PCT列。对于这两者,我们打印出解密的String,在括号中,是加密的RAW的stringValue()(除非为空)。我们只展示了ResultSet的第一排。 - 我们查询了该表,并以加密的形式将所有列放回一个串联的
String中。我们打印解密的数据,在括号中是加密的RAW的stringValue()。同样,我们只显示了第一行。 - 我们对
EMPLOYEES表进行插入或更新,插入EMPLOYEE_ID = 300。如果它已经存在,我们进行更新。到那时工资是 9000.25(现在我在做梦)。 - 我们从
EMPLOYEES中选择一行,请求数据WHERE EMPLOYEE_ID = 300。 - 我们试图用一个样本 SQL 注入字符串在我们的过程中查询
EMPLOYEES。这将失败,并且不会返回任何数据。 - 我们再次尝试使用示例 SQL 注入字符串通过我们的过程查询
EMPLOYEES,这一次是作为RAW传输的,只有在执行SELECT时才会被转换。这同样会失败,并且不会返回任何数据。 - 我们也可以编译
TestOracleJavaSecure来测试重置客户端密钥或重置 Oracle 连接。之后,我们向 Oracle 数据库发送加密数据进行插入/更新的尝试失败了,这是意料之中的。 - 我们成功地调用了
p_get_shared_passphrase过程并运行了makeDESKey()方法来完成密钥交换。 - 同样,我们将加密的数据发送到 Oracle 数据库进行插入/更新,我们成功了。现在工资 9700.75(现在老婆在做梦)。
查询员工以查看更新
通过作为HR用户连接到 Oracle 数据库并执行以下命令,您可以看到我们插入/更新的记录的状态:
SELECT * FROM employees WHERE employee_id=300;
打包模板实现加密
我知道人们发现安全成本,就像质量成本一样,令人不快;但不安全的潜在成本高得离谱。我们的安全结构只有投入使用才有价值。我们将在接下来的几章中增加一些价值,但就目前的情况来看,让人们使用我们的安全结构可能就像拔牙一样困难。这不是那种“如果你建造了它,他们就会来”的时刻。
我们将通过为应用开发人员提供模板来降低进入这些安全结构的门槛,他们可以使用这些模板来快速实现其应用的后端 Oracle 结构,以及对这些结构的前端 Java 调用。
Oracle 应用安全结构模板
我们将提供给开发人员的第一个文件提供了 Oracle 安全包的代码,他们需要在他们的应用模式中实现该包。在该文件中,有应用模式、包、过程、表/视图和列的通用名称。开发人员应该搜索这些名称,并用他们将使用的实际名称替换它们。
注你可以在 这个文件第七章 /AppPkgTemplate.sql 。
您的安全管理员secadm需要将appsec.app_sec_pkg上的 execute 授予开发人员的应用模式用户,这样应用就可以使用app_sec_pkg过程和函数。
为了创建应用包,开发人员将需要CREATE PROCEDURE系统权限。此外,为了让应用用户使用应用包,所有应用用户都需要被授予包的EXECUTE对象权限。
我们的软件包中有四个模板程序:
p_get_shared_passphrase p_select_APPTABLE_sensitive p_select_APPTABLE_by_COLUMN1_sens p_update_APPTABLE_sensitive
这里没有向您介绍任何新的东西——您已经看到了在HR模式中使用的所有这些结构。
【Java 调用应用安全的模板
我们还将为我们的应用开发人员提供一个模板 Java 文件。每个开发人员将搜索和替换模式、表、过程等的通用名称。应用中任何合适的名称。
注你可以在 第七章/**AppJavaTemplate.java中找到
AppAccessSecure模板类的 Java 代码。
这个文件与我们在HR模式、TestOracleJavaSecure.java中的结构测试代码非常相似。它由一个main()方法组成,该方法建立一个 Oracle 连接并调用开发人员将在应用模式中定义的应用结构。
对于应用开发人员来说,这可能是最令人生畏的代码,因为需要进行大量的密钥交换。你现在是专家了,所以你最好给你的应用开发者一些帮助。事实上,如果您能够帮助开发人员正确地实现这一点,并避免任何安全缺陷,就像我们对HR.EMPLOYEES表所做的那样,这将会为您省去一些麻烦。
应用使用的 Java 档案
除了为您的开发人员提供这两个模板文件之外,您还需要为他们提供orajavsec/Oracle javasecure . class文件。我建议你不要给开发人员 OracleJavaSecure.java 的代码文件,只是为了保证你的组织中没有使用这个类的修改版本。
可能用于分发类文件的最佳形式是 Java 归档(JAR)文件的形式。要创建合适的 jar 文件进行分发,可以运行 JDK 附带的 JAR 工具。如果你的PATH和CLASSPATH仍然按照第二章中的描述设置,你可以得到一个命令提示窗口,将目录切换到第七章第七章目录。从那里,执行以下命令:
jar cvf orajavsec.jar orajavsec/OracleJavaSecure.class
这将在当前目录下创建一个名为 orajavsec.jar 的文件。分发这个文件,并指示您的应用开发人员在开发期间和运行他们的应用代码时将这个文件名放在他们的CLASSPATH中。
现在不要停止
有了模板,我们就可以让 Oracle 应用开发人员了解网络上的加密数据,而您最好选择一款应用,并将其配置为使用这些结构和方法,以开辟道路。但这只是安全喘息的中途之家。我们将在接下来的章节中介绍一些强大的概念,这些概念将会吸引你的应用开发人员继续使用这个程序。步入安全的应用开发和操作就像找回自己的生活。安全感是一种幸福感。这需要努力,但有了努力,你就能决定自己的安全计算命运。
章节回顾
我们将在第六章中学到的密钥交换和数据加密概念应用于一个特定的 Oracle 应用,即HR模式。这是科学史上的伟大时刻之一吗?也许不是,但这是一个里程碑,是我们在应用安全性方面取得更大、更好成就的跳板。我们已经成功加密了通过网络传输的敏感应用数据。
我们构建了错误日志记录的结构,专门用于跟踪涉及我们的应用安全流程的错误和事件,而不管什么客户端应用当前正在使用它们。此外,我们构建了一个触发器来自动管理日志,丢弃超过 45 天的记录。
本章介绍了许多代码,包括 PL/SQL 和 Java。这段代码的目标相当简单——当从HR模式中检索敏感数据和记录时,以及当更新敏感数据和记录时,使用我们现有的数据加密过程。
一路上,我介绍了以下概念:
- 使用性能索引
- 触发器、自主事务和
COMMIT - 序列的使用和操作
- 默认程序参数
CURSOR TYPE的使用- 通过引用使用类型规范,数据类型的锚定声明
- 使用
java.util.Date和java.sql.Date
我们花了大量时间评估和测试我们对 SQL 注入攻击的防护能力。
最后,我们展示了可以交付给应用程序员的模板,以便他们可以在自己的应用中实现这些安全结构。
请研究图 7-1 和图 7-2 以获得安全应用数据查询和更新过程的直观概述。图 7-1 展示了在 Oracle 数据库中查询敏感数据并以加密形式检索的过程。在顶部,您可以看到对图 5-1**【A】**的引用,这是我们看到在客户端上创建 RSA 私钥/公钥对的地方。然后在图 7-1 中,我们看到客户端调用p_check_hrview_access来设置访问敏感HR数据的角色。
此时,我们拥有调用hr_sec_pkg包的执行特权。我们调用p_select_employees_sensitive从EMPLOYEES表中获取公共和敏感数据。首要任务是完成密钥交换。您可以在图 7-1 的右侧看到,我们在 Oracle 数据库上创建了一个等效的 RSA 公钥,我们创建了将返回给客户端的加密密码(DES)密钥。我们在图 6-1**【B】**中看到了那个过程的细节。
Oracle 数据库上该过程的下一步是查询EMPLOYEES表并加密SALARY和COMMISSION_PCT字段。最后,该过程向客户端返回数据,包括明文字段和敏感字段的加密形式。
回到客户端(图 7-1 的左侧),我们基于 Oracle 数据库返回的工件构建一个等价的密码密钥。请参见插图底部的关键图像。然后我们显示来自ResultSet的数据,使用秘密密码密钥解密敏感数据组件。
***图 7-1。*安全应用数据查询
图 7-2 说明了敏感数据的更新过程。在这个过程中,我们使用密码密钥对客户端的敏感数据进行加密。我们通过调用getCryptData()方法来实现。然后,我们通过调用p_update_employees_sensitive过程将明文字段和加密的敏感字段提交给 Oracle 数据库。
密钥交换必须已经发生,以便 Oracle 数据库可以使用等效的(原始)密码密钥来解密数据。然后程序执行一个INSERT或UPDATE命令来存储数据。
***图 7-2。*安全应用数据更新