Python 密码破解指南:22~24

322 阅读1小时+

协议:CC BY-NC-SA 4.0

译者:飞龙

本文来自【OpenDocCN 饱和式翻译计划】,采用译后编辑(MTPE)流程来尽可能提升效率。

收割 SB 的人会被 SB 们封神,试图唤醒 SB 的人是 SB 眼中的 SB。——SB 第三定律

二十二、寻找和生成质数

原文:inventwithpython.com/cracking/ch…

“数学家们至今都试图在质数序列中发现某种顺序,但却徒劳无功。我们有理由相信,这是一个人类思维永远无法深入的谜。”

——莱昂哈德·欧拉,18 世纪数学家

Images

到目前为止,这本书描述的所有密码都已经存在了数百年。当黑客不得不依赖纸笔时,这些密码工作得很好,但现在它们更容易受到攻击,因为计算机处理数据的速度比人快几万亿倍。这些经典密码的另一个问题是它们使用相同的密钥进行加密和解密。当您试图发送加密的消息时,使用一个密钥会导致问题:例如,如何安全地发送密钥来解密它?

在第 23 章中,你将了解公钥密码如何通过使用非常大的质数来创建两个密钥来改进旧密码:一个用于加密的公钥和一个用于解密的私钥。要为公钥密码的密钥生成质数,您需要了解使密码成为可能的质数的一些属性(以及分解大数的难度)。在这一章中,你将利用质数的这些特性来创建primeNum.py模块,它可以通过快速判断一个数是否是质数来生成密钥。

本章涵盖的主题

质数和合数

试验部门素性测试

厄拉多塞的筛子

拉宾-米勒素性检验

什么是质数?

一个质数是大于 1 的整数,只有两个因子:1 和它本身。回想一下,一个数的因数是那些可以乘以等于原始数的数。例如,数字 3 和 7 是 21 的因数。数字 12 有因数 2 和 6,也有因数 3 和 4。

每个数字都有 1 和它自身的因数,因为 1 乘以任何数字都等于那个数字。例如,1 和 21 是 21 的因数,数字 1 和 12 是 12 的因数。如果一个数没有其他因子,这个数就是质数。例如,2 是一个质数,因为它只有 1 和 2 两个因子。

这里有一个简短的质数列表(注意 1 不被认为是质数):2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139

质数有无穷多个,也就是说不存在最大质数这种东西。它们继续变得越来越大,就像普通的数字一样。公钥密码使用大质数使密钥变得太大而无法暴力破解。

质数可能很难找到,而大质数,如用于公钥的质数,就更难找到了。为了生成大质数作为公钥,我们将找到一个随机的大数,然后通过使用素性测试来检查该数是否是质数。如果根据质数测试,这个数是质数,我们就用它;否则,我们将继续创造和测试大数,直到我们找到一个质数。

让我们看一些非常大的数字来说明公钥密码中使用的质数可以有多大。

A googol是 10 的 100 次方,写成 1 后面跟 100 个零:

10,000,000,000,000,000,000,000,000,000,000,000,000,000,000,
000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,
000,000,000,000

十亿十亿个古戈尔比一个古戈尔多 27 个零:

10,000,000,000,000,000,000,000,000,000,000,000,000,000,000,
000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,
000,000,000,000,000,000,000,000,000,000,000,000,000

但是与公钥密码使用的质数相比,这些都是很小的数字。例如,公钥程序中使用的典型质数有数百位,可能如下所示:

112,829,754,900,439,506,175,719,191,782,841,802,172,556,768,
253,593,054,977,186,2355,84,979,780,304,652,423,405,148,425,
447,063,090,165,759,070,742,102,132,335,103,295,947,000,718,
386,333,756,395,799,633,478,227,612,244,071,875,721,006,813,
307,628,061,280,861,610,153,485,352,017,238,548,269,452,852,
733,818,231,045,171,038,838,387,845,888,589,411,762,622,041,
204,120,706,150,518,465,720,862,068,595,814,264,819

这个数字太大了,我打赌你甚至没有注意到里面的错别字。

了解质数的其他一些有趣的特征也是很有用的。因为所有的偶数都是 2 的倍数,所以 2 是唯一可能的偶数质数。同样,将两个质数相乘应该得到一个只有 1、它本身和被相乘的两个质数的因数的数。(例如,质数 3 和 7 相乘得到 21,其唯一的因子是 1、21、3 和 7。)

不是质数的整数被称为合数,因为它们至少由 1 和数字之外的两个因子组成。每个合数都有一个质因数分解,这是一个只由质数组成的因数分解。比如合数 1386 是由质数 2、3、7、11 组成的,因为2 × 3 × 3 × 7 × 11 = 1386。每个合数的质因数分解对于该合数是唯一的。

我们将使用这些关于什么使一个数成为质数的信息来编写一个模块,这个模块可以确定一个小数是否是质数并生成质数。模块primeNum.py将定义以下函数:

isPrimeTrialDiv():如果传递给它的数是质数,则使用试除法算法返回True,如果传递给它的数不是质数,则返回False

primeSieve():使用厄拉多塞算法的筛子来生成质数。

rabinMiller():使用 Rabin-Miller 算法检查传递给它的数字是否是质数。与试除法算法不同,该算法可以快速处理非常大的数字。该函数不是直接调用,而是由isPrime()调用。

isPrime():在用户必须判断一个大整数是否为质数时调用。

generateLargePrime():返回一个数百位长的大质数。该函数将在第 23 章的makePublicPrivateKeys.py程序中使用。

primeNum模块的源代码

就像第 13 章中介绍的cryptomath.py一样,primeNum.py程序是作为一个模块被其他程序导入的,当它自己运行时不做任何事情。primeNum.py模块导入 Python 的mathrandom模块,以便在生成质数时使用。

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

primeNum.py

# Prime Number Sieve
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import math, random


def isPrimeTrialDiv(num):
    # Returns True if num is a prime number, otherwise False.

    # Uses the trial division algorithm for testing primality.

    # All numbers less than 2 are not prime:
    if num < 2:
        return False

    # 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 False
    return True


