1. 程式人生 > >使用rsa進行http傳輸加密

使用rsa進行http傳輸加密

.org exce adding 證書 row ioe 拒絕 .get 初始化

? 版權聲明:本文為博主原創文章,轉載請註明出處

[TOC]

1. RSA算法

RSA是目前最有影響力和最常用的公鑰加密算法,它能夠抵抗到目前為止已知的絕大多數密碼攻擊,已被ISO推薦為公鑰數據加密標準。

今天只有短的RSA鑰匙才可能被強力方式破解。但在分布式計算和量子計算機理論日趨成熟的今天,RSA加密安全性收到了挑戰和質疑。

RSA算法基於一個十分簡單的數論事實:將兩個大質數相乘十分容易,但是想要對其乘積進行因式分解缺及其困難,因此可以將乘積公開作為加密密鑰。

2. HTTPS

2.1 HTTPS優點

1. 使用HTTPS協議可認證用戶和服務器,確保數據發送到正確的客戶機和服務器。

2. HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網絡協議,要比HTTP協議安全,可防止數據在傳輸過程中不被竊取、改變,確保數據的完整性。

3. HTTPS是現行框架下最安全的解決方案,雖然不是覺得安全,但它增加了中間人攻擊的成本。

2.2 HTTPS缺點

1. SSL的專業證書需要購買,功能越強大的證書費用越高

2. 相同的網絡環境下,HTTPS協議會使頁面的加載時間延長50%,增加10%-20%的耗電。此外,HTTPS協議還會影響緩存,增加數據開銷和功耗。

3. HTTPS協議的安全性是有範圍的,在黑客攻擊、拒絕服務攻擊、服務器劫持等方面幾乎起不到什麽作用。

4. 最關鍵的是,SSL證書的信用鏈體系並不安全。特別是在某些國家可以控制CA根證書的情況下,中間人攻擊一樣可行。

3. RSA傳輸加密實現

綜上所述(其實主要是因為HTTPS購買SSL證書需要花錢),可在某些關鍵數據傳輸過程中進行RSA加密。比如:登錄時對登錄密碼進行加密。

3.1 所需插件

3.1.1 JS插件

BigInt.js - 用於生成一個大整數(這是RSA算法的需要)
Barrett.js - RSA算法所需要用到的一個支持文件
RSA_Stripped.js - RSA的主要算法

下載密碼:bhiq

3.1.2 所需JAR

bcprov-jdk15on
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.58</version>
</dependency>

3.1.3 代碼

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.study</groupId>
    <artifactId>webrsa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- bcprov-jdk15on -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.58</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <target>1.7</target>
                    <source>1.7</source>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
</project>
RSAUtils.java
package com.study.webrsa.utils;

import java.io.ByteArrayOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Cipher;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

/**
 * RSA加解密工具類
 *
 */
public class RSAUtils {
    
    public static final String SECURITY = "RSA"; // 加密方式
    public static final String ALGORITHM = "MD5withRSA"; // 加密算法
    public static final String PUBLIC_KEY = "RSAPublicKey"; // 公鑰
    public static final String PRIVATE_KEY = "RSAPrivateKey"; // 私鑰
    
    /**
     * 獲取密鑰
     */
    public static Map<String, Object> getKey() {

        Map<String, Object> map = null;
        try {
            // 生成實現指定算法的KeyPairGenerator對象,用於生成密鑰對
            KeyPairGenerator keyPairGenerator = 
                    KeyPairGenerator.getInstance(SECURITY, new BouncyCastleProvider());
            keyPairGenerator.initialize(1024); // 初始化密鑰長度
            KeyPair keyPair = keyPairGenerator.generateKeyPair(); // 生成密鑰對
            RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); // 獲取公鑰
            RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); // 獲取私鑰
            
            // 保存到map中
            map = new HashMap<String, Object>();
            map.put(PUBLIC_KEY, rsaPublicKey);
            map.put(PRIVATE_KEY, rsaPrivateKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
        
    }
    
