Skip to content

Latest commit

 

History

History
995 lines (717 loc) · 55.9 KB

File metadata and controls

995 lines (717 loc) · 55.9 KB

24 公钥密码的编程

原文:https://inventwithpython.com/cracking/chapter24.html

“一位同事曾经告诉我,这个世界充满了糟糕的安全系统,它们是由阅读应用密码学的人设计的。” ——布鲁斯·施奈尔,应用密码学的作者

Images

在第 23 章中,您学习了公钥加密的工作原理,以及如何使用公钥生成程序生成公钥和私钥文件。现在,您已经准备好将您的公钥文件发送给其他人(或在线发布),这样他们就可以

在发送给你之前用它来加密他们的信息。在本章中,您将编写公钥密码程序,该程序使用您生成的公钥和私钥对消息进行加密和解密。

警告

本书中公钥密码的实现基于 RSA 密码。然而,我们在这里没有涉及的许多其他细节使得这个密码不适合在现实世界中使用。虽然公钥密码不受本书中密码分析技术的影响,但它容易受到专业密码学家今天使用的高级技术的攻击。

本章涵盖的主题

  • pow()功能

  • min()max()功能

  • insert()列表法

公钥密码的工作原理

与我们以前编写的密码类似,公钥密码将字符转换成数字,然后对这些数字进行数学运算来加密它们。公钥密码与您所了解的其他密码的不同之处在于,它将多个字符转换成一个称为的整数,然后一次加密一个块。

公钥密码需要在表示多个字符的块上工作的原因是,如果我们对单个字符使用公钥加密算法,相同的明文字符将总是加密成相同的密文字符。因此,公钥密码与简单的数学替换密码没有什么不同。

创建块

在密码学中,是一个大整数,代表固定数量的文本字符。我们将在本章中创建的publicKeyCipher.py程序将消息字符串值转换成块,每个块是一个表示 169 个文本字符的整数。最大块大小取决于符号集大小和密钥大小。在我们的程序中,符号集是 66 个字符串'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890 !?.',它包含了一个消息可以拥有的唯一字符。

等式2 ** keySize > len(SYMBOLS) ** blockSize必须为真。例如,当您使用 1024 位密钥和 66 个字符的符号集时,最大块大小是一个最多 169 个字符的整数,因为2 ** 1024大于66 * 169。但是2 ** 1024不大于66 * 170。如果你使用太大的块,公钥密码的数学将不起作用,你将不能解密程序产生的密文。

让我们探索一下如何将一个消息字符串转换成一个大的整数块。

将字符串转换成块

正如在我们以前的密码程序中一样,我们可以使用符号集字符串的索引将文本字符转换为整数,反之亦然。我们将符号集存储在名为SYMBOLS的常量中,其中索引0处的字符是'A',索引1处的字符是'B',以此类推。在交互式 shell 中输入以下内容,查看这种转换是如何进行的:

>>> SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456
7890 !?.'
>>> len(SYMBOLS)
66
>>> SYMBOLS[0]
'A'
>>> SYMBOLS[30]
'e'

我们可以通过符号集中的整数索引来表示文本字符。但是,我们还需要一种方法,将这些小整数组合成一个代表一个块的大整数。

为了创建块,我们将字符的符号集索引乘以符号集大小的递增幂。这个区块是所有这些数字的总和。让我们看一个例子,使用下面的步骤将小整数组合成一个大的字符串块'Howdy'

该块的整数从0开始,符号集的长度为 66 个字符。以这些数字为例,在交互式 shell 中输入以下内容:

>>> blockInteger = 0
>>> len(SYMBOLS)
66

'Howdy'消息中的第一个字符是'H',因此我们找到该字符的符号集索引,如下所示:

>>> SYMBOLS.index('H')
7

因为这是消息的第一个字符,所以我们将它的符号集索引乘以66 ** 0(在 Python 中,指数运算符是**),计算结果为7。我们将该值添加到块中:

>>> 7 * (66 ** 0)
7
>>> blockInteger = blockInteger + 7

然后我们为'Howdy'中的第二个字符'o'找到符号集索引。因为这是消息的第二个字符,我们将'o'的符号集索引乘以66 ** 1而不是66 ** 0,然后将其添加到块中:

>>> SYMBOLS.index('o')
40
>>> blockInteger += 40 * (66 ** 1)
>>> blockInteger
2647

该块现在是2647。我们可以使用一行代码来缩短查找每个字符的符号集索引的过程:

>>> blockInteger += SYMBOLS.index('w') * (len(SYMBOLS) ** 2)
>>> blockInteger += SYMBOLS.index('d') * (len(SYMBOLS) ** 3)
>>> blockInteger += SYMBOLS.index('y') * (len(SYMBOLS) ** 4)
>>> blockInteger
957285919

'Howdy'编码到一个大整数块中会产生整数 957,285,919,它唯一地引用该字符串。通过继续使用越来越大的 66 次方,我们可以使用一个大整数来表示最大到块大小的任意长度的字符串。例如,277,981 是表示字符串'42!'的块,10,627,106,169,278,065,987,481,042,235,655,809,080,528 表示字符串'I named my cat Zophie.'

因为我们的块大小是 169,所以我们在单个块中最多只能加密 169 个字符。如果我们要编码的消息超过 169 个字符,我们可以使用更多的块。在publicKeyCipher.py程序中,我们将使用逗号来分隔各个块,以便用户可以识别一个块何时结束,下一个块何时开始。

表 24-1 包含了一个分成块的示例消息,并显示了代表每个块的整数。每个块最多可以存储 169 个字符的消息。

表 24-1: 一条消息拆分成块

消息 块整数
第一个块 -> (169 个字符) 阿兰·麦席森·图灵是英国密码分析学家和计算机科学家。他对计算机科学的发展有很大的影响,并提供了
3013810338120027658120611166332270159047154
7608326152595431391575797140707837485089852
659286061395648657712401264848061468979996
871106525448961558640277994456848107158423
162065952633246425985956987627719631460939
2565956887693059829154012923414594664511373
093526087354321666137736234609864038110994
85392482698

| | 第二块 -> (169 个字符) | 图灵机的算法和计算概念。图灵被广泛认为是计算机科学和人工智能之父。W 期间 |

