C语言实现棋盘加密与凯撒加密:古典密码工具实战详解
简介这是一份基于C语言实现的棋盘加密与凯撒加密解密代码包面向密码学初学者与C语言编程爱好者可用于理解两种古典密码的编码原理与实现细节。棋盘加密Playfair Cipher采用5x5棋盘对明文双字母分组进行替换凯撒加密则通过固定移位完成加解密代码中通过数组、循环与条件语句呈现了完整的加密和解密流程配合txt说明文件便于对照算法逐步学习。压缩包内共2个文件包含1个cpp源文件与1个txt说明文档资源整体仅2KB结构精简、上手门槛低。目前已有2156人学习下载。阅读运行后读者既能掌握棋盘密码与凯撒密码的构造方法也能加深对字符处理、数组索引和条件分支等C语言基础知识的理解适合作为密码学入门和编程练习的参考。1. 棋盘加密与凯撒加密解密C代码一套能直接编译跑的古典密码工具拿到这份代码的第一个直觉是“凯撒加密不是几行就写完吗”但把棋盘加密和凯撒加密放进同一个C工程里事情就没那么简单了。棋盘加密用到5x5方格和I/J合并坐标映射的边界条件很多凯撒加密看似只有移位真正写起来会遇到负数取模、大小写混合、非字母字符跳过等一系列问题。这个项目把两种经典加密算法做成了统一的命令行工具支持加解密、自定义shift、处理带空格的完整句子适合正在学密码学基础、准备CTF、或者想用C语言快速搭一套加密原型的开发者。代码结构清晰核心函数独立拿回去改成自己的工具链也很快。2. 棋盘加密模块拆解5x5 方格生成与 I/J 合并策略棋盘加密也叫Polybius方格原理是把26个英文字母塞进一张5x5的表格每个字母用所在的行号和列号表示。5行5列一共25格字母却有26个所以必须合并一对字母最常见的做法是把I和J放进同一格。这一章我按照源码的模块顺序来拆先把表格初始化看明白再看加密和解密两条链路。2.1 棋盘方格初始化源码把棋盘数据直接定义成一张静态二维数组而不是程序运行时动态生成。这样做的好处是表格内容一眼可见改动时只需要换数据表逻辑代码不用碰。我摘出初始化部分#define GRID_SIZE 5 // 标准 Polybius 方格I/J 合并 static const char polybius_table[GRID_SIZE][GRID_SIZE] { {A,B,C,D,E}, {F,G,H,I,K}, {L,M,N,O,P}, {Q,R,S,T,U}, {V,W,X,Y,Z} }; // 将字符映射到棋盘坐标成功返回1失败返回0 int polybius_encode_char(char ch, int *row, int *col) { if (ch a ch z) ch ch - a A; // 统一大写 if (ch J) ch I; // I/J 合并 for (int r 0; r GRID_SIZE; r) { for (int c 0; c GRID_SIZE; c) { if (polybius_table[r][c] ch) { *row r 1; // 坐标从1开始方便人读 *col c 1; return 1; } } } return 0; // 非字母字符比如空格、数字返回失败 }这段代码的逻辑非常直白先把小写字母统一转成大写然后对J做特殊处理把J强行替换成I。这里要注意一个容易忽略的点——polybius_encode_char只负责单字符映射真正加密整行文本时需要在外面循环调用。函数返回1表示这个字符能在棋盘上找到返回0则说明是标点、空格或数字调用方拿到0就可以决定是跳过还是原样保留。行和列都从1开始计算因为市面上大多数棋盘加密教材都把坐标写成1到5从0开始会让其他人读输出时怀疑自己的实现。这段代码里还有一个设计细节值得学习静态表用了static const修饰放在函数外。const保证表不会被意外修改static限制作用域只在当前编译单元避免头文件被多次包含时产生重复定义。我一般会沿用这种写法把密码学里需要固定的置换表都声明为static const。2.2 棋盘加密函数实现拿到单字符的坐标以后下一步就是把整行明文转换成一串坐标输出。源码里对输出格式的定义是“行号列号连写坐标之间用空格分隔”比如字母A加密成11B是12。这种格式的好处是人眼可读密文可以直接复制到命令行里再解密不会因为分隔符产生歧义。// 加密一行输出格式为 row col row col ...坐标之间用空格隔开 void encrypt_polybius_line(const char *plain, char *cipher_out, int out_size) { int row, col; int pos 0; cipher_out[0] \0; for (int i 0; plain[i] ! \0 pos out_size - 4; i) { if (polybius_encode_char(plain[i], row, col)) { pos snprintf(cipher_out pos, out_size - pos, %d%d , row, col); } // 非字母字符直接忽略不产生输出 } }加密函数的关键在于缓冲区边界控制。snprintf每次写入前都会传入剩余长度防止坐标越界把内存写坏。循环条件里留了4个字符的余量因为单个坐标最多占3个字符两位数字加一个空格算上末尾的结束符一共4个。如果你自己重写这个函数我建议也这样预留否则明文很长且全是字母时最后一个坐标很容易把\0挤掉导致解密端读到一堆垃圾。关于输出缓冲区的大小我一般按明文字符数乘3再加1来分配。最坏情况下每个字母变成两个数字加一个空格正好3个字节如果明文里混有空格和标点被跳过实际输出会更短。所以3倍长度是最稳妥的上界不会浪费太多内存。2.3 棋盘解密函数实现解密是加密的逆过程输入的密文形如23 15 31 31 34需要把每组两位数字拆成行号和列号再去表格里查对应的字母。解密比加密更容易踩坑的地方在于密文可能被人为添加了空格、换行甚至混入了其他字符。源码的处理策略是遇到非数字字符就跳过只有两位连续的1到5之间的数字才被当作坐标解析。// 解密一行连续两位数字视为 row*10col遇到非数字字符跳过 void decrypt_polybius_line(const char *cipher, char *plain_out, int out_size) { int row, col; int pos 0; plain_out[0] \0; for (int i 0; cipher[i] ! \0 pos out_size - 1; i) { if (cipher[i] 1 cipher[i] 5) { if (i 1 (int)strlen(cipher) || cipher[i1] 1 || cipher[i1] 5) { // 后面没有合法数字直接跳过 continue; } row cipher[i] - 0; col cipher[i1] - 0; // 查表行从1开始所以索引要减1 plain_out[pos] polybius_table[row-1][col-1]; i; // 跳过第二个数字 if (pos out_size - 1) plain_out[pos] \0; } // 空格和分隔符被跳过 } plain_out[pos] \0; }这里我重点讲两个判断。第一个判断是cipher[i] 1 cipher[i] 5它把解析范围限定在棋盘坐标合法的1到5之间这样14A 15这种脏输入不会把A当作第二个数字A会被忽略。第二个判断是i 1 strlen(cipher)防止读取到字符串末尾之后的内存。由于解密后要写入plain_out每次写入后立刻补\0也是一种好习惯避免中途跳出循环时缓冲区残留旧数据。解密输出会保留I/J合并的状态。比如密文24 24解出来是I而这个字母在实际明文里可能原本是J。这是棋盘密码5x5方案的固有权衡不是程序bug。如果想区分I和J必须改用6x6方格或其他变体这点我在后面的排查章节里会展开。到这里棋盘模块就闭环了加密把字母压缩成坐标解密把坐标恢复成字母加解密共用同一张静态表。想要换成4x4或6x6的棋盘只需要改GRID_SIZE和polybius_table两张定义函数主体几乎不用动。3. 凯撒加密模块拆解移位取模与大小写保持凯撒加密的核心思想是“字母表循环平移”每个字母往后移动固定的位数。表面上看用ch shift再取模就行但C语言里这个操作藏着不少细节负数移位、大小写混合、非字母字符如何跳过每一处处理不好都会让加解密结果对不上。这一章把凯撒模块的代码拆开讲重点放在为什么这么写。3.1 单字符移位核心先看最底层的单字符移位函数整个凯撒模块都是围绕它构建的// 对单个可见字符做凯撒移位shift 可以为负 char caesar_shift_char(char ch, int shift) { if (ch A ch Z) { return (char)(A (ch - A shift) % 26); } if (ch a ch z) { return (char)(a (ch - a shift) % 26); } return ch; // 非字母原样返回 }很多初学者会直接写(ch shift) % 26这在ch是Z且shift为1时碰巧能得到A但只要shift超过26或者为负就翻车了。正确做法是先用ch - A算出字母在字母表中的真实位置这个位置是0到25之间的整数加上偏移后再对26取模最后加回基准ASCII码。这样无论偏移多大结果都稳定落在字母区间内。函数最后一行return ch处理的是非字母字符。空格、数字、标点都会被原样保留不会因为参与移位变成乱码。这个策略很符合实际加密习惯——你加密一段英文句子时通常希望空格和标点保持原样只对字母做变换否则解密出来的文本会丢失单词边界。源码里把这个函数设计成独立单元也方便单测我在自己工程里也喜欢把“字符级操作”和“字符串级操作”分成两层。3.2 字符串级加解密接口单字符能移位了但真正给用户用的是整行文本处理。源码提供caesar_encrypt_line和caesar_decrypt_line两个接口。注意解密函数没有单独写一套反向循环而是直接复用加密函数这一点值得不少项目学习// 处理用户输入的负数 key转为正偏移 int normalize_shift(int shift) { shift % 26; if (shift 0) shift 26; return shift; } void caesar_encrypt_line(const char *plain, int shift, char *out) { shift normalize_shift(shift); int i 0; while (plain[i] ! \0) { out[i] caesar_shift_char(plain[i], shift); i; } out[i] \0; } void caesar_decrypt_line(const char *cipher, int shift, char *out) { // 解密是加密的逆运算等价于移位 26 - shift caesar_encrypt_line(cipher, 26 - normalize_shift(shift), out); }先看normalize_shift。C语言的%运算符对负数的结果同样为负比如-3 % 26得到的是-3如果不处理加密时会算出负数加回基准ASCII后就成了字母表之外的符号。归一化的做法是先把shift对26取模结果如果是负数就再加26把它拉回0到25的区间。这一步做得干净利落后面所有调用都不用再担心负数问题。caesar_decrypt_line复用加密函数的技巧很有意思解密一个移位为shift的密文等价于加密时使用26 - shift的移位。因为字母表26个字母向前移动shift位再向后移动shift位就能回到原点。这个方法让解密逻辑和加密逻辑保持完全一致减少了代码重复也降低了“两套逻辑各写一遍导致不一致”的风险。输入空字符串时while循环不会执行out[i]直接写\0所以空串也能安全处理。3.3 主程序模式编排棋盘模块和凯撒模块都就绪后主程序像调度器一样根据参数分发到不同功能。源码采用命令行参数方式而不是写死在代码里这样方便自动化测试和脚本调用。主程序的分派逻辑如下int main(int argc, char *argv[]) { if (argc 4) { printf(用法: %s mode shift text\n, argv[0]); printf(mode: 1凯撒加密 2凯撒解密 3棋盘加密 4棋盘解密\n); return 1; } int mode atoi(argv[1]); int shift atoi(argv[2]); // argv[3] 之后的参数用空格拼接成完整原文 char text[1024] {0}; for (int i 3; i argc; i) { strcat(text, argv[i]); if (i argc - 1) strcat(text, ); } // 根据 mode 分派到不同模块 ... }这个设计最大的好处是解决了带空格文本的传入问题。如果只在argv[3]取字符串那么./crypto_tool 1 3 Hello World经过shell解析后Hello和World会分成两个参数只取argv[3]就会丢掉World。源码的做法是把argv[3]到argc-1全部拼接起来中间用空格还原成完整句子。模式1和模式2共用凯撒模块模式3和4共用棋盘模块其中棋盘模式会忽略shift参数所以调用时只要传任意整数占位即可。atoi在这里要留意一个默认行为如果用户传了非数字字符atoi会解析为0。在凯撒加密中shift为0意味着不加密输出等于输入这有可能让用户误以为程序坏了。遇到这种情况更稳的做法是在atoi之后检查原字符串的首字符是不是数字不过源码选择了保持简洁这个取舍我会在后面的排查章节里补充说明。4. 编译运行与参数设计从 gcc 到命令行交互代码写得再漂亮编译不通过或者运行参数用错前面的努力都白费。这一章带你从拿到源码到跑通四种模式包括编译命令的含义、参数表、以及三组必测的自测样例。如果你拿到的是单文件版本一条gcc就够如果是分文件工程我会额外说明多文件编译方式。4.1 编译与目录结构我习惯先看目录里有哪些文件再决定编译方式。典型的工程结构长这样polybius_caesar/ ├── crypto_tool.c # 主程序包含棋盘与凯撒全部逻辑 ├── crypto_tool.h # 函数声明和常量定义单文件版则集成在 .c 里 └── README.md # 使用说明单文件版编译最省心直接在源码目录执行gcc -O2 -Wall -o crypto_tool crypto_tool.c-Wall我非常建议保留它能打开几乎所有常见警告。这个项目里最容易触发的是strcat缓冲区溢出提示和隐式类型转换比如strlen返回size_t和int比较时的符号警告。看到警告不要急着关掉先定位到具体行通常能揪出潜在的数组越界。-O2是优化选项对这种小项目提升不明显但写上无妨也不会拖慢编译速度。如果源码被拆分成caesar.c、polybius.c、main.c多个文件编译命令需要把所有.c文件一起传给gccgcc -O2 -Wall caesar.c polybius.c main.c -o crypto_tool头文件里放函数声明和常量定义每个.c文件#include对应的头文件主程序负责解析参数和调用接口。这种分文件结构适合后续二次开发比如你想给凯撒模块单独写单元测试就不需要把主程序拉进来一起编译。4.2 运行参数说明程序支持四个模式分别对应两组算法。参数顺序固定为mode shift text其中棋盘模式会忽略shift参数但占位符必须传否则argc数量不够会直接报用法提示。我把四个模式整理成一张参数表模式含义示例命令输出结果1凯撒加密按整数移位./crypto_tool 1 3 HELLOKHOOR2凯撒解密按整数反向移位./crypto_tool 2 3 KHOORHELLO3棋盘加密忽略shift./crypto_tool 3 0 HELLO23 15 31 31 344棋盘解密忽略shift./crypto_tool 4 0 23 15 31 31 34HELLO凯撒模式里shift可以是任意整数源码内部会做归一化所以传负数也合法。棋盘模式的密文用空格分隔坐标整个串可以直接作为第五个参数传入解密命令shell会把它当作一个整体因为密文内部空格已经被引号包住了。4.3 功能自测样例拿到代码第一件事不是直接跑业务数据而是先跑三组自测确认核心链路没被改坏。我每次拆项目都会按“往返测试 边界测试”的方式验证这次也不例外# 凯撒往返加密再解密应还原 ./crypto_tool 1 10 Attack at dawn /tmp/caesar.enc ./crypto_tool 2 10 $(cat /tmp/caesar.enc) # 期望输出: Attack at dawn # 棋盘往返 ./crypto_tool 3 0 HELLO /tmp/polybius.enc ./crypto_tool 4 0 $(cat /tmp/polybius.enc) # 期望输出: HELLO # 边界测试shift26 时加密结果等于原文 ./crypto_tool 1 26 ABCDE # 期望输出: ABCDE第三组测试意义特殊shift26相当于绕字母表整整一圈加密结果应该和原文完全一样。如果实现里写成了(ch shift) % 26而没有先减基准这个测试会输出意料之外的字符。我第一次写凯撒加密时就因为在减基准上偷懒导致Z加1变成了[后来就是靠这组边界测试抓出来的。往返测试也最好每次都跑因为解密函数如果复用了加密函数一旦加密逻辑坏了解密也会跟着坏往返测试能一次性暴露两端的问题。5. 常见问题排查取模越界、空格读取与脏字符过滤这是我实际运行这份代码时遇到过的几类问题有些是源码设计带来的默认行为有些是我自己改造时踩进去的。按“现象 → 原因 → 解决”的格式写方便你对照排查。5.1 解密输出乱码出现方括号和反引号现象凯撒解密时输入KHOOR配 shift3输出不是HELLO而是KHOOR前面多了几个不可见字符或者出现[、这类符号。原因取模时没有先减去基准。如果写成(K 3) % 26K的ASCII码是7575加3等于7878对26取模等于0问题在于这里用的是ASCII码而不是字母序号。更隐蔽的是负数移位时C语言的%结果也可能是负数强转成char后高位被解释成负值输出乱码。解决统一使用ch - A先得到0到25的字母序号做完移位取模后再加回基准。同时在加密入口调用normalize_shift把负数偏移转为正偏移。如果你拿到的版本没有这个归一化函数自己加一行shift ((shift % 26) 26) % 26;就能覆盖所有正负偏移的情况。5.2 命令行传带空格文本时只拿到第一个单词现象执行./crypto_tool 1 3 Hello World输出只有Khoor后面的World丢失了。原因shell解析后Hello和World是两个独立参数如果程序只读取argv[3]自然只拿到第一个单词。很多C语言入门项目都栽在这因为最初练习时传的空格文本不多等到真实使用才发现问题。解决参考源码主程序的写法把argv[3]到argc-1全部拼接参数之间用空格还原。如果想彻底避开命令行解析可以给程序增加一个交互模式用fgets从标准输入读取整行。我一般在保留命令行拼接的同时额外加一个-i参数进入交互模式两边都能照顾到。5.3 棋盘解密时把 J 解成了 I现象棋盘加密输入JAVA解密回来变成IAVA字母J消失了。原因5x5 棋盘只有25格26个字母必须合并一组源码选择合并 I/J。加密时J被映射到I的坐标解密自然只能得到I这是算法定义使然不是实现错误。解决如果业务要求区分I和J需要把棋盘改成6x6多出的格子可以放数字或直接放置J。改动量不大只需要换掉polybius_table这张表加密和解密函数的主干都不用动。我在做CTF题时遇到不提示变体的棋盘密文一般先用I/J合并版解一遍再手动枚举所有J的位置替换成I看哪种组合语义通顺通常很快能找到答案。5.4 密文里混入换行符解密结果尾部多出空白现象从文件读取棋盘密文每行末尾有\r\n解密函数输出比预期多出空格或异常字符。原因Windows和Linux的换行符不同\r会被当作普通字符进入解析流程。虽然解密函数会对非数字字符跳过但有些版本在循环终止条件上用了strlen逐次求值遇到\r后继续读取可能越界。解决读取文件后先做净化用strtok按空白字符切分坐标每个token都是两位数字然后逐个解析。这样换行符、回车符、多层空格都不会干扰。如果不想引入strtok的状态破坏问题也可以逐字符判断遇到\r或\n直接跳过效果相同。5.5 UTF-8 中文环境编译后数组越界崩溃现象把英文和中文混在一起输入程序直接崩溃或者输出一段乱码。原因中文字符在UTF-8下占3个字节每个字节的最高位都是1char类型在多数平台是有符号的解析时cipher[i] 1 cipher[i] 5判断失败但循环仍在推进遇到二进制补码表示的负值后续内存访问可能越界。解决把所有字符比较改成(unsigned char)强转或者在加密入口对大于127的字节直接跳过。源码的字符处理函数最好声明成const unsigned char *这样每次取到的都是无符号值高位不会被当作符号位。中文混英文的场景虽然不常见但一旦遇到就是崩溃级问题值得提前预防。6. 二次开发实战给凯撒模块加暴力破解和词频评分凯撒加密只有25个有效密钥根本不需要手工一个一个试写个循环就能自动找到最优解。我拿到这份源码后做的第一个改造就是给凯撒模块加暴力破解从1到25遍历所有可能的shift每次解密后用词频统计打分分数最高的就是最可能的明文。核心代码贴在下面// 简单词频评分给常见英文字母加权返回总分 int score_text(const char *text) { const char *freq ETAOINSHRDLU; int score 0; for (int i 0; text[i]; i) { char c text[i]; if (c a c z) c - a - A; if (c A c Z strchr(freq, c)) score 3; if (c ) score 1; } return score; } void brute_force_caesar(const char *cipher) { char plain[1024]; int best_shift 0, best_score -1; for (int s 1; s 25; s) { caesar_decrypt_line(cipher, s, plain); int sc score_text(plain); printf(shift%2d score%3d %s\n, s, sc, plain); if (sc best_score) { best_score sc; best_shift s; } } printf(Best shift: %d\n, best_shift); }评分函数用了最简单的加权策略高频字母集合命中的加3分空格加1分。空格之所以加分是因为解密正确时单词边界会恢复空格数量也会接近正常英文的分布。你如果想让评分更准可以把freq替换成真实英文频率表给E打12分、T打9分区分度会更高。短句子可能误判但作为破解入门足够用。另一个我常做的改动是把棋盘密码的I/J合并做成可配置开关。只需加一个全局变量static int keep_j_separate 0; // 1 时使用不合并的 6x6 表然后在polybius_encode_char和查表函数里判断这个开关为0时走5x5表为1时走6x6表。这样一个二进制就能应对不同来源的棋盘密文不用重复编译。从那以后我每次拿到别人的古典密码源码都会先写一轮往返测试和边界测试确认取模和空格两条主链路没问题再动手加新功能。这次拆棋盘与凯撒加密C代码最值钱的收获不是那几行加解密函数而是养成了一套“还原验证”的手感。希望帮到你。本文还有配套的精品资源点击获取