def primeSieve(sieveSize):
    # Returns a list of prime numbers calculated using
    # the Sieve of Eratosthenes algorithm.

    sieve = [True] * sieveSize
    sieve[0] = False # Zero and one are not prime numbers.
    sieve[1] = False

    # Create the sieve:
    for i in range(2, int(math.sqrt(sieveSize)) + 1):
        pointer = i * 2
        while pointer < sieveSize:
            sieve[pointer] = False
            pointer += i

    # Compile the list of primes:
    primes = []
    for i in range(sieveSize):
        if sieve[i] == True:
            primes.append(i)

    return primes

def rabinMiller(num):
    # Returns True if num is a prime number.
    if num % 2 == 0 or num < 2:
        return False # Rabin-Miller doesn't work on even integers.
    if num == 3:
        return True
    s = num - 1
    t = 0
    while s % 2 == 0:
        # Keep halving s until it is odd (and use t
        # to count how many times we halve s):
        s = s // 2
        t += 1
    for trials in range(5): # Try to falsify num's primality 5 times.
        a = random.randrange(2, num - 1)
        v = pow(a, s, num)
        if v != 1: # This test does not apply if v is 1.
            i = 0
            while v != (num - 1):
                if i == t - 1:
                    return False
                else:
                    i = i + 1
                    v = (v ** 2) % num
    return True

# Most of the time we can quickly determine if num is not prime
# by dividing by the first few dozen prime numbers. This is quicker
# than rabinMiller() but does not detect all composites.
LOW_PRIMES = primeSieve(100)


def isPrime(num):
    # Return True if num is a prime number. This function does a quicker
    # prime number check before calling rabinMiller().
    if (num < 2):
        return False # 0, 1, and negative numbers are not prime.

    # See if any of the low prime numbers can divide num:

    for prime in LOW_PRIMES:

        if (num == prime):

            return True

        if (num % prime == 0):

            return False

    # If all else fails, call rabinMiller() to determine if num is prime:
    return rabinMiller(num)


def generateLargePrime(keysize=1024):
    # Return a random prime number that is keysize bits in size:
    while True:
        num = random.randrange(2**(keysize-1), 2**(keysize))
        if isPrime(num):
            return num

primeNum模块的示例运行

要查看primeNum.py模块的示例输出,请在交互式 shell 中输入以下内容:

>>> import primeNum
>>> primeNum.generateLargePrime()
122881168342211041030523683515443239007484290600701555369488271748378054744009
463751312511471291011945732413378446666809140502037003673211052153493607681619
990563076859566835016382556518967124921538212397036345815983641146000671635019
637218348455544435908428400192565849620509600312468757953899553441648428119
>>> primeNum.isPrime(45943208739848451)
False
>>> primeNum.isPrime(13)
True

导入primeNum.py模块让我们可以使用generateLargePrime()函数生成一个非常大的质数。它还允许我们将任何数字,无论大小,传递给isPrime()函数来确定它是否是一个质数。

试除法算法如何工作

为了找出一个给定的数是否是质数,我们使用试除法算法。该算法继续将一个数除以整数(从 2、3 等开始),以查看是否有任何整数以 0 为余数将该数整除。例如,为了测试 49 是否是质数,我们可以尝试用 2 开始的整数来除它:

49 ÷ 2 = 24 ... 1
49 ÷ 3 = 16 ... 1
49 ÷ 4 = 12 ... 1
49 ÷ 5 = 9 ... 4
49 ÷ 6 = 8 ... 1
49 ÷ 7 = 7 ... 0

因为 7 除以 49 的余数为 0,所以我们知道 7 是 49 的因数。这意味着 49 不可能是质数,因为它至少有 1 和它本身之外的一个因子。

我们可以通过只除以质数而不是合数来加快这个过程。如前所述,合数无非是质数的合成。这意味着,如果 2 不能将 49 整除,那么一个合数,比如 6,它的因子包括 2,也不会能够将 49 整除。换句话说,任何被 6 整除的数也可以被 2 整除,因为 2 是 6 的因数。图 22-1 说明了这个概念。

Images

图 22-1:任何被 6 整除的数也被 2 整除。

再举个例子,我们来测试一下 13 是不是质数:

13 ÷ 2 = 6 ... 1
13 ÷ 3 = 4 ... 1

我们只需要测试整数到(包括)我们测试的质数的平方根。一个数的平方根指的是与自身相乘得到那个原始数的数。比如 25 的平方根是 5,因为5 × 5 = 25。因为一个数不能有两个大于其平方根的因子,所以我们可以将试除法算法测试限制在小于该数平方根的整数上。13 的平方根大约是 3.6,所以我们只需要除以 2 和 3 就可以确定 13 是质数。

作为另一个例子,数字 16 的平方根是 4。两个大于 4 的数相乘总会得到一个大于 16 的数,任何大于 4 的 16 的因子总会与小于 4 的因子配对,比如 8 × 2。因此,通过查找任何小于平方根的因子,您将找到所有大于平方根的因子。

要在 Python 中找到一个数的平方根,可以使用math.sqrt()函数。在交互式 shell 中输入以下内容,查看该函数如何工作的一些示例:

>>> import math
>>> 5 * 5
25
>>> math.sqrt(25)
5.0
>>> math.sqrt(10)
3.1622776601683795

注意math.sqrt()总是返回一个浮点值。

实现试除法算法测试

primeNum.py中第 7 行的isPrimeTrialDiv()函数以一个数为参数num,用试除法算法测试,检查该数是否为质数。如果num是一个合数,函数返回False,如果num是一个质数,函数返回True

def isPrimeTrialDiv(num):
    # Returns True if num is a prime number, otherwise False.

    # Uses the trial division algorithm for testing primality.

    # All numbers less than 2 are not prime:
    if num < 2:
        return False

第 13 行检查num是否小于2,如果是,函数返回False,因为小于 2 的数不能是质数。

第 17 行开始了实现试除法算法的for循环。它还使用math.sqrt()得到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 False
    return True

第 18 行使用模运算符(%)检查余数是否为 0。如果余数为 0,num可被i整除,因此不是质数,循环返回False。如果第 17 行上的for循环没有返回False,则该函数返回第 20 行上的True以指示num可能是质数。

函数isPrimeTrialDiv()中的试除法算法很有用,但它不是测试素性的唯一方法。你也可以用厄拉多塞筛找到质数。

厄拉多塞的筛子

厄拉多塞的筛子(发音为“era-taws-thuh-knees”)是一种算法,可以找到一个数字范围内的所有质数。为了了解这个算法是如何工作的,想象一组盒子。每个盒子里装的是从 1 到 50 的整数,都标为质数,如图 22-2 所示。