1106890780922147455215935080195634373132680
102708192713651484085475402677752791958075
872272026708702634070281109709555761008584
1376819190225258032442691476944762174257333
902148064107269871669093655004577014280290
4244524711751435049117398986044838791597315
078937194860112574798016587564452792451567
15863348631

| | 第三块 -> (82 个字符) | 第二次世界大战期间,他为布莱奇利公园的政府代码和密码学校工作。 |

1583679754961601914428952447217583697875837
635974864128047509439056559022732095918077
290541944859809053286915764228326887495095
27709935741799076979034

|

在本例中,420 个字符的消息由两个 169 个字符的块组成,需要第三个块来容纳剩余的 82 个字符。

公钥密码加密与解密的数学

现在您已经知道了如何将字符转换成块整数,让我们来探索公钥密码如何使用数学来加密每个块。

以下是公钥密码的一般公式:

C = M^e mod n
M = C^d mod n

我们用第一个等式加密每个整数块,用第二个等式解密。M表示消息分组整数,C为密文分组整数。数字en组成加密的公钥,数字dn组成私钥。回想一下,每个人,包括密码分析者,都可以访问公钥(e, n)

通常,我们通过将每个块的整数提升到e的幂来创建加密的消息,就像我们在上一节中计算的那样,并通过n来修改结果。该计算产生一个整数,表示加密块C。组合所有的块产生完整的加密消息。

比如我们把五个字符的字符串'Howdy'加密后发给爱丽丝。当转换为整数块时,消息为[957285919](完整的消息适合一个块,所以列表值中只有一个整数)。Alice 的公钥是 64 位,太小了,不安全,但是我们将使用它来简化本例中的输出。其n为 116284564958604315258674918142848831759e为 13805220545651593223。(对于 1024 位密钥,这些数字会大得多。)

要加密,我们通过将这些数字传递给 Python 的pow()函数,计算(957285919 ** 13805220545651593223) % 116284564958604315258674918142848831759,就像这样:

>>> pow(957285919, 13805220545651593223, 
      116284564958604315258674918142848831759)
43924807641574602969334176505118775186

Python 的pow()函数使用了一种叫做模幂运算的数学技巧来快速计算如此大的指数。事实上,对表达式(957285919 ** 13805220545651593223) % 116284564958604315258674918142848831759求值会得到相同的答案,但需要几个小时才能完成。pow()返回的整数是一个表示加密消息的块。

要解密,加密消息的接收者需要有私钥(d, n),将每个加密块的整数提升到d的幂,然后用n进行 mod。当所有解密的块被解码成字符并组合时,接收者将得到原始的明文消息。

例如,Alice 尝试解密块整数 43,924,807,641,574,602,969,334,176,505,118,775,186。她的私钥的n与她的公钥的n相同,但是她的私钥的d为 7242447594969014539697070764378340583。为了解密,她运行以下代码:

>>> pow(43924807641574602969334176505118775186, 
      72424475949690145396970707764378340583, 
      116284564958604315258674918142848831759)
957285919

当我们将块整数957285919转换成一个字符串时,我们得到了'Howdy',这是原始消息。接下来,您将学习如何将块转换为字符串。

将块转换成字符串

要将块解密为原始的块整数,第一步是将其转换为每个文本字符的小整数。这个过程从添加到块中的最后一个字符开始。我们使用底除法和模运算符来计算每个文本字符的小整数。

