apache shiro
- 加密和解密数据,
- 哈希数据,
- 生成随机数。
Shiro没有实现任何加密算法。 所有计算都委托给Java密码学扩展(JCE)API。 使用Shiro代替Java中已经存在的主要好处是易于使用和安全的默认值。 Shiro加密模块以更高的抽象级别编写,默认情况下实现所有已知的最佳实践。
这是致力于Apache Shiro的系列文章的第三部分。 第一部分显示了如何保护Web应用程序的安全以及如何添加登录/注销功能。 第二部分介绍了如何在数据库中存储用户帐户,以及如何为用户提供通过PGP证书进行身份验证的选项。
这篇文章从Shiro和JCE的简短概述开始,并继续介绍一些有用的转换实用程序 。 以下各章介绍了随机数的生成 , 散列以及如何加密和解密数据。 最后一章介绍了如何自定义密码以及如何创建新密码。
总览
Shiro加密模块位于org.apache.shiro.crypto
包中。 它没有手册,但是幸运的是,所有加密类都是Javadoc繁重的。 Javadoc包含将用手动编写的所有内容。
Shiro在很大程度上依赖于Java密码学扩展。 您无需了解JCE即可使用Shiro。 但是,您需要JCE基础知识来对其进行自定义或添加新功能。 如果您对JCE不感兴趣,请跳到下一章。
JCE是一组高度可定制的API及其默认实现。 它是基于提供程序的。 如果默认实现没有您需要的实现,则可以轻松安装新的提供程序。
每个密码,密码选项,哈希算法或任何其他JCE功能都有一个名称。 JCE为算法和算法模式定义了两组 标准名称 。 这些可用于任何JDK。 任何提供程序(例如Bouncy Castle )都可以使用新算法和选项自由扩展名称集。
名称由所谓的转换字符串组成,用于查找所需的对象。 例如, Cipher.getInstance('DES/ECB/PKCS5Padding')
在ECB模式下以PKCS#5填充返回DES密码。 返回的密码通常需要进一步的初始化,可能不使用安全的默认值,也不是线程安全的。
Apache Shiro组成转换字符串,配置获取的对象并为其添加线程安全性。 最重要的是,它具有易于使用的API,并添加了无论如何都应实施的更高级别的最佳实践。
编码,解码和ByteSource
加密包对字节数组( byte[]
)进行加密,解密和散列。 如果需要对字符串进行加密或哈希处理,则必须先将其转换为字节数组。 相反,如果需要将散列或加密的值存储在文本文件或字符串数据库列中,则必须将其转换为字符串。
文本到字节数组
静态类CodecSupport能够将文本转换为字节数组并返回。 方法byte[] toBytes(String source)
将字符串转换为字节数组,方法String toString(byte[] bytes)
将其转换回来。
例
使用编解码器支持在文本和字节数组之间进行转换 :
@Test
public void textToByteArray() {
String encodeMe = 'Hello, I'm a text.';
byte[] bytes = CodecSupport.toBytes(encodeMe);
String decoded = CodecSupport.toString(bytes);
assertEquals(encodeMe, decoded);
}
编码和解码字节数组
从字节数组到字符串的转换称为编码。 反向过程称为解码。 Shiro提供了两种不同的算法:
- 在
Base64
类中实现的Base64
, - 在
Hex
类中实现的Hex
。
这两个类都是静态的,并且都具有encodeToString
和decode
实用程序方法。
例子
将随机数组编码为其十六进制表示形式,对其进行解码并验证结果:
@Test
public void testStaticHexadecimal() {
byte[] encodeMe = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
String hexadecimal = Hex.encodeToString(encodeMe);
assertEquals('020406080a0c0e101214', hexadecimal);
byte[] decoded = Hex.decode(hexadecimal);
assertArrayEquals(encodeMe, decoded);
}
将随机数组编码为其Byte64表示形式 ,对其进行解码并验证结果:
@Test
public void testStaticBase64() {
byte[] encodeMe = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
String base64 = Base64.encodeToString(encodeMe);
assertEquals('AgQGCAoMDhASFA==', base64);
byte[] decoded = Base64.decode(base64);
assertArrayEquals(encodeMe, decoded);
}
字节源
加密程序包通常返回ByteSource
接口的实例,而不是字节数组。 它的实现SimpleByteSource
是字节数组的简单包装,提供了其他可用的编码方法:
-
String toHex()
–返回十六进制字节数组表示形式, -
String toBase64()
–返回Base64字节数组表示形式, -
byte[] getBytes()
–返回包装的字节数组。
例子
该测试使用ByteSource将数组编码成其十六进制表示形式。 然后解码并验证结果:
@Test
public void testByteSourceHexadecimal() {
byte[] encodeMe = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
ByteSource byteSource = ByteSource.Util.bytes(encodeMe);
String hexadecimal = byteSource.toHex();
assertEquals('020406080a0c0e101214', hexadecimal);
byte[] decoded = Hex.decode(hexadecimal);
assertArrayEquals(encodeMe, decoded);
}
使用Bytesource将数组编码成其Base64表示形式。 对其进行解码并验证结果:
@Test
public void testByteSourceBase64() {
byte[] encodeMe = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
ByteSource byteSource = ByteSource.Util.bytes(encodeMe);
String base64 = byteSource.toBase64();
assertEquals('AgQGCAoMDhASFA==', base64);
byte[] decoded = Base64.decode(base64);
assertArrayEquals(encodeMe, decoded);
}
随机数发生器
随机数生成器由RandomNumberGenerator接口及其默认实现SecureRandomNumberGenerator组成 。
该接口非常简单,只有两种方法:
-
ByteSource nextBytes()
–生成一个随机的固定长度的字节源, -
ByteSource nextBytes(int numBytes)
–生成具有指定长度的随机字节源。
默认实现实现了这两种方法,并提供了一些其他配置:
-
setSeed(byte[] bytes)
–自定义种子配置, -
setDefaultNextBytesSize(int defaultNextBytesSize)
–nextBytes()
输出的长度。
种子是一个数字(实际上是字节数组),用于初始化随机数生成器。 它允许您生成“可预测的随机数”。 用同一种子初始化的同一随机生成器的两个实例始终生成相同的随机数序列。 它对于调试很有用,但要非常小心。
如果可以,请不要为加密指定自定义种子。 使用默认值。 除非您真的知道自己在做什么,否则攻击者可能会猜出定制的那个。 这将超过随机数的所有安全性目的。
在幕后:SecureRandomNumberGenerator将随机数生成委托给JCE SecureRandom实现。
例子
第一个示例创建两个随机数生成器,并验证它们是否生成两个不同的事物:
@Test
public void testRandomWithoutSeed() {
//create random generators
RandomNumberGenerator firstGenerator = new SecureRandomNumberGenerator();
RandomNumberGenerator secondGenerator = new SecureRandomNumberGenerator();
//generate random bytes
ByteSource firstRandomBytes = firstGenerator.nextBytes();
ByteSource secondRandomBytes = secondGenerator.nextBytes();
//compare random bytes
assertByteSourcesNotSame(firstRandomBytes, secondRandomBytes);
}
第二个示例创建两个随机数生成器,使用相同的种子对其进行初始化,并检查它们是否生成相同的预期20字节长的随机数组:
@Test
public void testRandomWithSeed() {
byte[] seed = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//create and initialize first random generator
SecureRandomNumberGenerator firstGenerator = new SecureRandomNumberGenerator();
firstGenerator.setSeed(seed);
firstGenerator.setDefaultNextBytesSize(20);
//create and initialize second random generator
SecureRandomNumberGenerator secondGenerator = new SecureRandomNumberGenerator();
secondGenerator.setSeed(seed);
secondGenerator.setDefaultNextBytesSize(20);
//generate random bytes
ByteSource firstRandomBytes = firstGenerator.nextBytes();
ByteSource secondRandomBytes = secondGenerator.nextBytes();
//compare random arrays
assertByteSourcesEquals(firstRandomBytes, secondRandomBytes);
//following nextBytes are also the same
ByteSource firstNext = firstGenerator.nextBytes();
ByteSource secondNext = secondGenerator.nextBytes();
//compare random arrays
assertByteSourcesEquals(firstRandomBytes, secondRandomBytes);
//compare against expected values
byte[] expectedRandom = {-116, -31, 67, 27, 13, -26, -38, 96, 122, 31, -67, 73, -52, -4, -22, 26, 18, 22, -124, -24};
assertArrayEquals(expectedRandom, firstNext.getBytes());
}
散列
哈希 函数将任意长数据作为输入,并将其转换为较小的固定长度数据。 哈希函数的结果称为哈希。 散列是一种操作方式。 无法将哈希转换回原始数据。
要记住的最重要的事情是:始终存储密码哈希而不是密码本身。 永远不要直接存储它。
Shiro提供了两个与哈希相关的接口,均支持安全密码哈希所必需的两个概念:盐析和哈希迭代:
-
Hash
-表示哈希算法。 -
Hasher
-用此来哈希密码。
盐是散列前与密码连接的随机数组。 它通常与密码一起存储。 没有盐,相同的密码将具有相同的哈希。 这将使密码破解变得容易得多。
指定许多哈希迭代,以减慢哈希操作的速度。 操作越慢,破解存储密码的难度就越大。 使用很多迭代。
杂凑
哈希接口实现计算哈希函数。 四郎器具六个标准散列函数: MD2 , 被Md5 , SHA1 , SHA256 , SHA384和SHA512 。
每个哈希实现都从ByteSource扩展。 构造函数获取输入数据,盐和所需的迭代次数。 盐和迭代数是可选的。
ByteSource接口方法返回:
-
byte[] getBytes()
–哈希, -
String toBase64()
– Base64表示形式的哈希, -
String toHex()
-以十六进制表示形式的哈希值。
以下代码可计算出不含盐的“ Hello Md5”文本的Md5哈希值:
@Test
public void testMd5Hash() {
Hash hash = new Md5Hash('Hello Md5');
byte[] expectedHash = {-7, 64, 38, 26, 91, 99, 33, 9, 37, 50, -22, -112, -99, 57, 115, -64};
assertArrayEquals(expectedHash, hash.getBytes());
assertEquals('f940261a5b6321092532ea909d3973c0', hash.toHex());
assertEquals('+UAmGltjIQklMuqQnTlzwA==', hash.toBase64());
print(hash, 'Md5 with no salt iterations of 'Hello Md5': ');
}
下一个代码段用salt计算Sha256的10次迭代:
@Test
public void testIterationsSha256Hash() {
byte[] salt = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Hash hash = new Sha256Hash('Hello Sha256', salt, 10);
byte[] expectedHash = {24, 4, -97, -61, 70, 28, -29, 85, 110, 0, -107, -8, -12, -93, -121, 99, -5, 23, 60, 46, -23, 92, 67, -51, 65, 95, 84, 87, 49, -35, -78, -115};
String expectedHex = '18049fc3461ce3556e0095f8f4a38763fb173c2ee95c43cd415f545731ddb28d';
String expectedBase64 = 'GASfw0Yc41VuAJX49KOHY/sXPC7pXEPNQV9UVzHdso0=';
assertArrayEquals(expectedHash, hash.getBytes());
assertEquals(expectedHex, hash.toHex());
assertEquals(expectedBase64, hash.toBase64());
print(hash, 'Sha256 with salt and 10 iterations of 'Hello Sha256': ');
}
比较由框架和客户端代码计算出的迭代 :
@Test
public void testIterationsDemo() {
byte[] salt = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//iterations computed by the framework
Hash shiroIteratedHash = new Sha256Hash('Hello Sha256', salt, 10);
//iterations computed by the client code
Hash clientIteratedHash = new Sha256Hash('Hello Sha256', salt);
for (int i = 1; i < 10; i++) {
clientIteratedHash = new Sha256Hash(clientIteratedHash.getBytes());
}
//compare results
assertByteSourcesEquals(shiroIteratedHash, clientIteratedHash);
}
在幕后:所有具体的哈希类都从SimpleHash
扩展,后者将哈希计算委托给JCE MessageDigest实现。 如果您希望使用其他哈希函数扩展Shiro,请直接对其进行实例化。 构造函数将JCE消息摘要(哈希)算法名称作为参数。
哈舍尔
Hasher在哈希函数之上工作,并实现了与盐腌相关的最佳实践。 该接口只有一种方法:
-
HashResponse computeHash(HashRequest request)
哈希请求提供了要哈希的字节源和可选的盐。 哈希响应返回哈希和盐。 响应盐不必与提供的盐相同。 更重要的是,它可能不是用于哈希操作的全部盐。
任何哈希器实现均可自由生成自己的随机盐。 仅当请求包含null
盐时,默认实现才执行此操作。 另外,用过的盐可以由“碱盐”和“公共盐”组成。 哈希响应中返回“公共盐”。
要了解为什么要用这种方法,必须记得盐通常与密码一起存储。 具有数据库访问权限的攻击者将拥有暴力攻击所需的所有信息。
因此,“公共盐”与密码存储在同一位置,“基本盐”存储在其他位置。 然后,攻击者需要访问两个不同的位置。
默认哈希器是可配置的。 您可以指定基本盐,要使用的迭代次数和哈希算法。 使用任何Shiro哈希实现中的哈希算法名称。 它还总是从哈希请求中返回公共盐。 观看演示 :
@Test
public void fullyConfiguredHasher() {
ByteSource originalPassword = ByteSource.Util.bytes('Secret');
byte[] baseSalt = {1, 1, 1, 2, 2, 2, 3, 3, 3};
int iterations = 10;
DefaultHasher hasher = new DefaultHasher();
hasher.setBaseSalt(baseSalt);
hasher.setHashIterations(iterations);
hasher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
//custom public salt
byte[] publicSalt = {1, 3, 5, 7, 9};
ByteSource salt = ByteSource.Util.bytes(publicSalt);
//use hasher to compute password hash
HashRequest request = new SimpleHashRequest(originalPassword, salt);
HashResponse response = hasher.computeHash(request);
byte[] expectedHash = {55, 9, -41, -9, 82, -24, 101, 54, 116, 16, 2, 68, -89, 56, -41, 107, -33, -66, -23, 43, 63, -61, 6, 115, 74, 96, 10, -56, -38, -83, -17, 57};
assertArrayEquals(expectedHash, response.getHash().getBytes());
}
如果您需要比较密码或数据校验和,请在同一哈希表中提供一个“公用盐”。 它将重现哈希操作。 该示例使用Shiro DefaultHasher
实现:
@Test
public void hasherDemo() {
ByteSource originalPassword = ByteSource.Util.bytes('Secret');
ByteSource suppliedPassword = originalPassword;
Hasher hasher = new DefaultHasher();
//use hasher to compute password hash
HashRequest originalRequest = new SimpleHashRequest(originalPassword);
HashResponse originalResponse = hasher.computeHash(originalRequest);
//Use salt from originalResponse to compare stored password with user supplied password. We assume that user supplied correct password.
HashRequest suppliedRequest = new SimpleHashRequest(suppliedPassword, originalResponse.getSalt());
HashResponse suppliedResponse = hasher.computeHash(suppliedRequest);
assertEquals(originalResponse.getHash(), suppliedResponse.getHash());
//important: the same request hashed twice may lead to different results
HashResponse anotherResponse = hasher.computeHash(originalRequest);
assertNotSame(originalResponse.getHash(), anotherResponse.getHash());
}
注意:由于上面示例中提供的公共盐为null
,因此默认的hashher会生成新的随机公共盐。
加密/解密
密码将数据加密为没有密钥的不可读的密文。 密码分为两组:对称和不对称。 对称密码使用相同的密钥进行加密和解密。 非对称密码使用两个不同的密钥,一个用于加密,另一个用于解密。
Apache Shiro包含两个对称密码: AES和Blowfish 。 两者都是无状态的,因此是线程安全的。 不支持非对称密码。
两种密码均能够生成随机加密密钥,并且均实现CipherService
接口。 该接口定义了两种加密和两种解密方法。 第一组用于字节数组的加密/解密:
-
ByteSource encrypt(byte[] raw, byte[] encryptionKey)
, -
ByteSource decrypt(byte[] encrypted, byte[] decryptionKey)
。
第二组加密/解密流:
-
encrypt(InputStream in, OutputStream out, byte[] encryptionKey)
, -
decrypt(InputStream in, OutputStream out, byte[] decryptionKey)
。
下一个代码段生成新密钥,使用AES密码对秘密消息进行加密,对其进行解密,并将原始消息与解密结果进行比较:
@Test
public void encryptStringMessage() {
String secret = 'Tell nobody!';
AesCipherService cipher = new AesCipherService();
//generate key with default 128 bits size
Key key = cipher.generateNewKey();
byte[] keyBytes = key.getEncoded();
//encrypt the secret
byte[] secretBytes = CodecSupport.toBytes(secret);
ByteSource encrypted = cipher.encrypt(secretBytes, keyBytes);
//decrypt the secret
byte[] encryptedBytes = encrypted.getBytes();
ByteSource decrypted = cipher.decrypt(encryptedBytes, keyBytes);
String secret2 = CodecSupport.toString(decrypted.getBytes());
//verify correctness
assertEquals(secret, secret2);
}
另一个片段显示了如何使用Blowfish加密/解密流。 Shiro密码不会关闭或刷新输入流或输出流。 您必须自己做:
@Test
public void encryptStream() {
InputStream secret = openSecretInputStream();
BlowfishCipherService cipher = new BlowfishCipherService();
// generate key with default 128 bits size
Key key = cipher.generateNewKey();
byte[] keyBytes = key.getEncoded();
// encrypt the secret
OutputStream encrypted = openSecretOutputStream();
try {
cipher.encrypt(secret, encrypted, keyBytes);
} finally {
// The cipher does not flush neither close streams.
closeStreams(secret, encrypted);
}
// decrypt the secret
InputStream encryptedInput = convertToInputStream(encrypted);
OutputStream decrypted = openSecretOutputStream();
try {
cipher.decrypt(encryptedInput, decrypted, keyBytes);
} finally {
// The cipher does not flush neither close streams.
closeStreams(secret, encrypted);
}
// verify correctness
assertStreamsEquals(secret, decrypted);
}
如果使用相同的密钥两次加密相同的文本 ,则会得到两个不同的加密文本:
@Test
public void unpredictableEncryptionProof() {
String secret = 'Tell nobody!';
AesCipherService cipher = new AesCipherService();
// generate key with default 128 bits size
Key key = cipher.generateNewKey();
byte[] keyBytes = key.getEncoded();
// encrypt two times
byte[] secretBytes = CodecSupport.toBytes(secret);
ByteSource encrypted1 = cipher.encrypt(secretBytes, keyBytes);
ByteSource encrypted2 = cipher.encrypt(secretBytes, keyBytes);
// verify correctness
assertArrayNotSame(encrypted1.getBytes(), encrypted2.getBytes());
}
前面的两个示例都使用Key generateNewKey()
方法生成密钥。 使用方法setKeySize(int keySize)
覆盖默认密钥大小(128位)。 或者,方法Key generateNewKey(int keyBitSize)
的方法的keyBitSize
参数以位为单位指定密钥大小。
有些密码仅支持某些密钥大小。 例如 ,AES仅支持128、192和256位日志密钥:
@Test(expected=RuntimeException.class)
public void aesWrongKeySize() {
AesCipherService cipher = new AesCipherService();
//The call throws an exception. Aes supports only keys of 128, 192, and 256 bits.
cipher.generateNewKey(200);
}
@Test
public void aesGoodKeySize() {
AesCipherService cipher = new AesCipherService();
//aes supports only keys of 128, 192, and 256 bits
cipher.generateNewKey(128);
cipher.generateNewKey(192);
cipher.generateNewKey(256);
}
就基础而言,就是这样。 您不需要更多的内容来加密和解密应用程序中的敏感数据。
更新:我在这里过于乐观。 了解更多信息总是很有用的,尤其是在处理敏感数据时。 此方法大部分是但不是完全安全的。 问题和解决方案都在我的另一篇文章中进行了介绍。
加密/解密-高级
上一章介绍了如何加密和解密某些数据。 本章将进一步介绍Shiro加密的工作原理以及如何对其进行自定义。 它还显示了如果标准的两个密码不适合您,那么如何轻松添加新密码。
初始化向量
初始化向量是在加密期间使用的随机生成的字节数组。 使用初始化向量的密码很难预测,因此对于攻击者来说很难解密。
Shiro自动生成初始化向量并将其用于加密数据。 然后,将向量与加密的数据连接起来并返回给客户端代码。 您可以通过在密码上调用setGenerateInitializationVectors(false)
将其关闭。 该方法在JcaCipherService
类上定义。 这两个默认加密类都对其进行了扩展。
初始化向量大小是特定于加密算法的。 如果默认大小(128位)不起作用,请使用setInitializationVectorSize
方法对其进行自定义。
随机发生器
关闭初始化向量不一定意味着密码变得可预测。 河豚和AES都具有随机性。
以下示例关闭了初始化向量,但是加密的文本仍然不同:
@Test
public void unpredictableEncryptionNoIVProof() {
String secret = 'Tell nobody!';
AesCipherService cipher = new AesCipherService();
cipher.setGenerateInitializationVectors(false);
// generate key with default 128 bits size
Key key = cipher.generateNewKey();
byte[] keyBytes = key.getEncoded();
// encrypt two times
byte[] secretBytes = CodecSupport.toBytes(secret);
ByteSource encrypted1 = cipher.encrypt(secretBytes, keyBytes);
ByteSource encrypted2 = cipher.encrypt(secretBytes, keyBytes);
// verify correctness
assertArrayNotSame(encrypted1.getBytes(), encrypted2.getBytes());
}
可以自定义或关闭随机性。 但是,永远不要在生产代码中这样做。 随机性是安全数据加密的绝对必要条件。
两种Shiro加密算法都从JcaCipherService
类扩展。 该类具有setSecureRandom(SecureRandom secureRandom)
方法。 安全随机数是标准的Java JCE随机数生成器。 扩展它以创建自己的实现,并将其传递给密码。
我们的SecureRandom
ConstantSecureRandom
实现始终返回零。 我们将其提供给密码,然后关闭初始化向量以创建不安全的可预测加密 :
@Test
public void predictableEncryption() {
String secret = 'Tell nobody!';
AesCipherService cipher = new AesCipherService();
cipher.setSecureRandom(new ConstantSecureRandom());
cipher.setGenerateInitializationVectors(false);
// define the key
byte[] keyBytes = {5, -112, 36, 113, 80, -3, -114, 77, 38, 127, -1, -75, 65, -102, -13, -47};
// encrypt first time
byte[] secretBytes = CodecSupport.toBytes(secret);
ByteSource encrypted = cipher.encrypt(secretBytes, keyBytes);
// verify correctness, the result is always the same
byte[] expectedBytes = {76, 69, -49, -110, -121, 97, -125, -111, -11, -61, 61, 11, -40, 26, -68, -58};
assertArrayEquals(expectedBytes, encrypted.getBytes());
}
持续安全的随机实现是漫长而无趣的。 在Github上可用 。
自定义密码
开箱即用Shiro仅提供Blowfish和AES加密方法。 该框架没有实现自己的算法。 而是将加密委托给JCE类。
Shiro仅提供安全的默认设置和更简单的API。 这种设计使得可以使用任何JCE分组密码来扩展Shiro。
块密码对每个块的消息进行加密。 所有块具有相等的固定大小。 如果最后一个块太短,则添加填充以使其与其他所有块相同。 每个块被加密并与先前加密的块组合。
因此,您必须配置:
- 加密方法
- 块大小
- 填充
- 如何结合块 。
加密方式
自定义密码扩展了DefaultBlockCipherService
类。 该类只有一个带有一个参数的构造函数:算法名称。 您可以提供任何与JCE兼容的算法名称 。
例如,这是Shiro AES密码的源代码:
public class AesCipherService extends DefaultBlockCipherService {
private static final String ALGORITHM_NAME = 'AES';
public AesCipherService() {
super(ALGORITHM_NAME);
}
}
AES不需要指定其他加密参数(块大小,填充,加密方法)。 对于AES,默认值足够好。
块大小
默认的块密码服务有两种方法可以自定义块大小。 setBlockSize(int blockSize)
方法仅适用于字节数组的编码和解码。 setStreamingBlockSize(int streamingBlockSize)
方法仅适用于流编码和解码。
值0
表示将使用默认算法特定的块大小。 这是默认值。
分组密码块大小是特定于算法的。 选定的加密算法可能不适用于任意块大小 :
@Test(expected=CryptoException.class)
public void aesWrongBlockSize() {
String secret = 'Tell nobody!';
AesCipherService cipher = new AesCipherService();
// set wrong block size
cipher.setBlockSize(200);
// generate key with default 128 bits size
Key key = cipher.generateNewKey();
byte[] keyBytes = key.getEncoded();
// encrypt the secret
byte[] secretBytes = CodecSupport.toBytes(secret);
cipher.encrypt(secretBytes, keyBytes);
}
填充
使用方法setPaddingScheme(PaddingScheme paddingScheme)
指定字节数组加密和解密填充。 setStreamingPaddingScheme( PaddingScheme paddingScheme)
指定流加密和解密填充。
枚举PaddingScheme
代表所有典型的填充方案。 默认情况下,并非所有功能都可用,您可能必须安装自定义JCE提供程序才能使用它们。
值null
表示将使用默认算法特定的填充。 这是默认值。
如果需要PaddingScheme
枚举中未包含的填充,请使用setPaddingSchemeName
或setStreamingPaddingSchemeName
方法。 这些方法采用带有填充方案名称作为参数的字符串。 它们比上述类型的类型安全性差,但更灵活。
填充非常特定于算法。 选定的加密算法可能不适用于任意填充 :
@Test(expected=CryptoException.class)
public void aesWrongPadding() {
String secret = 'Tell nobody!';
BlowfishCipherService cipher = new BlowfishCipherService();
// set wrong block size
cipher.setPaddingScheme(PaddingScheme.PKCS1);
// generate key with default 128 bits size
Key key = cipher.generateNewKey();
byte[] keyBytes = key.getEncoded();
// encrypt the secret
byte[] secretBytes = CodecSupport.toBytes(secret);
cipher.encrypt(secretBytes, keyBytes);
}
操作模式
操作模式指定块如何链接(组合)在一起。 与填充方案一样,您可以使用OperationMode
枚举或字符串来提供它们。
注意,并非每种操作模式都可用。 此外,他们并非天生平等。 某些链接模式不如其他链接模式安全。 默认的密码反馈操作模式既安全又可在所有JDK环境中使用。
设置字节数组加密和解密操作模式的方法:
-
setMode(OperationMode mode)
-
setModeName(String modeName)
设置流加密和解密操作模式的方法:
-
setStreamingMode(OperationMode mode)
-
setStreamingModeName(String modeName)
练习–解密Openssl
假设应用程序发送使用Linux openssl命令加密的数据。 我们知道用于加密数据的密钥和命令的十六进制表示形式:
- 密钥:
B9FAB84B65870109A6E8707BC95151C245BF18204C028A6A
。 - 命令:
openssl des3 -base64 -p -K
。-iv
每个消息都包含初始化向量的十六进制表示形式和base64编码的加密消息。
样本消息:
- 初始化向量:
F758CEEB7CA7E188
。 - 消息:
GmfvxhbYJbVFT8Ad1Xc+Gh38OBmhzXOV
。
使用OpenSSL生成样本
示例消息已使用以下命令加密:
#encrypt 'yeahh, that worked!'
echo yeahh, that worked! | openssl des3 -base64 -p -K B9FAB84B65870109A6E8707BC95151C245BF18204C028A6A -iv F758CEEB7CA7E188
使用OpenSSL选项-P可以生成密钥或随机初始向量。
解
首先,我们必须找出算法名称,填充和操作模式。 幸运的是,这三个文件都可以在OpenSSL 文档中找到 。 Des3 是 CBC模式下三重DES加密算法的别名 ,而OpenSSL 使用 PKCS#5填充。
密码块链接 (CBC)需要与块大小相同大小的初始化向量。 三重DES需要64位长的块。 Java JCE对Triple DES 使用“ DESede”算法名称。
我们的自定义密码扩展并配置了DefaultBlockCipherService
:
public class OpensslDes3CipherService extends DefaultBlockCipherService {
public OpensslDes3CipherService() {
super('DESede');
setMode(OperationMode.CBC);
setPaddingScheme(PaddingScheme.PKCS5);
setInitializationVectorSize(64);
}
}
Shiro密码decrypt
方法需要两个输入字节数组,密文和密钥。 密文应同时包含初始化向量和加密密文。 因此,在尝试解密消息之前,我们必须将它们组合在一起。 该方法combine
两个数组合并为一个:
private byte[] combine(byte[] iniVector, byte[] ciphertext) {
byte[] ivCiphertext = new byte[iniVector.length + ciphertext.length];
System.arraycopy(iniVector, 0, ivCiphertext, 0, iniVector.length);
System.arraycopy(ciphertext, 0, ivCiphertext, iniVector.length, ciphertext.length);
return ivCiphertext;
}
实际的解密通常像下面这样 :
@Test
public void opensslDes3Decryption() {
String hexInitializationVector = 'F758CEEB7CA7E188';
String base64Ciphertext = 'GmfvxhbYJbVFT8Ad1Xc+Gh38OBmhzXOV';
String hexSecretKey = 'B9FAB84B65870109A6E8707BC95151C245BF18204C028A6A';
//decode secret message and initialization vector
byte[] iniVector = Hex.decode(hexInitializationVector);
byte[] ciphertext = Base64.decode(base64Ciphertext);
//combine initialization vector and ciphertext together
byte[] ivCiphertext = combine(iniVector, ciphertext);
//decode secret key
byte[] keyBytes = Hex.decode(hexSecretKey);
//initialize cipher and decrypt the message
OpensslDes3CipherService cipher = new OpensslDes3CipherService();
ByteSource decrypted = cipher.decrypt(ivCiphertext, keyBytes);
//verify result
String theMessage = CodecSupport.toString(decrypted.getBytes());
assertEquals('yeahh, that worked!\n', theMessage);
}
结束
Apache Shiro教程的这一部分介绍了1.2版中可用的加密功能。 所有使用的示例都可以在Github上找到 。
参考: Apache Shiro第3部分–来自JCG合作伙伴 Maria Jurcovicova的“ 密码”,来自This is Stuff博客。
翻译自: https://www.javacodegeeks.com/2012/05/apache-shiro-part-3-cryptography.html
apache shiro