从质因数分解到RSA算法:C++实现非对称加密原理

📅 2026/7/16 12:52:42
从质因数分解到RSA算法:C++实现非对称加密原理
1. 项目概述为什么我们要从质因数分解开始聊RSA如果你对密码学感兴趣或者想理解我们每天使用的HTTPS、SSH、数字签名背后到底是怎么一回事那么“RSA加密算法”绝对是一个绕不开的名字。但很多教程一上来就扔给你一堆公式选两个大素数p和q计算np*q再算个欧拉函数找个e求个d……一套操作下来你可能记住了步骤但心里总有个疑问为什么非得是质因数分解为什么这样设计就安全了这正是我想在这篇分享里和你一起弄明白的。我不打算只给你一个能跑的C代码那太没意思了。我想做的是手把手带你从最基础的“质因数分解”这个小学数学概念出发一步步推导、理解并最终用C实现一个完整的、可运行的RSA加密解密过程。你会发现那些看似高深的数学其核心思想其实非常优雅和直观。当你真正理解了“为什么”写代码就变成了水到渠成的事情而且你会对代码里的每一个参数、每一步计算都了如指掌。简单来说RSA的安全性基石就在于“将一个大整数分解成两个质因数的乘积是极其困难的”这一事实。我们这篇文章就是要亲手用代码把这个“困难”的过程加密和理论上只有掌握私钥才能轻松完成的“简单”过程解密都实现一遍。无论你是正在学习密码学的学生还是对底层原理好奇的开发者亦或是想为你的C项目增加一些密码学理解的爱好者这篇内容都会给你带来实实在在的收获。2. 核心数学原理拆解从欧拉定理到非对称密钥在动手写代码之前我们必须把地基打牢。RSA的整个大厦建立在数论的几块基石之上理解它们之间的逻辑关系至关重要。2.1 质因数分解一切安全性的源头让我们从一个简单的游戏开始。我给你一个数字比如21你能很快告诉我它是哪两个质数相乘得到的吗3和7很简单。那3233呢可能需要花点时间但也能算出是53和61。但如果我给你一个300位十进制的数字呢比如RSA-1024这个级别的数以目前人类最强的计算能力包括超级计算机和量子计算机以外的所有算法想要分解它也需要耗费天文数字的时间可能比宇宙的年龄还长。这就是RSA安全性的核心假设大整数的质因数分解是一个在经典计算机上“难解”的问题。这里“难解”是计算复杂性理论中的术语意味着没有已知的多项式时间算法。在工程上我们选择足够大的素数目前推荐2048位或更长使得分解在可预见的未来是不现实的。注意这里说的是“经典计算机”。量子计算机上的Shor算法能在多项式时间内分解大整数这正是当前密码学界研究“后量子密码”的原因。但在我们讨论的经典计算范畴内这个假设是牢固的。在RSA中我们精心构造一个合数n p * q其中p和q是两个非常大的、随机生成的素数。我们把n公开但死死守住p和q。攻击者只知道n想要得到p和q就必须面对那个“难解”的质因数分解问题。2.2 欧拉定理与欧拉函数构建数学桥梁知道了n由p和q构成下一步我们需要一个数学工具能在模n运算的世界里构造出一种可逆的变换。这就是欧拉定理。欧拉函数 φ(n)它表示小于n的正整数中与n互质最大公约数为1的数的个数。对于质数pφ(p) p - 1因为1到p-1都跟p互质。对于一个由两个不同质数相乘得到的数n p * q有一个非常漂亮的性质φ(n) φ(p) * φ(q) (p-1) * (q-1)。这个性质是关键中的关键因为知道p和q的人可以轻松算出φ(n)而只知道n的人却很难算出φ(n)这又回到了质因数分解难题。欧拉定理如果正整数a和n互质那么a^φ(n) ≡ 1 (mod n)。这个定理是费马小定理的推广。它告诉我们在模n的世界里a的φ(n)次幂会“回归”到1。2.3 密钥对的生成巧妙的逆元构造现在我们把质因数分解和欧拉定理结合起来看看RSA的密钥是如何“设计”出来的而不是“发明”出来的。选择两个大素数p和q计算n p * q。n就是模数会出现在公钥和私钥中是公开的。计算欧拉函数 φ(n) (p-1)*(q-1)。这个值必须绝对保密因为它直接关联着私钥。选择一个公钥指数e。e是一个整数需要满足两个条件1 e φ(n)且 e 与 φ(n) 互质即 gcd(e, φ(n)) 1。通常选择65537 (0x10001)因为它是一个素数二进制表示中只有两个1计算效率高且安全性经过充分验证。计算私钥指数d。d是e关于模φ(n)的模逆元。也就是说d需要满足e * d ≡ 1 (mod φ(n))。计算d需要使用扩展欧几里得算法。这里的精妙之处在于由于我们选择了e与φ(n)互质根据数论知识e在模φ(n)下的模逆元d一定存在且唯一。知道p和q从而知道φ(n)的人可以轻松算出d。而只知道公开的n和e的人因为无法算出φ(n)也就无法算出d。2.4 加密与解密的互逆性证明现在公钥是(e, n)私钥是(d, n)。加密过程是对明文m需要先转换为一个小于n的整数计算c ≡ m^e (mod n)得到密文c。解密过程是对密文c计算m ≡ c^d (mod n)。为什么解密后就能恢复明文让我们推导一下c^d ≡ (m^e)^d ≡ m^(e*d) (mod n)根据密钥生成过程我们有e*d ≡ 1 (mod φ(n))所以存在整数k使得e*d k*φ(n) 1。 因此m^(e*d) ≡ m^(k*φ(n) 1) ≡ (m^φ(n))^k * m (mod n)这里需要分情况讨论如果m与n互质直接由欧拉定理m^φ(n) ≡ 1 (mod n)可得上式≡ 1^k * m ≡ m (mod n)。如果m与n不互质因为np*q所以m是p或q的倍数情况稍复杂但利用中国剩余定理同样可以证明m^(e*d) ≡ m (mod n)依然成立。这也是为什么要求m n且实践中会对信息进行填充使其几乎总是与n互质。至此我们完成了逻辑闭环基于质因数分解的困难性保护了φ(n)的秘密基于φ(n)的秘密保护了私钥d基于欧拉定理保证了使用d可以对e加密的结果进行正确解密。3. C实现核心模块从理论到代码理解了数学原理我们就可以用C来一步步实现它。我们会构建几个核心函数模块。为了清晰和教学目的我们暂时不处理超大整数比如2048位而是使用C的long long类型通常为64位来演示原理。在实际应用中你需要使用像GMP、OpenSSL BN这样的高精度数学库。3.1 基础数论工具函数实现首先我们需要实现几个关键的数论函数作为工具。#include iostream #include numeric // for std::gcd #include tuple // for std::tuple #include random // for prime generation #include cmath // 1. 模幂运算 (Modular Exponentiation): 快速计算 (base^exp) % mod // 这是RSA加密解密的核心操作必须高效。使用“平方-乘”算法。 long long mod_pow(long long base, long long exp, long long mod) { long long result 1; base base % mod; // 确保base小于mod while (exp 0) { // 如果当前指数位为1则乘上当前的base if (exp 1) { result (result * base) % mod; } // 指数右移一位底数平方 exp 1; base (base * base) % mod; } return result; } // 2. 扩展欧几里得算法 (Extended Euclidean Algorithm) // 用于求解 ax by gcd(a, b) 的一组整数解 (x, y)。 // 在RSA中我们用它来求模逆元e * d ≡ 1 (mod φ(n)) 中的 d。 std::tuplelong long, long long, long long extended_gcd(long long a, long long b) { if (b 0) { return {a, 1, 0}; // gcd, x, y } auto [gcd_val, x1, y1] extended_gcd(b, a % b); long long x y1; long long y x1 - (a / b) * y1; return {gcd_val, x, y}; } // 3. 求模逆元 (Modular Multiplicative Inverse) // 给定 a 和 m求 x 使得 a * x ≡ 1 (mod m)。 // 逆元存在的条件是 gcd(a, m) 1。 long long mod_inverse(long long a, long long m) { auto [gcd_val, x, y] extended_gcd(a, m); if (gcd_val ! 1) { throw std::runtime_error(模逆元不存在因为a和m不互质。); } // 确保返回正数 return (x % m m) % m; } // 4. 简单的素数检测 (Miller-Rabin 简化版适用于教学) // 实际应用请使用成熟的Miller-Rabin或其他概率性测试。 bool is_prime_simple(long long n) { if (n 1) return false; if (n 3) return true; if (n % 2 0 || n % 3 0) return false; // 检查从5开始的6k±1形式的因子 for (long long i 5; i * i n; i 6) { if (n % i 0 || n % (i 2) 0) return false; } return true; } // 5. 生成一个指定位数范围内的随机素数简易版 long long generate_prime(long long min, long long max) { std::random_device rd; std::mt19937_64 gen(rd()); std::uniform_int_distributionlong long dis(min, max); long long candidate; do { candidate dis(gen); // 确保是奇数 if (candidate % 2 0) candidate; } while (!is_prime_simple(candidate)); return candidate; }实操心得mod_pow函数是性能关键。直接使用pow(base, exp)再取模是绝对不可行的因为中间结果会巨大无比导致溢出。extended_gcd的递归实现清晰体现了数学原理但在处理极大整数时迭代实现可能栈空间更安全。我们的素数生成函数is_prime_simple是确定性的且只适用于小数字教学千万不能用于真实密钥生成真实场景必须使用概率性素数测试如Miller-Rabin并配合足够的迭代次数。3.2 RSA密钥生成器实现有了工具函数我们可以实现密钥生成的核心逻辑。#include iostream #include tuple struct RSAKeyPair { long long n; // 模数 (公共部分) long long e; // 公钥指数 long long d; // 私钥指数 // 以下字段仅用于演示和验证实际私钥只保存 (d, n)p和q应立即销毁 long long p; // 素数p (绝密!) long long q; // 素数q (绝密!) long long phi; // φ(n) (绝密!) }; RSAKeyPair generate_rsa_keys(long long prime_min 1000, long long prime_max 10000) { RSAKeyPair keys; // 1. 选择两个大素数 p 和 q keys.p generate_prime(prime_min, prime_max); do { keys.q generate_prime(prime_min, prime_max); } while (keys.q keys.p); // 确保p和q不同 // 2. 计算 n p * q keys.n keys.p * keys.q; // 3. 计算欧拉函数 φ(n) (p-1)*(q-1) keys.phi (keys.p - 1) * (keys.q - 1); // 4. 选择公钥指数 e通常为65537这里为演示选择一个较小的与φ(n)互质的数 keys.e 65537; // 常见选择 // 如果65537与φ(n)不互质概率极低则寻找一个小的互质数 if (std::gcd(keys.e, keys.phi) ! 1) { keys.e 3; while (std::gcd(keys.e, keys.phi) ! 1) { keys.e 2; // 尝试下一个奇数 } } // 5. 计算私钥指数 d即 e 关于模 φ(n) 的逆元 keys.d mod_inverse(keys.e, keys.phi); std::cout [密钥生成日志]\n; std::cout 素数 p: keys.p \n; std::cout 素数 q: keys.q \n; std::cout 模数 n: keys.n \n; std::cout φ(n): keys.phi \n; std::cout 公钥 e: keys.e \n; std::cout 私钥 d: keys.d \n; std::cout 验证 e*d mod φ(n) (keys.e * keys.d) % keys.phi (应为1)\n; return keys; }3.3 加密与解密函数实现加密和解密本质上就是模幂运算。// RSA加密使用公钥 (e, n) 加密明文整数 m (要求 m n) long long rsa_encrypt(long long m, long long e, long long n) { if (m n) { throw std::runtime_error(明文 m 必须小于模数 n。); } return mod_pow(m, e, n); } // RSA解密使用私钥 (d, n) 解密密文整数 c long long rsa_decrypt(long long c, long long d, long long n) { return mod_pow(c, d, n); }3.4 完整演示程序让我们把这些模块组合起来运行一个完整的例子。int main() { std::cout RSA加密算法C手把手实现 \n\n; // 第1步生成RSA密钥对 std::cout 1. 正在生成RSA密钥对...\n; RSAKeyPair my_keys generate_rsa_keys(500, 3000); // 生成中等大小的素数用于演示 std::cout 公钥 (e, n): ( my_keys.e , my_keys.n )\n; std::cout 私钥 (d, n): ( my_keys.d , my_keys.n )\n\n; // 第2步准备明文这里用一个整数代表信息实际应用需对数据进行填充和分组 long long plaintext 123456; // 示例明文 std::cout 2. 明文 (整数表示): plaintext \n; if (plaintext my_keys.n) { std::cerr 错误明文必须小于模数 n。请使用更小的明文或更大的密钥。\n; return 1; } // 第3步使用公钥加密 std::cout 3. 使用公钥 (e, n) 进行加密...\n; long long ciphertext rsa_encrypt(plaintext, my_keys.e, my_keys.n); std::cout 密文: ciphertext \n\n; // 第4步使用私钥解密 std::cout 4. 使用私钥 (d, n) 进行解密...\n; long long decrypted_text rsa_decrypt(ciphertext, my_keys.d, my_keys.n); std::cout 解密后的明文: decrypted_text \n\n; // 第5步验证 std::cout 5. 验证结果: ; if (plaintext decrypted_text) { std::cout 成功明文与解密结果一致。\n; } else { std::cout 失败解密结果错误。\n; } // 第6步安全性直观演示尝试暴力分解n std::cout \n6. 安全性直观演示尝试对公开的模数 n my_keys.n 进行质因数分解...\n; std::cout (已知 p my_keys.p , q my_keys.q 但攻击者不知道)\n; bool found false; // 从2开始尝试到sqrt(n)这是一个非常低效但直观的方法 for (long long i 2; i * i my_keys.n; i) { if (my_keys.n % i 0) { std::cout 分解成功找到因子: i 和 my_keys.n / i \n; std::cout 这证明了如果n太小分解是容易的。真实RSA中n极大此循环不可能完成。\n; found true; break; } } if (!found) { std::cout (在演示范围内未成功分解体现了大数分解的困难性。实际n更大困难性呈指数级增长)\n; } return 0; }运行这个程序你会看到类似以下的输出具体数字因随机生成而异 RSA加密算法C手把手实现 1. 正在生成RSA密钥对... [密钥生成日志] 素数 p: 1451 素数 q: 2383 模数 n: 3457733 φ(n): 3453900 公钥 e: 65537 私钥 d: 267833 验证 e*d mod φ(n) 1 (应为1) 公钥 (e, n): (65537, 3457733) 私钥 (d, n): (267833, 3457733) 2. 明文 (整数表示): 123456 3. 使用公钥 (e, n) 进行加密... 密文: 2087632 4. 使用私钥 (d, n) 进行解密... 解密后的明文: 123456 5. 验证结果: 成功明文与解密结果一致。 6. 安全性直观演示尝试对公开的模数 n 3457733 进行质因数分解... (已知 p1451, q2383但攻击者不知道) 分解成功找到因子: 1451 和 2383 这证明了如果n太小分解是容易的。真实RSA中n极大此循环不可能完成。这个演示清晰地展示了全过程密钥生成、加密、解密以及一个关键点——当n较小时我们演示的暴力分解可以成功这反衬出在真实场景中使用极大n的必要性。4. 从教学示例到工程实践关键差异与注意事项上面的代码完美诠释了RSA的原理但它离一个真正可用的、安全的RSA实现还有巨大差距。理解这些差距正是从业者与学习者的分水岭。4.1 大整数运算必须使用专用库我们的示例使用long long在64位系统上最多表示约1.8e19这连一个微型的RSA密钥比如256位都远远不够。真实的RSA密钥长度至少为2048位这意味着n、p、q、e、d这些数字都是几百位十进制数或几千位二进制数。你必须使用高精度数学库例如C (GMP库)GNU Multiple Precision Arithmetic Library这是C/C下处理大整数的黄金标准。OpenSSL BN库OpenSSL密码学工具包中的BIGNUM模块功能全面广泛应用于生产环境。其他语言Python有原生的大整数支持Java有BigInteger类等。使用这些库你的mod_pow、extended_gcd等函数调用会变成库函数调用但核心算法逻辑完全不变。4.2 密钥生成与素数测试我们的is_prime_simple函数是确定性的且效率极低仅适用于教学。真实环境使用概率性素数测试最常用的是Miller-Rabin测试。原理基于费马小定理的逆命题不一定成立但通过多次随机选择底数进行测试可以将一个合数误判为素数的概率降到极低例如2^(-80)。操作对于需要k比特的素数随机生成一个k比特的奇数然后用Miller-Rabin测试检验。通常进行40-64次迭代误判概率就足够低到可以忽略不计。OpenSSL的BN_is_prime_ex函数就实现了这个算法。重要警告绝对不要自己实现用于密码学的随机数生成器和素数测试。使用经过严格审计和广泛验证的库如OpenSSL或LibreSSL。不正确的实现会导致密钥可被预测或破解。4.3 数据填充方案PKCS#1 v1.5 与 OAEP我们的示例直接对一个整数加密。现实中我们需要加密任意长度的数据比如一个文件、一段消息并且要防止多种密码学攻击。为什么需要填充语义安全如果直接加密相同的明文会产生相同的密文。攻击者可以通过观察密文模式获取信息。适应性选择密文攻击没有填充的RSA是确定性的容易受到特定攻击。编码将任意数据转换为一个小于n的大整数。常用填充方案PKCS#1 v1.5 Padding较旧但广泛支持。它在加密前对明文进行特定格式的填充包含随机字节使得每次加密相同明文得到不同密文。OAEP (Optimal Asymmetric Encryption Padding)更安全、更现代的填充方案。它使用了哈希函数和掩码生成函数被证明在随机预言机模型下是安全的。目前新的应用推荐使用RSA-OAEP。在代码中加密前你需要先对明文字节流进行OAEP填充将其转化为一个整数m然后计算c m^e mod n。解密后得到整数m再进行OAEP解填充恢复原始明文。4.4 性能考量与典型应用场景RSA的模幂运算特别是解密因为d很大计算量很大。因此RSA通常不用于直接加密大量数据。它太慢了。典型应用模式混合加密系统发送方A随机生成一个对称密钥例如一个AES-256密钥。A使用接收方B的RSA公钥加密这个对称密钥。A使用这个对称密钥通过高效的AES算法加密实际的大段数据明文。A将加密后的对称密钥和加密后的数据一起发送给B。B使用自己的RSA私钥解密出对称密钥。B使用解密得到的对称密钥解密数据。这样RSA只用于加密短小的对称密钥发挥了其密钥分发安全的优势而AES用于加密主体数据发挥了其速度快的优势。HTTPS协议中密钥交换的环节就采用了类似的原理。5. 常见问题、调试技巧与安全实践在实际编写和调试RSA相关代码时你肯定会遇到各种问题。这里记录一些典型的坑和排查思路。5.1 常见问题速查表问题现象可能原因排查步骤与解决方案加密解密后结果不一致1. 明文m n。2. 密钥生成错误e*d mod φ(n) ! 1。3. 模幂运算函数(mod_pow)有bug中间结果溢出。1. 检查并确保m n。对于真实数据必须使用填充方案。2. 在密钥生成后立即验证(e * d) % φ(n) 1。3. 用小的测试用例如2^3 mod 5验证mod_pow函数。确保使用long long乘法时考虑溢出最好使用__int128或大数库。程序运行非常慢使用大数时1. 素数生成算法效率低下如试除法。2. 模幂运算未使用快速幂算法。3. 扩展欧几里得算法递归过深对大数。1.换用Miller-Rabin等概率性测试。2. 确认使用的是“平方-乘”快速幂算法。3. 将扩展欧几里得改为迭代实现。无法找到模逆元mod_inverse抛出异常在密钥生成时选择的公钥指数e与φ(n)不互质。在生成e后立即计算gcd(e, φ(n))如果不为1则重新选择e。选择65537几乎不会遇到此问题因为它是素数且通常远小于φ(n)。使用大数库如GMP后编译或链接错误1. 未正确包含头文件路径。2. 未链接对应的库文件如-lgmp。1. 检查编译器指令确保-I包含正确路径。2. 检查链接器指令确保-L和-l选项正确。填充后解密失败1. 填充和解填充过程不一致编码/解码错误。2. 填充后的消息长度超过了RSA模数n所能表示的长度。1.严格遵循标准如PKCS#1或OAEP的实现不要自己发明填充格式。使用库函数如OpenSSL的RSA_public_encrypt/RSA_private_decrypt。2. RSA能加密的数据块长度由密钥长度和填充方案决定。例如2048位密钥256字节使用OAEP填充最多能加密的数据远小于256字节。5.2 安全实践要点切记密钥长度绝对不要使用低于2048位的RSA密钥。1024位密钥已被认为不安全。目前推荐使用2048位或3072位。我们的示例使用小数字仅用于教学。随机性密钥生成中的随机素数p和q必须来自密码学安全的伪随机数生成器。使用std::rand()或普通的时间种子是致命的。私钥保护私钥d以及p,q,φ(n)必须被妥善保护通常存储在加密的密钥文件或硬件安全模块中。程序运行后内存中的这些敏感数据应被安全擦除不仅仅是释放。使用现成的库对于生产环境强烈建议使用成熟的密码学库如OpenSSL, LibreSSL, BoringSSL, libsodium等。自己实现RSA用于实际系统极易引入难以察觉的安全漏洞如侧信道攻击、填充预言攻击。理解算法的局限性RSA本身是确定性算法必须与安全的填充方案如OAEP结合使用。同时RSA加密有数据长度限制。5.3 调试与验证技巧从小开始先用极小的素数如p3, q11手动计算一遍所有参数n33, φ(n)20, 选e3, 计算d7然后用你的程序验证加密解密。这是定位逻辑错误最快的方法。中间变量输出在密钥生成和加解密函数中临时打印出中间变量如计算出的φ(n)、d、模幂运算的中间结果与手算或已知的正确结果对比。使用标准测试向量密码学标准文档如PKCS#1或权威库的测试用例中会提供标准的密钥、明文和密文对。用这些数据来测试你的核心加解密函数。边界测试测试m0,m1,mn-1等边界情况的加密解密是否正确。通过这个从数学原理到C实现再到工程实践考量的完整旅程我希望你收获的不仅仅是一段可以运行的代码更是一种对密码学核心思想——基于计算困难性问题来构建安全协议——的深刻理解。当你下次再看到“RSA-2048”或“非对称加密”这些词时希望你的脑海中能清晰地浮现出那两个大素数p和q以及由它们所构建起的、精妙而坚固的数学堡垒。