为了实现厄拉多塞筛,我们从我们的范围中排除非质数,直到只剩下质数。因为 1 从来不是质数,所以让我们先把数字 1 标为“不是质数”那我们就把所有 2 的倍数(除了 2)都标为“不是质数。”这意味着我们将把整数 4(2 × 2)、6(2 × 3)、8(2 × 4)、10、12 等等直到 50 标记为“非质数”,如图 22-3 所示。

Images

图 22-2:为数字 1 到 50 设置厄拉多塞筛

Images

图 22-3:消除数字 1 和所有偶数

然后,我们使用 3 的倍数重复这个过程:我们排除 3,并将 6、9、12、15、18、21 等标记为“非质数”我们对不包括 4 的 4 的倍数、不包括 5 的 5 的倍数等等重复这个过程,直到我们得到 8 的倍数。我们停在 8,因为它大于 7.071,7.071 是 50 的平方根。所有 9、10、11 等等的倍数都已经被标记出来了,因为任何一个比平方根大的因数都会和一个比平方根小的因数配对,我们已经标记过了。

完成的筛子应该看起来像图 22-4 ,质数显示在白色方框中。

Images

图 22-4:用厄拉多塞的筛子找到质数

利用厄拉多塞的筛子,我们发现小于 50 的质数是 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47。当您想要快速找到某个数字范围中的所有质数时,最好使用这种筛选算法。这比以前用试除法算法逐个检查每个数要快得多。

用厄拉多塞筛生成质数

primeNum.py模块第 23 行的primeSieve()函数使用厄拉多塞算法的筛子返回一个在1sieveSize之间的所有质数的列表:

def primeSieve(sieveSize):
    # Returns a list of prime numbers calculated using
    # the Sieve of Eratosthenes algorithm.

    sieve = [True] * sieveSize
    sieve[0] = False # Zero and one are not prime numbers.
    sieve[1] = False

第 27 行创建了表示sieveSize长度的布尔True值列表。因为01不是质数,所以01索引被标记为False

第 32 行的for循环遍历从2sieveSize的平方根的每个整数:

    # Create the sieve:
    for i in range(2, int(math.sqrt(sieveSize)) + 1):
        pointer = i * 2
        while pointer < sieveSize:
            sieve[pointer] = False
            pointer += i

变量pointerii的第一个倍数开始,也就是第 33 行的i * 2。然后while循环将sieve列表中的pointer索引设置为False,第 36 行将pointer改为指向i的下一个倍数。

在第 32 行的for循环结束后,sieve列表现在应该包含每个质数索引的True。我们创建一个新的列表,它在primes中以一个空列表开始,遍历整个sieve列表,当sieve[i]Truei是质数时追加数字:

    # Compile the list of primes:
    primes = []
    for i in range(sieveSize):
        if sieve[i] == True:
            primes.append(i)

第 44 行返回质数列表:

    return primes

primeSieve()函数可以找到小范围内的所有质数,isPrimeTrialDiv()函数可以快速判断一个小数字是否是质数。但是一个几百位数的大整数呢?

如果我们将一个大整数传递给isPrimeTrialDiv(),需要几秒钟来判断它是否是质数。如果这个数有几百位数长,就像我们将在第 23 章中的公钥密码程序中使用的质数一样,要花一万多亿年才能算出这个数是否是质数。

在下一节中,您将学习如何使用 Rabin-Miller 素性测试来确定一个非常大的数是否是质数。

拉宾-米勒素性算法

Rabin-Miller 算法的主要优点是,它是一个相对简单的素性测试,在普通计算机上运行只需几秒钟。尽管该算法的 Python 代码只有几行,但对其工作原理的数学证明的解释对于本书来说太长了。Rabin-Miller 算法不是素性的可靠测试。相反,它找到很可能是质数但不保证是质数的数字。但是假阳性的几率很小,这种方法对于本书来说已经足够好了。要了解更多关于拉宾-米勒算法的工作原理,你可以在en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test阅读。

*rabinMiller()函数实现这个算法来寻找质数:

def rabinMiller(num):
    # Returns True if num is a prime number.
    if num % 2 == 0 or num < 2:
        return False # Rabin-Miller doesn't work on even integers.
    if num == 3:
        return True
    s = num - 1
    t = 0
    while s % 2 == 0:
        # Keep halving s until it is odd (and use t
        # to count how many times we halve s):
        s = s // 2
        t += 1
    for trials in range(5): # Try to falsify num's primality 5 times.
        a = random.randrange(2, num - 1)
        v = pow(a, s, num)
        if v != 1: # This test does not apply if v is 1.
            i = 0
            while v != (num - 1):
                if i == t - 1:
                    return False
                else:
                    i = i + 1
                    v = (v ** 2) % num
    return True

不要担心这段代码是如何工作的。要记住的重要概念是,如果rabinMiller()函数返回True,那么num参数很可能是质数。如果rabinMiller()返回Falsenum肯定是复合的。

寻找大质数

我们将创建另一个名为isPrime()的函数来调用rabinMiller()。拉宾-米勒算法并不总是检验一个数是否是质数的最有效的方法;因此,在isPrime()函数的开始,我们将做一些简单的检查,作为判断存储在参数num中的数字是否是质数的捷径。让我们在常量变量LOW_PRIMES中存储一个所有小于 100 的质数的列表。我们可以用primeSieve ()函数来计算这个列表:

    # Most of the time we can quickly determine if num is not prime
    # by dividing by the first few dozen prime numbers. This is quicker
    # than rabinMiller() but does not detect all composites.
    LOW_PRIMES = primeSieve(100)

我们将像在isPrimeTrialDiv()中一样使用这个列表,并忽略任何小于 2 的数字(第 81 行和第 82 行):

def isPrime(num):
    # Return True if num is a prime number. This function does a quicker
    # prime number check before calling rabinMiller().
    if (num < 2):
        return False # 0, 1, and negative numbers are not prime.

num不小于2时,我们也可以使用LOW_PRIMES列表作为测试num的快捷方式。检查num是否能被所有小于 100 的质数整除不会明确地告诉我们这个数是否是质数,但它可能帮助我们找到合数。通过除以小于 100 的质数,大约 90% 传递给isPrime()的大整数可以被检测为复合整数。原因是,如果一个数能被一个质数,比如 3,整除,就不用检查这个数是否能被合数 6,9,12,15,或者其他任何 3 的倍数整除。用较小的质数除这个数比对这个数执行较慢的 Rabin-Miller 算法要快得多,所以这个快捷方式有助于程序在调用isPrime()的 90% 的时间里执行得更快。

