什么是凯撒密码
凯撒密码最早由古罗马军事统帅盖乌斯·尤利乌斯·凯撒在军队中用来传递加密信息,故称凯撒密码。这是一种位移加密方式,只对26个字母进行位移替换加密,规则简单,容易破解。
若将明文字母表向后移动3位:
明文 | X | Y | Z | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
密文 | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z |
则A变成了D,B变成了E……,Z变成了C。
字母表最多可以移动25位。凯撒密码的明文字母表向后或向前移动都是可以的,通常表述为向后移动,如果要向前移动1位,则等同于向后移动25位,位移选择为25即可。
加密解密方式
纵观常用的在线加解密网站,会发现通常有两种实现:
- 不论大小写,均统一成其中一种进行加解密输出,key值限定为非负整数;
- 大写字母若发生进位,变成小写;小写字母若发生进位,变成大写。
关键思路
假设 f(x) 为当前字母在字母表[a-z]中的位置,key为偏移量,z为字母表中z的位置,那么:
加密过程
key为非负整数,a 偏移到z 后,会再从a 继续开始偏移,故循环周期为字母表长度26
key = key Mod 26;
f(x) = f(x) + key > z ? f(x) + key -26 : f(x) + key
解密过程
key为非负整数,z 回退到a 后,会再从z继续回退偏移,故循环周期为字母表长度26
key = key Mod 26;
f(x) = f(x) - key < a ? f(x) - key + 26 : f(x) - key
方案1:忽略大小写,key值限定为非负整数
// 加密,需要明文(plaintext)和密钥(key)作为参数
function encryptCaesar(plaintext, key) {
// 校验key值有效性——非负整数
if (!(/^0|[1-9]\d*$/.test(key))) {
return new Error('位移量请输入非负整数');
}
// 字符串分解成数组
let arr = plaintext.toString().split('');
let charCodeOf_a = 'a'.charCodeAt();
let charCodeOf_z = 'z'.charCodeAt();
key = key % 26;
for (let i = 0; i < arr.length; i ++) {
// 当前字符小写字母的ASCII码值
let currentChar = arr[i].toLowerCase().charCodeAt();
if (currentChar >= charCodeOf_a && currentChar <= charCodeOf_z) {
// 偏移量,判断是否加密后回到[a-z]头部
let currentOffset = currentChar + key - charCodeOf_z;
if (currentOffset > 0) {
currentChar -= 26;
}
currentChar += key;
}
arr[i] = String.fromCharCode(currentChar);
}
return arr.join('');
}
encryptCaesar('Didi Family', 10); // "nsns pkwsvi"
// 解密,需要密文(ciphertext)和密钥(key)作为参数
function decryptCaesar(ciphertext, key) {
// 校验key值有效性——非负整数
if (!(/^0|[1-9]\d*$/.test(key))) {
return new Error('位移量请输入非负整数');
}
// 字符串分解成数组
let arr = ciphertext.toString().split('');
let charCodeOf_a = 'a'.charCodeAt();
let charCodeOf_z = 'z'.charCodeAt();
key = key % 26;
for (let i = 0; i < arr.length; i ++) {
let currentChar = arr[i].toLowerCase().charCodeAt();
if (currentChar >= charCodeOf_a && currentChar <= charCodeOf_z) {
// 偏移量,判断是否解密后回到[a-z]尾部
let currentOffset = currentChar - charCodeOf_a - key;
if (currentOffset < 0) {
currentChar += 26;
}
currentChar -= key;
}
arr[i] = String.fromCharCode(currentChar);
}
return arr.join('');
}
decryptCaesar('"nsns pkwsvi"', 10) // "didi family"
以上代码分别实现了不考虑大小写的情况下加密和解密的过程,当涉及到大小写变换的时候,情况变得稍微复杂些:
加密过程
key为非负整数,a偏移到z后,变为[A-Z]循环,需要改变字母大小写
cycle = (f(x) + key) / 26
当cycle为奇数时,需要变换大小写;cycle为偶数时,可以不用变换大小写
f(x) = (f(x) + key) % 26
if (cycle % 2) f(x) = f(x).toUpperCase();
解密过程
key为非负整数,Z回退到A后,变为[z-a]循环,需要改变字母大小写
cycle = (f(x) - key) / 26
当cycle为偶数时,需要变换大小写;cycle为奇数时,不用变换大小写
f(x) = (f(x) - key) % 26
if (!(cycle % 2)) f(x) = f(x).toLowerCase();
方案2:区分大小写,key为非负整数
// 加密,需要明文和密钥
function encryptCaesar(plaintext, key) {
if (!(/^0|[1-9]\d$/.test(key))) {
return new Error('请输入非负整数');
}
let arr = plaintext.toString().split('');
let charCodeOf_a = 'a'.charCodeAt(); // 小写字母a的ASCII码值
let charCodeOf_A = 'A'.charCodeAt(); // 大写字母A的ASCII码值
for (let i = 0; i < arr.length; i ++) {
let currentChar = arr[i].charCodeAt(); // 当前字母的ASCII码值
let cycle = 0; // 奇数改变大小写,偶数不变,默认不变
if (/^[a-z]+$/.test(arr[i])) {
let offset_a = currentChar - charCodeOf_a + key; // 算法中的f(x) + key
cycle = parseInt(offset_a / 26);
currentChar = charCodeOf_a + offset_a % 26;
let temp = String.fromCharCode(currentChar);
arr[i] = cycle % 2 ? temp.toUpperCase() : temp;
} else if (/^[A-Z]+$/.test(arr[i])) {
let offset_A = currentChar - charCodeOf_A + key;
cycle = parseInt(offset_A / 26);
currentChar = charCodeOf_A + (offset_A) % 26;
let temp = String.fromCharCode(currentChar);
arr[i] = cycle % 2 ? temp.toLowerCase() : temp;
}
}
return arr.join('');
}
// 解密,需要密文和密钥
function decryptCaesar(ciphertext, key) {
if (!(/^0|[1-9]\d$/.test(key))) {
return new Error('请输入非负整数');
}
let arr = ciphertext.toString().split('');
let charCodeOf_a = 'a'.charCodeAt();
let charCodeOf_A = 'A'.charCodeAt();
for (let i = 0; i < arr.length; i ++) {
let currentChar = arr[i].charCodeAt();
let cycle = 1;
if (/^[a-z]+$/.test(arr[i])) {
let offset_a = currentChar - charCodeOf_a - key;
// 注意offset_a为正时不需要变换大小写
cycle = offset_a < 0 ? parseInt(offset_a / 26) : 1;
if (charCodeOf_a + offset_a < charCodeOf_a) {
offset_a = offset_a % 26 + 26;
}
currentChar = charCodeOf_a + offset_a;
let temp = String.fromCharCode(currentChar);
arr[i] = cycle % 2 ? temp : temp.toUpperCase();
} else if (/^[A-Z]+$/.test(arr[i])) {
let offset_A = currentChar - charCodeOf_A - key;
// 注意offset_A为正时不需要变换大小写
cycle = offset_A < 0 ? parseInt(offset_A / 26) : 1;
if (charCodeOf_A + offset_A < charCodeOf_A) {
offset_A = offset_A % 26 + 26;
}
currentChar = charCodeOf_A + offset_A;
let temp = String.fromCharCode(currentChar);
arr[i] = cycle % 2 ? temp : temp.toLowerCase();
}
}
return arr.join('');
}
decryptCaesar("Nsns PkwsvI", 10) // "Didi Family"
decryptCaesar("RCzsrsr XIFLG", 17) // "Alibaba Group"