您的位置:首页 > 编程语言 > Java开发

Java中的消息摘要、加密与数字签名

2016-09-01 00:00 337 查看
摘要: 本文描述了消息摘要,消息加密和数字签名的原理与实现代码

Java中的消息摘要与加密

消息摘要(Message Digest)

消息在互联网传输过程中不可避免的会经历多个中间人,它可能是代理服务器,也可能是路由、网关等等。
消息在传输过程中有可能被人窃听,甚至是修改。为了保证消息在传输过程中的完整性我们可以使用消息摘要。

消息摘要算法会为每一个不同的消息生成其独有的摘要,这就像人的指纹一样。有两种生成消息摘要的办法,
第一种是对消息进行异或运算,所得到的消息摘要被称为checksum;第二种是利用哈希函数生成消息摘要。第一种方法
的安全性较弱,可以通过修改消息得到想要的checksum;利用哈希函数的方式,产生的消息摘要要安全的多,如果修改了消息的1个bit,那么理论上摘要中50%的内容都会发生改变。

Java支持的摘要生成算法包括:

MD2和MD5,生成的摘要长度为128位

SHA-1, 生成的摘要长度为160位

SHA-256, SHA-383和SHA-512, 分别生成256、383和512位的消息摘要

我们下面就看看如何利用MD5算法生成消息的摘要。

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;

public class MessageDigestExample {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
System.out.println("Did not find BouncyCastleProvider");
Security.addProvider(new BouncyCastleProvider());
}
}

public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchProviderException {
if(args.length != 1) {
System.err.println("Usage: java MessageDigestExample text");
System.exit(1);
}

byte[] plainText = args[0].getBytes("UTF8");

MessageDigest messageDigest = MessageDigest.getInstance("MD5", BouncyCastleProvider.PROVIDER_NAME);
System.out.println("\n" + messageDigest.getProvider().getInfo());
messageDigest.update(plainText);
System.out.println( "\nDigest: " );
System.out.println( new String( messageDigest.digest(), "UTF8") );
}
}

MessageDigest 类用于生成消息摘要,它接受第三方的算法实现。

初始化MessageDigest时,可接受第三方库所实现的摘要算法;

update()方法接受待生成摘要的消息作为参数;

digest()方法用于生成最终的摘要。

信息认证码(Message authentication code)

如果是利用密钥来生成消息摘要的话,所生成的消息摘要被称为信息认证码。

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class MessageAuthenticationCodeExample {
public static void  main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
if(args.length != 1) {
System.err.println("Usage: java MessageAuthenticationCodeExample text");
System.exit(1);
}

byte[] plainText = args[0].getBytes("UTF8");
System.out.println( "\nStart generating key" );
// 生成密钥
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey md5key = keyGen.generateKey();
System.out.println( "Finish generating key: \n" + md5key.toString() + "\n" );

// 创建Mac实例用于生成信息认证码
Mac mac = Mac.getInstance("HmacMD5");
mac.init(md5key);
mac.update(plainText);

System.out.println( "\n" + mac.getProvider().getInfo() );
System.out.println( "\nMAC: " );
// doFinal()方法生成信息认证码
System.out.println( new String( mac.doFinal(), "UTF8") );

}
}

消息加密

消息摘要只能帮助确认消息的完整性,只有消息加密在能确保消息在传输过程中不被人窃听。消息加密可以分为两种方式,
如果消息发送双方利用同一个密钥对消息进行加密解密,这种方式称为对称加密;如果使用不同的密钥对分别对消息进行加密和解密操作,这种方式被称为非对称加密。

对称加密

对称加密中,消息发送方与接收方都保存着一份相同的密钥。发送方在发生消息之前,利用密钥对消息进行加密,生成加密后的密文消息,然后将密文消息发送给接收方;
接收方接收到密文后,再用密钥对密文进行解密,还原出原来的消息。

在加密过程中可以针对单个bit进行逐一的加密,也可以将多个bit凑成bit块,对bit块加密。前者称为流加密(stream ciphers),后者成为块加密(block ciphers)。
在块加密过程中如果被加密的字符串的长度不够一个加密块的长度,就需要对字符串进行填充。

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;

public class PrivateExample {
public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
if (args.length != 1) {
System.err.println("Usage: java PrivateExample text");
System.exit(1);
}
byte[] plainText = args[0].getBytes("UTF8");

//Step 1: create private key
System.out.println( "\nStart generating DES key" );
KeyGenerator keyGen = KeyGenerator.getInstance("DES");
keyGen.init(56);
Key key = keyGen.generateKey();
System.out.println( "Finish generating DES key" );

//Step 2: create cipher
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
System.out.println( "\n" + cipher.getProvider().getInfo() );
cipher.init(Cipher.ENCRYPT_MODE, key);

//Step 3: use cipher to encrypt message
byte[] cipherText = cipher.doFinal(plainText);
System.out.println( "Finish encryption: " );
System.out.println( new String(cipherText, "UTF8") );

//Step 4: use cipher to decrypt message
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptText = cipher.doFinal(cipherText);
System.out.println( "Finish decryption: " );
System.out.println( new String(decryptText, "UTF8") );
}
}