第 85 行循环遍历LOW_PRIMES列表中的每个质数:

    # See if any of the low prime numbers can divide num:
    for prime in LOW_PRIMES:
        if (num == prime):
            return True87.         if (num % prime == 0):
            return False

如果num中的整数与prime相同,那么显然num必须是质数,第 86 行返回Truenum中的整数由每个质数使用第 87 行上的模运算符进行调制,如果结果求值为0,我们知道prime除以num所以num不是质数。在这种情况下,第 88 行返回False

这是我们将执行的三个快速测试,以确定一个数是否是质数。如果执行继续超过第 87 行,rabinMiller()函数检查num的素性。

第 90 行调用rabinMiller()函数判断数字是否为质数;然后,rabinMiller()函数获取它的返回值,并从isPrime()函数返回:

    # If all else fails, call rabinMiller() to determine if num is prime:
    return rabinMiller(num)

现在你知道如何确定一个数是否是质数,我们将使用这些质数测试来生成质数。这些将由第 23 章中的公钥程序使用。

生成大质数

使用无限循环,第 93 行的generateLargePrime()函数返回一个质数整数。它通过生成一个大随机数,将其存储在num中,然后将num传递给isPrime()来实现这一点。然后,isPrime()函数测试num是否是质数。

def generateLargePrime(keysize=1024):
    # Return a random prime number that is keysize bits in size:
    while True:
        num = random.randrange(2**(keysize-1), 2**(keysize))
        if isPrime(num):
            return num

如果num是质数,第 98 行返回num。否则,无限循环返回到第 96 行以尝试新的随机数。这个循环继续下去,直到找到一个被isPrime()函数确定为质数的数字。

generateLargePrime()函数的keysize参数的默认值为1024keysize越大,可能的密钥就越多,密码就越难被暴力破解。公钥的大小通常是以称为的数字来计算的,你将在第 23 章和第 24 章中了解更多。就目前而言,只需要知道 1024 位的数字非常大:大约 300 位数!

总结

质数在数学中有迷人的性质。正如你将在第 23 章中了解到的,它们也是专业加密软件中使用的密码的支柱。质数的定义很简单:它是一个只有 1 和它自己作为因子的数。但是确定哪些数字是质数需要一些聪明的代码。

在这一章中,我们编写了isPrimeTrialDiv()函数来判断一个数是否是质数,方法是用 2 和这个数的平方根之间的所有数来修改一个数。这是试除法算法。当一个质数由除了它的因子 1 和它本身之外的任何数构成时,它的余数不应该是 0。所以我们知道余数为 0 的数不是质数。

你知道了厄拉多塞的筛子可以快速找到一个数字范围内的所有质数,尽管它在寻找大质数时使用了太多的内存。

因为厄拉多塞的筛子和primeNum.py中的试除法算法不足以快速找到大质数,所以我们需要另一种公钥密码算法,它使用数百位长的超大质数。作为一种变通方法,您学会了使用 Rabin-Miller 算法,该算法使用复杂的数学推理来确定一个非常大的数是否是质数。

在第 23 章中,你将使用primeNum.py模块编写公钥密码程序。最后,您将创建一个比一次性密码更容易使用的密码,但不会被本书中介绍的简单黑客技术破解!

练习题

练习题的答案可以在本书的网站www.nostarch.com/crackingcodes找到。

  1. 有多少质数?

  2. 什么叫非质数的整数?

  3. 求质数的两种算法是什么?

二十三、生成公钥密码的密钥

原文:inventwithpython.com/cracking/ch…

“使用故意妥协的加密技术,它有一个后门,只有‘好人’才有密钥,你实际上没有安全保障。你还不如用预先破解的、被破坏的加密方法来加密它。”

——科利·多克托罗,科幻小说作者,2015

Images

到目前为止,你在本书中学到的所有密码都有一个共同的特点:加密密钥与解密密钥相同。这就引出了一个棘手的问题:如何与一个你从未交谈过的人分享加密信息?任何窃听者都可以截取你发送的加密密钥,就像他们截取加密信息一样容易。

在本章中,您将学习公钥加密,它允许陌生人使用公钥和私钥共享加密的消息。您将了解公钥密码,在本书中,它是基于 RSA 密码的。因为 RSA 密码很复杂,涉及多个步骤,所以您将编写两个程序。在本章中,您将编写公钥生成程序来生成您的公钥和私钥。然后,在第 24 章中,您将编写第二个程序,使用公钥密码并应用这里生成的密钥来加密和解密消息。在我们深入研究这个程序之前,让我们先来探索一下公钥加密是如何工作的。

本章涵盖的主题

公钥加密

认证

数字签名

MITM 袭击

生成公钥和私钥

混合密码系统

公钥加密

想象一下,在世界的另一端有人想和你交流。你们都知道间谍机构在监控所有的电子邮件、字母、短信和电话。要向此人发送加密邮件,双方必须就使用的密钥达成一致。但是,如果你们中的一个人用电子邮件把密钥发给另一个人,间谍机构就会截获这个密钥,然后解密任何用这个密钥加密的信息。私下见面交换密钥是不可能的。你可以试着加密密钥,但这需要将该消息的秘密密钥发送给其他人,这也会被拦截。

公钥加密通过使用两个密钥来解决这个加密问题,一个用于加密,一个用于解密,是非对称密码的一个例子。像本书之前的许多密码一样,使用相同密钥进行加密和解密的密码是对称密码

重要的是要知道使用加密密钥(公钥)加密的消息只能使用解密密钥(私钥)解密。因此,即使有人获得了加密密钥,他们也无法读取原始消息,因为加密密钥无法解密消息。

加密密钥被称为公钥,因为它可以与全世界共享。相比之下,私钥,或解密密钥,必须保密。

例如,如果 Alice 想给 Bob 发送一条消息,Alice 找到 Bob 的公钥,或者 Bob 可以把它给她。然后,Alice 使用 Bob 的公钥加密她给 Bob 的消息。因为公钥不能解密消息,所以其他每个人都可以访问 Bob 的公钥并不重要。

