人们对事物总是报以崇高的期许。
这并不算特别优秀的作品、代码不够优雅、前端界面过于简单、不能完全解耦、等等等等,但相比于以前好了太多太多,模块功能简洁明了、业务逻辑足够清晰、代码尽量解耦且不臃肿、最重要的注释足够多、用我现在看以前的代码、确实是头疼不已、好在命名规范、翻译翻译也能知道写的是啥破玩意。
写了啥?
一句话概括:基于RSA公私钥、网络编程、GUI三者进行数字签名与完整性认证的本地聊天程序。
实现出现的bug:
由于有经验、有些错误简单就不做赘述、提提给我使绊子的:
1、一个是通过字节数组传输到且解析后,可能会出现、解析错误,显示超过了127位或者128。
这个错误有两种原因:
1、是你发送的消息即字符串超过了117个,解决办法:分组发送。
2、接收到的字节数组以及接收用的字节数组长度是要超出或者等于,要接收的消息的,那么会照成字节数组的默认值0也会参与解析。
解决办法:利用字符串分割方法、我们发送时在消息末尾添加一个空格 ,接收到后先用分割方法一空格为分割符进行分割,这样可只取出我们所需要的消息。当然还有其他方法,比如接收端利用循环判断进行取出有效值(所以前者是我偷懒想出来的)
2、显示解密错误,即利用RSA进行解密时出错。
原因:很简单,秘钥用错了,要么是A的东西用了B的秘钥。要么是最狗血的一种:秘钥过时了,即我已经更新了秘钥但是你用旧秘钥解密,不错才怪。
后者解决办法:重新写一个产生密钥对的类,且序列化密钥对,之后只需要使用,不需要生成。
3、我在使用UDP传输时想完成一个请求连接的功能,即A端间隔1秒不断发送请求连接的消息,B则等待接收,若接收到,则给予A回执消息,通知A,A接收到通知则停止发送请求并显示连接完成。
原因:上述看似无差错,但是!A发给B后,B从接收到发送回执消息这段时间A是不知道它接收到了的,所以A又会继续发送请求,导致B会在连接成功后又收到一次请求消息。(这个错误没影响,但但是以为这个错误与第二个错误中的第二种错误有关,快烦死我了)
解决方法:发送请求的时间拉长一点,可三秒一次等等。
能让我头疼的错误不多,就想到这么多了。其实错误原因还是以为考虑少了,不能完全考虑到运行情况。但这也让我小小温故而知新一次、颇有收益。
实现手段:
通过java的jdk中的API进行公私钥的生成与序列化。
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
//1、公私钥获取:生成公私钥、序列化公私钥、获取公钥、获取私钥。
public class PublicPrivateKeyAcquisition {
public KeyPair keyPair;
// 产生公私钥。并初始化RSA算法。
public void produceKeyPair() throws NoSuchAlgorithmException {
//使用RSA算法获得密钥对生成器对象keyPairGenerator
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
//设置密钥长度为1024
keyPairGenerator.initialize(1024);
//生成密钥对
keyPair = keyPairGenerator.generateKeyPair();
}
// 即 将存储了公私钥的对象存储进文件中,方便下次使用
// 序列化对象,name是作为序列化后文件的名字
public void serialize(String name) throws Exception {
String str = name+".txt";
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(str));
oos.writeObject(keyPair);
oos.close();
}
// 通过相应的文件取出对应的公私钥对象,可通过公私钥对象获得公私钥。
// 反序列化
public KeyPair deserialization(String route) throws IOException, ClassNotFoundException {
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(route+".txt"));
Object obj=ois.readObject();
ois.close();
return (KeyPair) obj;
}
// 获得公钥
public Key getPublicKey(){
return keyPair.getPublic();
}
// 获得私钥
public Key getPrivateKey(){
return keyPair.getPrivate();
}
}
实现手段:
jdk自带的加密解密、SHA摘要生成、只有完整性认证是通过接收到的原文生成的SHA与接收到的SHA进行比较是否相同。
package com;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import java.security.Key;
import java.security.MessageDigest;
//2、RSA包含:加密信息,解密信息,SHA散列值获取,完整性认证。
public class RSAOperation {
// 参数1:明文,参数2:私钥,作用:加密原始消息
public String encryption(String str, Key Key) throws Exception {
//获取一个加密算法为RSA的加解密器对象cipher。
Cipher cipher = Cipher.getInstance("RSA");
//设置为加密模式,并将公钥给cipher。
cipher.init(Cipher.ENCRYPT_MODE, Key);
//获得密文
byte[] secret = cipher.doFinal(str.getBytes());
//进行Base64编码并返回
// 注:使用Base64编码方式,是因为加密后的消息为二进制形式的数据,
// 而我们传输的应该为字符串,所以要用到Base64的编码与解码。(Base64编码可将字节数组转换为字符串,解码为逆过程)
return new BASE64Encoder().encode(secret);
}
// 参数1:密文,参数2:公钥、作用:解密密文
public String decryption(String secret,Key Key) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
//传递私钥,设置为解密模式。
cipher.init(Cipher.DECRYPT_MODE, Key);
//解密器解密由Base64解码后的密文,获得明文字节数组
byte[] b = cipher.doFinal(new BASE64Decoder().decodeBuffer(secret));
//转换成字符串
return new String(b);
}
// 生成基于原始消息的 SHA 散列值
public String encoderSHA(String str) throws Exception {
MessageDigest sha=MessageDigest.getInstance("SHA");
BASE64Encoder base64en = new BASE64Encoder();
//加密后的字符串
// 使用digest方法后的数据为二进制数组,需通过encode变成字符串。后面接收到后逆过程可获得相同的字节数组
return base64en.encode(sha.digest(str.getBytes("utf-8")));
}
// 验证消息完整性
// 比较接收到的SHA摘要和通过接收到的原文产生的SHA摘要是否相同进行判断
public boolean checkIntegrity(String ReceivedSHA,String CalculatedSHA) throws Exception {
return (encoderSHA(ReceivedSHA)).equals(CalculatedSHA);
}
}
关于端口号要着重说明一下:
一共是要开启四个端口号,若是两者本地通讯。
A的发送与接收,B的发送与接收,都需要不同的端口号。其中,我们发送时还需要用到对方的端口号,这时是用到对方接收的端口号(这个不要错了)
实现手段:
jdk自带的网络编程知识、额外用到了多线程知识来实现一直接收消息,因接收程序是阻塞式,即接收到了才进行下一步、所以另起一线程。
package com;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.security.Key;
// 继承此接口,表示另外开启一个线程,线程会执行run()方法中的代码。
public class UDPChatReceiver implements Runnable{
DatagramSocket socket =null; // 用来创建接收端口号的变量
public int myPort; // 端口号
RSAOperation rsa = null; // 获得数据后用此类中的方法进行解密
GUIChat gui = null; // 获得数据且解密后,用此类显示在图形化界面中
String heName = ""; // 对方的名字
Key publicKey = null; // 对方公钥
public String receiveData=""; // 存储接收到的字符串
// 生成接收端口的进程
public UDPChatReceiver(int myPort,RSAOperation rsa,GUIChat gui,String heName){
this.myPort=myPort;
this.rsa = rsa;
this.gui = gui;
this.heName = heName;
//在本机地址上建立端口,以接收
try {
socket=new DatagramSocket(myPort);
// 获取对方公钥
publicKey = new PublicPrivateKeyAcquisition().deserialization(heName).getPublic();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
// 通过循环能够一直保持接收,因为下方接收消息是阻塞式,即只有接收到了才会执行下一步,所以才重新开启一个线程去作为接收消息的程序。
while (true) {
try {
//接收数据包
byte[] container = new byte[1024];
//构造一个 DatagramPacket用于接收长度的数据包 length 。
DatagramPacket packet = new DatagramPacket(container, 0, container.length);
//接收来自DatagramPacket的数据包,阻塞式
socket.receive(packet);
//获得包裹中的数据
receiveData = new String(packet.getData(), 0, packet.getData().length);
// 获取到数据后,解密数据,清空GUI中的文本域,然后重新赋值文本域。
decryptionAndDisplay(receiveData);
// 释放资源的判定
if(receiveData.equals("exit_0"))break;
} catch (IOException e) {
e.printStackTrace();
}
}
//关闭流
socket.close();
}
// 解密数据并显示数据
public void decryptionAndDisplay(String str){
try {
String[] s = str.split(" "); // 分割接收到的信息,方便解读
if (s[0].equals("请求连接")) {
receiveData = s[0];
}else {
// 解密密文
String str2 = rsa.decryption(s[0], publicKey);
// 将获得的密文与SHA显示在左边的文本域中,记得之前的文本不能删除了。
// 可直接在原基础上插入内容
gui.text_Ciphertext.append("密文:\n" + s[0] + "\n\n" + "SHA摘要:" + s[1] + "\n\n" + "完整性认证:" + rsa.checkIntegrity(str2, s[1]) + "\n" + "------------------------------------------------" + "\n");
// 将解密后的明文显示在右边文本域中
gui.text_Plaintext.append("\n" + heName + ": " + str2 + "\n");
}
} catch(Exception e){
e.printStackTrace();
}
}
}
实现手段:
jdk自带的API、但此时没有另起线程使用发送功能,因GUI界面点击发送按钮才会进行发送,所以不需要另外开线程。且GUI的程序本身就是一个线程
package com;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class UDPChatSender {
public int myPort; // 我自己发送的端口号
DatagramSocket socket=null;
public UDPChatSender(int myPort){
this.myPort=myPort;
//建立一个Socket
try {
socket=new DatagramSocket(myPort);
} catch (SocketException e) {
e.printStackTrace();
}
}
public void send(String str,int hePort){//str就是要发送的数据、hePort对方接收的端口号
if (!str.equals("exit_0")) { // 这是一个关闭IO流的操作,一般是结束时关闭
try {
//获取本机地址
InetAddress inetAddress = InetAddress.getByName("localhost");
//参数:数据(Byte类型),发送数据的长度,要发给谁
DatagramPacket packet = new DatagramPacket(str.getBytes(), 0, str.getBytes().length, inetAddress, hePort);
socket.send(packet);//发送包
} catch (IOException e) {
e.printStackTrace();
}
}else {
//关闭流
socket.close();
}
}
}
实现手段:
利用Swing知识。发送按钮点击后会先分析输入框有没有字符串,有才下一步。之后将要发送的消息在聊天界面先显示,然后再加密原消息,与生成对应的SHA摘要。然后将两者拼接,中间使用空格分开,末尾加上空格。最后调用发送程序。
package com;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.security.Key;
public class GUIChat extends JFrame {
public JTextArea text_Ciphertext; // 展示密文的文本框
public JTextArea text_Plaintext; // 展示明文的文本框
public JTextField text_Input; // 输入文本框
public JButton button_send; // 发送按钮
public JScrollPane jScrollPane_C; // 滚动面板有两个、且放在面板中
public JScrollPane jScrollPane_P; // 滚动面板有两个、且放在面板中
public JPanel jPanel; // 面板只有一个、且放在滚动面板中中
Key privateKey=null; // 自己的私钥
// name是聊天者身份,hePort,对方是端口号,send,是发送按钮点击时需要用的此类进行发送消息
// 此方法是生成图形化界面
public void init(String myName, int hePort, UDPChatSender send,RSAOperation rsa){
// 设置生成窗体的位置,宽高
this.setBounds(600,200,800,600);
//获得一个容器,只有这样设置颜色才会生效
Container contentPane = this.getContentPane();
contentPane.setBackground(Color.yellow);
//绝对布局,依照设定的x,y坐标定位布局组件
contentPane.setLayout(null);
// 生成面板
jPanel = new JPanel();
jPanel.setBackground(Color.black);// 设置面板颜色
jPanel.setLayout(null);//设置面板的布局类型
// 设置面板大小
jPanel.setBounds(0,0,800,600);
// 生成两个文本域、一个文本框、一个按钮
text_Ciphertext=new JTextArea(); // 20为超过20个字则换行
text_Plaintext=new JTextArea(20,10);
text_Input = new JTextField(20); // 20为文本框的高度
button_send = new JButton("发送");
// 设置文本域中字体的属性,行楷,字体类型(加粗等,这里是标准),大小
text_Ciphertext.setFont(new Font("行楷",Font.PLAIN,20));
text_Plaintext.setFont(new Font("楷书",Font.PLAIN,20));
// 设置文本框和按钮的位置,以生成的(即生成的窗体)JFrame的左上角为(0,0)开始设置的位置
text_Input.setBounds(400,535,300,30);
button_send.setBounds(700,535,100,30);
// 生成滚动面板、并将对应的文本域放入
jScrollPane_C = new JScrollPane(text_Ciphertext);
jScrollPane_P = new JScrollPane(text_Plaintext);
// 设置滚动面板在面板中的位置
jScrollPane_C.setBounds(0,0,400,565);
jScrollPane_P.setBounds(400,0,400,535);
// 将滚动面板、按钮放入面板
jPanel.add(jScrollPane_C);
jPanel.add(jScrollPane_P);
jPanel.add(text_Input);
jPanel.add(button_send);
//将面板放入JFarm中
contentPane.add(jPanel);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);//点 x 后直接退出
// 设置窗口大小固定
setResizable(false);
// 设置窗口名称
setTitle(myName);
// 设置可见性
setVisible(true);
// 从本地的序列化文件中获得自己的私钥
PublicPrivateKeyAcquisition acquisition = new PublicPrivateKeyAcquisition();
try {
// 获取私钥
privateKey = acquisition.deserialization(myName).getPrivate();
} catch (Exception e) {
e.printStackTrace();
}
// 按钮事件监听器,即点击按钮后会发生文本框中的内容
button_send.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String text_encryption="";
// 先获取文本框内容
String text = text_Input.getText();
// 判断文本框是否存在内容,如果存在则发送
if (!text.equals("")){
try {
// 将要发送的消息显示在右边文本域中
text_Plaintext.append("\n" + myName + ": " + text + "\n");
// 加密原消息
text_encryption = rsa.encryption(text, privateKey);
// 给加密后的消息添加SHA摘要
text_encryption = text_encryption+" "+rsa.encoderSHA(text)+" ";
} catch (Exception exception) {
exception.printStackTrace();
}
// 利用UDP发生内容给指定对象
send.send(text_encryption,hePort);
// 最后清空文本框
text_Input.setText("");
}
}
});
}
}
实现手段:
调用生成密钥对、序列化的工具类
package com.Test;
import com.PublicPrivateKeyAcquisition;
public class GeneratePublicPrivateKey {
public static void main(String[] args) {
// 生成公私钥,要生成两对密钥对。
try {
PublicPrivateKeyAcquisition acquisition = new PublicPrivateKeyAcquisition();
acquisition.produceKeyPair();// 生成密钥对
acquisition.serialize("Bob");// 序列化密钥对
acquisition.produceKeyPair();// 生成密钥对
acquisition.serialize("Alice");// 序列化密钥对
} catch (Exception e) {
e.printStackTrace();
}
}
}
实现手段:
两个用户实现手段差不多,具体看下面Alice的解释。
package com.Test;
import com.*;
public class Bob {
GUIChat guiChat = null; // 获得图形化界面的对象,用来作为启动图形化界面的变量
UDPChatSender sender = null; // 发送消息所用到的变量
RSAOperation rsaOperation = null; // 数字签名,加密,解密,完整性认证,SHA摘要所用到的变量
UDPChatReceiver receiver = null; // 接收消息所用到的变量
// 初始化、即启动图形化界面,启动接收线程。
public void init(){
// 开启图形化界面
guiChat = new GUIChat();
// 设置好发送所需要的信息
sender = new UDPChatSender(9999);
// new出其对象
rsaOperation = new RSAOperation();
// 初始化图形化界面
guiChat.init("Bob",8881,sender,rsaOperation);
// 先开启接收线程
receiver = new UDPChatReceiver(9991,rsaOperation,guiChat,"Alice");
new Thread(receiver).start();
// 尝试连接用户
initConnection();
}
// 尝试连接用户、即被动等待 A 发送过来的连接信息,接收到后再返回一个回执信息给A
public void initConnection(){
boolean flag=true; // 连接成功后用来退出连接的变量
int i=0;
// 不主动发信息,先获得对方的连接信息然后再回复对方一个信息
// 循环中内容是为了将尝试连接这个消息进行动态变化
while (flag) {
i=(++i)%3;
try {
Thread.sleep(1000); // 休眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
guiChat.text_Ciphertext.setText(""); //清空文本域
if (i==0)
// 设置文本域显示连接标志
guiChat.text_Ciphertext.setText("尝试连接中.");
else if (i==1)
// 设置文本域显示连接标志
guiChat.text_Ciphertext.append("尝试连接中..");
else if (i==2)
// 设置文本域显示连接标志
guiChat.text_Ciphertext.append("尝试连接中...");
if (receiver.receiveData.equals("请求连接")){
flag = false;
// 发送回执信息
sender.send("请求连接 B ",8881);
guiChat.text_Ciphertext.setText(""); //清空文本域,即设置文本域的内容为空
guiChat.text_Ciphertext.setText("连接成功!\n");
}
}
}
public static void main(String[] args) {
new Bob().init();
}
}
实现手段:
先初始化生成我们要用的类、图形化界面类、发送、接收类、RAS工具类。在设置发送、接收类时定义好端口号。
然后调用图形化界面的初始化方法,显示图形化界面。
开启接收线程、可以及时接收到请求连接的信息。
然后调用尝试连接用户的方法,等待连接。
连接成功、正常通信。
package com.Test;
import com.*;
public class Alice {
GUIChat guiChat = null;
UDPChatSender sender = null;
RSAOperation rsaOperation = null;
UDPChatReceiver receiver = null;
public void init(){
// 开启图形化界面
guiChat = new GUIChat();
sender = new UDPChatSender(8888);
rsaOperation = new RSAOperation();
guiChat.init("Alice",9991,sender,rsaOperation);
// 先开启接收线程
receiver = new UDPChatReceiver(8881,rsaOperation,guiChat,"Bob");
new Thread(receiver).start();
// 尝试连接用户
initConnection();
}
// 尝试连接用户
public void initConnection(){
boolean flag=true;
int i=0;
// 隔 3 秒发送一次信息、等待回执信息、获得回执后接束发送。
while (flag) {
i=(++i)%3;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guiChat.text_Ciphertext.setText(""); //清空文本域
if (i==1)
// 设置文本域显示连接标志
guiChat.text_Ciphertext.setText("尝试连接中.");
else if (i==2)
// 设置文本域显示连接标志
guiChat.text_Ciphertext.append("尝试连接中..");
else if (i==0) {
// 设置文本域显示连接标志
guiChat.text_Ciphertext.append("尝试连接中...");
// 发送信息
sender.send("请求连接 A ",9991);
}
if (receiver.receiveData.equals("请求连接")){
flag = false;
guiChat.text_Ciphertext.setText(""); //清空文本域
guiChat.text_Ciphertext.setText("连接成功!\n");
}
}
}
public static void main(String[] args) {
new Alice().init();
}
}
目标:编写基于RSA进行数字签名和图形化界面的本地通讯程序。
要求:1、发送的信息包括原始消息和签名,原始消息通过RSA的私钥加密,签名则是利用SHA散列函数对原始消息进行摘要。
2、接收到消息后通过SHA的值检测原始消息的完整性。
3、利用图形化界面完成。
架构设计:
整个程序应分为四部分:1、基于RSA的公私钥生成。2、RSA。3、本地聊天。4、图形化界面。
1、公私钥获取:生成公私钥、序列化公私钥、获取公钥、获取私钥。(PublicPrivateKeyAcquisition类)
2、RSA包含:加密信息,解密信息,SHA散列值获取,完整性认证。(RSAOperation类)
3、UDP。
4、GUI。
1、先生成自己的公私钥并序列化保存(手动运行GeneratePublicPrivateKey类)
2、开启聊天软件,等待与对方进行连接(分别启动Alice和Bob类)
3、连接成功,进行加密通信