RSA加密Socket傳輸文件、簽名
轉自 CSDN /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();
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);
//加密
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;
- }
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;
- }
- }
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);
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();
- }
- }
- }
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();
- }
- }
- }
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
#configure private key on server
rsa_private=d:/rsa1_private.key
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;
- }
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();
- }
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
# 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可以運行你的文件服務器。服務器啓動後 可以運行客戶端 測試一下。。。
到此,一個簡單的文件加密傳輸小例子就寫好了。。。
謝謝大家~!