Java JDK支持的对称密钥生成算法包括:

DES

TripleDES

AES

RC2, RC4和RC5

Blowfish

PBE

Java JDK支持的加密算法包括:

ECB

CBC

CFB

OFB

PCBC

非对称加密

密钥同步是对称加密中一个难以解决的问题,因为在发送加密消息之前,消息发送双方需要同步密钥,而密钥可能被别人截获。
而非对称加密可以解决这个问题,非对称加密过程中会使用到两个密钥,一个专门用于加密消息,被称为公钥,一个专门用来解密消息,被称为私钥。
公钥是可以公开的,而私钥必须留在自己的手上。在发送加密消息前,消息发送双方先将公钥发送给对方,消息发送者利用对方的公钥对消息进行加密,
然后将加密后的密文发送给接收方,接收方收到密文后,再用自己的私钥解开密文。



import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;

public class PublicExample {
public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchProviderException {
if (args.length != 1) {
System.err.println("Usage: java PublicExample text");
System.exit(1);
}

byte[] plainText = args[0].getBytes("UTF8");

//Step 1: generate a RSA key
System.out.println( "\nStart generating RSA key" );
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(1024);
KeyPair keyPair = keyGen.generateKeyPair();
System.out.println( "Finish generating RSA key" );

//Step 2: get an RSA cipher object and print the provider
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
System.out.println( "\n" + cipher.getProvider().getInfo() );

//Step 3: encrypt the plaintext using the public key
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] encryptedText = cipher.doFinal(plainText);
System.out.println( "Finish encryption: " );
System.out.println( new String(encryptedText, "UTF8") );

//Step 4: decrypt the encryptedText using the private key
System.out.println( "\nStart decryption" );
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte[] decryptedText = cipher.doFinal(encryptedText);
System.out.println( "Finish decryption: " );
System.out.println( new String(decryptedText, "UTF8") );
}
}

由于非对称加密方法的加密速度慢,在实际使用过程中,通常使用非对称加密的方式来发送对称加密的密钥(如HTTPS协议)。

数字签名

在非对称加密过程中,存在一个漏洞:消息接收者无法确定消息是否真正来自发送者。举个例子,Bob和Alice利用非对称加密方式传输消息,
Eve位于Bob和Alice之间,Eve可以截取到Bob发送给Alice的公钥,然后将自己伪装成Alice,利用公钥向Bob发送消息,然而Bob无法确认这个消息到底是来自Alice还是Eve,
这被称为中间人攻击(Man-in-the-Middle attack)。



消息数字签名(digital signature)是防止中间人攻击的有效手段,数字签名的原理是,在发送消息前,Alice利用自己的私钥对消息摘要进行加密
加密后的消息摘要称为数字签名,然后连同加密后的消息发送给Bob。Bob收到消息后,先用自己的私钥解密消息,然后利用Alice的公钥解密数字签名作为老的消息摘要,
Bob对收到的消息生成新的消息摘要,与老的摘要进行对比,如果一致则可确定消息来自Alice。在消息传输过程中,由于Eve无法获取到Bob和Alice的私钥所以无法伪造数字签名。

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
*
* 使用数字签名的目的在于防止中间人攻击(Man-in-the-Middle attack),例如Bob和Alice利用非对称密钥发送信息,由于公钥是公开的,
* Bob无法知道自己收到的消息是否真的来自于Alice,因为Eve可能在截取到Alice的公钥,然后用Alice的公钥向Bob发送信息。
*
* 利用数字签名方法,可以有效的防止中间人攻击,其原理如下:
* 1. 消息发送者生成消息的digest。
* 2. 发送者使用私钥对digest进行加密。
* 3. 发送者将加密后的digest和消息发送给接收者。
* 4. 接收者使用发送者的公钥解密digest,然后重新计算消息的digest。
* 5. 对比两个digest是否一致,如果一致表示消息未被修改。
*
* 由于私钥只保存于发送者手中,所以接收者能确保消息未被修改。
*/
public class DigitalSignatureExample {
public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
if (args.length != 1) {
System.err.println("Usage: java DigitalSignature1Example text");
System.exit(1);
}

byte[] plainText = args[0].getBytes("UTF8");

//Step 1: generate message digest
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
System.out.println( "\n" + messageDigest.getProvider().getInfo() );
messageDigest.update(plainText);
byte[] md = messageDigest.digest();
System.out.println( "\nDigest: " );
System.out.println( new String( md, "UTF8") );

//Step 2: generate RSA keypair
System.out.println("\nStart generating RSA key");
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(1024);
KeyPair keyPair = keyGen.generateKeyPair();
System.out.println("Finish generating RSA key");

//Step 3: Get RSA cipher and list the provider
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
System.out.printf("\n" + cipher.getProvider().getInfo());

//Step 4: Encrypt the message digest with the RSA private key to create the signature
System.out.printf("\nStart encryption");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
byte[] cipherText = cipher.doFinal(md);
System.out.printf("\nFinish encryption: ");
System.out.printf(new String(cipherText, "UTF8"));

//Step 5: Verify Signature, start by decrypting the signature with the RSA public key
System.out.printf("\nStart decryption");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPublic());