    /**
     * 利用私鑰進行解密
     * 
     * @param privateKey
     *                      私鑰
     * @param str
     *                      密文
     * @return
     */
    public static String decrypt(RSAPrivateKey privateKey, String str) {
        
        try {
            System.out.println("密文為:" + str);
            // 獲取實現指定轉換的Cipher對象
            Cipher cipher = Cipher.getInstance("RSA/NONE/NoPadding", new BouncyCastleProvider());
            cipher.init(Cipher.DECRYPT_MODE, privateKey); // 用密鑰初始化此Cipher對象
            
            int blockSize = cipher.getBlockSize(); // 返回塊的大小
            byte[] bytes = hexStringToBytes(str); // 將十六進制轉換為二進制
            int j = 0;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while (bytes.length - j * blockSize > 0) { // 將二進制數據分塊寫入ByteArrayOutputStream中
                baos.write(cipher.doFinal(bytes, j * blockSize, blockSize));
                j++;
            }
            
            // 將二進制數據轉換為字符串
            byte[] bs = baos.toByteArray();
            StringBuilder sb = new StringBuilder();
            sb.append(new String(bs));
            String pwd = sb.reverse().toString();
            System.out.println("明文為:" + pwd);
            return pwd;
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return null;
        
    }
    
    /**
     * 將十六進制字符串轉換為二進制數組
     * 
     * @param hexString
     *                  十六進制字符串
     * @return
     */
    private static byte[] hexStringToBytes(String hexString) {
        
        if (hexString == null || "".equals(hexString)) {
            return null;
        }
        
        hexString = hexString.toUpperCase(); // 全部轉換為大寫字符
        int length = hexString.length() / 2; // 獲取十六進制數據個數
        char[] hexChars = hexString.toCharArray(); // 將十六進制字符串轉換為字符數組
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2; // 開始位置
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); 
        }
        return d;
    }
    
    private static byte charToByte(char ch) {
        
        return (byte) "0123456789ABCDEF".indexOf(ch);
        
    }
    
}
login.jsp
<%@page import="java.util.Map"%>
<%@page import="java.security.interfaces.RSAPrivateKey"%>
<%@page import="java.security.interfaces.RSAPublicKey"%>
<%@page import="com.study.webrsa.utils.RSAUtils"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>用戶登錄</title>
    
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<%
    // 獲取密鑰對
    Map<String, Object> map = RSAUtils.getKey();
    // 獲取公鑰
    RSAPublicKey rsaPublicKey = (RSAPublicKey) map.get(RSAUtils.PUBLIC_KEY);
    // 獲取私鑰
    RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) map.get(RSAUtils.PRIVATE_KEY);
    // 保存私鑰到session中,便於後臺進行解密
    session.setAttribute("rsaKey", rsaPrivateKey);
    // 保存公鑰到request中,便於頁面加密
    String publicExponent = rsaPublicKey.getPublicExponent().toString(16);
    String publicModulus = rsaPublicKey.getModulus().toString(16);
    request.setAttribute("publicExponent", publicExponent);
    request.setAttribute("publicModulus", publicModulus);
%>
<body>
    <div class="container-fluid">
        <form action="login" method="post" class="col-md-6 col-md-offset-3" 
            onsubmit="return cmdEncrypt();">
            <div class="form-group">
                <label for="loginName">登錄名</label>
                <input type="text" id="loginName" name="loginName" class="form-control" 
                    placeholder="請輸入用戶名...">
            </div>
            <div class="form-group">
                <label for="loginPwd">登錄密碼</label>
                <input type="password" id="loginPwd" name="loginPwd" class="form-control" 
                    placeholder="請輸入登錄密碼...">
            </div>
            <button type="submit" class="btn btn-primary">登錄</button>
        </form>
    </div>
</body>
    <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
    <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    <script type="text/javascript" src="resources/rsa/BigInt.js"></script>
    <script type="text/javascript" src="resources/rsa/Barrett.js"></script>
    <script type="text/javascript" src="resources/rsa/RSA_Stripped.js"></script>
    <script type="text/javascript">
        
        // 提交前對密碼進行加密
        function cmdEncrypt() {
            
            setMaxDigits(131);
            var pwd = $("#loginPwd").val(); // 獲取原始密碼
            var key = new RSAKeyPair("${publicExponent}", "", "${publicModulus}");
            pwd = encryptedString(key, pwd); // 對密碼進行加密
            $("#loginPwd").val(pwd);
            return true;
            
        }
    
    </script>
</html>
LoginServlet.java
package com.study.webrsa.servlet;