当 Bob 收到 Alice 的加密消息时,他使用自己的私钥对其进行解密。只有 Bob 有私钥,可以解密用他的公钥加密的消息。如果 Bob 想要回复 Alice,他会找到她的公钥并用它来加密他的回复。因为只有爱丽丝知道她的私钥,所以爱丽丝是唯一能够解密来自鲍勃的加密响应的人。即使爱丽丝和鲍勃在世界的两端,他们也可以交换信息而不用担心被拦截。

我们将在本章中实现的特定公钥密码是基于 RSA 密码的,RSA 密码发明于 1977 年,并以其发明者的姓氏首字母命名:罗恩·里维斯特、阿迪·萨莫尔和伦纳德·阿德曼。

RSA 密码在其算法中使用数百位长的大质数。这就是我们在第 22 章中讨论质数数学的原因。公钥算法创建两个随机的质数,然后使用复杂的数学(包括寻找一个模逆,你在第 13 章中学会了如何做)来创建公钥和私钥。

使用教科书 RSA 的危险

尽管我们在本书中没有编写一个程序来破解公钥密码程序,但是请记住,你将在第 24 章中编写的publicKeyCipher.py程序不是安全的。获得正确的加密是非常困难的,并且需要大量的经验才能知道一个密码(以及实现它的程序)是否真正安全。

本章中基于 RSA 的程序被称为教科书 RSA ,因为即使它在技术上使用大质数正确地实现了 RSA 算法,它也容易受到破解。例如,使用伪随机而不是真正的随机数生成函数会使密码变得脆弱,正如你在第 22 章中了解到的,拉宾-米勒素性测试不能保证总是正确的。

因此,尽管你可能无法破解由publicKeyCipher.py创建的密文,但其他人可能可以。套用高度成功的密码学家 Bruce Schneier 的话,任何人都可以创建自己无法破解的密码算法。具有挑战性的是创建一个其他人无法破解的算法,即使经过多年的分析。本书中的程序只是一个有趣的例子,旨在教你 RSA 密码的基础知识。但是一定要使用专业的加密软件来保护你的文件。你可以在www.nostarch.com/crackingcodes找到一份(通常是免费的)加密软件清单。

认证的问题

尽管公钥密码听起来很巧妙,但还是有一个小问题。比如,想象你收到了这封邮件:“你好。我是爱麦虞埃尔·果尔德施坦因,反抗军的领袖。我想和你秘密地交流一些重要的事情。附上我的公钥。”

使用此公钥,您可以确保您发送的消息不会被除公钥发送者之外的任何人阅读。但是你怎么知道发信人实际上是爱麦虞埃尔·果尔德施坦因呢?你不知道你是在向爱麦虞埃尔·果尔德施坦因发送加密信息,还是在向一个伪装成爱麦虞埃尔·果尔德施坦因的间谍机构发送信息,来引诱你进入陷阱!

尽管公钥密码,事实上,本书中的所有密码都可以提供保密性(对消息保密),但它们并不总是提供认证(证明你与之通信的人就是他们所说的那个人)。

通常,这对于对称密码来说不是问题,因为当你和一个人交换密钥时,你可以看到那个人是谁。然而,当您使用公钥加密时,您不需要见到某人就可以获得他们的公钥并开始向他们发送加密的消息。当您使用公钥加密时,要记住身份验证是至关重要的。

一个名为公钥基础设施(PKI) 的完整字段管理认证,因此您可以将公钥与具有某种安全级别的人进行匹配;然而,这个话题超出了本书的范围。

数字签名

数字签名允许您使用加密技术对文档进行电子签名。为了理解为什么数字签名是必要的,让我们看看下面的例子,Alice 给 Bob 的电子邮件:

亲爱的鲍勃:

我答应用一百万美元买下你的旧笔记本电脑。

真诚地

爱丽丝

这对鲍勃来说是个好消息,因为他想不惜任何代价处理掉他那毫无价值的笔记本电脑。但是,如果爱丽丝后来声称她没有做出这个承诺,鲍勃收到的电子邮件是假的,她没有发送。毕竟,Bob 可以轻松创建这封电子邮件。

如果他们见面,爱丽丝和鲍勃可以简单地签署一份同意出售的合同。手写签名不那么容易伪造,并且提供了一些证据证明爱丽丝确实做出了这个承诺。但是,即使爱丽丝签署了这样的合同,用她的数码相机拍了照片,并把图像文件发送给鲍勃,仍然可以想象图像可能被修改过。

RSA 密码(像其他公钥密码一样)不仅加密消息,还允许我们对文件或字符串进行数字签名。例如,Alice 可以使用她的私钥加密消息,生成只有 Alice 的公钥才能解密的密文。该密文成为文件的数字签名。它实际上并不是一个秘密,因为世界上的每个人都可以使用 Alice 的公钥来解密它。但是通过用她的私钥加密信息,爱丽丝可以用一种无法伪造的方式对信息进行数字签名。 因为只有爱丽丝能接触到她的私钥,只有爱丽丝才有可能产生这个密文,她不能说是鲍勃伪造或者篡改的!

保证已经创作了一条消息的人不能在以后否认该消息的创作被称为不可否认性

人们在许多重要活动中使用数字签名,包括加密货币、公钥认证和匿名网上冲浪。要了解更多信息,请前往www.nostarch.com/crackingcodes。请继续阅读,了解为什么身份验证与安全加密一样重要。

当心 MITM 攻击

比有人入侵我们的加密信息更阴险的是中间人中间人(MITM)攻击。在这种类型的攻击中,有人截获了你的消息,并在你不知情的情况下传递出去。例如,假设爱麦虞埃尔·果尔德施坦因真的想和你通信,并用他的公钥给你发送了一条未加密的消息,但是间谍机构的路由器截获了这条消息。代理机构可以用自己的公钥替换电子邮件附带的公钥 Emmanuel,然后将消息发送给您。你不可能知道你收到的密钥是艾曼纽的还是间谍机构的!

然后当你加密给 Emmanuel 的回复时,你实际上是用间谍机构的公钥加密,而不是 Emmanuel 的公钥。间谍机构将能够截获该消息,解密并阅读它,然后在把你的消息发送给 Emmanuel 之前,用 Emmanuel 的实际公钥重新加密它。该机构可以对 Emmanuel 发送给您的任何回复进行同样的处理。图 23-1 显示了 MITM 攻击是如何运作的。

Images

图 23-1:一次 MITM 攻击