byte[] newMD = cipher.doFinal(cipherText);
System.out.printf("\nFinish decryption");
// here may throw "java.util.UnknownFormatConversionException", it is a bug of jdk...
// if that happened, change a input string and try again.
System.out.printf(new String(newMD, "UTF8"));

//Then, recreate the message digest from the plaintext to simulate what a recipient must do
System.out.printf("\nStart signature verification");
messageDigest.reset();
messageDigest.update(plainText);
byte[] oldMD = messageDigest.digest();

//Verify that the two message digests match
int len = newMD.length;
if (len > oldMD.length) {
System.out.printf("Signature failed, length error");
System.exit(1);
}
for (int i=0; i < len; i++) {
if (oldMD[i] != newMD[i]) {
System.out.printf("Signature failed, element error");
System.exit(1);
}
}
System.out.printf("Signature verified");
}
}

上面的方法比较繁琐,Java提供了Signature类,能更加方便的生成数字签名,代码如下:

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;

/**
*
* 利用Signature类,能够更加方便的实现数字签名,签名原理与example1相同
*/
public class DigitalSignatureExample2 {
public static void main(String[] args) {
if (args.length != 1) {
System.out.printf("Usage: DigitalSignatureExample2 nameOfFileToSign");
} else try {
//Step 1: generate DSA keypair
System.out.printf("\nStart generating DSA key");
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DSA", "SUN");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
keyGen.initialize(1024, random);
KeyPair keyPair = keyGen.genKeyPair();
System.out.printf("\nFinish generating DSA key");

//Step 2: create signature object and init it with private key
System.out.printf("\nStart creating signature object");
Signature signature = Signature.getInstance("SHA1withDSA", "SUN");
signature.initSign(keyPair.getPrivate());
System.out.printf("\nFinish creating signature object");

//Step 3: Supply the Signature Object the Data to be signed
System.out.printf("\nStart signing the file");
FileInputStream fis = new FileInputStream(args[0]);
BufferedInputStream bufin = new BufferedInputStream(fis);
byte[] buffer = new byte[1024];
int len;
while ((len = bufin.read(buffer)) >= 0) {
signature.update(buffer, 0, len);
}
bufin.close();
fis.close();
byte[] realSig = signature.sign();
System.out.printf("\n" + new String(realSig, "UTF8"));
System.out.printf("\nFinish signing the file");

//Step 4: Save signature and public key in files
System.out.printf("\nSaving signature and key pair to file");
FileOutputStream sigfos = new FileOutputStream("sig");
sigfos.write(realSig);
sigfos.close();
System.out.printf("\nSaved signature to sig");

byte[] pubKey = keyPair.getPublic().getEncoded();
FileOutputStream pubKeyfos = new FileOutputStream("pubk");
pubKeyfos.write(pubKey);
pubKeyfos.close();
System.out.printf("\nSaved public key to pubk");

byte[] privateKey = keyPair.getPrivate().getEncoded();
FileOutputStream privateKeyfos = new FileOutputStream("prik");
privateKeyfos.write(privateKey);
privateKeyfos.close();
System.out.printf("\nSaved private key to prik");

//Step 6: Verify Signature
verifySignature("pubk", "sig", "data");

} catch (Exception e) {
System.err.println("Caught exception " + e.toString());
}
}

public static void verifySignature(String publicKeyFile, String signatureFile, String dataFile) {
try {
//Step 1: Load public key and signature from file
FileInputStream keyfis = new FileInputStream(publicKeyFile);
byte[] encKey = new byte[keyfis.available()];
keyfis.read(encKey);
keyfis.close();

X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encKey);
KeyFactory keyFactory = KeyFactory.getInstance("DSA", "SUN");
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);

FileInputStream sigfis = new FileInputStream(signatureFile);
byte[] sigToVerify = new byte[sigfis.available()];
sigfis.read(sigToVerify);
sigfis.close();

//Step 2: Initialize the Signature Object for Verification
Signature sig = Signature.getInstance("SHA1withDSA", "SUN");
sig.initVerify(publicKey);

//Step 3: Supply the Signature Object with the data to be verified
FileInputStream datafis = new FileInputStream(dataFile);
BufferedInputStream bufin = new BufferedInputStream(datafis);
byte[] buffer = new byte[1024];
int len;
while (bufin.available() != 0) {
len = bufin.read(buffer);
sig.update(buffer, 0, len);
}
bufin.close();
datafis.close();

//Step 4: Verify the Signature
boolean verifies = sig.verify(sigToVerify);
System.out.printf("Signature verifies: " + verifies);
} catch (Exception e) {
e.printStackTrace();
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息