从数学原理到可视化实现:C#手搓RSA加密算法全解析

1. 项目概述:为什么我们要“手搓”RSA?

在信息安全领域,RSA加密算法就像一座基石,从HTTPS协议到数字签名,无处不在。很多开发者,尤其是刚接触C#和加密的朋友,可能都只是调用过System.Security.Cryptography.RSACryptoServiceProviderRSA类,传几个参数,加密解密就完成了。这当然没问题,也是生产环境的标准做法。但时间久了,心里总会有点不踏实:这黑盒子里到底发生了什么?公钥和私钥到底是什么数学关系?为什么它这么安全?

这就是我动手做这个项目的初衷。我想抛开.NET Framework/Core内置的、高度封装的RSA类,从最底层的数学原理开始,用C#代码一步步“手搓”出RSA的加密和解密过程。这不仅仅是为了学习,更是为了建立一种深刻的、直觉性的理解。当你自己实现了大素数的生成、欧拉函数、模逆元计算,并亲眼看到用公钥加密的密文能被对应的私钥解开时,那种对非对称加密“魔法”的领悟是完全不同的。

更进一步,我决定用WPF(Windows Presentation Foundation)来为这个“手搓”的RSA引擎打造一个可视化工具。为什么是WPF?因为它的数据绑定和MVVM模式,能让我们将复杂的加密过程和数据状态清晰地分离和展示。你可以实时输入明文、点击生成密钥对、观察加密后的密文(通常是一串巨大的数字),再用私钥解密回来。这个过程的可视化,能将抽象的数学概念转化为直观的操作和结果,无论是用于教学演示,还是自己调试验证算法,都极具价值。

所以,这个项目适合谁呢?如果你是C#初学者,想深入理解加密和WPF;如果你是学生,需要完成一个结合算法与界面的课程设计;或者你是一位经验丰富的开发者,希望重温密码学基础并探索WPF的现代UI开发——那么这个从数学原理到完整可视化工具的旅程,都会让你有所收获。接下来,我们就从最核心的数学部分开始拆解。

2. RSA算法的数学原理与核心步骤拆解

要自己实现RSA,绕不开它的数学基础。别担心,我们不需要成为数学家,但必须理解几个关键概念和步骤。RSA的安全性建立在“大数分解难题”之上,简单说:将两个大质数相乘很容易,但想从一个巨大的乘积中倒推出原来的两个质数,在现有计算能力下几乎不可能。

2.1 密钥生成的五个数学步骤

我们自己实现RSA密钥生成,需要严格遵循以下流程:

  1. 选择两个大质数 p 和 q:这是所有运算的起点。在真实应用中,p和q需要非常大(通常是1024位或2048位的二进制数),以确保安全。对于我们学习和演示,可以选择稍小的、可管理的质数,比如61和53。关键在于,它们必须是质数,并且需要一套算法来生成或验证。在C#中,我们可以自己实现一个简单的质数测试(如米勒-拉宾素性测试),或者为了简化,从一个预制的质数列表中选取。

  2. 计算模数 nn = p * q。这个n就是之后所有模运算的基数。它的长度(二进制位数)决定了密钥的强度。在我们的例子中,n = 61 * 53 = 3233

  3. 计算欧拉函数 φ(n):对于两个质数p和q,欧拉函数的值是φ(n) = (p-1) * (q-1)。它表示在小于n的正整数中,与n互质的数的个数。计算φ(3233) = (61-1) * (53-1) = 60 * 52 = 3120。这个 φ(n) 是后续选择公钥指数e和计算私钥指数d的关键。

  4. 选择公钥指数 e:公钥由(e, n)组成。e 需要满足两个条件:1 < e < φ(n),并且eφ(n)互质(即最大公约数 gcd(e, φ(n)) = 1)。通常,为了计算效率,我们会选择一个较小的、常见的质数,比如 65537 (0x10001)。它满足与大多数 φ(n) 互质,且二进制表示中1很少,能加速加密运算。在我们的例子中,我们检查 65537 是否与 3120 互质。由于 3120 的质因数包括 2, 3, 5, 13,而 65537 是质数且远大于这些数,所以肯定互质。如果 e 与 φ(n) 不互质,就需要重新选择 e。

  5. 计算私钥指数 d:私钥由(d, n)(d, p, q)组成。d 是 e 对于 φ(n) 的“模逆元”。也就是说,d 需要满足:(e * d) % φ(n) = 1。计算模逆元需要用到“扩展欧几里得算法”。这是手搓RSA中最核心的算法之一。给定 e=17(我们换个小点的例子,假设我们选了17作为e),φ(n)=3120,我们需要找到一个 d,使得(17 * d) % 3120 = 1。通过扩展欧几里得算法,我们可以计算出 d=2753(验证:17 * 2753 = 46801,46801 ÷ 3120 = 15 余 1,成立)。

