前置知识
异或运算(以下异或用“⊕”代替)
A | B | A ⊕ B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
异或的一些规律
A⊕0=A
B⊕B = 0
满足交换律:A⊕B⊕C = A⊕(B⊕C)
所以:(A⊕B)⊕B = A⊕(B⊕B) = A
有什么用呢?
假设A是原文件,我们随便搞个B文件出来(与A文件等长度),加密过程就是把A变成A⊕B,解密过程就是再异或一遍B,重新得到A,这里的B就是我们的秘钥(key)
主体思路
大致流程
思路
由于原文件是不定长的,而秘钥需要与原文件严格等长,我的解决方案是用一个无限长序列(比如斐波拉切数列)代替秘钥:
struct Key{
char a = 1;
char b = 1;
char next(){
char c = a+b;
a = b;
b = c;
return c;
}
};
当然这样的话就被写死了,我们可以传个char进来作为a的初始值:
struct Key{
char a,b;
Key(char x){
a = x;
b = 1;
}
char next(){
char c = a+b;
a = b;
b = c;
return c;
}
};
不过这样变化还是太少了,只有256种秘钥,那我们稍微搞复杂点:
把a和b该成long long的,就有18446744073709551616(2^64)种秘钥了
然后初始值a由一个字符串决定,这样我们就有模有样地搞出了密码这个功能。
不过虽然可以通过main函数传参的方式实现了拖拽文件到.exe上“用XXXX打开”的效果:
但是这样输入还是有点麻烦,站内找到
前辈“文件选择器”的实现(原文链接):
string openDlg(){
TCHAR szBuffer[MAX_PATH] = {0};
OPENFILENAME ofn= {0};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = NULL;
ofn.lpstrFilter = ("所有文件(*.*)\0*.*\0");//要选择的文件后缀
ofn.lpstrInitialDir = ("C:\\Program Files");//默认的文件路径
ofn.lpstrFile = szBuffer;//存放文件的缓冲区
ofn.nMaxFile = sizeof(szBuffer)/sizeof(*szBuffer);
ofn.nFilterIndex = 0;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER ;//标志如果是多选要加上OFN_ALLOWMULTISELECT
BOOL bSel = GetOpenFileName(&ofn);
return szBuffer;
}
缝合上去,这样就稍微“现代”一点了:
完整代码
#include <bits/stdc++.h>
#include <bits.h>
#include <conio.h>
#include <unistd.h>
using namespace std;
const string passwordSet = " aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ0123456789";
struct Key{
long long a,b,i;
Key(string s) {
a = 0;
for(int i=0; i<s.size(); i++){
a *= passwordSet.size();
a += passwordSet.find(s[i]);
}
b = 1;
i=0;
}
char next(){
long long c = a+b;
a = b;
b = c;
i++;
char ret = b>>((i%8)*8);
return ret;
}
};
string openDlg(){
TCHAR szBuffer[MAX_PATH] = {0};
OPENFILENAME ofn= {0};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = NULL;
ofn.lpstrFilter = ("所有文件(*.*)\0*.*\0");//要选择的文件后缀
ofn.lpstrInitialDir = ("C:\\Program Files");//默认的文件路径
ofn.lpstrFile = szBuffer;//存放文件的缓冲区
ofn.nMaxFile = sizeof(szBuffer)/sizeof(*szBuffer);
ofn.nFilterIndex = 0;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER ;//标志如果是多选要加上OFN_ALLOWMULTISELECT
BOOL bSel = GetOpenFileName(&ofn);
return szBuffer;
}
int main(int strlen, char *str[]){
string fileName;
if (strlen>=2){
fileName = str[1];
}else{
printf("请选择你需要加/解密的文件:\n");
fileName = openDlg();
}
FILE *fp = fopen(fileName.c_str(), "rb+");
if(fp){
cout << "请输入加/解密密码:";
string password;
while(1){
cin>>password;
bool ok = true;
for (int i = 0; i < password.size(); ++i){
if(passwordSet.find(password[i])==string::npos){
ok = false;
}
}
if(ok){
break;
}else{
printf("密码只能由数字和字母大小写组成\n");
printf("请重新输入加密密码:\n");
}
}
Key key(password);
int fileSize = max(50L, filelength( fileno(fp) ) );
cout << "!!!加/解密即将开始,过程中请勿退出此程序,否则将损害该文件!!!" << endl;
cout << "是否继续(y/n)" << endl;
while(getch()!='y');
int progressCounter = 0;
char ch;
for(int i=0; i<50; i++){
putchar('-');
}putchar('\n');
while(fread(&ch, sizeof(ch), 1, fp)){
if(++progressCounter%(fileSize/50) == 0){
putchar('-');
int printCnt = printf("(%d/%d)", progressCounter, fileSize);
while (printCnt--){
putchar('\b');
}
}
ch ^= key.next();
fseek(fp, -1L, SEEK_CUR);//向前移位
fwrite(&ch, sizeof(ch), 1, fp);
fseek(fp, 0L, SEEK_CUR);//切换读写模式
}
fclose(fp);
cout << endl;
printf("文件'%s'加/解密完成\n", fileName.c_str());
}else{
printf("文件'%s'打开失败\n");
}
printf("按任意键退出");
getch();
return 0;
}
效果
原文件:
输入密码123456进行加密:
加密后:
再解密回来(其实和加密过程就是一样的)
解密完毕,圣经回来啦
同样的图片、视频等其他文件也是一样的,加密过就会乱码或者无法打开(不然勒),解密后就能回来(废话):
加密前:
加密后:
P.S.
前辈的代码里”添加到右键菜单”的,缝合进去的话可以实现这样的效果:
直接右键需要加/解密的文件,点击菜单里的“加/解密”
不过那段代码有点长,缝合进来的话代码量一下子就堆上去了,为了保持代码的简洁性,这里就不加了(这个借口应该还可以)。
如果解密时密码输错了会怎么样?
源文件A经密钥B异或成A⊕B,又经过错误的密钥C异或,也就变成的A⊕B⊕C,要重新得到源文件A,我们需要再对密钥B和C分别异或一遍(由于异或运算满足交换律,所以顺序随意)。