RSA加密Socket传输文件、签名
转自 优快云 /dyyaries
(一)
RSA加密分为公钥加密和私钥解密以及可能的数字签名。
公钥、私钥分居客户端和服务器端,分别用于加密和解密。同时,私钥还用于签名,公钥还用于验证签名。
解密加密用到JDK中java.security、javax.crypto两个包中相关的接口和类
1.生成密钥的代码
SecureRandom sr = new SecureRandom();
KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA");
// 注意密钥大小最好为1024,否则解密会有乱码情况.
kg.initialize(1024, sr);
KeyPair kp = kg.generateKeyPair();
String privateKeyName = keyFile.substring(0,keyFile.lastIndexOf("."))+"_private"+keyFile.substring(keyFile.lastIndexOf("."));
String publicKeyName = keyFile.substring(0,keyFile.lastIndexOf("."))+"_public"+keyFile.substring(keyFile.lastIndexOf("."));
FileOutputStream fos = new FileOutputStream(privateKeyName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
// 生成私钥
oos.writeObject(kp.getPrivate());
oos.close();
//生成公钥
fos = new FileOutputStream(publicKeyName);
oos = new ObjectOutputStream(fos);
oos.writeObject(kp.getPublic());
oos.close();
2.加密、解密
(RSA加密速度比较慢,原因是每次只能为最多117 bytes加密,加密之后为128 Bytes)
//加密
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, (PublicKey)getKey(keyFile,"public"));
return cipher.doFinal(text);
//加密
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, (PrivateKey)getKey(keyFile,"private"));
return cipher.doFinal(text);
3.数字签名
用私钥进行数字签名,用于服务器端验证客户端数据是否是正确数据。
public byte[] getSignature(byte[] cipherText){
try {
coder = new RSASecurityCoder();
Signature sig = Signature.getInstance("SHA1withRSA");
PrivateKey privateKey = (PrivateKey)coder.getKey(this.key, "private");
sig.initSign(privateKey);
sig.update(cipherText);
return sig.sign();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public boolean verifySignature(byte[] cipherText,byte[] signature){
try {
coder = new RSASecurityCoder();
Signature sig = Signature.getInstance("SHA1withRSA");
PublicKey publicKey = (PublicKey)coder.getKey(this.key, "public");
sig.initVerify(publicKey);
sig.update(cipherText);
return sig.verify(signature);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
数字签名是在客户端进行,所用的私钥不是和用于加密的公钥成对的。
也就是说这样的通信 要在客户端保存有 服务器端的公钥和本地的私钥,而服务器端应该保存有本地的私钥和客户端的公钥。
(二)
服务器端采用多线程方式并行处理传输请求以提高效率。
因为我想用JavaServiceWrapper将 程序作为windows服务运行,所以main方法中需接收一些参数,比如端口、密钥文件位置、文件存放路径等。
1.从main方法接受配置参数
for(String arg : args){
String argName = arg.substring(0,arg.indexOf(":")).trim();
String argValue = arg.substring(arg.indexOf(":")+1).trim();
if(argName.equals("rsa_private")){
privateKey = argValue;
}else if(argName.equals("rsa_public")){
publicKey = argValue;
}else if(argName.equals("port")){
port = Integer.parseInt(argValue);
}else if(argName.equals("file_dir")){
file_dir = argValue;
}
}
2.初始化解密、签名类实例
RSASecurityCoder coder = new RSASecurityCoder(privateKey);
RSASecuritySignature sign = new RSASecuritySignature(publicKey);
3.建立服务器
private static void runServer(String[] args){
long start = System.currentTimeMillis();
//获取配置参数
for(String arg : args){
String argName = arg.substring(0,arg.indexOf(":")).trim();
String argValue = arg.substring(arg.indexOf(":")+1).trim();
if(argName.equals("rsa_private")){
privateKey = argValue;
}else if(argName.equals("rsa_public")){
publicKey = argValue;
}else if(argName.equals("port")){
port = Integer.parseInt(argValue);
}else if(argName.equals("file_dir")){
file_dir = argValue;
}
}
//配置
coder = new RSASecurityCoder(privateKey);
sign = new RSASecuritySignature(publicKey);
//启动服务器
System.out.println("服务器启动中...");
try {
ss = new ServerSocket(port);
clientCount = 0;
} catch (NumberFormatException e) {
System.err.println("端口配置错误");
} catch (IOException e) {
System.err.println("服务器在端口"+port+"启动失败");
}
//文件存放目录
dir = new File(file_dir);
if(!dir.exists()){
dir.mkdir();
}
System.out.println("服务器已启动,端口: "+port);
//计算启动时间
long end = System.currentTimeMillis();
System.out.println("共消耗 "+(end-start)+" ms.");
//接收数据
while(true){
try {
if(ss == null){
ss = new ServerSocket(port);
clientCount = 0;
}
Socket socket = ss.accept();
clientCount += 1;
System.out.println("有新的连接,当前总连接数:"+clientCount);
Thread th = new Thread(new ServerProcessor(socket));
th.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.建立线程类ServerProcessor处理每个文件
static class ServerProcessor extends Thread{
private Socket socket;
private InputStream in;
private String filename;
private long totalBytes;
private File file;
private FileOutputStream fos;
public ServerProcessor(Socket socket){
this.socket = socket;
}
@Override
public void run(){
try {
in = socket.getInputStream();
DataInputStream dis = new DataInputStream(in);
filename = dis.readUTF(); //读取文件名
totalBytes = dis.readLong(); //读取文件大小
file = new File(dir,filename);
fos = new FileOutputStream(file);
//FileOutputStream fos = new FileOutputStream(File.createTempFile(filename, null, dir));
//输出日志
//RSA加密以128 bytes位单位,一次最多加密117bytes.
byte[] buf = new byte[128];
int available;
while((available = in.read(buf)) != -1){
//读取签名
byte[] signature = buf;
//读取数据
buf = new byte[128];
available = in.read(buf);
byte[] availableBytes = null;
if(available == buf.length){
availableBytes = buf;
}else{
availableBytes = new byte[available];
for (int i = 0; i < available; i++) {
availableBytes[i] = (Byte) buf[i];
}
}
//验证数据签名
boolean flag = sign.verifySignature(availableBytes, signature);
//写入数据
if(flag){
//count += availableBytes.length;
fos.write(coder.decrypt(availableBytes));
//System.out.println(""+count+" bytes received");
}
}
fos.close();
socket.shutdownInput();
clientCount -= 1;
System.out.println("有连接断开,当前总连接数:"+clientCount);
System.out.println(new SimpleDateFormat("[yyyy-MM-DD HH:mm:ss ]").format(new Date())
+filename+" 接收完毕. 大小 "+(totalBytes/1024)+" kb,保存路径:"+dir.getAbsolutePath());
} catch (NumberFormatException e) {
System.err.println("端口配置错误");
e.printStackTrace();
} catch (FileNotFoundException e) {
System.err.println("文件没找到,文件名:"+filename+",可能是因为读取socket数据失败");
e.printStackTrace();
} catch (IOException e) {
System.err.println("文件读/写错误");
try {
fos.close();
} catch (IOException e1) {
e1.printStackTrace();
}
file.delete();
e.printStackTrace();
}
}
}
main方法
public static void main(String[] args) {
runServer(args);
}
(三)
接下来是客户端类CoderClient的编写。
1.配置信息
首先配置如下信息,包括服务器地址、端口、密钥文件位置.
conf.properties
#configure private key on server
rsa_private=d:/rsa1_private.key
#public key from client
rsa_public=d:/rsa_public.key
#server
server=127.0.0.1
#server port
port=888
2.发送文件
编写发送文件的方法,其中byte[] buf=new byte[117]的原因是RSA加密算法支持的最大字节数为117,而加密后变成128位,所以服务器端解密的时候可以使用128bytes的buf读取文件。
public boolean send(String filename){
try {
File f = new File(filename);
fis = new FileInputStream(filename);
byte[] buf = new byte[117];
int available;
socket = new Socket(server,port);
os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(f.getName()); //写入文件名
dos.writeLong(f.length()); //写入文件大小
while((available = fis.read(buf)) != -1){
byte[] availableBytes = null;
if(available == buf.length){
availableBytes = buf;
}else{
availableBytes = new byte[available];
for (int i = 0; i < available; i++) {
availableBytes[i] = (byte) buf[i];
}
}
byte[] cipherText = coder.encrypt(availableBytes);
byte[] signature = sign.getSignature(cipherText);
os.write(signature);
os.write(cipherText);
//count += cipherText.length;
//System.out.println("send "+count+" bytes");
}
socket.shutdownOutput();
fis.close();
return true;
} catch (FileNotFoundException e) {
log.error("文件"+filename+" 未找到");
} catch (UnknownHostException e) {
log.error("未知主机:"+server);
} catch (IOException e) {
log.error("文件读/写错误,可能是因为远程主机无响应");
}
return false;
}
3.多线程
为了测试多线程处理效果,可以将客户端继承于Thread,在重写的run方法中调用 send(String file)方法
@Override
public void run() {
send(this.file);
}
4.模拟多客户端
main中的测试可以这样来模拟多个客户端同时向服务器传输文件
File dir = new File("E://files//demo//");
File[] files = dir.listFiles();
for(int i=0; i<files.length; i++){
Thread th = new Thread(new CoderClient(files[i].getAbsolutePath()));
th.start();
}
(四)
最后 用JavaServiceWrapper把服务器端程序做成windows服务自动运行。
可以到http://wrapper.tanukisoftware.com/doc/english/download.jsp这里下载到wrapper-windows-x86-32-3.2.3.zip
首先,在本地建立一个文件夹,其中建立bin、lib、conf和logs目录。
解压wrapper-windows-x86-32-3.2.3.zip到本地,暂且把解压后的文件夹叫做wrapper_home.
- 1.将wrapper_home/bin/wrapper.exe拷贝至你的bin文件夹中,然后拷贝wrapper_home/src/bin中的App.bat.in、InstallApp-NT.bat.in和UninstallApp-NT.bat.in到
- 你的bin文件夹并将.in后缀去掉。
- 2.将wrapper_homw/lib/中的wrapper.dll和wrapper.jar拷贝至你的lib文件夹,并将你的可执行服务器端jar程序放到这里,暂且假定为server.jar.
- 3.将wrapper_home/src/bin/conf中的wrapper.conf.in拷贝至你的conf文件夹并去掉.in后缀。
- 4.修改wrapper.conf配置。要修改的地方我贴到下面列表:
# Java Application (和配置jdk环境变量类型)
wrapper.java.command=D:/installed app/Java/jdk1.6.0_13/bin/java
# Java Classpath (include wrapper.jar) Add class path elements as (本来是指向 ../lib/testwrapper.jar的,改成你要启动的server.jar)
wrapper.java.classpath.1=../lib/server.jar
# Application parameters. Add parameters as needed starting from 1 (这些是中要传入的参数,第一行是java程序入口,后面是main方法参数)
wrapper.app.parameter.1=com.vantasia.common.server.RSAServer
wrapper.app.parameter.2=rsa_private:d:/rsa_private.key
wrapper.app.parameter.3=rsa_public:d:/rsa1_public.key
wrapper.app.parameter.4=port:888
wrapper.app.parameter.5=file_dir:d:/files/
# 日志
wrapper.logfile.maxsize=3m
#服务名 描述 等
wrapper.ntservice.name=MyServer
wrapper.ntservice.displayname=MyServer
wrapper.ntservice.description=socket My server
最后到你的bin文件夹运行App.bat可以运行你的文件服务器。
服务器启动后 可以运行客户端 测试一下。。。
到此,一个简单的文件加密传输小例子就写好了。。。
谢谢大家~!