思路
针对文件加密特别是文件批量加密存在一个问题,就是如果对文件整体进行加密的话,文件越大则加密速度越慢,并且加密后的文件体积也会变大,基本上就是分段读取-加密-保存-删除原文件这种操作,解密也是一样的,时间开销比较大,不符合大家的需求。
那么就换一种思路,只读取文件的一部分,比如256个字节,然后对字节进行加密,再把加密后字节写回原来的位置,这样就无需对文件整体进行读写,自然速度也快,但是在二进制读取的方式下,从底层无法实现字节的查找替换,所以就要求加密前是256个字节的明文,那么加密后也一定得要是256个字节的密文才行。
能符合这种要求的加密算法有经典密码算法、RC4加密、SM4加密等,经典算法基本上没有安全性可言,RC4安全性较差,而SM4是国产加密算法,更好一些,SM4分为填充模式和无填充模式,这里我们只能选择无填充模式。
由于选择的是固定长度的加密,并且选择的是无填充模式,所以对于比读取明文长度更短的文件无法加密,比如读取256字节,而一个记事本文件100个字节,那么就会出现错误。
SM4加密解密实现
需要注意的是在引用BouncyCastle.Crypto一定要选择2.2.0版本的,1.8.0版本的需要自己实现无填充模式。
///
/// SM4加密
///
///
///
///
///
public static byte[] EncryptSM4(byte[] plaintext, byte[] key, byte[] iv)
{
byte[] input = plaintext;
IBufferedCipher cipher = CipherUtilities.GetCipher("SM4/CBC/NoPadding");
cipher.Init(true, new KeyParameter(key));
byte[] output = new byte[cipher.GetOutputSize(input.Length)];
int len = cipher.ProcessBytes(input, 0, input.Length, output, 0);
cipher.DoFinal(output, 0);
return output;
}
///
/// SM4解密
///
///
///
///
///
public static byte[] DecryptSM4(byte[] encrypted, byte[] key, byte[] iv)
{
IBufferedCipher cipher = CipherUtilities.GetCipher("SM4/CBC/NoPadding");
cipher.Init(false, new KeyParameter(key));
byte[] output = new byte[cipher.GetOutputSize(encrypted.Length)];
int len = cipher.ProcessBytes(encrypted, 0, encrypted.Length, output, 0);
cipher.DoFinal(output, len);
return output;
}
密钥和IV的生成,都是16个字符长度,通过生成指定长度的随机字符串来做密钥
///
/// 获取一个指定长度的不重复字符串
///
/// 指定的长度
/// 默认为true
/// 自定义字符串,当useLow为true时,它不起作用
///
public static string GetKey(int length, bool useLow, string custom)
{
byte[] b = new byte[4];
new System.Security.Cryptography.RNGCryptoServiceProvider().GetBytes(b);
Random r = new Random(BitConverter.ToInt32(b, 0));
string s = null, str = custom;
if (useLow == true) { str += "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ"; }
for (int i = 0; i < length; i++)
{
s += str.Substring(r.Next(0, str.Length - 1), 1);
}
return s;
}
文件读取和写入实现
从第10个字节开始,读取256个字节,前10个字节里包含有文件的类型、长度、版本等信息。在读取之前最好先判断下文件是否存在、长度信息、是否被其他进程占用等。
读取和写入的长度一定要一致,不然文件的内容在写入时会被覆盖掉,且无法恢复。
///
/// 读取文件中的256个字节
///
///
///
public static byte[] ReadFile(string path)
{
long startPosition = 10; // 从文件的第10个字节开始读取
long length =256; // 需要读取的字节数
string res = "";
byte[] bytes;
// 打开文件流
using (FileStream fileStream = new FileStream(path, FileMode.Open))
{
// 设置文件流的起始位置
fileStream.Seek(startPosition, SeekOrigin.Begin);
// 创建二进制读取器
using (BinaryReader binaryReader = new BinaryReader(fileStream))
{
// 读取指定长度的二进制数据
bytes = binaryReader.ReadBytes((int)length);
}
}
return bytes;
}
///
/// 向文件中写入256个字节
///
///
///
public static void WriteFile(string path, byte[] data)
{
long startPosition = 10; // 从文件的第10个字节开始写入
using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Write))
{
// 将文件指针移动到指定位置
fileStream.Seek(startPosition, SeekOrigin.Begin);
// 将数据写入文件
fileStream.Write(data, 0, data.Length);
}
}
加密、解密单个文件
在加密、解密文件时可以配合数据库一起使用,加密和解密使用的密钥要相同,如果要批量对文件进行加密、解密,那么就使用线程池来实现,避免用户界面无响应。
///
/// 使用普通方式加密文件中的一部分
///
/// 文件路径
/// 是否多线程
public void LockFile(string path,Boolean isThread)
{
byte[] key = SM4.StrToBytes(db.Key);
byte[] iv = SM4.StrToBytes(db.Iv);
//读取256字节数据
byte[] data = db.ReadFile(path);
byte[] Edata = SM4.EncryptSM4(data, key, iv);
db.WriteFile(path, Edata);
//更新数据库
string sql = "update 文件列表 set 状态='已加密' where 文件='" + path + "'";
db.ExecSQL(sql);
}
///
/// 使用普通方式解密文件,解密后的数据写入原文件
///
///
public void unLockFile(string path,Boolean isThread)
{
byte[] key = SM4.StrToBytes(db.Key);
byte[] iv = SM4.StrToBytes(db.Iv);
byte[] data = db.ReadFile(path);
byte[] Edata = SM4.DecryptSM4(data, key, iv);
db.WriteFile(path, Edata);
//更新数据库
string sql = "update 文件列表 set 状态='已解密' where 文件='" + path + "'";
db.ExecSQL(sql);
}
运行界面
在文件全部都处于未加密状态时,可以重置SM4密钥,每次重置时都随机生成一对全新的密钥。
在加密文本文件时,不管是否加密都可以打开,只是加密后打开的话文本内容部分是乱码了。