对你和艾曼纽来说,看起来你们在秘密地交流。但是间谍机构正在阅读你所有的信息,因为你和伊曼纽尔正在用间谍机构生成的公钥加密你的信息!同样,这个问题的存在是因为公钥密码只提供机密性,而不提供身份验证。对认证和公钥基础设施的深入讨论超出了本书的范围。但是现在您已经知道了公钥加密如何提供保密性,让我们来看看如何为公钥加密生成密钥。

生成公钥和私钥的步骤

公钥方案中的每个密钥都由两个数字组成。公钥将是两个数字ne。私钥将是两个数字nd

创建这些数字的三个步骤如下:

  1. 创建两个随机的、不同的、非常大的质数: pq。将这两个数字相乘得到一个名为n的数字。

  2. 创建一个随机数,叫做e,它与(p – 1) × (q – 1)互质。

  3. 计算e的模逆,我们称之为d

注意两个密钥中都使用了n。数字d必须保密,因为它可以解密信息。现在您已经准备好编写一个生成这些密钥的程序了。

公钥生成程序的源代码

选择文件 -> 新文件,打开新文件编辑器窗口。确保primeNum.pycryptomath.py模块与程序文件在同一个文件夹中。在文件编辑器中输入以下代码,保存为makepublicprivatekeys.py

makePublic 私钥. py

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

import random, sys, os, primeNum, cryptomath


def main():
    # Create a public/private keypair with 1024-bit keys:
    print('Making key files...')
    makeKeyFiles('al_sweigart', 1024)
    print('Key files made.')

def generateKey(keySize):
    # Creates public/private keys keySize bits in size.
    p = 0
    q = 0
    # Step 1: Create two prime numbers, p and q. Calculate n = p * q:
    print('Generating p prime...')
    while p == q:
        p = primeNum.generateLargePrime(keySize)
        q = primeNum.generateLargePrime(keySize)
    n = p * q

    # Step 2: Create a number e that is relatively prime to (p-1)*(q-1):
    print('Generating e that is relatively prime to (p-1)*(q-1)...')
    while True:
        # Keep trying random numbers for e until one is valid:
        e = random.randrange(2 ** (keySize - 1), 2 ** (keySize))
        if cryptomath.gcd(e, (p - 1) * (q - 1)) == 1:
            break

    # Step 3: Calculate d, the mod inverse of e:
    print('Calculating d that is mod inverse of e...')
    d = cryptomath.findModInverse(e, (p - 1) * (q - 1))

    publicKey = (n, e)
    privateKey = (n, d)

    print('Public key:', publicKey)
    print('Private key:', privateKey)

    return (publicKey, privateKey)


