密码学原理与实践笔记 - wuuconix's blog

IT知识
362
0
0
2023-04-16

Caesar Cipher

凯撒密码。所有的字母全部往后移3位。比如abc则变成def

Shift Cipher

移位密码。和凯撒密码原理类似。只不过移动的key不是3而可以是0~25中的任何值。所以Caesar Cipher是Shift Cipher的一种特例。

下面写了两个移位密码的编码和解码的小程序。

import sys

if(sys.argv[1] == '-s'):
    plaintext = sys.argv[2]
    cipyertext = ""
    if (sys.argv[3] == '-n'):
        key = int(sys.argv[4]) % 26
        for word in plaintext:
            if (word <= "z" and word >= "a"):
                cipyertext += chr( (ord(word) + key - ord('a')) %26 + ord('a') )
            elif (word <= "Z" and word >= "A"):
                cipyertext += chr( (ord(word) + key - ord('A')) %26 + ord('A') )
            else:
                cipyertext += word
        print(cipyertext)
    else:
        print(f"unrecognized signal {sys.argv[3]}")
else:
    print(f"unrecognized signal {sys.argv[1]}")
import sys

if(sys.argv[1] == '-s'):
    ciphertext = sys.argv[2]
    plaintext = ""
    if (sys.argv[3] == '-n'):
        key = int(sys.argv[4]) % 26
        for word in ciphertext:
            if (word <= "z" and word >= "a"):
                plaintext += chr( (ord(word) - key - ord('a') + 26) %26 + ord('a') )
            elif (word <= "Z" and word >= "A"):
                plaintext += chr( (ord(word) - key - ord('A') + 26) %26 + ord('A') )
            else:
                plaintext += word
        print(plaintext)
    elif (sys.argv[3] == '--all' or sys.argv[3] == '-a'):
        for key in range(1, 26):
            plaintext = ""
            for word in ciphertext:
                if (word <= "z" and word >= "a"):
                    plaintext += chr( (ord(word) - key - ord('a') + 26) %26 + ord('a') )
                elif (word <= "Z" and word >= "A"):
                    plaintext += chr( (ord(word) - key - ord('A') + 26) %26 + ord('A') )
                else:
                    plaintext += word
            print(f"key: {key}   {plaintext}")
    else:
        print(f"unrecognized signal {sys.argv[3]}")
else:
    print(f"unrecognized signal {sys.argv[1]}")

img

img

Affine Cipher

仿射密码。和凯撒密码和移位密码类似。前两者都是在原字母的基础上加上一个常数k。而仿射密码则是在原字母的基础上乘上一个常数a,再加上一个常数b。当然最后都是需要模26的。

为了保证能够被解密,a和26必须是互素的,因为只有这样,a才有逆元,才能够解密。

求逆元可以用拓展欧几里得算法,脑子太笨,还看不懂算法的原理。

以下是加密和解密的脚本

import sys

if(sys.argv[1] == '-s'):
    plaintext = sys.argv[2]
    cipyertext = ""
    if (sys.argv[3] == '-a'):
        key1 = int(sys.argv[4])
        if (key1 in [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25]):
            if (sys.argv[5] == '-b'):
                key2 = int(sys.argv[6])
                for word in plaintext:
                    if (word <= "z" and word >= "a"):
                        cipyertext += chr( ((ord(word) - ord('a')) * key1 + key2) % 26 + ord('a'))
                    elif (word <= "Z" and word >= "A"):
                        cipyertext += chr( ((ord(word) - ord('A')) * key1 + key2) % 26 + ord('A'))
                    else:
                        cipyertext += word
                print(cipyertext)
            else:
                print(f"unrecognized signal {sys.argv[5]}")
        else:
            print(f"error: a must be one of [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25]")
    else:
        print(f"unrecognized signal {sys.argv[3]}")
else:
    print(f"unrecognized signal {sys.argv[1]}")
import sys

