学号 20175313 《实验五 网络编程与安全》实验报告
一、实验内容
任务1:实现中缀表达式转后缀表达式,后缀表达式求值
0. 参考http://www.cnblogs.com/rocedu/p/6766748.html#SECDSA
- 结对实现中缀表达式转后缀表达式的功能 MyBC.java
- 结对实现从上面功能中获取的表达式中实现后缀表达式求值的功能,调用MyDC.java
- 上传测试代码运行结果截图和码云链接
任务2:通过客户端、服务器实现任务1
0. 注意责任归宿,要会通过测试证明自己没有问题
- 基于Java Socket实现客户端/服务器功能,传输方式用TCP
- 客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式通过网络发送给服务器
- 服务器接收到后缀表达式,调用MyDC.java的功能计算后缀表达式的值,把结果发送给客户端
- 客户端显示服务器发送过来的结果
- 上传测试结果截图和码云链接
任务3:在任务2的基础上,将客户端产生的后缀表达式加密后传给服务器
- 注意责任归宿,要会通过测试证明自己没有问题
- 基于Java Socket实现客户端/服务器功能,传输方式用TCP
- 客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式用3DES或AES算法加密后通过网络把密文发送给服务器
- 服务器接收到后缀表达式表达式后,进行解密(和客户端协商密钥,可以用数组保存),然后调用MyDC.java的功能计算后缀表达式的值,把结果发送给客户端
- 客户端显示服务器发送过来的结果
- 上传测试结果截图和码云链接
任务4:在任务3的基础上,将用来加、解密的密钥通过DH算法进行交换
- 注意责任归宿,要会通过测试证明自己没有问题
- 基于Java Socket实现客户端/服务器功能,传输方式用TCP
- 客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式用3DES或AES算法加密通过网络把密文发送给服务器
- 客户端和服务器用DH算法进行3DES或AES算法的密钥交换
- 服务器接收到后缀表达式表达式后,进行解密,然后调用MyDC.java的功能计算后缀表达式的值,把结果发送给客户端
- 客户端显示服务器发送过来的结果
- 上传测试结果截图和码云链接
任务5:在任务4的基础上,将后缀表达式的摘要值传给服务器,服务器通过解密后计算后缀表达式的MD5值,比对是否与客户端传来的值相同
0. 注意责任归宿,要会通过测试证明自己没有问题
- 基于Java Socket实现客户端/服务器功能,传输方式用TCP
- 客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式用3DES或AES算法加密通过网络把密文和明文的MD5値发送给服务器
- 客户端和服务器用DH算法进行3DES或AES算法的密钥交换
- 服务器接收到后缀表达式表达式后,进行解密,解密后计算明文的MD5值,和客户端传来的MD5进行比较,一致则调用MyDC.java的功能计算后缀表达式的值,把结果发送给客户端
- 客户端显示服务器发送过来的结果
- 上传测试结果截图和码云链接
任务6:将上述过程用Android实现,更多详情请参见学号 20175313 《实验五 网络编程与安全》实验报告番外篇
二、实验步骤
本次实验有5个小步,每一步都是建立在前一步的基础上进行的拓展。
三、关键代码解析
任务1:中缀转后缀,用后缀表达式规则进行计算
-
- 如果遇到操作数就直接输出。
- 如果遇到操作符则将其放入栈中,遇到左括号也将其放入
栈中。 - 如果遇到右括号,则将栈元素弹出,将弹出的操作符输出直到遇到遇到左括号为止,注意左括号只弹出不输出。
- 如果遇到任何其他操作符,如“+”,“-”,“*”,“÷”,“(”等,从栈中弹出元素直到遇到
更低优先级的元素或栈空为止。弹出完这些元素才将遇到的操作符压入栈中。 - 如果读到了输入的末尾,则将栈中所有元素依次弹出。
while (tokenizer.hasMoreTokens()){
token=tokenizer.nextToken();
if (isOperator(token)){
if (!OpStack.empty()){
if(judgeValue(token)>judgeValue(OpStack.peek()) && !token.equals(")") || token.equals("("))
OpStack.push(token);
else if (token.equals(")")){
//如果遇到一个右括号则将栈元素弹出,将弹出的操作符输出直到遇到左括号为止
while (!OpStack.peek().equals("("))
output=output.concat(OpStack.pop()+" ");//弹出左括号上面的所有东西
OpStack.pop();//弹出左括号
}
else {
while (!OpStack.empty() && judgeValue(token)<=judgeValue(OpStack.peek())){
////如果遇到其他任何操作符,从栈中弹出这些元素直到遇到发现更低优先级的元素或栈空为止
output=output.concat(OpStack.pop()+" ");
}
OpStack.push(token);
}
}
else
OpStack.push(token);//如果栈空则直接将遇到的操作符送入栈中,第一个不可能为右括号
}
else {
output=output.concat(token+" ");//如果遇到操作数就直接输出
}
}
while (!OpStack.empty()){
//如果读到了输入分末尾,则将占中所有元素依次弹出
output=output.concat(OpStack.pop()+" ");
}
- 使用后缀表达式规则进行计算
- 规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
while (tokenizer.hasMoreTokens()){
token=tokenizer.nextToken();
if(isOperator(token)){
//遇到操作符,两个操作数出栈
op2=stack.pop();
op1=stack.pop();
result=calcSingle(op1,op2,token);//两个操作数进行相关运算
stack.push(new Integer(result));//运算结果进栈
}
else {
//遇到数字进栈
stack.push(new Integer(token));
}
}
- 调用MyBC类的String getEquation(String s);方法将中缀表达式转化为后缀表达式
MyBC myBC = new MyBC();
output = myBC.getEquation(formula);
- 调用MyDC类的String calculate(String s)方法将得到的后缀表达式计算其结果
MyDC myDC = new MyDC();
result = myDC.calculate(formula);
任务2:客户端输入中缀表达式并转化成后缀表达式发送给服务器,服务器计算后缀表达式,将结果发送给客户端
- 使用正则表达式判断输入的中缀表达式是否合法
String formula = scanner.nextLine();
String regex = ".*[^0-9|+|\\-|*|÷|(|)|\\s|/].*";
if(formula.matches(regex)){
System.out.println("输入了非法字符");
System.exit(1);
}
任务3:客户端得到的后缀表达式经过3DES或AES加密后将密文发送给服务端,服务端收到后将其进行解密,然后再计算后缀表达式的值,把结果发送给客户端
- 获取密钥生成器:
KeyGenerator kg=KeyGenerator.getInstance("AES");
- 初始化密钥生成器:
kg.init(128);
- 生成密钥:
SecretKey k=kg.generateKey( );
- 创建密码器
Cipher cp=Cipher.getInstance("AES");
- 初始化密码器
cp.init(Cipher.ENCRYPT_MODE,k);
- 执行加密
byte []ctext=cp.doFinal(ptext);
- 注意:这里产生的密文是byte[]数组,但是传送给服务器时所使用的参数应该是String类型,如果直接使用
String s = new String(ctext);
这种构造方法的话可能会出现javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
报错。 - 原因:加密后的byte数组是不能强制转换成字符串的,换言之:字符串和byte数组在这种情况下不是互逆的;要避免这种情况,我们需要做一些修订,可以考虑将二进制数据转换成十六进制表示。
- 所以我这里将ctext[]二进制转化成十六进制传送给服务器,服务器接收后再将十六进制转为二进制即可。(后面密钥的传送也是如此)
- Client端代码:
String out1 = B_H.parseByte2HexStr(ctext);
out.writeUTF(out1);
- Server端代码:
String cformula = in.readUTF();//读取密文
byte cipher[] = H_B.parseHexStr2Byte(cformula);
- 密钥传送过程:
- 将SecretKey类型密钥转化为byte[],后面就可采用与密文相同的方式传送给服务器
byte kb[] = k.getEncoded();
- 将byte[]转化为SecretKeySpec,获得密钥
SecretKeySpec key = new SecretKeySpec(decryptKey,"AES");//获得密钥
- 将SecretKey类型密钥转化为byte[],后面就可采用与密文相同的方式传送给服务器
任务4:使用DH算法进行密钥3DES或AES的密钥交换
- 创建密钥对生成器:
KeyPairGenerator kpg=KeyPairGenerator.getInstance("DH");
- 初始化密钥生成器
kpg.initialize(1024);
- 生成密钥对
KeyPair kp=kpg.genKeyPair( );
- 获取公钥和私钥
PublicKey pbkey=kp.getPublic( );
PrivateKey prkey=kp.getPrivate( );
- 创建密钥协定对象:
KeyAgreement ka=KeyAgreement.getInstance("DH");
- 初始化密钥协定对象:
ka.init(prk);
- 执行密钥协定:
ka.doPhase(pbk,true);
- 生成共享信息:
byte[ ] sb=ka.generateSecret();
- 创建密钥:
SecretKeySpec k=new SecretKeySpec(sb,"AES");
客户端
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.Key;
import java.util.Scanner;
import java.net.*;
public class Client5_4 {
public static void main(String[] args) {
String mode = "AES";
//客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式通过网络发送给服务器
Scanner scanner = new Scanner(System.in);
Socket mysocket;
DataInputStream in = null;
DataOutputStream out = null;
try {
mysocket = new Socket("127.0.0.1", 2010);
in = new DataInputStream(mysocket.getInputStream());
out = new DataOutputStream(mysocket.getOutputStream());
System.out.println("请输入要计算的题目:");
String formula = scanner.nextLine();
String regex = ".*[^0-9|+|\\-|*|÷|(|)|\\s|/].*";
if (formula.matches(regex)) {
System.out.println("输入了非法字符");
System.exit(1);
}
String output = "";
MyBC myBC = new MyBC();
try {
//中缀转后缀
output = myBC.getEquation(formula);
} catch (ExprFormatException e) {
System.out.println(e.getMessage());
System.exit(1);
}
//使用AES进行后缀表达式的加密
KeyGenerator kg = KeyGenerator.getInstance(mode);
kg.init(128);
SecretKey k = kg.generateKey();//生成密钥
byte mkey[] = k.getEncoded();
Cipher cp = Cipher.getInstance(mode);
cp.init(Cipher.ENCRYPT_MODE, k);
byte ptext[] = output.getBytes("UTF8");
byte ctext[] = cp.doFinal(ptext);
//将加密后的后缀表达式传送给服务器
String out1 = B_H.parseByte2HexStr(ctext);
out.writeUTF(out1);
//创建客户端DH算法公、私钥
Key_DH.createPubAndPriKey("Clientpub.txt","Clientpri.txt");
//将客户端公钥传给服务器
FileInputStream fp = new FileInputStream("Clientpub.txt");
ObjectInputStream bp = new ObjectInputStream(fp);
Key kp = (Key) bp.readObject();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(kp);
byte[] kb = baos.toByteArray();
String pop = B_H.parseByte2HexStr(kb);
out.writeUTF(pop);
Thread.sleep(1000);
//接收服务器公钥
String push = in.readUTF();
byte np[] = H_B.parseHexStr2Byte(push);
ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (np));
Key k2 = (Key)ois.readObject();;
FileOutputStream f2 = new FileOutputStream("Serverpub.txt");
ObjectOutputStream b2 = new ObjectOutputStream(f2);
b2.writeObject(k2);
//生成共享信息,并生成AES密钥
SecretKeySpec key = KeyAgree.createKey("Serverpub.txt", "Clientpri.txt");
//对加密后缀表达式的密钥进行加密,并传给服务器
cp.init(Cipher.ENCRYPT_MODE, key);
byte ckey[] = cp.doFinal(mkey);
String Key = B_H.parseByte2HexStr(ckey);
out.writeUTF(Key);
//接收服务器回答
String s = in.readUTF();
System.out.println("客户收到服务器的回答:" + s);
} catch (Exception e) {
System.out.println("服务器已断开" + e);
}
}
}
服务器
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.Key;
public class Server5_4 {
public static void main(String[] args) {
String mode = "AES";
ServerSocket serverForClient = null;
Socket socketOnServer = null;
DataOutputStream out = null;
DataInputStream in = null;
try{
serverForClient = new ServerSocket(2010);
}catch (IOException e1){
System.out.println(e1);
}
String result;
try{
System.out.println("等待客户呼叫:");
socketOnServer = serverForClient.accept();
out = new DataOutputStream(socketOnServer.getOutputStream());
in = new DataInputStream(socketOnServer.getInputStream());
//接收加密后的后缀表达式
String cformula = in.readUTF();
byte cipher[] = H_B.parseHexStr2Byte(cformula);
//接收Client端公钥
String push = in.readUTF();
byte np[] = H_B.parseHexStr2Byte(push);
//生成服务器共、私钥
Key_DH.createPubAndPriKey("Serverpub.txt","Serverpri.txt");
//将服务器公钥传给Client端
FileInputStream fp = new FileInputStream("Serverpub.txt");
ObjectInputStream bp = new ObjectInputStream(fp);
Key kp = (Key) bp.readObject();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(kp);
byte[] kb = baos.toByteArray();
String pop = B_H.parseByte2HexStr(kb);
out.writeUTF(pop);
Thread.sleep(1000);
//生成共享信息,并生成AES密钥
SecretKeySpec key = KeyAgree.createKey("Serverpub.txt","Clientpri.txt");
String k = in.readUTF();//读取加密后密钥
byte[] encryptKey = H_B.parseHexStr2Byte(k);
//对加密后密钥进行解密
Cipher cp = Cipher.getInstance(mode);
cp.init(Cipher.DECRYPT_MODE,key);
byte decryptKey [] = cp.doFinal(encryptKey);
//对密文进行解密
SecretKeySpec plainkey=new SecretKeySpec(decryptKey,mode);
cp.init(Cipher.DECRYPT_MODE, plainkey);
byte []plain=cp.doFinal(cipher);
//计算后缀表达式结果
String formula = new String(plain);
MyDC myDC = new MyDC();
try{
result = myDC.calculate(formula);
//后缀表达式formula调用MyDC进行求值
}catch (ExprFormatException e){
result = e.getMessage();
}catch (ArithmeticException e0){
result = "Divide Zero Error";
}
//将计算结果传给Client端
out.writeUTF(result);
}catch (Exception e){
System.out.println("客户已断开"+e);
}
}
}
任务5:客户端利用MD5算法计算其摘要值clientMD5传送给服务器,服务器通过解密得到的明文利用MD5算法计算其摘要值serverMD5,比较二者是否相等
- 生成MessageDigest对象
MessageDigest m=MessageDigest.getInstance("MD5");
- 传入需要计算的字符串
m.update(x.getBytes("UTF8" ));
x为需要计算的字符串 - 计算消息摘要
byte s[ ]=m.digest( );
- 处理计算结果
String result="";
for (int i=0; i<s.length; i++){
result+=Integer.toHexString((0x000000ff & s[i]) | 0xffffff00).substring(6);
}
四、实验结果截图
任务1
- MyBC类测试
- MyDC类测试
任务2
- 输入非法字符'a'
- 连续输入三个操作数
- 连续输入两个操作符
- 缺少右括号
- 缺少左括号
- 除数为0
- 正常情况
任务3
任务4
任务5
五、实验过程中遇到的问题及其解决方法
-
使用
byte []ctext=cp.doFinal(ptext);
进行加密,产生的密文是byte[]数组,但是传送给服务器时所使用的参数应该是String类型,如果直接使用String s = new String(ctext);
这种构造方法会出现javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
报错。 -
原因:加密后的byte数组是不能强制转换成字符串的,换言之:字符串和byte数组在这种情况下不是互逆的;要避免这种情况,我们需要做一些修订,可以考虑将二进制数据转换成十六进制表示。
-
解决方法:将ctext[]二进制转化成十六进制传送给服务器,服务器接收后再将十六进制转为二进制即可。
-
Client端代码:
String out1 = B_H.parseByte2HexStr(ctext);
out.writeUTF(out1);
- Server端代码:
String cformula = in.readUTF();//读取密文
byte cipher[] = H_B.parseHexStr2Byte(cformula);
六、心得体会
- 本次实验就是在之前实验三的基础上加上了第十二章客户端、服务器的内容,看起来并不是很难,但是真正实践起来却还是出现了各种报错,总的来说还是要积极主动敲代码才能够真正学会,光靠看是永远学不会的。
- 通过本次实验,也让我明白了写博客的好处,就是在自己忘记了前面学过的知识的时候,能够通过翻看自己的博客来唤起它们。