概述
信息安全基本概念:
- BASE64 编码格式
- Base58 编码
- MD5(Message Digest algorithm 5,信息摘要算法)
- SHA(Secure Hash Algorithm,安全散列算法)
- HMAC(Hash Message Authentication Code,散列消息鉴别码)
- RIPEMD(RACE Integrity Primitives Evaluation Message Digest,RACE原始完整性校验消息摘要)
Base64
按照RFC2045的定义,Base64被定义为:Base64内容传送编码被设计用来把任意序列的8位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)
使用:推荐使用 org.apache.commons.codec.binary.Base64
Base64编码说明
Base64编码要求把3个8位字节(38=24)转化为4个6位的字节(46=24),之后在6位的前面补两个0,形成8位一个字节的形式。 如果剩下的字符不足3个字节,则用0填充,输出字符使用'=',因此编码后输出的文本末尾可能会出现1或2个'='。
为了保证所输出的编码位可读字符,Base64制定了一个编码表,以便进行统一转换。编码表的大小为2^6=64,这也是Base64名称的由来。
Base64编码表
码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 | |||
---|---|---|---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w | |||
1 | B | 17 | R | 33 | h | 49 | x | |||
2 | C | 18 | S | 34 | i | 50 | y | |||
3 | D | 19 | T | 35 | j | 51 | z | |||
4 | E | 20 | U | 36 | k | 52 | 0 | |||
5 | F | 21 | V | 37 | l | 53 | 1 | |||
6 | G | 22 | W | 38 | m | 54 | 2 | |||
7 | H | 23 | X | 39 | n | 55 | 3 | |||
8 | I | 24 | Y | 40 | o | 56 | 4 | |||
9 | J | 25 | Z | 41 | p | 57 | 5 | |||
10 | K | 26 | a | 42 | q | 58 | 6 | |||
11 | L | 27 | b | 43 | r | 59 | 7 | |||
12 | M | 28 | c | 44 | s | 60 | 8 | |||
13 | N | 29 | d | 45 | t | 61 | 9 | |||
14 | O | 30 | e | 46 | u | 62 | + | |||
15 | P | 31 | f | 47 | v | 63 | / |
原理
编辑
转码过程例子:
38=46
内存1个字节占8位
转前: s 1 3
先转成ascii:对应 115 49 51
2进制: 01110011 00110001 00110011
6个一组(4组) 011100110011000100110011
然后才有后面的 011100 110011 000100 110011
然后计算机是8位8位的存数 6不够,自动就补两个高位0了
所有有了 高位补0
科学计算器输入 00011100 00110011 00000100 00110011
得到 28 51 4 51
查对下照表 c z E z
Java版
import java.util.Base64;
/**
对于标准的Base64:
加密为字符串使用Base64.getEncoder().encodeToString();
加密为字节数组使用Base64.getEncoder().encode();
解密使用Base64.getDecoder().decode();
对于URL安全或MIME的Base64,只需将上述getEncoder()getDecoder()更换为getUrlEncoder()getUrlDecoder()
或getMimeEncoder()和getMimeDecoder()即可。
*/
@Test
public void testEncodeBase64() throws Exception {
byte[] encodeBase64 = org.apache.commons.codec.binary.Base64
.encodeBase64("进行Base64".getBytes("UTF-8"));
System.out.println(new String(encodeBase64));//6L+b6KGMQmFzZTY0
}
@Test
public void testSDecodeBase64() throws Exception {
byte[] decodeBase64 = org.apache.commons.codec.binary.Base64
.decodeBase64("6L+b6KGMQmFzZTY0");
System.out.println(new String(decodeBase64));//进行Base64
}
Base58
Base58是用于Bitcoin中使用的一种独特的编码方式,主要用于产生Bitcoin的钱包地址。相比Base64,Base58不使用数字"0",字母大写"O",字母大写"I",和字母小写"l",以及"+"和"/"符号。
基于ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"共计58个
base58和base64一样是一种二进制转可视字符串的算法,主要用来转换大整数值。区别是,转换出来的字符串,去除了几个看起来会产生歧义的字符,如 0 (零), O (大写字母O), I (大写的字母i) and l (小写的字母L) ,和几个影响双击选择的字符,如/, +。结果字符集正好58个字符(包括9个数字,24个大写字母,25个小写字母)。不同的应用实现中,base58 最后查询的字母表可能不同,所以没有具体的标准。
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
public class Base58 {
public static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
private static final int[] INDEXES = new int[128];
static {
for (int i = 0; i < INDEXES.length; i++) {
INDEXES[i] = -1;
}
for (int i = 0; i < ALPHABET.length; i++) {
INDEXES[ALPHABET[i]] = i;
}
}
/**
* Encodes the given bytes in base58. No checksum is appended.
*/
public static String encode(byte[] input) {
if (input.length == 0) {
return "";
}
input = copyOfRange(input, 0, input.length);
// Count leading zeroes.
int zeroCount = 0;
while (zeroCount < input.length && input[zeroCount] == 0) {
++zeroCount;
}
// The actual encoding.
byte[] temp = new byte[input.length * 2];
int j = temp.length;
int startAt = zeroCount;
while (startAt < input.length) {
byte mod = divmod58(input, startAt);
if (input[startAt] == 0) {
++startAt;
}
temp[--j] = (byte) ALPHABET[mod];
}
// Strip extra '1' if there are some after decoding.
while (j < temp.length && temp[j] == ALPHABET[0]) {
++j;
}
// Add as many leading '1' as there were leading zeros.
while (--zeroCount >= 0) {
temp[--j] = (byte) ALPHABET[0];
}
byte[] output = copyOfRange(temp, j, temp.length);
try {
return new String(output, "US-ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
public static byte[] decode(String input) throws IllegalArgumentException {
if (input.length() == 0) {
return new byte[0];
}
byte[] input58 = new byte[input.length()];
// Transform the String to a base58 byte sequence
for (int i = 0; i < input.length(); ++i) {
char c = input.charAt(i);
int digit58 = -1;
if (c >= 0 && c < 128) {
digit58 = INDEXES[c];
}
if (digit58 < 0) {
throw new IllegalArgumentException("Illegal character " + c + " at " + i);
}
input58[i] = (byte) digit58;
}
// Count leading zeroes
int zeroCount = 0;
while (zeroCount < input58.length && input58[zeroCount] == 0) {
++zeroCount;
}
// The encoding
byte[] temp = new byte[input.length()];
int j = temp.length;
int startAt = zeroCount;
while (startAt < input58.length) {
byte mod = divmod256(input58, startAt);
if (input58[startAt] == 0) {
++startAt;
}
temp[--j] = mod;
}
// Do no add extra leading zeroes, move j to first non null byte.
while (j < temp.length && temp[j] == 0) {
++j;
}
return copyOfRange(temp, j - zeroCount, temp.length);
}
public static BigInteger decodeToBigInteger(String input) throws IllegalArgumentException {
return new BigInteger(1, decode(input));
}
// number -> number / 58, returns number % 58
private static byte divmod58(byte[] number, int startAt) {
int remainder = 0;
for (int i = startAt; i < number.length; i++) {
int digit256 = (int) number[i] & 0xFF;
int temp = remainder * 256 + digit256;
number[i] = (byte) (temp / 58);
remainder = temp % 58;
}
return (byte) remainder;
}
//
// number -> number / 256, returns number % 256
//
private static byte divmod256(byte[] number58, int startAt) {
int remainder = 0;
for (int i = startAt; i < number58.length; i++) {
int digit58 = (int) number58[i] & 0xFF;
int temp = remainder * 58 + digit58;
number58[i] = (byte) (temp / 256);
remainder = temp % 256;
}
return (byte) remainder;
}
private static byte[] copyOfRange(byte[] source, int from, int to) {
byte[] range = new byte[to - from];
System.arraycopy(source, from, range, 0, range.length);
return range;
}
}
MD5
Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。
MD5即Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一(又译摘要算法、哈希算法),主流编程语言普遍已有MD5实现。将数据(如汉字)运算为另一固定长度值,是杂凑算法的基础原理,MD5的前身有MD2、MD3和MD4。
使用:
public void testMD5() throws Exception {
String md5Msg = msgSafeBase("测试MD5","MD5");
System.out.println(md5Msg);// c2dbb895a66c3ca924ccdbea49fa6884
}
public String msgSafeBase(String msg, String algorithmName) throws Exception {
MessageDigest m = MessageDigest.getInstance(algorithmName);
m.update(msg.getBytes("UTF8"));
byte s[] = m.digest();
return Hex.encodeHexString(s);
}
MD5是常用的加密算法,也经常用于校验信息完整,如文件的完整性。用术语讲,MD5是一种消息摘要算法(Message Digest Algorithm)。另外还有一种常用的消息摘要算法SHA1。如果想了解这些的话,可以去百度百科:MD5、SHA1、消息摘要算法。
Java已经实现了MD5、SHA1算法。利用java.security.MessageDigest类就可以获取字符串和文件的MD5以及SHA1结果。
消息摘要算法
消息摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。(摘要可以比方为指纹,消息摘要算法就是要得到文件的唯一职位)
特点:
- 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。
- 一般地,只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。
- 只能进行正向的信息摘要,而无法从摘要中恢复出任何的消息,甚至根本就找不到任何与原信息相关的信息(不可逆性)。
- 好的摘要算法,没有人能从中找到“碰撞”或者说极度难找到,虽然“碰撞”是肯定存在的(碰撞即不同的内容产生相同的摘要)。
应用:
一般地,把对一个信息的摘要称为该消息的指纹或数字签名。数字签名是保证信息的完整性和不可否认性的方法。数据的完整性是指信宿接收到的消息一定是信源发送的信息,而中间绝无任何更改;信息的不可否认性是指信源不能否认曾经发送过的信息。其实,通过数字签名还能实现对信源的身份识别(认证),即确定“信源”是否是信宿意定的通信伙伴。 数字签名应该具有唯一性,即不同的消息的签名是不一样的;同时还应具有不可伪造性,即不可能找到另一个消息,使其签名与已有的消息的签名一样;还应具有不可逆性,即无法根据签名还原被签名的消息的任何信息。这些特征恰恰都是消息摘要算法的特征,所以消息摘要算法适合作为数字签名算法。
有哪些具体的消息摘要算法?
MessageDigest的选择好多,包括MD2、MD4、MD5、SHA-1、SHA-256、RIPEMD128、RIPEMD160等等。
MD5
介绍:
message digest 5,消息摘要算法第五版,之前有md2,md4。
MD5消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由罗纳德·李维斯特设计,于1992年公开,用以取代MD4算法。
MD5已经广泛使用在为文件传输提供一定的可靠性方面。例如,服务器预先提供一个MD5校验和,用户下载完文件以后,用MD5算法计算下载文件的MD5校验和,然后通过检查这两个校验和是否一致,就能判断下载的文件是否出错。
应用:
因为消息摘要算法具有唯一性、不可逆性、不可伪造性,因此广泛用于验证性加密领域,之所以叫验证性加密是因为不可逆性决定了无法还原出明文。
加密:一致性验证、数字证书、安全访问认证
安全性:
2009年,冯登国、谢涛二人利用差分攻击,将MD5的碰撞算法复杂度从王小云的242进一步降低到221,极端情况下甚至可以降低至210。仅仅221的复杂度意味着即便是在2008年的计算机上,也只要几秒便可以找到一对碰
1.字符串的MD5(下面的代码有详细注释)
public static String stringMD5(String input) {
try {
// 拿到一个MD5转换器(如果想要SHA1参数换成”SHA1”)
MessageDigest messageDigest =MessageDigest.getInstance("MD5");
// 输入的字符串转换成字节数组
byte[] inputByteArray = input.getBytes();
// inputByteArray是输入字符串转换得到的字节数组
messageDigest.update(inputByteArray);
// 转换并返回结果,也是字节数组,包含16个元素
byte[] resultByteArray = messageDigest.digest();
// 字符数组转换成字符串返回
return byteArrayToHex(resultByteArray);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
//下面这个函数用于将字节数组换成成16进制的字符串
public static String byteArrayToHex(byte[] byteArray) {
// 首先初始化一个字符数组,用来存放每个16进制字符
char[] hexDigits = {'0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F' };
// new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方))
char[] resultCharArray =new char[byteArray.length * 2];
// 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去
int index = 0;
for (byte b : byteArray) {
resultCharArray[index++] = hexDigits[b>>> 4 & 0xf];
resultCharArray[index++] = hexDigits[b& 0xf];
}
// 字符数组组合成字符串返回
return new String(resultCharArray);
从上面代码可以看出,使用MessageDigest对字符串进行MD5算法的步骤是,先将字符串转换成字节数组,在进行MD5算法,最后返回的也是一个字节数组,要我们自己转成32位的字符串。
2.文件MD5
对文件进行MD5也可以像字符串MD5一样的,首先要把文件转成字节数组,后面和字符串MD5完全一样。
但是如果是一个特别大的文件,一下子把一个文件的数组全部读到内存中,那么估计内存也吃不消。
对于大文件,可以使用DigestInputStream。
public static String fileMD5(String inputFile) throws IOException {
// 缓冲区大小(这个可以抽出一个参数)
int bufferSize = 256 * 1024;
FileInputStream fileInputStream = null;
DigestInputStream digestInputStream = null;
try {
// 拿到一个MD5转换器(同样,这里可以换成SHA1)
MessageDigest messageDigest =MessageDigest.getInstance("MD5");
// 使用DigestInputStream
fileInputStream = new FileInputStream(inputFile);
digestInputStream = new DigestInputStream(fileInputStream,messageDigest);
// read的过程中进行MD5处理,直到读完文件
byte[] buffer =new byte[bufferSize];
while (digestInputStream.read(buffer) > 0);
// 获取最终的MessageDigest
messageDigest= digestInputStream.getMessageDigest();
// 拿到结果,也是字节数组,包含16个元素
byte[] resultByteArray = messageDigest.digest();
// 同样,把字节数组转换成字符串
return byteArrayToHex(resultByteArray);
} catch (NoSuchAlgorithmException e) {
return null;
} finally {
try {
digestInputStream.close();
} catch (Exception e) {
}
try {
fileInputStream.close();
} catch (Exception e) {
}
}
}
上面的方法本人亲测过大小约4G的文件,得出的MD5值和网上下载的一个MD5小工具得到的MD5值一样,说明上面的方式没有什么问题。不过取大文件的MD5很慢,4G的文件跑一下要一分钟(I5处理器 6G内存 64位XP系统 本本)。
附1:我在网上还看到一种给文件MD5的方式
public static String getFileMD5String(File file) throws IOException{
FileInputStream in = new FileInputStream(file);
FileChannel ch =in.getChannel();
MappedByteBuffer byteBuffer =ch.map(FileChannel.MapMode.READ_ONLY, 0,file.length());
messagedigest.update(byteBuffer);
return byteArrayToHex (messagedigest.digest());
}
我也尝试过这样的方式,但是如果文件大于2G,那么这种方式会出现异常。所以不推荐。
附2:测试文件MD5的main方法
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
try {
System.out.println(fileMD5("E:/软件/VS2008ProEdition90DayTrialCHSX1435983.iso"));
} catch (IOException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println((endTime - startTime)/1000);
}
参考:http://blog.csdn.net/xiao__gui/article/details/8148203
SHA
安全哈希算法(Secure Hash Algorithm)主要适用于数字签名标准(Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA)。对于长度小于2^64位的消息,SHA1会产生一个160位的消息摘要。该算法经过加密专家多年来的发展和改进已日益完善,并被广泛使用。该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。散列函数值可以说是对明文的一种“指纹”或是“摘要”所以对散列值的数字签名就可以视为对此明文的数字签名。
安全散列算法SHA(Secure Hash Algorithm,SHA)是美国国家标准技术研究所发布的国家标准FIPS PUB 180,最新的标准已经于2008年更新到FIPS PUB 180-3。其中规定了SHA-1,SHA-224,SHA-256,SHA-384,和SHA-512这几种单向散列算法。SHA-1,SHA-224和SHA-256适用于长度不超过264二进制位的消息。SHA-384和SHA-512适用于长度不超过2128二进制位的消息。
sha1已不推荐使用
使用:
@Test
public void testSHA() throws Exception {
// SHA-1,SHA-256,SHA-384,和SHA-512
String hashMsg = msgSafeBase("测试SHA", "SHA-1");
System.out.println(hashMsg);
// sha1:9bfec0ff7027c76c28fdaa51bd5a619c5e2f69bb
}
public String msgSafeBase(String msg, String algorithmName) throws Exception {
MessageDigest m = MessageDigest.getInstance(algorithmName);
m.update(msg.getBytes("UTF8"));
byte s[] = m.digest();
return Hex.encodeHexString(s);
}
HMAC
HMAC是密钥相关的哈希运算消息认证码,HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
运算作用
(1)验证TPM接受的授权数据和认证数据;
(2)确认TPM接受到的命令请求是已授权的请求,并且,命令在传送的过程中没有被改动过。
定义HMAC需要一个加密用散列函数(表示为H,可以是MD5或者SHA-1)和一个密钥K。我们用B来表示数据块的字节数。(以上所提到的散列函数的分割数据块字长B=64),用L来表示散列函数的输出数据字节数(MD5中L=16,SHA-1中L=20)。鉴别密钥的长度可以是小于等于数据块字长的任何正整数值。应用程序中使用的密钥长度若是比B大,则首先用使用散列函数H作用于它,然后用H输出的L长度字符串作为在HMAC中实际使用的密钥。一般情况下,推荐的最小密钥K长度是L个字节。
典型应用
HMAC的一个典型应用是用在“质疑/应答”(Challenge/Response)身份认证中。
认证流程
(1) 先由客户端向服务器发出一个验证请求。
(2) 服务器接到此请求后生成一个随机数并通过网络传输给客户端(此为质疑)。
(3) 客户端将收到的随机数提供给ePass,由ePass使用该随机数与存储在ePass中的密钥进行HMAC-MD5运算并得到一个结果作为认证证据传给服务器(此为响应)。
(4) 与此同时,服务器也使用该随机数与存储在服务器数据库中的该客户密钥进行HMAC-MD5运算,如果服务器的运算结果与客户端传回的响应结果相同,则认为客户端是一个合法用户
安全性浅析
由上面的介绍,我们可以看出,HMAC算法更象是一种加密算法,它引入了密钥,其安全性已经不完全依赖于所使用的HASH算法,安全性主要有以下几点保证:
(1) 使用的密钥是双方事先约定的,第三方不可能知道。由3.2介绍的应用流程可以看出,作为非法截获信息的第三方,能够得到的信息只有作为“挑战”的随机数和作为“响应”的HMAC结果,无法根据这两个数据推算出密钥。由于不知道密钥,所以无法仿造出一致的响应。
使用:
@Test
public void testHashMsgCode() throws Exception {
String macKey = initMacKey();
System.out.println(macKey);
//vTVhh1xBdDTm9/TZhVsOK0+G/Aw2fkCx0gC6KcM7o2lbCy6DyatcUSe66PTu70E7J0r/hhtodcZBPuLI4/aCgw==
String msgCode=hashMsgCode("测试HMAC".getBytes(),macKey);
System.out.println(msgCode);
//7e4f0f95cfef2c8f5af9799d03798e76
}
public static String initMacKey() throws Exception {
// HmacMD5,HmacSHA1,HmacSHA256,HmacSHA384,HmacSHA512
KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacMD5");
SecretKey secretKey = keyGenerator.generateKey();
return new String(Base64.encodeBase64(secretKey.getEncoded()));
}
public static String hashMsgCode(byte[] data, String key) throws Exception {
SecretKey secretKey = new SecretKeySpec(Base64.decodeBase64(key), "HmacMD5");
Mac mac = Mac.getInstance(secretKey.getAlgorithm());
mac.init(secretKey);
return new String(Hex.encodeHex(mac.doFinal(data)));
}
BC加密包:The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. This jar contains JCE provider and lightweight API for the Bouncy Castle Cryptography APIs for JDK 1.5 to JDK 1.8.
RIPEMD算法 原始完整性校验消息摘要
RIPEMD(RACE Integrity Primitives Evaluation Message Digest,RACE原始完整性校验消息摘要),是Hans Dobbertin等3人在md4,md5的基础上,于1996年提出来的。算法共有4个标准128、160、256和320,其对应输出长度分别为16字节、20字节、32字节和40字节。不过,让人难以致信的是RIPEMD的设计者们根本就没有真正设计256和320位这2种标准,他们只是在128位和160位的基础上,修改了初始参数和s-box来达到输出为256和320位的目的。所以,256位的强度和128相当,而320位的强度和160位相当。RIPEMD建立在md的基础之上,所以,其添加数据的方式和md5完全一样。
除了MD、SHA和MAC三大主流信息摘要算法之外,还有一些不常见的消息摘要算法。包括RipeMD系列、Tiger、Whirlpool和Gost3411算法。同时,RipeMD算法和MAC算法系列相结合,有产生了HmacRipeMD128和HmacRipeMD160两种算法。
针对这些算法进行简单介绍
1、RipeMD算法:针对MD4和MD5算法缺陷分析提出的算法。这些算法主要是针对摘要值得长度进行了区分
2、Tiger算法:号称最快的Hash算法,专门针对64为机器做优化了。其消息长度为192位
3、Whirlpool:被列入iso标准。与AES加密标准使用了相同的转化技术,极大提高了安全性,被称为最安全的摘要算法,长度为512位
4、Gost3411:信息摘要长度为256位
这些算法的实现java6都没提供。这里BouncyCastle进行了支持。其实这些算法的调用都一个样,就是换一个调用的名字而已。
二、RipeMD算法和HmacRipeMD算法系列
算法 | 摘要长度 | 备注 |
---|---|---|
RipeMD128 | 128 | BouncyCastle实现 |
RipeMD160 | 160 | BouncyCastle实现 |
RipeMD256 | 256 | BouncyCastle实现 |
RipeMD320 | 320 | BouncyCastle实现 |
HmacRipeMD128 | 128 | BouncyCastle实现 |
HmacRipeMD160 | 160 | BouncyCastle实现 |
四、总结
1、HmacRipeMD消息摘要的长度与相应的摘要算法的摘要长度相同:HmacRipeMD128与RipeMD128相对应,消息摘要长度都是32个字符的16进制串。HmacRipeMD160与RipeMD160相对应,消息摘要长度都是40个字符的16进制串。
2、BouncyCastle不仅仅提供了HmacRipeMD算法的实现,还提供了HmacTiger算法的实现。实现方式与上边的代码清单相似