难以理解凯撒解密步骤

Trouble understanding Caesar decryption steps

以下代码将根据密文和密钥解密凯撒加密字符串:

#include <iostream>

std::string decrypt(std::string cipher, int key) {
    std::string d = "";
    for(int i=0; i<cipher.length();i++) {
        d += ((cipher[i]-65-key+26) %26)+65;
    }
    return d;
}

int main()
{
    std::cout << decrypt("WKLVLVJRRG", 3) << std::endl; // THISISGOOD
    std::cout << decrypt("NBCMCMAIIX", 20) << std::endl; // THISISGOOD
}

我无法理解在这一行计算新字符 ASCII 代码所执行的操作:

d += ((cipher[i]-65-key+26) %26)+65;
  1. 第一次减法应该移动数字范围
  2. 然后我们将减去密钥,因为凯撒解密是如何定义的
  3. 我们加26来处理负数(?)
  4. 模块会限制输出,因为ASCII码的范围是26位长度
  5. 我们通过在末尾添加 65 回到原来的范围

我错过了什么?

如果我们稍微重新排序表达式,如下所示:

d += (((cipher[i] - 65) + (26 - key)) % 26) + 65;

我们得到旋转cipher[i]的公式key:

  • cipher[i] - 65 将 ASCII 范围 A..Z 转换为整数范围 0..25
  • (cipher[i] - 65 + 26 - key) % 26 将该值向左旋转 key(减去 key 模 26)
  • + 65 将范围 0..25 移回 ASCII 范围 A..Z.

例如假设 key 为 2,A 变为 YB 变为 ZC 变为 A,等等

让我详细解释一下凯撒密码,以帮助您理解该公式。我还将展示超简单的代码示例,但也会展示更高级的代码示例。

最大的问题是潜在的溢出。所以,我们需要处理这个问题。

然后我们需要了解加密和解密是什么意思。如果加密会将所有内容向右移动一位,那么解密会将其再次向左移动。

  • 因此,使用“def”和 key=1,加密后的字符串将为“efg”。
  • 然后使用 key=1 解密,将再次向左移动。结果:“def”

我们可以观察到我们只需要移动-1,所以密钥的负值。

所以,基本上加密和解密可以用相同的套路完成。我们只需要反转密钥。

现在让我们看看溢出问题。目前,我们将仅从大写字符开始。字符具有关联代码。例如,在 ASCII 中,字母 'A' 编码为 65,'B' 编码为 66,依此类推。因为我们不想用这样的数字来计算,所以我们将它们归一化。我们只需从每个字符中减去 'A'。然后

  • 'A' - 'A' = 0
  • 'B' - 'A' = 1
  • 'C' - 'A' = 2
  • 'D' - 'A' = 3

你看到规律了。如果我们现在想用密钥 3 加密字母 'C',我们可以执行以下操作。

'C' - 'A' + 3 = 5 然后我们再次添加 'A' 来取回字母,我们将得到 5 + 'A' = 'F'

这就是全部魔法。

但是如何处理超出 'Z' 的溢出。这可以通过简单的模除法来处理。

让我们看看 'Z' + 1。我们做 'Z' - 'A' = 25,然后 +1 = 26 现在模 26 = 0 然后加上 'A' 将是 'A'

等等等等。结果公式为:​​(c-'A'+key)%26+'A'

接下来,负键呢?这也很简单。假设 'A' 和 key=-1

结果将是 'Z'。但这与向右移动 25 是一样的。因此,我们可以简单地将负键转换为正键。简单的语句将是:

if (key < 0)  key = (26 + (key % 26)) % 26;

然后我们可以用一个简单的 Lambda 调用我们的转换函数。一个函数用于加密和解密。只是用一个倒置的钥匙。

并且使用上面的公式,甚至不需要检查负值。它适用于正值和负值。

因此,key = (26 + (key % 26)) % 26; 将始终有效。


一些扩展信息,如果您使用 ASCII 字符表示。请查看任何 ASCII table。你会看到任何大写和小写字符相差 32。或者,如果你查看二进制:

char  dez  bin           char  dez  bin
'A'   65   0100 0001     'a'   97   0110 0001 
'B'   66   0100 0010     'b'   98   0110 0010 
'C'   67   0100 0011     'b'   99   0110 0011 
 . . .

所以,如果你已经知道一个字符是alpha,那么大写和小写之间的唯一区别就是第5位。如果我们想知道,如果char 是小写,我们可以得到这个通过屏蔽这一点。 c & 0b0010 0000 等于 c & 32c & 0x20.

如果我们想对大写或小写字符进行操作,我们可以将“大小写”屏蔽掉。使用 c & 0b00011111c & 31c & 0x1F 我们将始终获得大写字符的等价物,已经标准化为以一个开头。

char  dez  bin        Masking         char  dez  bin         Masking
'A'   65   0100 0001  & 0x1b = 1      'a'   97   0110 0001   & 0x1b = 1
'B'   66   0100 0010  & 0x1b = 2      'b'   98   0110 0010   & 0x1b = 2
'C'   67   0100 0011  & 0x1b = 3      'b'   99   0110 0011   & 0x1b = 3
 . . .

