五、凯撒密码
“老大哥在看着你。”
——乔治·奥威尔,1984 年
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FI7hKBWA-1692873504732)(https://gitcode.net/OpenDocCN/invent-with-python-zh/-/raw/master/docs/cracking/img/3e754c09a1a42c45ac36ea03cdd9684e.png)]
在第 1 章中,我们使用了一个密码轮和一个字母数字表来实现凯撒密码。在这一章中,我们将在计算机程序中实现凯撒密码。
我们在第四章中制作的反向密码总是以同样的方式加密。但是凯撒密码使用密钥,根据使用的密钥不同,加密信息的方式也不同。凯撒密码的密钥是从0
到25
的整数。即使密码分析人员知道使用了凯撒密码,这也不足以给他们充分的信息来破解密码,除非他们知道对应的密钥。
本章涵盖的主题
import
语句- 常量
for
循环if, else
和elif
语句in
和not in
运算符find()
字符串方法
凯撒密码程序的源代码
在文件编辑器中输入以下代码,并保存为caesarCipher.py
。然后从www.nostarch.com/crackingcodes
下载pyperclip.py
模块,放在与文件caesarCipher.py
相同的目录(即同一个文件夹)下。该模块将被caesarCipher.py
导入;我们将在第 56 页的[的导入模块和设置变量中对此进行更详细的讨论。
完成文件设置后,按F5
运行程序。如果您的代码遇到任何错误或问题,您可以在www.nostarch.com/crackingcodes
使用在线比较工具将它与书中的代码进行比较。
*caesarCipher.py
# Caesar Cipher # https://www.nostarch.com/crackingcodes/ (BSD Licensed) import pyperclip # The string to be encrypted/decrypted: message = 'This is my secret message.' # The encryption/decryption key: key = 13 # Whether the program encrypts or decrypts: mode = 'encrypt' # Set to either 'encrypt' or 'decrypt'. # Every possible symbol that can be encrypted: SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz12345 67890 !?.' # Store the encrypted/decrypted form of the message: translated = '' for symbol in message: # Note: Only symbols in the SYMBOLS string can be encrypted/decrypted. if symbol in SYMBOLS: symbolIndex = SYMBOLS.find(symbol) # Perform encryption/decryption: if mode == 'encrypt': translatedIndex = symbolIndex + key elif mode == 'decrypt': translatedIndex = symbolIndex - key # Handle wraparound, if needed: if translatedIndex >= len(SYMBOLS): translatedIndex = translatedIndex - len(SYMBOLS) elif translatedIndex < 0: translatedIndex = translatedIndex + len(SYMBOLS) translated = translated + SYMBOLS[translatedIndex] else: # Append the symbol without encrypting/decrypting: translated = translated + symbol # Output the translated string: print(translated) pyperclip.copy(translated)
凯撒密码程序的运行示例
当您运行caesarCipher.py
程序时,输出如下:
guv6Jv6Jz!J6rp5r7Jzr66ntrM
输出是使用密钥13
进行凯撒密码加密的字符串'This is my secret message.'
。您刚才运行的凯撒密码程序会自动将这个加密字符串复制到剪贴板,以便您可以将其粘贴到电子邮件或文本文件中。因此,您可以轻松地将程序的加密输出发送给其他人。
运行该程序时,您可能会看到以下错误信息:
Traceback (most recent call last): File "C:\caesarCipher.py", line 4, in <module> import pyperclip ImportError: No module named pyperclip
如果是这样,你可能没有将pyperclip.py
模块下载到正确的文件夹中。如果您确认pyperclip.py
在带有caesarCipher.py
的文件夹中,但仍然无法让模块工作,只需在caesarCipher.py
程序的第 4 行和第 45 行代码(其中有文本pyperclip
)前面加上一个#
就可以了。这使得 Python 忽略了依赖于pyperclip.py
模块的代码,允许程序成功运行。请注意,如果您注释掉该代码,加密或解密的文本不会在程序结束时复制到剪贴板。你也可以在以后的章节中注释掉程序中的pyperclip
代码,这也将从那些程序中移除复制到剪贴板的功能。
要解密消息,只需将输出文本作为新值粘贴到第 7 行的message
变量中。然后修改第 13 行的赋值语句,将字符串'decrypt'
存储在变量mode
中:
# The string to be encrypted/decrypted: message = 'guv6Jv6Jz!J6rp5r7Jzr66ntrM' # The encryption/decryption key: key = 13 # Whether the program encrypts or decrypts: mode = 'decrypt' # Set to either 'encrypt' or 'decrypt'.
当您现在运行该程序时,输出如下所示:
This is my secret message.
导入模块和设置变量
尽管 Python 包含许多内置函数,但有些函数存在于称为模块的独立程序中。模块是 Python 程序,包含你的程序可以使用的附加函数。我们用恰如其名的import
语句导入模块,该语句由import
关键字和模块名组成。
第 4 行包含一个import
语句:
# Caesar Cipher # https://www.nostarch.com/crackingcodes/ (BSD Licensed) import pyperclip
在本例中,我们导入了一个名为pyperclip
的模块,这样我们就可以在程序的后面调用pyperclip.copy()
函数。pyperclip.copy()
函数会自动将字符串复制到你电脑的剪贴板,这样你就可以方便地将它们粘贴到其他程序中。
在caesarCipher.py
中接下来的几行设置了三个变量:
# The string to be encrypted/decrypted: message = 'This is my secret message.' # The encryption/decryption key: key = 13 # Whether the program encrypts or decrypts: mode = 'encrypt' # Set to either 'encrypt' or 'decrypt'.
message
变量存储要加密或解密的字符串,key
变量存储加密密钥的整数。mode
变量要么存储字符串'encrypt'
,让程序后面的代码加密message
中的字符串,要么存储'decrypt'
,让程序解密而不是加密。
常量和变量
常量是程序运行时其值不应改变的变量。例如,凯撒密码程序需要一个字符串,该字符串包含可以用这个凯撒密码加密的每个可能的字符。因为该字符串不应该改变,所以我们将它存储在第 16 行名为SYMBOLS
的常量变量中:
# Every possible symbol that can be encrypted: SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz12345 67890 !?.'
符号是密码学中的常用术语,指密码可以加密或解密的单个字符。一个符号集是一个密码用来加密或解密的每一个可能的符号。因为我们将在这个程序中多次使用符号集,并且因为我们不想每次在程序中出现时都键入完整的字符串值(我们可能会输入错误,这将导致错误),所以我们使用一个常量变量来存储符号集。我们输入一次字符串值的代码,并将其放入SYMBOLS
常量中。
注意SYMBOLS
全是大写字母,这是常量的命名约定。虽然我们可以改变SYMBOLS
就像任何其他变量一样,但是全大写的名字提醒程序员不要写这样做的代码。
就像所有的惯例一样,我们不需要必须遵循这个惯例。但是这样做可以让其他程序员更容易理解这些变量是如何使用的。(它甚至可以帮助你以后查看自己的代码。)
在第 19 行,程序在一个名为translated
的变量中存储一个空字符串,这个变量稍后将存储加密或解密的消息:
# Store the encrypted/decrypted form of the message: translated = ''
就像在第五章的中的反向密码一样,在程序结束时,translated
变量将包含完全加密(或解密)的消息。但是现在它以一个空字符串开始。
###for
循环语句
在第 21 行,我们使用了一种叫做for
循环的循环:
for symbol in message:
回想一下,只要某个条件为True
,一个while
循环就会循环。for
循环的目的略有不同,它没有像while
循环那样的条件。相反,它在一个字符串或一组值上循环。图 5-1 显示了一个for
回路的六个部分。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UpnLQC2Z-1692873504732)(https://gitcode.net/OpenDocCN/invent-with-python-zh/-/raw/master/docs/cracking/img/578f225b6e3836fb8288381140c9ad83.png)]
图 5-1:for
循环语句的六个部分
每次程序执行循环时(也就是说,在循环的每次迭代中),for
语句中的变量(在第 21 行中是symbol
)取包含字符串的变量中的下一个字符的值(在本例中是message
)。for
语句类似于赋值语句,因为变量被创建并赋值,除了for
语句循环不同的值来给变量赋值之外。
一个for
循环的例子
例如,在交互式 shell 中键入以下内容。注意,在您键入第一行之后,>>>
提示符将会消失(在我们的代码中表示为...
,因为 shell 期望在for
语句的冒号之后有一段代码。在交互式 shell 中,当您输入一个空行时,该块将结束:
>>> for letter in 'Howdy': ... print('The letter is ' + letter) ... The letter is H The letter is o The letter is w The letter is d The letter is y
这段代码循环遍历字符串'Howdy'
中的每个字符。当它开始时,变量letter
按顺序一次一个地取'Howdy'
中每个字符的值。为了看到这一点,我们在循环中编写了代码,为每次迭代打印出letter
的值。
等价于for
循环的while
循环
for
循环非常类似于while
循环,但是当你只需要迭代一个字符串中的字符时,使用for
循环更有效。通过编写更多的代码,您可以使while
循环像for
循环一样:
>>> i = 0 # ➊ >>> while i < len('Howdy'): # ➋ ... letter = 'Howdy'[i] # ➌ ... print('The letter is ' + letter) # ➍ ➎ ... i = i + 1 ... The letter is H The letter is o The letter is w The letter is d The letter is y
注意这个while
循环与for
循环的工作结果相同,但是没有for
循环那么简短。首先,我们在while
语句 ➊ 前设置一个新变量i
为0
。该while
语句有一个条件,只要变量i
小于字符串'Howdy'
➋ 的长度,该条件将求值为True
。因为i
是一个整数,并且只跟踪字符串中的当前位置,我们需要声明一个单独的letter
变量来保存字符串中位于i
位置 ➌ 的字符。然后我们可以打印出letter
的当前值,以获得与for
循环 ➍ 相同的输出。当代码执行完毕后,我们需要通过增加1
来增加i
,以移动到下一个位置 ➎ 。
要理解caesarCipher.py
中的第 23 行和第 24 行,您需要了解if
、elif
和else
语句、in
和not in
操作符,以及find()
字符串方法。我们将在接下来的章节中讨论这些。
if
语句
凯撒密码中的第 23 行有另一种 Python 指令——if
语句:
if symbol in SYMBOLS:
你可以把一个if
语句理解为,“如果这个条件是True
,执行下面块中的代码。否则,如果是False
,跳过这个代码块。”一个if
语句的格式是使用关键字if
后跟一个条件,再跟一个冒号(:
)。与循环一样,要执行的代码缩进在一个块中。
if
语句的案例
让我们尝试一个if
语句的例子。打开一个新的文件编辑器窗口,输入以下代码,保存为checkPw.py
:
checkPw.py
print('Enter your password.') typedPassword = input() # ➊ if typedPassword == 'swordfish': # ➋ print('Access Granted') # ➌ print('Done') # ➍
当你运行这个程序时,它会显示文本Enter your
password.
并让用户输入密码。然后密码被存储在变量typedPassword
➊ 中。接下来,if
语句检查密码是否等于字符串'swordfish'
➋。如果是,执行移到跟随if
语句的块内,向用户 ➌ 显示文本Access Granted
;否则,如果typedPassword
不等于'swordfish'
,执行将跳过if
语句的块。无论哪种方式,执行都继续到if
块之后的代码,以显示Done
➍。
else
语句
通常,我们想要测试一个条件,如果条件是True
就执行一段代码,如果条件是False
就执行另一段代码。我们可以在if
语句块后使用else
语句,如果if
语句的条件为False
,则else
语句的代码块将被执行。对于一个else
语句,您只需编写关键字else
和一个冒号(:
)。它不需要条件,因为如果if
语句的条件不为真,它就会运行。您可以将代码读作“如果这个条件是True
,则执行这个块,否则,如果是False
,则执行另一个块。”
修改checkPw.py
程序,如下所示(新行以粗体显示):
checkPw.py
print('Enter your password.') typedPassword = input() if typedPassword == 'swordfish': # ➊ print('Access Granted') else: print('Access Denied') # ➋ print('Done') # ➌
这个版本的程序几乎和以前的版本一样。如果if
语句的条件为True
➊,文本Access Granted
仍将显示。但是现在如果用户键入除了swordfish
之外的东西,if
语句的条件将是False
,导致执行进入else
语句的块并显示Access Denied
➋。无论哪种方式,执行仍将继续,并显示Done
➌。
elif
语句
另一个语句叫做elif
语句,也可以和if
成对出现。就像一个if
语句,它有一个条件。像一个else
语句一样,它跟随一个if
(或另一个elif
)语句,如果前一个if
(或elif
)语句的条件为False
,则执行该语句。您可以将if
、elif
和else
语句理解为“如果这个条件是True
,运行这个块。否则,检查该下一个条件是否为True
。否则,就跑完这最后一个代码块。”任意数量的elif
语句可以跟在if
语句之后。再次修改checkPw.py
程序,使其看起来如下:
checkPw.py
print('Enter your password.') typedPassword = input() if typedPassword == 'swordfish': # ➊ print('Access Granted') # ➋ elif typedPassword == 'mary': # ➌ print('Hint: the password is a fish.') elif typedPassword == '12345': # ➍ print('That is a really obvious password.') else: print('Access Denied') print('Done')
该代码包含四个模块,分别用于if
、elif
和else
语句。如果用户输入12345
,则typedPassword == 'swordfish'
的计算结果为False
➊,因此跳过第一个带有print('Access Granted')
➋ 的块。接下来执行检查typedPassword == 'mary'
条件,该条件也求值为False
➌,因此第二个块也被跳过。typedPassword == '12345'
条件是True
➍,因此执行进入该elif
语句之后的块以运行代码print('That is a really obvious password.')
并跳过任何剩余的elif
和else
语句。注意,这些程序块中有且只有一个会被执行。
在一个if
语句之后可以有零个或多个elif
语句。您可以有零个或一个但不是多个else
语句,并且else
语句总是最后一个,因为它只在没有一个条件求值为True
时执行。具有True
条件的第一条语句执行其块。其余的条件(即使它们也是True
)没有被检查。
in
和not in
运算符
caesarCipher.py
中的第 23 行也使用了in
操作符:
if symbol in SYMBOLS:
一个in
操作符可以连接两个字符串,如果第一个字符串在第二个字符串内,它将计算为True
,否则计算为False
。in
操作符也可以与not
配对,后者的作用正好相反。在交互式 shell 中输入以下内容:
>>> 'hello' in 'hello world!' True >>> 'hello' not in 'hello world!' False >>> 'ello' in 'hello world!' True >>> 'HELLO' in 'hello world!' # ➊ False >>> '' in 'Hello' # ➋ True
注意in
和not in
操作符区分大小写 ➊。此外,空白字符串总是被认为是在任何其他字符串 ➋ 中。
如果一个字符串存在于另一个字符串中,使用in
和not
in
操作符的表达式可以方便地用作if
语句的条件来执行一些代码。
返回到caesarCipher.py
,第 23 行检查symbol
中的字符串(第 21 行的for
循环将其设置为来自message
字符串的单个字符)是否在SYMBOLS
字符串中(该密码程序可以加密或解密的所有字符的符号集)。如果symbol
在SYMBOLS
中,执行进入从第 24 行开始的下一个程序块。如果不是,执行将跳过这个块,转而进入第 39 行的else
语句后面的块。根据符号是否在符号集中,密码程序需要运行不同的代码。
find()
字符串方法
第 24 行找到了SYMBOLS
字符串中的索引,其中symbol
是:
symbolIndex = SYMBOLS.find(symbol)
这段代码包含一个方法调用。方法就像函数一样,只不过它们附加了一个带句点的值(或者在第 24 行,一个包含值的变量)。这个方法的名字是find()
,它被存储在SYMBOLS
字符串值中用于调用。
大多数数据类型(如字符串)都有方法。find()
方法接受一个字符串参数,并返回该参数在方法字符串中出现位置的整数索引。在交互式 shell 中输入以下内容:
>>> 'hello'.find('e') 1 >>> 'hello'.find('o') 4 >>> spam = 'hello' >>> spam.find('h') 0 # ➊
你可以在一个字符串或者一个包含字符串值的变量上使用find()
方法。记住 Python 中的索引是从0
开始的,所以当find()
返回的索引是字符串中的第一个字符时,就会返回一个0
➊。
如果找不到字符串参数,find()
方法返回整数-1
。在交互式 shell 中输入以下内容:
>>> 'hello'.find('x') -1 >>> 'hello'.find('H') # ➊ -1
注意,find()
方法也区分大小写 ➊。
作为参数传递给find()
的字符串可以超过一个字符。find()
返回的整数将是找到参数的第一个字符的索引。在交互式 shell 中输入以下内容:
>>> 'hello'.find('ello') 1 >>> 'hello'.find('lo') 3 >>> 'hello hello'.find('e') 1
字符串方法类似于使用in
操作符的一个更具体的版本。它不仅告诉你一个字符串是否存在于另一个字符串中,还告诉你在哪里。
加密和解密符号
既然你已经理解了if
、elif
和else
语句;in
运算符;和find()
字符串方法,这将更容易理解凯撒密码程序的其余部分是如何工作的。
密码程序只能加密或解密符号集中的符号:
if symbol in SYMBOLS: symbolIndex = SYMBOLS.find(symbol)
所以在运行第 24 行的代码之前,程序必须弄清楚symbol
是否在符号集中。然后可以在SYMBOLS
中找到symbol
所在的索引。find()
调用返回的索引存储在symbolIndex
中。
现在我们已经将当前符号的索引存储在symbolIndex
中,我们可以对它进行加密或解密运算。凯撒密码将密钥号添加到符号的索引中进行加密,或者从符号的索引中减去密钥号进行解密。该值存储在translatedIndex
中,因为它将是已翻译符号在SYMBOLS
中的索引。
caesarCipher.py
# Perform encryption/decryption: if mode == 'encrypt': translatedIndex = symbolIndex + key elif mode == 'decrypt': translatedIndex = symbolIndex - key
mode
变量包含一个字符串,告诉程序应该加密还是解密。如果这个字符串是'encrypt'
,那么第 27 行的if
语句的条件将是True
,执行第 28 行将key
加上symbolIndex
(跳过elif
语句后的块)。否则,如果mode
是'decrypt'
,则执行第 30 行减去key
。
处理绕回
当我们在第一章中用纸和笔实现凯撒密码时,有时增加或减少密钥会导致一个大于或等于符号集大小或小于零的数。在这些情况下,我们必须增加或减少符号集的长度,以便它能够“绕回”,或者返回到符号集的开头或结尾。我们可以使用代码len(SYMBOLS)
来做这件事,它返回66
,即SYMBOLS
字符串的长度。第 33 到 36 行在密码程序中处理这种绕回。
# Handle wraparound, if needed: if translatedIndex >= len(SYMBOLS): translatedIndex = translatedIndex - len(SYMBOLS) elif translatedIndex < 0: translatedIndex = translatedIndex + len(SYMBOLS)
如果translatedIndex
大于等于66
,则第 33 行的条件为True
,执行第 34 行(跳过第 35 行的elif
语句)。从translatedIndex
中减去SYMBOLS
的长度将变量的索引指向SYMBOLS
字符串的开头。否则 Python 会检查translatedIndex
是否小于0
。如果条件是True
,则执行第 36 行,并且translatedIndex
绕到SYMBOLS
字符串的末尾。
你可能想知道为什么我们不直接使用整数值66
而不是len(SYMBOLS)
。通过使用len(SYMBOLS)
而不是66
,我们可以添加或删除SYMBOLS
中的符号,代码的其余部分仍然可以工作。
现在您已经在translatedIndex
中有了translated
变量中符号集的索引,SYMBOLS[translatedIndex]
将对translated
变量中符号集求值。第 38 行使用字符串连接将这个加密/解密的符号添加到translated
字符串的末尾:
translated = translated + SYMBOLS[translatedIndex]
最终,translated
字符串将是整个编码或解码的消息。
处理符号集外的符号
message
字符串可能包含不在SYMBOLS
字符串中的字符。这些字符在密码程序的符号集之外,无法加密或解密。相反,它们将被直接追加到translated
字符串中,这发生在第 39 到 41 行:
else: # Append the symbol without encrypting/decrypting: translated = translated + symbol
第 39 行的else
语句有四个缩进空间。如果您查看上面行的缩进,您会看到它与第 23 行的if
语句成对出现。尽管在这个if
和else
语句之间有很多代码,但它们都属于同一个代码块。
如果第 23 行的if
语句的条件是False
,该块将被跳过,程序执行将从第 41 行开始进入else
语句的块。这个else
块只有一行。它将未更改的symbol
字符串添加到translated
的末尾。结果,符号集之外的符号,例如'%'
或'('
,被添加到翻译后的字符串中,而没有被加密或解密。
显示和复制翻译后的字符串
第 43 行没有缩进,这意味着它是从第 21 行开始的块(for
循环的块)之后的第一行。当程序执行到第 44 行时,它已经遍历了message
字符串中的每个字符,加密(或解密)了这些字符,并将它们添加到translated
:
# Output the translated string: print(translated) pyperclip.copy(translated)
第 44 行调用print()
函数在屏幕上显示translated
字符串。注意,这是整个程序中唯一的print()
调用。计算机做了大量的工作来加密message
中的每个字母,处理绕回,以及处理非字母字符。但是用户不需要看到这些。用户只需要在translated
中看到最后一个字符串。
第 45 行调用copy()
,它接受一个字符串参数并将其复制到剪贴板。因为copy()
是pyperclip
模块中的一个函数,我们必须通过在函数名前面加上pyperclip.
来告诉 Python 这一点。如果我们输入copy(translated)
而不是pyperclip.copy(translated)
,Python 会给我们一个错误消息,因为它找不到这个函数。
如果你在试图调用pyperclip.copy()
之前忘记了import
pyperclip
行(第 4 行),Python 也会给出错误信息。
这就是整个凯撒密码程序。当您运行它时,请注意您的计算机在不到一秒的时间内如何执行整个程序并加密字符串。即使你输入一个很长的字符串存储在message
变量中,你的计算机也能在一两秒钟内加密或解密消息。相比之下,使用密码轮需要几分钟的时间。该程序甚至自动将加密文本复制到剪贴板,这样用户就可以简单地将其粘贴到电子邮件中发送给某人。
加密其他符号
我们实现的凯撒密码的一个问题是,它不能加密其符号集之外的字符。例如,如果您用密钥20
加密字符串'Be sure to bring the $$$.'
,消息将加密到'VyQ?A!yQ.9Qv!381Q.2yQ$$$T'
。这个加密的消息并没有隐藏你指的是$$$
。然而,我们可以修改程序来加密其他符号。
通过改变存储在SYMBOLS
中的字符串以包含更多的字符,程序也将对它们进行加密,因为在第 23 行,条件symbol in SYMBOLS
将是True
。在这个新的、更大的SYMBOLS
常量变量中,symbolIndex
的值将是symbol
的索引。“环绕”将需要增加或减少这个新字符串中的字符数,但这已经得到了处理,因为我们使用了len(SYMBOLS)
而不是直接在代码中键入66
(这就是为什么我们这样编写代码的原因)。
例如,您可以将第 16 行扩展为:
SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz12345 67890 !?.`[email protected]#$%^&*()_+-=[]{}|;:<>,/'
请记住,消息必须使用相同的符号集进行加密和解密。
总结
您已经学习了几个编程概念,并通读了相当多的章节,现在您有了一个实现秘密密码的程序。更重要的是,您了解这些代码是如何工作的。
模块是包含有用函数的 Python 程序。要使用这些函数,您必须首先使用一个import
语句导入它们。要调用导入模块中的函数,在函数名前加一个句点,像这样:module.function()
。
常量变量按照约定用大写字母书写。这些变量并不意味着它们的值被改变(尽管没有什么能阻止程序员编写这样做的代码)。常量很有帮助,因为它们为程序中的特定值提供了一个“名称”。
方法是附加到特定数据类型的值的函数。find()
字符串方法返回传递给它的字符串参数在被调用的字符串中的位置的整数值。
您了解了几种新的方法来控制哪些代码行运行以及每行运行多少次。一个for
循环遍历一个字符串值中的所有字符,在每次迭代中为每个字符设置一个变量。if
、elif
和else
语句根据条件是True
还是False
来执行代码块。
in
和not in
运算符检查一个字符串是否在另一个字符串中,并相应地对True
或False
求值。
学习编程方法,让你有能力用计算机能理解的语言,写下像用凯撒密码加密或解密这样的过程。一旦计算机知道如何执行这个过程,它就能比任何人做得更快,而且不会出错(除非错误出在你的程序中)。尽管这是一项非常有用的技能,但事实证明,知道如何编程的人可以轻松破解凯撒密码。在第 6 章中,你将使用你所学的技能编写一个凯撒密码黑客,这样你就可以读取其他人加密的密文。让我们继续学习如何破解加密。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 使用
caesarCipher.py
,用给定的密钥加密以下句子:
'"You can show black is white by argument," said Filby, "but you will never convince me."'
用密钥8
'1234567890'
用密钥21
- 使用
caesarCipher.py
,用给定的密钥解密以下密文:
'Kv?uqwpfu?rncwukdng?gpqwijB'
用密钥2
'XCBSw88S18A1S 2SB41SE .8zSEwAS50D5A5x81V'
用密钥22
- 哪个 Python 指令会导入一个名为
watermelon.py
的模块? - 下列代码在屏幕上显示了什么?
spam = 'foo' for i in spam: spam = spam + i print(spam)
if 10 < 5: print('Hello') elif False: print('Alice') elif 5 != 5: print('Bob') else: print('Goodbye')
print('f' not in 'foo')
print('foo' in 'f')
print('hello'.find('oo'))
六、暴力破解凯撒密码
“阿拉伯学者发明了密码分析,即在不知道密钥的情况下解读信息的科学。”
——西蒙·辛格,《密码之书》
我们可以通过使用一种叫做暴力破解的密码分析技术来破解凯撒密码。暴力破解攻击用每一个可能的密钥尝试对一个密码进行解密。没有什么可以阻止密码分析者猜测一个密钥,用那个密钥解密密文,查看输出,然后如果他们没有找到秘密消息,就继续下一个密钥。因为暴力破解技术对凯撒密码非常有效,所以您实际上不应该使用凯撒密码来加密秘密信息。
理想情况下,密文永远不会落入任何人手中。但是 Kerckhoffs 原则(以 19 世纪密码学家 Auguste Kerckhoffs 命名)指出,即使每个人都知道密码是如何工作的,并且其他人也有密文,密码仍然应该是安全的。这个原则被 20 世纪数学家克劳德·香农重述为香农的格言:“敌人知道这个系统。”密码中使信息保密的部分就是密钥,对于凯撒密码来说,这个信息很容易找到。
本章涵盖的主题
- Kerckhoffs 原则和香农准则
- 暴力破解技术
range()
函数- 字符串格式化(字符串插值)
凯撒密码破解程序的源代码
选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,保存为caesarHacker.py
。然后下载pyperclip.py
模块(如果你还没有下载的话)(www.nostarch.com/crackingcodes
)并把它放在与caesarCipher.py
文件相同的目录(也就是同一个文件夹)中。这个模块将由caesarCipher.py
导入。
完成文件设置后,按F5
运行程序。如果您的代码遇到任何错误或问题,您可以在www.nostarch.com/crackingcodes
使用在线比较工具将它与书中的代码进行比较。
caesarHacker.py
# Caesar Cipher Hacker # https://www.nostarch.com/crackingcodes/ (BSD Licensed) message = 'guv6Jv6Jz!J6rp5r7Jzr66ntrM' SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz12345 67890 !?.' # Loop through every possible key: for key in range(len(SYMBOLS)): # It is important to set translated to the blank string so that the # previous iteration's value for translated is cleared: translated = '' # The rest of the program is almost the same as the Caesar program: # Loop through each symbol in message: for symbol in message: if symbol in SYMBOLS: symbolIndex = SYMBOLS.find(symbol) translatedIndex = symbolIndex - key # Handle the wraparound: if translatedIndex < 0: translatedIndex = translatedIndex + len(SYMBOLS) # Append the decrypted symbol: translated = translated + SYMBOLS[translatedIndex] else: # Append the symbol without encrypting/decrypting: translated = translated + symbol # Display every possible decryption: print('Key #%s: %s' % (key, translated))
请注意,这段代码的大部分与最初的凯撒密码程序中的代码相同。这是因为凯撒密码破解程序使用相同的步骤来解密消息。
凯撒密码破解程序的运行示例
当您运行凯撒密码破解程序程序时,它会打印以下输出。它通过用所有 66 个可能的密钥解密密文来破解密文guv6Jv6Jz!J6rp5r7Jzr66ntrM
:
Key #0: guv6Jv6Jz!J6rp5r7Jzr66ntrM Key #1: ftu5Iu5Iy I5qo4q6Iyq55msqL Key #2: est4Ht4Hx0H4pn3p5Hxp44lrpK Key #3: drs3Gs3Gw9G3om2o4Gwo33kqoJ Key #4: cqr2Fr2Fv8F2nl1n3Fvn22jpnI --snip-- Key #11: Vjku?ku?o1?ugetgv?oguucigB Key #12: Uijt!jt!nz!tfdsfu!nfttbhfA Key #13: This is my secret message. Key #14: Sghr0hr0lx0rdbqds0ldrrZfd? Key #15: Rfgq9gq9kw9qcapcr9kcqqYec! --snip-- Key #61: lz1 O1 O5CO wu0w!O5w sywR Key #62: kyz0Nz0N4BN0vt9v N4v00rxvQ Key #63: jxy9My9M3AM9us8u0M3u99qwuP Key #64: iwx8Lx8L2.L8tr7t9L2t88pvtO Key #65: hvw7Kw7K1?K7sq6s8K1s77ousN
因为密钥13
的解密输出是简单的英语,我们知道原始的加密密钥一定是13
。
设置变量
破解程序将创建一个message
变量,存储程序试图解密的密文字符串。SYMBOLS
常量包含密码可以加密的每个字符:
# Caesar Cipher Hacker # https://www.nostarch.com/crackingcodes/ (BSD Licensed) message = 'guv6Jv6Jz!J6rp5r7Jzr66ntrM' SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz12345 67890 !?.'
SYMBOLS
的值需要与我们试图破解的加密密文的凯撒密码程序中使用的SYMBOLS
的值相同;否则破解程序无法运行。注意,字符串值中的0
和!
之间有一个空格。
range()
函数循环
第 8 行是一个for
循环,它不遍历字符串值,而是遍历对range()
函数调用的返回值:
# Loop through every possible key: for key in range(len(SYMBOLS)):
range()
函数接受一个整数参数并返回一个数据类型为range
的值。范围值可以用在for
循环中,根据你给函数的整数循环特定的次数。让我们试一个例子。在交互式 shell 中输入以下内容:
>>> for i in range(3): ... print('Hello') ... Hello Hello Hello
for
循环将循环三次,因为我们将整数3
传递给了range()
。
更具体地说,从range()
函数调用返回的范围值将把for
循环的变量设置为从0
到(但不包括)传递给range()
的参数的整数。例如,在交互式 shell 中输入以下内容:
>>> for i in range(6): ... print(i) ... 0 1 2 3 4 5
这段代码将变量i
设置为从0
到(但不包括)6
的值,这类似于caesarHacker.py
中第 8 行的操作。第 8 行用从0
到(但不包括)66
的值设置key
变量。我们没有将值66
直接硬编码到我们的程序中,而是使用来自len(SYMBOLS)
的返回值,因此如果我们修改SYMBOLS
,程序仍然会工作。
程序执行第一次经过这个循环时,key
被设置为0
,用密钥0
解密message
中的密文。(当然,如果0
不是真正的密钥,message
只是“解密”成废话。)从第 9 行到第 31 行的for
循环中的代码,我们接下来将解释,类似于原始的凯撒密码程序并进行解密。在第 8 行的for
循环的下一次迭代中,key
被设置为1
用于解密。
虽然我们不会在这个程序中使用它,但是您也可以向range()
函数传递两个整数参数,而不是一个。第一个参数是范围应该开始的位置,第二个参数是范围应该停止的位置(直到但不包括第二个参数)。参数由逗号分隔:
>>> for i in range(2, 6): ... print(i) ... 2 3 4 5
变量i
将取从2
(包括2
)到6
(不包括6)
)的值。
解密消息
接下来几行中的解密代码将解密后的文本添加到translated
中的字符串末尾。在第 11 行,translated
被设置为空字符串:
# Loop through every possible key: for key in range(len(SYMBOLS)): # It is important to set translated to the blank string so that the # previous iteration's value for translated is cleared: translated = ''
在这个for
循环的开始,我们将translated
重置为空字符串,这一点很重要;否则,用当前密钥解密的文本将被添加到循环中最后一次迭代的translated
解密文本中。
第 16 行到第 30 行几乎与第 5 章中的凯撒密码程序中的代码相同,但是稍微简单一些,因为这段代码只需要解密:
# The rest of the program is almost the same as the Caesar program: # Loop through each symbol in message: for symbol in message: if symbol in SYMBOLS: symbolIndex = SYMBOLS.find(symbol)
在第 16 行,我们遍历存储在message
中的密文字符串中的每个符号。在这个循环的每次迭代中,第 17 行检查symbol
是否存在于SYMBOLS
常量变量中,如果存在,就解密它。第 18 行的find()
方法调用定位SYMBOLS
中symbol
所在的索引,并将其存储在一个名为symbolIndex
的变量中。
然后我们从第 19 行的symbolIndex
中减去key
来解密:
translatedIndex = symbolIndex - key # Handle the wraparound: if translatedIndex < 0: translatedIndex = translatedIndex + len(SYMBOLS)
这个减法操作可能会导致translatedIndex
变得小于零,并且当我们在SYMBOLS
中找到要解密的字符的位置时,需要我们“环绕”常量SYMBOLS
。第 22 行检查这种情况,如果translatedIndex
小于0
,第 23 行增加66
(这是len(SYMBOLS)
返回的值)。
现在translatedIndex
已经被修改,SYMBOLS[translatedIndex]
将计算出解密后的符号。第 26 行将这个符号添加到存储在translated
中的字符串的末尾:
# Append the decrypted symbol: translated = translated + SYMBOLS[translatedIndex] else: # Append the symbol without encrypting/decrypting: translated = translated + symbol
如果在SYMBOL
集合中没有找到值,第 30 行只是将未修改的symbol
添加到translated
的末尾。
使用字符串格式显示密钥和解密后的消息
尽管第 33 行是我们的凯撒密码破解程序中唯一的print()
函数调用,但它将执行多次,因为它在第 8 行的for
循环的每次迭代中被调用一次:
# Display every possible decryption: print('Key #%s: %s' % (key, translated))
print()
函数调用的参数是一个使用字符串格式化(也称为字符串插值)的字符串值。使用%s
文本的字符串格式将一个字符串放在另一个字符串中。字符串中的第一个%s
被字符串末尾括号中的第一个值替换。
在交互式 shell 中输入以下内容:
>>> 'Hello %s!' % ('world') 'Hello world!' >>> 'Hello ' + 'world' + '!' 'Hello world!' >>> 'The %s ate the %s that ate the %s.' % ('dog', 'cat', 'rat') 'The dog ate the cat that ate the rat.'
在这个例子中,首先将字符串'world'
插入到字符串'Hello %s!'
中,代替%s
。它的工作原理就好像你已经将字符串中位于%s
之前的部分与插入的字符串和位于%s
之后的部分连接起来。当您插入多个字符串时,它们会按顺序替换每个%s
。
字符串格式通常比使用+
操作符的字符串连接更容易键入,尤其是对于大型字符串。而且,与字符串连接不同,您可以将整数等非字符串值插入到字符串中。在交互式 shell 中输入以下内容:
>>> '%s had %s pies.' % ('Alice', 42) 'Alice had 42 pies.' >>> 'Alice' + ' had ' + 42 + ' pies.' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't convert 'int' object to str implicitly
当您使用插值时,整数42
被插入到字符串中没有任何问题,但是当您尝试连接该整数时,它会导致错误。
caesarHacker.py
的第 33 行使用字符串格式创建一个字符串,该字符串同时具有key
和translated
变量的值。因为key
存储一个整数值,所以我们使用字符串格式将它放入一个传递给print()
的字符串值中。
总结
凯撒密码的致命弱点是没有很多用来加密的密钥。任何计算机都可以很容易地用所有 66 个可能的密钥进行解密,密码分析人员只需要几秒钟就可以在解密的信息中找到一个英文的。为了使我们的信息更安全,我们需要一个有更多潜在密钥的密码。第七章中讨论的换位密码可以为我们提供这种安全性。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 破解下面的密文,一次解密一行,因为每一行都有不同的密钥。请记住对任何引用字符进行转义:
qeFIP?eGSeECNNS, 5coOMXXcoPSZIWoQI, avnl1olyD4l'ylDohww6DhzDjhuDil, z.GM?.cEQc. 70c.7KcKMKHA9AGFK, ?MFYp2pPJJUpZSIJWpRdpMFY, ZqH8sl5HtqHTH4s3lyvH5zH5spH4t pHzqHlH3l5K Zfbi,!tif!xpvme!qspcbcmz!fbu!nfA
七、用换位密码加密
“辩称你不在乎隐私权是因为你没什么可隐瞒的,这与说你不在乎言论自由是因为你无话可说没什么区别。”
——爱德华·斯诺登,2015
凯撒密码不安全。对一台计算机来说,暴力破解所有 66 个可能的密钥并不需要太多时间。另一方面,换位密码更难以暴力破解,因为可能的密钥数量取决于消息的长度。有许多不同类型的换位密码,包括围栏密码、路由密码、Myszkowski 换位密码和中断换位密码。本章涵盖了一个简单的换位密码,称为列式换位密码。
本章涵盖的主题
- 用
def
语句创建函数 - 自变量和参数
- 全局和局部作用域内的变量
main()
函数- 列表数据类型
- 列表和字符串的相似性
- 列表的列表
- 扩展赋值运算符
(+=, -=, *=, /=)
join()
字符串方法- 返回值和
return
语句 __name__
变量
换位密码的工作原理
换位密码不是用其他字符替换字符,而是将邮件的符号重新排列成使原始邮件不可读的顺序。因为每一个密钥都创造了不同的字符顺序,或称排列,密码分析者不知道如何将密文重新排列成原始信息。
用换位密码加密的步骤如下:
- 计算消息和密钥中的字符数。
- 画一排与密钥数量相等的盒子(例如,8 个盒子代表 8 个密钥)。
- 开始从左到右填写方框,每个方框输入一个字符。
- 当你用完了所有的方块,但仍然有更多的字符,添加另一行方块。
- 当到达最后一个字符时,在最后一行未使用的框中添加阴影。
- 从左上角开始,沿着每一列,写出字符。当到达一列的底部时,移动到右边的下一列。跳过任何阴影框。这将是密文。
为了了解这些步骤在实践中是如何工作的,我们将手动加密一条消息,然后将这个过程转换成一个程序。
手动加密信息
在我们开始写代码之前,让我们加密“常识并不那么普遍”这个消息。使用铅笔和纸。包括空格和标点,这条消息有 30 个字符。对于本例,您将使用数字 8 作为密钥。此密码类型的可能密钥范围是从 2 到消息大小的一半,即 15。但是消息越长,可能的密钥就越多。使用柱状置换密码加密整本书将允许数千个可能的密钥。
第一步,连续画八个方框与密钥号匹配,如图 7-1 。
图 7-1:第一行的框数要和密钥数匹配。
第二步是开始将你想要加密的信息写入盒子,每个盒子放一个字符,如图 7-2 所示。记住空格也是字符(这里用/表示)。
图 7-2:每框填写一个字符,包括空格。
你只有八个盒子,但是消息里有 30 个字符。当你用完盒子后,在第一行下面再画一行八个盒子。继续创建新行,直到你写完整个消息,如图 7-3 所示。
图 7-3:添加更多的行,直到填满整个消息。
在最后一行的两个框中画阴影,以提醒忽略它们。密文由从左上方的方框中读取的字母组成。C
、e
、n
和o
来自第一列,如图所示。当到达一列的最后一行时,移动到右边下一列的顶行。接下来的字符是o
、n
、o
、m
。忽略阴影框。
密文是Cenoonommstmme oo snnio. s s c
,它被充分地扰乱,以防止有人通过查看它来理解原始消息。
创建加密程序
要制作一个加密程序,你需要将这些纸上谈兵的步骤翻译成 Python 代码。让我们再次看看如何使用密钥8
加密字符串'Common sense is not so common.'
。对于 Python 来说,一个字符在字符串中的位置就是它的编号索引,所以把字符串中每个字母的索引加到你原来的加密图的方框中,如图 7-4 所示。(记住索引以0
开始,而不是1
。)
图 7-4:给每个盒子加上索引号,从 0 开始。
这些方框显示第一列具有索引0
、8
、16
和24
处的字符(它们是'C'
、'e'
、'n'
和'o'
)。下一列具有索引1
、9
、17
和25
处的字符(它们是'o'
、'n'
、'o'
和'm'
)。注意出现的模式:第n
列包含字符串中索引为0 + (n – 1)
、8 + (n – 1)
、16 + (n – 1)
、24 + (n – 1)
的所有字符,如图 7-5 所示。
图 7-5:每个盒子的索引遵循一个可预测的模式。
第 7 列和第 8 列的最后一行有一个例外,因为24+(7–1)
和24+(8–1)
将大于 29,这是字符串中最大的索引。在这些情况下,你只需将 0、8 和 16 加到n
(并跳过 24)。
数字 0、8、16 和 24 有什么特别的?这些是从 0 开始添加密钥(在本例中是 8)时得到的数字。所以,0 + 8
是 8,8 + 8
是 16,16 + 8
是 24。24 + 8
的结果将是 32,但是因为 32 比消息的长度大,所以您将在 24 处停止。
对于第n
列的字符串,从索引(n–1
)开始,继续加 8(密钥)得到下一个索引。只要索引小于 30(消息长度),就一直加 8,此时移到下一列。
如果您将每一列想象成一个字符串,那么结果将是一个包含八个字符串的列表,如下所示:'Ceno'
、'onom'
、'mstm'
、'me o'
、'o sn'
、'nio.'
、' s '
、's c'
。如果您按顺序将字符串连接在一起,结果将是密文:'Cenoonommstmme oo snnio. s s c'
。在这一章的后面,你会学到一个叫做列表的概念,它会让你做到这一点。
换位密码加密程序的源代码
选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,然后保存为transpositonecrypt.py
。记得将pyperclip.py
模块放在与transpositonecrypt.py
文件相同的目录下。然后按F5
运行程序。
换位
Encrypt.py
# Transposition Cipher Encryption # https://www.nostarch.com/crackingcodes/ (BSD Licensed) import pyperclip def main(): myMessage = 'Common sense is not so common.' myKey = 8 ciphertext = encryptMessage(myKey, myMessage) # Print the encrypted string in ciphertext to the screen, with # a | ("pipe" character) after it in case there are spaces at # the end of the encrypted message: print(ciphertext + '|') # Copy the encrypted string in ciphertext to the clipboard: pyperclip.copy(ciphertext) def encryptMessage(key, message): # Each string in ciphertext represents a column in the grid: ciphertext = [''] * key # Loop through each column in ciphertext: for column in range(key): currentIndex = column # Keep looping until currentIndex goes past the message length: while currentIndex < len(message): # Place the character at currentIndex in message at the # end of the current column in the ciphertext list: ciphertext[column] += message[currentIndex] # Move currentIndex over: currentIndex += key # Convert the ciphertext list into a single string value and return it: return ''.join(ciphertext) # If transpositionEncrypt.py is run (instead of imported as a module) call # the main() function: if __name__ == '__main__': main()
换位密码加密程序的运行示例
当您运行transpositonecrypt.py
程序时,它会产生以下输出:
Cenoonommstmme oo snnio. s s c|
竖线字符(|
)标记密文的结尾,以防结尾有空格。这个密文(末尾没有管道字符)也被复制到剪贴板,因此您可以将它粘贴到给某人的电子邮件中。如果您想要加密不同的消息或使用不同的密钥,请更改第 7 行和第 8 行中分配给myMessage
和myKey
变量的值。然后再次运行该程序。
用def
语句创建自己的函数
导入pyperclip
模块后,您将在第 6 行使用一个def
语句创建一个定制函数main()
。
# Transposition Cipher Encryption # https://www.nostarch.com/crackingcodes/ (BSD Licensed) import pyperclip def main(): myMessage = 'Common sense is not so common.' myKey = 8
def
语句意味着你正在创建,或者说定义一个你可以在程序中稍后调用的新函数。def
语句后的代码块是调用函数时将运行的代码。当你调用这个函数时,执行在函数的def
语句之后的代码块内移动。
正如你在第三章中了解到的,在某些情况下,函数会接受参数,这些参数是函数可以在代码中使用的值。例如,print()
可以将一个字符串值作为其括号之间的参数。当您定义一个接受参数的函数时,您在它的def
语句中的括号之间放置一个变量名。这些变量被称为参数。这里定义的main()
函数没有参数,所以在调用时没有参数。如果您试图调用一个函数,而该函数的参数数量太多或太少,Python 将会抛出一条错误消息。
定义带参数的函数
让我们用一个参数创建一个函数,然后用一个参数调用它。打开一个新的文件编辑器窗口,并在其中输入以下代码:
你好
Function.py
def hello(name): # ➊ print('Hello, ' + name) # ➋ print('Start.') # ➌ hello('Alice') # ➍ ➎ print('Call the function again:') ➏ hello('Bob') ➐ print('Done.')
将该程序另存为helloFunction.py
并按F5
运行。输出如下所示:
Start. Hello, Alice Call the function again: Hello, Bob Done.
当helloFunction.py
程序运行时,执行从顶部开始。def
语句 ➊ 用一个参数定义了hello()
函数,这个参数就是变量name
。执行会跳过def
语句 ➋ 之后的块,因为该块只在函数被调用时运行。接下来,它执行print('Start.')
➌,这就是为什么'Start.'
是运行程序时打印的第一个字符串。
print('Start.')
之后的下一行是对hello()
的第一次函数调用。程序执行跳转到hello()
函数块 ➋ 的第一行。字符串'Alice'
作为参数传递,并被赋给参数name
。这个函数调用将字符串'Hello, Alice'
打印到屏幕上。
当程序执行到def
语句块的底部时,执行跳回到函数调用 ➍ 的那一行,并从那里继续执行代码,所以'Call the function again:'
被打印 ➎ 。
接下来是对hello()
➏ 的第二次调用。程序执行跳回到hello()
函数的定义 ➋ 并再次执行那里的代码,在屏幕上显示'Hello, Bob'
。然后函数返回,执行到下一行,即print('Done.')
语句 ➐ ,并执行它。这是程序的最后一行,所以程序退出。
对参数的更改只存在于函数内部
在交互式 shell 中输入以下代码。这段代码定义并调用一个名为func()
的函数。请注意,交互式 shell 要求您在param = 42
后输入一个空行来关闭def
语句块:
>>> def func(param): param = 42 >>> spam = 'Hello' >>> func(spam) >>> print(spam) Hello
func()
函数接受一个名为param
的参数,并将其值设置为42
。函数外的代码创建一个spam
变量并将其设置为字符串值,然后在spam
上调用该函数并打印出spam
。
当你运行这个程序时,最后一行的print()
调用将打印'Hello'
,而不是42
。当以spam
作为参数调用func()
时,只有spam
内的值被复制并赋给param
。在函数内部对param
所做的任何改变将不改变spam
变量中的值。(当您传递列表或字典值时,此规则有一个例外,但这在第 119 页的列表变量使用引用中有解释。)
每次调用函数时,都会创建一个局部作用域。在函数调用过程中创建的变量存在于这个局部作用域内,被称为局部变量。参数总是存在于局部作用域内(它们是在调用函数时创建并赋值的)。把一个作用域想象成一个容器,变量存在于其中。当函数返回时,局部作用域被销毁,作用域中包含的局部变量被遗忘。
在每个函数之外创建的变量存在于全局作用域中,被称为全局变量。当程序退出时,全局作用域被破坏,程序中的所有变量都被遗忘。(分别在第 5 章和第 6 章中的逆向密码和凯撒密码程序中的所有变量都是全局变量。)
变量必须是局部的或全局的;不可能两者兼得。两个不同的变量可以有相同的名字,只要它们在不同的作用域内。它们仍然被认为是两个不同的变量,就像旧金山的大街和伯明翰的大街是不同的一样。
需要理解的重要思想是,被“传递”到函数调用中的参数值是被复制到参数中的。因此,即使参数被更改,提供参数值的变量也不会更改。
定义main()
函数
在transpositonecrypt.py
中的第 6 到第 8 行,你可以看到我们已经定义了一个main()
函数,它将在被调用时为变量myMessage
和myKey
设置值:
def main(): myMessage = 'Common sense is not so common.' myKey = 8
本书中其余的程序也有一个名为main()
的函数,在每个程序开始时被调用。我们有一个main()
函数的原因会在本章末尾解释,但是现在只需要知道在本书中的程序运行后不久main()
就会被调用。
第 7 行和第 8 行是定义main()
的代码块中的前两行。在这几行中,变量myMessage
和myKey
存储要加密的明文消息和用于加密的密钥。第 9 行是一个空行,但仍然是代码块的一部分,它将第 7 行和第 8 行与第 10 行分开,以使代码更具可读性。第 10 行通过调用一个带有两个参数的函数将变量ciphertext
指定为加密的消息:
ciphertext = encryptMessage(myKey, myMessage)
进行实际加密的代码在第 21 行后面定义的encryptMessage()
函数中。这个函数有两个参数:密钥的整数值和要加密的消息的字符串值。在这种情况下,我们传递变量myMessage
和myKey
,我们刚刚在第 7 行和第 8 行定义了它们。向函数调用传递多个参数时,用逗号分隔参数。
encryptMessage()
的返回值是加密密文的字符串值。该字符串存储在ciphertext
中。
密文信息打印到屏幕的第 15 行,并复制到剪贴板的第 18 行:
# Print the encrypted string in ciphertext to the screen, with # a | ("pipe" character) after it in case there are spaces at # the end of the encrypted message: print(ciphertext + '|') # Copy the encrypted string in ciphertext to the clipboard: pyperclip.copy(ciphertext)
该程序在消息的末尾打印一个管道字符(|
),这样用户就可以看到密文末尾的任何空格字符。
第 18 行是main()
函数的最后一行。在它执行之后,程序执行返回到调用它的行之后的行。
将密钥和消息作为参数传递
第 21 行括号之间的key
和message
变量是参数:
def encryptMessage(key, message):
当在第 10 行调用encryptMessage()
函数时,传递两个参数值(在myKey
和myMessage
中的值)。当执行移动到函数的顶部时,这些值被分配给参数key
和message
。
您可能想知道为什么还要有key
和message
参数,因为在main()
函数中已经有了变量myKey
和myMessage
。我们需要不同的变量,因为myKey
和myMessage
在main()
函数的局部作用域内,不能在main()
之外使用。
列表数据类型
transpositonecrypt.py
程序中的第 23 行使用了一种叫做列表的数据类型:
# Each string in ciphertext represents a column in the grid: ciphertext = [''] * key
在我们继续之前,你需要理解列表是如何工作的,以及你能用它们做什么。列表值可以包含其他值。类似于字符串如何以引号开始和结束,列表值以左括号[
开始,以右括号]
结束。列表中存储的值在括号之间。如果列表中有多个值,则这些值用逗号分隔。
要查看运行中的列表,请在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert'] >>> animals ['aardvark', 'anteater', 'antelope', 'albert']
animals
变量存储一个列表值,在这个列表值中有四个字符串值。列表中的单个值也被称为项或元素。当您必须在一个变量中存储多个值时,列表是理想的选择。
您可以对字符串进行的许多操作也适用于列表。例如,索引和切片处理列表值的方式与处理字符串值的方式相同。索引指的是列表中的一项,而不是字符串中的单个字符。在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'albert'] >>> animals[0] # ➊ 'aardvark' >>> animals[1] 'anteater' >>> animals[2] 'albert' >>> animals[1:3] # ➋ ['anteater', 'albert']
请记住,第一个指标是0
,而不是1
➊。类似于对字符串使用切片会得到一个作为原始字符串一部分的新字符串,对列表使用切片会得到一个作为原始列表一部分的列表。请记住,如果一个切片有第二个索引,该切片只走到第二个索引 ➋ 处的前一个项目。
一个for
循环也可以遍历列表中的值,就像它可以遍历字符串中的字符一样。存储在for
循环变量中的值是列表中的单个值。在交互式 shell 中输入以下内容:
>>> for spam in ['aardvark', 'anteater', 'albert']: ... print('For dinner we are cooking ' + spam) ... For dinner we are cooking aardvark For dinner we are cooking anteater For dinner we are cooking albert
每次循环迭代时,spam
变量从列表中被赋予一个新值,从列表的0
索引开始,直到列表的结尾。
重新赋值列表中的项目
您还可以通过使用带有普通赋值语句的列表索引来修改列表中的项目。在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'albert'] >>> animals[2] = 9999 # ➊ >>> animals ['aardvark', 'anteater', 9999] # ➋
为了修改animals
列表的第三个成员,我们使用索引来获得第三个值animals[2]
,然后使用赋值语句将其值从'albert'
更改为值9999
➊。当我们再次检查列表的内容时,'albert'
不再包含在 ➋ 列表中。
重新赋值字符串中的字符
虽然您可以重新赋值列表中的项目,但不能重新赋值字符串值中的字符。在交互式 shell 中输入以下代码:
>>> 'Hello world!'[6] = 'X'
您会看到以下错误:
Traceback (most recent call last): File <pyshell#0>, line 1, in <module> 'Hello world!'[6] = 'X' TypeError: 'str' object does not support item assignment
您看到这个错误的原因是 Python 不允许您在字符串的索引值上使用赋值语句。相反,要更改字符串中的字符,您需要使用切片创建一个新的字符串。在交互式 shell 中输入以下内容:
>>> spam = 'Hello world!' >>> spam = spam[:6] + 'X' + spam[7:] >>> spam 'Hello Xorld!'
首先,从字符串的开头开始,一直到要更改的字符,获取一个片段。然后你可以把它连接到新字符的字符串,以及从新字符后的字符到字符串末尾的一段。这导致原始字符串只有一个字符发生了变化。
列表的列表
列表值甚至可以包含其他列表。在交互 shell 中输入以下内容:
>>> spam = [['dog', 'cat'], [1, 2, 3]] >>> spam[0] ['dog', 'cat'] >>> spam[0][0] 'dog' >>> spam[0][1] 'cat' >>> spam[1][0] 1 >>> spam[1][1] 2
spam[0]
的值求值为列表['dog', 'cat']
,它有自己的索引。用于spam[0][0]
的双索引括号表示我们从第一个列表中取出第一个项目:spam[0]
计算为['dog', 'cat']
,['dog', 'cat'][0]
计算为'dog'
。
对列表使用len()
和in
运算符
您已经使用了len()
来表示字符串中的字符数(即字符串的长度)。len()
函数也作用于列表值,并返回列表中项目数的整数。
在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert'] >>> len(animals) 4
类似地,您已经使用了in
和not in
操作符来指示一个字符串是否存在于另一个字符串值中。in
操作符也用于检查列表中是否有值,而not in
操作符检查列表中是否没有值。在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert'] >>> 'anteater' in animals True >>> 'anteater' not in animals False >>> 'anteat' in animals # ➊ False >>> 'anteat' in animals[1] # ➋ True >>> 'delicious spam' in animals False
为什么 ➊ 的表达式返回False
,而 ➋ 的表达式返回True
?记住animals
是一个列表值,而animals[1]
计算的是字符串值'anteater'
。因为字符串'anteat'
不在animals
列表中,所以 ➊ 处的表达式求值为False
。然而, ➋ 处的表达式求值为True
,因为animals[1]
是字符串'anteater'
并且'anteat'
存在于该字符串中。
类似于一组空引号表示一个空字符串值,一组空括号表示一个空列表。在交互式 shell 中输入以下内容:
>>> animals = [] >>> len(animals) 0
animals
列表为空,因此其长度为0
。
用+
和*
运算符链接和复制列表
您知道+
和*
操作符可以连接和复制字符串;相同的操作符也可以连接和复制列表。在交互式 shell 中输入以下内容。
>>> ['hello'] + ['world'] ['hello', 'world'] >>> ['hello'] * 5 ['hello', 'hello', 'hello', 'hello', 'hello']
关于字符串和列表的相似之处,已经说得够多了。请记住,您可以对字符串值进行的大多数操作也适用于列表值。
换位加密算法
我们将在加密算法中使用列表来创建密文。让我们回到transpositionEncrypt.py
程序中的代码。在我们前面看到的第 23 行中,ciphertext
变量是一个空字符串值列表:
# Each string in ciphertext represents a column in the grid: ciphertext = [''] * key
ciphertext
变量中的每个字符串代表换位密码网格中的一列。因为列的数量等于密钥的数量,所以可以使用列表复制将一个包含一个空字符串值的列表乘以key
中的值。这就是第 23 行如何计算出包含正确数量的空白字符串的列表。字符串值将被分配到网格的一列中的所有字符。结果将是代表每一列的字符串值的列表,如本章前面所讨论的。因为列表索引从 0 开始,所以还需要从 0 开始标记每一列。所以ciphertext[0]
是最左边的一列,ciphertext[1]
是右边的一列,以此类推。
为了了解这是如何工作的,让我们再次从“常识并不常见”的角度来看网格。本章前面的示例(列表索引对应的列号添加到顶部),如图 7-6 所示。
图 7-6:带有每列列表索引的示例消息网格
如果我们手动将字符串值分配给该网格的ciphertext
变量,它将如下所示:
>>> ciphertext = ['Ceno', 'onom', 'mstm', 'me o', 'o sn', 'nio.', ' s ', 's c'] >>> ciphertext[0] 'Ceno'
下一步是向ciphertext
中的每个字符串添加文本,就像我们刚刚在手动示例中所做的那样,只是这次我们添加了一些代码,让计算机以编程方式完成:
# Loop through each column in ciphertext: for column in range(key): currentIndex = column
第 26 行的for
循环对每一列迭代一次,变量column
具有用于索引ciphertext
的整数值。在通过for
循环的第一次迭代中,column
变量被设置为0
;第二次迭代时,设置为1
;然后2
;诸如此类。我们在ciphertext
中有字符串值的索引,我们希望稍后使用表达式ciphertext[column]
来访问它。
同时,currentIndex
变量保存了程序在for
循环的每次迭代中查看的message
字符串的索引。在循环的每次迭代中,第 27 行将currentIndex
设置为与column
相同的值。接下来,我们将通过一次一个字符地将加扰后的消息连接在一起来创建密文。
扩展赋值运算符
到目前为止,当我们相互连接或添加值时,我们使用了+
操作符将新值添加到变量中。通常,当您为变量赋值新值时,您希望它基于变量的当前值,因此您将变量作为表达式的一部分来计算并赋值给变量,如交互式 shell 中的示例所示:
>>> spam = 40 >>> spam = spam + 2 >>> print(spam) 42
还有其他方法可以根据变量的当前值来操作变量中的值。例如,您可以通过使用扩展赋值操作符来实现这一点。语句spam
+=
2
使用了+=
扩展赋值运算符,它将做与spam
=
spam
+
2
同样的事情。只是打字时间短了一点。操作符使用整数做加法,使用字符串做字符串连接,使用列表做列表连接。表 7-1 显示了扩展赋值运算符及其等价赋值语句。
表 7-1: 扩展赋值运算符
扩展赋值 | 等效普通赋值 |
spam += 42 |
spam = spam + 42 |
spam -= 42 |
spam = spam - 42 |
spam *= 42 |
spam = spam * 42 |
spam /= 42 |
spam = spam / 42 |
我们将使用扩展赋值操作符将字符连接到我们的密文中。
通过message
移动currentIndex
currentIndex
变量保存了message
字符串中下一个字符的索引,该字符串将被连接到ciphertext
列表中。在第 30 行的while
循环的每次迭代中,key
被添加到currentIndex
中,以指向message
中的不同字符,并且在第 26 行的for
循环的每次迭代中,currentIndex
被设置为column
变量中的值。
为了对message
变量中的字符串进行加扰,我们需要取message
的第一个字符'C'
,并将其放入ciphertext
的第一个字符串中。然后,我们将跳过八个字符进入message
(因为key
等于8
,并将这个字符'e'
连接到密文的第一个字符串。我们将继续根据密钥跳过字符,并连接每个字符,直到我们到达消息的结尾。这样做将创建字符串'Ceno'
,这是密文的第一列。然后我们将再次这样做,但是从message
中的第二个字符开始创建第二列。
从第 26 行开始的for
循环中有一个从第 30 行开始的while
循环。这个while
循环在message
中找到并连接正确的字符来生成每一列。当currentIndex
小于message
的长度时循环;
# Keep looping until currentIndex goes past the message length: while currentIndex < len(message): # Place the character at currentIndex in message at the # end of the current column in the ciphertext list: ciphertext[column] += message[currentIndex] # Move currentIndex over: currentIndex += key
对于每一列,while
循环遍历原始的message
变量,并通过将key
加到currentIndex
中来挑选出key
间隔中的字符。在第 27 行上,对于for
循环的第一次迭代,currentIndex
被设置为从0
开始的column
的值。
如图 7-7 中的所示,message[currentIndex]
是message
第一次迭代的第一个字符。在message[currentIndex]
T13 的字符被连接到ciphertext[column]
以在第 33 行开始第一列。第 36 行每次通过循环将key
(即8
)中的值添加到currentIndex
。第一次是message[0]
,第二次message[8]
,第三次message[16]
,第四次message[24]
。
图 7-7:当列设置为 0 时,在for
循环的第一次迭代中,指向message[currentIndex]
所指内容的箭头
虽然currentIndex
中的值小于message
字符串的长度,但是您希望继续将message[currentIndex]
处的字符连接到ciphertext
中column
索引处的字符串末尾。当currentIndex
大于message
的长度时,执行退出while
循环,回到for
循环。因为在while
循环之后的for
块中没有代码,所以for
循环迭代,column
被设置为1
,currentIndex
从与column
相同的值开始。
现在,当第 36 行在第 30 行的while
循环的每次迭代中将8
加到currentIndex
时,索引将为1
、9
、17
和25
,如图 7-8 所示。
图 7-8:当列设置为 1 时,在循环的的第二次迭代中,指向message[currentIndex]
所指的箭头
由于message[1]
、message[9]
、message[17]
和message[25]
连接在ciphertext[1]
的末尾,它们形成了字符串'onom'
。这是网格的第二列。
当for
循环完成其余列的循环后,ciphertext
中的值为['Ceno',
'onom',
'mstm',
'me o',
'o sn',
'nio.',
' s ',
's c']
。有了字符串列的列表后,我们需要将它们连接在一起,形成一个字符串,这就是整个密文:'Cenoonommstmme oo snnio. s s c'
。
join()
字符串方法
第 39 行使用了join()
方法将ciphertext
的各个列字符串连接成一个字符串。对一个字符串值调用join()
方法,并获取一个字符串列表。它返回一个字符串,列表中的所有成员都由调用join()
的字符串连接。(如果您只想将字符串连接在一起,这是一个空白字符串。)在交互式 shell 中输入以下内容:
>>> eggs = ['dogs', 'cats', 'moose'] >>> ''.join(eggs) # ➊ 'dogscatsmoose' >>> ', '.join(eggs) # ➋ 'dogs, cats, moose' >>> 'XYZ'.join(eggs) # ➌ 'dogsXYZcatsXYZmoose'
当你在一个空字符串上调用join()
并加入列表eggs
➊ 时,你会得到列表的字符串,它们之间没有字符串。在某些情况下,你可能想要在一个列表中分离每个成员,以使其更具可读性,我们已经在 ➋ 通过在字符串', '
上调用join()
做到了这一点。这将在列表的每个成员之间插入字符串', '
。你可以在列表成员之间插入任何你想要的字符串,正如你在 ➌ 看到的。
返回值和返回语句
函数(或方法)调用总是计算出一个值。这是函数或方法调用返回的值,也称为函数的返回值。当您使用def
语句创建自己的函数时,return
语句会告诉 Python 该函数的返回值是什么。第 39 行是一个return
语句:
# Convert the ciphertext list into a single string value and return it: return ''.join(ciphertext)
第 39 行调用空白字符串上的join()
并将ciphertext
作为参数传递,因此ciphertext
列表中的字符串被连接成一个字符串。
返回语句示例
return
语句是return
关键字,后跟要返回的值。您可以使用一个表达式来代替一个值,如第 39 行所示。当您这样做时,返回值是该表达式计算的任何值。打开一个新的文件编辑器窗口,进入如下程序,保存为addNumbers.py
,然后按F5
运行:
addNumbers.py
def addNumbers(a, b): return a + b print(addNumbers(2, 40))
当您运行addNumbers.py
程序时,输出如下:
42
这是因为第 4 行的函数调用addNumbers(2, 40)
计算结果为42
。第 2 行addNumbers()
中的return
语句对表达式a + b
求值,然后返回求值结果。
返回加密的密文
在transpositonecrypt.py
程序中,encryptMessage()
函数的return
语句返回一个字符串值,该值是通过连接ciphertext
列表中的所有字符串而创建的。如果密文中的列表是['Ceno',``'onom',``'mstm',``'me o',``'o sn',``'nio.',``' s ',``'s c']
,那么join()
方法调用将返回'Cenoonommstmme oo snnio. s s c'
。这个最后的字符串,即加密代码的结果,由我们的encryptMessage()
函数返回。
使用函数的最大好处是程序员必须知道函数做什么,但不需要知道函数的代码是如何工作的。程序员可以理解,当他们调用encryptMessage()
函数并传递给它一个整数以及一个用于key
和message
参数的字符串时,函数调用计算出一个加密的字符串。他们不需要知道encryptMessage()
中的代码实际上是如何做到这一点的,这类似于你如何知道当你将一个字符串传递给print()
时,它将打印该字符串,即使你从未见过print()
函数的代码。
__name__
变量
你可以使用一个特殊的技巧将换位加密程序变成一个模块,这个技巧涉及到main()
函数和一个名为__name__
的变量。
当运行 Python 程序时,甚至在程序的第一行运行之前,__name__
(即name
之前的两个下划线和之后的两个下划线)就被赋予了字符串值'__main__'
(同样,main
之前和之后的两个下划线)。双下划线在 Python 中通常被称为dunder
,而__main__
被称为“双下划线main
双下划线”。
在脚本文件的末尾(更重要的是,在所有的def
语句之后),您希望有一些代码来检查__name__
变量是否被赋予了'__main__'
字符串。如果是,你要调用main()
函数。
第 44 行的if
语句实际上是运行程序时执行的第一行代码之一(在第 4 行的import
语句和第 6、21 行的def
语句之后)。
# If transpositionEncrypt.py is run (instead of imported as a module) call # the main() function: if __name__ == '__main__': main()
以这种方式设置代码的原因是,尽管 Python 在程序运行时将__name__
设置为'__main__'
,但如果程序是由另一个 Python 程序导入的,它会将其设置为字符串'transpositionEncrypt'
。类似于程序如何导入pyperclip
模块来调用其中的函数,其他程序可能想要导入transpositonecrypt.py
来调用其encryptMessage()
函数,而不运行main()
函数。当执行一个import
语句时,Python 通过添加。py
到文件名的末尾(这就是为什么import
pyperclip
导入pyperclip.py
文件)。这就是我们的程序如何知道它是作为主程序运行还是作为模块被不同的程序导入。(您将在第 9 章的中导入transpositonecrypt.py
作为一个模块。)
当您导入一个 Python 程序时,在程序执行之前,__name__
变量被设置为文件名的 before 部分。py
。当transpositonecrypt.py
程序被导入时,所有的def
语句都被运行(以定义导入程序想要使用的encryptMessage()
函数),但是main()
函数没有被调用,所以'Common
sense
is
not
so
common.'
的带密钥8
的加密代码没有被执行。
这就是为什么用myKey
密钥加密myMessage
字符串的代码在一个函数内部(这个函数按照惯例被命名为main()
)。当transpositonecrypt.py
被其他程序导入时main()
里面的这段代码不会运行,但是这些其他程序仍然可以调用它的encryptMessage()
函数。这就是函数的代码可以被其他程序重用的方式。
注
了解一个程序如何工作的一个有用的方法是在它运行时一步一步地跟踪它的执行。您可以使用在线程序跟踪工具查看 Hello 函数和换位密码加密程序的踪迹。跟踪工具将为您提供每行代码执行时程序正在做什么的可视化表示。
总结
咻!在本章中,你学习了几个新的编程概念。换位密码程序比第六章中的凯撒密码程序更复杂(但安全得多)。您在本章中学到的新概念、函数、数据类型和运算符使您能够以更复杂的方式操作数据。请记住,理解一行代码的大部分工作是按照 Python 的方式一步一步地求值它。
您可以将代码组织成称为函数的组,这些函数是用def
语句创建的。参数值可以作为函数的参数传递给函数。参数是局部变量。所有函数之外的变量都是全局变量。局部变量不同于全局变量,即使它们与全局变量同名。一个函数中的局部变量也与另一个函数中的局部变量分开,即使它们同名。
列表值可以存储多个其他值,包括其他列表值。许多可以在字符串上使用的操作(比如索引、切片和使用len()
函数)也可以在列表上使用。扩展赋值操作符为常规赋值操作符提供了一个很好的捷径。join()
方法可以连接包含多个字符串的列表以返回单个字符串。
如果你对这些编程概念还不太熟悉,最好回顾一下这一章。在第八章中,你将学习如何使用换位密码解密。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 用纸和笔,使用换位密码,用密钥 9 加密以下消息。为了您的方便,已经提供了字符数。
Underneath a huge oak tree there was of swine a huge company
(61 个字符)That grunted as they crunched the mast: For that was ripe and fell full fast
(77 个字符)Then they trotted away for the wind grew high: One acorn they left, and no more might you spy
(94 个字符)
- 在下面的程序中,每个垃圾邮件是全局变量还是局部变量?
spam = 42 def foo(): global spam spam = 99 print(spam)
- 下列每个表达式的值是多少?
[0, 1, 2, 3, 4][2] [[1, 2], [3, 4]][0] [[1, 2], [3, 4]][0][1] ['hello'][0][1] [2, 4, 6, 8, 10][1:3] list('Hello world!') list(range(10))[2]
- 下列每个表达式的值是多少?
len([2, 4]) len([]) len(['', '', '']) [4, 5, 6] + [1, 2, 3] 3 * [1, 2, 3] + [9] 42 in [41, 42, 42, 42]
- 四种扩展赋值运算符是什么?
八、用换位密码解密
“弱化加密或为加密设备和数据创建后门以供好人使用,实际上会产生漏洞供坏人利用。”
——蒂姆·库克,苹果公司首席执行官,2015 年
与凯撒密码不同,换位密码的解密过程不同于加密过程。在本章中,您将创建一个名为transpositionecrypt.py
的独立程序来处理解密。
本章涵盖的主题
- 用换位密码解密
round()
、math.ceil()
和math.floor()
函数- 布尔运算符
and
和or
- 真值表
如何用纸上的换位密码解密
假装你已经发送了密文Cenoonommstmme oo snnio. s s c
给朋友(并且他们已经知道秘密密钥是 8)。他们解密密文的第一步是计算他们需要画的盒子的数量。要确定这个数字,他们必须用密钥除密文的长度,如果结果不是整数,就四舍五入到最接近的整数。密文的长度是 30 个字符(与明文相同),密钥是 8,所以 30 除以 8 是 3.75。
图 8-1:通过反转网格解密消息
将 3.75 四舍五入为 4,你的朋友将画出一个由四列(他们刚刚计算出的数字)和八行(密钥)组成的格子。
你的朋友还需要计算需要遮蔽的箱子数量。使用盒子的总数(32),他们减去密文的长度(30):32–30 = 2
。它们在最右边列的底部的两个方框中着色。
然后他们开始填充盒子,在每个盒子里放一个密文字符。从左上角开始,它们向右填充,就像加密时一样。密文是Cenoonommstmme oo snnio. s s c
,所以Ceno
在第一行,onom
在第二行,依此类推。当它们完成后,这些方框看起来将像图 8-1 中的(一个/代表一个空格)。
你收到密文的朋友注意到,当他们阅读这些列中的文本时,原始的明文被恢复:“常识并不那么普遍。”
概括地说,解密的步骤如下:
- 通过将消息的长度除以密钥,然后向上舍入,计算出所需的列数。
- 按列和行绘制方框。使用您在步骤 1 中计算的列数。行数与密钥相同。
- 通过计算框的总数(行数乘以列数)并减去密文消息的长度来计算要加阴影的框的数量。
- 在最右边一栏的底部画出你在第三步中计算出的盒子数量。
- 从第一行开始,从左到右填写密文的字符。跳过任何阴影框。
- 从上到下读取最左边的列,并在每一列中继续这样做,从而获得明文。
请注意,如果您使用不同的密钥,您将绘制错误的行数。即使您正确地遵循了解密过程中的其他步骤,明文也将是随机垃圾(类似于您在凯撒密码中使用了错误的密钥)。
换位密码解密程序的源代码
点击文件 -> 新建文件,打开一个新建文件编辑器窗口。在文件编辑器中输入以下代码,然后保存为transpositondecrypt.py
。记得把pyperclip.py
放在同一个目录下。按F5
运行程序。
transpositondecrypt.py
# Transposition Cipher Decryption # https://www.nostarch.com/crackingcodes/ (BSD Licensed) import math, pyperclip def main(): myMessage = 'Cenoonommstmme oo snnio. s s c' myKey = 8 plaintext = decryptMessage(myKey, myMessage) # Print with a | (called "pipe" character) after it in case # there are spaces at the end of the decrypted message: print(plaintext + '|') pyperclip.copy(plaintext) def decryptMessage(key, message): # The transposition decrypt function will simulate the "columns" and # "rows" of the grid that the plaintext is written on by using a list # of strings. First, we need to calculate a few values. # The number of "columns" in our transposition grid: numOfColumns = int(math.ceil(len(message) / float(key))) # The number of "rows" in our grid: numOfRows = key # The number of "shaded boxes" in the last "column" of the grid: numOfShadedBoxes = (numOfColumns * numOfRows) - len(message) # Each string in plaintext represents a column in the grid: plaintext = [''] * numOfColumns # The column and row variables point to where in the grid the next # character in the encrypted message will go: column = 0 row = 0 for symbol in message: plaintext[column] += symbol column += 1 # Point to the next column. # If there are no more columns OR we're at a shaded box, go back # to the first column and the next row: if (column == numOfColumns) or (column == numOfColumns - 1 and row >= numOfRows - numOfShadedBoxes): column = 0 row += 1 return ''.join(plaintext) # If transpositionDecrypt.py is run (instead of imported as a module), # call the main() function: if __name__ == '__main__': main()
换位密码解密程序的示例运行
当您运行transpositondecrypt.py
程序时,它会产生以下输出:
Common sense is not so common.|
如果您想要解密不同的消息或使用不同的密钥,请更改第 7 行和第 8 行中分配给myMessage
和myKey
变量的值。
导入模块并设置main()
函数
transpositonecrypt.py
程序的第一部分与transpositonecrypt.py
的第一部分类似:
# Transposition Cipher Decryption # https://www.nostarch.com/crackingcodes/ (BSD Licensed) import math, pyperclip def main(): myMessage = 'Cenoonommstmme oo snnio. s s c' myKey = 8 plaintext = decryptMessage(myKey, myMessage) # Print with a | (called "pipe" character) after it in case # there are spaces at the end of the decrypted message: print(plaintext + '|') pyperclip.copy(plaintext)
第 4 行的模块pyperclip
和另一个名为math
的模块一起被导入。如果用逗号分隔模块名,可以用一个import
语句导入多个模块。
我们在第 6 行开始定义的main()
函数创建名为myMessage
和myKey
的变量,然后调用解密函数decryptMessage
()
。decryptMessage()
的返回值是密文和密钥的解密明文。这被存储在一个名为plaintext
的变量中,该变量被打印到屏幕上(在消息末尾有一个管道字符,以防消息末尾有空格),然后被复制到剪贴板。
用密钥解密消息
decryptMessage()
函数遵循第 100 页中描述的六个解密步骤,然后以字符串形式返回解密结果。为了使解密更容易,我们将使用来自math
模块的函数,该模块是我们在程序的前面导入的。
round()
、math.ceil()
和math.floor()
函数
Python 的round()
函数会将浮点数(带小数点的数字)四舍五入为最接近的整数。math.ceil()
和math.floor()
函数(在 Python 的math
模块中)将分别向上和向下舍入一个数字。
当使用/
运算符对数字进行除法运算时,表达式返回一个浮点数(带小数点的数字)。即使数字被均匀地分割,也会发生这种情况。例如,在交互式 shell 中输入以下内容:
>>> 21 / 7 3.0 >>> 22 / 5 4.4
如果你想把一个数字四舍五入到最接近的整数,你可以使用round()
函数。要查看该函数如何工作,请输入以下内容:
>>> round(4.2) 4 >>> round(4.9) 5 >>> round(5.0) 5 >>> round(22 / 5) 4
如果你只想取整,你需要使用math.ceil()
函数,它代表“天花板”如果您只想向下舍入,请使用math.floor()
。这些函数存在于math
模块中,您需要在调用它们之前导入它们。在交互式 shell 中输入以下内容:
>>> import math >>> math.floor(4.0) 4 >>> math.floor(4.2) 4 >>> math.floor(4.9) 4 >>> math.ceil(4.0) 4 >>> math.ceil(4.2) 5 >>> math.ceil(4.9) 5
math.floor()
函数将总是从浮点数中移除小数点,并将其转换为整数以向下舍入,而math.ceil()
将递增浮点数的 1 位,并将其转换为整数以向上舍入。
decryptMessage()
函数
decryptMessage()
函数将每个解密步骤实现为 Python 代码。它接受一个整数key
和一个message
字符串作为参数。math.ceil()
函数用于decryptMessage
的换位解密,计算列数时确定需要制作的盒数:
def decryptMessage(key, message): # The transposition decrypt function will simulate the "columns" and # "rows" of the grid that the plaintext is written on by using a list # of strings. First, we need to calculate a few values. # The number of "columns" in our transposition grid: numOfColumns = int(math.ceil(len(message) / float(key))) # The number of "rows" in our grid: numOfRows = key # The number of "shaded boxes" in the last "column" of the grid: numOfShadedBoxes = (numOfColumns * numOfRows) - len(message)
第 25 行通过将len(message)
除以key
中的整数来计算列数。这个值被传递给math.ceil()
函数,返回值被存储在numOfColumns
中。为了让这个程序与 Python 2 兼容,我们调用了float()
,因此key
变成了一个浮点值。在 Python 2 中,两个整数相除的结果会自动向下舍入。调用float()
避免了这种行为,而不影响 Python 3 下的行为。
第 27 行计算行数,这是存储在key
中的整数。该值存储在变量numOfRows
中。
第 29 行计算网格中阴影框的数量,即列数乘以行数,减去消息的长度。
如果你在解密Cenoonommstmme oo snnio. s s c
键为 8,numOfColumns
设置为4
,numOfRows
设置为8
,numOfShadedBoxes
设置为2
。
就像加密程序有一个名为ciphertext
的变量,它是代表密文网格的字符串列表一样,decryptMessage()
也有一个名为plaintext
的字符串列表变量:
# Each string in plaintext represents a column in the grid: plaintext = [''] * numOfColumns
这些字符串最初是空白的,网格的每一列都有一个字符串。使用列表复制,您可以将一个包含一个空白字符串的列表乘以numOfColumns
,得到一个包含几个空白字符串的列表,其数量等于所需的列数。
请记住,这个plaintext
不同于main()
函数中的plaintext
。因为decryptMessage
()
函数和main()
函数都有自己的局部作用域,所以函数的plaintext
变量是不同的,只是碰巧具有相同的名称。
请记住,'Cenoonommstmme oo snnio. s s c'
示例的网格看起来像第 100 页上的图 8-1 。
plaintext
变量将有一个字符串列表,列表中的每个字符串将是这个网格的一个单独的列。对于这个解密,您希望plaintext
以下面的值结束:
['Common s', 'ense is ', 'not so c', 'ommon.']
这样,您可以将列表中的所有字符串连接在一起,以返回'Common
sense
is
not
so
common.'
字符串值。
为了制作列表,我们首先需要将message
中的每个符号一次一个地放在plaintext
列表中的正确字符串中。我们将创建两个名为column
和row
的变量来跟踪message
中下一个字符应该去的列和行;这些变量应该从第一列和第一行的0
开始。第 36 行和第 37 行是这样做的:
# The column and row variables point to where in the grid the next # character in the encrypted message will go: column = 0 row = 0
第 39 行开始一个for
循环,遍历message
字符串中的字符。在这个循环中,代码将调整column
和row
变量,以便将symbol
连接到plaintext
列表中的正确字符串:
for symbol in message: plaintext[column] += symbol column += 1 # Point to the next column.
第 40 行将symbol
连接到plaintext
列表中索引column
处的字符串,因为plaintext
中的每个字符串代表一列。然后第 41 行将1
加到column
(也就是说,它递增column
),这样在循环的下一次迭代中,symbol
将被连接到plaintext
列表中的下一个字符串。
我们已经处理了递增的column
和row
,但是在某些情况下我们还需要将变量重置为0
。要理解这样做的代码,您需要理解布尔运算符。
布尔运算符
布尔运算符比较布尔值(或求值为布尔值的表达式)并求值为布尔值。布尔运算符and
和or
可以帮助您为if
和while
语句形成更复杂的条件。and
运算符连接两个表达式,如果两个表达式的计算结果都为True
,则计算结果为True
。or
运算符连接两个表达式,如果一个或两个表达式的计算结果为True
,则计算结果为True
;否则,这些表达式的计算结果为False
。在交互式 shell 中输入以下内容,看看and
操作符是如何工作的:
>>> 10 > 5 and 2 < 4 True >>> 10 > 5 and 4 != 4 False
第一个表达式的计算结果为True
,因为and
操作符两边的表达式都计算结果为True
。换句话说,表达式10 > 5 and 2 < 4
计算为True and True
,后者又计算为True
。
然而,在第二个表达式中,虽然10 > 5
的计算结果是True
,但是表达式4 != 4
的计算结果是False
。这意味着表达式计算结果为True and False
。因为两个表达式都必须是True
,以便and
操作符计算出True
,所以整个表达式计算出False
。
如果您忘记了布尔运算符是如何工作的,您可以查看它的真值表,它显示了基于所使用的运算符,布尔值的不同组合的计算结果。表 8-1 是and
运算符的真值表。
表 8-1:和运算符真值表
A 和 B | 求值为 |
True and True |
True |
True and False |
False |
False and True |
False |
False and False |
False |
要查看or
操作符是如何工作的,请在交互式 shell 中输入以下内容:
>>> 10 > 5 or 4 != 4 True >>> 10 < 5 or 4 != 4 False
当你使用or
操作符时,只有表达式的一边必须是True
,这样or
操作符才能将整个表达式计算为True
,这就是为什么10 > 5 or 4 != 4
计算为True
。但是,因为表达式10 < 5
和表达式4 != 4
都是False
,所以第二个表达式的计算结果是False or False
,而后者的计算结果是False
。
or
运算符真值表见表 8-2 。
表 8-2:或运算符真值表
A 或 B | 求值为 |
True or True |
True |
True or False |
True |
False or True |
True |
False or False |
False |
第三个布尔运算符是not
。not
运算符的计算结果是它所运算的值的相反布尔值。所以not True
是False
,not False
是True
。在交互式 shell 中输入以下内容:
>>> not 10 > 5 False >>> not 10 < 5 True >>> not False True >>> not not False False >>> not not not not not False True
正如您在最后两个表达式中看到的,您甚至可以使用多个not
操作符。not
运算符真值表见表 8-3 。
表 8-3:非运算符真值表
非 A | 求值为 |
not True |
False |
not False |
True |
作为快捷方式的and
和or
运算符
类似于for
循环让你用更少的代码完成与while
循环相同的任务,and
和or
操作符也让你缩短代码。在交互式 shell 中输入以下两段具有相同结果的代码:
>>> if 10 > 5: ... if 2 < 4: ... print('Hello!') ... Hello! >>> if 10 > 5 and 2 < 4: ... print('Hello!') ... Hello!
and
操作符可以代替两个if
语句分别检查表达式的每个部分(其中第二个if
语句在第一个if
语句的块内)。
您也可以用or
操作符替换if
和elif
语句。要尝试一下,请在交互式 shell 中输入以下内容:
>>> if 4 != 4: ... print('Hello!') ... elif 10 > 5: ... print('Hello!') ... Hello! >>> if 4 != 4 or 10 > 5: ... print('Hello!') ... Hello!
if
和elif
语句将分别检查表达式的不同部分,而or
操作符可以在一行中检查这两个语句。
布尔运算符的运算顺序
你知道数学运算符是有运算顺序的,and
、or
、not
运算符也是如此。首先求值not
,然后求值and
,然后求值or
。在交互式 shell 中输入以下内容:
>>> not False and False # not False evaluates first False >>> not (False and False) # (False and False) evaluates first True
在第一行代码中,not False
首先被求值,所以表达式变成了True and False
,它求值为False
。在第二行,括号先被求值,甚至在not
运算符之前,所以False and False
被求值为False
,表达式变成not (False)
,也就是True
。
调整列和行变量
现在你知道了布尔运算符是如何工作的,你可以学习如何在transpositonencrypt.py
中重置column
和row
变量。
在两种情况下,您会希望将column
重置为0
,以便在循环的下一次迭代中,symbol
被添加到plaintext
中列表的第一个字符串中。在第一种情况下,如果column
增加到超过plaintext
中的最后一个索引,您就要这样做。在这种情况下,column
中的值将等于numOfColumns
。(记住plaintext
中的最后一个索引将是numOfColumns
减一。所以当column
等于numOfColumns
时,已经过了最后一个索引。)
第二种情况是如果column
在最后一个索引处,并且row
变量指向最后一列中有阴影框的行。作为一个直观的例子,在图 8-2 中显示了列索引在顶部、行索引在底部的解密网格。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-twv1QZC2-1692873504740)(https://gitcode.net/OpenDocCN/invent-with-python-zh/-/raw/master/docs/cracking/img/70da1c2a7fc02ef810347e4794e56122.png)]
图 8-2:列和行索引的解密网格
您可以看到阴影框位于第 6 行和第 7 行的最后一列(其索引为numOfColumns - 1
)。要计算哪些行索引可能有阴影框,请使用表达式row >= numOfRows - numOfShadedBoxes
。在我们的八行示例中(索引为 0 到 7),第 6 行和第 7 行是阴影的。无阴影框的数量是总行数(在我们的例子中是 8)减去阴影框的数量(在我们的例子中是 2)。如果电流row
等于或大于这个数字(8–2 = 6
),我们可以知道我们有一个阴影框。如果这个表达式是True
,column
也等于numOfColumns
- 1
,那么 Python 遇到了阴影框;此时,您想要为下一次迭代将column
重置为0
:
# If there are no more columns OR we're at a shaded box, go back # to the first column and the next row: if (column == numOfColumns) or (column == numOfColumns - 1 and row >= numOfRows - numOfShadedBoxes): column = 0 row += 1
这两种情况就是为什么第 45 行的条件是(column ==
numOfColumns
) or (column == numOfColumns - 1 and row >= numOfRows -
numOfShadedBoxes
)
。尽管这看起来像一个很大很复杂的表达式,但是记住你可以把它分解成更小的部分。表达式(
column
== numOfColumns)
检查列变量是否超出索引范围,表达式的第二部分检查我们是否在一个阴影框的column
和row
索引处。如果这两个表达式中的任何一个为真,执行的代码块将通过将column
设置为0
来将column
重置为第一列。您还将增加变量row
。
当第 39 行的for
循环结束对message
中每个字符的循环时,plaintext
列表的字符串已经被修改,所以它们现在是解密后的顺序(如果使用了正确的密钥)。第 49 行的join()
字符串方法将plaintext
列表中的字符串连接在一起(每个字符串之间有一个空白字符串):
return ''.join(plaintext)
第 49 行还返回了函数decryptMessage()
返回的字符串。
对于解密,plaintext
将是['Common s', 'ense is ', 'not so c',
'ommon.']
,所以''.join(plaintext)
将求值为'Common sense is not so common.'
调用main()
函数
我们的程序在导入模块并执行def
语句后运行的第一行是第 54 行的if
语句。
# If transpositionDecrypt.py is run (instead of imported as a module), # call the main() function: if __name__ == '__main__': main()
与换位加密程序一样,Python 通过检查__name__
变量是否被设置为字符串值'__main__'
来检查该程序是否已经运行(而不是由不同的程序导入)。如果是,代码执行main()
函数。
总结
解密程序到此结束。大部分程序都在decryptMessage()
函数中。我们编写的程序可以加密和解密“常识并不常见”这一信息用密钥 8;但是,您应该尝试其他几种消息和密钥,以检查加密然后解密的消息是否会产生相同的原始消息。如果你没有得到你期望的结果,你就会知道要么是加密代码要么是解密代码不起作用。在第九章中,我们将通过编写一个程序来测试我们的程序,从而自动化这个过程。
如果你想看到换位密码解密程序执行的一步一步的踪迹,请访问www.nostarch.com/crackingcodes
。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 使用纸和笔,用密钥 9 解密以下消息。
▪
标记一个空格。总字符数已经帮你统计好了。
H▪cb▪▪irhdeuousBdi▪▪▪prrtyevdgp▪nir▪▪eerit▪eatoreechadihf▪pak en▪ge▪b▪te▪dih▪aoa.da▪tts▪tn (89 characters)
A▪b▪▪drottthawa▪nwar▪eci▪t▪nlel▪ktShw▪leec,hheat▪.na▪▪e▪soog mah▪a▪▪ateniAcgakh▪dmnor▪▪ (86 characters)
Bmmsrl▪dpnaua!toeboo'ktn▪uknrwos.▪yaregonr▪w▪nd,tu▪▪oiady▪h gtRwt▪▪▪A▪hhanhhasthtev▪▪e▪t▪e▪▪eo (93 characters)
- 当您在交互式 shell 中输入下面的代码时,每行会显示什么?
>>> math.ceil(3.0) >>> math.floor(3.1) >>> round(3.1) >>> round(3.5) >>> False and False >>> False or False >>> not not True
- 画出
and
、or
和not
运算符的完整真值表。 - 以下哪一项是正确的?
if __name__ == '__main__': if __main__ == '__name__': if _name_ == '_main_': if _main_ == '_name_':
九、编写一个程序来测试你的程序
“安装有朝一日可能会促进警察国家的技术是糟糕的公民卫生。”
——布鲁斯·施奈尔,秘密与谎言
换位程序似乎在用不同的密钥加密和解密不同的信息方面工作得很好,但是你怎么知道它们总是工作呢?除非你用各种各样的message
和key
参数值测试encryptMessage() and decryptMessage()
函数,否则你不能绝对肯定程序总是工作的。但是这要花很多时间,因为你必须在加密程序中输入信息,设置密钥,运行加密程序,将密文粘贴到解密程序中,设置密钥,然后运行解密程序。您还需要用几个不同的密钥和消息重复这个过程,这导致了许多令人厌烦的工作!
相反,让我们编写另一个生成随机消息和随机密钥的程序来测试密码程序。这个新程序会用来自transpositonecrypt.py
的encryptMessage()
对消息进行加密,然后将密文从transpositonecrypt.py
传给decryptMessage()
。如果decryptMessage()
返回的明文与原始消息相同,程序就知道加密和解密程序工作了。使用另一个程序自动测试一个程序的过程叫做自动化测试。
需要尝试几种不同的信息和组合密钥,但计算机只需一分钟左右的时间就可以测试成千上万种组合。如果所有这些测试都通过了,您就可以更加确定您的代码能够正常工作。
本章涵盖的主题
random.randint()
函数random.seed()
函数- 列表的引用
copy.deepcopy()
函数random.shuffle()
函数- 随机打乱字符串
sys.exit()
函数
换位密码测试器程序的源代码
选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,保存为transpositionTest.py
。然后按F5
运行程序。
换位
Test.py
# Transposition Cipher Test # https://www.nostarch.com/crackingcodes/ (BSD Licensed) import random, sys, transpositionEncrypt, transpositionDecrypt def main(): random.seed(42) # Set the random "seed" to a static value. for i in range(20): # Run 20 tests. # Generate random messages to test. # The message will have a random length: message = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' * random.randint(4, 40) # Convert the message string to a list to shuffle it: message = list(message) random.shuffle(message) message = ''.join(message) # Convert the list back to a string. print('Test #%s: "%s..."' % (i + 1, message[:50])) # Check all possible keys for each message: for key in range(1, int(len(message)/2)): encrypted = transpositionEncrypt.encryptMessage(key, message) decrypted = transpositionDecrypt.decryptMessage(key, encrypted) # If the decryption doesn't match the original message, display # an error message and quit: if message != decrypted: print('Mismatch with key %s and message %s.' % (key, message)) print('Decrypted as: ' + decrypted) sys.exit() print('Transposition cipher test passed.') # If transpositionTest.py is run (instead of imported as a module) call # the main() function: if __name__ == '__main__': main()
换位密码测试程序的示例运行
当您运行transpositionTest.py
程序时,输出应该是这样的:
Test #1: "JEQLDFKJZWALCOYACUPLTRRMLWHOBXQNEAWSLGWAGQQSRSIUIQ..." Test #2: "SWRCLUCRDOMLWZKOMAGVOTXUVVEPIOJMSBEQRQOFRGCCKENINV..." Test #3: "BIZBPZUIWDUFXAPJTHCMDWEGHYOWKWWWSJYKDQVSFWCJNCOZZA..." Test #4: "JEWBCEXVZAILLCHDZJCUTXASSZZRKRPMYGTGHBXPQPBEBVCODM..." --snip-- Test #17: "KPKHHLPUWPSSIOULGKVEFHZOKBFHXUKVSEOWOENOZSNIDELAWR..." Test #18: "OYLFXXZENDFGSXTEAHGHPBNORCFEPBMITILSSJRGDVMNSOMURV..." Test #19: "SOCLYBRVDPLNVJKAFDGHCQMXIOPEJSXEAAXNWCCYAGZGLZGZHK..." Test #20: "JXJGRBCKZXPUIEXOJUNZEYYSEAEGVOJWIRTSSGPUWPNZUBQNDA..." Transposition cipher test passed.
测试器程序通过将transpositonecrypt.py
和transpositonecrypt.py
程序作为模块导入来工作。然后测试程序从加密和解密程序中调用encryptMessage()
和decryptMessage()
。测试程序创建一个随机消息并选择一个随机密钥。消息只是随机的字母并不重要,因为程序只需要检查加密然后解密消息的结果是原始消息。
使用一个循环,程序重复这个测试 20 次。如果从transpositionDecrypt()
返回的字符串与原始消息不同,程序会打印一个错误并退出。
让我们更详细地探索源代码。
导入模块
程序从导入模块开始,包括您已经看到的 Python 自带的两个模块,random
和sys
:
# Transposition Cipher Test # https://www.nostarch.com/crackingcodes/ (BSD Licensed) import random, sys, transpositionEncrypt, transpositionDecrypt
我们还需要导入换位密码程序(即transpositonecrypt.py
和transpositonecrypt.py
),只需输入它们的名称,而不需要输入.py
扩展。
创建伪随机数
为了创建随机数来生成消息和密钥,我们将使用random
模块的seed()
函数。在我们深入研究种子做什么之前,让我们通过尝试random.randint()
函数来看看随机数在 Python 中是如何工作的。我们稍后将在程序中使用的random.randint()
函数接受两个整数参数,并返回这两个整数之间的一个随机整数(包括整数)。在交互式 shell 中输入以下内容:
>>> import random >>> random.randint(1, 20) 20 >>> random.randint(1, 20) 18 >>> random.randint(100, 200) 107
当然,您得到的数字可能与这里显示的不同,因为它们是随机数。
但是 Python 的random.randint()
函数生成的数字并不是真正随机的。它们是由伪随机数发生器算法产生的,该算法采用一个初始数字,并根据一个公式产生其他数字。
伪随机数发生器开始使用的初始数字称为种子。如果您知道种子,生成器生成的其余数字是可预测的,因为当您将种子设置为某个特定数字时,相同的数字将以相同的顺序生成。这些看起来随机但可预测的数字被称为伪随机数。没有设置种子的 Python 程序使用计算机的当前时钟时间来设置种子。你可以通过调用random.seed()
函数来重置 Python 的随机种子。
要证明伪随机数不是完全随机的,请在交互式 shell 中输入以下内容:
>>> import random >>> random.seed(42) # ➊ >>> numbers = [] # ➋ >>> for i in range(20): ... numbers.append(random.randint(1, 10)) ... [2, 1, 5, 4, 4, 3, 2, 9, 2, 10, 7, 1, 1, 2, 4, 4, 9, 10, 1, 9] # ➌ >>> random.seed(42) >>> numbers = [] >>> for i in range(20): ... numbers.append(random.randint(1, 10)) ... [2, 1, 5, 4, 4, 3, 2, 9, 2, 10, 7, 1, 1, 2, 4, 4, 9, 10, 1, 9] # ➍
在这段代码中,我们使用相同的种子两次生成 20 个数字。首先,我们导入random
并将种子设置为42
➊。然后我们建立一个名为numbers
➋ 的列表,在那里我们将存储我们生成的数字。我们使用一个for
循环来生成 20 个数字,并将每个数字添加到numbers
列表中,我们打印这个列表,这样我们就可以看到生成的每个数字 ➌。
当 Python 的伪随机数发生器的种子设置为42
时,1
和10
之间的第一个“随机”数将始终是2
。第二个数字永远是1
,第三个数字永远是5
,依此类推。当您将种子重置为42
并再次使用该种子生成数字时,从random.randint()
返回相同的伪随机数集,您可以通过比较 ➌ 和 ➍ 的numbers
列表看到这一点。
在后面的章节中,随机数对于密码将变得很重要,因为它们不仅用于测试密码,还用于更复杂密码的加密和解密。随机数如此重要,以至于加密软件中一个常见的安全缺陷就是使用可预测的随机数。如果你程序中的随机数是可以预测的,密码分析员就可以用这些信息来破解你的密码。
以真正随机的方式选择加密密钥对于密码的安全性是必要的,但是对于其他用途,比如这个代码测试,伪随机数就可以了。我们将使用伪随机数为我们的测试程序生成测试字符串。你可以通过使用random.SystemRandom().randint()
函数用 Python 生成真正的随机数,你可以在/www.nostarch.com/crackingcodes
了解更多。
创建随机字符串
现在你已经学会了如何使用random.randint()
和random.seed()
来创建随机数,让我们回到源代码。为了完全自动化我们的加密和解密程序,我们需要自动生成随机的字符串消息。
为此,我们将在消息中使用一个字符串,随机复制几次,并将其存储为一个字符串。然后,我们将得到重复字符的字符串,并将它们打乱,使它们更加随机。我们将为每个测试生成一个新的随机字符串,这样我们就可以尝试许多不同的字母组合。
首先,让我们设置main()
函数,它包含测试密码程序的代码。它首先为伪随机字符串设置一个种子:
def main(): random.seed(42) # Set the random "seed" to a static value.
通过调用random.seed()
设置随机种子对测试程序很有用,因为您想要可预测的数字,所以每次程序运行时都选择相同的伪随机消息和密钥。因此,如果您注意到一条消息未能正确加密和解密,您将能够重现这个失败的测试用例。
接下来,我们将使用一个for
循环复制一个字符串。
将一个字符串随机复制若干次
我们将使用一个for
循环来运行 20 个测试,并生成我们的随机消息:
for i in range(20): # Run 20 tests. # Generate random messages to test. # The message will have a random length: message = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' * random.randint(4, 40)
每次for
循环迭代时,程序都会创建并测试一条新消息。我们希望这个程序运行多个测试,因为我们尝试的测试越多,我们就越能确定程序是有效的。
第 13 行是测试代码的第一行,创建一条随机长度的消息。它获取一串大写字母,并使用randint()
和字符串复制在4
和40
之间随机复制该字符串。然后,它将新字符串存储在message
变量中。
如果我们让message
字符串保持现在的样子,它将永远只是重复了随机次数的字母字符串。因为我们想测试不同的字符组合,我们需要更进一步,打乱message
中的字符。要做到这一点,让我们先学习更多关于列表的知识。
列表变量与引用
变量存储列表与存储其他值不同。变量将包含对列表的引用,而不是列表本身。一个引用是指向某个数据位的值,一个列表引用是指向一个列表的值。这导致代码的行为略有不同。
你已经知道变量存储字符串和整数值。在交互式 shell 中输入以下内容:
>>> spam = 42 >>> cheese = spam >>> spam = 100 >>> spam 100 >>> cheese 42
我们将42
赋给spam
变量,然后复制spam
中的值,赋给变量cheese
。当我们稍后将spam
中的值更改为100
时,新数字不会影响cheese
中的值,因为spam
和cheese
是存储不同值的不同变量。
但是列表不是这样工作的。当我们把一个列表赋给一个变量时,我们实际上是把一个列表引用赋给了这个变量。下面的代码使这种区别更容易理解。在交互式 shell 中输入以下代码:
>>> spam = [0, 1, 2, 3, 4, 5] # ➊ >>> cheese = spam # ➋ >>> cheese[1] = 'Hello!' # ➌ >>> spam [0, 'Hello!', 2, 3, 4, 5] >>> cheese [0, 'Hello!', 2, 3, 4, 5]
您可能会觉得这段代码很奇怪。代码只改变了cheese
列表,但是cheese
和spam
列表都改变了。
当我们创建列表 ➊ 时,我们在spam
变量中为它分配一个引用。但是下一行 ➋ 只复制了spam
到cheese
中的列表引用,而不是列表值。这意味着存储在spam
和cheese
中的值现在都指向同一个列表。只有一个底层列表,因为实际的列表实际上从未被复制过。所以当我们修改cheese
➌ 的第一个元素时,我们修改的是spam
引用的同一个列表。
请记住,变量就像包含值的盒子。但是列表变量实际上并不包含列表——它们包含对列表的引用。(这些引用会有 Python 内部使用的 ID 号,但是你可以忽略它们。)用盒子来比喻变量,图 9-1 显示了当一个列表被分配给spam
变量时会发生什么。
图 9-1:spam = [0, 1, 2, 3, 4, 5]
存储一个列表的引用,而不是实际的列表。
然后,在图 9-2 中,将spam
中的引用复制到cheese
。只有一个新的引用被创建并存储在cheese
中,而不是一个新的列表。请注意,这两个引用指的是同一个列表。
图 9-2:spam = cheese
复制引用,非列表。
当我们改变cheese
引用的列表时,spam
引用的列表也会改变,因为cheese
和spam
引用的是同一个列表。你可以在图 9-3 中看到这一点。
虽然 Python 变量在技术上包含了对列表值的引用,但人们经常会随口说变量“包含列表”
图 9-3:cheese[1] = 'Hello!'
修改两个变量引用的列表。
引用传递
引用对于理解参数如何传递给函数特别重要。当一个函数被调用时,参数的值被复制到形参变量中。对于列表,这意味着引用的副本用于参数。要查看此操作的结果,请打开一个新的文件编辑器窗口,输入以下代码,并将其保存为passingReference.py
。按F5
运行代码。
passingReference.py
def eggs(someParameter): someParameter.append('Hello') spam = [1, 2, 3] eggs(spam) print(spam)
当您运行代码时,请注意当调用eggs()
时,返回值并不用于为spam
分配新值。而是直接修改列表。运行时,该程序产生以下输出:
[1, 2, 3, 'Hello']
尽管spam
和someParameter
包含不同的引用,但它们都引用同一个列表。这就是为什么函数内部的append('Hello')
方法调用即使在函数调用返回后也会影响列表。
请记住这种行为:忘记 Python 以这种方式处理列表变量会导致令人困惑的错误。
使用copy.deepcopy()
复制一个列表
如果您想要复制一个列表值,您可以导入copy
模块来调用copy.deepcopy()
函数,该函数返回一个单独的列表副本:
>>> spam = [0, 1, 2, 3, 4, 5] >>> import copy >>> cheese = copy.deepcopy(spam) >>> cheese[1] = 'Hello!' >>> spam [0, 1, 2, 3, 4, 5] >>> cheese [0, 'Hello!', 2, 3, 4, 5]
因为使用了copy.deepcopy()
函数将spam
中的列表复制到cheese
,所以当cheese
中的一项发生变化时,spam
不受影响。
当我们破解简单的替换密码时,我们将在第 17 章中使用这个函数。
random.shuffle()
函数
有了关于引用如何工作的基础,您现在可以理解我们接下来要使用的random.shuffle()
函数是如何工作的。random.shuffle()
函数是random
模块的一部分,它接受一个列表参数,并随机重新排列其条目。在交互式 shell 中输入以下内容,看看random.shuffle()
是如何工作的:
>>> import random >>> spam = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> spam [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> random.shuffle(spam) >>> spam [3, 0, 5, 9, 6, 8, 2, 4, 1, 7] >>> random.shuffle(spam) >>> spam [1, 2, 5, 9, 4, 7, 0, 3, 6, 8]
需要注意的一个重要细节是shuffle()
不返回列表值。相反,它改变传递给它的列表值(因为shuffle()
直接从传递给它的列表引用值修改列表)。shuffle()
函数在处修改列表*,这就是为什么我们执行random.shuffle(spam)
而不是spam = random.shuffle(spam)
。*
随机打乱字符串
让我们回到transpositionTest.py
。为了打乱字符串值中的字符,我们首先需要使用list()
将字符串转换成一个列表:
# Convert the message string to a list to shuffle it: message = list(message) random.shuffle(message) message = ''.join(message) # Convert the list back to a string.
从list()
返回的值是一个列表值,传递给它的是字符串中每个字符的一个字符串;所以在第 16 行,我们将message
重新赋值为它的字符列表。接下来,shuffle()
将message
中的项目顺序随机化。然后程序使用join()
字符串方法将字符串列表转换回字符串值。这种对message
字符串的混排允许我们测试许多不同的消息。
测试每条消息
既然已经生成了随机消息,程序就用它来测试加密和解密函数。我们将让程序打印一些反馈,这样我们可以在测试时看到它在做什么:
print('Test #%s: "%s..."' % (i + 1, message[:50]))
第 20 行有一个print()
调用,显示程序的测试号(我们需要给i
加 1,因为i
从0
开始,测试号应该从1
开始)。因为message
中的字符串可能很长,所以我们使用字符串切片只显示message
的前 50 个字符。
第 20 行也使用了字符串插值。i + 1
求值的值替换字符串中的第一个%s
,message[:50]
求值的值替换第二个%s
。使用字符串插值时,请确保字符串中的%s
的数量与它后面的括号中的值的数量相匹配。
接下来,我们将测试所有可能的密钥。尽管凯撒密码的密钥可以是从0
到65
的整数(符号集的长度),换位密码的密钥可以在1
和消息长度的一半之间。第 23 行上的for
循环使用密钥1
运行测试代码,直到(但不包括)消息的长度。
# Check all possible keys for each message: for key in range(1, int(len(message)/2)): encrypted = transpositionEncrypt.encryptMessage(key, message) decrypted = transpositionDecrypt.decryptMessage(key, encrypted)
第 24 行使用encryptMessage()
函数加密message
中的字符串。因为这个函数在transpositonecrypt.py
文件里面,所以我们需要在函数名前面加上transpositionEncrypt.
(以句号结尾)。
从encryptMessage()
返回的加密字符串然后被传递给decryptMessage()
。我们需要对两个函数调用使用同一个密钥。来自decryptMessage()
的返回值存储在一个名为decrypted
的变量中。如果函数正常,message
中的字符串应该与decrypted
中的字符串相同。接下来我们将看看程序如何检查这一点。
检查密码是否有效并结束程序
在我们加密和解密消息之后,我们需要检查两个过程是否都正常工作。为此,我们只需检查原始消息是否与解密后的消息相同。
# If the decryption doesn't match the original message, display # an error message and quit: if message != decrypted: print('Mismatch with key %s and message %s.' % (key, message)) print('Decrypted as: ' + decrypted) sys.exit() print('Transposition cipher test passed.')
第 29 行测试message
和decrypted
是否相等。如果不是,Python 会在屏幕上显示一条错误消息。第 30 行和第 31 行打印了key
、message
和decrypted
值作为反馈,帮助我们找出哪里出错了。然后程序退出。
通常情况下,程序在执行到代码末尾时退出,并且不再有代码行要执行。然而,当调用sys.exit()
时,程序会立即结束并停止测试新消息(因为即使有一次测试失败,你也会想要修复你的密码程序!).
但是如果message
和decrypted
中的值相等,程序执行会跳过if
语句的阻塞和对sys.exit()
的调用。程序继续循环,直到完成所有的测试。循环结束后,程序运行第 34 行,您知道这在第 9 行的循环之外,因为它有不同的缩进。34 行版画'
Transposition cipher test passed.'
。
调用main()
函数
与我们的其他程序一样,我们希望检查程序是作为模块导入还是作为主程序运行。
# If transpositionTest.py is run (instead of imported as a module) call # the main() function: if __name__ == '__main__': main()
第 39 和 40 行完成了这个任务,检查特殊变量__name__
是否被设置为'__main__'
,如果是,调用main()
函数。
测试测试程序
我们已经编写了一个测试换位密码程序的程序,但是我们怎么知道这个测试程序有效呢?如果测试程序有一个 bug,它只是表明换位密码程序工作,而实际上它们并不工作,怎么办?
我们可以通过故意在加密或解密函数中添加错误来测试测试程序。然后,如果测试程序没有检测到问题,我们知道它没有按预期运行。
为了给程序添加一个 bug,我们打开transpositonecrypt.py
并将+ 1
添加到第 36 行:
换位
Encrypt.py
# Move currentIndex over: currentIndex += key + 1
既然加密代码被破解了,当我们运行测试程序时,它应该会打印一个错误,就像这样:
Test #1: "JEQLDFKJZWALCOYACUPLTRRMLWHOBXQNEAWSLGWAGQQSRSIUIQ..." Mismatch with key 1 and message JEQLDFKJZWALCOYACUPLTRRMLWHOBXQNEAWSLGWAGQQSRSIUIQTRGJHDVCZECRESZJARAVIPFOBWZ XXTBFOFHVSIGBWIBBHGKUWHEUUDYONYTZVKNVVTYZPDDMIDKBHTYJAHBNDVJUZDCEMFMLUXEONCZX WAWGXZSFTMJNLJOKKIJXLWAPCQNYCIQOFTEAUHRJODKLGRIZSJBXQPBMQPPFGMVUZHKFWPGNMRYXR OMSCEEXLUSCFHNELYPYKCNYTOUQGBFSRDDMVIGXNYPHVPQISTATKVKM. Decrypted as: JQDKZACYCPTRLHBQEWLWGQRIITGHVZCEZAAIFBZXBOHSGWBHKWEUYNTVNVYPDIKHYABDJZCMMUENZ WWXSTJLOKJLACNCQFEUROKGISBQBQPGVZKWGMYRMCELSFNLPKNTUGFRDVGNPVQSAKK
在我们故意插入一个 bug 后,测试程序在第一条消息时失败了,所以我们知道它完全按照我们的计划工作!
总结
除了编写程序,你还可以使用新的编程技能。你也可以给计算机编程来测试你写的程序,以确保它们适用于不同的输入。编写代码来测试代码是一种常见的做法。
在本章中,您学习了如何使用random.randint()
函数来产生伪随机数,以及如何使用random.seed()
来重置种子以创建更多伪随机数。虽然伪随机数在加密程序中不够随机,但在本章的测试程序中足够好。
您还了解了列表和列表引用之间的区别,以及copy.deepcopy()
函数将创建列表值的副本,而不是引用值。此外,您还了解了random.shuffle()
函数如何通过使用引用来打乱列表项的位置,从而打乱列表值中各项的顺序。
到目前为止,我们开发的所有程序都只加密短消息。在第 10 章中,你将学习如何加密和解密整个文件。
练习题
练习题的答案可以在本书的网站www.nostarch.com/crackingcodes
找到。
- 如果你运行下面的程序,它打印出数字 8,下次运行时会打印出什么?
import random random.seed(9) print(random.randint(1, 10))
- 下面的程序输出什么?
spam = [1, 2, 3] eggs = spam ham = eggs ham[0] = 99 print(ham == spam)
- 哪个模块包含
deepcopy()
函数? - 下面的程序输出什么?
import copy spam = [1, 2, 3] eggs = copy.deepcopy(spam) ham = copy.deepcopy(eggs) ham[0] = 99 print(ham == spam)