0%

用C#写一个最简单的文件加解密小工具

用C#编写最简单的文件加解密工具

接下来我们用C#编写命令行加解密工具, 顺便看看C#有没有加入好用的语法特性。

C#的发展速度非常快, 几乎加上了我所见过的所有语法特性, 要跟上巨硬的节奏非常不容易, 不过它是开创者。

比如可空类型就是C#首次捣鼓出来的, 当时学习的时候没在意, 后来经过kotlin的发扬光大后发现这语言特性这么香, 避免nullPointerException最好的办法就是尽量不用它。

所以有空可以关注一下C#的最新进展。

书归正传……

加解密原理

加解密文件实现原理就是替换大法, 比如有这样的字符串:

2023通过我们的加密后变成这样4245, 这样简单的原理适用于任何计算机文件, 因为文件就是一大串0-255的数字的集合……

我们对文件的每一个数字做一定的运算后得到加密后的文件, 对加密文件的每一个数字做逆运算后得到解密后的文件。

具体而言每一个数字加上3, 解密就是给每一个数字减去3, 那么这里有个问题如果得到的数据大于255不就溢出了么?

我们把0-255这256个值当作一个首尾相连的整体就很好解决溢出问题, 如果得到的值大于255就减去256, 得到的值小于0就加上256。

关于实现思路到此为止, 接下来编码实现!

小工具的上下文环境

我们通过命令行参数获取目标数据, 包括 动作要解密或加密, 文件路径, 其他必要信息。

处理完成后返回一个结果数据, 动作是否成功, 处理后的结果, 其他必要信息。

为此我们需要一个枚举类型叫Action表示程序的动作, 此外一个泛型类Context里面有三个泛型字段。

看如下代码:

Action枚举类型

enum Action
{
ENCRYPTION,
DECIPHERING,
HELP,
ERROR
}

加密、 解密; 帮助和错误, 很好理解。

Context类

class Context<T1, T2, T3>
{
public Context(T1 first, T2 second, T3 third)
{
this.first = first;
this.second = second;
this.third = third;
}
public T1 first { get; }
public T2 second { get; }
public T3 third { get; set; }
}

接下来在Context类的基础上需要两个类型, Target表示目标, Result表示结果。

我们这次用C#的顶层语句, 就像编写脚本语言一样, 不过需要注意的是我们把Action和Context放在源文件的最后。

using语句不变在源程序的开头, 下来是调用main函数, 中间是其他函数定义。

Target和Result

using Target = Context<Action, string, string>;
using Result = Context<bool, byte[], string>;

这样我们得到了两个上下文类型Target的三个字段分别表示Action动作, string文件路径, string必要信息。

Result的三个字段表示bool处理是否成功, byte数组表示处理结果, string相关信息。

这样一来我们可以使用普通的类一样用Result和Target类型就可以了。

小工具可以这样搞first, second和third不会容易搞混, 如果面对较大项目的时候这样的技巧慎用!

还是老老实实用success, secretContent和msg这样的字段名。

这就是C#的表现力, 本来需要定义类似的两个类型, 现在直接用一个泛型类搞定, 顺便感叹C#using语句的功能特别多, 列举一下我所知的功能列表:

  • 引用命名空间

  • 重命名命名空间类型等

  • 安全释放资源

  • 定义新的类型, 存疑

    这样一来我们的准备工作已经顺利完成, 接下来顺着程序的运行轨迹编码实现就可以了。

    using语句后就是我们的main函数调用

main(args);

需要注意的是在顶层语句我们直接使用args变量即可, 无需额外动作……

接下来看看main函数是如何运行的。

Main函数

void Main(string[] querys)
{
Target target = ParseTarget(querys); // 解析命令行参数
// 开始处理
Result result = target.first switch {
Action.ENCRYPTION => Encryption(target),
Action.DECIPHERING => Deciphering(target),
_ => new Result(false, new byte[0], target.third)
};
// 扫尾, 我们的加密后的文件扩展名是.sec
try
{
string path = target.second;
path = target.first == Action.DECIPHERING? path.Substring(0, path.Length - path.LastIndexOf(".sec")): $"{path}.sec";
File.WriteAllBytes(path, result.second);
}
catch (Exception e)
{
result.third = e.Message;
}
Console.WriteLine(result.third);
}

这里需要注意的是switch语句, 这样的语法可以去掉case; break, 当然你的case语句有多行的话还是用回传统的编写方式。

生成Target对象

// 接受一个字符串数组, 返回Target对象
Target ParseTarget(string[] querys)
{
return querys.Length switch {
0 or 1 => new Target(Action.HELP, "", "用法 选项 文件名\n-e --encryption 文件名 加密该文件\n-d --decipherion 文件名 解密文件"),
2 => querys[0].ToLower() switch {
"-e" or "--encryption" => new Target(Action.ENCRYPTION, querys[1], "加密文件"),
"-d" or "--deciphreing" => new Target(Action.DECIPHERING, querys[1], "解密文件"),
_ => new Target(Action.ERROR, "", "错误选项")
},
_ => new Target(Action.ERROR, "", "错误选项")
};
}

这次是更复杂的switch语句。

命令行参数解析的不算特别精细不过也大差不差。

接下来程序开始调用加解密函数, 特别简单如下所示:

加解密函数

Result Encryption(Target target)
{
// 读取目标文件, first表明是否成功
Result result = ReadBytes(target);
if (!result.first)
{
return result;
}
calculate(result.second, 10); // 真正加密
return result;
}

Result Deciphering(Target target)
{
Result result = ReadBytes(target);
if (!result.first)
{
return result;
}
// 真正解密, 加密就是往上加, 解密就是往下减
calculate(result.second, -10);
return result;
}

接下来是读取文件的函数:

Result ReadBytes(Target target)
{
try
{
var content = File.ReadAllBytes(target.second);
return new Result(true, content, "Ok");
}
catch (Exception e)
{
return new Result(false, new byte[0], e.Message);
}
}

最后是核心功能, 也特别简单:

void calculate(byte[] buf, int num)
{
// 计算文件的所有字节
for (int i = 0; i < buf.Length; i++)
{
int b = buf[i] + num;
// 保证不溢出
buf[i] = b switch
{
> 255 => (byte)(b - 256),
< 0 => (byte)(b + 256),
_ => (byte)b
};
}
}

编写一个玩具文件加解密工具就是这么简单轻松。

编译运行看看效果。

> dotnet run -e hello.txt
Ok
> dotnet run -d hello.txt.sec
Ok

最后

这样的工具还有哪些可以优化的空间?

至少两个方面需要继续做。

1.友好明确的错误提示尤其是这样的用法

> secret.exe -d hello.txt
Index and length must refer to a location within the string. (Parameter 'length')

这样的提示只有我们自己知道啥情况, 过了几个月恐怕自己也搞不清这意味着什么!

2.加入口令, 为了实现简单可以限制密码的长短和类型, 比如必须提供六位数字等。

不怕挑战可以用任意长度和类型的任何密码, 只是算法实现有难度……