至此,我们得到:

  • 公钥 (e, n): (17, 3233)
  • 私钥 (d, n): (2753, 3233)

注意:在实际的、安全的RSA实现中,私钥通常包含更多信息(如p, q, d mod (p-1), d mod (q-1)等),以便使用中国剩余定理(CRT)来加速解密过程。我们手搓版本为了聚焦原理,可以先使用最基本的 (d, n) 形式。

2.2 加密与解密过程

有了密钥对,加密和解密过程在数学上就非常简洁了。

  • 加密:假设明文是一个数字m(文本需要先转换为数字,例如使用ASCII或Unicode编码,且m必须小于n)。加密过程是计算密文c = m^e mod n。这里^表示幂运算,mod是取模运算。
  • 解密:拿到密文c后,用私钥解密,计算明文m = c^d mod n

为什么这样能恢复明文?这背后的数学原理是欧拉定理。简单理解,因为ed是关于φ(n)的模逆元,所以(m^e)^d mod n = m^(e*d) mod n = m^(k*φ(n)+1) mod n。根据欧拉定理,当mn互质时,m^φ(n) mod n = 1,因此上式等于m。即使mn不互质,利用中国剩余定理也能证明解密成立。

一个手工计算的例子: 假设明文m = 65(ASCII码的 ‘A’)。

  • 加密:c = 65^17 mod 3233。直接计算65的17次方是个天文数字,但我们可以用“快速模幂算法”来高效计算。结果是c = 2790
  • 解密:m = 2790^2753 mod 3233。同样用快速模幂算法,计算后得到m = 65。成功还原!

这里引出了另一个手搓RSA必须实现的核心算法:快速模幂算法。直接计算a^b mod n在指数b很大时(如我们的d=2753)是完全不可行的,会溢出且极慢。快速模幂算法通过将指数二进制化并迭代平方、取模,将计算复杂度从 O(b) 降低到 O(log b),是工程实现的基石。

3. C# 核心算法实现:大数运算与关键模块

理解了数学原理,我们就可以开始用C#编码了。.NET内置的BigInteger结构(位于System.Numerics命名空间)是我们“手搓”RSA的得力助手,它能处理任意大小的整数,完美满足我们对大素数和大数模运算的需求。

3.1 质数生成与素性测试

首先,我们需要一种方法来获得大质数p和q。完全随机生成然后测试是最直接的方法。

