密码学 - RSA 公钥密码算法原理及原生实现

1,161 阅读17分钟

一. 简介

最初的公钥方案是在 1977 年由 Ron Rivest、Adi Shamir 和Len Adleman 在 MIT 提出的,并且于 1978 年首次发表。 RSA 方案从那时起便占据了绝对的统治地位,成为最广泛接受和实现的通用公钥加密方法。 RSA 是分组密码,对于某个 m 它的明文和密文是 0〜n-1 之间的整数。

二. 用途

用于在开放的网络环境上保护电子通信,而不依赖于隐藏或隐蔽的通道,甚至用于密钥交换。开放的网络环境容易受到各种通信安全问题的影响,比如中间人攻击和欺骗。通信安全通常包括要求通信不得在运输途中可读(保存保密),通信在运输途中不能修改(保证沟通完整性),沟通必须来自一个确定原(发送方真实性),和收件人必须不能否定或拒绝接收的通信。将非对称加密与 Enveloped Public Key Encryption(EPKE)方法相结合,允许在开放的网络环境中安全发送通信。换句话说,即使密码分析者听了包括密钥交换在内的整个对话也无法解释对话。

公钥密码术中使用的区别技术是使用非对称密钥算法,其中一方用于执行加密的密钥与另一方用于解密的密钥不同。每个用户都有一对加密密钥——一个公共加密密钥和一个私有解密密钥。例如,用于数字签名的密钥对(包括一个私有签名密钥和一个公共验证密钥)。公钥可以广泛分发,而私钥只有其所有者知道。它们在数学上是相关的,但是选择参数是为了从公钥计算私钥是不可行的。

相反,对称密钥算法使用的是一个秘密密钥,它必须由发送方(用于加密)和接收方)用于解密)共享并保持私有。要使用对称加密方案,发送方和接收方必须事先安全地共享密钥。

由于对称密钥算法几乎总是比非对称密钥算法计算量小得多,所以通常使用密钥交换算法交换密钥,然后使用该密钥和对称密钥算法传输数据。PGP 和 SSL/TLS 方案家族使用这个过程,因此称为混合加密系统。综上非对称算法适用于

  • 密钥交换
  • 数字签名
  • 与对称加密算法混合使用(PGP, SSL, TLS)

三. 原理

对于某一明文块M和密文块C,加密和解密有如下的形式:

C = M^e \ mod \ n
M = C^d \ mod \ n = (M^e)^d \ mod\ n = M^{ed} mod \ n

发送方和接收方都必须知道 n 和 e 的值,并且只有接收者知道d的值。RSA 公钥密码算法的公钥KU=<e, n>私钥KR=<d, n>。为使该算法能够用于公钥加密,它必须满足下列要求:

  • 可以找到 e、d、n 的值,使得对所有的 Medmod n = M 成立。
  • 对所有满足 M < n 的值,计算 M 和 Cd 相对容易。
  • 给定 e 和 n 不可能推出 d

前两个要求很容易得到满足。当 e 和 n 取很大的值时,第三个要求也能够得到满足。下面的图 1 总结了 RSA 算法。

  1. 开始时选择两个素数 p 和 q,计算它们的积 n 作为加密和解密时的模。
  2. 接着需要计算 n 的欧拉函数值 φ(n)。φ(n) 表示小于 n 且与 n 互素的正整数的个数。
  3. 然后选择与 φ(n) 互素的整数 e,即 e 和 φ(n) 的最大公约数为 1。
  4. 最后,计算 e 关于模 φ(n) 的乘法逆元 d。d 和 e 具有所期望的属性。

假设用户 A 已经公布了他的公钥,且用户 B 希望给 A 发送消息 M。那么B计算 C = Me(mod n) 并且发送 C。当接收到密文时,用户 A 通过计算 M = Cd (mod n) 解密密文。

上面的图 2 显示一个例子。对于这个例子,按下列步骤生成密钥:

  • 选择两个素数:p=17 和 q=11。
  • 计算 n = pq = 17x11 = 187。
  • 计算 φ(n) = (p-l)(q-l) = 16xl0 = 160。
  • 选择 e,使得 e与φ(n) =160 互素且小于 φ(n):选择 e=7
  • 计算 d,使得 de mod 160 = 1 且 d<160。 正确的值是 d=23 。这是因为 23x7 = 161 = 10x16 + 1。