import java.io.IOException;
import java.security.interfaces.RSAPrivateKey;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.study.webrsa.utils.RSAUtils;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        doPost(req, resp);
        
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        // 設置編碼格式
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        
        // 獲取前臺參數
        String loginName = req.getParameter("loginName");
        String loginPwd = req.getParameter("loginPwd");
        
        // 獲取私鑰
        RSAPrivateKey privateKey = (RSAPrivateKey) req.getSession().getAttribute("rsaKey");
        
        // 對密碼進行解密
        loginPwd = RSAUtils.decrypt(privateKey, loginPwd);
        
        // 校驗
        if (true) {
            req.setAttribute("username", loginName);
            System.out.println("用戶[" + loginName + "]用密碼[" + loginPwd + "]登錄本系統");
            req.getRequestDispatcher("/success.jsp").forward(req, resp);
        }
        
    }

    
    
}
success.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登錄成功</title>
</head>
<body>
    <center>
        <h1>歡迎您,${username }</h1>
    </center>
</body>
</html>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    
    <welcome-file-list>
        <welcome-file>login.jsp</welcome-file>
    </welcome-file-list>
    
</web-app>

4. 註意事項

4.1 setMaxDigits()

setMaxDigits(),到底應該傳值多少?

在JS文件中給出公式為:n * 2 / 16。其中n為密鑰長度。
    如果n為1024,則值應為 1024 * 2 / 16 = 128。

經過測試,傳128後臺解密會報錯;正確的值應該大於128。

個人喜好的公式是:n * 2 / 16 + 3
即  密鑰長度若為1024,其值為 131
    密鑰長度若為2048,其值為 259

4.2 解密方式

在網上百度的代碼,解密方式一般如下所示:
// 獲取實現指定轉換的Cipher對象
Cipher cipher = Cipher.getInstance("RSA/NONE/NoPadding", new BouncyCastleProvider());
cipher.init(Cipher.DECRYPT_MODE, privateKey); // 用密鑰初始化此Cipher對象
            
int blockSize = cipher.getBlockSize(); // 返回塊的大小
byte[] bytes = new BigInteger(str, 16).toByteArray();
int j = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while (bytes.length - j * blockSize > 0) { // 將二進制數據分塊寫入ByteArrayOutputStream中
    baos.write(cipher.doFinal(bytes, j * blockSize, blockSize));
    j++;
}
用上述方式,偶爾會報錯如下所示:
java.lang.IllegalArgumentException: Bad arguments
    at javax.crypto.Cipher.doFinal(Cipher.java:2185)
    at com.study.webrsa.utils.RSAUtils.decrypt(RSAUtils.java:76)
    at com.study.webrsa.servlet.LoginServlet.doPost(LoginServlet.java:43)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:650)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:218)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:110)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:506)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:962)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:445)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1087)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
後發現問題就出現在toByteArray()上面,因為在用上面的三個JS進行加密時,偶爾得出的密文會比正確的密文多出一個byte,裏面是o。

因此可使用如下方式:
// 獲取實現指定轉換的Cipher對象
Cipher cipher = Cipher.getInstance("RSA/NONE/NoPadding", new BouncyCastleProvider());
cipher.init(Cipher.DECRYPT_MODE, privateKey); // 用密鑰初始化此Cipher對象

int blockSize = cipher.getBlockSize(); // 返回塊的大小
byte[] bytes = hexStringToBytes(str); // 將十六進制轉換為二進制
int j = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while (bytes.length - j * blockSize > 0) { // 將二進制數據分塊寫入ByteArrayOutputStream中
    baos.write(cipher.doFinal(bytes, j * blockSize, blockSize));
    j++;
}
            
/**
 * 將十六進制字符串轉換為二進制數組
 * 
 * @param hexString
 *                  十六進制字符串
 * @return
 */
private static byte[] hexStringToBytes(String hexString) {
    
    if (hexString == null || "".equals(hexString)) {
        return null;
    }
    
    hexString = hexString.toUpperCase(); // 全部轉換為大寫字符
    int length = hexString.length() / 2; // 獲取十六進制數據個數
    char[] hexChars = hexString.toCharArray(); // 將十六進制字符串轉換為字符數組
    byte[] d = new byte[length];
    for (int i = 0; i < length; i++) {
        int pos = i * 2; // 開始位置
        d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); 
    }
    return d;
}

private static byte charToByte(char ch) {
    
    return (byte) "0123456789ABCDEF".indexOf(ch);
    
}

參考:
JS加密Java解密報rsa bad argument
HTTPS優缺點、原理解析:我們的網站該不該做HTTPS?

更好的markdown體驗:https://www.zybuluo.com/chy282/note/975080


使用rsa進行http傳輸加密