回想一下,上一个'Howdy'示例中的块整数是957285919;原始消息有五个字符长,使得最后一个字符的索引为 4;并且用于该消息的符号集是 66 个字符长。为了确定最后一个字符的符号集索引,我们计算957,285,919 / 66 ** 4 并向下舍入,得到 50。我们可以使用整数除法运算符(//)进行除法和向下舍入。符号集(SYMBOLS[50])中索引 50 处的字符是'y',这实际上是'Howdy'消息的最后一个字符。

在交互式 shell 中,我们使用以下代码计算这个块整数:

>>> blockInteger = 957285919
>>> SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456
7890 !?.'
>>> blockInteger // (66 ** 4)
50
>>> SYMBOLS[50]
'y'

下一步是将块整数乘以66 ** 4以获得下一个块整数。计算957,285,919 % (66 ** 4)得到 8,549,119,这恰好是字符串'Howd'的块整数值。我们可以用66 ** 3的底除法来确定这个方块的最后一个字符。为此,请在交互式 shell 中输入以下内容:

>>> blockInteger = 8549119
>>> SYMBOLS[blockInteger // (len(SYMBOLS) ** 3)]
'd'

这个块的最后一个字符是'd',使得转换后的字符串到目前为止'dy'。我们可以像以前一样从块整数中删除这个字符:

>>> blockInteger = blockInteger % (len(SYMBOLS) ** 3)
>>> blockInteger
211735

整数211735是字符串'How'的块。通过继续这个过程,我们可以从块中恢复完整的字符串,就像这样:

>>> SYMBOLS[blockInteger // (len(SYMBOLS) ** 2)]
'w'
>>> blockInteger = blockInteger % (len(SYMBOLS) ** 2)
>>> SYMBOLS[blockInteger // (len(SYMBOLS) ** 1)]
'o'
>>> blockInteger = blockInteger % (len(SYMBOLS) ** 1)
>>> SYMBOLS[blockInteger // (len(SYMBOLS) ** 0)]
'H'

现在您知道了如何从原始块整数值957285919中检索字符串'Howdy'中的字符。

为什么我们无法破解公钥密码

当公钥密码正确实现时,我们在本书中使用的所有不同类型的加密攻击对公钥密码都是无用的。以下是几个原因:

  1. 暴力攻击不会起作用,因为有太多可能的密钥需要检查。

  2. 字典攻击不会起作用,因为密钥基于数字,而不是单词。

  3. 单词模式攻击不起作用,因为相同的明文单词可以根据它在块中出现的位置进行不同的加密。

  4. 频率分析不起作用,因为单个加密块代表几个字符;我们无法获得单个字符的频率计数。

因为公钥(e, n)是大家都知道的,如果密码分析者能够截获密文,他们就会知道en,和C。但是在不知道 dd的情况下,数学上不可能解出原始消息M

回想一下第 23 章中的e与数字(p – 1) × (q – 1)互质,并且de(p – 1) × (q – 1)的模逆。在第十三章中,你了解到两个数的模逆是通过对方程(ai) % m = 1 i来计算的,其中am是模问题a mod m中的两个数。这意味着密码分析者知道de mod (p – 1) × (q – 1)的逆,所以我们可以通过求解方程ed mod (p – 1) × (q – 1)找到d得到整个解密密钥.但是,没有办法知道什么是(p – 1) × (q – 1)

我们从公钥文件中知道密钥的大小,所以密码分析者知道pq小于2 ** 1024并且e(p – 1) × (q – 1)互质。但是e很多的数字是相对质数,在 0 到2 ** 1024的范围内寻找(p – 1) × (q – 1)可能的数字对蛮力来说是一个太大的问题。

虽然这还不足以破解密码,但密码分析员可以从公开密钥中收集到另一个线索。公钥由两个数字组成( en,我们知道n = p × q,因为这是我们在第 23 章中创建公钥和私钥时计算n的方法。而且因为pq都是质数,所以对于一个给定的数n,可以正好有两个数可以是pq

回想一下,一个质数除了 1 和它本身之外,没有任何因子。因此,如果你把两个质数相乘,乘积将只有 1 和它自己以及你开始时的两个质数作为它的因数。

因此,要破解公钥密码,我们需要做的就是找出n的因子。因为我们知道两个且只有两个数可以相乘得到n,所以我们不会有太多不同的数可供选择。在我们算出哪两个质数( pq相乘得到n之后,我们就可以计算(p – 1) × (q – 1),然后用那个结果来计算d。这个计算似乎很容易做到。让我们用我们在第 22 章的primeNum.py程序中写的isPrime()函数来做计算。

我们可以修改isPrime()来返回它找到的第一个因子,因为我们知道除了 1 和n之外,只能有两个因子n :

def isPrime(num):
    # Returns (p,q) where p and q are factors of num.
    # See if num is divisible by any number up to the square root of num:
    for i in range(2, int(math.sqrt(num)) + 1):
        if num % i == 0:
            return (i, num / i)
    return None # No factors exist for num; num must be prime.

如果我们编写一个公钥密码黑客程序,我们可以只调用这个函数,将n传递给它(我们将从公钥文件中获得),并等待它找到因子pq。然后我们可以找到(p – 1) × (q – 1)是什么,这意味着我们可以计算e mod (p – 1) × (q – 1)的模逆,以获得d,即解密密钥。那么计算明文消息M就容易了。

但是有一个问题。回想一下,n是一个大约 600 位数的数字。Python 的math.sqrt()函数不能处理这么大的数字,所以它会给你一个错误消息。但是,即使它能够处理这个数字,Python 也要执行那个for循环很长时间。以为例,即使你的计算机从现在开始继续运行 50 亿年,它仍然几乎没有机会找到n的因子。这些数字就是这么大。

而这正是公钥密码的长处:*数学上,求一个数的因子没有捷径。*很容易得出两个质数pq,将它们相乘得到n。但是,几乎不可能得到一个数字n并计算出pq会是多少。例如,当你看到像 15 这样的小数字时,你可以很容易地说 5 和 3 是两个相乘等于 15 的数字。但要想搞清楚一个数字的因子,比如 178,565,887,643,607,245,654,502,737,完全是另一回事。这个事实使得公钥密码实际上不可能被破解。

公钥密码程序的源代码

选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,保存为publicKeyCipher.py

publicKey Cipher.py

# Public Key Cipher
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import sys, math

# The public and private keys for this program are created by
# the makePublicPrivateKeys.py program.
# This program must be run in the same folder as the key files.

SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz12345
       67890 !?.'

def main():
    # Runs a test that encrypts a message to a file or decrypts a message
    # from a file.
    filename = 'encrypted_file.txt' # The file to write to/read from.
    mode = 'encrypt' # Set to either 'encrypt' or 'decrypt'.

    if mode == 'encrypt':
        message = 'Journalists belong in the gutter because that is where
               the ruling classes throw their guilty secrets. Gerald Priestland.
               The Founding Fathers gave the free press the protection it must
               have to bare the secrets of government and inform the people.
               Hugo Black.'
        pubKeyFilename = 'al_sweigart_pubkey.txt'
        print('Encrypting and writing to %s...' % (filename))
        encryptedText = encryptAndWriteToFile(filename, pubKeyFilename,
               message)

        print('Encrypted text:')
        print(encryptedText)

    elif mode == 'decrypt':
        privKeyFilename = 'al_sweigart_privkey.txt'
        print('Reading from %s and decrypting...' % (filename))
        decryptedText = readFromFileAndDecrypt(filename, privKeyFilename)

        print('Decrypted text:')
        print(decryptedText)


def getBlocksFromText(message, blockSize):
    # Converts a string message to a list of block integers.
    for character in message:
        if character not in SYMBOLS:
            print('ERROR: The symbol set does not have the character %s' %
                   (character))
            sys.exit()
    blockInts = []
    for blockStart in range(0, len(message), blockSize):
        # Calculate the block integer for this block of text:
        blockInt = 0
        for i in range(blockStart, min(blockStart + blockSize,
               len(message))):
            blockInt += (SYMBOLS.index(message[i])) * (len(SYMBOLS) **
                   (i % blockSize))
        blockInts.append(blockInt)
    return blockInts


def getTextFromBlocks(blockInts, messageLength, blockSize):
    # Converts a list of block integers to the original message string.
    # The original message length is needed to properly convert the last
    # block integer.
    message = []
    for blockInt in blockInts:
        blockMessage = []
        for i in range(blockSize - 1, -1, -1):
            if len(message) + i < messageLength:
                # Decode the message string for the 128 (or whatever
                # blockSize is set to) characters from this block integer:
                charIndex = blockInt // (len(SYMBOLS) ** i)
                blockInt = blockInt % (len(SYMBOLS) ** i)
                blockMessage.insert(0, SYMBOLS[charIndex])
        message.extend(blockMessage)
    return ''.join(message)


def encryptMessage(message, key, blockSize):
    # Converts the message string into a list of block integers, and then
    # encrypts each block integer. Pass the PUBLIC key to encrypt.
    encryptedBlocks = []
    n, e = key

    for block in getBlocksFromText(message, blockSize):
        # ciphertext = plaintext ^ e mod n
        encryptedBlocks.append(pow(block, e, n))
    return encryptedBlocks


def decryptMessage(encryptedBlocks, messageLength, key, blockSize):
    # Decrypts a list of encrypted block ints into the original message
    # string. The original message length is required to properly decrypt
    # the last block. Be sure to pass the PRIVATE key to decrypt.
    decryptedBlocks = []
    n, d = key
    for block in encryptedBlocks:
        # plaintext = ciphertext ^ d mod n
        decryptedBlocks.append(pow(block, d, n))
    return getTextFromBlocks(decryptedBlocks, messageLength, blockSize)


def readKeyFile(keyFilename):
    # Given the filename of a file that contains a public or private key,
    # return the key as a (n,e) or (n,d) tuple value.
    fo = open(keyFilename)
    content = fo.read()
    fo.close()
    keySize, n, EorD = content.split(',')
    return (int(keySize), int(n), int(EorD))


def encryptAndWriteToFile(messageFilename, keyFilename, message,
       blockSize=None):
    # Using a key from a key file, encrypt the message and save it to a
    # file. Returns the encrypted message string.
    keySize, n, e = readKeyFile(keyFilename)
    if blockSize == None:
        # If blockSize isn't given, set it to the largest size allowed by
               the key size and symbol set size.
        blockSize = int(math.log(2 ** keySize, len(SYMBOLS)))
    # Check that key size is large enough for the block size:
    if not (math.log(2 ** keySize, len(SYMBOLS)) >= blockSize):
        sys.exit('ERROR: Block size is too large for the key and symbol
               set size. Did you specify the correct key file and encrypted
               file?')
    # Encrypt the message:
    encryptedBlocks = encryptMessage(message, (n, e), blockSize)

    # Convert the large int values to one string value:
    for i in range(len(encryptedBlocks)):
        encryptedBlocks[i] = str(encryptedBlocks[i])
    encryptedContent = ','.join(encryptedBlocks)

    # Write out the encrypted string to the output file:
    encryptedContent = '%s_%s_%s' % (len(message), blockSize,
           encryptedContent)
    fo = open(messageFilename, 'w')
    fo.write(encryptedContent)
    fo.close()
    # Also return the encrypted string:
    return encryptedContent


def readFromFileAndDecrypt(messageFilename, keyFilename):
    # Using a key from a key file, read an encrypted message from a file
    # and then decrypt it. Returns the decrypted message string.
    keySize, n, d = readKeyFile(keyFilename)


    # Read in the message length and the encrypted message from the file:
    fo = open(messageFilename)
    content = fo.read()
    messageLength, blockSize, encryptedMessage = content.split('_')
    messageLength = int(messageLength)
    blockSize = int(blockSize)

    # Check that key size is large enough for the block size:
    if not (math.log(2 ** keySize, len(SYMBOLS)) >= blockSize):
        sys.exit('ERROR: Block size is too large for the key and symbol
               set size. Did you specify the correct key file and encrypted
               file?')

    # Convert the encrypted message into large int values:
    encryptedBlocks = []
    for block in encryptedMessage.split(','):
        encryptedBlocks.append(int(block))

    # Decrypt the large int values:
    return decryptMessage(encryptedBlocks, messageLength, (n, d),
           blockSize)


# If publicKeyCipher.py is run (instead of imported as a module), call
# the main() function.
if __name__ == '__main__':
    main()

公钥密码程序的运行示例

让我们试着运行publicKeyCipher.py程序来加密一条秘密消息。要向使用此程序的人发送秘密消息,请获取此人的公钥文件,并将其放在与程序文件相同的目录中。

要加密消息,确保第 16 行的mode变量被设置为字符串'encrypt'。将第 19 行的message变量更新为您想要加密的消息字符串。然后将第 20 行的pubKeyFilename变量设置为公钥文件的文件名。第 21 行的filename变量保存了密文被写入的文件名。filenamepubKeyFilenamemessage变量都被传递给encryptAndWriteToFile()以加密消息并将其保存到文件中。

运行程序时,输出应该是这样的:

Encrypting and writing to encrypted_file.txt...
Encrypted text:
258_169_45108451524907138236859816039483721219475907590237903918239237768643699
4856660301323157253724978022861702098324427738284225530186213380188880577329634
8339229890890464969556937797072434314916522839692277034579463594713843559898418
9307234650088689850744361262707129971782407610450208047927129687841621734776965
7018277918490297215785759257290855812221088907016904983025542174471606494779673
6015310089155876234277883381345247353680624585629672939709557016107275469388284
5124192568409483737233497304087969624043516158221689454148096020738754656357140
74772465708958607695479122809498585662785064751254235489968738346795649,1253384
3336975115539761332250402699868835150623017582438116840049236083573741817645933
3719456453133658476271176035248597021972316454526545069452838766387599839340542
4066877721135511313454252589733971962219016066614978390378611175964456773669860
9429545605901714339082542725015140530985685117232598778176545638141403657010435
3859244660091910391099621028192177415196156469972977305212676293746827002983231
4668240693230032141097312556400629961518635799478652196072316424918648787555631
6339424948975804660923616682767242948296301678312041828934473786824809308122356
133539825048880814063389057192492939651199537310635280371

程序将这个输出写到一个名为encrypted_file.txt的文件中。这是对第 19 行的message变量中的字符串的加密。因为您使用的公钥可能与我的不同,所以您得到的输出可能不同,但是输出的格式应该是相同的。正如您在本例中所看到的,加密分为两个块,或两个大整数,用逗号分隔。

加密开始处的数字258代表原始消息长度,后面是下划线和另一个数字169,代表块大小。要解密此消息,将mode变量改为'decrypt',并再次运行程序。与加密一样,确保第 28 行的privKeyFilename被设置为私钥文件名,并且该文件与publicKeyCipher.py在同一个文件夹中。此外,确保加密文件encrypted_file.txtpublicKeyCipher.py在同一个文件夹中。当您运行程序时,encrypted_file.txt中的加密消息被解密,输出应该是这样的:

Reading from encrypted_file.txt and decrypting...
Decrypted text:
Journalists belong in the gutter because that is where the ruling classes throw
their guilty secrets. Gerald Priestland. The Founding Fathers gave the free
press the protection it must have to bare the secrets of government and inform
the people. Hugo Black.

注意publicKeyCipher.py程序只能加密和解密普通(简单)文本文件。

让我们仔细看看publicKeyCipher.py程序的源代码。

设置程序

公钥密码处理数字,所以我们将把字符串消息转换成整数。这个整数是基于符号集中的索引计算的,存储在第 10 行的SYMBOLS变量中。

# Public Key Cipher
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import sys, math

# The public and private keys for this program are created by
# the makePublicPrivateKeys.py program.
# This program must be run in the same folder as the key files.

SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz12345
       67890 !?.'

程序如何确定是加密还是解密

通过将值存储在变量中,publicKeyCipher.py程序决定是否加密或解密文件,以及使用哪个密钥文件。在我们研究这些变量如何工作的同时,我们还将研究程序如何打印加密和解密输出。

我们告诉程序它应该在main()内加密还是解密:

def main():
    # Runs a test that encrypts a message to a file or decrypts a message
    # from a file.
    filename = 'encrypted_file.txt' # The file to write to/read from.
    mode = 'encrypt' # Set to either 'encrypt' or 'decrypt'.

如果第 16 行的mode设置为'encrypt',程序通过将信息写入filename中指定的文件来加密信息。如果mode设置为'decrypt',程序读取加密文件的内容(由filename指定)进行解密。

第 18 到 25 行指定了如果程序确认用户想要加密文件,它应该做什么。

    if mode == 'encrypt':
        message = 'Journalists belong in the gutter because that is where
               the ruling classes throw their guilty secrets. Gerald Priestland.
               The Founding Fathers gave the free press the protection it must
               have to bare the secrets of government and inform the people.
               Hugo Black.'
        pubKeyFilename = 'al_sweigart_pubkey.txt'
        print('Encrypting and writing to %s...' % (filename))
        encryptedText = encryptAndWriteToFile(filename, pubKeyFilename,
               message)

        print('Encrypted text:')
        print(encryptedText)

第 19 行的message变量包含要加密的文本,第 20 行的pubKeyFilename包含公钥文件的文件名,在本例中为al_sweigart_pubkey.txt。请记住,该消息只能包含SYMBOLS变量中的字符,这是该密码的符号集。第 22 行调用encryptAndWriteToFile()函数,该函数使用公钥加密message,并将加密的消息写入filename指定的文件。

第 27 到 28 行告诉程序如果mode被设置为'decrypt'该做什么。程序从第 28 行的privKeyFilename中的私有密钥文件中读取,而不是加密。

    elif mode == 'decrypt':
        privKeyFilename = 'al_sweigart_privkey.txt'
        print('Reading from %s and decrypting...' % (filename))
        decryptedText = readFromFileAndDecrypt(filename, privKeyFilename)

        print('Decrypted text:')
        print(decryptedText)

然后我们将filenameprivKeyFilename变量传递给readFromFileAndDecrypt ()函数(稍后在程序中定义),该函数返回解密的消息。第 30 行将来自readFromFileAndDecryptT5 的返回值存储在decryptedText中,第 33 行将其打印到屏幕上。这是main()功能的结尾。

现在让我们看看如何执行公钥密码的其他步骤,例如将消息转换成块。

使用getBlocksFromText()将字符串转换为块

让我们看看程序如何将消息字符串转换成 128 字节的块。第 36 行上的getBlocksFromText()函数将一个message和一个块大小作为参数,返回一个表示消息的块列表或一个大整数值列表。

def getBlocksFromText(message, blockSize):
    # Converts a string message to a list of block integers.
    for character in message:
        if character not in SYMBOLS:
            print('ERROR: The symbol set does not have the character %s' %
                   (character))
            sys.exit()

第 38 到 41 行确保消息参数只包含在SYMBOLS变量的符号集中的文本字符。blockSize参数是可选的,可以采用任何块大小。为了创建块,我们首先将字符串转换成字节。

为了生成一个块,我们将所有的符号集索引组合成一个大整数,正如我们在第 350 页的“将字符串转换成块”中所做的那样。当我们创建块时,我们将使用第 42 行的blockInts空列表来存储它们。

    blockInts = []

我们希望块的长度为blockSize字节,但是当消息不能被blockSize整除时,最后一个块的长度将小于blockSize字符。为了处理这种情况,我们使用了min()函数。

最小()和最大()函数

min()函数返回其参数的最小值。在交互式 shell 中输入以下内容,看看min()函数是如何工作的:

>>> min(13, 32, 13, 15, 17, 39)
13

您还可以将单个列表或元组值作为参数传递给min()。在交互式 shell 中输入以下内容以查看示例:

>>> min([31, 26, 20, 13, 12, 36])
12
>>> spam = (10, 37, 37, 43, 3)
>>> min(spam)
3

在这种情况下,min(spam)返回列表或元组中的最小值。与min()相反的是max(),它返回其参数的最大值,如下所示:

>>> max(18, 15, 22, 30, 31, 34)
34

让我们回到我们的代码,看看publicKeyCipher.py程序如何使用min()来确保message的最后一个块被截断到合适的大小。

在 blockInt 中存储块

第 43 行的for循环中的代码通过将blockStart中的值设置为正在创建的块的索引来为每个块创建整数。

    for blockStart in range(0, len(message), blockSize):
        # Calculate the block integer for this block of text:
        blockInt = 0
        for i in range(blockStart, min(blockStart + blockSize,
               len(message))):

我们将存储我们在blockInt中创建的块,我们最初在第 45 行将其设置为0。第 46 行的for循环将i设置为来自message的块中所有字符的索引。这些索引应该从blockStart开始,上升到blockStart + blockSizelen(message),以较小者为准。第 46 行的min()调用返回这两个表达式中较小的一个。

第 46 行的range()的第二个参数应该是blockStart + blockSizelen(message)中较小的一个,因为除了最后一个块的之外,每个块总是由 128 个字符*(或者blockSize中的任何值)组成。最后一个块可能正好是 128 个字符,但更有可能少于全部 128 个字符。在这种情况下,我们希望ilen(message)停止,因为这是message中的最后一个索引。

有了组成块的字符后,我们使用数学将字符转换成一个大整数。回想一下在第 350 页的“将字符串转换成块中,我们通过将每个字符的符号集索引整数值乘以66 ** SYMBOLS.index(66 是SYMBOLS字符串的长度)创建了一个大整数。为了在代码中做到这一点,我们为每个字符计算SYMBOLS.index(message[i])(字符的符号集索引整数值)乘以(len(SYMBOLS) ** (i % blockSize)),并将每个结果加到blockInt

            blockInt += (SYMBOLS.index(message[i])) * (len(SYMBOLS) **
                   (i % blockSize))

我们希望指数是相对于当前迭代的块的索引,它总是从0blockSize。我们不能直接使用变量i作为等式的字符索引部分,因为它指的是整个message字符串中的索引,从0len(message)都有索引。使用i会产生一个比 66 大得多的整数。通过用blockSize修改i,我们可以得到相对于块的索引,这就是为什么第 47 行是len(SYMBOLS) ** (i % blockSize)而不是简单的len(SYMBOLS) ** i

在第 46 行上的for循环完成后,已经计算了该块的整数。我们使用第 48 行的代码将这个块整数附加到blockInts列表中。第 43 行的for循环的下一次迭代计算消息的下一个块的块整数。

        blockInts.append(blockInt)
    return blockInts

在第 43 行的for循环结束后,所有的块整数应该已经被计算并存储在blockInts列表中。49 线从getBlocksFromText()返回blockInts

此时,我们已经将整个message字符串转换成了块整数,但是我们还需要一种方法来将块整数转换回原始的明文消息,以用于解密过程,这是我们接下来要做的。

使用getTextFromBlocks()解密

第 52 行的getTextFromBlocks()功能与getBlocksFromText ()功能相反。这个函数将一列块整数作为参数blockInts、消息的长度和blockSize来返回这些块代表的字符串值。我们需要messageLength中编码消息的长度,因为getTextFromBlocks()函数使用这个信息从最后一个块整数中获取字符串,当它的大小不是blockSize字符时。该过程在第 354 页的将块转换成字符串中有所描述。

def getTextFromBlocks(blockInts, messageLength, blockSize):
    # Converts a list of block integers to the original message string.
    # The original message length is needed to properly convert the last
    # block integer.
    message = []

第 56 行创建的空白列表message为每个字符存储了一个字符串值,我们将从blockInts中的块整数中计算这个值。

第 57 行的for循环遍历blockInts列表中的每个块整数。在for循环中,第 58 到 65 行的代码计算当前迭代块中的字母。

    for blockInt in blockInts:
        blockMessage = []
        for i in range(blockSize - 1, -1, -1):

getTextFromBlocks()中的代码将每个块整数分割成blockSize个整数,每个整数代表一个字符的符号集索引。我们必须反向工作以从blockInt中提取符号集索引,因为当我们加密消息时,我们从较小的指数(66 ** 066 ** 166 ** 2等等)开始,但是当解密时,我们必须首先使用较大的指数进行除法和模运算。这就是为什么第 59 行上的for循环从blockSize - 1开始,然后在每次迭代中减去1,直到(但不包括)-1。这意味着最后一次迭代中i的值是0

在我们将符号集索引转换为字符之前,我们需要确保解码的块没有超过message的长度。为此,我们检查到目前为止已经从块中翻译出来的字符数len(message) + i,仍然少于第 60 行的messageLength

            if len(message) + i < messageLength:
                # Decode the message string for the 128 (or whatever
                # blockSize is set to) characters from this block integer.
                charIndex = blockInt // (len(SYMBOLS) ** i)
                blockInt = blockInt % (len(SYMBOLS) ** i)

为了从块中获取字符,我们遵循第 354 页上的“将块转换成字符串中描述的过程。我们将每个字符放入到message列表。对每个块进行编码实际上颠倒了字符,这一点你早就看到了,所以我们不能只把解码后的字符附加到message上。相反,我们在message的前面插入字符,这需要用insert()列表方法来完成。

使用插入()列表方法

append()列表方法只在列表末尾添加值,但是insert()列表方法可以在列表的任意位置添加值。insert()方法将列表中要插入值的位置的整数索引和要插入的值作为其参数。在交互式 shell 中输入以下内容,看看insert()方法是如何工作的:

>>> spam = [2, 4, 6, 8]
>>> spam.insert(0, 'hello')
>>> spam
['hello', 2, 4, 6, 8]
>>> spam.insert(2, 'world')
>>> spam
['hello', 2, 'world', 4, 6, 8]

在这个例子中,我们创建一个spam列表,然后在0索引处插入字符串'hello'。如您所见,我们可以在列表中的任何现有索引处插入值,比如在索引2处。

将消息列表合并成一串

我们可以使用SYMBOLS字符串将charIndex中的符号集索引转换为相应的字符,并将该字符插入到列表开头的索引0处。

                blockMessage.insert(0, SYMBOLS[charIndex])
        message.extend(blockMessage)
    return ''.join(message)

然后这个字符串从getTextFromBlocks()返回。

编写encryptMessage()函数

encryptMessage()函数使用message中的明文字符串和存储在key中的公钥的两个整数元组对每个块进行加密,这是用我们将在本章后面编写的readKeyFile()函数创建的。encryptMessage()函数返回加密块的列表。

def encryptMessage(message, key, blockSize):
    # Converts the message string into a list of block integers, and then
    # encrypts each block integer. Pass the PUBLIC key to encrypt.
    encryptedBlocks = []
    n, e = key

第 73 行创建了encryptedBlocks变量,它以一个空列表开始,该列表将保存整数块。然后第 74 行将key中的两个整数分配给变量ne。现在我们已经设置了公钥变量,我们可以对每个要加密的消息块执行数学运算。

为了加密每个块,我们对它执行一些数学运算,得到一个新的整数,这就是加密的块。我们将模块提升到e的幂,然后使用第 78 行的pow(block, e, n)通过n对其进行修改。

    for block in getBlocksFromText(message, blockSize):
        # ciphertext = plaintext ^ e mod n
        encryptedBlocks.append(pow(block, e, n))
    return encryptedBlocks

加密的块整数然后被附加到encryptedBlocks

编写decryptMessage()函数

第 82 行的decryptMessage()函数解密块并返回解密的消息字符串。它将加密块列表、消息长度、私钥和块大小作为参数。

我们在第 86 行设置的decryptedBlocks变量存储了解密块的列表,并使用多重赋值技巧,将key元组的两个整数分别放在nd中。

def decryptMessage(encryptedBlocks, messageLength, key, blockSize):
    # Decrypts a list of encrypted block ints into the original message
    # string. The original message length is required to properly decrypt
    # the last block. Be sure to pass the PRIVATE key to decrypt.
    decryptedBlocks = []
    n, d = key

解密的数学与加密的数学相同,除了整数块被提升到d而不是e,正如你在第 90 行看到的。

    for block in encryptedBlocks:
        # plaintext = ciphertext ^ d mod n
        decryptedBlocks.append(pow(block, d, n))

解密的块连同messageLengthblockSize参数被传递给getTextFromBlocks(),以便decryptMessage()在第 91 行返回解密的明文作为字符串。

    return getTextFromBlocks(decryptedBlocks, messageLength, blockSize)

现在,您已经了解了使加密和解密成为可能的数学,让我们看看readKeyFile()函数如何读取公钥和私钥文件来创建我们传递给encryptMessage ()decryptMessage()的元组值。

从自己的密钥文件中读入公钥和私钥

调用readKeyFile()函数从用makePublicPrivateKeys.py程序创建的密钥文件中读取值,该程序是我们在第 23 章中创建的。要打开的文件名传递给keyFilename,文件必须和publicKeyCipher.py程序在同一个文件夹。

第 97 到 99 行打开这个文件,将内容作为一个字符串读入到content变量中。

def readKeyFile(keyFilename):
    # Given the filename of a file that contains a public or private key,
    # return the key as a (n,e) or (n,d) tuple value.
    fo = open(keyFilename)
    content = fo.read()
    fo.close()
    keySize, n, EorD = content.split(',')
    return (int(keySize), int(n), int(EorD))

密钥文件将密钥大小以字节存储为n,以及ed,这取决于密钥文件是用于加密密钥还是解密密钥。正如您在上一章中了解到的,这些值被存储为文本并以逗号分隔,所以我们使用split()字符串方法在逗号处分割content中的字符串。split()返回的列表中有三个条目,第 100 行的多重赋值将这些条目分别放入keySizenEorD变量中。

回想一下,content从文件中读取时是一个字符串,而split()返回的列表中的项目也将是字符串值。为了将这些字符串值转换成整数,我们将keySizenEorD的值传递给int()。然后,readKeyFile()函数返回三个整数,int(keySize)int(n)int(EorD),您将使用它们进行加密或解密。

将加密写入文件

在第 104 行,encryptAndWriteToFile()函数调用encryptMessage()用密钥加密字符串,并创建包含加密内容的文件。

def encryptAndWriteToFile(messageFilename, keyFilename, message,
       blockSize=None):
    # Using a key from a key file, encrypt the message and save it to a
    # file. Returns the encrypted message string.
    keySize, n, e = readKeyFile(keyFilename)

encryptAndWriteToFile()函数接受三个字符串参数:要写入加密消息的文件名(messageFilename(、要使用的公钥文件名(keyFilename(和要加密的消息(message)。blockSize参数被指定为第四个参数。

加密过程的第一步是通过调用第 107 行上的readKeyFile()从密钥文件中读入keySizene的值。

blockSize参数有一个默认参数None:

    if blockSize == None:
        # If blockSize isn't given, set it to the largest size allowed by
               the key size and symbol set size.
        blockSize = int(math.log(2 ** keySize, len(SYMBOLS)))

如果没有为blockSize参数传递参数,则blockSize将被设置为符号集和密钥大小允许的最大大小。请记住,等式2 ** keySize > len(SYMBOLS) ** blockSize必须为真。为了计算最大可能的块大小,Python 的math.log()函数被调用来计算第 110 行上以len(SYMBOLS)为底的2 ** keySize的对数。

只有当密钥大小等于或大于块大小时,公钥密码的数学运算才能正确工作,所以在继续之前,我们必须在第 112 行检查这一点。

    # Check that key size is large enough for the block size:
    if not (math.log(2 ** keySize, len(SYMBOLS)) >= blockSize):
        sys.exit('ERROR: Block size is too large for the key and symbol
             set size. Did you specify the correct key file and encrypted
             file?')

如果keySize太小,程序退出并显示错误信息。用户必须减小为blockSize传递的值,或者使用更大的密钥。

现在我们有了键的ne值,我们调用第 115 行的函数encryptMessage(),它返回一个整数块列表。

    # Encrypt the message
    encryptedBlocks = encryptMessage(message, (n, e), blockSize)

encryptMessage()函数期望键使用两个整数的元组,这就是为什么ne变量被放在元组中,然后作为第二个参数传递给encryptMessage()

接下来,我们将加密的块转换成可以写入文件的字符串。为此,我们将这些块连接成一个字符串,每个块用逗号分隔。使用','.join(encryptedBlocks)这样做是行不通的,因为join()只对字符串值的列表有效。因为encryptedBlocks是一个整数列表,我们要先把这些整数转换成字符串:

    # Convert the large int values to one string value:
    for i in range(len(encryptedBlocks)):
        encryptedBlocks[i] = str(encryptedBlocks[i])
    encryptedContent = ','.join(encryptedBlocks)

第 118 行的for循环遍历encryptedBlocks中的每个索引,用整数的字符串形式替换encryptedBlocks[i]处的整数。当循环完成时,encryptedBlocks应该包含一个字符串值列表,而不是一个整数值列表。

然后我们可以将encryptedBlocks中的字符串值列表传递给join()方法,该方法返回列表中连接在一起的字符串,每个字符串由逗号分隔。第 120 行将这个组合字符串存储在encryptedContent变量中。

除了加密的整数块之外,我们还将消息的长度和块大小写入文件:

    # Write out the encrypted string to the output file:
    encryptedContent = '%s_%s_%s' % (len(message), blockSize,
           encryptedContent)

第 123 行修改了encryptedContent变量,将消息的大小作为一个整数len(message),后面跟着一个下划线、blockSize,另一个下划线,最后是加密的整数块(encryptedContent)。

加密过程的最后一步是将内容写入文件。由messageFilename参数提供的文件名是通过调用第 124 行上的open()创建的。请注意,如果同名文件已经存在,新文件将覆盖它。

    fo = open(messageFilename, 'w')
    fo.write(encryptedContent)
    fo.close()
    # Also return the encrypted string:
    return encryptedContent

通过调用第 125 行的write()方法将encryptedContent中的字符串写入文件。程序写完文件内容后,第 126 行关闭了fo中的文件对象。

最后,encryptedContent中的字符串从第 128 行的函数encryptAndWriteToFile ()返回。(这是为了让调用该函数的代码可以使用该字符串,例如,在屏幕上打印它。)

现在您知道了encryptAndWriteToFile()函数如何加密消息字符串并将结果写入文件。让我们看看程序如何使用readFromFileAndDecrypt()函数来解密一条加密的消息。

从文件中解密

encryptAndWriteToFile()类似,readFromFileAndDecrypt()函数具有加密消息文件的文件名和密钥文件的文件名的参数。确保传递的是keyFilename的私钥文件名,而不是公钥。

def readFromFileAndDecrypt(messageFilename, keyFilename):
    # Using a key from a key file, read an encrypted message from a file
    # and then decrypt it. Returns the decrypted message string.
    keySize, n, d = readKeyFile(keyFilename)

第一步与encryptAndWriteToFile()相同:调用readKeyFile()函数获取keySizend变量的值。

第二步是读入文件的内容。第 138 行打开messageFilename文件进行读取。

    # Read in the message length and the encrypted message from the file:
    fo = open(messageFilename)
    content = fo.read()
    messageLength, blockSize, encryptedMessage = content.split('_')
    messageLength = int(messageLength)
    blockSize = int(blockSize)

第 139 行的read()方法调用返回文件全部内容的字符串,如果您在记事本或 TextEdit 之类的程序中打开文本文件,复制全部内容,并将其作为字符串值粘贴到您的程序中,您将会看到这样的结果。

回想一下,加密文件的格式有三个由下划线分隔的整数:一个表示消息长度的整数,一个表示所用块大小的整数,以及加密的整数块。第 140 行调用split()方法返回这三个值的列表,多重赋值技巧将这三个值分别放入messageLengthblockSizeencryptedMessage变量中。

因为split()返回的值是字符串,所以第 141 行和第 142 行使用int()messageLengthblockSize分别转换为它们的整数形式。

readFromFileAndDecrypt()函数还在第 145 行检查块大小是否等于或小于密钥大小。

    # Check that key size is large enough for the block size:
    if not (math.log(2 ** keySize, len(SYMBOLS)) >= blockSize):
        sys.exit('ERROR: Block size is too large for the key and symbol
             set size. Did you specify the correct key file and encrypted
             file?')

这种检查应该总是通过的,因为如果块太大,一开始就不可能创建加密文件。很可能为参数keyFilename指定了错误的私钥文件,这意味着该密钥无论如何都无法正确解密该文件。

encryptedMessage字符串包含许多用逗号连接在一起的块,我们将其转换回整数并存储在变量encryptedBlocks中。

    # Convert the encrypted message into large int values:
    encryptedBlocks = []
    for block in encryptedMessage.split(','):
        encryptedBlocks.append(int(block))

第 150 行的for循环遍历通过调用encryptedMessage上的split()方法创建的列表。该列表包含单个块的字符串。每次执行第 151 行时,这些字符串的整数形式被附加到encryptedBlocks列表(第 149 行的空列表)。在第 150 行的for循环完成后,encryptedBlocks列表应该包含encryptedMessage字符串中数字的整数值。

在第 154 行,encryptedBlocks中的列表连同messageLength、私钥(两个整数nd的元组值)和块大小一起被传递给decryptMessage()函数。

    # Decrypt the large int values:
    return decryptMessage(encryptedBlocks, messageLength, (n, d),
           blockSize)

第 154 行的decryptMessage()函数返回解密消息的单个字符串值,该值本身是从readFileAndDecrypt()返回的值。

调用 main()函数

最后,如果publicKeyCipher.py作为一个程序运行,而不是作为一个模块被另一个程序导入,那么第 159 和 160 行调用main()函数。

# If publicKeyCipher.py is run (instead of imported as a module), call
# the main() function.
if __name__ == '__main__':
    main()

这就完成了我们对publicKeyCipher.py程序如何使用公钥密码执行加密和解密的讨论。

总结

恭喜你,你完成了这本书!没有“破解公钥密码”这一章,因为对公钥密码的简单攻击不需要几万亿年。

在这一章中,RSA 算法被大大简化了,但它仍然是专业加密软件中使用的真正的密码。例如,当你登录一个网站或在互联网上购物时,像这样的密码可以防止任何人截获你的网络流量,从而保护你的密码和信用卡号码。

虽然用于专业加密软件的基本数学与本章所描述的相同,但是你不应该使用这个程序来保护你的秘密文件。针对像publicKeyCipher.py这样的加密程序的攻击非常复杂,但它们确实存在。(例如,因为创建的随机数random.randint()不是真正随机的,并且是可以预测的,黑客可以找出哪些数字被用作你的私钥的质数。)

本书中讨论的所有密码都可以被破解,变得一文不值。一般来说,避免编写自己的加密代码来保护您的秘密,因为您可能会在这些程序的实现中犯一些微妙的错误。黑客和间谍机构可以利用这些错误来破解你的加密信息。

只有当除了密钥之外的所有内容都可以被揭示,同时仍然保持消息的机密性时,密码才是安全的。你不能依赖于密码分析者无法使用相同的加密软件或者不知道你使用的是哪种密码。永远假设敌人了解系统!

专业加密软件是由花了数年时间研究各种密码的数学和潜在弱点的密码学家编写的。即使这样,他们编写的软件也会被其他密码学家检查,以检查错误或潜在的弱点。你完全有能力学习这些密码系统和密码数学。不是成为最聪明的黑客,而是花时间去学习成为最有知识的黑客。

我希望这本书给了你成为精英黑客和程序员所需要的基础。编程和密码学有比这本书涵盖的更多的东西,所以我鼓励你探索和学习更多!我强烈推荐西蒙·辛格的《密码本:从古埃及到量子密码术的秘密科学》( T1 ),这是一本关于密码术通史的伟大著作。你可以去 https://www.nostarch.com/crackingcodes/的查看其他书籍和网站的列表,了解更多关于密码学的知识。请将您的编程或加密问题通过电子邮件发送到[`email@protection`](/cdn-cgi/l/email-protection#ee8f82ae8780988b809a99879a869e979a868180c08d8183)或发布到[`reddit.com/r/inventwithpython`](https://reddit.com/r/inventwithpython/)。

祝你好运!