代碼路徑:https://github.com/prophetss/rsa
之前分享過三種常用MD5、SHA2和AES加密算法(點這里)實現源碼,前三者分別屬於哈希加密和對稱加密,而另一種很常用的非對稱加密RSA算法實現這次分享出來。RSA算法的原理和用途大家可以網上自行搜索。雖然其算法原理很簡單,但是由於其密鑰長度很長(一般至少1024位),所以實際在其相互運算以及大質數查找會牽扯很多算法理論,因此我這里代碼實現中數學運算是直接基於GMP(The GNU Multiple Precision Arithmetic Library),我將其linux64位下編譯好的動靜態庫都已上傳,如果環境相同可以不用安裝直接使用。另外針對RSA算法內的大質數p和q以及公鑰e和私鑰d等也是有要求的(參考這里),這塊我是參考JDK內RSA算法的實現。
下面開始准備工作,首先是大質數的選取,由於大質數判斷是相當困難,所以當前對於大質數的選取都是概率選取,先看下JDK內實現(我查看的代碼版本是jdk-10.0.1)
BigInteger.java內762-769行,其中SMALL_PRIME_THRESHOLD為常量95 ,DEFAULT_PRIME_CERTAINTY為常量100,rnd入參是一個隨機數,所以是按95位為分界對應兩個不同方法,我這里只考慮長密鑰所以直接看大的
public static BigInteger probablePrime(int bitLength, Random rnd) { if (bitLength < 2) throw new ArithmeticException("bitLength < 2"); return (bitLength < SMALL_PRIME_THRESHOLD ? smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) : largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd)); }
BigInteger.java內822-841行,certainty就是之前傳入的常量100,而其結果是調用BitSieve類中的retrieve方法獲取,為了簡單些我這就跳過這個函數代碼了,里面大體是循環調用BigInteger類內primeToCertainty方法。
private static BigInteger largePrime(int bitLength, int certainty, Random rnd) { BigInteger p; p = new BigInteger(bitLength, rnd).setBit(bitLength-1); p.mag[p.mag.length-1] &= 0xfffffffe; // Use a sieve length likely to contain the next prime number int searchLen = getPrimeSearchLen(bitLength); BitSieve searchSieve = new BitSieve(p, searchLen); BigInteger candidate = searchSieve.retrieve(p, certainty, rnd); while ((candidate == null) || (candidate.bitLength() != bitLength)) { p = p.add(BigInteger.valueOf(2*searchLen)); if (p.bitLength() != bitLength) p = new BigInteger(bitLength, rnd).setBit(bitLength-1); p.mag[p.mag.length-1] &= 0xfffffffe; searchSieve = new BitSieve(p, searchLen); candidate = searchSieve.retrieve(p, certainty, rnd); } return candidate; }
BigInteger.java內934-962行,可以看到最后調用MillerRabin和LucasLehmer算法,前者中rounds為迭代輪數,而這個算法檢測一輪為質數實際為不為質數的概率為1/4(參考這里),后者也是一個偽素數檢測算法,感興趣可以參考這里,所以可以看出JDK中RSA算法獲取的是概率質數,其概率跟其位數相關。
boolean primeToCertainty(int certainty, Random random) { int rounds = 0; int n = (Math.min(certainty, Integer.MAX_VALUE-1)+1)/2; // The relationship between the certainty and the number of rounds // we perform is given in the draft standard ANSI X9.80, "PRIME // NUMBER GENERATION, PRIMALITY TESTING, AND PRIMALITY CERTIFICATES". int sizeInBits = this.bitLength(); if (sizeInBits < 100) { rounds = 50; rounds = n < rounds ? n : rounds; return passesMillerRabin(rounds, random); } if (sizeInBits < 256) { rounds = 27; } else if (sizeInBits < 512) { rounds = 15; } else if (sizeInBits < 768) { rounds = 8; } else if (sizeInBits < 1024) { rounds = 4; } else { rounds = 2; } rounds = n < rounds ? n : rounds; return passesMillerRabin(rounds, random) && passesLucasLehmer(); }
通過查詢GMP庫中對應函數,其文檔有兩個相關函數int mpz_probab_prime_p (const mpz t n, int reps)和void mpz_nextprime (mpz t rop, const mpz t op)前者是概率判斷n(入參)是否為質數,其概率值為4的reps(入參)次方,后者是直接給出一個大於op(入參)的概率質數rop(出參)。所以前者就是對應JDK中的primeToCertainty方法,后者對應probablePrime方法,為了簡單我這里使用mpz_nextprime 函數直接獲取,這個函數沒有給出是質數的概率,文檔也沒有說明只是說結果是合數的概率很小(原文:“This function uses a probabilistic algorithm to identify primes. For practical purposes it’s adequate, the chance of a composite passing will be extremely small.”)。我查了下源碼,這個概率可能是4的-25次方(最后調用了25輪MillerRabin算法)
有了獲取大質數的獲取方法剩下的難點就是查詢GMP文檔學習英語了..JDK內的RSA加密解密是基於CRT的,我不太清楚這塊原理所以將這部分去掉了,代碼封裝了字符串加解密(每次加密數據大小不可超過密鑰大小),下面展示下在我機子上的運行結果(2048bit密鑰):