RSA資料摘要+數字簽名(Java)
背景
之前我一直不明白,比如A、B雙方加密通訊的時候,為什麼A要使用B的公鑰來加密,為A什麼不使用A的私鑰加密,然後B用A的公鑰解密不就可以了嗎?
其實,A的私鑰主要是A用來簽名的,簽名顧名思義是表明這個東西是A傳送的,而不是別人發過來的。為什麼A用自己的私鑰簽名就說明這個東西就是A發過來的呢?因為用B用A的公鑰解籤的時候,得到了原文的資料摘要,然後B根據得到的明文資訊然後使用Hash函式重新得到資料摘要,兩個資料摘要一對比,如果一致,那麼就是A發過來的,表明了即使A發過來的且原文資訊沒有被改動過。
具體流程圖下圖所示:
目標
使用Java語言,實現RSA資料摘要+資料簽名。
程式碼
程式入口檔案:MainEntrance
package RSA; /** * @author yourname * @date 2018年10月16日 下午5:12:18 * */ public class MainEntrance { public static void main(String[] args) { try { new Test(); } catch (Exception e) { e.printStackTrace(); } } }
視窗介面以及事件處理檔案:Test
package RSA; import java.awt.Color; import java.awt.Container; import java.awt.Font; import java.awt.FontMetrics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Map; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JRadioButton; import javax.swing.JTextField; /** * @author yourname * @date 2018年10月16日 下午6:45:43 * */ public class Test extends JFrame { /** * */ private static final long serialVersionUID = 1L; String a2=""; int pd; Map<String, Object> keyMap; String textPlainMD5 = ""; String byte2Base64 = ""; byte[] publicDecrypt = null; public Test() throws Exception { //使用Map存放初始化的公鑰和私鑰 keyMap = CreateSecrteKey.initKey(); //建立容器和控制元件 Container c1 = getContentPane(); getContentPane().setLayout(null);//佈局 getContentPane().setBackground(Color.LIGHT_GRAY); JLabel jl1 =new JLabel("請輸入明文:"); JLabel jl2 =new JLabel("選擇方式:"); JLabel jl3 =new JLabel("結果:"); JTextField jt1 = new JTextField(10); JLabel jt3 = new JLabel(""); //設定jt3的字型 jt3.setFont(new Font("宋體",Font.BOLD, 8)); JButton jb = new JButton("開始"); jb.setBorderPainted(true);//設定按鈕顯示邊界 JButton jb1 = new JButton("反解"); jb1.setBorderPainted(true);//設定按鈕顯示邊界 JRadioButton jr1 = new JRadioButton("提取資料摘要(MD5)");//單選按鈕 JRadioButton jr2 = new JRadioButton("RSA資料簽名"); ButtonGroup group = new ButtonGroup();//沒有這句,你的單選按鈕就不叫單選按鈕,可以自己去掉試試 group.add(jr1); group.add(jr2); //控制元件加入到容器 c1.add(jl1); c1.add(jt1); c1.add(jl2); c1.add(jr1); c1.add(jr2); c1.add(jl3); c1.add(jt3); c1.add(jb); c1.add(jb1); setSize(400, 200); setVisible(true); setTitle("RSA簽名演算法"); //設定各個控制元件的絕對位置 jl1.setBounds(5,5,80,20); jt1.setBounds(85, 5, 150, 20); jl2.setBounds(5, 25, 80, 20); jr1.setBounds(85, 25, 160, 25); jr2.setBounds(85,65,140,25); jl3.setBounds(5,95,80,25); jt3.setBounds(85, 95, 200, 70); jt3.setOpaque(true); jt3.setBackground(Color.cyan); jb.setBounds(285, 95, 60, 30); jb1.setBounds(285,125,60,30); //"反解按鈕"監聽事件 jb1.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub //對資料摘要進行Hash對比 if(pd==1) { if(textPlainMD5.length()>0) { String e1 = jt1.getText();//輸入的資料 //使用MD5對原文資訊提取資料摘要 String textPlainMD51 = CreateSecrteKey.MD5(e1); //把解簽得到的Hash值和原文重新MD5生成的Hash值對比 if(new String(publicDecrypt).equals(textPlainMD51)) { System.out.println("原文資訊的Hash值和解簽得到的Hash一樣的"); try { JlabelSetText(jt3, "原文資訊的Hash值和解簽得到的Hash一樣的"); } catch (InterruptedException e2) { // TODO Auto-generated catch block e2.printStackTrace(); } } } else { JOptionPane.showMessageDialog(null, "請先完成資料摘要提取和資料簽名!", "提示", JOptionPane.ERROR_MESSAGE); } } //對資料簽名進行解籤 else if(pd==2) { if(byte2Base64.length()>0) { try { //獲得公鑰 String publicKey_Str = CreateSecrteKey.getPublicKey(keyMap); // System.out.println(publicKey_Str); //獲得公鑰PK物件 PublicKey publicKey = CreateSecrteKey.string2PublicKey(publicKey_Str); //對密文進行Base64解碼 byte[] base642Byte = CreateSecrteKey.base642Byte(byte2Base64); //用公鑰解籤 publicDecrypt = CreateSecrteKey.publicDecrypt(base642Byte, publicKey); //解密後的資料摘要 // System.out.println("解密後的MD5: " + new String(publicDecrypt)); JlabelSetText(jt3, new String(publicDecrypt)); } catch (Exception e2) { // TODO: handle exception } } else { JOptionPane.showMessageDialog(null, "請先完成資料摘要提取和資料簽名!", "提示", JOptionPane.ERROR_MESSAGE); } } } } ); //"開始按鈕"轉換按鈕新增監聽事件 jb.addActionListener( new ActionListener(){ public void actionPerformed(ActionEvent arg0){ //彈出對話方塊 String e1 = jt1.getText();//輸入的資料 // System.out.println("e1:"+e1); //如果沒有輸入明文資訊,就提示錯誤 if(e1.equals("")) { JOptionPane.showMessageDialog(null, "請先輸入明文資訊!", "提示", JOptionPane.ERROR_MESSAGE); } //選擇提取資料摘要的按鈕處理事件 if(pd==1&&e1.length()>0) { //使用MD5對原文資訊提取資料摘要 textPlainMD5 = CreateSecrteKey.MD5(e1); //顯示摘要資訊,並自動換行 try { JlabelSetText(jt3, textPlainMD5); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //選擇RSA資料簽名的按鈕處理事件 else if(pd==2&&e1.length()>0) { try { //獲得私鑰 String privateKey_Str = CreateSecrteKey.getPrivateKey(keyMap); // System.out.println(privateKey_Str); //獲得私鑰PK物件 PrivateKey privateKey = CreateSecrteKey.string2PrivateKey(privateKey_Str); //用私鑰簽名 byte[] privateEncrypt = CreateSecrteKey.privateEncrypt(textPlainMD5.getBytes(), privateKey); //加密內容用Base64編碼(密文) byte2Base64 = CreateSecrteKey.byte2Base64(privateEncrypt); // System.out.println("資料簽名:"+byte2Base64); //顯示簽名信息,並自動換行 JlabelSetText(jt3, byte2Base64); } catch (Exception e) { e.printStackTrace(); } } } }); jr1.addActionListener(//提取資料摘要按鈕 new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { // TODO 自動生成的方法存根 //提取資料摘要時候標記為1 pd=1; } } ); jr2.addActionListener(//RSA資料簽名按鈕 new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { // TODO 自動生成的方法存根 //資料簽名的時候標記為2 pd=2; } } ); } //讓JLabel自動換行 void JlabelSetText(JLabel jLabel, String longString) throws InterruptedException { StringBuilder builder = new StringBuilder("<html>"); char[] chars = longString.toCharArray(); FontMetrics fontMetrics = jLabel.getFontMetrics(jLabel.getFont()); int start = 0; int len = 0; while (start + len < longString.length()) { while (true) { len++; if (start + len > longString.length())break; if (fontMetrics.charsWidth(chars, start, len) > jLabel.getWidth()) { break; } } builder.append(chars, start, len-1).append("<br/>"); start = start + len - 1; len = 0; } builder.append(chars, start, longString.length()-start); builder.append("</html>"); jLabel.setText(builder.toString()); } }
工具類檔案(公私鑰生成、MD5、Base64等):CreateSecrteKey
package RSA;
import java.io.IOException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
/**
* @author yourname
* @date 2018年10月17日 上午12:26:27
*
*/
public class CreateSecrteKey {
public class Keys {
}
//使用RSA演算法
public static final String KEY_ALGORITHM = "RSA";
//公鑰
private static final String PUBLIC_KEY = "RSAPublicKey";
//私鑰
private static final String PRIVATE_KEY = "RSAPrivateKey";
//獲得公鑰
public static String getPublicKey(Map<String, Object> keyMap) throws Exception {
//獲得map中的公鑰物件 轉為key物件
Key key = (Key) keyMap.get(PUBLIC_KEY);
//byte[] publicKey = key.getEncoded();
//編碼返回字串
return encryptBASE64(key.getEncoded());
}
//獲得私鑰
public static String getPrivateKey(Map<String, Object> keyMap) throws Exception {
//獲得map中的私鑰物件 轉為key物件
Key key = (Key) keyMap.get(PRIVATE_KEY);
//byte[] privateKey = key.getEncoded();
//編碼返回字串
return encryptBASE64(key.getEncoded());
}
//解碼返回byte
public static byte[] decryptBASE64(String key) throws Exception {
return (new BASE64Decoder()).decodeBuffer(key);
}
//編碼返回字串
public static String encryptBASE64(byte[] key) throws Exception {
return (new BASE64Encoder()).encodeBuffer(key);
}
//map物件中存放公私鑰
public static Map<String, Object> initKey() throws Exception {
//獲得物件 KeyPairGenerator 引數 RSA 1024個位元組
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(1024);
//通過物件 KeyPairGenerator 獲取物件KeyPair
KeyPair keyPair = keyPairGen.generateKeyPair();
//通過物件 KeyPair 獲取RSA公私鑰物件RSAPublicKey RSAPrivateKey
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
//公私鑰物件存入map中
Map<String, Object> keyMap = new HashMap<String, Object>(2);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
//私鑰加密
public static byte[] privateEncrypt(byte[] content, PrivateKey privateKey) throws Exception{
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] bytes = cipher.doFinal(content);
return bytes;
}
//公鑰解密
public static byte[] publicDecrypt(byte[] content, PublicKey publicKey) throws Exception{
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] bytes = cipher.doFinal(content);
return bytes;
}
//位元組陣列轉Base64編碼
public static String byte2Base64(byte[] bytes){
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(bytes);
}
//Base64編碼轉位元組陣列
public static byte[] base642Byte(String base64Key) throws IOException{
BASE64Decoder decoder = new BASE64Decoder();
return decoder.decodeBuffer(base64Key);
}
//將Base64編碼後的公鑰轉換成PublicKey物件
public static PublicKey string2PublicKey(String pubStr) throws Exception{
byte[] keyBytes = base642Byte(pubStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
return publicKey;
}
//將Base64編碼後的私鑰轉換成PrivateKey物件
public static PrivateKey string2PrivateKey(String priStr) throws Exception{
byte[] keyBytes = base642Byte(priStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return privateKey;
}
public static String MD5(String s) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(s.getBytes("utf-8"));
return toHex(bytes);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String toHex(byte[] bytes) {
final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
StringBuilder ret = new StringBuilder(bytes.length * 2);
for (int i=0; i<bytes.length; i++) {
ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]);
ret.append(HEX_DIGITS[bytes[i] & 0x0f]);
}
return ret.toString();
}
}
程式演示
一、開始執行介面
(1) 執行程式的時候,就已經生成了公鑰和私鑰,每執行一次就會得到不同的公私鑰對,生成的公私鑰對方便下面使用。
二、輸入原文資訊,開始提取原文資訊的資料摘要
(1) 輸入框輸入原文資訊
(2) 點選"提取資料摘要(MD5)"的選項
(3) 點選"開始"按鈕,得到資料摘要
三、對資料摘要進行簽名
(1) 步驟二已經得到了資料摘要
(2) 點選"RSA資料簽名"選項,然後點選"開始按鈕",使用上面得到的私鑰對資料摘要進行簽名。
四、對資料簽名進行解籤
(1) 選擇"RSA資料簽名"選項,然後點選"反解"按鈕。反解過程就是使用上面得到的公鑰對資料簽名進行解籤,得到資料摘要。
五、驗證資料完整性
(1) 選擇"提取資料摘要(MD5)"選項,然後點選"反解"按鈕。這個步驟就是把原文資訊重新用Hash生成資料摘要Q1,然後用Q1跟步驟四中得到的資料摘要Q2對比,如果Q1和Q2一樣就輸入一樣的提示。如果不一致則提示原文資訊被修改。
可能遇到的問題
return (new BASE64Encoder()).encodeBuffer(key);
這句程式碼可能會提示錯誤,因為Base64Encoder()方法是JDK8之前的方法,使用JDK8或者以上的則不能使用這個方法。
如果要使用這個方法,需要對專案配置進行修改。
選擇你的專案–>JRE System Library–> Build Path–>Confugure Build Path
選擇Libraries–> Access rules–>Edit
點選"Add"–>選擇"Accessible"–>Rule Rattern寫上"**",點選儲存