def makeKeyFiles(name, keySize):
    # Creates two files 'x_pubkey.txt' and 'x_privkey.txt' (where x
    # is the value in name) with the n,e and d,e integers written in
    # them, delimited by a comma.

    # Our safety check will prevent us from overwriting our old key files:
    if os.path.exists('%s_pubkey.txt' % (name)) or
          os.path.exists('%s_privkey.txt' % (name)):
        sys.exit('WARNING: The file %s_pubkey.txt or %s_privkey.txt
              already exists! Use a different name or delete these files and
              rerun this program.' % (name, name))

    publicKey, privateKey = generateKey(keySize)

    print()
    print('The public key is a %s and a %s digit number.' %
          (len(str(publicKey[0])), len(str(publicKey[1]))))
    print('Writing public key to file %s_pubkey.txt...' % (name))
    fo = open('%s_pubkey.txt' % (name), 'w')
    fo.write('%s,%s,%s' % (keySize, publicKey[0], publicKey[1]))
    fo.close()

    print()
    print('The private key is a %s and a %s digit number.' %
          (len(str(publicKey[0])), len(str(publicKey[1]))))
    print('Writing private key to file %s_privkey.txt...' % (name))
    fo = open('%s_privkey.txt' % (name), 'w')
    fo.write('%s,%s,%s' % (keySize, privateKey[0], privateKey[1]))
    fo.close()


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

公钥生成程序的运行示例

当我们运行makePublicPrivateKeys.py程序时,输出应该类似如下(当然,您的密钥的数字看起来会不同,因为它们是随机生成的):

Making key files...
Generating p prime...
Generating q prime...
Generating e that is relatively prime to (p-1)*(q-1)...
Calculating d that is mod inverse of e...
Public key: (210902406316700502401968491406579417405090396754616926135810621
2161161913380865678407459875355468897928072386270510720443827324671435893274
8583937496850624116776147241821152026946322876869404394483922202407821672864
2424789208131826990008473526711744296548563866768454251404951960805224682425
498975230488955908086491852116348777849536270685085446970952915640050522212
204221803744494065881010331486468305317449607027884787770315729959789994713
265311327663776167710077018340036668306612665759417207845823479903440572724
068125211002329298338718615859542093721097258263595617482450199200740185492
04468791300114315056117093, 17460230769175161021731845459236833553832403910869
129054954200373678580935247606622265764388235752176654737805849023006544732896
308685513669509917451195822611398098951306676600958889189564599581456460070270
393693277683404354811575681605990659145317074127084557233537504102479937142530
0216777273298110097435989)
Private key: (21090240631670050240196849140657941740509039675461692613581062
1216116191338086567840745987535546889792807238627051072044382732467143589327
4858393749685062411677614724182115202694632287686940439448392220240782167286
4242478920813182699000847352671174429654856386676845425140495196080522468242
5498975230488955908086491852116348777849536270685085446970952915640050522212
204221803744494065881010331486468305317449607027884787770315729959789994713
265311327663776167710077018340036668306612665759417207845823479903440572724
068125211002329298338718615859542093721097258263595617482450199200740185492
04468791300114315056117093, 47676735798137710412166884916983765043173120289416
904341295971552286870991874666099933371008075948549008551224760695942666962465
968168995404993934508399014283053710676760835948902312888639938402686187075052
360773062364162664276144965652558545331166681735980981384493349313058750259417
68372702963348445191139635826000818122373486213256488077192893119257248107794
25681884603640028673273135292831170178614206817165802812291528319562200625082
55726168047084560706359601833919317974375031636011432177696164717000025430368
26990539739057474642785416933878499897014777481407371328053001838085314443545
845219087249544663398589)
The public key is a 617 and a 309 digit number.
Writing public key to file al_sweigart_pubkey.txt...
The private key is a 617 and a 309 digit number.
Writing private key to file al_sweigart_privkey.txt...

因为这两个密钥非常大,所以它们都被写入各自的文件中,al_sweigart_pubkey.txtal_sweigart_privkey.txt。我们将在第 24 章的公钥密码程序中使用这两个文件来加密和解密消息。这些文件名来自字符串'al_sweigart',它被传递给程序第 10 行的makeKeyFiles()函数。您可以通过传递不同的字符串来指定不同的文件名。

如果我们再次运行makePublicPrivateKeys.py并将相同的字符串传递给makeKeyFiles(),程序的输出应该是这样的:

Making key files...
WARNING: The file al_sweigart_pubkey.txt or al_sweigart_privkey.txt already
exists! Use a different name or delete these files and rerun this program.

提供此警告是为了防止我们意外地覆盖我们的密钥文件,这将使任何用它们加密的文件无法恢复。一定要保管好这些密钥文件!

创建main()函数

当我们运行makePublicPrivateKeys.py时,main()函数被调用,它使用makeKeyFiles()函数创建公钥和私钥文件,我们将很快定义这个函数。

def main():
    # Create a public/private keypair with 1024-bit keys:
    print('Making key files...')
    makeKeyFiles('al_sweigart', 1024)
    print('Key files made.')

因为计算机可能需要一段时间来生成密钥,所以我们在调用makeKeyFiles()之前在第 9 行打印一条消息,让用户知道程序正在做什么。

第 10 行的makeKeyFiles()调用传递字符串'al_sweigart'和整数1024,生成 1024 位的密钥并存储在文件al_sweigart_pubkey.txtal_sweigart_privkey.txt中。密钥越大,可能的密钥就越多,密码的安全性就越强。但是,大密钥意味着加密或解密消息需要更长的时间。对于本书的例子,我选择 1024 位的大小作为速度和安全性之间的平衡;但实际上,2048 位甚至 3072 位的密钥大小对于安全公钥加密是必要的。

使用generateKey()函数生成密钥

创建密钥的第一步是产生两个随机质数pq。这些数字必须是两个大而不同的质数。

def generateKey(keySize):
    # Creates public/private keys keySize bits in size.
    p = 0
    q = 0
    # Step 1: Create two prime numbers, p and q. Calculate n = p * q:
    print('Generating p prime...')
    while p == q:
        p = primeNum.generateLargePrime(keySize)
        q = primeNum.generateLargePrime(keySize)
    n = p * q

你在第 22 章的primeNum.py程序中写的generateLargePrime()函数在第 20 行和第 21 行返回两个质数作为整数值,我们将它们存储在变量pq中。这是在一个while循环中完成的,只要pq相同,该循环就会继续。如果generateLargePrime ()pq返回相同的整数,程序将再次尝试为pq寻找唯一的质数。keySize中的值决定了pq的大小。在第 22 行,我们将pq相乘,并将乘积存储在n中。

接下来,在步骤 2 中,我们计算公钥的另一部分: e

计算e

通过找到一个与(p – 1) × (q – 1)互质的数来计算e的值。我们不会详细讨论为什么以这种方式计算e,但是我们希望e(p – 1) × (q – 1)互质,因为这确保了e将总是产生唯一的密文。

使用一个无限的while循环,第 26 行计算一个数e,它与p – 1q – 1的乘积互质。

    # Step 2: Create a number e that is relatively prime to (p-1)*(q-1):
    print('Generating e that is relatively prime to (p-1)*(q-1)...')
    while True:
        # Keep trying random numbers for e until one is valid:
        e = random.randrange(2 ** (keySize - 1), 2 ** (keySize))
        if cryptomath.gcd(e, (p - 1) * (q - 1)) == 1:
            break

第 28 行上的random.randrange()调用返回一个随机整数,并将其存储在e变量中。然后第 29 行使用cryptomath模块的gcd()函数测试e是否与(p - 1) * (q - 1)互质。如果e是互质的,那么第 30 行的break语句就跳出了无限循环。否则,程序执行跳回到第 26 行,继续尝试不同的随机数,直到找到一个与(p - 1) * (q - 1)互质的随机数。

接下来,我们计算私钥。

计算d

第三步,找到用于解密的另一半私钥,也就是dd只是e的模逆,我们已经在cryptomath模块中有了findModInverse()函数,我们在第 13 章中使用它来计算。

第 34 行调用findModInverse()并将结果存储在变量d中。

    # Step 3: Calculate d, the mod inverse of e:
    print('Calculating d that is mod inverse of e...')
    d = cryptomath.findModInverse(e, (p - 1) * (q - 1))

我们现在已经定义了生成公钥和私钥所需的所有数字。

找回密钥

回想一下,在公钥密码中,公钥和私钥各由两个数字组成。存储在ne中的整数代表公钥,存储在nd中的整数代表私钥。第 36 行和第 37 行将这些整数对作为元组值存储在publicKeyprivateKey中。

    publicKey = (n, e)
    privateKey = (n, d)

在第 39 行和第 40 行使用print()调用,generateKey()函数中的其余行在屏幕上打印出密钥。

    print('Public key:', publicKey)
    print('Private key:', privateKey)

    return (publicKey, privateKey)

然后当调用generateKey()时,第 42 行返回一个包含publicKeyprivateKey的元组。

在我们生成了公钥和私钥之后,我们还希望将它们存储在文件中,以便我们的公钥密码程序可以在以后使用它们来加密和解密。此外,将密钥存储在文件中非常有用,因为组成每个密钥的两个整数长达数百位,这使得它们难以记忆或方便地写下来。

使用makeKeyFiles()函数创建密钥文件

makeKeyFiles()函数通过将文件名和密钥大小作为参数将密钥存储在文件中。

def makeKeyFiles(name, keySize):
    # Creates two files 'x_pubkey.txt' and 'x_privkey.txt' (where x
    # is the value in name) with the n,e and d,e integers written in
    # them, delimited by a comma.

该函数创建格式为<name>_pubkey.txt<name>_privkey.txt的两个文件,分别存储公钥和私钥。传递给函数的name参数决定了文件的<name>部分。

为了防止再次运行程序时意外删除密钥文件,第 51 行检查具有给定名称的公钥或私钥文件是否已经存在。如果是这样,程序会退出并显示一条警告消息。

    # Our safety check will prevent us from overwriting our old key files:
    if os.path.exists('%s_pubkey.txt' % (name)) or
          os.path.exists('%s_privkey.txt' % (name)):
      sys.exit('WARNING: The file %s_pubkey.txt or %s_privkey.txt
            already exists! Use a different name or delete these files and
            rerun this program.' % (name, name))

在这个检查之后,第 54 行调用generateKey()并通过keySize来创建指定大小的公钥和私钥。

    publicKey, privateKey = generateKey(keySize)

generateKey()函数返回一个由两个元组组成的元组,它使用多重赋值将其赋给publicKeyprivateKey变量。第一个元组有两个整数用于公钥,第二个元组有两个整数用于私钥。

现在我们已经完成了创建密钥文件的设置,我们可以创建实际的密钥文件了。我们将把组成每个密钥的两个数字存储在文本文件中。

第 56 行打印一个空行,然后第 57 行为用户打印关于公钥的信息。

    print()
    print('The public key is a %s and a %s digit number.' %
          (len(str(publicKey[0])), len(str(publicKey[1]))))
    print('Writing public key to file %s_pubkey.txt...' % (name))

第 57 行通过使用str()函数将这些值转换成字符串,然后使用len()函数找到字符串的长度,来指示在publicKey[0]publicKey[1]的整数中有多少位数。然后,第 58 行报告公钥正被写入文件。

密钥文件的内容包括密钥大小、一个逗号、n整数、另一个逗号和e(或d)整数。例如,文件的内容应该是这样的:密钥大小,整数n,ed,如下面的al_sweigart_pubkey.txt文件所示:

Images

第 59 行到第 61 行以写模式打开<name>_pubkey.txt文件,将密钥写入文件,然后关闭它。

    fo = open('%s_pubkey.txt' % (name), 'w')
    fo.write('%s,%s,%s' % (keySize, publicKey[0], publicKey[1]))
    fo.close()

第 63 到 68 行做的和第 56 到 61 行一样,除了它们把私钥写到<name>_privatekey.txt文件中。

    print()
    print('The private key is a %s and a %s digit number.' %
          (len(str(publicKey[0])), len(str(publicKey[1]))))
    print('Writing private key to file %s_privkey.txt...' % (name))
    fo = open('%s_privkey.txt' % (name), 'w')
    fo.write('%s,%s,%s' % (keySize, privateKey[0], privateKey[1]))
    fo.close()

确保没有人侵入你的电脑并复制这些密钥文件。如果他们获得了你的私人密钥文件,他们就能解密你所有的信息!

调用main()函数

在程序结束时,如果makePublicPrivateKeys.py作为一个程序运行,而不是作为一个模块被另一个程序导入,第 73 和 74 行调用main()函数。

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

现在你有了一个可以生成公钥和私钥的程序。虽然公钥密码在无法安全交换密钥的两个人之间开始通信时很有用,但由于公钥加密的缺点,它通常不用于所有加密通信。通常,公钥密码被用在混合密码系统中。

混合密码系统

RSA 和公钥加密需要大量的计算时间。对于每秒钟需要与其他计算机进行成千上万次加密连接的服务器来说尤其如此。作为一种变通方法,人们可以使用公钥加密来加密和分发密钥,以获得更快的对称密钥密码,这是解密和加密密钥相同的任何类型的密码。在使用公钥加密的消息将对称密码的密钥安全地发送给接收方之后,发送方可以将对称密码用于将来的消息。使用对称密钥密码和非对称密钥密码进行通信被称为混合密码系统。你可以在en.wikipedia.org/wiki/Hybrid_cryptosystem了解更多关于混合密码系统的信息。

总结

在本章中,您学习了公钥加密的工作原理,以及如何编写生成公钥和私钥的程序。在第 24 章中,你将使用这些密钥来执行使用公钥密码的加密和解密。

练习题

练习题的答案可以在本书的网站www.nostarch.com/crackingcodes找到。

  1. 对称密码和非对称密码有什么区别?

  2. Alice 生成一个公钥和一个私钥。不幸的是,她后来丢失了她的私钥。

    1. 其他人能给她发送加密信息吗?

    2. 她能够解密之前发给她的信息吗?

    3. 她能够对文档进行数字签名吗?

    4. 其他人能够验证她之前签署的文件吗?

  3. 什么是认证和保密?它们有什么不同?

  4. 什么是不可否认性?

二十四、公钥密码的编程

原文:inventwithpython.com/cracking/ch…

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

——布鲁斯·施奈尔,《应用密码学》的作者

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 个字符)

Alan Mathison Turing was a British cryptanalyst and computer scientist. He was highly influential in the development of computer science and provided a formalisation of
3013810338120027658120611166332270159047154
7608326152595431391575797140707837485089852
659286061395648657712401264848061468979996
871106525448961558640277994456848107158423
162065952633246425985956987627719631460939
2565956887693059829154012923414594664511373
093526087354321666137736234609864038110994
85392482698

第二块 -> (169 个字符)

the concepts of algorithm and computation with the Turing machine. Turing is widely considered to be the father of computer science and artificial intelligence. During W
1106890780922147455215935080195634373132680
102708192713651484085475402677752791958075
872272026708702634070281109709555761008584
1376819190225258032442691476944762174257333
902148064107269871669093655004577014280290
4244524711751435049117398986044838791597315
078937194860112574798016587564452792451567
15863348631

第三块 -> (82 个字符)

orld War II he worked for the Government Code and Cypher School at Bletchley Park.
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为 116284564958604315258674918142848831759,e为 13805220545651593223。(对于 1024 位密钥,这些数字会大得多。)

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

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

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

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

例如,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。但是在不知道d的情况下,数学上不可能解出原始消息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()max()函数

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()列表方法来完成。

使用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()不是真正随机的,并且是可以预测的,黑客可以找出哪些数字被用作你的私钥的质数。)

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

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

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

我希望这本书给了你成为精英黑客和程序员所需要的基础。编程和密码学有比这本书涵盖的更多的东西,所以我鼓励你探索和学习更多!我强烈推荐西蒙·辛格的《密码本:从古埃及到量子密码术的秘密科学》,这是一本关于密码术通史的伟大著作。你可以去 www.nostarch.com/crackingcod…](/cdn-cgi/l/email-protection#ee8f82ae8780988b809a99879a869e979a868180c08d8183)或发布到[reddit.com/r/inventwithpython](reddit.com/r/inventwit…

祝你好运!