这样就得到公钥PU = <7, 187>,私钥 PR = <23, 187>。下面的例子说明输入明文M= 88时密钥的使用情况。

对于加密,需要计算C = 887modl 87。利用模运算的性质,计算如下:

88^7 mod \ 187= [(88^4 \ mod \ 187) \ x \ (88^2 \ mod \ 187) \ x\ (88^1 \ modi \ 87)] \ mod \ 187
88^1\ mod \ 187 = 88
88^2 \ mod \ 187 = 7744 \ mod \ 187 = 77
88^4 \ mod \ 187 = 59969536 \ mod \ 187 = 132
88^7 \ mod \ 187 = (88 \ x \ 77\ x\ 132) \ modl \ 87 = 894432 \ mod \ 187=11

对于解密,计算M = 1123 mod 187:

11^{23} \ mod \ 187 = [(11^1 \ mod \ 187) \ x \ (11^2 \ mod \ 187) \  x \ (11^4 \ mod \ 187) \ x \ (11^8 \ mod \ 187) \ x \ (11^8 \ mod \ 187)] \ mod \ 187
11^1 \ modl\ 87 = 11
11^2 \ mod \ 187 = 121
11^4 \ mod \ 187 = 14 641 \ mod \ 187 = 55
l11^8 \ mod \ 187 = 214 358 881 \ mod \ 187 = 33
11^{23} \ mod \ 187 = (11 \ x \ 121 \ x \ 55 \ x\ 33 \ x\ 33)\ mod\ 187 = 79 720 245\  mod\ 187 = 88

四. 代码实现

说到这里,我们知道,要实现 RSA 算法,需要解决下面的数学问题:

  • 如何产生素数?怎么进行素数测试?
  • 怎么进行模幂运算,怎么实现快速模幂?
  • 如何用欧几里得算法求最大公因子?
  • 怎么用中国剩余定理(扩展欧几里得算法)求解 ax = b (mod m) ?
  • 怎么构造一次同余方程,求乘法逆元?

1. 素数选择与素性测试

通过随机的方式生成大数,对这个大数进行一定轮数的素性测试。

素数又称质数,是在大于 1 的整数中,只能被 1 和其自身整除的数(如 2、3、5 等)。素性测试是检验一个给定的整数是否为素数的测试。常见的素性测试方法有:

  • 试除法
  • 概率性检验算法
    • 费马小定理 (Fermat’s little theorem)
    • 费马概率性检验 (Fermat primality test)
    • 米勒-拉宾概率性检验 (Miller–Rabin primality test)

在这里不展开说了,感兴趣的朋友可以看看我的 这篇博客。下面PrimalityTester.isProbablePrime使用的是米勒-拉宾素性测试。

/**
 * 随机生成一个可能是素数的数
 * @param range 随机数产生范围
 * @return 一个可能是素数的数
 */
public static long probablePrime(int range, int rounds) {
    if (range > 0 && rounds > 0) {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        while (true) {
            int num = random.nextInt(range) + 2;
            // 进行rounds轮素性测试
            if (PrimalityTester.isProbablePrime(num, rounds))
                return num;
        }
    }
    return  -1;
}

2. 实现模逆算法

欧几里得算法:利用辗转相除法求最大公约数。

public static long gcd(long m, long n) {
    while(true){
        if ((m = m % n) == 0)
            return n;
        if ((n = n % m) == 0)
            return m;
    }
}

使用实现非递归的扩展欧几里得算法求解ax + by = gcd(a,b)

private static long extendedEuclid(long a, long b) {
    long x = 0, y = 1, lastX = 1, lastY = 0, temp;

    if (a < b) {
        temp = a;
        a = b;
        b = temp;
    }

    while (b != 0) {
        long q = a / b, r = a % b;
        a = b;
        b = r;

        temp = x;
        x = lastX - q * x;
        lastX = temp;

        temp = y;
        y = lastY - q * y;
        lastY = temp;
    }
    return lastY;
}

通过求解线性同余方程ax ≡ b(mod m)求解乘法逆元:

public static long linearCongruence(long a, long b, long m) {
    if(b % gcd(a, m) != 0)
        return - 1;
    /*
      通过扩展欧几里得算法求得x的逆元x'
      x = kx', b = k(a, m)
      所以要求地 x = (b / gcd(a, m)) * x'
     */
    long result = (b / gcd(a, m)) * extendedEuclid(a, m);
    if(result < 0)
        result += m;
    return result;
}

3. 实现快速模指运算

实现了模指运算的递归及非递归算法,因为是理论性实验,将实验数据控制在不会发生溢出的范围内,所以可以选用非递归的做法。modExpNonRec()实现了快速的运算算法,即反复平方乘算法。

实现了模指运算的递归及非递归算法,因为是理论性实验,将实验数据控制在不会发生溢出的范围内,所以可以选用非递归的做法。modExpNonRec()实现了快速的运算算法,即反复平方乘算法。

public static int exp(int x, int b, int n) {
    if (b == 1) {
        return x % n;
    }
    if ((b & 1) 
L
/** Function to calculate (a ^ b) % c **/
public static long modExpNonRec(long a, long b, long c) {
    long res = 1;
    for (int i = 0; i < b; i++) {
        res *= a;
        res %= c;
    }
    return res % c;
}

4. 编程实现 RSA 算法

基于上述的基础函数,可以写出RSA加解密算法,并进行封装。在加密时,因为当数值val超过2 << 16时,将val转换为char类型会发生溢出问题,所以将模逆结果值分割成两个字符表示,在解密时再进行还原。

package org.jordon.security.core.crypto.assymmetry;

import org.jordon.security.util.MathUtil;

import java.util.Base64;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 原生RSA加解密算法实现
 * @author Jrodon
 * @since 2018/10
 */
public class RawRSACipherService {
    // 密钥属性组
    private long n, d, e;
    // 素数的选择范围
    private static final int RANGE = 2 << 10;
    // 加密完得到的
    private static final int SPLIT_POINT = 2 << 8;
    // 素性测试轮数
    private static final int PRIMALITY_TESTING_ROUNDS = 4;
    // Base64编码工具
    private static final Base64.Encoder encoder = Base64.getEncoder();
    private static final Base64.Decoder decoder = Base64.getDecoder();

    public RawRSACipherService() {
        Random random = ThreadLocalRandom.current();
        // 随机选择两个大素数
        long p = MathUtil.probablePrime(RANGE, PRIMALITY_TESTING_ROUNDS), q;
        do {
            q = MathUtil.probablePrime(RANGE, PRIMALITY_TESTING_ROUNDS);
        } while (p == q);

        this.n = p * q;
        long eulerVal = (p - 1) * (q - 1);

        // 随机地取一个正整数e,1<e<φ(n)且(e,φ(n))=1,将e公开
        do {
            e = Math.abs(random.nextLong()) % eulerVal + 1;
        }while (MathUtil.gcd(e, eulerVal) != 1);

        // 根据ed = 1 (mod φ(n)),求出d,并对d保密
        d = (MathUtil.linearCongruence(e, 1, eulerVal)) % eulerVal;
    }

    /**
     * 加密函数,当数值val超过2 << 16时,将val转换为char类型会发生溢出问题
     *         所以将模逆结果值分割成两个字符表示,在解密时再进行还原
     * @param plaintext 明文
     * @return 密文
     */
    public String encrypt(String plaintext) {
        int[] plaintextBytes = changeToInts(plaintext.toCharArray());
        StringBuilder builder = new StringBuilder();
        for (int plaintextByte : plaintextBytes) {
            int modExpResult = (int) MathUtil.modExpNonRec(plaintextByte, e, n);
            builder.append((char) (modExpResult / SPLIT_POINT)).
                    append((char) (modExpResult % SPLIT_POINT));
        }
        return encoder.encodeToString(builder.toString().getBytes());
    }

    /**
     * 解密函数
     * @param cipher 密文
     * @return 明文
     */
    public String decrypt(String cipher) {
        // 获取解码后的字符串的字符数组
        char[] cipherChars = new String(decoder.decode(cipher)).toCharArray();

        // 将相邻两个字符合并,用一个整型数表示,得到原加密结果数组
        int[] cipherInts = new int[cipherChars.length / 2];
        for(int i = 0; i < cipherInts.length; i++)
            cipherInts[i] = (int)cipherChars[i * 2] * SPLIT_POINT
                                    + (int)cipherChars[i * 2 + 1];
        // 解密
        int[] plaintextInts = new int[cipherInts.length];
        for(int i = 0; i < cipherInts.length; i++)
            plaintextInts[i] = (int)MathUtil.modExpNonRec(cipherInts[i], d, n);

        StringBuilder plainText = new StringBuilder();
        for (int plaintextInt : plaintextInts)
            plainText.append((char) plaintextInt);
        return plainText.toString();
    }

    /**
     * 考虑到Java的字符编码问题和扩展性,将字符数组转化为int数组
     * 而不用字节表示
     * @param chars 字符数组
     * @return int数组
     */
    private int[] changeToInts(char[] chars) {
        if (chars != null && chars.length > 0) {
            int[] result = new int[chars.length];
            for (int i = 0; i < chars.length; i++) {
                result[i] = chars[i];
            }
            return result;
        }
        return null;
    }
}

5. 利用 RSA 进行数据加解密

使用 JUnit 编写测试代码,为了便于查看,随机生成可打印的明文样例的进行 n 轮测试。为了便于查看,将素数选择范围调到 100,明文长度调到 20 个以内。

package org.jordon.security.core.crypto.assymmetry;

import org.jordon.security.util.ArrayUtil;
import org.junit.Test;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class RawRSACipherServiceTest {
    private Random random = ThreadLocalRandom.current();

    @Test
    public void test() {
        RawRSACipherService service = new RawRSACipherService();
        for (int i = 0; i < 4; i++) { // 进行30次测试
            String example = genPlaintext();
            ArrayUtil.printInfo("example", example, false);
            String cipher = service.encrypt(example);
            ArrayUtil.printInfo("cipher", cipher, false);
            String plaintext = service.decrypt(cipher);
            ArrayUtil.printInfo("plaintext", plaintext, true);
        }
    }

    // 随机生成明文样例
    private String genPlaintext() {
        // 随机生成含有[1, 50]个可打印字符的字符串
        int count = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < count; i++) {
            // 在可打印字符范围内随机获取
            int val = random.nextInt(126 -33) + 33;
            builder.append((char) val);
        }
        return builder.toString();
    }
}

测试结果

example                       PQ?j+fMFCVcql`Q!W"Ca                                       
cipher                        AMKeAC8ASQDCjAArAMKIAE0AJABDAMKJAFgAfQDCtQAoAC8AIQBXACIAQwAF          
plaintext                     PQ?j+fMFCVcql`Q!W"Ca                                       

example                       qX(zN0"X                                                   
cipher                        AH0AYwBgAMKFAMKxAAMAIgBj                                   
plaintext                     qX(zN0"X                                                   

example                       \QWD6XO*=k?D6                                              
cipher                        AMKSAC8AVwARAEEAYwAGAF0AGAAdAEkAEQBB                       
plaintext                     \QWD6XO*=k?D6                                                         
example                       abXluHtY}37j5                                               
cipher                        AAUAYgBjAMK1AMKXAHsAwpwAWQBxAFUANwDCjABo                   
plaintext                     abXluHtY}37j5                                          

6. 实现对大数据进行加密

为了实现对大数据进行加密,可以将上述过程中的数据类型从 long 改成 BigInteger,因为BigInteger中提供一系列专门用于 RSA 设计的方法

  • 大素数产生: BigInteger.probablePrime (内置两重素性检验)
  • 模幂运算:modPow()
  • 模逆云散:modInverse()

所以很容易就能写出相应的 RSA 算法:

package org.jordon.security.core.crypto.assymmetry;

import lombok.Getter;
import java.math.BigInteger;
import java.security.SecureRandom;

@Getter
public class RSACipherService {
    private final static BigInteger one      = new BigInteger("1");
    private final static SecureRandom random = new SecureRandom();

    private BigInteger privateKey;
    private BigInteger publicKey;
    private BigInteger modulus;

    // generate an N-bit (roughly) public and private key
    public RSACipherService(int N) {
        // 随机选取选取素数p,q
        BigInteger p = BigInteger.probablePrime(N / 2, random);
        BigInteger q = BigInteger.probablePrime(N / 2, random);
        // 计算欧拉函数值
        BigInteger phi = (p.subtract(one)).multiply(q.subtract(one));

        modulus    = p.multiply(q);
        // 在实际应用中公钥通常为2^16 + 1
        publicKey  = new BigInteger("65537");
        // 模逆运算
        privateKey = publicKey.modInverse(phi);
    }
	// 加密
    public BigInteger encrypt(BigInteger message) {
        return message.modPow(publicKey, modulus);
    }
	// 解密
    public BigInteger decrypt(BigInteger encrypted) {
        return encrypted.modPow(privateKey, modulus);
    }

    public String toString() {
        String s = "";
        s += "public  = " + publicKey  + "\n";
        s += "private = " + privateKey + "\n";
        s += "modulus = " + modulus;
        return s;
    }
}

添加单元测试

package org.jordon.security.core.crypto.assymmetry;

import org.junit.Test;
import java.math.BigInteger;
import java.security.SecureRandom;

public class RSACipherServiceTest {
    private RSACipherService service = new RSACipherService(2 << 10);
    private final static SecureRandom random = new SecureRandom();

    @Test
    public void test() {
        int N = 1024;
        RSACipherService key = new RSACipherService(N);
        System.out.println(key);
        // create random message, encrypt and decrypt
        BigInteger message = new BigInteger(N - 1, random);
        BigInteger encrypt = key.encrypt(message);
        BigInteger decrypt = key.decrypt(encrypt);
        System.out.println("message   = " + message);
        System.out.println("encrypted = " + encrypt);
        System.out.println("decrypted = " + decrypt);
    }
}

测试结果

public  = 65537
private = 78525902072320391608737916884640910364505357174525590710167120993759405509547140594627878863225003441361395545158796231923361941968429447718551660867382833237960574251865474859924434185132857131903215178186236747385107077061523018585463899345444036005905759639826169641884190715470009961170662302049779058113
modulus = 111123511057904247384303352454411628574852901907645613196843638982726078745879922118460167927517210453802508633681810948599960519752655050853574023973606618611715320478128088434295076817222464874390543837050033282496413714543565095232472865740444355218882825770580971590396819382161816782243359703760806220343
message   = 64817416795314078589627041975736817397997584028795185521897274181295008592767171589585106415811184609379911184733318659651706555695848036601359747359521829105599014520845612387012461222627857364612289243289267763784602632108473742072081572298228631598483984218130386362800051626492833278994468945333676583030
encrypted = 67902482420062658281496748456690089583528632570276215918392904236166977373756787220355016586353876542515948791235556706391024025337777674873358863284240161449718904162441130678507273673627624342258514489584294519298979557329482563284543751683385911800817642057923152429340572932972532657573862337149437977501
decrypted = 64817416795314078589627041975736817397997584028795185521897274181295008592767171589585106415811184609379911184733318659651706555695848036601359747359521829105599014520845612387012461222627857364612289243289267763784602632108473742072081572298228631598483984218130386362800051626492833278994468945333676583030

7. 实现简单的 GUI 界面

实现了普通 RSA 加解密界面 RSAView 和对大数据进行加解密的界面 RSABigIntegerView ,代码很长不贴了。

8. 分析总结

  • RSA 算法是基于大合数难分解的数学事实实现的,只要 n 选的足够大,密码是很难被攻破的。
  • 在确定密码参数情况下,解密时使用不同的d,不能还原明文。
  • 在大素数产生环节,需要确定较可靠的素性检验算法,并进行足够多轮的测试验证。
  • 随机数产生环节应考虑使用较安全的随机数生成算法,如对种子进行哈希等。
  • 模幂运算是 RSA 的核心运算,在设计 RSA 实现时应该考虑使用更加高效的模幂算法,以提高 RSA 加解密的速度。

另外,本文涉及的代码片段已经上传到 Github,感兴趣的朋友可以去看看,如果对你有帮助,希望能点个 star 或 fork 支持一下。