def ext_euclid(a, b):     
    if b == 0:         
        return 1, 0, a     
    else:         
        x, y, q = ext_euclid(b, a % b) 
        # q = gcd(a, b) = gcd(b, a%b)         
        x, y = y, (x - (a // b) * y)         
        return x, y, q

if(sys.argv[1] == '-s'):
    ciphertext = sys.argv[2]
    plaintext = ""
    if (sys.argv[3] == '-a'):
        key1 = int(sys.argv[4])
        if (key1 in [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25]):
            key1_reverse = (ext_euclid(key1, 26)[0] + 26) % 26 #逆元
            if (sys.argv[5] == '-b'):
                key2 = int(sys.argv[6]) % 26
                for word in ciphertext:
                    if (word <= "z" and word >= "a"):
                        plaintext += chr( ((ord(word) - ord('a') - key2 + 26) * key1_reverse) % 26 + ord('a') )
                    elif (word <= "Z" and word >= "A"):
                        plaintext += chr( ((ord(word) - ord('A') - key2 + 26) * key1_reverse) % 26 + ord('A') )
                    else:
                        plaintext += word
                print(plaintext)
        else:
            print(f"error: a must be one of [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25]")
    elif (sys.argv[3] == '--all'): #尝试所有的a,b组合
        for key1 in [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25]:
            key1_reverse = ((ext_euclid(key1, 26))[0] + 26) % 26
            for key2 in range(1, 26):
                plaintext = ""
                for word in ciphertext:
                    if (word <= "z" and word >= "a"):
                        plaintext += chr( ((ord(word) - ord('a') - key2 + 26) * key1_reverse) % 26 + ord('a') )
                    elif (word <= "Z" and word >= "A"):
                        plaintext += chr( ((ord(word) - ord('A') - key2 + 26) * key1_reverse) % 26 + ord('A') )
                    else:
                        plaintext += word
                print(f"a: {key1} b: {key2}  {plaintext}")
    else:
        print(f"unrecognized signal {sys.argv[3]}")
else:
    print(f"unrecognized signal {sys.argv[1]}")

img

img

仿射密码的密钥空间是ϕ(26)∗26=312\phi(26)*26=312ϕ(26)∗26=312 遍历起来还是很快的。

仿射密码是线性的,于是为了提高加密强度,便出现了非线性加密方式,比如以下的单表代替。

Monoalphabetic Substitution Ciphers

单表代替密码,其实就是用了一张表,将明文中的字母做一个转换,形成密文。

它的密钥空间十分庞大,即把每一种表的可能都试一遍,当然这个表中的字母不能重复,所以密钥空间是26!=40329146112660563558400000026! = 40329146112660563558400000026!=403291461126605635584000000。

从表面上来看,单表代替貌似十分危险,毕竟很难通过暴力测试的方式得到明文。

但是这种单表替换中传承了一个英语的巨大的特征——字母的频率。

img

我们可以分析明文中各个字母的频率来猜测字母的对应顺序。

以下是统计给定字符串中各个字母的个数并按降序排序的脚本。

import sys

dict = {}
ciphertext = sys.argv[1]
alphanum = 0
for i in ciphertext:
    if (i.isalpha()):
        if (i.lower() in dict.keys()): #已经有键
            dict[i.lower()] += 1
        else:
            dict[i.lower()] = 1
        alphanum += 1
dict = sorted(dict.items(), key=lambda x:x[1], reverse=True)
print(dict)

img

为了消除密文中的这种频率信息,又有了多表代替密码和多名代替密码。

Vigenere Cipher

维吉尼亚密码就是一种多表代替密码Polyalphabetic Substituton Ciphers。下面我用我的理解来说一下为什么维吉尼亚密码是一种多表代替密码。

假设维吉尼亚的密钥只有一个字母"b"。由于在维吉尼亚密码中"a"实际上是0,那么"b"就是1,所以实际上这时就是偏移为1的移位密码。效果如下。

img

而这里其实就是一张特殊的单表。

A

B

C

D

E

B

C

D

E

F

所以这里也可以看到凯撒密码和移位密码都属于单表密码,都可以用频率分析来得到明文。

而如果维吉尼亚密钥中有两个字母呢?比如说"bc"。很容易想到,这时候其实是有两张表的。奇数位明文用的是"b"的表,而偶数位明文用的是"c"的表。

这就是我理解的多表

维吉尼亚加密起来非常简单,其实就是做一个ascii变化。代码如下。

import sys

plaintext = sys.argv[1].lower()
key = sys.argv[2].lower()
ciphertext = ""
num = 0
for i in plaintext:
    if (i.isalpha()):
        ciphertext += chr((ord(i) - ord('a') + (ord(key[num % len(key)]) - ord('a'))) % 26 + ord('a'))
        num +=1
    else:
        ciphertext += i
print(ciphertext)

img

如果知道密钥的话,解密也是十分简单的。

import sys

ciphertext = sys.argv[1].lower()
key = sys.argv[2].lower()
plaintext = ""
num = 0
for i in ciphertext:
    if (i.isalpha()):
        plaintext += chr((ord(i) - ord('a') - (ord(key[num % len(key)]) - ord('a')) + 26) % 26 + ord('a'))
        num +=1
    else:
        plaintext += i
print(plaintext)

img

那如果我们不知道密钥如何破解维吉尼亚密码呢?由于密钥中每个字母都单独对应一个移位密码表,这就导致明文中同一个字母可能被不同的表加密,从而产生出不同的密文字母,从而整体的频率信息将被破化。看似是坚不可摧了。不知道密文,密钥空间将成为26^n,同时还”消除“了单表代替拥有的频率信息。

实际上频率信息仍然存在着,只不过不是整体的密文体现出来的,我们需要对密文进行分组,分组的依据是模密文长度的余数。

比如这个将 “wuuconixyyds” 用 "abc"加密产生的密文 “wvwcppiyayeu”。将密文每三个分为一组。

wciy

vpye

wpau

可以看到,分好之后,每组的密文字母加密时其实用的都是同一个表,比如"wciy"实际上都是用"a"的表,其实就是没动2333。

其实也就是由于密钥是循环使用的,会导致分组之后的密文重新表现出英文字母的频率性。

说是频率性,其实每一组密文都是移位后的结果 ,我们完全可以遍历所有的偏移来尝试。

但是这里有一个问题,平常我们破解移位密码的时候,就是遍历所有的移位,然后观察每种可能移位解密的结果,去看文本是否有意义。但是这里每组的字母在密文整体中是分散的,这就导致我们无法通过肉眼去判断尝试的偏移是否正确,从而无法判断。

那不弄偏移了,直接看频率来分析。但是很快就也会发现这个问题,我们在破解单表密码的频率分析中充斥着猜测。因为许多字母的频率都是十分相近,除了能够远超其他 字母的e一马当先,其他字母我们都需要代入文本,看看是否组成某个单词等等来猜测。这时我们遇到了和爆破移位同样的问题,即组内密文是离散的,它们无法组成有意义的单词或句子,我们就无法进行猜测了。

img

这个问题困扰了我很久,便去咨询了密码学大佬shrimpwr。他给出了一个方法,即遍历了偏移,又分析了频率,而且分析的频率不依靠人眼,而真正依靠数学,可以实现全自动化破译。

过程如下。

img

大体就是去统计密文中各个字母的频率,从"a"到"z"的频率分别和正常文本的“a”到"z"的频率相乘,并把这26个乘数相加,得到一个相关值 Correlation

然后去模拟偏移,得到26个相关值,这26个相关值中的最大值对应的偏移就是移位的偏移。

为什么正确的偏移的时候相关值最大呢?因为正确偏移的时候,我们相当于做了一遍"a"乘"a"的频率加到"z"乘"z"的频率,即各个字母频率的平方和。在这种情况下 ,最大的频率e会乘上自己,强强联合,实现最大值,最小的频率会乘上自己,自生自灭,不去影响别的大频率,我们想象一下也能得到这种情况是得到的总和是最大的。利用这种方法我们根本就不需要去手动尝试,直接就得出结果了。

以下是脚本 。

q=[ 0.08167, 0.01492, 0.02782, 0.04253, 0.12702, 0.02228, 0.02015, 0.06094, \
    0.06966, 0.00153, 0.00772, 0.04025, 0.02406, 0.06749, 0.07507, 0.01929, \
    0.00095, 0.05987, 0.06327, 0.09056, 0.02758, 0.00978, 0.02360, 0.00150, \
    0.01974, 0.00074 ] #一般文本a~z频率
dict = {} #密文各个字母的个数
for i in range(0, 26): #各字母个数初始化为0
    dict[chr(ord('a') + i)] = 0

ciphertext = ""
with open("ciphertext.txt", "r") as f: #文本中读入密文
    ciphertext = f.read().lower()

alphanum = 0
for i in ciphertext:
    if (i.isalpha()):
        if (i in dict.keys()):
            dict[i] += 1
        alphanum += 1

p = []
for i in range(0, 26):
    p.append( round(dict[chr(ord('a') + i)] / alphanum, 3))

correlations = []
for i in range(0, 26): #模拟偏移,计算correlation
    correlation = 0
    for j in range(0, 26): #26个字母的频率相乘
        correlation += q[j] * p[(j + i - 26) % 26]
    correlations.append(round(correlation, 3))

shift = correlations.index(max(correlations))
print(f"偏移:{shift}  相关值: {max(correlations)}")

with open("result.txt", "w") as f:
    plaintext = ""
    for i in ciphertext:
        if i.isalpha():
            plaintext += chr( (ord(i) - ord('a') - shift + 26) % 26 + ord('a'))
        else:
            plaintext += i
    f.write(plaintext)
    print("result saved in 'result.txt'")

img

img

这种方法甚至对一些很短的句子都有效果。

img

img

这个句子中中甚至都没有"e",要手工去分析频率几乎是噩梦,但是用这种方法就是游刃有余了。

好了,现在我们对如何破译每组的密文已经很有把握了,那如何对密文进行分组呢?很显然 ,我们需要知道密钥长度。

但是密钥长度可不是一开始就给你的,破译密钥长度有两种方法,一种是Kasiski Test,它的思路就是由于密钥的循环利用,可能能遇到明文中相同单词用相同密钥部分加密的情况,这就导致密文的一致。我们可以在密文中查找这种一样的,记录它们的距离,距离的公因数就是密钥长度。这种方法感觉脚本编写起来非常麻烦,需要匹配相同字符串,感觉更适合用眼睛看。第二种就十分优秀了,重合指数法,它的思路就是在正常的英文文本中,两个字母重复的概率是接近于"0.065"的,而完全随机的文本的概率则接近"0.038“,而且这个重合指数计算 起来也非常容易,只需要计算字母的频率的平方和即可。所以它非常适合来自动化破解密钥长度。

img

下面是一段计算重合指数的脚本。

import sys

if (sys.argv[1] == "-h" or sys.argv[1] == "--help" or sys.argv[1] == "help"):
    print("python3 IndexOfCoincidence.py [-s string] [-f filepath]")
    exit(0)
ciphertext = ""
if (sys.argv[1] == "-s"):
    ciphertext = sys.argv[2].lower()
elif (sys.argv[1] == "-f"):
    with open(sys.argv[2], "r") as f:
        ciphertext = f.read().lower()
else:
    print("error. please type -h for more information")
    exit(0)

f = [] #频数
for i in range(0, 26):
    f.append(0) #每个位置对应相依字母,初值置零
alphancount = 0
for i in ciphertext:
    if i.isalpha():
        f[ord(i) - ord('a')] += 1
        alphancount += 1
p = [] #对应频率
ioc = 0
for i in range(0, 26):
    p.append(round(f[i] / alphancount, 3))
    ioc += (f[i] / alphancount) ** 2
print(round(ioc, 4))

我们尝试一下不同的文本,来看看它们的重合指数。

img

img

高于正常文本的0.065。

img

img

这里简爱中的一句话比之前的flag更加接近理想值。

img

img

当尝试简爱中的一个段落的重合指数时更加接近理想值。

img

img

当我们把目标变成简爱的第一个章节的时候,重合指数已经稳稳的变成理想值了。

在确定密钥长度时,我利用的方法是计算和理想值的差值,差值最小的那组就是密钥的长度。但是这就需要要求密文的文本较长,否则就像这里的第一个和第二个例子,与理想值差的有些多了,到时候差值也会很大,很可能就被认为不是密钥长度了。

以下是我的维吉尼亚密码最终解密脚本,包含输入密钥的普通解密和不输入密钥的自动破译。

import copy
import sys

def get_key_length(ciphertext):
    dict_model = {} #记录组内各个字母的个数
    for i in range(0, 26):
        dict_model[chr( ord('a') + i )] = 0
    differences = []
    for i in range(1, 10):
        ll = [] #存各组密文字典的列表 
        for j in range(0, i): #加入对应个数的字典
            dict = copy.deepcopy(dict_model) #深拷贝,防止一个变了另一个也变
            ll.append(dict) 
        num = 0
        for c in ciphertext:
            if c.isalpha():
                ll[num % i][c] += 1
                num += 1
        difference = 0
        for dict in ll:
            ioc = 0
            for j in range(0, 26):
                ioc += (dict[chr( ord('a') + j )] / (num // i)) ** 2 #注意这里每组的ioc除以的总数是单组的字母总数,而不是所有的字母总数
            difference += abs(ioc - 0.065) #计算重合指数和0.065的绝对差值
        differences.append(round(difference / i, 3))    #平均插值

    key_length = differences.index(min(differences)) + 1
    return key_length

def get_shift(ciphertext):
    q=[ 0.08167, 0.01492, 0.02782, 0.04253, 0.12702, 0.02228, 0.02015, 0.06094, \
    0.06966, 0.00153, 0.00772, 0.04025, 0.02406, 0.06749, 0.07507, 0.01929, \
    0.00095, 0.05987, 0.06327, 0.09056, 0.02758, 0.00978, 0.02360, 0.00150, \
    0.01974, 0.00074 ] #一般文本a~z频率
    dict = {} #密文各个字母的个数
    for i in range(0, 26): #各字母个数初始化为0
        dict[chr(ord('a') + i)] = 0

    for i in ciphertext:
        dict[i] += 1

    p = []
    for i in range(0, 26):
        p.append( round(dict[chr(ord('a') + i)] / len(ciphertext), 3))

    correlations = []
    for i in range(0, 26): #模拟偏移,计算correlation
        correlation = 0
        for j in range(0, 26): #26个字母的频率相乘
            correlation += q[j] * p[(j + i - 26) % 26]
        correlations.append(round(correlation, 3))

    shift = correlations.index(max(correlations))
    return shift

def decode(ciphertext, key):
    plaintext = ""
    num = 0
    for i in ciphertext:
        if (i.isalpha()):
            plaintext += chr((ord(i) - ord('a') - (ord(key[num % len(key)]) - ord('a')) + 26) % 26 + ord('a'))
            num +=1
        else:
            plaintext += i
    with open("result.txt", "w") as f:
        f.write(plaintext)
    print("result saved  in 'result.txt'")
    if (len(plaintext) < 100):
        print(plaintext)

if __name__ == "__main__":
    if (sys.argv[1] == '-h' or sys.argv[1] == '--help' or sys.argv[1] == 'help'):
        print("python3 vigenere_decode.py [-f filepath] [-s string] [-k key]")
        exit(0)
    if (sys.argv[1] == '-f'):
        ciphertext = ""
        with open(sys.argv[2], "r") as f:
            ciphertext = f.read().lower()
    elif (sys.argv[1] == '-s'):
        ciphertext = sys.argv[2]
    
    try:
        if (sys.argv[3] == '-k'):
            decode(ciphertext, sys.argv[4])
    except Exception: #报错说明没有-k,需要去爆破密钥值
        key_length = get_key_length(ciphertext)
        groups = []
        for i in range(0, key_length):
            groups.append("")
        num = 0
        for i in ciphertext:
            if i.isalpha():
                groups[num % key_length] += i
                num += 1
        key = ""
        for i in groups:
            key += chr( ord('a') + get_shift(i) )
            decode(ciphertext, key)

这里举几个例子,我们先从对解密脚本简单的情况开始,直接把简爱第一章的密文丢给它看看能不能解密出来。

先用加密脚本加密一下,密钥为wuuconix

python3 vigenere_encdoe.py -f "plaintext.txt" -k wuuconix

img

img

成功解密。

再把难度提升一些,只弄一个段落。

img

img

也成功了。再直接用第一句话试试。

img

img

失败了2333,果然密文太少了,利用重合指数法就不准了。

Hill Cipher

希尔密码,于1929年发明,利用基本的矩阵来进行替换。它是一种多名代替密码 Polygraphic Substitution Ciphers

下面我来讲一下我对Monoalphabetic Ciphers,Polyalphabetic CiphersPolygraphic Ciphers的理解。

单表代替,每个明文字母只有一种特定的置换方式。

多表代替,每个明文字母会根据在明文中位置的不同,可能有不同的置换方式。(Vigenere Cipher)

多名代替,就是把所有的明文字母一起加密成某个密文,如下图。我们以第一个密文字母c1为例。

img

c1=m1∗k11+m2∗k21+m3∗k31+m4∗k41c_1 = m_1*k_{11}+m_2 * k_{21}+m_3*k_{31}+m_4*k_{41}c1​=m1​∗k11​+m2​∗k21​+m3​∗k31​+m4​∗k41​

我们可以看到第一个密文的加密过程中运用到了所有的明文字母,这大概就是多名的意义了。

希尔密码的密钥需要是一个可逆矩阵,这样才能够被正确解密。

在不知道密钥的情况下也能被破译,但是我已经看不懂了(

img

希尔加密运用到了现代数学的矩阵知识。希尔密码也很难用人力来加密和解密,但是在现代计算机强大的算力下也被证明是可破译的。

维基百科Polygraphic substitution - Wikipedia也把希尔加密称为是古典密码和现代密码的分界线。

In 1929, Lester S. Hill developed the Hill cipher, which uses matrix algebra to encrypt blocks of any desired length. However, encryption is very difficult to perform by hand for any sufficiently large block size, although it has been implemented by machine or computer. This is therefore on the frontier between classical and modern cryptography.

Permutation Cipher

之前讲的所有密码都属于代替密码,就是通过某种规则把明文的字母代替为另外的字母。

接下来 讲的置换密码Permutation Cipher(又叫做Transpostion Cipher)是什么意思呢?实际上就是把明文中的字母打乱,换个顺序。

下面举几个例子。

铁轨密码 Rail Fence Cipher Rail fence cipher - Wikipedia

实际上就是把明文按照WWWW的形式来写,然后一行一行读,同时为了可读性,每5个密文加个空格。

比如flag{wuuconix_yyds}的加密过程

f...{...c...x...d..
.l.g.w.u.o.i._.y.s.
..a...u...n...y...}

它最终的密文就是 f{cxd lgwuo i_ysa uny}

三角密码 Triangle Transposition Ciphe

大概就是把明文以金字塔的方式以从横向写出来。

然后以竖向的方式读出来就成了密文。

img

铝带密码Scytale Cipher

这就非常牛逼了,把密文绕着斯巴达加密棒绕一圈就可以得到密文。

img

块置换加密 Block Permutation Cipher

举一个例子,明文wuuconix,密钥flag

我们现根据密钥字母在字母表中的次序写出一下次序2413。即fflag中排第三位,aflag中排第一位…

然后我们根据密钥长度,即4。每行写4个字母,把明文写成一个方块。

w

u

u

c

o

n

i

x

然后我们根据刚刚得到的次序2413。先读第2列,再读第4列…得到密文uncxwoui

知道密钥解密的话也很简单,我们知道密钥flag的情况下,就可以知道一行有4个字母,而密文uncxwoui有八个字母,所以可以推断一列有2个字母,然后再分别写到各个列上,再按行读,就能得到明文啦!

Product Cipher

乘积密码被称为古典密码和现代密码的桥梁。

实际上就是对操作进行进行迭代,比如两次移位,两次代替,两次置换。还可以代替后置换。

img

Block Cipher

分组密码。根据我的理解,分组密码的和我们之前写的古典密码不同点就在于,它的明文、密钥和密文都是用二进制表示的。

In cryptography, a block cipher is a deterministic algorithm operating on fixed-length groups of bits, called blocks.

一般来说n位的明文会被加密问n位的密文。但是密钥的长度不一定是n位,可能不一样 。

分组加密一般内部运用的加密方式,无非就是代替和置换。

代替的目的是为了混淆 Substitution for Confusion

置换的目的是为了扩散 Permutation for Diffusion

在现代分组密码Modern Block Cipher之中,一轮中通常都有一个代替-置换网络Substitution-Permutation Network 可以简写为SPN

SPN的代替元素被称为S盒S-box。置换元素被称为P盒P-box

这个SPN可以说是很多现代分组密码的的基础,比如DES,AES,RC6

DES

Data Encryption Standard。数据加密标准。

它的明文输入是64位二进制,输出同样。密钥输入看似也是64位,但是其8,16,24...位在置换中被丢弃,实际上起作用的密钥只有56位。

下面我简单说一下DES的大体流程。先说明文的处理过程。

64位的明文传入后,首先需要进行一个初始置换 Initial Permutation

置换完之后将结果分为两部分,左32位称为L0L_0L0​,右32位称为R0R_0R0​。

L0L_0L0​划水。开始对R0R_0R0​进行处理。首先32位的R0R_0R0​首先需要进行一次拓展置换 Expansion Permutation,将32位猛增为48位,为什么需要把位数扩大呢?因为每轮的密钥实际上是48位的,这个之后说,将R0R_0R0​拓展为48位之后,就可以和密钥进行操作了。

R0R_0R0​拓展成为48位后,与密钥进行按位与运算。

然后48位的R0R_0R0​将进入8个S-box。将48位重新变为32位。

S-box是怎么实现的位数的减少的呢?我们知道 ,一共有8个S盒,所有的输入一共48位,相当于每个S盒处理6位。

img

我们看这个S盒一共有4行16列,一共64个元素,每行的元素都是0~15

假设我们的S-box 8的输入是101100一共六位,拿出第一位和最后一位10,转为为十进制就是2,于是我们就选择第二行。中间的四位0110是6,于是我们选择第六列,所以就是第二行第六列,即14。再将14转化为二进制1110。所以最后S-box 8的输出就是4位二进制数1110。从原来的输入的六位变成了输出的四位。一共有8个类似的S盒,每个S盒的输出都是4位,总共的输出就是32位啦!

经过S盒的处理之后再经过一个P盒置换一下,仍然是32位。

将这32位和一直在摸鱼的L0L_0L0​按位与一下,就得到了R1R_1R1​。R1R_1R1​有了,L1L_1L1​怎么来呢?左半部分全程摸鱼,L1L_1L1​直接等于R0R_0R0​哈哈。

以下为DES一轮的流程图。

img

连续经过16轮这样一样的操作,最后得到L16L_{16}L16​和和R16R_{16}R16​。把这两个32位二进制位拼接起来 ,但是这里要注意 ,R16R_{16}R16​要拼在前面 ,即R16+L16R_{16}+L_{16}R16​+L16​。我们可以这样理解,这16轮,左半部分全程划水,右半部分奋力拼搏,所以最后把右半部分放在前面以资鼓励2333。

拼接完成得到64位二进制位,这个64位二进制位最后再做一个逆初始置换 Reverse Initial Permutation,这个置换是初始置换的逆。

转置好了就得到 我们的64位密文啦!

以上讲了明文到密文的过程。但是还没有将密钥是如何生成的。刚刚我们讲到和R0R_0R0​做与运算的密钥是48位的,那我们输入的64位密钥是怎么变成每轮的48位密钥 的呢?先把流程图放这里吧。

img

64位密钥输入,首先经过一个叫PC1 Permuted Choice 1的置换。这个置换会把8,16,24..的位数丢失。以下是PC1的具体内容。

57

49

41

33

25

17

9

1

58

50

42

34

26

18

10

2

59

51

43

35

27

19

11

3

60

52

44

36

63

55

47

39

31

23

15

7

62

54

46

38

30

22

14

6

61

53

45

37

29

21

13

5

28

20

12

4

我们可以发现它是7行8列,一共56个元素,并且仔细观察的话,是没有8的倍数的值的,这就导致原始64位密钥的8倍数的下标的值被丢弃,最终成为56位。

得到的56位二进制数,我们还是把它分为左右两个部分,分别叫C0C_0C0​和D0D_0D0​。然后C0C_0C0​和D0D_0D0​需要分别进行循环左移。左移几位呢?我们需要按照这个表来行动。

img

我们可以看到是左移1位。我们将C0C_0C0​和D0D_0D0​分别循环左移一位后,再将两者合并,再进行 一次PC2 Permuted Choice 2的置换。这个置换将56位又减少为48位。以下是PC2的值。

14

17

11

24

1

5

3

28

15

6

21

10

23

19

12

4

26

8

16

7

27

20

13

2

41

52

31

37

47

55

30

40

51

45

33

48

44

49

39

56

34

53

46

42

50

36

29

32

是8行6列的,一共48位元素。我们可以发现原56位二进制中的9,18,22,25,35,38,43,54不在此表中,被丢弃。

经过这个PC2得到的48位将成为第一轮密钥K1K_1K1​,在R0R_0R0​转化为R1R_1R1​过程中发挥作用。

以上就是DES的全部原理了,看似很复杂,但是实际上还是比较简单的,无非就是置换、代替、按位与,然后轮数多了点,人手算那肯定是要命的。现代密码就需要用计算机来实现加密和解密。下面我来分析一下加密和解密的脚本编写的难点。

明文和密钥的初始值都是64位的,我们肯定不能写64个01,那太麻烦了,而且那样也没啥意思,加密算法是让我们用来加密某种有意义的信息的,所以我们这里用8个字符来作为明文,8个字符来作为密文。比如我们的明文就是8个字符的字符串wuuconix。要怎么把它转化为64位二进制位呢?实际上也非常简单,每个字符都ascii码值,我们直接把它的二进制码值转化为二进制位就行了,但是由于ascii码一共只有127个,也就是最多7位二进制,所以前面需要手动补零。有些ascii码小的字母缺的位数可能大于1个,我们需要补零补到8位。

1

"0" + bin(ord('w'))[2:]

img

然后我们还需要解决的一个问题就是置换如何实现。在许多参考资料中,置换表都是一个方块,那是不是需要用一个二维列表来存储呢?

img

其实不用,经过我的实验,直接把置换表写成一行反而更加清楚。

这里我举一个例子,为了观察方便 ,就不用01了。

输入abcd。置换表为2 3 1 4。这个置换表里的值是什么意思呢?其实就是在输入字符串里的字符下标,分别取出第二位b,第一位a,依次类推,我们将取出来的字符进行合并,就得到了bcad。这就是置换结果了,十分方便。所有的置换表都可以这要来考虑,写出来的脚本也非常简单易懂。

permutation_table = [2, 3, 1, 4]
input = "abcd"
output = ""
for i in permutation_table:
	output += input[i - 1]
print(output)

img

置换就是这么简单,就是遍历置换表的值,拿到下标,从输入字符串对应下标出拿到字母加入到输出字符串中。

以上就是我认为的脚本实现DES的难点了,其他的就十分简单了。以下是DES加密和解密脚本。

def get_bin(alpha, mod):
    alpha_bin = ""
    if (mod == "encode"):
        for i in alpha :
            bit = bin(ord(i)).replace("0b", "")
            while (len(bit) < 8): #有些ascii不足7位,在前面补零
                bit = "0" + bit
            alpha_bin += bit
    elif (mod == "decode"):
        alpha_bin = bin(int(alpha, 16))[2:]
        while(len(alpha_bin) < 64):
            alpha_bin = "0" + alpha_bin
    return alpha_bin

def get_key(key_alpha):
    #key_alpha表示密钥的字母形态
    pc1 = [ 57,49,41,33,25,17,9,1,58,50,42,34,26,18,10,2,59,51,43,35,27,19,11,3,60,52,44,36,63,55,47,39,31,23,15,7,62,54,46,38,30,22,14,6,61,53,45,37,9,21,13,5,28,20,12,4 ]
    pc2 = [ 14,17,11,24,1,5,3,28,15,6,21,10,23,19,12,4,26,8,16,7,27,20,13,2,41,52,31,37,47,55,30,40,51,45,33,48,44,49,39,56,34,53,46,42,50,36,29,32 ]
    shift = [ 1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1 ] #每轮左移的位数
    key_alpha_bin = get_bin(key_alpha, "encode") #密钥字母的二进制形态
    key_pc1 = ""
    for i in pc1: #将密钥二进制形态做pc1,得到56位密钥
        key_pc1 += key_alpha_bin[i - 1] #位置1实际上是列表的0
    c, d = key_pc1[0:28], key_pc1[28:] #左半和右半
    keys = [] #存放每轮加密真正要用的48位密钥
    for i in range(0, 16):
        if (shift[i] == 1):
            c = c[1:28] + c[0]
            d = d[1:28] + d[0]
        elif (shift[i] == 2):
            c = c[2:28] + c[0] + c[1]
            d = d[2:28] + d[0] + d[1]
        cd = c + d #cd合并
        key = ""
        for i in pc2:
            key += cd[i - 1]
        keys.append(key)
    return keys

def des(data_alpha, key_alpha, mod):
    ip = [58,50,42,34,26,18,10,2,60,52,44,36,28,20,12,4,62,54,46,38,30,22,14,6,64,56,48,40,32,24,16,8,57,49,41,33,25,17,9,1,59,51,43,35,27,19,11,3,61,53,45,37,29,21,13,5,63,55,47,39,31,23,15,7]
    ep = [32,1,2,3,4,5,4,5,6,7,8,9,8,9,10,11,12,13,12,13,14,15,16,17,16,17,18,19,20,21,20,21,22,23,24,25,24,25,26,27,28,29,28,29,30,31,32,1]
    s = [[14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7,0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8,4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0,15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13], \
        [15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10,3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5,0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15,13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9], \
        [10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8,13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1,13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7,1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12], \
        [7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15,13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9,10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4,3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14], \
        [2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9,14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6,4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14,11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3], \
        [12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11,10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8,9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6,4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13], \
        [4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1,13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6,1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2,6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12], \
        [13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7,1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2,7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8,2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11]]
    p = [16,7,20,21,29,12,28,17,1,15,23,26,5,18,31,10,2,8,24,14,32,27,3,9,19,13,30,6,22,11,4,25]
    ip_reverse = [40,8,48,16,56,24,64,32,39,7,47,15,55,23,63,31,38,6,46,14,54,22,62,30,37,5,45,13,53,21,61,29,36,4,44,12,52,20,60,28,35,3,43,11,51,19,59,27,34,2,42,10,50,18,58,26,33,1,41,9,49,17,57,25]
    data_alpha_bin = get_bin(data_alpha, mod)
    data_ip = ""
    for i in ip:
        data_ip += data_alpha_bin[i - 1]
    l, r = data_ip[0:32], data_ip[32:64] #左半右半
    keys = get_key(key_alpha) #获得16轮的密钥列表
    range_ = range(0,16) if mod == "encode" else range(15, -1, -1)
    for wheel in range_: #wheel代表轮数,取自车轮意
        r_ep = ""
        for i in ep:
            r_ep += r[i - 1]
        key = keys[wheel]
        r_ep_xor = ""
        for i in range(0, 48):
            r_ep_xor += str(int(r_ep[i])^int(key[i]))
        r_s_i = [] #s-box的输入,一共8个,每个为六个字母
        for i in range(0, 8):
            s_i_temp = r_ep_xor[i * 6 : i * 6 + 6]
            r_s_i.append(s_i_temp)
        r_s_o = ""
        for i in range(0, 8):
            si = r_s_i[i]
            row = int(si[0]) * 2 + int(si[5])
            column = int(si[1]) * 8 + int(si[2]) * 4 + int(si[3]) * 2 + int(si[4])
            index = row * 16 + column
            so_int = s[i][index]
            so_bin = bin(so_int).replace('0b', '') #转化为二进制
            while len(so_bin) < 4: #补零
                so_bin = "0" + so_bin
            r_s_o += so_bin
        r_p = ""
        for i in p:
            r_p += r_s_o[i - 1]
        r_next = ""
        for i in range(0, 32):
            r_next += str(int(l[i])^int(r_p[i]))
        l = r #L1 = R0
        r = r_next #R1 = F(L0, R0, Key1)
    data_preout = r + l #最后一步逆转置置换时r在前面
    data_code_bin = "" #密文/明文2进制形态,这里的code表示包含了encode和decode
    for i in ip_reverse:
        data_code_bin += data_preout[i - 1]
    data_code_hex = hex(int(data_code_bin, 2))[2:]
    while (len(data_code_hex) < 16): #密文/明文前面全部为零的情况下直接转化为16进制可能少位数,需要补零
        data_code_hex = "0" + data_code_hex
    if (mod == "decode"):
        data_code_alpha = ""
        for i in range(0, 8):
            bytes_ascii = chr(int(data_code_hex[i * 2 : i * 2 + 2], 16))
            data_code_alpha += bytes_ascii
        return data_code_alpha
    else:
        return data_code_hex

result = des("flagyyds", "wuuconix", "encode")
print(f"des加密结果:{result}")
result = des("46edc4a79ab0f007", "wuuconix", "decode")
print(f"des解密结果:{result}")

img

Block Cipher——Mode of Operation

由于分组密码加密的对象都是二进制位,而我们输入的明文一般都是字母,比如DES要求明文位64位,也就是8个字母,但是我们实际上要加密的数据肯定不止64位,实际工作中就需要对数据进行处理。分组密码主要有5种工作模式。

  1. ECB Electronic Codebook Book 电码本模式。这个思想十分简单 ,就是把明文和密文分组,每组就是那个我们需要的位数。 因为每组独立,无错误传播。还可以并行处理。 但是它也有缺点,就是这种方式当某两组的明文同样的话,由于它们的密钥是相同的56位密钥,这就导致它们加密出来的结果也是一样的。 以下是用ECB方式加密的图片,表现出了明显的轮廓特征。所以一般ECB只会用来加密一些长度很小的内容,比如加密密钥 。

img

img

  1. CBC Cipher Block Chaining 密文链接模式。CBC的特点就是它的每一组的密文都会影响到下一组的加密,因为它和下一组的明文做了按位与的操作。同时由于除了第一组外每一组都会接受前面一组的密文,为了保证统一性,第一组明文会给它一个初始向量IV

img

我们可以很容易知道,CBC会有错误传播,也就是说前一轮密文的错误会影响到后一轮的密文。同时因为一环扣一环,无法做到并行处理。 有错误传播不是说这个工作模式就不好,因为我们有时候就需要让它错误传播,比如一份重要的文件加密,我们要防止别人篡改其中的某一位。而错误传播就可以让这个微小的修改被无限放大而让接收者知道这份文件发生了错误 。 由于不同组的明文会收到之前密文的影响,这就导致相同的明文会加密出不同的密文。这就避免了暴露明文的数据模式。

img

img

还有三种工作模式分别是CFB Cipher Feedback 密文反馈模式, OFB Output Feedback 输出反馈模式, CTRCounter Mode 计数器模式。 它们的方式都非常奇怪,明文本身没有参与DES加密,进行加密的是初始向量,然后用初始向量的加密结果和8位明文进行按位与。对,因为明文本身不参与DES加密,所以它每组没有把明文分成64位每组,而可以分成更加遍历的位数。甚至可以每组1位,十分牛逼。

  1. CFB Cipher Feedback 密文反馈模式。明文是和某个向量经过DES的结果的左边j位进行按位与。同时把密文移动到下个向量的右边j位。 由于每次对明文产生影响的只有j位,可能这样就导致了错误传播得不会太远。这是它得一个特点。错误传播有界。

img

  1. OFB Output Feedback 输出反馈模式。和CFB类似,但是OFB中影响下一个向量的j位不是上一组的密文,而是直接由上一组向量的加密结果的左j位。所以这就导致密文的错误不会传播。 还是和之前说的一样,无错误传播不一定好,我们无法确定密文是否被篡改。所以这使用于冗余度较大的图片视频等错了几位不怕的数据。OFB经常被用来加密卫星的传输。

img

  1. CTR Counter Mode 计数器模式相比前两个CFB和OFB就简单多了,也没有初始向量,上一组的输出或者密文也不会影响到下一组。 这让它可以并行处理。还可以随机区块解密,不用必须从后往前全部解一遍。

img

AES

Advanced Encryption Standard。高级加密标准。其设计出来便是为了代替因为密钥位数太少而已经不安全的DES

AES提供了3种不同的规格,按照实际需求进行选择,十分灵活,具体参数如下。

密钥长度

128

192

256

分组长度

128

128

128

轮数

10

12

14

轮密钥长度

128

128

128

我们可以看见,就算是最低规格的AES,都远比DES的位数长的多。

我们再来看AES的加密流程。

img

我们先来看其中最重要的S盒。AES的S盒盒DES的S盒不一样,AES的S盒是可以算出来的,而DES只是给你一堆数,不知道这些数是怎么的出来的。

AES S盒替换又叫做字节代替,其操作就是把明文的的每个字节进行整体代替,相当于把一个字节换成另一个完全不一样的字节。而DES中做法是输入48位二进制,因为 有8个S盒,故分为8组,每组6位二进制,第一位和最后一位组成行,中间4位组成列,在S表中进行定位元素,表中的元素都是4位的,整体S盒是48位输入,32位输出。而AES的S盒实际上比DES的S盒简单的多,因为AES只有一个S盒。这个S盒为16行,16列的正方形。

它的行对应AES输入字节的前八位,它的列对应AES输入字节的后八位,S表的每个元素都是一个字节,所以就实现了输入一个字节,然后替换为另一个字节。

img

如果要利用查表得话,程序实现起来也是非常得简单。

但是我们之前也说过,这个S表实际上是算出来得。也就是我们可以根据一个字节,然后手动计算出输出,不用查表。

img

实际上是把输入得字节转化为一个多项式,然后求这个多项式得逆,再把这个逆转化为二进制,然后乘上一一个矩阵,再加上一个矩阵。

听起来挺简单得,但是以我目前的知识,我无法利用python实现多项式的表示,更别说求多项式的逆了。numpy貌似可以解决这个问题,之后再研究吧。

这里就用一个例子手动演示一下计算过程吧!

假设我么输入的字节为0x53,即01010011

  1. 其对应的多项式为x6+x4+x1+1x^6+x^4+x^1+1x6+x4+x1+1。
  2. 然后需要求该多项式在模x8+x4+x3+x+1x^8+x^4+x^3+x+1x8+x4+x3+x+1意义下的逆,这个8次多项式是伽罗华域里面指定的,说实话我不太懂2333。
  3. 和整数的求逆的过程类似,先写出它的辗转相除的过程。(利用多项式除法 注意 在模意义下 加和减等效) x8+x4+x3+x+1x^8+x^4+x^3+x+1x8+x4+x3+x+1 = (x2+1)∗(x6+x4+x1+1)+x2(x^2+1)*(x^6+x^4+x^1+1)+x^2(x2+1)∗(x6+x4+x1+1)+x2 x6+x4+x1+1=(x4+x2)∗x2+(x+1)x^6+x^4+x^1+1 = (x^4+x^2)*x^2+(x+1)x6+x4+x1+1=(x4+x2)∗x2+(x+1) x2=(x+1)∗(x+1)+1x^2 = (x+1)*(x+1)+1x2=(x+1)∗(x+1)+1 然后开始手动推逆。 1=x2+(x+1)∗(x+1)1 = x^2 + (x+1)*(x+1)1=x2+(x+1)∗(x+1) //减直接等效为加来简化运算 1=x2+(x+1)∗((x6+x4+x+1)+x2∗(x4+x2))1 = x^2 + (x+1) * ((x^6 + x^4+x+1)+x^2*(x^4+x^2))1=x2+(x+1)∗((x6+x4+x+1)+x2∗(x4+x2)) 1=(x+1)(x6+x4+x+1)+(x5+x4+x3+x2+1)x21 = (x+1)(x^6+x^4+x+1)+(x^5+x^4+x^3+x^2+1)x^21=(x+1)(x6+x4+x+1)+(x5+x4+x3+x2+1)x2 1=(x+1)(x6+x4+x+1)+(x5+x4+x3+x2+1)((x8+x4+x3+x+1)+(x2+1)∗(x6+x4+x1+1))1 = (x+1)(x^6+x^4+x+1) + (x^5+x^4+x^3+x^2+1)((x^8+x^4+x^3+x+1)+(x^2+1)*(x^6+x^4+x^1+1))1=(x+1)(x6+x4+x+1)+(x5+x4+x3+x2+1)((x8+x4+x3+x+1)+(x2+1)∗(x6+x4+x1+1)) 1=(x5+x4+x3+x2+1)(x8+x4+x3+x+1)+(x7+x6+x3+1)(x6+x4+x+1)1 = (x^5+x^4+x^3+x^2+1)(x^8+x^4+x^3+x+1)+(x^7+x^6+x^3+1)(x^6+x^4+x+1)1=(x5+x4+x3+x2+1)(x8+x4+x3+x+1)+(x7+x6+x3+1)(x6+x4+x+1) 则我们可以得到多项式x6+x4+x1+1x^6+x^4+x^1+1x6+x4+x1+1的逆为(x7+x6+x3+1)(x^7+x^6+x^3+1)(x7+x6+x3+1)。 同时我们因为一开始的两个式子很长,导致最后最后推导的时候写那两个式子很麻烦,我们可以一开始设置p(x)=x8+x4+x3+x+1p(x)=x^8+x^4+x^3+x+1p(x)=x8+x4+x3+x+1,设置q(x)=x6+x4+x+1q(x)=x^6+x^4+x+1q(x)=x6+x4+x+1。我们再试着重新写一下过程。 辗转相除 p(x)=(x2+1)q(x)+x2p(x)=(x^2+1)q(x)+x^2p(x)=(x2+1)q(x)+x2 q(x)=(x4+x2)x2+(x+1)q(x)=(x^4+x^2)x^2+(x+1)q(x)=(x4+x2)x2+(x+1) x2=(x+1)(x+1)+1x^2=(x+1)(x+1)+1x2=(x+1)(x+1)+1 求逆 1=x2+(x+1)(x+1)1 = x^2 + (x+1)(x+1)1=x2+(x+1)(x+1) 1=x2+(x+1)q(x)+x2(x4+x2))1 = x^2 + (x+1)q(x)+x^2(x^4+x^2))1=x2+(x+1)q(x)+x2(x4+x2)) 1=(x+1)q(x)+(x5+x4+x3+x2+1)x21 = (x+1)q(x)+(x^5+x^4+x^3+x^2+1)x^21=(x+1)q(x)+(x5+x4+x3+x2+1)x2 1=(x+1)q(x)+(x5+x4+x3+x2+1)(p(x)+(x2+1)q(x)1 = (x+1)q(x) + (x^5+x^4+x^3+x^2+1)(p(x)+(x^2+1)q(x)1=(x+1)q(x)+(x5+x4+x3+x2+1)(p(x)+(x2+1)q(x) 1=(x5+x4+x3+x2+1)p(x)+(x7+x6+x3+x)q(x)1 = (x^5+x^4+x^3+x^2+1)p(x)+(x^7+x^6+x^3+x)q(x)1=(x5+x4+x3+x2+1)p(x)+(x7+x6+x3+x)q(x) 显得简单了许多。 所以手动求逆有两个技巧 1. 减转化为加 2.设p(x)和q(x)。
  4. 现在我们得到了多项式的逆x7+x6+x3+1x^7+x^6+x^3+1x7+x6+x3+1。再这个逆转化为二进制11001010
  5. 让这个二进制乘上一个Z矩阵,再加上一个C矩阵。注意将那个二进制的矩阵低位在上,高位在下。

img

最后得到的矩阵为 (10110111)\begin{pmatrix} 1 \\ 0 \\ 1 \\ 1 \\ 0 \\ 1 \\ 1 \\ 1 \\ \end{pmatrix} ⎝⎜⎜⎜⎜⎜⎜⎜⎜⎜⎜⎛​10110111​⎠⎟⎟⎟⎟⎟⎟⎟⎟⎟⎟⎞​ 它代表的二进制是10110111嘛?不!刚才说过,低位在上,高位在下。实际的结果应该从下往上读11101101 0xed。 这就是一开始输入字节01010011 0x53的置换结果了! 如果要用程序实现这个过程,需要解决多项式求逆的难点,矩阵相乘倒是好求。 ok,现在我们解决了AES中的字节代替。 接下来我们看一看行移位。

img

手动实现和程序实现的难度都很低,第0行不变,第1行循环右移1,第二行循环右移2,第三行循环右移3。 然后是列混合。

img

列混合实际是把一列转化为一个多项式,下面为高位,下面为低位。 即C3,0x3+C2,0x2+C1,0x+C0,0C_{3, 0}x^3+C_{2, 0}x^2+C_{1, 0}x+C_{0,0}C3,0​x3+C2,0​x2+C1,0​x+C0,0​ 值得注意的是每个C实际上是一个字节,它也可以等价转化为一个多项式。 所以上面这个多项式的实际次数可能不止3。 然后我们要把这个列对应的多项式乘上一个C(x)=′03′x3+′01′x2+′01′x1+′02′C(x)='03'x^3+'01'x^2+'01'x^1+'02'C(x)=′03′x3+′01′x2+′01′x1+′02′ 同样的,这个C(x)和列多项式一样,实际的次数不止3次,因为′03′'03'′03′对应了多项式x2x^2x2。所以C(x)C(x)C(x)这个多项式的最高次为5。 然后我要要做的是把列对应的多项式乘上这个C(x)C(x)C(x),同时模上x4+1x^4+1x4+1。最后的结果如上图。 然后我们把最后结果的每个次数的x前面的系数对应到新的列上。就完成了列混合。 实际上我们计算列混合的结果时不用考虑的这么多,只需要利用最后的d和c之间的结果即可,实际上它们之间的关系就是乘上了一个矩阵。

img

即d0,0=′02′C0,0+′03′C1,0+′01′C2,0+′01′C3,0d_{0,0}='02'C_{0,0}+'03'C_{1,0}+'01'C_{2,0}+'01'C_{3,0}d0,0​=′02′C0,0​+′03′C1,0​+′01′C2,0​+′01′C3,0​ 和之前的结果时一致的。 值得注意的是,我们一开始的方法多项式表面上最高此次为3,因为没有将C实际转化为多项式,所以模的是x4+1x^4+1x4+1。 而现在我们根据结论的时候,是需要将C展开为多项式的,故不能再模x4+1x^4+1x4+1,需要模那个神奇的多项式x8+x4+x3+x+1x^8+x^4+x^3+x+1x8+x4+x3+x+1。 以下为例子。

img

ok,最后一个步骤就是和轮密钥按位与,十分简单。

img

那轮密钥是怎么产生的呢?因为AES一共有十轮,所以我们需要根据初始128位的密码进行生成之后的密钥。

img

我们先把128位的密钥,即16个字节的密钥的形式换一些,把第一列写成wow_owo​。 实际上 w0=(k0k1k2k3)w_0= \begin{pmatrix} k_{0} \\ k_{1} \\ k_{2} \\ k_{3} \\ \end{pmatrix} w0​=⎝⎜⎜⎛​k0​k1​k2​k3​​⎠⎟⎟⎞​ 然后我们就可以得到w0,w1,w2,w3w_0,w_1,w_2,w_3w0​,w1​,w2​,w3​。这实际上就是初始密钥,没有任何变化,我们只是换种形式表示。 然后我们需要以此位基础生成第一轮的轮密钥w4,w5,w6,w7w_4,w_5,w_6,w_7w4​,w5​,w6​,w7​。 公式如下 {w(i)=w(i−1)⊕w(i−4)i mod 4≠0w(i)=g(w(i−1))⊕w(i−4)i mod 4=0\begin{cases} w(i)=w(i-1)\oplus w(i-4)& i\ mod\ 4\neq 0\\ w(i)=g(w(i-1))\oplus w(i-4)& i\ mod\ 4 = 0 \end{cases} {w(i)=w(i−1)⊕w(i−4)w(i)=g(w(i−1))⊕w(i−4)​i mod 4​=0i mod 4=0​ 如果w不是4的倍数,那很简单,只需要将w(i-1)和w(i-4)按位与即可。 如果w是4的倍数,那么复杂一些,需要先把w(i-1)做一点处理,然后再和w(i-4)按位与。 处理如下。

img

课件里把w(i-1)写成横的了,实际上写成竖的更好,整体往上移位。然后将移位后的结果进行字节代替。 然后将字节代替后的结果的第一个字节加上一个数r。这个r和i相关。r(i)=2(i−1)/4r(i)=2^{(i-1)/4}r(i)=2(i−1)/4 如此操作,我们就能得到10轮轮密钥。

img

值得注意的是,我们实际用的时候应该是还是得把w转化为方块,然后一行一行来进行按位与。当然把处理后得明文直接按列来直接与也行233。只要是两个16字节的方块按位与即可。 至此,我们已经厘清了AES的操作。接下来我们分析一下其安全性。 由于密钥长度的大小,使得暴力破解几乎不可能。

img

同时由于行移位和列混合,导致AES一字节的更改,在10轮的迭代后都产生了蝴蝶效应,防止了伪造明文。

img

其他分组密码

  1. IDEA
  2. RC6
  3. Twofish
  4. Serpent

脑容量不够了,这几个就不分析了2333

差分分析

以后再说

One-Time Pad

维吉尼亚密码做为古典密码中最出色的密码,由于密钥是重复使用的,使得重合指数法利用频率进行分析成为可能。那么如果密钥和明文一样长,并且完全随机呢?

这种方式加密起来也十分简答, 只要将明文和密钥按位与即可。

C=M⊕KC=M \oplus KC=M⊕K

同时可以证明,OTP是绝对安全的。

流密码概述

用伪随机来代替随机

Pseudorandom Number Generator(PRNG) Pseudorandom: 伪随机数

种子是秘密密钥。

和OTP类似,只不过把K换掉了。

C=M⊕PRNG(seed)C=M \oplus PRNG(seed)C=M⊕PRNG(seed)

分组密码和流密码的方式的差别:

  1. 因为分组密码的明文都有位数要求,比如DES的明文要求为64位。而实际要加密的数据肯定不止64位,所以要把数据分成一组一组的,每组都是64位,去加密,这也是分组密码名称的由来。 c=(c1c2..)(cici+1..=ek(m1m2..)ek(cici+1))c=(c_1c_2..)(c_ic_{i+1}..=e_k(m_1m_2..)e_k(c_ic_{i+1}))c=(c1​c2​..)(ci​ci+1​..=ek​(m1​m2​..)ek​(ci​ci+1​))
  2. 而流密码实际上做的就是和伪随机密钥流进行按位与,所以它不用分组,来多少按位与多少即可。 c=c1c2..=ek1(m1)ek2(m2)..c=c_1c_2..=e_{k1}(m_1)e_{k2}(m_2)..c=c1​c2​..=ek1​(m1​)ek2​(m2​).. 密钥流(k1,k2,..)(k_1,k_2,..)(k1​,k2​,..)

流密码性质

  1. 数学性质好,因为它模仿OTP,而OTP是绝对保密的,并且伪随机也有很长的研究历史
  2. 加密速度非常快,因为只有按位与
  3. 密钥流只能使用一次。
  4. 应用很广 比如网路、移动通信等等

伪随机序列发生器 Pseudorandom Number Generation

由于流密码实际的加密过程只有按位与,几乎不用怎么讲。流密码最难得部分就是伪随机密钥流的生成。

密码学中对伪随机数的要求:

  1. 随机性 均匀分布:数列中每个数出现 的频率相等或者近似相等
  2. 不可预测性 密码分析者很难从数列前面的数预测后面的数

伪随机数的应用

  1. 相互认证 使用一次性随机数来防止重放攻击
  2. 会话密钥的产生 用随机数做为会话密钥
  3. 公钥密码算法中密钥的产生 用随机数做为公钥密码算法中的密钥

PRNG定义:

一个伪随机学列发生器G是一个确定的多项式算法,使得

  1. 拓展性:
  2. 伪随机性

Golomb随机性测试

  1. 在序列的一个周期内,0和1的个数相差至多为1。
  2. 在序列的一个 周期内,长为1的游程占总游程数的1/2,长为2的游程占总数的1/221/2^21/22,…长为i的游程占总数的1/2i1/2^i1/2i,…,且在等长的游程中,0和1游程的个数至多差1.
  3. 自相关函数为双值(话说双值是什么意思233

img

img

周期为31

img

1的个数为16,0的个数为15,符合Golomb测试的第一个条件

img

Golomb测试的第二个条件中的游程是什么意思呢?实际上就是连续的1或者连续0的情况,比如000就是0的3-游程

img

这个字符串总共的游程个数是16个。0和1的1-游程都是4个,总共8个,占16个的一半,正好1/22-游程4个,1/43-游程两个,1/84-游程1个,1/165-游程1个,1/16

同时在等长的游程中,0和1的游程数量几乎一致,最多差1,符合Golomb测试。

LSFR (Linear Feedback Shift Register)

是一种PRNG的实例,大概可以翻译为线性反馈移位寄存器233

img

这张图中,zoz_ozo​寄存器做为输出端,会把一位传到外面,相当于产生了一个随机数。

然后就是移位,本来z1z_1z1​寄存器的值会传到z0z_0z0​上,z2z_2z2​的值会传到z1z_1z1​上。而z2z2z2的值是由z0z_0z0​和z1z_1z1​的值相与得到的,这就是LSFR中的feedback

一开始这三个寄存器中的初始值叫做seed种子。

这个实例比较简单,如果寄存器再多一些就比较复杂了,因为每个寄存器都可能参与反馈,也可能不参与,我们需要一个简单的方法来让简化。

这里引入反馈系数。

img

每一个寄存器都对应一个反馈系数,来代表它有没有参与反馈。

这里的反馈实际都是反馈到zn−1z_{n-1}zn−1​这个寄存器的下一个状态,也就是knk_nkn​了。因为zn−2z_{n-2}zn−2​到z0z_0z0​的寄存器都是前一个寄存器移位下来的。

我们可以写出这样的反馈式子

img

然后我们呢可以把这个式子转化为一个对应的特征多项式

p(x)=1+cj−1x+cj−2x2+..+cj−nxnp(x)=1+c_{j-1}x+c_{j-2}x^2+..+c_{j-n}x^np(x)=1+cj−1​x+cj−2​x2+..+cj−n​xn

然后我们来思考一下LSFR的周期。

首先,我们很容器想到一个具体的LSFR是有周期上线的,因为n个寄存器,最多有2n2^n2n中可能性,而其中全零是不可能的,不然就一直全零,肯定不是随机序列了嘛233,所以周期最大为2n−12^n-12n−1

img

这个LSFR实例就达到了最大周期23−12^3-123−1。值得注意的是,因为LSFR只是一个伪随机数发生器,它实际上产生的随机数序列为0010111 0010111 00101110010111\ 0010111\ 00101110010111 0010111 0010111。周期为7。

m-序列:由LSFR产生的最大长度序列是m-序列,结合上面的结论,m序列的周期是2n−12^n-12n−1。并且它满足Golomb随机性检测。在一个周期内,0比1的个数少1个。

定理:

{ai}\{a_i\}{ai​}是m-序列当且仅当它的特征多项式p(x)p(x)p(x)是本原多项式

定义:如果p(x)的次数为n,且其周期为2n−12^n-12n−1则称p(x)为本原多项式。

定义:设p(x)为GF(2)上的多项式,使得式子p(x)∣xp−1p(x)|x^p-1p(x)∣xp−1成立的最小正整数p为p(x)p(x)p(x)的周期

这里实际上对周期这个概念做了拓展,让一个多项式也有了周期。

非线性序列

LSFR做到操作是抑或,属于线性操作,如果我们把这种操作改编为非线性的,实际上就可以产生出非线性序列。

img

它有一下特征:

  1. 输出序列的周期为2n2^n2n *?
  2. 最大长度序列是M−序列M-序列M−序列
  3. M-序列有很好的随机性质
  4. M-序列的个数是22n−1−n2^{2^{n-1}-n}22n−1−n
  5. 非线性函数是22n−2n2^{2^n}-2^n22n−2n

对非线性序列的研究还在初级阶段

实际应用中的非线性序列(用m-序列做为驱动序列)

  1. 非线性混合序列
  2. 钟控序列

非线性混合序列:

它实际上就是把n个LSFR的输出进行非线性混合。

img

钟控序列:

引入了时钟,看不懂了233

Blum Blum Shub发生器

BBS发生器基于数论中的二次剩余问题。

挺简单的,开局选两个大素数p和q。得到n=pq

然后选一个和n互素的随机数x。

然后得到bbs初始输入x0=x2(mod n)x_0=x^2(mod\ n)x0​=x2(mod n)

然后我们就根据这些x的二进制的最低位来生成01伪随机序列。注意x0x_0x0​的二进制最低位没有加入到输出中。

img

这里输出99位测试一下。

n=9788476140853110794168855217413715781961
x=873245647888478349013
prn = ""
x = x**2 % n  #x0的二进制最后一位不算入随机数序列中
for i in range(1, 100):
        x = x**2 % n
        prn += bin(x)[-1]
print(prn)

img

img

只差1,符合了Golomn测试的第一条。

游程测试也大致符合。

img

所以BBS产生的随机数还是挺不错的。就是计算量比LSFR大多了2333。

流密码设计目标

  1. 长周期
  2. 高线性复杂度
  3. 统计性能好
  4. 足够的混乱

A5

img

实际上就是对3个LSFR进行非线性综合后输出,同时这三个LSRF是钟控的。

RC4

RC4十分简单与高效。它又有点像分组加密,因为它的密钥一次性生成一个字节,也就是8位。而不像普通的PRNG,每次吐出一位来。

以下位RC4的代码

s = []
k = [22, 33, 233]
for i in range(0, 256):
        s.append(i)
j = 0
for i in range(0, 256):
    j = (j + s[i] + k [i % 3]) % 256
    s[i], s[j] = s[j], s[i]
print(s)
print("----------------------------")
Ks = []

i, j = 0, 0
for num in range(0, 100):
    i = (i + 1) % 256;
    j = (j + s[i]) % 256;
    s[i], s[j] = s[j], s[i]
    t = (s[i] + s[j]) % 256
    Ks.append(s[t])
print(Ks)

首先把0~255进行打乱,打乱的过程中运用到了我们输入的Secret key。

然后进行简单又神秘的操作就可以不断地生成8位密钥。以下运行结果,上部分是打乱后地数组,下面是生成的随机数。

img

流密码到这里就没了,你可能会疑惑流密码怎么只讲了加密,如何解密呢?

233,实际上因为流密码加密的时候只是与密钥流向亦或了,只要再亦或一个,就回到明文啦!

所以流密码是如此的简单与高效。我觉得我也能设计一个不注重安全的流密码,正常人应该也不会去差分分析之类的把233,所以应该是十分安全的。

非对称密码

之前我们学得密码都属于对称密码。发信者和收信者用得是同一个密钥。

1976年Diffie & Hellman开启了密码学得新方向——非对称密码。这被称为近千年来密码学上最有意义得成就。

这里的非对称指的是密钥的非对称,即加密是用的某个密钥,解密是用的另一个密钥。

RSA

RSA加密过程

选择两个大素数pppqqq。得到n=pqn=pqn=pq。然后计算nnn的欧拉函数φ(n)\varphi(n)φ(n),然后选择一个和φ(n)\varphi(n)φ(n)互素的eee。

这个eee(意为encode)会用在加密中。然后求d=e−1 mod φ(n)d=e^{-1}\ mod\ \varphi(n)d=e−1 mod φ(n)。这个ddd(意为decode)会用在解密中。

以下为加密和解密的过程

c=Em(m)=me(mod m)c=E_m(m)=m^e(mod\ m )c=Em​(m)=me(mod m)

m=Dd(c)=cd(mod n)m=D_d(c)=c^d(mod\ n)m=Dd​(c)=cd(mod n)

加密者会把(n,e)(n,e)(n,e)放出来做为公钥,大家都可以用来加密。但是由于素数分解的难题,别人无法知道ddd,也就无法解密。

私钥就是(p,q,d)(p,q,d)(p,q,d)。其实我觉得写成(n,d)(n, d)(n,d)就行?直接根据ddd就能解密,不用实际知道p,qp,qp,q。

RSA例子1

  1. p选择7,q选择11,则n为77。φ(n)=60\varphi(n)=60φ(n)=60。
  2. 选择一个和φ(n)\varphi(n)φ(n)互素的值,比如37。这个37就是eee了,用来加密。
  3. 再计算一下ddd。d是e关于φ(n)\varphi(n)φ(n)的逆。由libnum库计算d为13

img

  1. 如果密文为2的话。c=me mod n=237 mod 77=51c=m^e\ mod\ n=2^{37}\ mod\ 77=51c=me mod n=237 mod 77=51

img

  1. 解密很简单。m=cd mod n=5113 mod n=2m=c^d\ mod\ n=51^{13}\ mod\ n=2m=cd mod n=5113 mod n=2。 可以看见解密出来的就是原文。

RSA例子2

例子1的数比较小。我们试一下这个大的。

img

这里试着用python实现一波。

import libnum
p, q = 885320963, 238855417
n = p * q #211463707796206571
phi = (p - 1) * (q - 1) #211463706672030192
e = 9007 #encode
d = libnum.invmod(e, phi) #decode 116402471153538991
m = 30120
c = m ** e % n #11353585903572286
print(f"encode: {c}")
result = c ** d % n
print(f"decode: {result}")

能够正常的加密,但是解密就卡住了。

img

我们分析一波原因,加密的时候的式子为301209007 mod 21146370779620657130120^{9007}\ mod\ 211463707796206571301209007 mod 211463707796206571。实际上指数已经算很大了,但是由于python强大的计算能力,结果还是秒传。

我们再来看一看解密时候的式子 11353585903572286116402471153538991 mod 21146370779620657111353585903572286^{116402471153538991}\ mod\ 21146370779620657111353585903572286116402471153538991 mod 211463707796206571

底数这么大,指数也这么大,天王老子来了也算不出来,这也就是python为什么卡住了。

我们需要一种快速指数算法Efficient Exponentiation,这里便引出了平方乘算法Square-and-Multiply Algorithm

Sqaure-and-Multiply Algorithm

平方乘算法的思想实际很简单,首先我们 需要将指数转化为二进制。然后的操作类似于将二进制数转化为十进制数的过程。

比如51的二进制数110011。如何将二进制转化回十进制呢?

  1. 1∗20+1∗21+1∗24+1∗251*2^0+1*2^1+1*2^4+1*2^51∗20+1∗21+1∗24+1∗25
  2. (((1∗2)+1)∗2∗2∗2+1)∗2+1(((1*2)+1)*2*2*2+1)*2+1(((1∗2)+1)∗2∗2∗2+1)∗2+1 第二种方法实际上的从最左端出发 111 101010->111111 110110110 110011001100 110001100011000->110011100111001 110010110010110010->110011110011110011 每次的乘二相当于整体左移,如果对应的二进制为1,则加上1。如果对应二进制为0,那就是不加1,只乘二右移。 一系列操作后从最高位的111得到了110011110011110011,也就完成了从1计算得到51的过程。

平方乘算法就利用了上面的第二种方法,只不过算法中的二进制因为是在指数上,所以乘2就是平方,加一就是乘一个底数。

算法如下

1234567891011

bottom = 113535859035722866exp = 116402471153538991modu = 211463707796206571bin_ = bin(exp)[2:]z = 1for i in bin_: z = z ** 2 % modu if i == "1": z = z * bottom % moduprint(z)

img

结果秒出,我们很快得到了例子2中没有得到的结果。

能否用上面说的将二进制转化为十进制方法中的第一种方法来实现平方乘算法呢?我试了一下,很难。但是有一个奇怪的现象,刘杨教我们用快速指数算法手算的时候,用的都是第一种方法,ppt如下。

img

ppt中的实际和平方乘算法的过程完全不一样,平方乘算法不用刻意的去就求出7925,7924...79^{2^5},79^{2^4}...7925,7924...的值,而在一次次平方与乘中自然而然得到了最终的答案。

我们用真正的平方乘算法来写一下过程。

  • 51=(110011)251=(110011)_251=(110011)2​
  • 初始值为1
  • 12 mod 90=11^2\ mod\ 90=112 mod 90=1 1∗79 mod 90=791*79\ mod\ 90=791∗79 mod 90=79
  • 792 mod 90=3179^2\ mod\ 90=31792 mod 90=31 31∗79 mod 90=1931*79\ mod\ 90=1931∗79 mod 90=19
  • 192 mod 90=119^2\ mod\ 90=1192 mod 90=1
  • 12 mod 90=11^2 \ mod\ 90=112 mod 90=1
  • 12 mod 90=11^2\ mod\ 90=112 mod 90=1 1∗79 mod 90=791*79\ mod\ 90=791∗79 mod 90=79
  • 792 mod 90=3179^2\ mod\ 90=31792 mod 90=31 31∗79 mod 90=1931*79\ mod\ 90=1931∗79 mod 90=19

可以看到手动算的时候,我们也可以直接利用算法原本的过程,计算量比ppt更小。

但是也许ppt上的方法更容易让人理解,但是程序是较难实现的。

Miller-Rabin Algorithm (Fermat Test)

费马测试的原理是费马定理。

费马定理:对于素数p,取与p互素的a(0<a<p)。有ap−1=1 mod pa^{p-1}=1\ mod\ pap−1=1 mod p。 费马定理实际上就是欧拉公式当n为素数时的的特例。 欧拉公式:若a与n互素,则aφ(n)=1 mod na^{\varphi(n)}=1\ mod\ naφ(n)=1 mod n

因为素数满足费马定理,所以费马测试就去测试一个数n,若an−1=1 mod na^{n-1}=1\ mod\ nan−1=1 mod n,则n被判定为一个素数。

我们很容易就容易发现,这是一个偏是的测试,因为有一些合数也会满足费马测试,它们被称为伪素数。

然后式子的测试过程中,为了简便操作,将n−1n-1n−1分解为2km2^km2km(m为奇数)。第一眼看可能感觉这个n-1不一定能分解为2km2^km2km,但是由于k应该可以取0,所以是一定能够分解的。

费马测试中就去计算b=amb=a^mb=am是不是1,如果是1,ok满足条件(这里b为-1貌似也可以,但是不知道为什么ppt里没写)。不满足则看b2,b4,..b^2,b^4,..b2,b4,..是不是正负1,如果遇到了一个正负1,ok,满足条件。如果到最后b2kb^{2^k}b2k都没有得到正负1,说明它是个合数。

这里举一个例子。

  1. 取n为97,则n-1为96。96可以分解为25∗32^5*325∗3,即32∗332*332∗3
  2. 随便取一个a,比如说2,则看b=am=23 mod 97b=a^m=2^{3}\ mod\ 97b=am=23 mod 97是不是1。

img

b=8b=8b=8,不是1,则继续。

  1. b2 mod 97=64b^2\ mod\ 97=64b2 mod 97=64。继续
  2. b4 mod 97=22b^4\ mod\ 97=22b4 mod 97=22。继续
  3. b8 mod 97=96=−1b^8\ mod\ 97=96=-1b8 mod 97=96=−1。发现-1,测试完毕,输出“素数”。 这里再详细解释一下为什么遇到-1也能说素数,因为an−1=a2km=b2ka^{n-1}=a^{2^km}=b^{2^k}an−1=a2km=b2k。当遇到b2xb^{2^x}b2x等于正负1时,最后的结果式子b2kb^{2^k}b2k 实际就是又把-1平方了即便,最后的结果是1,满足费马定理,就判断该数为素数了。

一个合数有四分之一的概率通过费马测试,我们可以通过取多个a的方式来降低误判的概率。但是有一些数,不管你取什么a,都能被判定为素数,这些数被成为Carmichael数,比如561,645,1105。

我们这里对561进行一轮费马测试来模拟一下,a还是取2。

  • n=561n=561n=561
  • n−1=560n-1=560n−1=560
  • 560=24∗35560=2^4*35560=24∗35
  • b=am=235 mod 561=263b=a^m=2^{35}\ mod\ 561=263b=am=235 mod 561=263
  • b2 mod 561=166b^2\ mod\ 561=166b2 mod 561=166
  • b4 mod 561=67b^4\ mod\ 561=67b4 mod 561=67
  • b8 mod 561=1b^8\ mod\ 561=1b8 mod 561=1 返回“素数”

分解大素数

其实RSA知道加密解密过程。然后了解平方乘算法来使指数运算称为现实,再利用费马测试来选择一个素数后,我们已经了解完RSA了。

接下来讲的分解大素数,该知识点应用在破解RSA密码中,也就是我们不知道d,p,q,我们只知道e,ne, ne,n。

在这种情况下我们就需要对n进行分解。分解出p,q后,φ(n)=(p−1)(q−1)\varphi(n)=(p-1)(q-1)φ(n)=(p−1)(q−1)也就知道了,d=e−1 mod nd=e^{-1}\ mod\ nd=e−1 mod n也就知道了。就可以解密了。但是分解素数不是这么简单的。

此内容之后填充。

  1. Elgmal 此密码和之后的椭圆曲线密码一样,都基于离散对数问题 Discrete Logarithm Problem DLP。 公钥与私钥的产生
  2. 产生一个大素数p
  3. 选择ZpZ_pZp​的一个生成元g。(大概就是g的幂能够遍历ZpZ_pZp​
  4. 选择一个随机数a, 0≤a≤p−10\leq a\leq p-10≤a≤p−1
  5. 计算β=ga mod p\beta = g^a\ mod \ pβ=ga mod p
  6. 公钥为(p,g,β\betaβ)
  7. 私钥为aaa
  8. 加密过程
  9. 选择一个随机数k,0≤k≤p−10\leq k\leq p-10≤k≤p−1
  10. 如果要加密消息m
  11. 则c1=gk(mod p)c_1=g^k(mod\ p)c1​=gk(mod p),c2=m(β)k(mod p)c_2=m(\beta)^k(mod\ p)c2​=m(β)k(mod p)
  12. 解密过程 m=c2((c1)a)−1m=c_2((c_1)^a)^{-1}m=c2​((c1​)a)−1 解密原理 因为c2=m(β)k=mgak(mod p)c_2=m(\beta)^k=mg^{ak}(mod\ p)c2​=m(β)k=mgak(mod p) 又((c1)a)−1=(gak)−1(mod p)((c_1)^a)^{-1}=(g^{ak})^{-1}(mod\ p)((c1​)a)−1=(gak)−1(mod p) 故m=c2((c1)a)−1m=c_2((c_1)^a)^{-1}m=c2​((c1​)a)−1 例子
  13. p=97, g=5, 选择的随机数为a=58, β=ga=558(mod 97)=44\beta=g^a=5^{58}(mod\ 97)=44β=ga=558(mod 97)=44(这一步需要用到平方乘算法,计算量较大
  14. 于是(97, 5, 44)便为公钥,大家可以利用公钥来进行加密。58为私钥,只有知道私钥的人才可以解密。
  15. 假设需要加密m=3,再选择一个随机数k=36。 则c1=gk(mod p)=536(mod 97)=50c_1=g^k(mod\ p)=5^{36}(mod\ 97)=50c1​=gk(mod p)=536(mod 97)=50(同样平方乘 c2=mβk(mod p)=3∗4436(mod 97)=31c_2=m\beta^k(mod\ p)=3*44^{36}(mod\ 97)=31c2​=mβk(mod p)=3∗4436(mod 97)=31 这里经过老杜的指点,不必完全利用平方乘算法来计算,那样计算量太大了。因为如果有是1还要乘44,平方以后再乘44让人受不了,这时候还是之前手工算的时候更灵活,先算平方,再算四次方…$
  16. 解密 m=c2∗((c1)a)−1(mod p)=31∗(5058)−1(mod 97)m=c_2*((c_1)^a)^{-1}(mod\ p)=31*(50^{58})^{-1}(mod\ 97)m=c2​∗((c1​)a)−1(mod p)=31∗(5058)−1(mod 97) 5058(mod 97)50^{58}(mod\ 97)5058(mod 97)认真算一下,发现4次方是-1,之后的八次方,十六次方,以及三十二次方都是1,故最后的结果是75。 75的逆为22,故m=31∗22(mod 97)=3m=31*22(mod\ 97)=3m=31∗22(mod 97)=3。成功解密得到了明文。

img

Elliptic Curve 椭圆曲线密码与Elgmal极为类似,可以对照记忆。 椭圆曲线定义 椭圆曲线是满足y2=x3+ax+b a,b∈Ry^2=x^3+ax+b \ \ \ a,b\in Ry2=x3+ax+b a,b∈R的所有解(x,y)∈R×R(x,y)\in R\times R(x,y)∈R×R,加上一个无穷远点O。 椭圆曲线上的加法 这个椭圆曲线上的加法十分重要,必须要背出来,之后的加密需要用到。

img

img

当两个点关于x轴对称的话,那么他们相加为O。一个点加上一个O,还是本身。 如果那么普通的两个点P(xP,yPx_P,y_PxP​,yP​)与Q(xQ,yQx_Q,y_QxQ​,yQ​),假设它们相加为R点(xR,yRx_R,y_RxR​,yR​)的话。 我们首先需要算一个神秘的值α\alphaα。这个α\alphaα需要分两种情况讨论。如果PPP和QQQ不是同一个点,α\alphaα就是就是两点的斜率。如果P和Q是同一个点,那么这时α=(3x2+a)/2y\alpha = (3x^2+a)/2yα=(3x2+a)/2y,这里实际上不用区分xRx_RxR​还是xPx_PxP​了,因为两者的坐标相同。 然后xR=α2−xP−xQ mod px_R=\alpha^2-x_P-x_Q\ mod\ pxR​=α2−xP​−xQ​ mod p。 yR=α(xP−xR)−yP mod py_R=\alpha(x_P-x_R)-y_P\ mod\ pyR​=α(xP​−xR​)−yP​ mod p 没什么好记忆的法子,考试前多背几遍。 椭圆曲线上加法例子

img

α=(3∗1+4)/6 mod 2773\alpha = (3 * 1 + 4 )/ 6\ mod\ 2773α=(3∗1+4)/6 mod 2773。 这里我们可以发现式子里的除法不是真的除,而是要转化为乘它的逆。 2773=462∗6+12773=462*6+12773=462∗6+1 1=2773−462∗61 = 2773-462*61=2773−462∗6 6的逆为-462,即2311, 则α=2312\alpha = 2312α=2312。 ok,然后xR=α2−xp−xp=1771 (mod 2773)x_R=\alpha^2-x_p-x_p=1771\ (mod\ 2773)xR​=α2−xp​−xp​=1771 (mod 2773) yR=α(xP−xR)−yR=705 (mod 2773)y_R=\alpha(x_P-x_R)-y_R=705\ (mod\ 2773)yR​=α(xP​−xR​)−yR​=705 (mod 2773) 二次剩余定义

img

img

当我们判断d是p的二次剩余后,去求解x的时候,如果p是一个模4余3的素数,那么x就可以直接给出结果 x=±d(p−1)/4 mod px=\pm d^{(p-1)/4} \ mod\ px=±d(p−1)/4 mod p

img

ECC中的离散对数DLP问题

img

在椭圆曲线群中的k×αk\times\alphak×α,对应ZpZ_pZp​中的αk\alpha ^kαk。 在ZpZ_pZp​下求kkk很难,在椭圆曲线中也很难求kkk,便以此为依据设计了一个陷门函数,形成了ECC。 密钥生成

  1. 选择一个椭圆曲线E(mod p),p是一个大素数。
  2. 选择E上的点G,使得n×G=On\times G=On×G=O
  3. 选择随机数a, a<na<na<n
  4. 计算PA=a×GP_A=a\times GPA​=a×G。

公钥(p,E,PA)(p, E, P_A)(p,E,PA​),私钥aaa。

Elgmal

此密码和之后的椭圆曲线密码一样,都基于离散对数问题 Discrete Logarithm Problem DLP

公钥与私钥的产生

  1. 产生一个大素数p
  2. 选择ZpZ_pZp​的一个生成元g。(大概就是g的幂能够遍历ZpZ_pZp​
  3. 选择一个随机数a, 0≤a≤p−10\leq a\leq p-10≤a≤p−1
  4. 计算β=ga mod p\beta = g^a\ mod \ pβ=ga mod p
  5. 公钥为(p,g,β\betaβ)
  6. 私钥为aaa

加密过程

  1. 选择一个随机数k,0≤k≤p−10\leq k\leq p-10≤k≤p−1
  2. 如果要加密消息m
  3. 则c1=gk(mod p)c_1=g^k(mod\ p)c1​=gk(mod p),c2=m(β)k(mod p)c_2=m(\beta)^k(mod\ p)c2​=m(β)k(mod p)

解密过程

m=c2((c1)a)−1m=c_2((c_1)^a)^{-1}m=c2​((c1​)a)−1

解密原理

因为c2=m(β)k=mgak(mod p)c_2=m(\beta)^k=mg^{ak}(mod\ p)c2​=m(β)k=mgak(mod p)

又((c1)a)−1=(gak)−1(mod p)((c_1)^a)^{-1}=(g^{ak})^{-1}(mod\ p)((c1​)a)−1=(gak)−1(mod p)

故m=c2((c1)a)−1m=c_2((c_1)^a)^{-1}m=c2​((c1​)a)−1

例子

  1. p=97, g=5, 选择的随机数为a=58, β=ga=558(mod 97)=44\beta=g^a=5^{58}(mod\ 97)=44β=ga=558(mod 97)=44(这一步需要用到平方乘算法,计算量较大
  2. 于是(97, 5, 44)便为公钥,大家可以利用公钥来进行加密。58为私钥,只有知道私钥的人才可以解密。
  3. 假设需要加密m=3,再选择一个随机数k=36。 则c1=gk(mod p)=536(mod 97)=50c_1=g^k(mod\ p)=5^{36}(mod\ 97)=50c1​=gk(mod p)=536(mod 97)=50(同样平方乘 c2=mβk(mod p)=3∗4436(mod 97)=31c_2=m\beta^k(mod\ p)=3*44^{36}(mod\ 97)=31c2​=mβk(mod p)=3∗4436(mod 97)=31 这里经过老杜的指点,不必完全利用平方乘算法来计算,那样计算量太大了。因为如果有是1还要乘44,平方以后再乘44让人受不了,这时候还是之前手工算的时候更灵活,先算平方,再算四次方…$
  4. 解密 m=c2∗((c1)a)−1(mod p)=31∗(5058)−1(mod 97)m=c_2*((c_1)^a)^{-1}(mod\ p)=31*(50^{58})^{-1}(mod\ 97)m=c2​∗((c1​)a)−1(mod p)=31∗(5058)−1(mod 97) 5058(mod 97)50^{58}(mod\ 97)5058(mod 97)认真算一下,发现4次方是-1,之后的八次方,十六次方,以及三十二次方都是1,故最后的结果是75。 75的逆为22,故m=31∗22(mod 97)=3m=31*22(mod\ 97)=3m=31∗22(mod 97)=3。成功解密得到了明文。

img

Elliptic Curve

椭圆曲线密码与Elgmal极为类似,可以对照记忆。

椭圆曲线定义

椭圆曲线是满足y2=x3+ax+b a,b∈Ry^2=x^3+ax+b \ \ \ a,b\in Ry2=x3+ax+b a,b∈R的所有解(x,y)∈R×R(x,y)\in R\times R(x,y)∈R×R,加上一个无穷远点O。

椭圆曲线上的加法

这个椭圆曲线上的加法十分重要,必须要背出来,之后的加密需要用到。

img

img

当两个点关于x轴对称的话,那么他们相加为O。一个点加上一个O,还是本身。

如果那么普通的两个点P(xP,yPx_P,y_PxP​,yP​)与Q(xQ,yQx_Q,y_QxQ​,yQ​),假设它们相加为R点(xR,yRx_R,y_RxR​,yR​)的话。

我们首先需要算一个神秘的值α\alphaα。这个α\alphaα需要分两种情况讨论。如果PPP和QQQ不是同一个点,α\alphaα就是就是两点的斜率。如果P和Q是同一个点,那么这时α=(3x2+a)/2y\alpha = (3x^2+a)/2yα=(3x2+a)/2y,这里实际上不用区分xRx_RxR​还是xPx_PxP​了,因为两者的坐标相同。

然后xR=α2−xP−xQ mod px_R=\alpha^2-x_P-x_Q\ mod\ pxR​=α2−xP​−xQ​ mod p。

yR=α(xP−xR)−yP mod py_R=\alpha(x_P-x_R)-y_P\ mod\ pyR​=α(xP​−xR​)−yP​ mod p

没什么好记忆的法子,考试前多背几遍。

椭圆曲线上加法例子

img

α=(3∗1+4)/6 mod 2773\alpha = (3 * 1 + 4 )/ 6\ mod\ 2773α=(3∗1+4)/6 mod 2773。

这里我们可以发现式子里的除法不是真的除,而是要转化为乘它的逆。

2773=462∗6+12773=462*6+12773=462∗6+1

1=2773−462∗61 = 2773-462*61=2773−462∗6

6的逆为-462,即2311, 则α=2312\alpha = 2312α=2312。

ok,然后xR=α2−xp−xp=1771 (mod 2773)x_R=\alpha^2-x_p-x_p=1771\ (mod\ 2773)xR​=α2−xp​−xp​=1771 (mod 2773)

yR=α(xP−xR)−yR=705 (mod 2773)y_R=\alpha(x_P-x_R)-y_R=705\ (mod\ 2773)yR​=α(xP​−xR​)−yR​=705 (mod 2773)

二次剩余定义

img

img

当我们判断d是p的二次剩余后,去求解x的时候,如果p是一个模4余3的素数,那么x就可以直接给出结果

x=±d(p−1)/4 mod px=\pm d^{(p-1)/4} \ mod\ px=±d(p−1)/4 mod p

img

ECC中的离散对数DLP问题

img

在椭圆曲线群中的k×αk\times\alphak×α,对应ZpZ_pZp​中的αk\alpha ^kαk。

在ZpZ_pZp​下求kkk很难,在椭圆曲线中也很难求kkk,便以此为依据设计了一个陷门函数,形成了ECC。

密钥生成

  1. 选择一个椭圆曲线E(mod p),p是一个大素数。
  2. 选择E上的点G,使得n×G=On\times G=On×G=O
  3. 选择随机数a, a<na<na<n
  4. 计算PA=a×GP_A=a\times GPA​=a×G。

公钥(p,G,PA)(p, G, P_A)(p,G,PA​),私钥aaa。

和Elgmal完全一致。

加密过程

假设需要加密的信息为PmP_mPm​。和Elgmal一样,会加密出来一个C1C_1C1​,一个C2C_2C2​。

C1=kGC_1=kGC1​=kG

C2=Pm+kPAC_2=P_m+kP_AC2​=Pm​+kPA​

我们发现实际上就是把Elgmal的乘号变成加号,幂变成乘号,就能从Elgmal类推出ECC。这里值得注意的是,我们的信息PmP_mPm​是一个点,实际上C1C_1C1​和C2C_2C2​也是点,里面有横坐标和纵坐标两个信息。

解密过程

Elgmal的解密是这样的m=c2∗((c1)a)−1m=c_2*((c_1)^a)^{-1}m=c2​∗((c1​)a)−1。我们可以类推得到ECC的解密公式为Pm=C2+(−a∗C1)=C2−aC1P_m=C_2+(-a*C_1)=C_2-aC_1Pm​=C2​+(−a∗C1​)=C2​−aC1​

值得注意的是,加密过程和解密过程中的计算都没有模p。因为这些都是点,两个点相加得时候得计算公式中就是有模p了,相当于每次计算实际都在模p,故最外层就不用写模p了,写了也没用2333,一个点最后怎么模p呢?

例子

Z23Z_{23}Z23​上的椭圆曲线: y2=x3+x+1y^2=x^3+x+1y2=x3+x+1

取G=(6,4)G=(6, 4)G=(6,4), a=3a=3a=3,计算

PA=3∗(6,4)=(6,4)+(6,4)+(6,4)P_A=3*(6, 4)=(6,4)+(6,4)+(6,4)PA​=3∗(6,4)=(6,4)+(6,4)+(6,4)

我们先算(6,4)+(6,4)(6,4)+(6,4)(6,4)+(6,4),求α=5\alpha=5α=5。

xR=13, yR=7x_R=13,\ y_R=7xR​=13, yR​=7。即(6,4)+(6,4)=(13,7)(6,4)+(6,4)=(13,7)(6,4)+(6,4)=(13,7)。再计算(13,7)+(6,4)(13,7)+(6,4)(13,7)+(6,4)。

求得α=3/7=3∗10=30=7\alpha=3/7=3*10=30=7α=3/7=3∗10=30=7。

xR=7x_R=7xR​=7,yR=12y_R=12yR​=12

得最后的结果为(7,12)(7, 12)(7,12)

从这里可见ECC的手工计算量有多大,一个普通的乘法实际上内部复杂无比。

于是公钥为(23,(6,4),(7,12))(23, (6,4), (7,12))(23,(6,4),(7,12)),私钥为a=3

假设明文为Pm=(5,4)P_m=(5, 4)Pm​=(5,4)

然后选择随机数k=2,现在开始加密

C1=kG=2∗(6,4)=(6,4)+(6,4)=(13,7)C_1=kG=2*(6,4)=(6,4)+(6,4)=(13,7)C1​=kG=2∗(6,4)=(6,4)+(6,4)=(13,7)

这里舒服了哈哈,因为之前求3∗(6,4)3*(6,4)3∗(6,4)的过程中已经把(6,4)+(6,4)(6,4)+(6,4)(6,4)+(6,4)算出来了。

C2=Pm+k∗PA=(5,4)+2∗(7,12)C_2=P_m+k*P_A=(5,4)+2*(7,12)C2​=Pm​+k∗PA​=(5,4)+2∗(7,12)

先求(7,12)+(7,12)(7,12)+(7,12)(7,12)+(7,12),求得α=(3∗49+1)/24\alpha=(3*49+1)/24α=(3∗49+1)/24,这里就比较有趣了,因为24就是1,1的逆还是1,所以直接不用算了。

α=10\alpha=10α=10。

xR=α2−xp−xq=100−14=86=17x_R=\alpha^2-x_p-x_q=100-14=86=17xR​=α2−xp​−xq​=100−14=86=17

yR=α(xp−xR)−yp=10∗(−10)−12=158=3y_R=\alpha(x_p-x_R)-y_p=10*(-10)-12=158=3yR​=α(xp​−xR​)−yp​=10∗(−10)−12=158=3。

故(7,12)+(7,12)=(17,3)(7,12)+(7,12)=(17,3)(7,12)+(7,12)=(17,3)。

再求(17,3)+(5,4)(17,3)+(5,4)(17,3)+(5,4)

先求α=21\alpha=21α=21。xR=212−17−5=5x_R=21^2-17-5=5xR​=212−17−5=5。

yR=21∗(17−5)−3=19y_R=21*(17-5)-3=19yR​=21∗(17−5)−3=19。

得最后得结果为(5,19)(5,19)(5,19)

所以我们加密的结果为((13,7),(5,19))((13,7),(5,19))((13,7),(5,19))。

现在还是解密m=C2−aC1=(5,19)−3∗(13,7)m=C_2-aC_1=(5,19)-3*(13,7)m=C2​−aC1​=(5,19)−3∗(13,7)

(13+7)+(13,7)=(5,4)(13+7)+(13,7)=(5,4)(13+7)+(13,7)=(5,4)。

(5,4)+(13,7)=(17,3)(5,4)+(13,7)=(17,3)(5,4)+(13,7)=(17,3)

所以m=(5,19)−(17,3)m=(5,19)-(17,3)m=(5,19)−(17,3)。

到这一步你可能不知所措了,因为我们之前所有的运算都是两个点之间的加法,那减法要怎么求呢?

我们可以根据一开始的定义

设P=(x,y)P=(x,y)P=(x,y)则−P=(x,−y)-P=(x,-y)−P=(x,−y)。

所以m=(5,19)−(17,3)=(5,19)+(−(17,3))=(5,19)+(17,−3)m=(5,19)-(17,3)=(5,19)+(-(17,3))=(5,19)+(17,-3)m=(5,19)−(17,3)=(5,19)+(−(17,3))=(5,19)+(17,−3)

所以实际上减法就是减数的纵坐标转化为相反数,就能转化加法了。

α=22/(−12)=22/11\alpha=22/(-12)=22/11α=22/(−12)=22/11。这里我们又遇到了一个比较特别的情况,上下数可以整除了,但是我们应该将将除法转变为乘上分母的逆。11的逆是21。则α=22∗21=(−1)∗(−2)=2\alpha=22*21=(-1)*(-2)=2α=22∗21=(−1)∗(−2)=2。竟然与直接除是一样的结果。

以后如果发现分母和分子能够直接整除,便可以直接除得到结果,如果发现不能整除,便将乘以分母的逆。

xR=22−22=−18=5x_R=2^2-22=-18=5xR​=22−22=−18=5

yR=2(5−5)−19=4y_R=2(5-5)-19=4yR​=2(5−5)−19=4。

所以最后的结果为(5,4)(5,4)(5,4)。和一开始的信息相同,解密成功。

Rabin

Rabin密码利用的数学原理是离散平方根问题,实际上就是合数模下二次剩余求解难的问题。

它的形式非常简单,选择两个模四余数3的素数,做为私钥,将它们的乘积做为公钥。

即,私钥(p,q)(p,q)(p,q)。公钥nnn。

加密也十分简单。c=m2 (mod n)c=m^2\ (mod\ n)c=m2 (mod n)

解密的话就是求解同余方程m2=c(mod n)m^2=c(mod\ n)m2=c(mod n)。把m求出来即可。

由数学基础知

img

也就是将模n的同余式转化为求一个同余式组。由于模4余三的素数的解可以直接写出来。然后再根据解出来的信息根据中国剩余定理求出解。一般的解会有4个。

Rabin举例

n=77n=77n=77,c=23c=23c=23。

这里我们可以轻松得将合数777777分解为777和111111。7和11正好都是模4余3得素数,相当于白给了。实际上的Rabin密码肯定不会用这么简单的n,肯定是两个大素数相乘的,所以这个Rabin密码看起来利用的数学问题是离散平方根,其基础还是分解问题,如果可以轻易的分解,那就和这道题一样白给了。

m2=23 mod 77m^2=23\ mod\ 77m2=23 mod 77

可以转化为以下两个式子

m2=23 mod 11m^2=23\ mod\ 11m2=23 mod 11

m2=23 mod 7m^2 = 23\ mod\ 7m2=23 mod 7

然后根据模4余三素数的结论

m=±2312/4 mod 11=±1m=\pm23^{12/4}\ mod\ 11=\pm1m=±2312/4 mod 11=±1

m=±238/4 mod 7=±4m=\pm 23^{8/4}\ mod\ 7=\pm4m=±238/4 mod 7=±4

这里进行运算的时候,比如第一个式子,因为是模11意义下的,所以23直接就是1,所以直接得出结果了,十分方便。

接下来我们复习一下中国剩余定理。

x=b1 mod m1x=b_1\ mod\ m_1x=b1​ mod m1​

x=b2 mod m2x=b_2\ mod\ m_2x=b2​ mod m2​

m=m1∗m2m=m_1*m_2m=m1​∗m2​

得M1=m2,M2=m1M_1=m2,M_2=m1M1​=m2,M2​=m1。因为mi∗Mi=mm_i*M_i=mmi​∗Mi​=m

求M1′=invmod(M1,m1)M_1'=invmod(M_1,m_1)M1′​=invmod(M1​,m1​)

M2′=invmod(M2,m2)M_2'=invmod(M_2,m_2)M2′​=invmod(M2​,m2​)

最后得解就是x=b1M1M1′+b2M2M2′ mod(m)x=b_1M_1M_1'+b_2M_2M_2'\ mod(m)x=b1​M1​M1′​+b2​M2​M2′​ mod(m)

我们来算一下这道例子。

m1=11,m2=7,m=77m_1=11,m_2=7,m=77m1​=11,m2​=7,m=77。

M1=7,M2=11M_1=7,M_2=11M1​=7,M2​=11

M1′=8,M2′=2M_1'=8,M_2'=2M1′​=8,M2′​=2 这里求逆得时候可以直接试,7关于11的逆怎么求呢?直接从1试到8,发现7∗8=56=17*8 =56=17∗8=56=1,那8就是逆了。

则最后的解为x=56b1+22b2 mod 77x=56b_1+22b_2\ mod\ 77x=56b1​+22b2​ mod 77。

这里的b1b_1b1​和b2b_2b2​实际上都有两种可能,分别为±1\pm1±1和±4\pm4±4。

最后的解分别为67,45,32,1067,45,32,1067,45,32,10。

Knapsack

背包密码。将信息01串隐藏在选择哪个背包中,比如第一个选,第二个不选,那就是10。

这里我们只考虑简单背包,也就是给你一个重量后,到底选择哪些背包是唯一确定的。

img

img

img

img

值得注意的是,这里的例子还不是公钥密码,因为它只有一个密钥。我们需要将这种背包的思路转化为一个巧妙的公钥密码。

于是,我们便对这个背包改变一波,给大众一个更改后的背包。

首先取一个整数S,S需要比原本背包的总共重量大。

然后再取一个和S互素的数ttt。

然后将原始的背包的每个重量都乘上这个t,构成一个新包(记得模

即u=(u1,u2,..un)=(ta1 mod s,ta2 mod s,...,tan mod s)u=(u_1,u_2,..u_n)=(ta_1\ mod\ s, ta_2\ mod\ s,...,ta_n\ mod\ s)u=(u1​,u2​,..un​)=(ta1​ mod s,ta2​ mod s,...,tan​ mod s)

这个u就是新包了。向外公开(u,s)(u,s)(u,s)做为公钥,(a,t)(a,t)(a,t)做为私钥。

加密要怎么样呢?直接加就行了!最后模一个s。

img

所以加密出来是一个数。

我们这里可能会想,破译者能不能直接根据密文和这个假背包得到明文呢?仔细思考一下就会发现是不可能的,因为我们能够准确的求出一个序列,也就是确定哪些包选择,哪些包不选择的前提条件是这个背包是超递增序列,而这个u呢?它是经过模运算后的,它的值是乱七八糟的,肯定不是超递增序列了,所以是不可能直接通过u和c得到明文的。

也就是我们如果要得到序列,肯定是要一个超递增序列,那么我们就需要把u这个包重新转换为a这个包,实际上乘上t−1t^{-1}t−1即可。

img

乘上-1后那个值实际上就是在原背包里的重量。我们很容易根据超递增序列来求出哪些包取哪些包不取,也就解密出来了。

背包例子1

img

首先我们求一下u背包 u=(575,436,1586,1030,1921,569,721,1183,1570)u=(575,436,1586,1030,1921,569,721,1183,1570)u=(575,436,1586,1030,1921,569,721,1183,1570)

这里算吐了,如果刘杨出这么长的背包和这么大的数,我就要寄刀片了。

之后的加密反而容易。

假设你要进行加密的信息为(101100111)(101100111)(101100111)。

则c=575+1586+1030+721+1188+1570 mod 2003=656c=575+1586+1030+721+1188+1570\ mod\ 2003=656c=575+1586+1030+721+1188+1570 mod 2003=656。

如何解密呢?我们只需要求一下一开始的t的逆(关于s)为317。

然后c∗t−1=656∗317 mod 2003=1643c*t^{-1}=656*317\ mod\ 2003=1643c∗t−1=656∗317 mod 2003=1643。

然后根据原始背包,我们很容易得到序列(101100111)(101100111)(101100111)。

背包例子2

上一题显然计算量太大了,这里还有一道简单的,估计考试就是这种简单的难度。

img

这里我们先求一下t−1=4t^{-1}=4t−1=4。

再求u包u=(23,68,69,5,11)u=(23,68,69,5,11)u=(23,68,69,5,11)。

假设需要加密的信息为m=(01011)m=(01011)m=(01011)。

则c=84c=84c=84。

解密:

84∗4 mod 89=6984*4\ mod\ 89=6984∗4 mod 89=69。

得(01011)(01011)(01011),解密完成。

手算了例子1,现在做这个例题2简直爽翻了!

Hash

看了一看2008年得卷子,没有Hash函数得计算题。我决定对接下去得Hash和签名都注重理解,对它们具体得实现原理不做详解。

我们之前学习得古典密码、流密码、公钥密码都是有加密有解密,一个信息在加密后还能够恢复到原样,这有什么用呢?其目的就在于隐藏这个信息而只让某些人(有密钥)的人阅读,这做到了信息的保密。

但是如何保证这个信息有没有被篡改呢?这可以用到Hash函数。Hash的目的不是为了解密,从目的上讲,它不应该被解密,它应该是一个单向函数。

以下为Hash函数的应用,建议背下来,非常有可能出个多项选择题。

  1. 消息认证
  2. 软件完整性
  3. 数字签名
  4. 一次性口令
  5. 时间戳
  6. 证书

哈希分为强无碰撞哈希和若不碰撞哈希。

它们的前三个性质一致

  1. 压缩
  2. 容易计算
  3. 单向

然后它们最后一个性质分别为强无碰撞性和弱无碰撞性。

弱碰撞性指的是,给定m1m_1m1​,找到m2≠m1m_2\neq m_1m2​​=m1​使得h(m1)=h(m2)h(m_1)=h(m_2)h(m1​)=h(m2​),在计算上不可行。

也就是说如果一个函数要被称为hash函数,它起码要满足,给定一个明文,你在给定时间内无法找到另一个明文,使它们值相等。

那强碰撞性是不是指根本不可能存在两个两个明文使得他们的hash值相等呢?不是。

这里我们思考hash的第一个性质,压缩,任何长度的明文经过hash之后都会产生固定长度的内容,比如128位。

那就如生日重复这个问题,找366个人来,至少有两个人的生日是重复的,对于hash来说也是一样的,21282^{128}2128个明文的hash值中至少有两个的值是相等的,所以hash函数的第一个性质就决定了它一定会存在碰撞。

那这个强碰撞是什么意思呢?是在规定时间里,你找不到两个明文,它们hash值相等。我们可以看到这比弱碰撞性的更牛,因为它允许你随便去找,而不是去找给定明文的碰撞。

对于一个强碰撞哈希,你在给定时间里甚至都找不到两个哈希值相等的明文,更不用说找到某个特定明文的碰撞了。

安全哈希函数的构造

img

这里例子满足hash函数的定义嘛?压缩了,容易计算并且是单向的,因为你无法根据一个余数知道它到底是哪个数的余数。但是不满足弱碰撞性,我们很容易就能获得一个同余的数。

img

该例子同样满足压缩,易算,单向。但是还是不满足弱碰撞行。只要将明文换顺序,还是同样的hash值。

MD5特性

  1. 直接构造法: 不依赖任何密码系统和假设条件
  2. 算法简洁
  3. 计算速度快
  4. 特别适合32位计算机软件实现
  5. 倾向于使用低端结构

MD5需要记忆的一些东西

我也不知道MD5会考察的多么深,这里简单的分析一下。

  1. 消息的长度模512如果不是448,那么就首先需要进行填充,填充的方法十分简单,就是往最后添加10000..。至于有多少个零,就只要填到总的长度模512为448就行了。
  2. 为什么不直接填到512的整数倍呢?因为还要加64位的明文长度。什么意思呢?也就是还要加64位的二进制数,这个二进制数代表了明文的长度。所以加上这个明文长度的64位后,最后的长度就是512位的整数倍了,这也就是一开始为填充到448的原因。
  3. 然后初始化4个128位的寄存器A,B,C,D。
  4. 消息由512位的数据块进行处理4轮,每轮是有16轮的迭代。

img

MD5分析

img

经过这么多年,md5的碰撞已经被发现了,所以它不是一个强碰撞哈希了。但是它仍然是一个弱碰撞哈希。因为现在还没有人能够在可以接受的时间内找到给定明文的碰撞。

更不可能根据一个哈希值通过运算得到它的原文,因为这是哈希的性质单向。现在的哈希破解都是把一些常见明文的哈希值存入数据库然后搜索得到的,不是解密。未来MD5可能会找到快速找到给定明文的哈希碰撞的方法,但是绝对不可能出现解密,这违背它做为哈希函数的底线。

SHA(Secure Hash Algorithm)

img

有5种SHA,分别为SHA-1,SHA-2,SHA-224,SHA-256,SHA-384,SHA-512。

课件上介绍的SHA-1感觉和MD5极为类似。貌似只是把寄存器个数设置成了5个,然后每个寄存器的位数变成了160位。

仍然是4轮,但是每轮的迭代次数从16次变成了20次。

感觉就是个强化版MD5,企图通过加强属性来让碰撞变得困难。

但是这对于王小云来说没有什么用,这对她来说应该就像做同种题型一样简单。

img

img

img

我们可以看到,SHA-256,SHA-224,SHA-512,SHA-384还没有找到一组碰撞,说明这四个SHA在目前还属于强碰撞哈希,非常牛皮。

生日攻击

生日攻击的目标是将一个让一条真消息和假消息的哈希值相同,从而传递假消息。

我们假设两条消息的哈希值不同,这个概率很高,但是这个概率经过多次次方后肯定会下降。

数字签名

一段信息的传输在安全性上有三个目标,第一个是加密,我们可以利用之前的加密算法让信息在信道中加密,从而让中间人不知道内容。第二个是完整,我们用hash实现了这个目标。第三个目标是认证,就是通信的双方得知道对方得身份,确保自己是在和正确的人对话。而接下去讲的数字签名可以做到这一点。

数字签名有以下应用

  1. 身份认证
  2. 数据完整性
  3. 不可否认性
  4. 匿名性 这个匿名性 我还没明白是什么意思。

一个合格的数字签名应该有以下要求

  1. 签名者事后不能抵赖自己的签名
  2. 任何其他人不能伪造签名
  3. 数字签名可由第三方认证,从而能够解决通信双方的争议
  4. 签名的产生必须使用发方独有的一些信息以防伪造和否认
  5. 签名的产生应比较容易
  6. 签名的识别和验证应比较容易
  7. 对已知的数字签名构造一新的信息或对已知信息构造一假冒的签名在计算上都是不可行的

RSA签名——签名

签名,和它的名字一样,应该是一个人独特的。所以如果你要对某个信息进行签名的话,你需要首先产生出一些只有你知道的东西。

由于是RSA签名,你便选定了RSA公钥(e,n)和私钥(d,p,q)。

我们可以看到这里的公钥和私钥和RSA密码中的一模一样。也同样是将公钥公开,私钥保留。

但是它们的用法发生了调转。在RSA公钥密码中,私钥是用来解密的,公钥是用来加密的,但是在签名中,私钥是用来签名的,而别人验证的时候,用的是公钥。这也能够理解,因为签名中有一条要求,数字签名可由第三方认证,从而嫩黄瓜狗解决通信双方的争议

实际的签名过程非常简单s=md(mod n)s=m^d(mod\ n)s=md(mod n)。

ok,完毕了。这个s就是你产生的签名。

RSA签名——验证

假设武丑兄收到了wuuconix发送的信息,其中附带了签名即(m,s)(m,s)(m,s)。

这个时候武丑兄要怎么验证这条信息是不是wuuconix发送的呢?

首先,武丑兄需要去下载wuuconix的公钥(e,n)(e,n)(e,n)。用公钥来验证这个签名是否正确。

se=m(mod n)s^e=m(mod\ n)se=m(mod n)。

实际上只要验证以上实际是否成立即可。

签名和加密先后顺序

这里有一个先签名再加密还是先加密再签名的问题。

最后的结论是先签名后加密。但是不是特别懂。这是一篇参考文章

流程如下

img

我们可以看到发送方即用到了自己的私钥用来签名,又用到了收件方的公钥用来加密。收件方即用到了自己的私钥用来解密,又用到了发送方的公钥进行验证。

这种非对称密码的思想在信息的传递过程中发挥得淋漓尽致。

RSA签名的存在性伪造

  1. 假设(m1,s1)(m_1,s_1)(m1​,s1​)和(m2,s2)(m_2,s_2)(m2​,s2​)是有效的签名。 那么(m1m2,s1s2)(m_1m_2,s_1s_2)(m1​m2​,s1​s2​)也是有效的签名。 这里不是单纯的字符串连接,而是两个数的乘。因为我们已经采用RSA签名了,实际上都是指数运算,输入和输出都是一个个数。 因为s1=m1d(mod n)s_1=m_1^d(mod\ n)s1​=m1d​(mod n) s2=m2d(mod n)s_2=m_2^d(mod\ n)s2​=m2d​(mod n) 所以两个签名一乘,s1s2=m1dm2d =(m1m2)d mod(n)s_1s_2=m_1^dm_2^d\ =(m_1m_2)^d\ mod(n)s1​s2​=m1d​m2d​ =(m1​m2​)d mod(n)。 所以(m1m2,s1s2)(m_1m_2,s_1s_2)(m1​m2​,s1​s2​)也是合法的签名。 我觉得这个说法挺奇怪的,我更倾向于说s1s2s_1s_2s1​s2​是m1m2m_1m_2m1​m2​的合法签名。
  2. 假设(m,s)(m,s)(m,s)是有效的签名 则(m−1,s−1)(m^{-1},s^{-1})(m−1,s−1)也是有效的签名。和第一个伪造的证法类似,都用到了幂的性质。

这两个伪造中我认为更有破坏性的是第一个,因为我们可以搜集大量明文和签名,记录下来,然后进行组合,从而发送我们需要的信息,并且通过验证。

可以应对这种伪造,我们可以对先对明文进行一层hash,再进行签名。

但是我比较疑惑,这种伪造发生在什么样得情况下。正常情况下,(m,s)(m,s)(m,s)做为一个整体在传递过程中是被加密得,只有收信方,也就是有私钥的那个人才能进行解密,从而看到mmm与sss。正常情况下中间人是看不到签名的。

那我们假设中间人破解了加密,获得了签名,那这时它肯定也知道了mmm。因为(m,s)(m,s)(m,s)是做为一个整体被加密的,你一旦看到了sss,就说明你进行了解密,这时候mmm对你来说不是唾手可得嘛?那对签名进行强化,即对密文的哈希进行签名还有什么用呢?

我认为这么做唯一的好处就是可以让生成签名的时候更加快速,因为哈希之后的明文的长度将大大减小,这时候再进行RSA的指数运算,就会快得多了。

PGP

PGPPretty Good Privacy是最著名的保密电子邮件软件系统。该系统中就用到了RSA数字签名。

img

考试前把流程都背下来,非常有可能让你写。

Elgamal签名密钥生成

和Elgamal密码类似,密钥生成过程是一样的。

img

但是签名和验证的部分和加密解密有所区别。

Elgamal签名 签名

  1. 选择随机数k,k需要和p−1p-1p−1互素。
  2. 计算x=gk(mod p)x=g^k(mod \ p)x=gk(mod p)
  3. 计算y=(m−ax)k−1(mod p−1)y=(m-ax)k^{-1}(mod\ p-1)y=(m−ax)k−1(mod p−1)
  4. 签名:SIGa,k=(x,y)SIG_{a,k}=(x,y)SIGa,k​=(x,y)

和加密过程一样,签名过程会产生出两个值,xxx与yyy,在加密过程中那两个数叫做c1,c2c_1,c_2c1​,c2​。c1c_1c1​和xxx一致。c2c_2c2​与yyy不同。c2=mβk mod(p)c_2=m\beta^k\ mod(p)c2​=mβk mod(p)

Elgamal签名 验证

  1. 收到(m,x,y)(m,x,y)(m,x,y)
  2. 则去计算gm与g^m与gm与βxxy\beta^xx^yβxxy在模p下是否相等,如果相等,则验证通过。

在正常情况下两者就是相等的,这里简单证明一下。βx=gax\beta^x=g^{ax}βx=gax,xy=gk(m−ax)k−1=gm−axx^y=g^{k(m-ax)k^{-1}}=g^{m-ax}xy=gk(m−ax)k−1=gm−ax。

两者一乘就是gmg^mgm啦!

Elgamal签名 例子

  1. 假设p=11,g=2,a=8p=11,g=2,a=8p=11,g=2,a=8
  2. 计算β=ga(mod p)=28(mod 11)=3\beta=g^a(mod\ p)=2^8(mod\ 11)=3β=ga(mod p)=28(mod 11)=3
  3. 则(p,g,β)=(11,2,3)(p,g,\beta)=(11,2,3)(p,g,β)=(11,2,3)为公钥,私钥为a=8a=8a=8
  4. 现在对明文m=5m=5m=5进行签名 假设选择的随机数k为999,因为gcd(9,10)gcd(9,10)gcd(9,10)互素,符合条件 x=gk=29=6 mod(11)x=g^k=2^9=6\ mod(11)x=gk=29=6 mod(11)。 y=(m−ax)k−1 mod(10)=3y=(m-ax)k^{-1}\ mod(10)=3y=(m−ax)k−1 mod(10)=3 则签名s=(x,y)=(6,3)s=(x,y)=(6,3)s=(x,y)=(6,3)
  5. 收信方收到(m,x,y)=(5,6,3)(m,x,y)=(5,6,3)(m,x,y)=(5,6,3),现开始进行验证。 gm(mod p)=25 mod(11)=−1g^m(mod\ p)=2^5\ mod(11)=-1gm(mod p)=25 mod(11)=−1 βx=36 mod(11)=3\beta^x=3^6\ mod(11)=3βx=36 mod(11)=3 xy=63 mod(11)=7x^y=6^3\ mod(11)=7xy=63 mod(11)=7 则βxxy=21=−1=gm mod(11)\beta^xx^y=21=-1=g^m\ mod(11)βxxy=21=−1=gm mod(11) 验证成功。

Elgamal签名 注意点

  1. 签名人用的k绝对不能泄露。 因为y=(m−ax)k−1y=(m-ax)k^{-1}y=(m−ax)k−1,简单移项之后可以得到 a=(m−ky)x−1a=(m-ky)x^{-1}a=(m−ky)x−1。 相当于如果知道了k,那么攻击者相当于知道了密钥a,这时候就可以伪造身份,以发件人的身份向别人发送信息了。
  2. 必须使用hash函数 ppt上说否则容易受到存在性伪造。 而我认为使用hash函数更有说服力的一点是,Elgamal签名的长度是明文的2倍,所以如果不将明文hash后再签名,那么就会导致传输中,为了发送1个G的信息,你需要生成2个G的签名,这太离谱了,签名肯定是越短越好的,所以hash是一个非常好的解决办法。

DSS

DSS是美国数字签名标准,是Elgamal签名的变形,极为类似,其中用到了SHA hash算法。

img

img

盲签名和不可否认签名

这两个就不深究了。还无法想象使用场景,应该也不会考。