using System.Numerics; public static class PrimeGenerator { private static Random _random = new Random(); // 生成一个指定位数的可能质数(奇数) public static BigInteger GeneratePossiblePrime(int bitLength) { byte[] bytes = new byte[bitLength / 8]; _random.NextBytes(bytes); BigInteger number = new BigInteger(bytes); // 确保是正数且是奇数 number = BigInteger.Abs(number); if (number.IsEven) number++; // 确保位数足够(避免前导零导致位数变少) number |= (BigInteger.One << (bitLength - 1)); return number; } // 米勒-拉宾素性测试(概率性测试,但精度足够高) public static bool IsProbablePrime(BigInteger source, int certainty = 10) { if (source == 2 || source == 3) return true; if (source < 2 || source.IsEven) return false; BigInteger d = source - 1; int s = 0; while (d.IsEven) { d /= 2; s += 1; } // 使用一些小质数作为基数进行测试,提高效率 BigInteger[] bases = { 2, 3, 5, 7, 11, 13, 17 }; for (int i = 0; i < bases.Length && bases[i] < source; i++) { if (!MillerRabinTest(source, d, s, bases[i])) return false; } // 通过多轮测试,可以认为是质数的概率极高 return true; } private static bool MillerRabinTest(BigInteger n, BigInteger d, int s, BigInteger a) { BigInteger x = BigInteger.ModPow(a, d, n); if (x == 1 || x == n - 1) return true; for (int r = 1; r < s; r++) { x = BigInteger.ModPow(x, 2, n); if (x == n - 1) return true; if (x == 1) return false; } return false; } // 生成一个指定位数的质数 public static BigInteger GeneratePrime(int bitLength) { BigInteger candidate; do { candidate = GeneratePossiblePrime(bitLength); } while (!IsProbablePrime(candidate)); return candidate; } }

实操心得

  • BigInteger.ModPow方法本身就是快速模幂算法的实现,我们直接调用即可,这大大简化了工作。但在教学意义上,你可以尝试自己实现一个ModularExponentiation函数来加深理解。
  • certainty参数控制测试轮数,轮数越多,误判(将合数判为质数)的概率越低,但耗时也越长。对于演示,10轮足够了。真实应用需要更高 certainty。
  • 生成大质数是相对耗时的操作。在WPF UI线程中直接调用可能会导致界面卡顿。务必在后台线程(如Task.Run)中执行,并通过Dispatcher更新UI

3.2 扩展欧几里得算法求模逆元

这是计算私钥指数d的关键。给定eφ(n),我们需要解方程e*d ≡ 1 (mod φ(n)),即求d = e^(-1) mod φ(n)

public static class MathUtils { // 扩展欧几里得算法,返回 gcd(a, b), 以及满足 ax + by = gcd(a, b) 的 x, y public static (BigInteger gcd, BigInteger x, BigInteger y) ExtendedEuclidean(BigInteger a, BigInteger b) { if (b == 0) return (a, 1, 0); var (gcd, x1, y1) = ExtendedEuclidean(b, a % b); BigInteger x = y1; BigInteger y = x1 - (a / b) * y1; return (gcd, x, y); } // 计算 a 在模 m 下的模逆元,即 a^(-1) mod m // 返回的逆元是正数(在模 m 的意义下) public static BigInteger ModInverse(BigInteger a, BigInteger m) { var (gcd, x, _) = ExtendedEuclidean(a, m); if (gcd != 1) throw new ArgumentException($"数字 {a} 和模数 {m} 不互质,不存在模逆元。"); // 确保结果是正数 BigInteger result = (x % m + m) % m; return result; } }

为什么这样算?扩展欧几里得算法不仅能求最大公约数,还能找到贝祖等式ax + by = gcd(a,b)的整数解x, y。当am互质时,gcd(a, m)=1,等式变为a*x + m*y = 1。对两边同时取模m,得到a*x ≡ 1 (mod m),所以x就是am的逆元。最后(x % m + m) % m是为了确保结果在0m-1的正数范围内。

3.3 封装RSA核心类

现在,我们可以把上述模块组合起来,形成一个完整的RSA密钥生成和加解密类。

public class ManualRSA { public BigInteger ModulusN { get; private set; } public BigInteger PublicExponentE { get; private set; } public BigInteger PrivateExponentD { get; private set; } public BigInteger PhiN { get; private set; } // 仅内部使用,不暴露 public int KeySize => ModulusN.ToByteArray().Length * 8; // 生成密钥对 public void GenerateKeys(int primeBitLength = 512, BigInteger? customE = null) { // 1. 生成两个大质数 BigInteger p = PrimeGenerator.GeneratePrime(primeBitLength); BigInteger q = PrimeGenerator.GeneratePrime(primeBitLength); // 确保 p 和 q 不相等(极低概率但需检查) while (p == q) { q = PrimeGenerator.GeneratePrime(primeBitLength); } // 2. 计算 n 和 φ(n) ModulusN = p * q; PhiN = (p - 1) * (q - 1); // 3. 选择公钥指数 e PublicExponentE = customE ?? new BigInteger(65537); // 常用默认值 65537 // 验证 e 与 φ(n) 互质 if (BigInteger.GreatestCommonDivisor(PublicExponentE, PhiN) != 1) { throw new InvalidOperationException("选择的公钥指数 e 与 φ(n) 不互质,请尝试其他值(如 17)。"); } // 4. 计算私钥指数 d PrivateExponentD = MathUtils.ModInverse(PublicExponentE, PhiN); } // 加密:将明文数字 m 加密为密文数字 c public BigInteger Encrypt(BigInteger plaintextNumber) { if (plaintextNumber >= ModulusN) throw new ArgumentException($"明文数字必须小于模数 n ({ModulusN})。"); // 使用快速模幂算法:c = m^e mod n return BigInteger.ModPow(plaintextNumber, PublicExponentE, ModulusN); } // 解密:将密文数字 c 解密为明文数字 m public BigInteger Decrypt(BigInteger ciphertextNumber) { // 使用快速模幂算法:m = c^d mod n return BigInteger.ModPow(ciphertextNumber, PrivateExponentD, ModulusN); } // 辅助方法:将字符串转换为BigInteger(简单使用UTF8编码) public BigInteger StringToBigInteger(string text) { byte[] bytes = System.Text.Encoding.UTF8.GetBytes(text); // 注意:直接转换可能导致数字过大,实际应用中需要对长文本进行分块 return new BigInteger(bytes, isUnsigned: true); } // 辅助方法:将BigInteger转换回字符串 public string BigIntegerToString(BigInteger number) { byte[] bytes = number.ToByteArray(isUnsigned: true); return System.Text.Encoding.UTF8.GetString(bytes); } }

注意事项与常见陷阱

  1. 文本分块:上面的StringToBigInteger方法对于长文本是有问题的。因为RSA要求加密的明文数字m必须小于模数n。如果文本编码后的字节数组代表的数字大于n,加密就会失败。真正的RSA加密文本,需要结合对称加密(如AES)或使用OAEP等填充方案,并对数据进行分块。我们这里为了演示核心算法,假设处理的都是很短的、转换后小于n的字符串。在可视化工具中,我们会加入长度检查和提示。
  2. 密钥存储:生成的密钥(e, d, n)都是BigInteger。为了在UI上显示和交换,通常需要转换为Base64或十六进制字符串。注意,.NET内置的RSA使用特定的XML或PKCS#1格式,我们手搓的密钥格式是自定义的,两者不兼容。
  3. 性能BigInteger.ModPow对于大数运算已经高度优化,但解密过程(指数d很大)仍然比加密(指数e较小)慢很多。这也是为什么RSA通常只用于加密短数据(如对称密钥)。

4. WPF可视化工具的设计与实现(MVVM模式)

有了RSA核心引擎,我们接下来用WPF构建一个用户友好的界面来驱动它。采用MVVM(Model-View-ViewModel)模式是WPF开发的最佳实践,它能将UI逻辑与业务逻辑清晰分离,便于测试和维护。

4.1 视图模型(ViewModel)设计

ViewModel是连接View(界面)和Model(我们的ManualRSA类)的桥梁。它包含属性(用于数据绑定)和命令(用于响应按钮点击)。

using System; using System.Numerics; using System.Threading.Tasks; using System.Windows.Input; using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.Input; namespace RSATool.ViewModels { public class MainViewModel : ObservableObject { private ManualRSA _rsaEngine = new ManualRSA(); // 绑定到UI的属性 private string _primeBitLength = "512"; public string PrimeBitLength { get => _primeBitLength; set => SetProperty(ref _primeBitLength, value); } private string _publicKeyE = "65537"; public string PublicKeyE { get => _publicKeyE; set => SetProperty(ref _publicKeyE, value); } private string _modulusN = ""; public string ModulusN { get => _modulusN; set => SetProperty(ref _modulusN, value); } private string _privateKeyD = ""; public string PrivateKeyD { get => _privateKeyD; set => SetProperty(ref _privateKeyD, value); } private string _inputText = "Hello RSA!"; public string InputText { get => _inputText; set => SetProperty(ref _inputText, value); } private string _encryptedText = ""; public string EncryptedText { get => _encryptedText; set => SetProperty(ref _encryptedText, value); } private string _decryptedText = ""; public string DecryptedText { get => _decryptedText; set => SetProperty(ref _decryptedText, value); } private bool _isGeneratingKeys; public bool IsGeneratingKeys { get => _isGeneratingKeys; set => SetProperty(ref _isGeneratingKeys, value); } // 命令 public IAsyncRelayCommand GenerateKeysCommand { get; } public IRelayCommand EncryptCommand { get; } public IRelayCommand DecryptCommand { get; } public MainViewModel() { GenerateKeysCommand = new AsyncRelayCommand(GenerateKeysAsync); EncryptCommand = new RelayCommand(Encrypt); DecryptCommand = new RelayCommand(Decrypt); } private async Task GenerateKeysAsync() { IsGeneratingKeys = true; try { // 在后台线程执行耗时的密钥生成 await Task.Run(() => { int bitLength = int.Parse(PrimeBitLength); BigInteger? customE = null; if (BigInteger.TryParse(PublicKeyE, out BigInteger eValue)) { customE = eValue; } _rsaEngine.GenerateKeys(bitLength, customE); }); // 更新UI(自动回到UI线程) ModulusN = _rsaEngine.ModulusN.ToString("X"); // 十六进制显示 PrivateKeyD = _rsaEngine.PrivateExponentD.ToString("X"); } catch (Exception ex) { // 实际项目中应使用日志或更友好的错误提示 System.Diagnostics.Debug.WriteLine($"生成密钥失败: {ex.Message}"); } finally { IsGeneratingKeys = false; } } private void Encrypt() { try { if (string.IsNullOrWhiteSpace(InputText)) return; // 将字符串转换为BigInteger BigInteger plainNumber = _rsaEngine.StringToBigInteger(InputText); // 检查明文是否小于模数n if (plainNumber >= _rsaEngine.ModulusN) { EncryptedText = "[错误] 明文过长,请缩短文本或增大密钥长度。"; return; } BigInteger cipherNumber = _rsaEngine.Encrypt(plainNumber); EncryptedText = cipherNumber.ToString("X"); // 十六进制显示密文 } catch (Exception ex) { EncryptedText = $"[加密错误] {ex.Message}"; } } private void Decrypt() { try { if (string.IsNullOrWhiteSpace(EncryptedText)) return; // 将十六进制密文字符串解析回BigInteger if (!BigInteger.TryParse(EncryptedText, System.Globalization.NumberStyles.HexNumber, null, out BigInteger cipherNumber)) { DecryptedText = "[错误] 密文格式无效。"; return; } BigInteger plainNumber = _rsaEngine.Decrypt(cipherNumber); DecryptedText = _rsaEngine.BigIntegerToString(plainNumber); } catch (Exception ex) { DecryptedText = $"[解密错误] {ex.Message}"; } } } }

设计要点

  • 异步命令:密钥生成是CPU密集型操作,会阻塞UI。使用AsyncRelayCommandTask.Run将其放到后台线程,并通过IsGeneratingKeys属性绑定按钮的IsEnabled或显示进度条,防止界面卡死。
  • 数据绑定:所有属性都实现了INotifyPropertyChanged(通过ObservableObject),当属性值改变时,UI会自动更新。
  • 错误处理:在加解密和转换过程中加入try-catch,并将友好错误信息绑定到UI,而不是让程序崩溃。
  • 显示格式BigInteger直接以十进制显示会非常长,使用十六进制(ToString("X"))更紧凑易读。

4.2 视图(View)设计

View是用户直接交互的界面。我们使用XAML来定义布局,并利用数据绑定将UI元素与ViewModel的属性连接起来。

<Window x:Class="RSATool.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RSATool.ViewModels" mc:Ignorable="d" Title="手搓RSA加密解密工具" Height="650" Width="800"> <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!-- 密钥生成区域 --> <GroupBox Grid.Row="0" Header="1. 生成RSA密钥对" Margin="0,0,0,10"> <StackPanel> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock Text="质数位数:" VerticalAlignment="Center" Width="80"/> <TextBox Text="{Binding PrimeBitLength, UpdateSourceTrigger=PropertyChanged}" Width="80" Margin="5,0"/> <TextBlock Text="(例如: 512, 1024)" VerticalAlignment="Center" Foreground="Gray" Margin="5,0"/> <TextBlock Text="公钥指数 e:" VerticalAlignment="Center" Width="80" Margin="20,0,0,0"/> <TextBox Text="{Binding PublicKeyE, UpdateSourceTrigger=PropertyChanged}" Width="120" Margin="5,0"/> <TextBlock Text="(常用 65537)" VerticalAlignment="Center" Foreground="Gray"/> </StackPanel> <Button Content="生成密钥对" Command="{Binding GenerateKeysCommand}" Margin="5" Padding="20,5" HorizontalAlignment="Left"> <Button.Style> <Style TargetType="Button"> <Setter Property="IsEnabled" Value="True"/> <Style.Triggers> <DataTrigger Binding="{Binding IsGeneratingKeys}" Value="True"> <Setter Property="Content" Value="生成中..."/> <Setter Property="IsEnabled" Value="False"/> </DataTrigger> </Style.Triggers> </Style> </Button.Style> </Button> <Grid Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Text="模数 n:" VerticalAlignment="Top" Grid.Column="0"/> <TextBox Text="{Binding ModulusN}" IsReadOnly="True" Grid.Column="1" Margin="5,0" TextWrapping="Wrap" AcceptsReturn="True" Height="60" VerticalScrollBarVisibility="Auto" FontFamily="Consolas"/> </Grid> <Grid Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Text="私钥 d:" VerticalAlignment="Top" Grid.Column="0"/> <TextBox Text="{Binding PrivateKeyD}" IsReadOnly="True" Grid.Column="1" Margin="5,0" TextWrapping="Wrap" AcceptsReturn="True" Height="60" VerticalScrollBarVisibility="Auto" FontFamily="Consolas"/> </Grid> </StackPanel> </GroupBox> <!-- 加解密操作区域 --> <GroupBox Grid.Row="1" Header="2. 加密与解密" Margin="0,0,0,10"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!-- 明文输入 --> <GroupBox Header="明文输入" Grid.Row="0" Grid.Column="0" Margin="5"> <TextBox Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" FontFamily="Consolas"/> </GroupBox> <!-- 操作按钮 --> <StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center"> <Button Content="加密 →" Command="{Binding EncryptCommand}" Margin="5" Padding="20,5"/> <Button Content="← 解密" Command="{Binding DecryptCommand}" Margin="5" Padding="20,5"/> </StackPanel> <!-- 密文显示 --> <GroupBox Header="密文 (十六进制)" Grid.Row="0" Grid.Column="2" Grid.RowSpan="2" Margin="5"> <TextBox Text="{Binding EncryptedText}" IsReadOnly="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" FontFamily="Consolas"/> </GroupBox> <!-- 解密结果 --> <GroupBox Header="解密结果" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Margin="5"> <TextBox Text="{Binding DecryptedText}" IsReadOnly="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" FontFamily="Consolas"/> </GroupBox> </Grid> </GroupBox> <!-- 状态栏 --> <StatusBar Grid.Row="2"> <StatusBarItem> <TextBlock> <Run Text="密钥长度:"/> <Run Text="{Binding KeySize, Mode=OneWay}"/> <Run Text=" bits"/> </TextBlock> </StatusBarItem> <Separator/> <StatusBarItem> <TextBlock Text="提示:首次使用请先生成密钥对。明文不宜过长。" FontStyle="Italic" Foreground="Gray"/> </StatusBarItem> </StatusBar> </Grid> </Window>

UI设计技巧

  • 分组布局:使用GroupBox将功能模块(密钥生成、加解密)清晰分隔,提升用户体验。
  • 多行文本框:对于可能很长的密钥和密文,使用TextBox并设置AcceptsReturnTextWrappingVerticalScrollBarVisibility,使其可以显示多行和滚动。
  • 等宽字体:对显示数字和十六进制字符串的文本框使用FontFamily="Consolas",使字符对齐,更易于阅读。
  • 数据触发器:通过DataTrigger在密钥生成时改变按钮状态(显示“生成中...”并禁用),提供即时反馈。
  • 状态栏:显示当前密钥长度等动态信息,增强工具的专业感。

4.3 应用启动与数据上下文绑定

最后,在App.xaml中移除StartupUri,在App.xaml.cs中手动启动主窗口并设置DataContext。

// App.xaml.cs protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var mainView = new MainWindow(); mainView.DataContext = new MainViewModel(); // 实例化ViewModel mainView.Show(); }

至此,一个完整的、从数学原理到可视化界面的“手搓”RSA工具就完成了。运行程序,你可以体验生成密钥、加密一段文字、再解密的完整过程,并直观地看到每一步产生的巨大数字。

5. 进阶优化、安全考量与常见问题

我们的基础版本已经可以工作,但距离一个健壮、实用的工具还有距离。这里探讨几个进阶方向和必须注意的安全问题。

5.1 性能优化:中国剩余定理(CRT)加速解密

我们实现的解密公式m = c^d mod n在私钥指数d很大时计算很慢。实际的标准RSA实现(如.NET内置的)会使用中国剩余定理,利用私钥中包含的pq来大幅加速。

原理简述: 已知n = p * q,且pq互质。

  1. 分别计算:
    • m_p = c^(d mod (p-1)) mod p
    • m_q = c^(d mod (q-1)) mod q(根据费马小定理,指数可以模p-1q-1简化,且d mod (p-1)d mod (q-1)可以预计算并存储在私钥中)
  2. 由于pqn小得多,这两个模幂运算比直接算mod n快得多。
  3. 利用CRT,由m_pm_q可以唯一确定m mod n

实现思路:在ManualRSA类中,密钥生成时保存p,q,dP (d mod (p-1)),dQ (d mod (q-1))qInv (q^(-1) mod p)。在解密方法中,实现CRT版本的计算。这会使解密速度提升数倍。

5.2 数据填充(Padding)与长文本处理

我们之前的实现有一个致命缺陷:没有使用填充方案,并且直接对字符串进行编码加密。这存在两大问题:

  1. 确定性加密:同样的明文每次加密产生同样的密文,这在语义上是不安全的。
  2. 长度限制:明文必须小于模数n。对于2048位密钥,n约等于2^2048,能加密的原始数据长度非常有限(约256字节)。

解决方案:使用OAEP填充在实际应用中,RSA从不直接加密原始数据。而是采用像PKCS#1 OAEP这样的填充方案。它的作用:

  • 随机化:引入随机数,使得每次加密相同明文得到不同密文。
  • 增强安全性:防止特定的数学攻击。
  • 编码:将任意长度的数据(在一定限制内)编码为符合RSA加密块长度的格式。

对于长文本,标准的做法是:

  1. 生成一个随机的对称密钥(如AES-256密钥)。
  2. 用这个对称密钥加密你的长文本。
  3. 用RSA公钥加密这个对称密钥
  4. 将RSA加密后的对称密钥和AES加密后的密文一起发送或存储。 解密时,先用RSA私钥解出对称密钥,再用对称密钥解密文本。

在我们的工具中如何改进?我们可以引入一个“混合加密”模式。在UI上增加一个选项:“使用混合加密(RSA+AES)”。当勾选时:

  • 加密流程:生成随机AES密钥 -> 用AES加密明文 -> 用RSA公钥加密AES密钥 -> 输出[RSA加密的AES密钥][AES密文]的Base64组合。
  • 解密流程:拆分组合 -> 用RSA私钥解密出AES密钥 -> 用AES密钥解密密文。

5.3 常见问题与排查技巧

在开发和测试过程中,你可能会遇到以下问题:

问题现象可能原因排查与解决思路
加密时抛出“明文必须小于模数”异常输入的文本太长,转换后的BigInteger大于或等于n1. 检查密钥长度。512位密钥只能加密极短文本(几个字符)。
2. 实现分块加密或切换到“混合加密”模式。
3. 在加密前,先计算明文数字的大小并给出友好提示。
解密结果乱码1. 加密和解密使用的密钥不匹配。
2. 密文在传输或显示过程中被篡改(如复制丢失字符)。
3. 文本编码不一致(加密用UTF8,解密用其他编码)。
1.务必确保加密用的公钥(e,n)和解密用的私钥(d,n)是同一对密钥生成的。重新生成密钥对后,旧的密文无法用新私钥解密。
2. 密文(十六进制字符串)要完整复制。可以增加一个“复制到剪贴板”按钮减少出错。
3. 在StringToBigIntegerBigIntegerToString中统一使用Encoding.UTF8
密钥生成非常慢(尤其是1024位以上)大质数生成和素性测试是计算密集型操作。1. 这是正常现象。在UI上显示进度指示器(如ProgressBar)。
2. 考虑使用更高效的素性测试算法(如 Baillie-PSW)。
3.切勿在UI线程执行,必须使用异步。
“公钥指数e与φ(n)不互质”错误自定义的公钥指数eφ(n)有公因数。1.e必须是奇数,且通常选质数。最安全省事的选择是固定使用65537
2. 如果自定义e,在生成密钥的方法中加入重试逻辑,如果选择的e不互质,则自动尝试下一个奇数。
用其他工具生成的密文,无法用本工具解密密钥格式和填充方案不兼容。本工具是自定义的、原始的、无填充的RSA实现。标准工具(如OpenSSL)使用PKCS#1等标准格式和填充。两者不互通。本工具主要用于教育演示,理解算法本质。

一个重要的实操心得:在调试加解密过程时,可以先用很小的、可手算的密钥进行验证。例如,使用之前例子中的p=61, q=53, e=17, d=2753, n=3233。在工具中硬编码这些值,然后加密数字65,看结果是否为2790。这种“已知答案测试”是验证算法实现是否正确的最有效方法。

6. 项目总结与扩展方向

走完从数学原理推导、C#算法实现到WPF可视化界面构建的整个流程,相信你对RSA不再感到神秘。这个“手搓”的过程,强迫我们去关注每一个细节:如何生成大素数、如何计算模逆元、快速模幂算法如何工作、以及公钥私钥如何配对。这些知识,是单纯调用API所无法给予的。

这个项目本身还有很大的扩展空间,你可以基于它做更深入的探索:

  1. 实现完整的RSA标准:集成PKCS#1 v1.5或OAEP填充方案,让你的工具能够与其他标准RSA库(如OpenSSL)生成的密钥和密文进行互操作。这会涉及到ASN.1编码解码,是一个不小的挑战。
  2. 增加密钥导入/导出功能:支持将生成的密钥对以PEM或XML格式保存到文件,并从文件加载。这需要你理解这些格式的规范。
  3. 添加数字签名功能:RSA除了加密,另一个重要用途是数字签名。尝试实现用私钥“签名”一段数据的哈希值,然后用公钥“验证”签名。
  4. 性能分析与可视化:在工具中加入计时功能,比较不同密钥长度下加密、解密的速度,并用WPF的图表控件(如LiveCharts)绘制成曲线,直观展示RSA性能随密钥长度增长而下降的趋势。
  5. 移植到 .NET MAUI 或 Avalonia:将WPF前端替换为跨平台的UI框架,让你手搓的RSA引擎能在macOS或Linux上运行。

最后,必须再次强调安全警告:我们这个用于教育和演示的“手搓”RSA实现,绝对不应用于任何真实的生产环境或保护敏感数据。密码学实现中细微的错误都可能导致严重的漏洞。生产环境请务必使用经过严格审计和长期考验的库,如 .NET 自带的System.Security.Cryptography.RSA类。这个项目的价值,在于照亮黑盒,让你在下次使用那些强大、安全的官方API时,心中更有底气,知道脚下坚实的数学和工程基础究竟是什么。