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

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)。

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();
        }
    }
}

转载于:https://my.oschina.net/u/189332/blog/741227

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值