因此,如果我们使用字母字符,将其屏蔽并减去 1,那么对于任何大写或小写字符,我们都会得到 0..25。


此外,我想重复一下按键处理。正密钥将加密字符串,负密钥将解密字符串。但是,如上所述,负键可以转换为正键。示例:

Shifting by  -1  is same as shifting by  +25
Shifting by  -2  is same as shifting by  +24
Shifting by  -3  is same as shifting by  +23
Shifting by  -4  is same as shifting by  +22

所以,很明显我们可以通过26 + key计算出一个总是肯定的密钥。对于否定键,这将为我们提供上述偏移量。

对于正键,我们会溢出超过 26,我们可以通过模 26 除法来消除:

'A'-->  0 + 26 = 26    26 % 26 = 0 
'B'-->  1 + 26 = 27    27 % 26 = 1 
'C'-->  2 + 26 = 28    28 % 26 = 2 
'D'-->  3 + 26 = 29    29 % 26 = 3

--> (c + key) % 26 将消除溢出并生成正确的新 en/decryptd 字符。

而且,如果我们将其与上述关于否定键的智慧结合起来,我们可以写成:((26+(key%26))%26) 这将适用于所有肯定键和否定键。

将它与那个掩码结合起来,可以得到以下程序:

const char potentialLowerCaseIndicator = c & 0x20;
const char upperOrLower = c & 0x1F;
const char normalized = upperOrLower - 1;
const int withOffset =  normalized + ((26+(key%26))%26);
const int withOverflowCompensation = withOffset % 26;
const char newUpperCaseCharacter = (char)withOverflowCompensation + 'A';
const char result = newUpperCaseCharacter | (potentialLowerCaseIndicator );

当然,上面的很多语句都可以转换成一个Lambda:

#include <string>
#include <algorithm>
#include <cctype>
#include <iostream>

// Simple function for Caesar encyption/decyption

std::string caesar(const std::string& in, int key) {
    std::string res(in.size(), ' ');

    std::transform(in.begin(), in.end(), res.begin(), [&](char c) {return std::isalpha(c) ? (char)((((c & 31) - 1 + ((26 + (key % 26)) % 26)) % 26 + 65) | (c & 32)) : c; });

    return res;
}

int main() {
    std::string test{ "aBcDeF xYzZ" };
    std::cout << caesar(test, 5);
}

最后一个函数也可以更详细一些以便于理解:

std::string caesar1(const std::string& in, int key) {
    std::string res(in.size(), ' ');

    auto convert = [&](const char c) -> char {
        char result = c;
        if (std::isalpha(c)) {

            // Handling of a negative key (Shift to left). Key will be converted to positive value
            if (key < 0) {
                // limit the key to 0,-1,...,-25
                key = key % 26;
                // Key was negative: Now we have someting between 0 and 26
                key = 26 + key;
            };

            // Check and remember if the original character was lower case
            const bool originalIsLower = std::islower(c);

            // We want towork with uppercase only
            const char upperCaseChar = (char)std::toupper(c);

            // But, we want to start with 0 and not with 'A' (65)
            const int normalized = upperCaseChar - 'A';

            // Now add the key
            const int shifted = normalized + key;

            // Addition result maybe bigger then 25, so overflow. Cap it
            const int capped = shifted % 26;

            // Get back a character
            const char convertedUppcase = (char)capped + 'A';

            // And set back the original case
            result = originalIsLower ? (char)std::tolower(convertedUppcase) : convertedUppcase;
        }
        return result;
    };
    std::transform(in.begin(), in.end(), res.begin(), convert);
    return res;
}

如果您想查看仅包含最简单语句的解决方案,请参阅下文。

#include <iostream>
#include <string>

using namespace std;

string caesar(string in, int key) {

    // Here we will store the resulting encrypted/decrypted string
    string result{};


    // Handling of a negative key (Shift to left). Key will be converted to positive value
    if (key < 0) {
        // limit the key to 0,-1,...,-25
        key = key % 26;
        // Key was negative: Now we have someting between 0 and 26
        key = 26 + key;
    };
   
    // Read character by character from the string
    for (unsigned int i = 0; i < in.length(); ++i) {

        char c = in[i];

        // CHeck for alpha character
        if ((c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z')) {

            // Check and remember if the original character was lower case
            bool originalIsLower = (c >= 'a' and c <= 'z');

            // We want to work with uppercase only
            char upperCaseChar = originalIsLower ? c - ('a' - 'A') : c;

            // But, we want to start with 0 and not with 'A' (65)
            int normalized = upperCaseChar - 'A';

            // Now add the key
            int shifted = normalized + key;

            // Addition result maybe bigger then 25, so overflow. Cap it
            int capped = shifted % 26;

            // Get back a character
            char convertedUppcase = (char)capped + 'A';

            // And set back the original case
            result += originalIsLower ? convertedUppcase + ('a' - 'A') : convertedUppcase;

        }
        else
            result += c;
    }
    return result;
}

int main() {
    string test{ "aBcDeF xYzZ" };
    string encrypted = caesar(test, 5);
    string decrypted = caesar(encrypted, -5);

    cout << "Original:  " << test << '\n';
    cout << "Encrpyted: " << encrypted << '\n';
    cout << "Decrpyted: " << decrypted << '\n';
}