金融系统中的RSA算法(二)
前言
在上一章金融系统中的RSA算法(一)中,我们了解了对称加密与非对称加密的特点,也了解了如何制作RSA的两把秘钥,现在我们就来尝试实现一个简易版的RSA加解密库吧。
RSA算法的实现过程
“如何制作两把钥匙”
我们来回忆一下上一章所说的制作钥匙的步骤:
-
“挑选两张上好的铁皮”:挑两个不重复的大素数
-
“根据调好的铁皮随便做一个加密钥匙”:在0~φ(n)范围内随机选取一个不为0且与φ(n)互质的数作为加密钥匙
-
“根据加密钥匙精心设计一把解密钥匙”:求取加密钥匙在φ(n)下的逆元
-
“用两个精美钥匙盒将加密钥匙、解密钥匙以及给他们配的同款钥匙扣打包到一起”:将加密钥匙与n打包到加密钥匙盒,把解密钥匙和n打包到解密钥匙盒
钥匙准备好了,接下来就是如何使用这两把钥匙,我们可以使用快速幂算法使用我们的加密钥匙和解密钥匙分别进行加解密,快速幂算法详见:快速幂_百度百科
RSA算法简易实现版本
注意:由于我们用于加解密的素数都是比较大的,为了避免溢出,运算时都采用bigint类型进行运算
class RSA {
/**
* 素数表
*/
public readonly primes: number[];
/**
* 初始化素数表
* 素数筛算法
* @param len
*/
private initPrime(len: number) {
// primes数组的第0位存储的就是我们小于或等于len的所有质数的数量
for (let i = 2; i <= len; i++) {
// 标记质数
if (!this.primes[i]) {
this.primes[++this.primes[0]] = i;
}
for (let j = 1; j <= this.primes[0]; j++) {
// 如果超界则跳出
if (i * this.primes[j] > len) break;
// 将没超界的部分标记为合数
this.primes[i * this.primes[j]] = 1;
// 将倍数也跳过
if (i % this.primes[j] === 0) break;
}
}
}
// 用于生成公钥的两个不同素数索引,我们要确保这个素数尽可能大
private pIdx: number;
private qIdx: number;
// 欧拉函数φ(n)的值
private phiN: number;
// 加密钥匙
private e: number;
// 解密钥匙
private d: bigint;
// 加密秘钥组
private encodeKey: { e: bigint; n: bigint } = {
e: 0n,
n: 0n,
};
// 解密密秘钥组
private decodeKey: { d: bigint; n: bigint } = {
d: 0n,
n: 0n,
};
constructor(len: number) {
this.primes = new Array(len);
this.primes.fill(0);
this.initPrime(len);
// 随机选择素数表中最后100个素数
do {
this.pIdx = Math.floor(Math.random() * 100) + this.primes[0] - 99;
this.qIdx = Math.floor(Math.random() * 100) + this.primes[0] - 99;
} while (this.pIdx === this.qIdx);
// 获取两个大素数
const p = this.primes[this.pIdx], q = this.primes[this.qIdx];
// 求取φ(n) = (p - 1) * (q - 1);
this.phiN = (p - 1) * (q - 1);
// 生成一个不为 0 且与 phiN 互质的数 e,即加密钥匙
do {
this.e = Math.floor(Math.random() * this.phiN);
} while (
this.e === 0 ||
this.gcd(BigInt(this.e), BigInt(this.phiN)) !== 1n
);
// n 将会与秘钥一起打包进秘钥组当中,用于辅助秘钥的加解密
const n = BigInt(p * q);
// 用于辅助求取 e 在 φ(n) 下的逆元
const info: { x: bigint; y: bigint } = {
x: 0n,
y: 0n,
};
// 解密钥匙
this.getInv(BigInt(this.e), BigInt(this.phiN), info);
// 为了防止出现负数,做如下处理生成解密秘钥
this.d =
((info.x % BigInt(this.phiN)) + BigInt(this.phiN)) % BigInt(this.phiN);
// 打包加密秘钥
this.encodeKey = {
e: BigInt(this.e),
n,
};
// 打包解密秘钥
this.decodeKey = {
d: this.d,
n,
};
// 必须确保:this.e * this.d % φ(n) = 1,加密秘钥和解密秘钥才是真正合法可用的
if ((BigInt(this.e) * this.d) % BigInt(this.phiN) === 1n) {
console.log(`encode: (${this.encodeKey.e},${this.encodeKey.n})`);
console.log(`decode: (${this.decodeKey.d},${this.decodeKey.n})`);
}
// console.log(this.quickMod(35n, 1025735837n, 2434387639n));
}
/**
* 使用快速幂算法根据明(密)文、加(解)密秘钥、n 进行加(解)密
* @param a
* @param b
* @param c
* @returns
*/
private quickMod(a: bigint, b: bigint, c: bigint) {
let ans = 1n,
temp = a;
while (b) {
// b是奇数的情况
if (b & 1n) ans = (ans * temp) % c;
temp = (temp * temp) % c;
// b不断除2向下取整
b >>= 1n;
}
return ans;
}
/**
* 加密方法
* @param s
* @returns
*/
public encode(s: number) {
return Number(this.quickMod(BigInt(s), this.encodeKey.e, this.encodeKey.n));
}
/**
* 解密方法
* @param s
* @returns
*/
public decode(s: number) {
return Number(this.quickMod(BigInt(s), this.decodeKey.d, this.decodeKey.n));
}
/**
* 求取两个数的最大公约数
* @param a
* @param b
* @returns
*/
private gcd(a: bigint, b: bigint): bigint {
if (b) return this.gcd(b, a % b);
return a;
}
/**
* 求取 y 在 x 下的逆元
* (关于逆元的知识,请自行了解狄利克雷卷积和莫比乌斯反演相关知识)
* @param x
* @param y
* @returns
*/
private getInv(a: bigint, b: bigint, info: { x: bigint; y: bigint }): bigint {
if (b == 0n) {
(info.x = 1n), (info.y = 0n);
return a;
}
[info.x, info.y] = [info.y, info.x];
let r = this.getInv(b, a % b, info);
[info.x, info.y] = [info.y, info.x];
info.y -= BigInt(a / b) * info.x;
return r;
}
}
// 选择一百万以内的素数作为做钥匙用的铁皮库,从中随机选取两个大素数
const rsa = new RSA(1000000);
let string = "hello, my name is kiner";
let str1 = [], str2 = [];
for(let i = 0;i<string.length;i++) {
const m = rsa.encode(string[i].charCodeAt(0));
str1.push(m);
const d = rsa.decode(m);
str2.push(d);
console.log(`加密 -> 明文:${string[i]};密文:${m}`);
console.log(`解密 -> 密文:${m};明文:${String.fromCharCode(d)}`);
}
console.log(`原文:${string}`);
console.log(`密文:${str1.join(',')}`);
console.log(`明文:${str2.map(item => String.fromCharCode(item)).join('')}`);
// encode: (384935524753,998320560439)
// decode: (709643850937,998320560439)
// 加密 -> 明文:h;密文:444495150174
// 解密 -> 密文:444495150174;明文:h
// 加密 -> 明文:e;密文:727821516482
// 解密 -> 密文:727821516482;明文:e
// 加密 -> 明文:l;密文:147868591221
// 解密 -> 密文:147868591221;明文:l
// 加密 -> 明文:l;密文:147868591221
// 解密 -> 密文:147868591221;明文:l
// 加密 -> 明文:o;密文:881860730129
// 解密 -> 密文:881860730129;明文:o
// 加密 -> 明文:,;密文:820810446678
// 解密 -> 密文:820810446678;明文:,
// 加密 -> 明文: ;密文:643479203849
// 解密 -> 密文:643479203849;明文:
// 加密 -> 明文:m;密文:308313604166
// 解密 -> 密文:308313604166;明文:m
// 加密 -> 明文:y;密文:513496946981
// 解密 -> 密文:513496946981;明文:y
// 加密 -> 明文: ;密文:643479203849
// 解密 -> 密文:643479203849;明文:
// 加密 -> 明文:n;密文:572273915
// 解密 -> 密文:572273915;明文:n
// 加密 -> 明文:a;密文:386721002411
// 解密 -> 密文:386721002411;明文:a
// 加密 -> 明文:m;密文:308313604166
// 解密 -> 密文:308313604166;明文:m
// 加密 -> 明文:e;密文:727821516482
// 解密 -> 密文:727821516482;明文:e
// 加密 -> 明文: ;密文:643479203849
// 解密 -> 密文:643479203849;明文:
// 加密 -> 明文:i;密文:892876013331
// 解密 -> 密文:892876013331;明文:i
// 加密 -> 明文:s;密文:214672362698
// 解密 -> 密文:214672362698;明文:s
// 加密 -> 明文: ;密文:643479203849
// 解密 -> 密文:643479203849;明文:
// 加密 -> 明文:k;密文:544794144716
// 解密 -> 密文:544794144716;明文:k
// 加密 -> 明文:i;密文:892876013331
// 解密 -> 密文:892876013331;明文:i
// 加密 -> 明文:n;密文:572273915
// 解密 -> 密文:572273915;明文:n
// 加密 -> 明文:e;密文:727821516482
// 解密 -> 密文:727821516482;明文:e
// 加密 -> 明文:r;密文:5994886524
// 解密 -> 密文:5994886524;明文:r
// 原文:hello, my name is kiner
// 密文:444495150174,727821516482,147868591221,147868591221,881860730129,820810446678,643479203849,308313604166,513496946981,643479203849,572273915,386721002411,308313604166,727821516482,643479203849,892876013331,214672362698,643479203849,544794144716,892876013331,572273915,727821516482,5994886524
// 明文:hello, my name is kiner