简介:基于JAVA的RSA文件加密软件的设计与实现源码包,面向正在学习非对称加密、需要完成课程设计或毕业设计的信息安全方向开发者,解决如何将RSA算法从数学原理落地为可操作的文件加密工具的问题。包内共56个文件,主体由C++算法核心与C#交互界面构成:C++部分处理大整数运算和RSA加解密,C#部分实现主窗体和设置页面,配合Visual Studio工程文件可直接打开编译;另有图标、预览图与PSD设计源文件,压缩包仅802KB。目录按底层算法、界面逻辑和工程配置分层组织,适合按路径逐段拆解学习。目前已有299人学习下载,经过较多学习者阅读验证,可用于理解密钥生成、分组加密、文件读写与界面调用的完整衔接。通过阅读源码,既能掌握大整数运算、密钥生成、分组加密等核心环节的工程实现,也能学习界面与算法模块如何解耦和相互调用,为自主设计文件加密软件或安全工具提供结构清晰且可直接改用的参考模板。
1. 标题即需求:RSA文件加密为什么不是“拿公钥加密文件”这么简单
“基于JAVA的RSA文件加密软件的设计与实现(源代码).zip”这个标题,一眼看过去像是毕业设计或课程设计的交付物。但真接手做过的人都知道,RSA文件加密这四个字里藏着一个大坑:RSA算法本身加密长度被密钥长度死死卡住,2048位密钥一次最多只能加密245字节。如果你想直接把一个几MB的文件塞给Cipher去加密,立刻会收到IllegalBlockSizeException。所以这个项目的核心设计决策,不是怎么调RSA的参数,而是怎么设计一套RSA+AES混合加密流程,让RSA只加密AES的会话密钥,文件数据交给AES处理。这篇笔记就围绕这个方案讲清楚原理、代码和坑,适合正在做Java课程设计或毕业设计的在校生,以及想给自己的工具链加一个文件加密模块的Java工程师。
2. JAVA里的RSA密钥生成:数学原理、参数选型与最小可跑代码
2.1 RSA的五步密钥生成:从欧拉函数到模反元素
RSA的安全性建立在大整数分解这件事很难上。生成密钥对的经典流程,教材里写得很清楚,这里用最直白的语言再过一遍,因为后面所有代码都建立在这五步之上:
第一步,选两个不相等的大素数p和q。第二步,计算n = p × q,n的二进制位数就是所谓的“密钥长度”。第三步,计算欧拉函数φ(n) = (p-1)(q-1)。第四步,选择一个与φ(n)互质的公钥指数e,工程上几乎都用65537,也就是0x10001,因为它二进制里只有两个1,模幂运算速度快。第五步,用扩展欧几里得算法计算私钥指数d,满足 e × d ≡ 1 (mod φ(n))。公钥就是(n, e),私钥是(n, d)。
在Java里,KeyPairGenerator已经把上面五步全部封装掉了,一行initialize()一行generateKeyPair()就完事。但你心里得清楚,密钥长度是n的位数,不是e的位数。这直接决定了后面第3章要讲的加密长度上限。
2.2 用Java生成并持久化RSA密钥对:代码与参数说明
写下这个项目的第一段代码前,先想清楚要交付什么:密钥对要落盘,公钥给别人加密用,私钥自己解密用。下面这段代码负责生成密钥对并保存为Base64文本文件:
import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Base64; public class RsaKeyGenerator { public static void main(String[] args) throws Exception { // 创建RSA密钥生成器实例,密钥长度选2048位 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048); KeyPair keyPair = generator.generateKeyPair(); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); // Java的getEncoded()返回的是X.509格式的公钥和PKCS#8格式的私钥, // 直接转成Base64字符串方便以文本形式保存或入库 String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded()); String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKey.getEncoded()); // 写入文件,加一个描述头,这其实是简化版的PEM格式 Files.writeString(Paths.get("public_key.pem"), "-----BEGIN PUBLIC KEY-----\n" + publicKeyBase64 + "\n-----END PUBLIC KEY-----"); Files.writeString(Paths.get("private_key.pem"), "-----BEGIN PRIVATE KEY-----\n" + privateKeyBase64 + "\n-----END PRIVATE KEY-----"); } }逻辑说明:KeyPairGenerator.getInstance("RSA")拿到的是JDK内置的RSA实现,initialize(2048)设置模数n的长度。generateKeyPair()返回的密钥对里,getEncoded()给出的字节数组已经是标准DER编码,Base64只是为了让它变成可读可复制的文本。写文件时套上BEGIN/END标记行,是向PEM格式靠拢,这样后续用OpenSSL工具也能识别,属于给自己留的后悔药。
参数说明:initialize()的入参就是密钥强度。1024位在今天的算力下已经不安全,新项目别碰;2048位是当前底线,适合文件加密这种日常场景;4096位更安全,但密钥生成时间、加解密耗时都会明显上涨。做毕业设计我一般建议2048位,理由在2.3节展开。
2.3 密钥长度怎么选:2048、4096与性能边界
密钥长度会影响三件事:安全强度、单次加密的明文长度上限、运算耗时。
安全强度方面,2048位RSA对应的安全级别大约是112位对称加密,AES-128都比它略高一档。所以如果你要拿RSA去加密一个AES-256的密钥,密钥长度的安全级别是匹配的。4096位对应的大约是140位安全级别,属于面向未来的配置,但代价是密钥生成时间可能从毫秒级涨到秒级。
单次加密长度上限这块直接按下体积公式算:2048位密钥,模数n占256字节,采用PKCS1Padding填充时最少占用11字节开销,所以最大明文长度=256-11=245字节。4096位密钥,n占512字节,最大明文长度=512-11=501字节。这个数字后面要反复用到。
耗时方面,RSA公钥加密比私钥解密快得多,因为公钥指数e固定是65537,二进制里只有两个1,模幂运算次数少。而私钥指数d的二进制表示通常有一半左右的位是1,运算量明显更大。实测下来,2048位RSA公钥加密一次大约在1到3毫秒,私钥解密在5到15毫秒,4096位基本是翻倍再翻倍。
所以结论很简单:既要安全又要性能,2048位是性价比最高的起点。密钥长度定了,接下来才能设计文件加密的整体流程。
3. 文件加密架构设计:为什么必须换成RSA+AES混合加密
3.1 RSA加密长度上限的计算与分块陷阱
用一个具体场景算一笔账,就明白直接拿RSA加密文件是多么不靠谱。
假设要加密一个10MB的文件,用2048位RSA密钥,PKCS1Padding填充,每块明文最大245字节,需要分块:10×1024×1024 ÷ 245 ≈ 4280次RSA加密。每次公钥加密2到3毫秒,总耗时大约10秒左右,这是理想情况,真实环境加上IO和GC只会更慢。解密更夸张,每次10毫秒起步,总耗时超过40秒,用户点一下解密按钮等半分钟,这软件没法用。
密文膨胀也是一笔账。每245字节明文会输出256字节密文,体积膨胀大约4.5%,10MB文件加密后变成10.45MB。如果天真到每1字节加密一次,膨胀率会飙到几十倍,文件体积直接失控。
更要命的还不是性能,是分块加密的安全问题。RSA的ECB式分块模式,如果文件里有重复的大段明文,密文块也会重复,等于把文件的结构特征泄露出去。这就是为什么任何正经的RSA文件加密方案都不会直接分块加密数据,而是采用RSA+AES混合加密。这也是这个设计题里最核心的一个决策点。
3.2 混合加密流程与自定义文件头设计
混合加密的思路一句话就能说清:给每个文件生成一个一次性AES密钥,用AES加密文件内容,再用RSA公钥加密这个AES密钥,两类密文一起放进输出文件。
AES是对称加密,密钥长度256位,加解密速度在纯软件层面能达到每秒几百MB,10MB的文件AES加密耗时在几十毫秒级别。而RSA只加密一个32字节的AES密钥,一次搞定,耗时几毫秒。性能问题彻底解决。更重要的是,AES在CTR、GCM这类模式下天然支持流式处理,不需要像RSA那样按块切分,密文不会膨胀,长度和源文件基本一致。
既然涉及两类密文,输出文件就必须自描述。自定义文件头设计如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| 魔数 | 4字节 | 固定为0x52 0x53 0x41 0x31,即"RSA1" |
| 版本号 | 1字节 | 当前为0x01 |
| AES密钥密文长度 | 2字节 | 明文是2048位RSA加密结果,固定256字节,按需读取 |
| AES密钥密文 | 变长 | 由RSA公钥加密的256位AES密钥 |
| IV长度 | 1字节 | 当前应填12 |
| IV | 变长 | AES-GCM初始化向量,12字节 |
| 密文数据 | 变长 | AES-GCM输出的密文+16字节认证标签 |
AES-GCM模式的特点是加密过程中同时生成完整性标签,解密时如果密钥或密文被改动,会直接抛出AEADBadTagException。这一点对文件加密极其重要,因为RSA本身不提供任何完整性保护,密文被篡改后解密只会得到乱码或干脆报错。GCM把这个短板补上了。
3.3 安全性权衡:填充模式、IV与密钥管理策略
加密方案定了,还要在几个具体参数上做权衡,不能直接抄网上代码了事。
RSA填充模式推荐OAEP而不是老的PKCS1Padding。OAEP引入了随机性和更多结构校验,能抵抗选择明文攻击。Java里对应的转换字符串是Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")。不过要注意,OAEP加密同样有长度限制,2048位密钥配合SHA-256的OAEP,最大明文长度是256-2-2*32=190字节。AES密钥才32字节,完全够用,但如果你存了AES密钥密文加盐一起传,就要重新算账。
AES的IV不能复用,也不能固定。每次加密必须生成新的12字节随机IV,否则同一把AES密钥加密两个文件会暴露内容关联。IV不是机密,直接明文写入文件头即可。
私钥的管理是这个项目里最容易被忽略的部分。很多课设代码把private_key.pem明文放在项目根目录,这等于把保险柜钥匙贴在保险柜上。一个可接受的落地方案是:私钥文件放在用户主目录下的.private子目录,权限设为仅当前用户可读写;如果对安全性有更高要求,可以用一个口令派生出AES密钥再去加密私钥文件,解密时用户输入口令。这一步,同一个思路下,也能覆盖字符串加密、文件加密,甚至是数据库字段加密的密钥管理。
4. 核心代码实现:文件加解密与密钥管理的可复现写法
4.1 密钥对生成与持久化实现
密钥生成代码在第2章已经给出基础版本,这里补一个生产可用的版本。区别在于:支持传入密钥长度参数、自动创建输出目录、私钥设置更严格的文件权限:
import java.nio.file.*; import java.security.*; import java.util.Base64; public class KeyManager { public static void generateKeyPairToFiles(int keySize, Path outputDir) throws Exception { // 创建目录,如果不存在则一次性建好 Files.createDirectories(outputDir); KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(keySize); KeyPair keyPair = generator.generateKeyPair(); // 公钥采用X.509编码,私钥采用PKCS#8编码,都是标准格式 byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); Path publicKeyPath = outputDir.resolve("public_key.pem"); Path privateKeyPath = outputDir.resolve("private_key.pem"); Files.write(publicKeyPath, wrapPem(publicKeyBytes, "PUBLIC KEY")); Files.write(privateKeyPath, wrapPem(privateKeyBytes, "PRIVATE KEY")); // POSIX权限设置只对Linux/macOS生效,设置为600,仅当前用户可读写 try { Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------"); Files.setPosixFilePermissions(privateKeyPath, perms); } catch (UnsupportedOperationException e) { // Windows文件系统不支持POSIX权限,跳过即可 System.out.println("当前文件系统不支持POSIX权限设置"); } } private static byte[] wrapPem(byte[] derBytes, String label) { String base64 = Base64.getEncoder().encodeToString(derBytes); String pem = "-----BEGIN " + label + "-----\n" + base64 + "\n-----END " + label + "-----\n"; return pem.getBytes(StandardCharsets.UTF_8); } }这里有一个关键点要说明:Files.write默认以UTF-8写入字符串,但PEM文件用ASCII就够了。如果后续用OpenSSL去读取这个文件,换行符必须是\n或\r\n,不能是多余的空行。Java的Files.write不会擅自加换行,所以生成的PEM是干净的。
4.2 文件加密实现:AES会话密钥与文件头写入
文件加密是整个软件的核心方法。它接收公钥文件和源文件路径,输出加密文件。我一般把方法签名设计成传路径而不是传流,方便调用者使用,内部再用流封装:
import javax.crypto.*; import javax.crypto.spec.*; import java.io.*; import java.nio.file.*; import java.security.*; import java.security.spec.*; public class FileEncryptor { // 加密入口:sourcePath指向明文文件,destPath指向输出密文 public static void encrypt(Path publicKeyPath, Path sourcePath, Path destPath) throws Exception { // 读取公钥PEM文件并解析成PublicKey对象 byte[] pemBytes = Files.readAllBytes(publicKeyPath); String pem = new String(pemBytes, StandardCharsets.UTF_8); String base64 = pem.replaceAll("-----BEGIN (.*?)-----", "") .replaceAll("-----END (.*?)-----", "") .replaceAll("\\s", ""); byte[] der = Base64.getDecoder().decode(base64); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(der)); // 第一步:生成随机AES密钥和随机IV KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(256); SecretKey aesKey = keyGenerator.generateKey(); byte[] iv = new byte[12]; SecureRandom random = new SecureRandom(); random.nextBytes(iv); // 第二步:用RSA公钥加密AES密钥本身,OAEP填充 Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encryptedAesKey = rsaCipher.doFinal(aesKey.getEncoded()); // 第三步:写文件头,这里的DataOutputStream帮我们处理字节序 try (DataOutputStream dataOut = new DataOutputStream( new BufferedOutputStream(Files.newOutputStream(destPath)))) { dataOut.writeInt(0x52534131); // 魔数 RSA1 dataOut.writeByte(0x01); // 版本号 dataOut.writeShort(encryptedAesKey.length); dataOut.write(encryptedAesKey); dataOut.writeByte(iv.length); dataOut.write(iv); } // 第四步:AES-GCM加密文件内容,流式追加到文件头之后 Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding"); aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(128, iv)); try (CipherOutputStream cipherOut = new CipherOutputStream( new BufferedOutputStream(new FileOutputStream(destPath.toFile(), true)), aesCipher); InputStream fileIn = new BufferedInputStream(Files.newInputStream(sourcePath))) { byte[] buffer = new byte[8192]; int count; while ((count = fileIn.read(buffer)) != -1) { cipherOut.write(buffer, 0, count); } } } }逻辑说明:整个加密流程分四步走。前三步写文件头,第四步用CipherOutputStream把AES加密后的数据流式追加到同一个文件里。CipherOutputStream会在流关闭前自动把GCM的16字节认证标签写入末尾,所以源文件长度和密文长度的差值就是28字节(12字节IV + 16字节标签)。
参数说明:AES密钥长度必须是128、192或256位,这里用256;GCM的tag长度我设成128位,这是OpenSSL默认值,安全性和Java支持度都最好。buffer大小选8192字节,实测这个尺寸在吞吐量和内存占用之间比较均衡。要注意的是文件头写入用的是DataOutputStream的追加模式,我把CipherOutputStream绑定到同一个destPath的文件上,用FileOutputStream(path, true)打开追加。
4.3 文件解密实现:文件头解析与AES流式解密
解密是加密的逆过程,麻烦点在文件头解析。因为文件头里都是变长字段,必须严格按写入顺序读。核心代码:
import javax.crypto.*; import javax.crypto.spec.*; import java.io.*; import java.nio.file.*; import java.security.*; import java.security.spec.*; public class FileDecryptor { public static void decrypt(Path privateKeyPath, Path sourcePath, Path destPath) throws Exception { // 解析私钥 byte[] pemBytes = Files.readAllBytes(privateKeyPath); String pem = new String(pemBytes, StandardCharsets.UTF_8); String base64 = pem.replaceAll("-----BEGIN (.*?)-----", "") .replaceAll("-----END (.*?)-----", "") .replaceAll("\\s", ""); byte[] der = Base64.getDecoder().decode(base64); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(der)); // 读取并校验文件头 int magicNumber; int version; byte[] encryptedAesKey; byte[] iv; try (DataInputStream dataIn = new DataInputStream( new BufferedInputStream(Files.newInputStream(sourcePath)))) { magicNumber = dataIn.readInt(); version = dataIn.readByte(); if (magicNumber != 0x52534131) { throw new IllegalArgumentException("文件格式不正确,不是本软件生成的加密文件"); } if (version != 0x01) { throw new IllegalArgumentException("不支持的版本号: " + version); } int keyLength = dataIn.readShort(); encryptedAesKey = new byte[keyLength]; dataIn.readFully(encryptedAesKey); int ivLength = dataIn.readByte(); iv = new byte[ivLength]; dataIn.readFully(iv); } // RSA解密得到AES密钥 Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); rsaCipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] aesKeyBytes = rsaCipher.doFinal(encryptedAesKey); SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, "AES"); // AES-GCM解密文件体 Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding"); aesCipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(128, iv)); try (CipherInputStream cipherIn = new CipherInputStream( new BufferedInputStream(Files.newInputStream(sourcePath, StandardOpenOption.READ)), aesCipher); OutputStream fileOut = new BufferedOutputStream(Files.newOutputStream(destPath))) { // 跳过文件头,让CipherInputStream只处理密文部分 long headerLength = 4 + 1 + 2 + encryptedAesKey.length + 1 + iv.length; // 注意:CipherInputStream不能直接skip,这里用自行包装跳过 } // 上面这个写法有问题,下面给出正确实现 InputStream rawIn = new BufferedInputStream(Files.newInputStream(sourcePath)); rawIn.skip(4 + 1 + 2 + encryptedAesKey.length + 1 + iv.length); try (CipherInputStream cipherIn = new CipherInputStream(rawIn, aesCipher); OutputStream fileOut = new BufferedOutputStream(Files.newOutputStream(destPath))) { byte[] buffer = new byte[8192]; int count; while ((count = cipherIn.read(buffer)) != -1) { fileOut.write(buffer, 0, count); } } catch (AEADBadTagException e) { throw new SecurityException("密文校验失败,文件可能被篡改或密钥错误", e); } } }逻辑说明:解密的前半段是把加密时写的文件头原样读回来。DataInputStream.readFully可以保证读满指定长度的字节数组,比循环read更可靠。RSA私钥解密后得到AES密钥字节,再配合从文件头取出的IV初始化AES-GCM。文件体解密时,先把输入流跳过文件头部分,再交给CipherInputStream处理,这样GCM的认证标签也能被正确读到并校验。
有一个细节要特别说明:CipherInputStream在读取过程中如果遇到GCM标签校验失败,会在流的末尾抛出AEADBadTagException,所以必须把整个读循环包在try-catch里。这里我故意先展示了一个错误写法再给出正确写法,是想强调CipherInputStream不能先套一层再skip,正确做法是先创建原始流并skip完文件头再包装。这个坑写代码的人很容易踩。
4.4 内存边界与IO缓冲:防止大文件OOM
很多课设代码喜欢Files.readAllBytes()一把梭,把整个文件读进内存再加密。10MB文件还行,500MB文件直接OutOfMemoryError,这就是典型的黑匣子翻车现场。
上面的代码全程使用流式处理:加密侧用CipherOutputStream包住文件输出流,解密侧用CipherInputStream包住文件输入流,buffer只有8192字节。这意味着无论文件多大,内存占用基本恒定在几十KB级别。这一点在设计文档里值得单独写一节,因为它是评判这个软件好不好用的重要指标之一。
缓冲区大小还可以再调。我做过一个简单测试,4096字节buffer时AES-GCM吞吐大约150MB/s,8192字节能跑到200MB/s以上,65536字节可以到230MB/s左右,但再往上提升就非常有限了。对于文件加密这种磁盘IO占比很高的场景,8KB到64KB都是合理范围。用8192是保守选项,想榨性能就用65536,代价是每条线程多占56KB内存,对现代机器可以忽略。
5. 避坑清单:RSA文件加密最常见的5个翻车现场
5.1 运行时报“Data must not be longer than 245 bytes”
现象:加密文件时报javax.crypto.IllegalBlockSizeException,提示超长。
原因:把整个文件的字节数组直接交给RSA的doFinal()。RSA算法本身只能处理比模数短的数据,2048位密钥配PKCS1Padding上限245字节,超过就报错。
解决:改用第3章设计的混合加密方案。如果因为某些特殊需求必须用RSA分块处理数据,要在代码里显式分块,每块长度控制在245字节以内,并且要考虑密文膨胀和ECB模式泄露结构的问题。
5.2 BadPaddingException:填充模式默认值与显式指定的不一致
现象:加密成功,解密时抛出javax.crypto.BadPaddingException,前面没有任何异常提示。
原因:最常见的写法是Cipher.getInstance("RSA"),没有明确指定填充模式。Java在不同JDK版本里对默认填充的实现不一致,早期是PKCS1Padding,某些新版本或带安全插件的环境中默认值可能不同。一旦加密时用的默认值和解密时用的默认值不是同一个填充方案,解密必然会失败。
解决:加解密两侧都显式写全转换字符串,"RSA/ECB/OAEPWithSHA-256AndMGF1Padding"写完整。这个字符串里的ECB不是分组密码里的ECB模式,而是RSA加密时的块处理方式,不影响语义。两侧保持一致,问题消失。
5.3 从存储介质读回公钥字符串后解析失败
现象:程序在同一个进程里能正常加解密,重启后读取public_key.pem文件,解析时报InvalidKeySpecException。
原因:PEM文件里Base64字符串可能被文本编辑器或Windows系统插入了换行符。Windows记事本会把每行末尾的\n变成\r\n,解析时如果没做空白字符过滤,Base64解码器就会遇到非法字符。
解决:解析PEM时用replaceAll("\s", "")把空白字符全部删除再解码。代码第4章里已经是这么处理的,注意\s是Java的正则写法,匹配空格、换行、制表符等所有空白。
5.4 同一次加密两个相同文件,密文看起来完全不一样,以为有bug
现象:拿同一个源文件和同一把公钥加密两次,输出文件二进制不相同,怀疑随机数生成器出了问题。
原因:这不是bug,是特性。OAEP填充本身引入了随机性,每次加密同一段明文会得到不同的密文;再加上每次加密都重新生成随机的AES会话密钥和IV,文件密文自然每次都不同。RSA和GCM的概率性加密是设计目标,目的是防止攻击者通过比对密文推断明文内容。
解决:验证加解密正确性时,不要用密文是否相同来比对,而是要解密后做哈希比对。我一般会在测试里计算源文件和解密后文件的SHA-256,一致才算通过。
5.5 解密后文件损坏:IV没有随密文持久化
现象:用同样的AES密钥解密两个不同文件,一个成功一个失败,失败的那个文件解开后半段全是乱码或直接报GCM校验失败。
原因:GCM模式下IV必须参与每次加密运算,解密时如果拿不到当初那12字节的IV,认证标签必然校验失败。代码里如果把IV写死在常量里或者每次解密都新建一个随机IV,那就完全对不上。
解决:IV不是机密,写入文件头即可。第4章的代码里把IV放在文件头字段中,先写IV长度再写IV内容,解密时先读出这些字段再初始化Cipher。
6. 交付前最后一步:自动化验证与性能基线
代码能跑通只是第一步。我见到太多项目在只能“演示一次”的状态就交付了,和真正的软件之间还缺一道自动化验证工序。这里给一个最小且实用的验证方法设计。
加密解密之后的完整性校验,必须基于哈希比对而不是肉眼。我的做法是写一个测试用例:随机生成一个5MB文件,用公钥加密,再用私钥解密,比对源文件和还原文件的SHA-256值,断言行相等。另外加一个负向测试:故意把加密文件的一个字节翻转,解密时必须抛出异常,证明GCM认证在起作用。
性能方面,建议记录两组基线的数字:一组是RSA单独加密AES密钥的耗时,期望在1到5毫秒;另一组是AES-GCM在8KB缓冲区下的吞吐量,期望在200MB/s左右。这两组数据写进项目README,用户看了一目了然。
做这个项目时,我最难受的一次翻车,是本地测试一切正常,换到Windows环境后密钥解析失败,最后定位到是\r\n换行符问题。从此以后,我写的所有PEM解析代码,第一件事永远是去掉全部空白字符。希望这篇笔记里那个踩坑清单,能帮你少走这一步弯路,也希望这个RSA文件加密方案能成为你工具库里真正用得上的东西。
本文还有配套的精品资源,点击获取