ElGamal
文章给出了ElGamal加密算法的一个实现。未做任何优化,仅供学习参考,计算速度跟专业的库相比应该有很大差距。虽然密码学算法是安全的,但是工程实现中可能存在其他攻击,没有足够的信心,不要使用自己写的加密算法做加密。
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;
/**
* @author miracle
* @date 2022/9/21 19:06
* @description: <br/>
*/
public class Main
{
// --------------------------------------------------- 工具函数 -------------------------------------------------------------------
/**
* 大整数的大整数幂对一个大整数求幂的结果(Big Integer Power Modulus)
*
* @param base 底数
* @param exponent 指数
* @param n 模数
* @return base的exponent次幂模n
*/
public static BigInteger BigIntegerPM(BigInteger base,BigInteger exponent,BigInteger n){
// 保证底数大于模数
if(base.compareTo(n) == -1){
base = base.mod(n);
}
BigInteger res = solvePM(base,exponent,n);
return res;
}
/**
* 1、数论中有如下结论
* (a + b) % p = (a % p + b % p) % p
* (a - b) % p = (a % p - b % p) % p
* (a * b) % p = (a % p * b % p) % p
* (a ^ b) % p = ((a % p) ^ b) % p
* (a ^ b) % p = ((a % p) ^ b) % p
* <p>
* 2、分治法求解
* (1) 若 exponent 为奇数: 令 exponent = 2k+1, k 为非负整数 , 则
* a^(2k+1) = (a^k)^2 *a ; a^(2k+1) % n = ((a^k % n)^2 % n * a % n) % n = [ (a^k % n)^2 * a ] % n
* (2) 若power 为偶数: 令 power = 2k, k为非负整数, 则
* a^(2k) = (a^k)^2 ; a^(2k) % n = (a^k % n)^2 % n
* (3) 若 power == 0: 返回 1。
* <p>
* 3、算法分析
* 时间复杂度 T(n) = O( log(n) ) (这里的n指的是 exponent)
* 空间复杂度 T(n) = O( lon(n) ) (递归函数的空间开销)
*
* @param base 底数
* @param exponent 指数
* @param n 模数
* @return base的exponent次幂模n
*/
public static BigInteger solvePM(BigInteger base,BigInteger exponent,BigInteger n){
if(exponent == BigInteger.ZERO){
return new BigInteger("1");
}
if(exponent.mod(new BigInteger("2")) == BigInteger.ZERO){
BigInteger two = new BigInteger("2");
base = solvePM(base,exponent.divide(two),n).pow(2);
return base.mod(n);
}
else{
BigInteger two = new BigInteger("2");
base = solvePM(base,exponent.divide(two),n).pow(2)
.multiply(base);
return base.mod(n);
}
}
/**
* 产生一个范围在 [start,end]之间的随机大整数
*
* @param start 随机数的最小值
* @param end 随机数的最大值
* @return 返回一个范围在[start,end]之间的随机大整数,如果start<end , 返回null
*/
public static BigInteger random(BigInteger start,BigInteger end){
if(start.compareTo(end) > 0){
return null;
}
if(start.compareTo(end) == 0){
return end;
}
BigInteger a;
do{
a = new BigInteger((end.subtract(start)).bitLength(),new Random());
}
while(a.compareTo(end.subtract(start)) > 0);
a = a.add(start);
return a;
}
/**
* 针对大整数的扩展的欧几里得算法
* 时间复杂度T(n) = O(log n)
*
* @param a 任意长度的整数
* @param b 任意长度的整数
* @return a 和 b 的最大公约数以及 s 和 t组成的map,其中 key = "gcd" 的 value 是 gcd(a,b)
*/
public static Map<String,BigInteger> gcdExtend(BigInteger a,BigInteger b){
Map<String,BigInteger> res = new HashMap<>();
BigInteger r, r0 = a, r1 = b;
BigInteger s0 = BigInteger.ONE;
BigInteger t0 = BigInteger.ZERO;
BigInteger s1 = BigInteger.ZERO;
BigInteger t1 = BigInteger.ONE;
BigInteger s = BigInteger.ONE;
BigInteger t = BigInteger.ZERO;
// 如果 a < b; 交换 a,b ; 保证 a >= b
if(a.compareTo(b) == -1){
r0 = b;
r1 = a;
s0 = BigInteger.ZERO;
t0 = BigInteger.ONE;
s1 = BigInteger.ONE;
t1 = BigInteger.ZERO;
}
// 以下两种特殊情况处理
if(r1 == BigInteger.ZERO){
res.put("gcd",r0);
res.put("s",BigInteger.ONE);
res.put("t",BigInteger.ZERO);
return res;
}
if(a.mod(b) == BigInteger.ZERO){
res.put("gcd",r1);
res.put("s",BigInteger.ZERO);
res.put("t",BigInteger.ONE);
return res;
}
// 迭代求解 s和t 使得 as + bt = gcd(a,b)
BigInteger qi = BigInteger.ZERO;
while(r0.mod(r1) != BigInteger.ZERO){
qi = r0.divide(r1);
s = s0.subtract(s1.multiply(qi));
t = t0.subtract(t1.multiply(qi));
s0 = s1;
t0 = t1;
s1 = s;
t1 = t;
r = r0.mod(r1);
r0 = r1;
r1 = r;
}
// 找到相应的 s和t 后 , 返回结果
BigInteger as = a.multiply(s);
BigInteger bt = b.multiply(t);
res.put("gcd",as.add(bt));
res.put("s",s);
res.put("t",t);
return res;
}
/**
* 求 a 在 模n 下的乘法逆元(扩展的欧几里得算法)
* 这里 a 和 n 都是 BigInteger 类型的
*
* @param a 求a的乘法逆元
* @param n 模数
* @return 存在乘法逆元时,返回 a 在 模n 下的乘法逆元。否则返回 -1
*/
public static BigInteger getMultiplicativeInverse(BigInteger a,BigInteger n){
Map<String,BigInteger> map = gcdExtend(a,n);
if(map.get("gcd")
.compareTo(BigInteger.ONE) == 0){
BigInteger s = map.get("s");
while(s.compareTo(BigInteger.ZERO) == -1){
s = s.add(n);
}
while(s.compareTo(n) == 1){
s = s.subtract(n);
}
return s;
}
else{
System.out.println("getMultiplicativeInverse()提示您: " + a + "在模" + n + "下没有乘法逆元!");
return new BigInteger("-1");
}
}
/**
* 返回int型数据的位数
*
* @param number 数据
* @return 数据的位数
*/
public static int bitOfInt(int number){
if(number < 0){
number = -number;
}
int[] sizeTable = {9,99,999,9999,99999,999999,9999999,99999999,999999999,Integer.MAX_VALUE};
int i = 0;
while(number > sizeTable[i++])
;
return i;
}
/**
* Miller-Rabin素性判定
* 算法思路
* 设n是一个奇数,n-1 = 2^t * m,其中m是一个奇数。要测试n是否为素数,
* ① 先随机选择一个介于 [2,n-2]区间内的整数a,如果a^m ≠ 1 且
* ② 对所有的满足 r ∈ [0,k-1] 的整数有,a^(2^r * m) ≠ -1 , 则有n一定是合数。否则,n有很大可能是素数。
* (目前可以在数学上证明,Miller-Rabin将合数判定为素数的概率小于1/4,而事实上经验告诉我们,概率比1/4小得多)
* <p>
* 时间复杂度: T(n) = O(log n)
*
* @param n 待判定数字
* @param k 迭代的轮数,迭代k次后,将合数判定为素数的概率为 1/4^k
* @return 是素数返回true,是合数返回false。注意返回false一定是合数,返回true,有1/4^k 的概率是合数
*/
public static boolean isPrime_Miller_Rabin(BigInteger n,int k){
if(n.compareTo(BigInteger.ONE) <= 0){
return false;
}
// 特殊情况,n为2或3时,MyTools.random返回为null
if(n.compareTo(new BigInteger("2")) == 0 || n.compareTo(new BigInteger("3")) == 0){
return true;
}
// Miller-Rabin素性判定
BigInteger two = new BigInteger("2");
BigInteger m = n.subtract(BigInteger.ONE);
int t = 0;
while(m.mod(two) == BigInteger.ZERO){
m = m.divide(two);
t++;
}
for(int i = 0; i < k; i++){
BigInteger a = random(two,n.subtract(two));
if(BigIntegerPM(a,m,n)
.compareTo(BigInteger.ONE) != 0){
int r;
for(r = 0; r <= t - 1; r++){
if(BigIntegerPM(a,two.pow(r)
.multiply(m),n)
.subtract(n)
.compareTo(new BigInteger("-1")) == 0){
break;
}
}
if(r == t){
return false;
}
}
}
return true;
}
// 获得Miller-Rabin素数检测的检测次数
private static int getTryTime(BigInteger n){
//copy自jdk源码, n的bit数越多, 需要的Miller-Rabin检测次数就越少
int sizeInBits = n.bitLength();
int tryTime = 0;
if(sizeInBits < 100){
tryTime = 50;
}
else if(sizeInBits < 256){
tryTime = 27;
}
else if(sizeInBits < 512){
tryTime = 15;
}
else if(sizeInBits < 768){
tryTime = 8;
}
else if(sizeInBits < 1024){
tryTime = 4;
}
else{
tryTime = 2;
}
return tryTime;
}
/**
* 判断一个数是否是素数
* 当 num 较小时,使用查询素数表和查看num是不是小素数的倍数的方法对num进行素性判定
* 当 使用上述方法无法判断 num 的素性时,采用 Miller-Rabin进行k轮素性判定,其中k的值根据num的大小不同而动态求出。
*
* @param num 待判断的数
* @return 素数返回true,合数返回false
*/
public static boolean isPrime(BigInteger num){
// # 创建小素数的列表,可以大幅加快速度
int[] small_primes = {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997};
if(num.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) < 0){
long n = num.longValue();
// # 如果是小素数,那么直接返回true
for(int i = 0; i < small_primes.length; i++){
if(n == small_primes[i]){
return true;
}
}
// #如果大数是这些小素数的倍数,那么就是合数,返回false
for(int i = 0; i < small_primes.length; i++){
if(n % small_primes[i] == 0){
return false;
}
}
}
// #如果这样没有分辨出来,就一定是大整数,那么就调用rabin算法
return isPrime_Miller_Rabin(num,getTryTime(num));
}
/**
* 牛顿下降法对大整数开根号
* 由于开根号结果不一定是整数,这里的结果使用的直接抹掉小数部分,效果即为 floor(根号F7)
*
* @param F7 待开放的整数
* @return 对F7开放后,结果的整数部分
*/
public static BigInteger sqrt(BigInteger F7){
// 牛顿法求解平方根, 求解a的平方根
// x为a的平方根,x的初始值为1, 按x = (x+a/x)/2迭代, 误差为error,事实上,当error < 1时即可求出x平方根的整数部分
BigDecimal x = BigDecimal.ONE;
BigDecimal a = new BigDecimal(F7.toString());
BigDecimal eps = new BigDecimal("1");
final BigDecimal error = new BigDecimal("1E-10");
int scale = 100;
// 进入循环
while(eps.compareTo(error) > 0){ // eps > error是执行循环
x = x.add(a.divide(x,scale,BigDecimal.ROUND_HALF_UP))
.divide(new BigDecimal("2.0"),scale,BigDecimal.ROUND_HALF_UP);
eps = x.multiply(x)
.subtract(a)
.abs();
}
BigInteger sqrt = x.toBigInteger(); // 求平方根的整数部分
return sqrt;
}
// --------------------------------------------------- 加解密函数 -------------------------------------------------------------------
/**
* ElGamal加密
*
* @param m 密文
* @param p 随机选择的大素数
* @param alpha p的一个本原根
* @param beta 公钥
* @return 返回加密后的密文
*/
private static Map<String,BigInteger> encryptCore(BigInteger m,BigInteger p,BigInteger alpha,BigInteger beta){
HashMap<String,BigInteger> res = new HashMap<>();
BigInteger two = new BigInteger("2");
// 1. 产生随机数k
BigInteger gcd, k;
while(true){
k = random(two,p.subtract(two));
gcd = gcdExtend(k,p.subtract(BigInteger.ONE))
.get("gcd");
if(gcd.equals(BigInteger.ONE)){
break;
}
}
// BigInteger k = new BigInteger("853");// 书上的例子选用的随机数
BigInteger r = BigIntegerPM(alpha,k,p);
BigInteger t = (BigIntegerPM(beta,k,p)).multiply(m)
.mod(p);
res.put("r",r);
res.put("t",t);
return res;
}
/**
* ElGamal解密
*
* @param r 密文
* @param t 密文
* @param x 私钥
* @param p 大素数p(公钥)
* @return 返回解密后的明文
*/
private static BigInteger decryptCore(BigInteger r,BigInteger t,BigInteger x,BigInteger p){
BigInteger m;
BigInteger r_ = getMultiplicativeInverse(r,p);
m = (t.multiply(BigIntegerPM(r_,x,p))).mod(p);
return m;
}
// --------------------------------------------------- DSA签名 -------------------------------------------------------------------
// --------------------------------------------------- 加密解密函数 -------------------------------------------------------------------
/**
* 加密字符串
* 可以将字符换转化为byte数组。将byte数组的数字划为三位数,不够三位的,首位补 9(这样可以恢复) ,将byte拼接为字符串,转化为BigInteger
* 随机生成一个数k,且k一与n-1互素
* 计算 r = alpha^k (mod p) , t = beta^k * m
* 密文为 c = (r,t)
* <p>
* 注意:
* - 这里的字符串长度不能太长,必须保证字符串转化为的byte数组组成的BigInteger 小于 p , 否则无法解密
*
* @param plaintext 明文字符串
* @param p 公钥:模数
* @param alpha 公钥:本原根
* @param beta 公钥
* @return 加密后的字符串,r和t之间使用 "\n" 隔开,以便于解密时解析
*/
private static String encrypt(String plaintext,String p,String alpha,String beta){
byte[] bytes = plaintext.getBytes();
int[] ints = new int[bytes.length];
int i;
for(i = 0; i < bytes.length; i++){
ints[i] = bytes[i];
}
StringBuilder builder = new StringBuilder();
// 目前只能小于 1024
i = 0;
while(i < ints.length){
if(bitOfInt(ints[i]) == 2){
ints[i] += 900;
}
builder.append(ints[i++]);
}
// System.out.println("plain = " + builder.toString());
BigInteger m = new BigInteger(builder.toString());
Map<String,BigInteger> cipher = encryptCore(m,new BigInteger(p),new BigInteger(alpha),new BigInteger(beta));
return cipher.get("r")
.toString() + "\n" + cipher.get("t")
.toString();
}
/**
* ElGamal解密
* 先解析加密后的字符串,分离 r,t 然后求 p = t*r^(-x) (mod p)恢复明文的BigInteger。
* 然后将解析p,即每三位作为一个十进制的数字,如果第一位为9,则将其去掉,否则这三位不做改变。
* 将上述数字转化为字符,然后拼接成字符串即可恢复明文
*
* @param ciphertext 密文
* @param x 私钥 p
* @param p 私钥 q
* @return 解密后的明文字符串
*/
private static String decrypt(String ciphertext,String x,String p){
StringBuilder res = new StringBuilder();
StringBuilder t = new StringBuilder();
StringBuilder r = new StringBuilder();
int i;
for(i = 0; i < ciphertext.length(); i++){
if(ciphertext.charAt(i) == '\n'){
for(int j = i + 1; j < ciphertext.length(); j++){
t.append(ciphertext.charAt(j));
}
break;
}
r.append(ciphertext.charAt(i));
}
BigInteger rr = new BigInteger(r.toString());
BigInteger tt = new BigInteger(t.toString());
BigInteger xx = new BigInteger(x);
BigInteger pp = new BigInteger(p);
String plaintext = decryptCore(rr,tt,xx,pp).toString();
for(i = 0; i < plaintext.length(); i = i + 3){
StringBuilder builder = new StringBuilder();
builder.append(plaintext.charAt(i));
builder.append(plaintext.charAt(i + 1));
builder.append(plaintext.charAt(i + 2));
int integer = new Integer(builder.toString());
if(integer / 100 == 9){
integer -= 900;
}
char temp = (char) integer;
res.append(temp);
}
return res.toString();
}
// --------------------------------------------------- 测试函数 -------------------------------------------------------------------
private void testElGamalCore(){
System.out.println("----------------------- 测试ElGamal算法的核心加密函数 ----------------------------");
BigInteger m = new BigInteger("2199");
BigInteger p = new BigInteger("2579");
BigInteger alpha = new BigInteger("2");
BigInteger x = new BigInteger("765");
BigInteger beta = BigIntegerPM(alpha,x,p);
Map<String,BigInteger> cipher = encryptCore(m,p,alpha,beta);
System.out.println(cipher);
BigInteger plaintext = decryptCore(cipher.get("r"),cipher.get("t"),x,p);
System.out.println(plaintext);
}
private void testElgamal(){
System.out.println("----------------------- 测试ElGamal算法加密字符串 ----------------------------");
String plain = "I love you!";
Map<String,BigInteger> key = getKey(35,new BigInteger("521"));
BigInteger p = key.get("p");
BigInteger alpha = key.get("alpha");
BigInteger x = key.get("x");
BigInteger beta = key.get("beta");
String encrypt = encrypt(plain,p.toString(),alpha.toString(),beta.toString());
System.out.println("encrypt = " + encrypt);
String decrypt = decrypt(encrypt,x.toString(),p.toString());
System.out.println("decrypt = " + decrypt);
}
private void testGenerateKey(){
System.out.println("----------------------- 测试密钥生成算法 ----------------------------");
BigInteger x = new BigInteger("521");
Map<String,BigInteger> key = getKey(4,x);
System.out.println(key);
System.out.println("----------------------- 测试指定位数的素数生成器 ----------------------------");
for(int i = 0; i < 10; i++){
System.out.print(getPrime(4) + ",");
}
System.out.println();
}
// --------------------------------------------------- 密钥生成 -------------------------------------------------------------------
/**
* 随机大素数的生成
* 生成一个随机的 bitCount位的大素数
* 注意:可使用 BigInteger.probablePrime() 方法生成大素数,速度比这个函数速度快10倍以上
*
* @param bitCount 生成素数的在十进制下占的位数
* @return 随机的bitCount位的大素数
*/
private static BigInteger getPrime(int bitCount){
//随机生成一个n bit的大整数
BigInteger start = BigInteger.TEN.pow(bitCount - 1);
BigInteger end = BigInteger.TEN.pow(bitCount)
.subtract(BigInteger.ONE);
BigInteger init = random(start,end);
//如果n为偶数, 则加一变为奇数
if(!init.testBit(0)){
init = init.setBit(0);
}
//基于素数定理, 平均只需要不到n次搜索, 就能找到一个素数
while(!isPrime(init)){
init = init.add(BigInteger.valueOf(2));
}
return init;
}
/**
* 生成ElGamal算法中的公钥
* 1. 步骤:
* 第一步:取一个随机的素数q,并且这个素数满足p = 2q + 1也是一个素数
* 第二步:随机取一个数g(在[2,p - 2]之间),判断g^2 mod p 与 g ^q mod p是否为1;只要有一个为1,说明g不是p的原根,就重新取随机数
* <p>
* 2. 参考文献:
* https://blog.csdn.net/yibaobao2019/article/details/110429353
*
* @param bitCount 公钥中,大素数p的十进制位数
* @param x 私钥
* @return 返回一个map{ "p",大素数p; "alpha",p的一个原根; "beta",alpha^x(mod p) 其中x为私钥; "x",私钥 }
*/
private static Map<String,BigInteger> getKey(int bitCount,BigInteger x){
HashMap<String,BigInteger> res = new HashMap<>();
BigInteger p;
BigInteger two = new BigInteger("2");
while(true){
BigInteger q = getPrime(bitCount);
p = q.multiply(two)
.add(BigInteger.ONE);
if(!isPrime(p)){
continue;
}
BigInteger alpha = random(two,p.subtract(two));
if(BigIntegerPM(alpha,two,p)
.equals(BigInteger.ONE) || BigIntegerPM(alpha,q,p)
.equals(BigInteger.ONE)){
continue;
}
else{
res.put("p",p);
res.put("alpha",alpha);
res.put("beta",BigIntegerPM(alpha,x,p));
res.put("x",x);
return res;
}
}
}
// --------------------------------------------------- DSA攻击 -------------------------------------------------------------------
/**
* 大步小步算法(Shank算法, Baby-Step Giant-Step Algorithm)
* 给出ElGamal算法的公钥,求解私钥 x
* ① BabyStep 中存储的是 ( beta * alpha^r , r )
* ② GiantStep 中存储的是 ( alpha^(t*s) , t )
* 当第一个左边相同时,即 beta * alpha^r = alpha^(t*s)
* 将 beta = alpha^x 代入得到: alpha^(x+r) = alpha^(t*s)
* 即 x + r = t * s
* 所以 x = t*s - r
* <p>
* 时间复杂度 T(n) = O(n^0.5) (事实上求解大的整数幂复杂度为log n,因此T(n)实际大概为 O( n^0.5 * log n^0.5 ) )
* 空间复杂度 S(n) 大概为 O( n^0.5 * log n^0.5 )
*
* @param alpha 公钥
* @param beta 公钥
* @param p 公钥
* @return 私钥
*/
private BigInteger Shanks(BigInteger alpha,BigInteger beta,BigInteger p){
HashMap<BigInteger,BigInteger> BabyStep = new HashMap<>();
HashMap<BigInteger,BigInteger> GiantStep = new HashMap<>();
BigInteger s = sqrt(p);
// 计算大步小步的值
for(BigInteger r = BigInteger.ZERO; r.compareTo(s.subtract(BigInteger.ONE)) <= 0; r = r.add(BigInteger.ONE)){
BigInteger babyKey = beta.multiply(BigIntegerPM(alpha,r,p))
.mod(p);
BabyStep.put(babyKey,r);
BigInteger t = r.add(BigInteger.ONE);
BigInteger giantKey = BigIntegerPM(alpha,t.multiply(s),p);
GiantStep.put(giantKey,t);
}
// System.out.println(BabyStep);
// System.out.println(GiantStep);
// 找出键相同的值
for(BigInteger babyKey : BabyStep.keySet()){
if(GiantStep.containsKey(babyKey)){
BigInteger t = GiantStep.get(babyKey);
BigInteger r = BabyStep.get(babyKey);
return t.multiply(s)
.subtract(r);
}
}
return null;
}
public static void main(String[] args){
Main elGamal = new Main();
// elGamal.testElGamalCore();
elGamal.testElgamal();
// elGamal.testGenerateKey();
// System.out.println(elGamal.Shanks(new BigInteger("7190183321"),new BigInteger("1333381262"),new BigInteger("7788872843")));
}
}