本文是课程「应用密码学与网络安全」的2024年6月25日期末考前复习和实验报告的整合。

期末复习

引言

网络安全核心地位的关键目标是CIA3,即保密性confidentiality完整性Integrity可用性availability真实性authenticity可追溯性accountability

OSI安全框架共包括安全攻击、服务和机制三个方面。安全攻击中分为被动和主动两种攻击;前者重点在于预防,后者可以被检测出来但很难有效防止。安全机制分为特定和普遍两种,应具备检测、防御或从安全攻击中恢复的能力——没有单一的机制可以提供所有安全服务。

传统加密技术

密码学是研究编制密码和破译密码的学科,因此分为编码学和分析学两个分支。本学科重点在于前者。

基于密码分析的攻击分为唯密文攻击、已知明文攻击、选择明文攻击、选择密文攻击和选择文本攻击五种。

绝对安全(也称无条件安全或信息论安全)的密码系统中敌手即使拥有无限的计算能力也能保障其安全性;其无法凭密文获取关于明文的任何信息。目前大多数公钥密码学原型能保证的是计算安全(即假设该公钥密码学方法在计算上安全)。

现在考虑以下几个传统加密方法。

Caesar加密

假设现在有明文P=P1P2PnP=\overline{P_1P_2\dots P_n},那么不妨将字母PiP_i按照字母表顺序编号为pp。那么明文可以表示为P=p1p2pn,piN26.P=\overline{p_1p_2\dots p_n}, p_i\in \mathbb{N}_{26}.此时利用Caesar密钥kk进行加密运算

ci=pi+k(mod26),c_i=p_i+k\pmod{26},

得到若干密码cic_i组合而成的密文编码Ci=c1c2cn,piN26.C_i=\overline{c_1c_2\dots c_n},p_i\in\mathbb{N}_{26}.可以选择对其按照字母表中的顺序转换为各种字母,得到密文。随后解密时只要用

pi=cik(mod26)p_i=c_i-k\pmod{26}

即可得到明文编码P=p1p2pnP=\overline{p_1p_2\dots p_n}。之后在按照字母表顺序对照解码即可得到明文。

单表置换加密

先将字母顺序随意打乱,使每个明文字母映射到一个不同的随机密文字母。因此,理论上这种加密方式的密钥总数可以有26!26!种;这个值显然比4×10264\times 10^{26}要大得多。然而因为每个字母使用频率不同等人类语言自带的特性,这个加密方法看似安全其实在实际应用时也并不是那么的理想。

Playfair加密

吸取了单表置换加密中密钥空间小导致无法提供安全性的经验,Charles Wheatstone在1854年以其朋友Baron Playfair为名发明了一种多字母代替密码。其在一个五阶矩阵中首先填写密钥单词,之后将剩余(未出现在这个单词中的)字母拿来填写矩阵中尚未填入的空白中;当将I与J等效时,26个字母恰好能够放入这个总共25个格子的矩阵中。

现在思考以下实例。

以「monarchy」为密钥的Playfair加密。

首先先将密钥有序填入表格。

M O N A R
C H Y

此时剩下的字母还有“BDEFGIJKLPQSTUVWXZ”。将这些字母填入,得到密码表

M O N A R
C H Y B D
E F G I/J K
L P Q S T
U V W X Z

现在开始加密。以2个字母为一组进行加密;对于这个字母组可能会出现如下情况:

  1. 字母组的2个字母在这张表格中位于同行。假设这个字母组是“hd”,那么将这个字母组的两个字母分别向右移动一格,得到密文“YC”(如果明文已经在这一行的最右端则循环回最左端)。
  2. 字母组的2个字母在这张表格中位于同列。假设这个字母组是“pv”,那么将这个字母组的两个字母分别向下移动一格,得到密文“VO”(如果明文已经在这一列的底部则循环回顶部)。
  3. 其他情况。假设字母组是“SE”,那么将这两个字母作为对角线所构造出的矩形单独取出,寻找对应的另一条对角线,其两端按明文中两个字母所在行号出现的顺序进行排序得到“LI”或“LJ”。

解密的思路根据加密方法逆向完成即可。

Hill密码

由Lester Hill于1929年发明的多字母代替密码。其利用模运算意义下的矩阵乘法、矩阵求逆、线性空间等概念实现。

现在,考虑下方的例子。

使用下方所示的矩阵K对明文P=“mor”进行加密。

K=(17175211821229)\mathbf{K}= \begin{pmatrix} 17 & 17 & 5\\ 21 & 18 & 21\\ 2 & 2 & 9 \end{pmatrix}

对上述密钥矩阵求逆,可以得到

K1=(49151517624017)\mathbf{K}^{-1}= \begin{pmatrix} 4 & 9 & 15\\ 15 & 17 & 6\\ 24 & 0 & 17 \end{pmatrix}

而明文

P=(m,o,r)T=(121417)\mathbf{P}= \begin{pmatrix} m , o , r \end{pmatrix}^{T} = \begin{pmatrix} 12\\ 14\\ 17 \end{pmatrix}

(注意上方字母转编码是从00开始编排的)因此密文

C=KP=(527651375)(7311)(mod26)=(H,D,L)T\textbf{C}=\textbf{K}\textbf{P}= \begin{pmatrix} 527\\ 651\\ 375 \end{pmatrix}\equiv\begin{pmatrix} 7\\ 3\\ 11 \end{pmatrix}\pmod{26}=\begin{pmatrix} H, D, L \end{pmatrix}^{T}

因此密文就是HDL。解密时只要用PK1C(mod26)\mathbf{P}\equiv\mathbf{K}^{-1}\mathbf{C}\pmod{26}即可得到明文P\mathbf{P}的矩阵。

多表置换密码

在明文消息中采用不同的单表置换,通过使各个字母的频率分布更平坦来提高安全性。

现在考虑下面两个例子。

Vigenère密码)用密钥“MYKEY”加密明文“encode and decode”。

显然根据背景知道明文编码P=(4,13,2,14,3,4,0,13,3,3,4,2,14,3,4)T,\mathbf{P}=(4, 13, 2, 14, 3, 4, 0, 13, 3, 3, 4, 2, 14, 3, 4)^T,密钥编码k=(12,24,10,4,24)T.\mathbf{k}=(12, 24, 10, 4, 24)^T.因为明文长度大于密钥长度,所以将密钥通过重复循环延长至15位,即K=(12,24,10,4,24,12,24,10,4,24,12,24,10,4,24)T.\mathbf{K}=(12, 24, 10, 4, 24, 12, 24, 10, 4, 24, 12, 24, 10, 4, 24)^T.随后进行加密运算

C=P+K=(16,37,12,18,27,16,24,23,7,27,16,26,24,7,28)T(16,11,12,18,1,16,24,23,7,1,16,0,24,7,2)T(mod26),\begin{align*} \mathbf{C}=\mathbf{P}+\mathbf{K}&=(16, 37, 12, 18, 27, 16, 24, 23, 7, 27, 16, 26, 24, 7, 28)^T\\ &\equiv(16, 11, 12, 18, 1, 16, 24, 23, 7, 1, 16, 0, 24, 7, 2)^T\pmod{26}, \end{align*}

转码得到密文“QLMSBQYXHBQAYHC”。解密时用等式PCK(mod26)\textbf{P}\equiv \textbf{C}-\textbf{K}\pmod{26}即可得到明文的编码。

置换加密

通过某种置换改变明文字母的顺序来完成加密。一般采用的是一种“按波浪线顺序写出明文,以行的顺序写出密文”的栅栏技术;这种加密方式非常容易被破译。但是,通过多次置换后这个密码的安全性就会有非常大的提升。

现在考虑以下例子。

对明文“meet me after the toga party”依照置换加密方法进行1次加密。

将明文中的奇数位和偶数位分别拆开,得到

1
2
m e m a t r h t g p r y    奇数位
e t e f e t e p a a t 偶数位

因此经过一次置换加密得到的密文就是“MEMATRHTGPRYETEFETEOAAT”。

一次一密

随机生成每一次的密钥,理论上完全不可破解,但实际上也完全不可行。这是因为,产生大量的随机密钥是一件非常困难的事,这些密钥的分配和保护工作则让这个工作的难度进一步提高。

分组密码与DES

分组密码

将一个明文组作为整体进行加密,得到的密文组一般与明文等长。其作用于n位明文分组上,产生的自然是n位密文分组;因此分组过小时很容易被统计分析方法攻破。当分组规模相当大的时候明文的统计消息会被掩盖,密码也可以任意选择变换构造,因此这些密码成为“理想分组密码”。但是理想分组密码的密钥规模过大,显然不能在实际中广泛应用。

密码系统的性能可以通过混淆和扩散两个方面来衡量。前者的关键在于使密文的统计特性与密钥之间的关系尽量复杂,后者的关键在于使明文的统计特征消散在密文中(即将明密文之间的统计关系最大程度地复杂化)。因此,引入了Feistel加解密。

Feistel解密过程的结构与加密一致。若将第ii次加密后的明文分为LefttoEncrypt,i{Left}_{toEncrypt,i}RighttoEncrypt,i{Right}_{toEncrypt,i}这左右两半,则对其再次利用流加密函数F(text,key)F(\text{text}, \text{key})进行多轮逐位加密时可以按下方顺序持续循环:

LefttoEncrypt,i+1=RighttoEncrypt,i\text{Left}_{\text{toEncrypt},i+1}=\text{Right}_{\text{toEncrypt},i}

RighttoEncrypt,i+1=LefttoEncrypt,iF(RighttoEncrypt,i,Ki+1)\text{Right}_{\text{toEncrypt},i+1}=\text{Left}_{\text{toEncrypt},i}\oplus F(\text{Right}_{\text{toEncrypt},i}, K_{i+1})

相对应地,若解密过程已经进入第jj轮,那么此时可以按下方运算继续进行解密:

LefttoDecrypt,j+1=RighttoDecrypt,j\text{Left}_{\text{toDecrypt},j+1}=\text{Right}_{\text{toDecrypt}, j}

RighttoDecrypt,j+1=LefttoDecrypt,jF(RighttoDecrypt,j,Kj)\text{Right}_{\text{toDecrypt},j+1}=\text{Left}_{\text{toDecrypt}, j}\oplus F(\text{Right}_{\text{toDecrypt}, j},K_j)

而显然这种加解密过程都是需要知道加密次数的。假设加密过程一共有nn轮,那么必然有

RighttoEncrypt,i=LefttoEncrypt,i+1=RighttoDecrypt,ni1=LefttoDecrypt,ni\text{Right}_{\text{toEncrypt},i}=\text{Left}_{\text{toEncrypt},i+1}=\text{Right}_{\text{toDecrypt}, n-i-1}=\text{Left}_{\text{toDecrypt}, n-i}

换言之,某次加密后的明文左右两部分交换之后左半部分会等于此次变换之前的右半部分,也会等于从反方向解密而来的密文的左半部分或其此次解密之前的右半部分。除此之外还有

RighttoDecrypt,i+1=LefttoDecrypt,iF(RighttoDecrypt,i,Kni),\text{Right}_{\text{toDecrypt},i+1}=\text{Left}_{\text{toDecrypt}, i}\oplus F(\text{Right}_{\text{toDecrypt}, i}, K_{n-i}),

在代入为加密过程中的值后可以得到

RighttoDecrypt,i+1=LefttoEncrypt,ni1F(RighttoEncrypt,ni1,Kni).\text{Right}_{\text{toDecrypt}, i+1}=\text{Left}_{\text{toEncrypt}, n-i-1}\oplus F(\text{Right}_{\text{toEncrypt}, n-i-1},K_{n-i}).

因此,Feistel结构中的第ii轮解密后的左右两半一定与加密过程中第nin-i轮后的一致。

DES通过56位密钥来加密64位的数据。其会经过初始置换和初始逆置换两次置换,随后在生成的轮密钥控制下进行16轮基于Feistel的迭代加密。DES加密算法除了S盒之外的算法均为线性,而S盒提供了密码算法所必须的混乱作用。S盒的分析难度为这套加密算法提供了非常高的安全性。

明文或者密钥中哪怕只变化一位都会导致密文中很多位都发生变化;这种效应也称为「雪崩avalanche效应」。

代数基础

基于离散数学关于逆元、群、交换群、环、可交换环、整环和域的知识继续讨论。

Euclid算法

一种通过递归来实现的最大公约数算法。下面给出通过C语言实现的一种Euclid算法函数模板,就能明白其原理。

1
2
3
int Euclid(int a, int b){
return a % b == 0 ? b : Euclid(b, a % b);
}

如果aabb的最大公约数是1,那么一定存在cc使ac1(modb)ac\equiv1\pmod{b}。这个cc也称为aabb的乘法逆元。

现在给出一个利用Euclid算法计算最大公约数的例子。

计算1970与1066的最大公约数。

用Euclid算法的求法如下:

1970=1×1066+9041066=1×904+162904=5×162+9410=1×6+46=1×4+24=2×2\begin{align*} & 1970 &= &1\times 1066+904\\ \Rightarrow & 1066 &= &1 \times 904+162\\ \Rightarrow & 904 &= &5\times 162+94\\ \Rightarrow & & &\dots \\ \Rightarrow & 10&=&1\times6+4\\ \Rightarrow & 6 &= &1\times 4+2\\ \Rightarrow & 4 &= &2\times 2 \end{align*}

因此这两个数的最大公约数是2。

现在考虑如何利用Euclid算法求取余运算的逆元。

求81 mod 28的逆元。

首先对81和28用Euclid方法作如下处理:

81=2×28+2528=1×25+325=8×3+13=3×1\begin{align*} & 81 & = & 2\times 28+25\\ \Rightarrow &28 & = &1\times 25+3\\ \Rightarrow & 25 & = & 8\times 3+1\\ \Rightarrow & 3 & = & 3\times 1 \end{align*}

因此这两个数互质。另一方面,

1=258×3=258×(2825)=(8)×28+9×(812×28)=9×81+(26)×28,\begin{align*} 1 &= 25-8\times 3\\ &=25-8\times(28-25)\\ &=(-8)\times 28 +9\times(81-2\times 28)\\ &=9\times 81+(-26)\times 28, \end{align*}

因此81×91(mod28).81\times 9\equiv 1\pmod{28}.所以99就是81 mod 28的逆元;用这个等式还可以得到2655(mod81)-26\equiv 55\pmod{81},对应的5555是28 mod 81的逆元。

多项式运算

如果系数在Zp={0,1,...,p1}\mathbb{Z}_p=\{0, 1, ..., p-1\}上且次数小于等于m1m-1的所有多项式f(x)=i=0m1aixif(x)=\sum_{i=0}^{m-1}a_ix^i共同组成一个集合SS,那么这个拥有pmp^m个不同多项式的集合就称为代数系统载体GF(pm)GF(p^m)

现在考虑GF(2m)GF(2^m)。对于一个mm确定的有限域,多项式的乘法在完成了简单的乘法后需要对次数不少于mm的各项对m(x)m(x)取模。下面以p=3p=3为例进行简单的解释。

GF(23).

首先结合定义可以知道这个集合里应该是所有系数在Z2\mathbb{Z}_2中且次数不大于22的多项式。显然可以发现x3+x+1x^3+x+1不能由这个集合里的任何两个多项式相乘得到,因此这个式子就可以作为不可约多项式m(x)m(x)

现在以f(x)=x2+x+1f(x)=x^2+x+1g(x)=xg(x)=x为例讨论这个集合上的多项式加、乘法。显然

f(x)+g(x)=x2+x+1+x=x2+2x+1,f(x)+g(x)=x^2+x+1+x=x^2+2x+1,

但此时一次项的系数不在Z2\mathbb{Z}_2内,因此实际上

f(x)+g(x)=x2+0x+1=x2+1.f(x)+g(x)=x^2+0x+1=x^2+1.

现在再考虑乘法。

f(x)g(x)=(f(x)g(x))(modm(x))=(x3+x2+x)(modx3+x+1)=(1×(x3+x+1)+(x21))(modx3+x+1)x21(modm(x)),\begin{align*} f(x)g(x)&=(f(x)g(x)) \pmod{m(x)}\\ &=(x^3+x^2+x)\pmod{x^3+x+1}\\ &= (1\times(x^3+x+1)+(x^2-1))\pmod{x^3+x+1}\\ &\equiv x^2-1\pmod{m(x)}, \end{align*}

1-1不在Z2\mathbb{Z}_2中,所以常数项循环至11;因此知道乘法的结果是x2+1x^2+1

现在考虑f(x)modm(x)f(x)\mod m(x)的逆元。显然刚刚已经知道

m(x)=x3+x+1=xf(x)+x2+1,m(x)=x^3+x+1=xf(x)+x^2+1,

故而只要求x2+1x^2+1x2+x+1x^2+x+1的最大公约数即可。

x2+x+1=1×(x2+1)+xx2+1=xx+1x=x1\begin{align*} &x^2+x+1&=&1\times(x^2+1)+x\\ \Rightarrow &x^2+1&= &x\cdot x+1\\ \Rightarrow & x &= & x\cdot 1 \end{align*}

因此m(x)m(x)f(x)f(x)互质。而由上方分析又可知道

1=x2+1xx=x2+1x((x2+x+1)1×(x2+1))=(1+x)(x2+1)x(x2+x+1)=(1+x)((x3+x+1)x(x2+x+1))x(x2+x+1)=(1+x)(x3+x+1)x2(x2+x+1)\begin{align*} 1 & = & x^2+1-x\cdot \underline{x}\\ & = & x^2+1-x\cdot((x^2+x+1)-1\times(x^2+1))\\ & = & (1+x)\underline{(x^2+1)}-x\cdot(x^2+x+1)\\ & = & (1+x)((x^3+x+1)-x(x^2+x+1))-x(x^2+x+1)\\ &= & (1+x)(x^3+x+1)-x^2(x^2+x+1) \end{align*}

其中划线的对应需要根据之前求公约数时的等式展开的项。因此

1(x2)(x2+x+1)(modm(x)).1\equiv (-x^2)(x^2+x+1)\pmod{m(x)}.

1-1不是Z2\mathbb{Z}_2中的项,所以循环移动到11,对应项也要改为x2x^2。因此逆元是x2(x2+x+1)x^2(x^2+x+1)

AES加密

采用宽轨迹策略来抵抗差分分析和线性分析。

轮函数分别经过字节代替(利用S盒中的映射将输入转换为输出)、行移位(对输入状态矩阵的每一行循环左移一定字节)、列混淆(对输入状态矩阵的每一列左乘一个列矩阵)和轮密钥加(将输入和轮密钥异或得到输出)这四个步骤。其中S盒可逆,所属字节代替步骤是AES加密中唯一的非线性运算;最后一轮的加密没有列混淆操作

分组密码工作原理

中间相遇攻击

考虑一个双重DES加密。顾名思义有加密公式C=Ek2(Ek1(P))C=E_{k_2}(E_{k_1}(P))和解密公式P=Dk1(Dk2(P))P=D_{k_1}(D_{k_2}(P)),密钥长度位112位;那么这两次DES加密的中间产物XX显然与Ek1(P)E_{k1}(P)Dk2(C)D_{k2}(C)都相等。这时运用所有可能的密钥分别尝试加密明文和解密密文,并分别与XX比较;若相等则获得了一组可能的密钥对。这时尝试其他新的明密文组合进行进一步验证;根据概率论,在此条件下与第二组明密文对进行比较后筛选剩下的密钥正确的概率是1-2-16;再比较一组进行筛选后所得的密钥正确的概率上升到了1-2-80。以此类推,找到正确密钥的时间复杂度仅为O(256)O(2^{56}),而单次DES加密找到正确的密钥对应的时间复杂度也不过O(255)O(2^{55})

事实上,中间相遇攻击并不依赖于DES的性质。DES只是一个典型的分组密码;中间相遇攻击对所有的分组密码都适用。

多重DES加密

现在考虑三重两密、三重三密、五重三密这三种常见多重DES加密的思路。

三重两密

处理明文时涉及两个加密密钥,采用C=Ek1(Dk2(Ek1(P)))C=E_{k_1}(D_{k_2}(E_{k_1}(P)))的思路进行加密。

这个思路可以适应单DES——当且仅当k1=k2k_1=k_2时这个三重加密算法和一重加密一致。目前,这个思路还没有可行的攻击方法。

三重三密

处理明文时涉及三个加密密钥,每次处理所用的密钥都不同。思路本质上是C=Ek3(Dk2(Ek1(P)))C=E_{k_3}(D_{k_2}(E_{k_1}(P))).

这个思路的应用比较多——PGP和S/MINE都使用这种多重DES加密思路。

五重三密

处理明文时涉及三个加密密钥,遵循等式C=Ek1(Dk2(Ek3(Dk2(Ek1(P)))))C=E_{k_1}(D_{k_2}(E_{k_3}(D_{k_2}(E_{k_1}(P)))))

这种思路可以适应三重两密的情形,因此也可以适应单DES。

分组密码工作模式

DES加密在明文的最后一个片段不足分组长度时会补上0/1/随机串。

电码本(ECB)

经常应用于单个数据的安全传输。

Cj=Ek(Pj)Pj=Dk(Cj)C_j=E_k(P_j)\Rightarrow P_j=D_k(C_j)

结合加解密公式可以知道这种模式下密文中某位出错后只会影响这一位明文的恢复,所以这种加密算法不会传播错误

密文分组链接(CBC)

在对长度大于64位的明文进行加密时会考虑使用。

Cj=Ek(Cj1Pj),Pj=Cj1Dk(Cj);C0=Initial VectorC_j=E_k(C_{j-1}\oplus P_j), P_j=C_{j-1}\oplus D_k(C_j); C_0=\text{Initial Vector}

由于加解密会涉及到相邻的位,所以如果密文传输过程中有一位出错会导致明文中它的后一位也会出错,进而造成多米诺效应。因此,这种加密算法会传播错误

密文反馈(CFB)

加密长度大于64位的明文时会考虑使用。这种工作模式比较保密且分组随意,可以作为流密码使用。

这种模式的错误传播比CBC更厉害。当密文的第tt位在传输过程中出错后,其后的64s\lceil \frac{64}{s} \rceil位都会收到影响。其中,ss是明密文的位数。

输出反馈(OFB)

噪声信道上传输数据流(如卫星通信等)常用的工作模式。这种工作模式抗攻击能力不如CFB,但是错误不传播

计数器模式(CTR)

用于分组高速传输。这种工作模式可以并行处理加密,硬件效率非常高,加之整个加解密过程只涉及加密函数而不涉及解密函数,这个工作模式是非常理想的。

Cj=PjEk(IV+j1),Pj=CjEk(IV+j1)C_j=P_j\oplus E_k(IV+j-1), P_j=C_j\oplus E_k(IV+j-1)

其中IVIV是计数器初值。这个模式的错误不传播。

随机数和流密码

密码学意义上的“随机”指的是不能预测。一般还有“真随机”(不能重复产生)和“伪随机”(看上去随机且能通过一部分随机性测试)两种随机;密码学中应用的随机数至少需要是强伪随机数,即满足随机性和不可预测性

当一个伪随机数生成器的均匀性(0和1出现率大致相等)、可伸缩性(任何子序列都能通过随机性测试)和一致性(对所有种子产生的序列和这些序列的子序列都应该通过随机性测试)较好时,这个发生器较好。

已知一个周期为pp的序列{xn}\{x_n\},当存在i0i_0使任意ii0i\geq i_0都有xi=xi+px_i=x_{i+p}时这个序列也是终归周期序列。

伪随机数发生器

将一个短的随机数“种子”扩展为较长伪随机序列的确定算法称为伪随机数发生器。根据原理分为“基于数论方法的”和“用加密方式的”两种;前者包括线性同余发生器和BBS发生器,后者包括对称和非对称加密算法及Hash函数与消息认证码。

RC4流密码

一种面向字节操作的流密码,密钥长度可变;广泛应用于Web SSL、TLS、Wi-Fi WPA、IEEE 802.11 WEP等现实场景。其中消息的处理以字节为单位,以流为进行方式,使用伪随机密钥流与明文进行按位异或运算。重复使用这个密钥流会导致被破译。

数论基础

费马小定理

pp是不能整除aa的素数,那么ap11(modp).a^{p-1}\equiv 1\pmod{p}.

这句话也可以理解成“对正整数aa和质数pp一定有apa(modp)a^p\equiv a\pmod{p}。这个结论可以大幅度简化求余运算,但有自身的局限性——当且仅当pp为质数时才能用此定理。

欧拉定理

Φ(n)\Phi(n)为小于nn且与nn互质的正整数个数,那么Φ(n)\Phi(n)称为欧拉函数。

欧拉函数有如下性质:

  1. Φ(1)=1\Phi(1)=1
  2. nn为素数时Φ(n)=n1\Phi(n)=n-1
  3. Φ(2k)=2k1(k>1)\Phi(2^k)=2^{k-1}(k>1)
  4. n=pqn=pqppqq均为质数时Φ(n)=Φ(p)Φ(q)=(p1)(q1)\Phi(n)=\Phi(p)\Phi(q)=(p-1)(q-1)
  5. n=i=1tpiain=\prod_{i=1}^tp_i^{a_i},那么Φ(n)=i=1t(piai1(pi1))\Phi(n)=\prod_{i=1}^t(p_i^{a_i-1}(p_i-1))

欧拉定理:若aann互质,则aΦ(n)1(modn).a^{\Phi(n)}\equiv1\pmod{n}.

欧拉定理凭借模数可以为非质数的优势比费马小定理更加通用。

求2100 mod 35。

显然因为模数35不为质数所以无法使用费马小定理。现在记n=35,a=2n=35, a=2,那么Φ(n)=Φ(5)Φ(7)=24\Phi(n)=\Phi(5)\Phi(7)=24。因此

2100mod35=224×4+4mod35=24Φ(35)+4mod35=24×(2Φ(35))4mod3516×14mod35=16.\begin{align*} 2^{100}\mod 35 &= 2^{24\times 4+4}\mod 35 &= 2^{4\Phi(35)+4}\mod 35\\ &=2^4\times(2^{\Phi(35)})^4\mod 35&\equiv 16\times 1^4\mod 35 &=16. \end{align*}

离散对数

根据欧拉定理一望而知,任意aa都存在对应的m=Φ(n)m=\Phi(n),使am1(modn)a^m\equiv 1\pmod{n}。这样的mm的最小值称为aa的阶(或aa关于模nn运算的指数或周期)。

aa的阶为Φ(n)\Phi(n),则称aann的本原根。对于一个质数pp的本原根aa,显然amodp,a2modp,,ap1modpa\mod p, a^2\mod p, \dots, a^{p-1} \mod p的值各不相同。

若任意整数bb和模数pp的本原根aa有唯一的指数ii使bai(modp)(i[0,p1])b\equiv a^i\pmod{p}(i\in[0,p-1]),则称ii为以aa为底、模为ppbb的离散对数,记作dloga,pb\text{d}\log_{a,p}b

公钥密码学·RSA

一个拥有n个用户的系统两两之间都需要利用对称加密进行保密通信,则显然需要C2n个密钥,每个用户有n-1个密钥。

显然对称密码的弱点在于密钥量大且管理困难。网络中的签名、认证等应用也无法通过对称加密实现;因此考虑非对称密码。

公钥密码通信的过程主要依靠加密和解密两个不同的密钥来完成。收信人Alice在设计两个密钥之后将加密密钥公开给发信人Bob,后者利用这个加密密钥将自己的信息转为密文,随后发送给Alice;Alice收到密文后利用自己所保存的解密密钥进行解密,得到信息明文。

在这个过程中,最关键的部分其实是密钥的设计;RSA非对称加密根据欧拉函数,利用2个大素数就可以完成。Alice先选择两个大素数ppqq,之后得到n=pqn=pqΦ(n)=(p1)(q1)\Phi(n)=(p-1)(q-1);随后选择一个与Φ(n)\Phi(n)互质的数ee作为加密密钥,计算de1(modΦ(n))d\equiv e^{-1}\pmod{\Phi(n)}作为解密密钥。随后Alice会将nnee公开给Bob,自己留存dd;Bob遵从等式Cme(modn)C\equiv m^e\pmod{n}进行加密的同时Alice遵从mcd(modn)m\equiv c^d\pmod{n}即可实现成功的加解密。

公钥密码学·其他体制

Diffie-Hellman密钥交换

现有大素数pp和模数pp的本原根gg公开在世界。那么手握随机数xAx_A的Alice可以经过计算得到公钥yA=gxAmodpy_A=g^{x_A}\mod p并将其发送给手握随机数xBx_B的Bob;Bob也可以通过一样的手段将yB=gxBmodpy_B=g^{x_B}\mod p发送给Alice。此时两人分别计算密钥值kA=yBxAmodpk_A={y_B}^{x_A}\mod pkB=yAxBmodpk_B={y_A}^{x_B}\mod p;显然两人得到的密钥kk相等且均为gxAxBmodpg^{x_Ax_B}\mod p。这时,两人完成了一次成功的密钥交换。

现在假设一个人在Alice和Bob中间。它在截获了yAy_AyBy_B之后可以经过任意的篡改让Alice和Bob同时收到相同的信息yM=gxMmodpy_M=g^{x_M}\mod p;对应会导致两人得到的密钥分别是gxAxMmodpg^{x_Ax_M}\mod pgxBxMmodpg^{x_Bx_M}\mod p,可能不相等。这次中间人攻击之所以能够实现正是因为Diffie-Hellman密钥交换中没有提供密钥认证功能

ElGamal密码

现有大素数pp和模数pp的本原根gg公开在世界。这时选择随机数xx并计算y=gxmodpy=g^x\mod p并公开yy,那么加密期间可以通过随机选择一个与p1p-1互质的整数kk计算c1=gkmodpc_1=g^k\mod pc2=mykmodpc_2=my^k\mod p得到密文c=(c1,c2)c=(c_1,c_2);解密过程则可以利用自己保管的xx根据密文c=(c1,c2)c=(c_1,c_2)计算m=c2(c1x)1modpm=c_2(c_1^x)^{-1}\mod p得到明文mm

Hash函数

对任意长度明文进行变换所用的一个函数。变换后的函数值也称为散列值,其长度一般固定。

Hash函数一般用于消息认证码、数字签名的预处理和口令衍生密钥等领域,其无法求逆且对给定xx很难找到使h(x)=h(x)h(x')=h(x)成立的xxx'≠x,也很难找到任意一对xxxx'使h(x)=h(x)h(x')=h(x)。换言之,Hash函数同时具备抗原像攻击性(单向性)、抗弱碰撞性和抗强碰撞性

(生日悖论)一个房间内有k人,当k多大时其中存在两人有相同生日的概率大于0.5?

kk个人的生日排列总数一共有365k365^k个,有不同生日的排列总数为A365kA_{365}^k。因此需要使1A365k365k>0.51-\frac{A_{365}^k}{365^k}>0.5,解得k23k\geq23

用上述方法也可以得到当k=100k=100时有两人同生日的概率为0.99999997,已经很接近必然事件了。类似地,Yuval提出了对Hash函数的生日攻击方案。攻击者产生一份合法明文后产生2322^{32}个不同的变形,并将其作为一个合法的明文组。随后其生成一份非法明文按上述方式产生一个非法明文组;分别对以上两个组产生散列值,在两组明文中找出散列值相同的一对。这时攻击者就找到了与合法明文具有相同散列值的非法明文;如果没有找到可以在增加每组明文的变形个数,直到找到。显然根据刚刚“生日悖论”的结论这很难不成功。

消息认证码

单纯的加解密算法是无法保证能够抵抗伪装、内容修改、顺序修改、计时修改和收发方否认这五种攻击的。这些攻击本质上属于数据完整性范畴而不是机密性范畴;对于前四个可以用消息认证码(MAC)来解决。

MAC本质上是密码校验和,信息的长度可变,但是密钥需要保密且收发双方共享。这种认证码长度固定;很多消息会对应同一个MAC值,但找到这个MAC值所对应的多个消息很困难。

一般通过函数MAC=CK(M)=C(K,M)\text{MAC}=C_K(M)=C(K,M)对消息MM运用密钥KK进行加密。有时加密算法会改为Hash函数,因为Hash函数较快且没有出口限制。这种基于密码的Hash函数的消息认证码称为HMAC;由于hash函数并不依赖密钥,所以不能直接用在MAC上

数字签名

一般基于RSA。在利用私钥dd计算完s=mdmodns=m^d\mod n后发送(m,s)(m,s)给收信方,收信人会利用公钥ee计算m=semodnm'=s^e\mod nmm是否相等。如果不等于就意味着信息有误,因此会被拒绝。需要注意的是如果攻击者有能力进行模nn的大整数分解,那么它很容易就能计算Φ(n)\Phi(n)并且进而利用Euclid算法得到发信者签名时所用的私钥。因此,发信人要谨慎选取ppqq

认证协议

重放攻击

把有效的签名信息拷贝后重新发送给收信方。

针对这种攻击方式可以通过时间戳、序列号和随机数的方式来解决。其中后者目前最为常用,时间戳则需要同步时钟。

Needham-Schroeder协议

有第三方(KDC)作为中介参与的密钥分发协议。协议包括A到第三方、第三方到A、A到B、B到A和A到B一共五个步骤。

AKDC:IDAIDBN1KDCA:EKa[KsIDBN1EKb[KsIDA]]AB:EKb[KsIDA]BA:EKs[N2]AB:EKs[f(N2)]\begin{align*} \text{A}&\rightarrow &\text{KDC}: & ID_A || ID_B || N_1\\ \text{KDC}&\rightarrow &\text{A}: &E_{Ka}[K_s||ID_B||N_1||E_{Kb}[K_s||ID_A]]\\ \text{A}&\rightarrow &\text{B}: & E_{Kb}[K_s||ID_A]\\ \text{B}&\rightarrow &\text{A}: & E_{Ks}[N_2]\\ \text{A}&\rightarrow &\text{B}: & E_{Ks}[f(N_2)] \end{align*}

Denning AS协议

有第三方(AS)作为中介参与的密钥分发协议。协议包括A到第三方、第三方到A、A到B三个步骤;会话密钥由A选择所以不会被AS泄密。这个协议需要依靠时间戳来防止重放攻击,因此需要时钟同步或者临时交互号。

AAS:IDAIDBASA:EPRas[IDAPUaT]EPRas[IDBPUbT]AB:EPRas[IDAPUaT]EPRas[IDBPUbT]EPUb[EPRa[KsT]]\begin{align*} \text{A}&\rightarrow &\text{AS}: & ID_A || ID_B\\ \text{AS}&\rightarrow &\text{A}: &E_{PRas}[ID_A||PU_a||T]||E_{PRas}[ID_B||PU_b||T]\\ \text{A}&\rightarrow &\text{B}: & E_{PRas}[ID_A||PU_a||T]||E_{PRas}[ID_B||PU_b||T]||E_{PUb}[E_{PRa}[K_s||T]] \end{align*}

Kerberos

分为认证服务器(AS)和票据授权服务器(TGS)两个服务器。前者在初始时与用户对话,使用户验证身份,随后发放高度可信的「票据授权票据」(TGT);用户持TGT才能在后者得到访问其他应用服务的票据。

票据除了TGT之外还有「服务授权票据」(SGT)。TGT是客户访问TGS服务器时所需要提供的票据,目的是为了申请某一个应用服务器的SGT;SGT在客户请求服务时由服务器提供。访问TGS服务器的票据(Tickettgs)在用户登录时向AS申请一次即可,可以多次重复使用。

在Kerberos的身份验证过程总共3步。客户端与身份认证服务器先进行互相的身份验证,随后客户与票据授权服务器进行互相的身份验证。之后客户端与服务器进行互相的身份验证,为客户端提供服务。

传输层安全

SSL协议

一种在两个端实体之间提供安全通道的协议。其提供了通道级别的安全——这两端知道所传输的数据保密且没有被篡改

SSL主要包括记录和握手两个主要协议。前者建立在可靠的传输协议(如TCP等)之上来保证保密性和完整性,并用来封装高层协议;后者用于客户和服务器之间的相互认证与有关加密算法和密钥的协商,提供了连接的安全性。SSL记录协议传输的是安全记录,将数据流分割成了若干个片段并单独进行保护与输送;SSL握手协议则会在完成安全相关协商后交换证书和相应密码信息以便身份认证。产生主密钥后其会将安全参数提供给SSL记录层,检验双方是否已经获得了同样的安全参数。

SSL握手

主要包括建立安全协商、服务器认证和密钥交换、客户端认证和密钥交换和完成四个步骤。

在第一个阶段,客户会发送包括协议版本、会话ID、客户支持的密码算法列表、客户支持的压缩方法列表、初始随机数(32位时间戳+28字节随机序列)在内的client_hello信息;作为反馈,服务器会发送包括客户端建议的低版本以及服务器支持的最高版本、会话ID、客户支持列表中选出的密码算法和压缩方法、服务器产生的随机数在内的server_hello信息。

在第二个阶段,服务器会发送一个含有X.509证书(或一条证书链)的certificate信息,之后选择性地发送server_key_exchangecertificate_request,最后发送server_hello_done等待客户端应答。certificate除了匿名Diffie-Hellman密钥交换之外的其他交换方法都需要提供;server_key_exchange在证书消息没有包含必需的签名、被签名内容中的随机数或服务器参数时才会发送。

在第三个阶段,客户开始验证服务器证书。其将检查server_hello消息参数是否可接受;若无问题则发送至少一条消息给服务器。如果服务器请求证书,则客户首先发送certificate消息;若客户没有证书,则发送无证书警报。在这之后客户根据密钥交换的类型发送client_key_exchange消息,最后选择性地发送包含一个签名的certificate_verify消息。

在第四个阶段,客户发送change_cipher_spec消息,将协商得到的密码规范复制到当前连接的状态,随后用新协商的算法和密钥参数发送finished消息,可以验证密钥交换和认证过程是否成功。其中包括的校验值会对所有握手消息进行校验。服务器同样会发送这两个名字的消息;至此握手过程完成,客户和服务器自此可以交换应用层数据。

IPSec

IP安全性在协议部分分为认证头(AH)和封装安全性有效载荷(ESP)。密钥管理部分有SA、ISAKMP和IKE三个;ISAKMP定义了密钥管理框架,IKE是目前正式确定用于IPSec的密钥交换协议。

IPSec在IPv6中强制,在IPv4中可选

AH和ESP都同时支持传输模式和隧道模式;这两个模式中前者为上层协议提供保护,后者保护整个IP包。AH是认证的扩展报头,ESP是实现加密(也可选择认证)的扩展报头。

为了结合认证和机密性,目前主流采用的是传输邻接和传输隧道束两种组合。前者使用两个捆绑的传输SA,内部是没有认证选项的ESP SA,外部是AH SA;后者的组成为内部AH的传输SA+外部ESP的传输SA,在加密之前就进行了认证,更加可取。

ISAKMP定义了建立、维护、删除安全关联(SA)的过程和包格式,也定义了生成交换密钥的载荷和认证数据。其独立于特定密钥交换协议、加密算法、认证机制。作为一个针对认证和密钥交换的框架,其协商分为建立ISAKMP SA和针对其他安全协议的SA两个阶段。

实验报告

实验一 古典密码实验

实验目的:了解并掌握Caesar密码、仿射密码和单表置换密码的加解密原理。

Caesar密码

自选语言设计Caesar算法,并能任意指定英文字母组合和密钥对前者进行加密。

实验原理

Caesar加密/解密是一种替换加密技术,明文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换成密文。例如,当偏移量是3的时候,明密文对照表可以如下所示:

明文ABCDEFGHIJKLMNOPQRSTUVWXYZ
密文DEFGHIJKLMNOPQRSTUVWXYZABC
这种加密方式非常容易受到选取语言的制约。例如,当对英语体系进行加密时,选择的偏移量显然不能超过25。因此,即使使用唯密文攻击,凯撒密码也是一种非常容易破解的加密方式。
实验代码与运行记录
加密算法

显然根据实验原理中所描述的定义和思路不难得到凯撒密码的加密算法。为了区分英文字母的大小写,大写字母的加密结果会变为小写,而小写则会被加密为大写。

算法中暗含了一个隐形的“明密文对照表”。英文Caesar密码的解密过程本质上是将密文进行新一轮加密,只是发生了反向的偏移。换言之,加密的偏移量kek_e和解密时的偏移量kdk_d在处理英文文本时显然有kd=26kek_d=26-k_e。利用此即可通过语言kotlin得到如下所示的算法代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private fun caesarCipher(text:String, key:Int, decryptMode:Boolean): String {
val newKey=when(decryptMode){
true -> 26-key
else -> key
}
val textChars=text.toCharArray()
val ansChars=CharArray(textChars.size)
for(i in textChars.indices){
if(textChars[i] in 'A'..'Z'){
ansChars[i] = ((textChars[i].code-'A'.code+newKey)%26+'a'.code).toChar()
}
else if(textChars[i] in 'a'..'z'){
ansChars[i] = ((textChars[i].code-'a'.code+newKey)%26+'A'.code).toChar()
}
else{
ansChars[i]=textChars[i]
}
}
return String(ansChars)
}
运行结果

对该算法代码设计主程序的输入、输出。随后运行该程序;以加密时偏移量为5,并对文本“Hello”进行加密的情况为例,运行结果如下所示。

显然解密结果与原本的明文一致。

仿射密码

自选语言设计算法,使该算法能够对任意英文字母组合和密钥进行仿射加密。

实验原理

仿射密码本质上与Caesar密码类似,也是一种替代加密技术。对总字母数量为mm的语言体系中的任意字母xx,其加密后的结果e(x)e(x)满足e(x)=ax+b(modm)e(x)=ax+b\pmod{m},其中aa是与mm互质的任意值,bb是任意值。

与加密函数相对应地,解密函数为d(x)=a1(xb)(modm)d(x)=a^{-1}(x-b)\pmod{m},其中a1a^{-1}aa的一个取模倒数。

显然可以通过等式

D(E(x))=a1(E(x)b)modm=a1(((ax+b)modm)b)modm=a1(ax+bb)modm=a1axmodm=xmodm.\begin{align} D(E(x)) &= a^{-1}(E(x)-b)\mod{m}\\ &= a^{-1}(((ax+b)\mod{m})-b)\mod{m} \\ &= a^{-1}(ax+b-b)\mod{m} \\ &= a^{-1}ax \mod{m}\\ &= x\mod{m}. \end{align}

证明加密结果的解密结果就是原文本身。

实验代码与运行记录
算法代码

根据上方定义不难得到仿射加解密的算法。在讨论算法之前,首先要对算法中等式的一部分参数进行设计。

首先要确保aamm互质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private fun isPrior(a:Int,b:Int):Boolean{
if(a==1||b==1) return true
var realA=a
var realB=b
while(true)
{
val t = realA % realB
if(t == 0)
{
break
}
else
{
realA = realB
realB = t
}
}
return realB <= 1
}

然后生成aa的取模倒数a1a^{-1};此值应能确保与aa的乘积对mm的求余结果为1。

1
2
3
4
5
6
7
8
9
10
11
12
private fun inverseGenerator(a:Int,m:Int):Int{
var result=0
var i=0
do{
i++
if(a*i%m==1){
result=i
break
}
}while(true)
return result
}

这时考虑加密。由于是英文字母分大小写总共52个,故不妨令m=53m=53。在这之后先生成一个暗含的明密文对照表cipherSheet,再根据此表通过ReferenceForIndex对每一个给定的字符查找到该字符在表中所对应的索引;类似地,再通过cipherSheetReferenceForChar可以找到特定索引号在表内所对应的字符。之后根据上述所有函数利用定义即可用kotlin写出算法代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private val cipherSheet=CharArray(55)

private fun cipherSheetReferenceForIndex(c:Char):Int{
if(c in 'a'..'z') return c.code-'a'.code+1
if(c in 'A'..'Z') return c.code-'A'.code+27
return -1
}
private fun cipherSheetReferenceForChar(i:Int):Char{
return cipherSheet[i]
}
private fun affineCipher(text:String,a:Int,b:Int,m:Int,decryptMode:Boolean):String{
for(i in 0 until 26){
cipherSheet[i+1]=(i+'a'.code).toChar()
cipherSheet[i+27]=(i+'A'.code).toChar()
}
val textChars=text.toCharArray()
val ansChars=CharArray(textChars.size)
for(i in text.indices){
val n = cipherSheetReferenceForIndex(text[i])
if(n!=-1){
val index = when(decryptMode) {
true->(inverseGenerator(a,m) * (n - b + m)) % m
else->(a*n+b)%m
}
ansChars[i]=cipherSheetReferenceForChar(index)
}
else{
ansChars[i]=textChars[i]
}
}
return String(ansChars)
}
运行结果

根据算法代码设计输入输出,并运行该程序。以利用a=91,b=2a=91,b=2对明文“Hello”进行加解密为例,得到的结果如下所示:

可以看到,解密的结果与原本的明文一致。

单表置换加密

自选语言设计算法,使该算法可以对文本进行单表置换加解密。

实验原理

单表置换加密本质上是一种置换式加密;与替代式加密的不同之处在于,其可以通过打乱明文(通常是字符或字符组)中的各字符所处的相对位置而实现加密,并在打乱后生成文本密文。在没有密钥的情况下,生成的消息极难破译,因为字符有意义的排列方式可以有很多种。

单表置换加密算法中维护着一个置换表,其中记录了明文和密文的对照关系。在没有加密(即没有发生置换)之前,其置换表可以如下。

明文abcdefghijklm
密文abcdefghijklm
明文nopqrstuvwxyz
密文nopqrstuvwxyz
明文ABCDEFGHIJKLM
密文ABCDEFGHIJKLM
明文NOPQRSTUVWXYZ
密文NOPQRSTUVWXYZ

在设定密钥为“Munich”后,置换表将发生改变;改变后的结果如下。

明文abcdefghijklm
密文munichabdefgj
明文nopqrstuvwxyz
密文klopqrstvwxyz
明文ABCDEFGHIJKLM
密文MUNICHABDEFGJ
明文NOPQRSTUVWXYZ
密文KLOPQRSTVWXYZ

比较后不难看出两置换表之间的区别。选用的密钥“Munich”将小写字母置换表区域中的munich按密钥中的顺序提前至首位,剩余的未出现在密钥中的字母按字母表顺序排列在小写字母置换表中的剩余位置。大写字母置换表区域与其一致。

实验代码与运行记录
算法代码

和仿射加密类似,算法中暗含了一个加密对照表和一个解密对照表。在加解密的过程中,只要通过对字符查找索引和根据索引查找字符即可实现置换。据此可以通过kotlin写出算法如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private val encryptSheet= IntArray(30)
private val decryptSheet= IntArray(30)
private fun sheetReferenceForIndex(c:Char):Int{
if (c in 'a'..'z') return c.code-'a'.code
return if (c in 'A'..'Z') c.code-'A'.code
else -1
}
private fun singleTableCipher(text:String,key:String,decryptMode:Boolean):String{
val flags=IntArray(30){_->0}
var k=0
for(i in key.indices){
var n = sheetReferenceForIndex(key[i])
if(flags[n] == 0){
encryptSheet[k++]=n
decryptSheet[n]=k-1
flags[n]=1
}
}
for(i in 0..<26){
if(flags[i] == 0){
encryptSheet[k++]=i
decryptSheet[i]=k-1
}
}
val textLen=text.length
val ansChars=CharArray(textLen)
for(i in text.indices){
if(text[i] in 'a' .. 'z') ansChars[i] = (when(decryptMode){
true->decryptSheet[text[i].code-'a'.code]
else->encryptSheet[text[i].code-'a'.code]
}+'a'.code).toChar()
else if(text[i] in 'A'..'Z') ansChars[i]=(when(decryptMode){
true->decryptSheet[text[i].code-'A'.code]
else->encryptSheet[text[i].code-'A'.code]
}+'A'.code).toChar()
else ansChars[i]=text[i]
}
return String(ansChars)
}
运行结果

根据上述算法设计输入输出。以采取密钥“Munich”对明文“Berlin”进行加解密为例,可以得到运行结果如下所示。

显然,解密结果和原本的明文一致。

实验心得

古典密码学的各种加密都是通过简单的表内对照或基本运算实现的。也正是因此,这些加密算法都非常容易被破解;只有密钥相对复杂的置换式加密才有可能存在破译难度。

算法的编写过程中完全根据定义就能写出,没有遇到什么困难——这也比较符合这些密码广为使用时的实际场景。

实验二 对称密钥加密(DES)实验

实验目的:了解并掌握DES加密的基本思路和加密算法。

实验内容:自行选取语言编写算法,使该算法在不使用该语言自带的加密库的情况下能够对给定文本进行加解密。

实验原理

数据加密标准(英语:Data Encryption Standard,缩写为DES)是一种基于使用56位密钥的对称密钥加密算法,1976年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),随后在国际上受到广泛使用。算法的整体结构中有16次相同的循环处理过程;在主处理这些循环之前,待加密的文本会被分成两个32位的半块后分别处理;这种交叉的方式也被称为Fiestel结构。Fiestel结构保证了加解密过程的相似度,唯一的区别在于子密钥在解密时是应用顺序与加密时相反。

在对这两个32位的半块做处理时主要包含以下四个步骤:

  1. 扩张:用扩张置换将32位的半块扩展到48位,其输出包括8个6位的块,每块包含4位对应的输入位,加上两个邻接的块中紧邻的位。
  2. 密钥混合:用异或操作将扩张的结果和16个48位的子密钥中的每一个进行混合;这些子密钥都是利用密钥调度从主密钥生成的。
  3. S盒:在与子密钥混合之后,块被分成8个6位的块,然后使用“S盒”(或称“置换盒”)进行处理。8个S盒的每一个都使用以查找表方式提供的非线性的变换将它的6个输入位变成4个输出位。
  4. 置换:将S盒的32个输出位利用固定的“P置换”进行重组,从而将每个S盒的4位输出在下一次循环中的扩张步骤时使用4个不同的S盒进行处理。

在上方密钥混合步骤中所提到的“密钥调度”主要先使用选择置换1(PC-1)从64位输入密钥中选出56位的密钥。剩下的8位要么直接丢弃,要么作为奇偶校验位。然后,56位分成两个28位的半密钥;每个半密钥接下来都被分别处理。在接下来的每次循环中,两个半密钥都被左移1或2位(由循环次数数决定),然后通过选择置换2(PC-2)产生48位的子密钥——每个半密钥24位。移位表明每个子密钥中使用了不同的位;每个位大致在16个子密钥出现14次。

处理之后将结果与另一个半块异或。在此之后将得到的结果与原本的半块组合并交换顺序,进入下一次循环。在最后一次循环完成时,两个半块需要交换顺序。

实验代码与运行结果

算法代码

根据上述定义利用python语言作代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
import time

## 淫趴矩阵(imperative matrix)
## 这些矩阵使用时下标要减一
## IP置换作用于进行16轮f函数作用之前,IP逆置换作用于16轮f函数作用之后
## IP置换表

IP_table = [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
]
## 逆IP置换表
_IP_table = [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
]
## 每一个S-盒的输入数据是6位,输出数据是4位,但是每个S-盒自身是64位
## 设入的六位为b1,b2,b3,b4,b5,b6,b1、b6位组合得到列号,b2,b3,b4,b5组合得到行号。
## S盒中的S1盒
S1 = [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
]
## S盒中的S2盒
S2 = [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
]
## S盒中的S3盒
S3 = [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
]
## S盒中的S4盒
S4 = [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
]
## S盒中的S5盒
S5 = [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
]
## S盒中的S6盒
S6 = [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
]
## S盒中的S7盒
S7 = [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
]
## S盒中的S8盒
S8 = [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
]
## S盒;将8个S盒组成一个8行64列的2维数组
S = [S1, S2, S3, S4, S5, S6, S7, S8]
## P盒置换将每一位输入位映射到输出位。任何一位都不能被映射两次,也不能被略去。
## P盒
P_table = [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
]
## 压缩置换表1,不考虑每字节的第8位,将64位密钥减至56位。然后进行一次密钥置换。
## 忽略第8位奇偶校验的同时,进行置换,
compressed_table1 = [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
]

## 压缩置换表2,用于将循环左移和右移后的56bit密钥压缩为48bit
## 完成左移和右移后
## 去掉第9、18、22、25、35、38、43、54位,从56位变成48位,再按表的位置置换。
compressed_table2 = [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
]

## 扩展置换改变了位的次序,重复了某些位
## 用于对数据进行扩展置换,将32bit数据扩展为48bit
## 目的A:产生与秘钥相同长度的数据以进行异或运算,R0是32位,子秘钥是48位,
## 目的B:提供更长的结果,使得在替代运算时能够进行压缩。
extend_table = [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
]


## --------------------------从字符到bit--------------------------
## 将字符转换为对应的Unicode码,中文用2个字节表示
def char2unicode_ascii(toBeProcessed, length):
result = []
for i in range(length):
## ord是chr()的及配对函数,以‘c’“c”做为参数
## 返回对应的ASCII或Unicode数值,若超出,返回TypeError
result.append(ord(toBeProcessed[i]))
return result


## 将Unicode码转为bit
def unicode2bit(toBeProcessed, length):
resultBit = []
for i in range(length * 16):
## 每一位Unicode都是16bit组成
## 2bProcessed[int(i/16)]length*16:对应待处理文本中的每一个Unicode,进行16次操作
## 每轮操作右移0,1,2...,15位,然后与1且运算,一次得到16bit
## !!!得到的这16位resultBit是个逆序
resultBit.append(toBeProcessed[int(i / 16)] >> (i % 16) & 1)
return resultBit


## 将8位ASCII码转为bit
def byte2bit(charToBeProcessed, length):
resultBit = []
for i in range(length * 8):
## 原理同上
resultBit.append(charToBeProcessed[int(i / 8)] >> (i % 8) & 1)
return resultBit


## --------------------------从bit到字符--------------------------
## 将bit转为Unicode码
def bit2unicode(bitToBeProcessed, length):
out = []
temp = 0
for i in range(length):
## 这和上面是同步的,将bit,反着求出,刚刚好
## 或的运算符在这里相当+,左边是int,右边是int,这里相当于二进制数,妙
## 每16位bit,对应着一位Unicode,
## 所以当i对16求模,余数为15时
## 将append(temp),并且重新设置temp为0
temp = temp | (bitToBeProcessed[i] << (i % 16))
if i % 16 == 15:
out.append(temp)
temp = 0
return out


## 将bit转为ascii码
def bit2byte(bitToBeProcessed, length):
out = []
temp = 0
for i in range(length):
temp = temp | (bitToBeProcessed[i] << (i % 8))
if i % 8 == 7:
out.append(temp)
temp = 0
return out


## 将unicode码转为字符(中文或英文)
def unicode2char(byteToBeProcessed, length):
out = ""
for i in range(length):
out = out + chr(byteToBeProcessed[i])
return out


## ------------------生成每一轮的key------------------
def createKeys(keyToBeProcessed):
keyResult = []
## 将char型秘钥转化为bit型
key_ascii_ver = char2unicode_ascii(keyToBeProcessed, len(keyToBeProcessed))
initialKey = byte2bit(key_ascii_ver, len(key_ascii_ver))
## print("initialKey = ", end = '')
## print(initialKey)
## 用0初始化列表key0,key1,
key0 = [0 for i in range(56)]
key1 = [0 for i in range(48)]
## 进行密码压缩置换1,
## 用压缩置换表1,不考虑每字节的第8位,
## 将64位密码压缩为56位
for i in range(56):
key0[i] = initialKey[compressed_table1[i] - 1]

## 进行16轮的密码生成
for i in range(16):
## ---------------确定左移的次数---------------
if i == 0 or i == 1 or i == 8 or i == 15:
movestep = 1
else:
movestep = 2
## --------------------------------------------

## -----分两部分,每28bit位一部分,进行循环左移-----
## 因为每次循环左移就是第28个移动至第1个
for j in range(movestep):
## 以行为单位
## 移动2位就是,移动一位进行2次操作
'''
for k in range(8):
#以行为单位左移
#除去最后一位,每一行都左移一位,操作前保留第一位
temp = key0[k*7]
for m in range(7*k, 7*k + 6):
key0[m] = key0[m+1]
key0[7*k + 6] = temp
'''
## 以组为单位左移
temp = key0[0]
for k in range(27):
key0[k] = key0[k + 1]
key0[27] = temp
temp = key0[28]
for k in range(28, 55):
key0[k] = key0[k + 1]
key0[55] = temp
## -----------------------------------------------

## -------对56位密钥进行压缩置换,压缩为48位--------
## 置换选择表2(PC-2),得到子密匙K1
for k in range(48):
key1[k] = key0[compressed_table2[k] - 1]
## keyResult为16行48列的二维数组
keyResult.extend(key1)
## -------------------------------------------------

return keyResult


'''
## 用来检验代码
key0 = [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]


## 以组为单位左移
temp = key0[0]
for k in range(27):
key0[k] = key0[k+1]
key0[27] = temp
temp = key0[28]
for k in range(28, 55):
key0[k] = key0[k+1]
key0[55] = temp
key0
'''


def DES(text, key, optionType):
keyResult = createKeys(key)
## 初始化最终得到的结果
finalTextOfBit = [0 for i in range(64)]
finalTextOfUnicode = [0 for i in range(4)]

## 打印出来检验一下
## print(keyResult)

if optionType == 0:
## 初始化
## 用于临时盛放IP拟置换前,将L部分和R部分合成64的结果
tempText = [0 for i in range(64)]

## 初始化
## 用于盛放R部分的扩展到48位的结果
extendR = [0 for i in range(48)]

## char型向bit型转化
unicodeText = char2unicode_ascii(text, len(text))
## print(unicodeText)
bitText = unicode2bit(unicodeText, len(unicodeText))
## print(bitText)

## 初始化
## 用于存放IP置换之后的结果
initTrans = [0 for i in range(64)]

## ---------------进行初始IP置换---------------
for i in range(64):
initTrans[i] = bitText[IP_table[i] - 1]
## 将64位明文分为左右两部分

L = initTrans[:32]
R = initTrans[32:]

## 开始进行16轮运算
for i in range(16):
## 临时存放R
tempR = R

## -------对R进行扩展,将32位扩展为48位-------
for j in range(48):
extendR[j] = R[extend_table[j] - 1]
## print(len(keyResult))

## 第i轮的秘钥,从keyResult中取出
key_i = [keyResult[j] for j in range(i * 48, i * 48 + 48)]
## print(i,key_i)
## ---------与key进行异或运算--------
## 初始化
XORResult = [0 for j in range(48)]
for j in range(48):
if key_i[j] != extendR[j]:
XORResult[j] = 1
## ----------------------------------

## ---------开始进行盒运算----------
## 初始化
SResult = [0 for k in range(32)]
for k in range(8):
## 此处使用移位转进制
row = ((XORResult[k * 6]) << 1) | (XORResult[k * 6 + 5])
column = 0
for j in range(1, 5):
column = column | (XORResult[k * 6 + j] << (4 - j))
temp = S[k][row * 16 + column]
for m in range(4):
SResult[k * 4 + m] = (temp >> m) & 1
## ---------------------------------
## if i == 0:
## print('PResult is :;;:',PResult)

## -------------开始进行P盒置换-------------
## 初始化
PResult = [0 for k in range(32)]
for k in range(32):
PResult[k] = SResult[P_table[k] - 1]
## -----------------------------------------
## if i == 0:
## print('PRsult is :;;:',PResult)
## --------------与L部分的数据进行异或------------
XORWithL = [0 for k in range(32)]
for k in range(32):
if L[k] != PResult[k]:
XORWithL[k] = 1
## ----------------------------------------------

## 将临时保存的R部分值,即tempR复制给L
L = tempR
R = XORWithL

## print("当前i为%d",(i))
## print("当前L,R为",L,R)
## -----------------循环结束-----------------

## 交换左右两部分
L, R = R, L
## if i == 0:
## print('LRis :;;:',L,R)
## 合并为一部分
tempText = L
tempText.extend(R)

## IP逆置换
for k in range(64):
finalTextOfBit[k] = tempText[_IP_table[k] - 1]

## bit型转化为char型
finalTextOfUnicode = bit2byte(finalTextOfBit, len(finalTextOfBit))
## print(finalTextOfUnicode)
## print("finalTextOfUnicode", finalTextOfUnicode)
finalTextOfChar = unicode2char(finalTextOfUnicode, len(finalTextOfUnicode))
return finalTextOfChar
else:
## 初始化
## 用于临时盛放IP拟置换前,将L部分和R部分合成64的结果
tempText = [0 for i in range(64)]

## 初始化
## 用于盛放R部分的扩展到48位的结果
extendR = [0 for i in range(48)]

## char型向bit型转化
unicodeText = char2unicode_ascii(text, len(text))
## print(unicodeText)
bitText = byte2bit(unicodeText, len(unicodeText))
## print(bitText)

## 初始化
## 用于存放IP置换之后的结果
initTrans = [0 for i in range(64)]

## ---------------进行初始IP置换---------------
for i in range(64):
initTrans[i] = bitText[IP_table[i] - 1]
## 将64位明文分为左右两部分
L = [initTrans[i] for i in range(32)]
R = [initTrans[i] for i in range(32, 64)]

## 开始进行16轮运算
for i in range(15, -1, -1):
## 临时存放R
tempR = R

## -------对R进行扩展,将32位扩展为48位-------
for j in range(48):
extendR[j] = R[extend_table[j] - 1]
## print(len(keyResult))

## 第i轮的秘钥,从keyResult中取出
key_i = [keyResult[j] for j in range(i * 48, i * 48 + 48)]
## ---------与key进行异或运算--------
## 初始化
XORResult = [0 for j in range(48)]
for j in range(48):
if key_i[j] != extendR[j]:
XORResult[j] = 1
## ----------------------------------

## ---------开始进行S盒运算----------
## 初始化
SResult = [0 for k in range(32)]

for k in range(8):
## 此处使用移位转进制
row = (XORResult[k * 6] << 1) | (XORResult[k * 6 + 5])
column = 0
for j in range(1, 5):
column = column | (XORResult[k * 6 + j] << (4 - j))
temp = S[k][row * 16 + column]
for m in range(4):
SResult[k * 4 + m] = (temp >> m) & 1
## ---------------------------------
## print('SResult',SResult)
## -------------开始进行P盒置换-------------
## 初始化
PResult = [0 for k in range(32)]
for k in range(32):
PResult[k] = SResult[P_table[k] - 1]
## -----------------------------------------

## --------------与L部分的数据进行异或------------
XORWithL = [0 for k in range(32)]
for k in range(32):
if L[k] != PResult[k]:
XORWithL[k] = 1
## ----------------------------------------------

## 将临时保存的R部分值,即tempR复制给L
L = tempR
R = XORWithL
## print("L, R在第%d轮",(i, L,R))
## 交换左右两部分
L, R = R, L
## print("L, R",(L,R))
## 合并为一部分
tempText = L
tempText.extend(R)
## ----------------------IP逆置换----------------------
for k in range(64):
finalTextOfBit[k] = tempText[_IP_table[k] - 1]

## bit型转化为char型
finalTextOfUnicode = bit2unicode(finalTextOfBit, len(finalTextOfBit))
## print(finalTextOfUnicode)
finalTextOfChar = unicode2char(finalTextOfUnicode, len(finalTextOfUnicode))
## print(finalTextOfChar)
return finalTextOfChar


运行结果

根据上方算法设计输入输出,并运行该程序。以利用八位密钥“DESCrypt”加密文本“Hello DES”为例,得到加解密情况如下:

显然解密得到的文本与加密前一致。

实验心得

作为一个已经有了完整体系且至今仍受欢迎的加密算法,DES加密算法的代码量非常大。为了降低转码的工作量,选取了中英文字母都占2个字节的Unicode作为文本处理后的第一步。

实验三 非对称加密(RSA)实验

实验目的:了解并掌握RSA加密算法的原理。

实验内容:自行选取语言编写算法,使该算法在不利用该语言自带密码库的情况下能够对文本进行RSA加解密。

实验原理

RSA加密算法是一种非对称加密算法,在公开密钥加密和电子商业中被广泛使用。

公私钥

若Alice想要接收Bob的私人訊息,她可以用以下的方式來产生公钥和私钥:

  1. 选取任意两个较大且不相等的质数ppqq,并计算N=pqN=pq
  2. 根据欧拉函数求r=φ(N)=φ(p)×φ(q)=(p1)(q1)r=\varphi (N) = \varphi (p)\times\varphi (q)=(p-1)(q-1)
  3. 任取一个小于rr且与之互质的整数ee,并求ee关于rr的取模倒数dd

通过此方法得到的数对(N,e)(N,e)即公钥,(N,d)(N,d)即私钥。Alice自己保留私钥的同时将公钥发送给Bob即可开始使用非对称算法的加密信息传送。

信息加(解)密

假设Bob想在知晓Alice生成的NNee的情况下给Alice发送消息mm,则Bob只需要将mm中的每一个字通过unicode或其他转码方式转换为一个小于NN的非负整数nn,随后通过公式c=nemodNc = n^e \bmod{N}将信息mm加密为密文cc,再将cc发送即可。Alice收到消息后解密的方式也类似,只需要利用cc和她的私钥dd通过公式n=cdmodNn = c^d \bmod {N}得到原文nn即可。

显然可以证明

cdned (mod N)ned=n1+hφ(N)=nnhφ(N)=n(nφ(N))h\begin{align} c^d &\equiv n^{e \cdot d}\ (\mathrm{mod}\ N) &\equiv n ^ {ed} = n ^ {1 + h \varphi(N)} = n \cdot n ^ {h \varphi(N) } = n \left( n ^ {\varphi(N)} \right) ^ h \end{align}

nnNN互质,则由欧拉定理显然有

nedn(nφ(N))hn(1)hn(modN)n^{ed} \equiv n \left( n ^ {\varphi(N)} \right) ^ h \equiv n (1) ^ h \equiv n \pmod{N}

否则不妨设n=ph,ed1=k(q1)n = ph, ed -1 = k(q-1),进而有

ned=(ph)ed0phn(modp)n ^ {ed} = (ph) ^ {ed} \equiv 0 \equiv ph \equiv n \pmod p

ned=ned1n=nk(q1)n=(nq1)kn1knn(modq)n ^ {ed} = n ^{ed - 1} n = n^{k(q - 1)} n = (n^{q - 1})^k n \equiv 1^k n \equiv n \pmod{q}

nedn(modN)n ^ {ed} \equiv n \pmod N

实验代码与运行结果

実験ご注意
本章节所使用的代码存在一定问题。在运行期间一旦涉及到Unicode比较大的字符就会出现转码错误;分析后认为是值的类型超限造成的。

利用语言kotlin编写算法如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import kotlin.random.Random

// Functions of Mathematical Methods
private fun euclid4GCDPlus(a: Int, b: Int): Triple<Int, Int, Int> {
//Extended Euclid Methods are used to search for a solution to the equation ax+by=1.
return if (b == 0) {
Triple(1, 0, a)
} else {
val (x, y, euclidResult) = euclid4GCDPlus(b, a % b)
Triple(y, x - (a / b) * y, euclidResult)
}
}

private fun euclid4GCD(a: Int, b: Int): Int {
//Euclid Methods are employed to find the greatest common divisor of 2 numbers
return if (a % b == 0) {
b
} else {
euclid4GCD(b, a % b)
}
}

private fun montgomeryModularMultiplication4Mod(base: Int, exponent: Int, n: Int): Int {
//Montgomery Modular Multiplication Methods are applied for calculate the mod for an exponential number.
val binArray = exponent.toString(2).reversed()
val r = binArray.length
val baseArray = mutableListOf<Int>()
var preBase = base
baseArray.add(preBase)
repeat(r - 1) {
val nextBase = (preBase * preBase) % n
baseArray.add(nextBase)
preBase = nextBase
}
var result = 1
for (i in baseArray.indices) {
if (binArray[i] == '0') {
continue
}
result *= baseArray[i]
result %= n
}
return result % n
}

//Functions for cipher
private fun keyPairGenerator(p: Int, q: Int): Pair<Pair<Int, Int>, Pair<Int, Int>> {
val n = p * q
val phi = (p - 1) * (q - 1)
//Private key
var prK = Random.nextInt(2, phi)
//Public key
var puK = 0
while (puK == 0) {
if (euclid4GCD(prK, phi) == 1) {
puK = euclid4GCDPlus(prK, phi).first % phi
} else {
prK = Random.nextInt(2, phi)
}
}
return Pair(Pair(n, prK), Pair(n, puK))
}

private fun isPrime(n: Int): Boolean {
if (n <= 1) {
return false
}
for (i in 2..n / 2) {
if (n % i == 0) {
return false
}
}
return true
}

private class RSACipher(keyPair: Pair<Int, Int>) {
val n = keyPair.first
val key = keyPair.second
fun encrypt(c: List<Int>): List<Int> {
return c.map { montgomeryModularMultiplication4Mod(it, key, n) }
}

fun decrypt(e: List<Int>): List<Int> {
return e.map { montgomeryModularMultiplication4Mod(it, key, n) }
}
}

private fun encode2Integer(text: String): List<Int> {
return text.map { it.code }
}

private fun decode2Character(list: List<Int>): String {
return list.map { it.toChar() }.joinToString("")
}

private fun randomPrimeGenerator(x: Int): Int {
val newNum = Random.nextInt(x)
return if (isPrime(newNum)) {
newNum
} else {
randomPrimeGenerator(x)
}
}

根据上述代码设计输入输出如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private fun main() {
//Prime Number Prompt
println("Please select 2 PRIME numbers for key pair generation.")
print("First PRIME number: ")
var p = readln().toInt()
print("Second PRIME number: ")
var q = readln().toInt()
println("Confirmed. p = $p, q = $q")
//Prime check
println("Checking whether p & q are PRIME...")
if (isPrime(p) && isPrime(q)) {
println("Checked: BOTH PRIME")
} else if (isPrime(q)) {
println("Checked: p is NOT PRIME")
p = randomPrimeGenerator(p)
println("Correction:p has been changed to be a PRIME.")
} else if (isPrime(p)) {
println("Checked: q is NOT PRIME")
q = randomPrimeGenerator(q)
println("Correction: q has been changed to be a PRIME.")
} else {
println("Checked: NO PRIMES")
p = randomPrimeGenerator(p)
q = randomPrimeGenerator(q)
println("Correction: p and q has been changed to be PRIME.")
}
println("Check finished. (p = $p, q = $q)")
val (privateKeyPair, publicKeyPair) = keyPairGenerator(p, q)
val encryptService = RSACipher(privateKeyPair)
val decryptService = RSACipher(publicKeyPair)
print("Enter text to be processed: ")
val message = readln()
println("Gotcha. Transcode in progress.")
Thread.sleep(1000)
val transcodeText = encode2Integer(message)
println("Transcode finished: $transcodeText")
if (transcodeText.max() > p * q) {
println("WARNING: p & q is not great enough. The result may NOT be correct.")
}
println("Encryption in progress.")
Thread.sleep(1000)
val encryptedCodeList = encryptService.encrypt(transcodeText)
println("Encryption done: $encryptedCodeList")
val decodedEncrypt = decode2Character(encryptedCodeList)
println("Which can be translated into text: $decodedEncrypt")
if (encryptedCodeList.max() > p * q) {
println("WARNING: p & q is not great enough. The result may NOT be correct.")
}
println("Decryption in progress.")
val decryptedCodeList = decryptService.decrypt(encryptedCodeList)
println("Decryption done: $decryptedCodeList")
val decodedDecrypt = decode2Character(decryptedCodeList)
println("Decryption done: $decodedDecrypt")
println("<Private Key Pair: $privateKeyPair, Public Key Pair: $publicKeyPair>")
}

运行该程序。以利用质数41、281加密明文“Hello RSA”为例,运行结果如下图所示:

显然解密结果与原文一致。

实验心得

根据RSA实验原理定义,对极大整数做因数分解的难度决定了RSA算法的可靠性——对一极大整数做因数分解愈困难,RSA算法愈可靠;如若后期有人能够找到一种快速分解质因数的算法,那么用RSA加密的信息的可靠性就会骤降。

实验四 MD5加密实验

实验目的:了解并掌握哈希加密算法的工作方式。

实验内容:自行选择语言编写算法,使该算法在不利用该语言自带密码库的情况下可以实现对文本的MD5加密。

实验原理

MD5消息摘要算法是一种被广泛使用的密码散列函数,可以通过求余、调整长度等方式产生一个特定长度的的散列值来确定信息传输始末内容的完整性和一致性。经过程序流程,MD5加密算法可以生成四个32位数据,最后联合起来成为一个128-bits散列。

F(X,Y,Z)=(XY)(¬XZ)F(X,Y,Z) = (X\wedge{Y}) \vee (\neg{X} \wedge{Z})

G(X,Y,Z)=(XZ)(Y¬Z)G(X,Y,Z) = (X\wedge{Z}) \vee (Y \wedge \neg{Z})

H(X,Y,Z)=XYZH(X,Y,Z) = X \oplus Y \oplus Z

I(X,Y,Z)=Y(X¬Z)I(X,Y,Z) = Y \oplus (X \vee \neg{Z})

实验代码与加密结果

通过python编写的加密程式如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import math

## 定义常量,用于初始化128位变量,注意字节顺序,文中的A=0x01234567,这里低值存放低字节,即01 23 45 67,所以运算时A=0x67452301,其他类似。
## 这里用字符串的形势,是为了和hex函数的输出统一,hex(10)输出为'0xA',注意结果为字符串。
hexStrA = '0x67452301'
hexStrB = '0xefcdab89'
hexStrC = '0x98badcfe'
hexStrD = '0x10325476'
## 定义每轮中用到的函数。L为循环左移,注意左移之后可能会超过32位,所以要和0xffffffff做与运算,确保结果为32位。
F = lambda x, y, z: ((x & y) | ((~x) & z))
G = lambda x, y, z: ((x & z) | (y & (~z)))
H = lambda x, y, z: (x ^ y ^ z)
I = lambda x, y, z: (y ^ (x | (~z)))
L = lambda x, n: (((x << n) | (x >> (32 - n))) & 0xffffffff)

## 定义每轮中循环左移的位数,这里用4个元组表示,用元组是因为速度比列表快。
shi_1 = (7, 12, 17, 22) * 4
shi_2 = (5, 9, 14, 20) * 4
shi_3 = (4, 11, 16, 23) * 4
shi_4 = (6, 10, 15, 21) * 4

## 定义每轮中用到的M[i]次序。
m_1 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
m_2 = (1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12)
m_3 = (5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2)
m_4 = (0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9)


## 定义函数,用来产生常数T[i],常数有可能超过32位,同样需要&0xffffffff操作。注意返回的是十进制的数。
def constantGenerator(i):
result = (int(4294967296 * abs(math.sin(i)))) & 0xffffffff
return result


## 定义函数,用来将列表中的元素循环右移。原因是在每轮操作中,先运算A的值,然后是D,C,B,16轮之后右恢复原来顺序,所以只要每次操作第一个元素即可。
def shift(shift_list):
shift_list = [shift_list[3], shift_list[0], shift_list[1], shift_list[2]]
return shift_list


## 定义主要的函数,参数为当做种子的列表,每轮用到的F,G,H,I,生成的M[],以及循环左移的位数。该函数完成一轮运算。
def md5Cipher(fun_list, f, m, shi):
count = 0
global constantGeneratorApplicationTimeCount
## 引入全局变量,T(i)是从1到64循环的。
while count < 16:
xx = int(fun_list[0], 16) + f(int(fun_list[1], 16), int(fun_list[2], 16), int(fun_list[3], 16)) + int(m[count],
16) + constantGenerator(
constantGeneratorApplicationTimeCount)
xx = xx & 0xffffffff
ll = L(xx, shi[count])
## fun_list[0] = hex((int(fun_list[1],16) + ll)&(0xffffffff))[:-1]
fun_list[0] = hex((int(fun_list[1], 16) + ll) & 0xffffffff)
## 最后的[:-1]是为了去除类似'0x12345678L'最后的'L'
fun_list = shift(fun_list)
count += 1
constantGeneratorApplicationTimeCount += 1
## print(fun_list)
return fun_list


## 该函数生成每轮需要的M[],最后的参数是为了当有很多分组时,进行偏移。
def m16Generator(order, ascii_list, f_offset):
ii = 0
m16 = [0] * 16
f_offset = f_offset * 64
for i in order:
i = i * 4
m16[ii] = '0x' + ''.join((ascii_list[i + f_offset] + ascii_list[i + 1 + f_offset] + ascii_list[
i + 2 + f_offset] + ascii_list[i + 3 + f_offset]).split('0x'))
ii += 1
for c in m16:
ind = m16.index(c)
m16[ind] = reverse_hex(c)
return m16


## 翻转十六进制数的顺序:'0x01234567' => '0x67452301'
def reverse_hex(hex_str):
hex_str = hex_str[2:]
hex_str_list = []
for i in range(0, len(hex_str), 2):
hex_str_list.append(hex_str[i:i + 2])
hex_str_list.reverse()
hex_str_result = '0x' + ''.join(hex_str_list)
return hex_str_result


## 显示结果函数,将最后运算的结果列表进行翻转,合并成字符串的操作。
def show_result(f_list):
result = ''
f_list1 = [0] * 4
for i in f_list:
f_list1[f_list.index(i)] = reverse_hex(i)[2:]
result = result + f_list1[f_list.index(i)]
return result


## 程序主循环
while True:
abcd_list = [hexStrA, hexStrB, hexStrC, hexStrD]
constantGeneratorApplicationTimeCount = 1
input_m = input('MSG>>>')
## 对每一个输入先添加一个'0x80',即'10000000'
ascii_list = list((map(hex, map(ord, input_m))))
## print('ascii_list:',ascii_list)
msg_length = len(ascii_list) * 8
ascii_list.append('0x80')

## 补充0
while (len(ascii_list) * 8 + 64) % 512 != 0:
ascii_list.append('0x00')

## 最后64为存放消息长度,注意长度存放顺序低位在前。
## 例如,消息为'a',则长度为'0x0800000000000000'
msgLength0x = hex(msg_length)[2:]
msgLength0x = '0x' + msgLength0x.rjust(16, '0')
msgLength0xBigOrder = reverse_hex(msgLength0x)[2:]
msgLength0xList = []
for i in range(0, len(msgLength0xBigOrder), 2):
msgLength0xList.append('0x' + msgLength0xBigOrder[i:i + 2])
ascii_list.extend(msgLength0xList)
## print (ascii_list)

## 对每个分组进行4轮运算
for i in range(0, len(ascii_list) // 64):
## 将最初128位种子存放在变量中,
aa, bb, cc, dd = abcd_list
## 根据顺序产生每轮M[]列表
order_1 = m16Generator(m_1, ascii_list, i)
order_2 = m16Generator(m_2, ascii_list, i)
order_3 = m16Generator(m_3, ascii_list, i)
order_4 = m16Generator(m_4, ascii_list, i)
## 主要四轮运算,注意打印结果列表已经被进行过右移操作!
abcd_list = md5Cipher(abcd_list, F, order_1, shi_1)
abcd_list = md5Cipher(abcd_list, G, order_2, shi_2)
abcd_list = md5Cipher(abcd_list, H, order_3, shi_3)
abcd_list = md5Cipher(abcd_list, I, order_4, shi_4)
## 将最后输出与最初128位种子相加,注意,最初种子不能直接使用abcd_list[0]等,因为abcd_list已经被改变
output_a = hex((int(abcd_list[0], 16) + int(aa, 16)) & 0xffffffff)
output_b = hex((int(abcd_list[1], 16) + int(bb, 16)) & 0xffffffff)
output_c = hex((int(abcd_list[2], 16) + int(cc, 16)) & 0xffffffff)
output_d = hex((int(abcd_list[3], 16) + int(dd, 16)) & 0xffffffff)
## 将输出放到列表中,作为下一次128位种子
abcd_list = [output_a, output_b, output_c, output_d]
## 将全局变量Ti_count恢复,一遍开始下一个分组的操作。
constantGeneratorApplicationTimeCount = 1
## 最后调用函数,格式化输出
print('MD5>>>' + show_result(abcd_list))
break

运行该程序。以加密文本“hello md5”为例,得到散列值如下所示:

在其他网站上使用在线md5加密工具,加密同一个文本,得到的散列值与之相同。

因此,编写的算法是正确的。

实验心得

MD5作为验证被传输信息完整性和一致性的散列值生成方式,其算法也很难被模仿出来,因此代码量也非常的大。在16进制与10进制数相互转换的时候很容易因为疏忽而错转漏转。

实验五 密码学应用·文件加密传输试验

实验目的:掌握对称加密、非对称加密和哈希算法生成散列值的使用场景并能灵活应用这三种加密方法。

实验内容

自选语言编写代码文件若干,使这些程式在不借助这些语言自带密码库的情况下能够模拟文件的加密传输与解密且保证满足信息传输的诸多基本要求(如不可否认性、完整性等)。标注清楚发送端需要发送的文件和这些文件的产生方式,并说明接收端接收到的文件和之后的处理方式。发送的文件须打包发送。

功能实现

根据实验内容主要考虑以下几个方面。加密算法的代码在前文均已提及,为减少篇幅在此不与呈现。

机制

文件传输最经典的应用莫过于电子邮箱。因此类比电子邮箱的操作,可以将收发信两端简单模拟成一个个人电子邮箱中的sent(已发送)和inbox(收件箱)两个文件夹。注意到实验内容要求对文件进行打包发送,因此考虑电子邮箱中的outbox(待发送)文件夹——其将暂时存储加密之后的文件,在所有文件生成完毕后询问用户是否确定发送(类似于邮件的“确认发送倒计时”操作)。完成发送后,outbox中的文件将全部转移至sent中;如若用户在这一环节取消发送,outbox中的所有文件将会被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
### fileTransmit.py ##
import os
import shutil
import time

shutil.rmtree(r'inbox')
os.makedirs('inbox')
willingness = input("Are you sure to send all files in the outbox?\nNO(0)/YES(1)>>> ")
if willingness == "1":
print("Sending", end=">")
time.sleep(1)
for i in range(10):
print(">", end="")
time.sleep(0.2)
shutil.move('outbox/encrypted_hash.txt', r'sent/encrypted_hash.txt')
shutil.move('outbox/encrypted_msg.txt', r'sent/encrypted_msg.txt')
shutil.move('outbox/encrypted_pwd_des.txt', r'sent/encrypted_pwd_des.txt')
print("\rPack sent.")
else:
os.unlink(r'outbox/encrypted_hash.txt')
os.unlink(r'outbox/encrypted_msg.txt')
os.unlink(r'outbox/encrypted_pwd_des.txt')

此外,注意到收发信两端还可能需要用到的非对称加密,另需要一位作为管理者(keyProvider)的角色为他们分发各自的公私钥。考虑到私钥需要二人自行保管,keyProvider会将私钥private_key_sender.txtprivate_key_receiver.txt置于双方各自的沙盒sandbox_sendersandbox_receiver中作为隔离,公钥public_key_sender.txtpublic_key_receiver.txt则将放置在公共空间中。

1
2
3
4
5
6
7
8
9
10
11
12
13
def individualized_key_pair_generator(client):
p = random_prime(100, 200)
q = random_prime(200, 300)
private_rsa_key_pair, public_rsa_key_pair = generate_key_pair(p, q)
with open('public_key_{}.txt'.format(client), 'w') as client_pub_key:
client_pub_key.write(str(public_rsa_key_pair))
with open("sandbox_{}/private_key_{}.txt".format(client, client), 'w') as client_private_key:
client_private_key.write(str(private_rsa_key_pair))
print("Key pair for {} generated. <<p = {}, q = {}>>".format(client, p, q))


individualized_key_pair_generator('sender')
individualized_key_pair_generator('receiver')

因此整个实验过程应须要inboxoutboxsentsandbox_receiversandbox_sender五个文件夹。在实验每次模拟开始前,可以对这些文件夹进行适当的初始化,便于在实验进行过程中查看每一步的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
## Python file to initialize the overall progress of the file transmit
## Finished May 31 2:20 p.m.

import os
import shutil


def does_not_exist(directory_path):
if os.path.exists(directory_path):
return False
return True


def not_empty_dir(directory_path):
if len(os.listdir(directory_path)) == 0:
return False
return True


if does_not_exist('inbox'):
os.mkdir('inbox')

if does_not_exist('outbox'):
os.mkdir('outbox')

if does_not_exist('sent'):
os.mkdir('sent')

if does_not_exist('sandbox_receiver'):
os.mkdir('sandbox_receiver')

if not_empty_dir('inbox'):
shutil.rmtree(r'inbox')
os.mkdir(r'inbox')

if not_empty_dir('sent'):
shutil.rmtree(r'sent')
os.mkdir(r'sent')

if not_empty_dir('outbox'):
shutil.rmtree(r'outbox')
os.mkdir(r'outbox')

os.unlink('public_key_sender.txt')
os.unlink('public_key_receiver.txt')
os.unlink('sandbox_sender/private_key_sender.txt')
os.unlink('sandbox_receiver/private_key_receiver.txt')

print('Initialization Complete')
发送方

发送方显然是消息的撰写者和整个流程的开端。他需要对自己撰写的信息进行加密,并且对此次加密所使用的密码也进行加密。随后,他还需要对这两个加密之后的内容都生成散列值,然后对这一散列值进行加密进行发送。为保安全,这三个文件使用同一个加密方式进行加密显然是不行的;加之需要考虑信息传输的基本安全要求,故可分别利用对称加密密钥、发送方非对称加密私钥和接收方对称加密公钥按如下方式进行加密:

1
2
3
4
message_encrypt()
des_key_encrypt_with_rsa()
signature()
print("Compulsory files packed. Check them in the outbox.")
  1. 使用对称加密密钥对信息进行加密,确保文件的机密性(对称加密密钥和信息明文sample.txt可以直接存储在sandbox_sender中);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    def message_encrypt():
    encrypted_lines = ""
    with open('sandbox_sender/desPassword.txt', 'r') as des_password_gotcha:
    des_pwd = des_password_gotcha.readline()
    with open('sandbox_sender/sample.txt', 'r') as original_file:
    original = original_file.read()
    original_length = len(original)
    if original_length % 4 != 0:
    original = original + int(4 - (original_length % 4)) * " "
    original_length = len(original)
    encrypted_line = ""
    for i in range(int(original_length / 4)):
    temp_text = [original[j] for j in range(i * 4, i * 4 + 4)]
    encrypted_line = "".join([encrypted_line, DES(temp_text, des_pwd, 0)])
    encrypted_lines += str(encrypted_line)
    ## print(encrypted_lines)
    with open('outbox/encrypted_msg.txt', 'w') as container_of_encrypted_contents:
    container_of_encrypted_contents.write(encrypted_lines)
    print("Message has been encrypted.")

  2. 对加密信息所用的对称加密密钥利用接收方的非对称加密公钥进行加密,使接收方无法否认文件由他接收;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def des_key_encrypt_with_rsa():
    with open('sandbox_sender/desPassword.txt', 'r') as des_password_gotcha:
    des_pwd = des_password_gotcha.readline()
    with open('public_key_receiver.txt', 'r') as pub_key_radio:
    public_key_pair_string = pub_key_radio.readline()
    public_key_pair_string2pair_step_1 = public_key_pair_string.split(', ')
    public_key_pair_string2pair_step_2_a = public_key_pair_string2pair_step_1[0].removeprefix('(')
    public_key_pair_string2pair_step_2_b = public_key_pair_string2pair_step_1[1].removesuffix(')')
    public_rsa_key_pair = int(public_key_pair_string2pair_step_2_a), int(public_key_pair_string2pair_step_2_b)
    rsa_encrypt_service = RSA(public_rsa_key_pair)
    encoded_password = Encode2int(des_pwd)
    encrypted_password = rsa_encrypt_service.Encrypt(encoded_password)
    encrypted_des_pwd_chr = Decode2chr(encrypted_password)
    with open('outbox/encrypted_pwd_des.txt', 'w') as encrypted_des_pwd_container:
    encrypted_des_pwd_container.write(str(encrypted_des_pwd_chr))
    print("DES password has been encrypted.")
  3. 对确保信息完整性和一致性的散列值用发送方的非对称加密私钥进行加密作为“签名”,使发送方无法否认文件由他发送。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def signature():
    with open('sandbox_sender/private_key_sender.txt', 'r') as private_key_container:
    private_key_pair_string = private_key_container.readline()
    private_key_pair_string2pair_step_1 = private_key_pair_string.split(', ')
    private_key_pair_string2pair_step_2_a = private_key_pair_string2pair_step_1[0].removeprefix('(')
    private_key_pair_string2pair_step_2_b = private_key_pair_string2pair_step_1[1].removesuffix(')')
    private_rsa_key_pair = int(private_key_pair_string2pair_step_2_a), int(private_key_pair_string2pair_step_2_b)
    rsa_encrypt_service = RSA(private_rsa_key_pair)
    encoded_hash = Encode2int(hash_abstract())
    encrypted_hash = rsa_encrypt_service.Encrypt(encoded_hash)
    encrypted_hash_chr = Decode2chr(encrypted_hash)
    with open('outbox/encrypted_hash.txt', 'w') as hash_container_to_send:
    hash_container_to_send.write(str(encrypted_hash_chr))
    print("Hash has been signed (encrypted).")

因此,发送方需要发送的文件显然是加密过后的信息encrypted_msg.txt、加密过后的对称密码encrypted_pwd_des.txt和加密过后的散列值encrypted_hash.txt三个。

接收方

根据“发送方”章节中所提到的发送文件清单,接收方应当对这些文件有针对性地进行解密。不难得到他的任务如下:

1
2
3
message_decrypt()
hash_decrypt()
hash_check()
  1. 使用自己的非对称加密私钥对encrypted_pwd_des.txt进行解密,得到对称加密密码,随后凭此密码对encrypted_msg.txt进行解密,得到decrypted_msg.txt
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    def des_pwd_decrypt():
    with open('sent/encrypted_pwd_des.txt', 'r') as des_pwd_container:
    des_pwd_encrypted = des_pwd_container.readline()
    with open('sandbox_receiver/private_key_receiver.txt', 'r') as prv_key_radio:
    private_key_pair_string = prv_key_radio.readline()
    private_key_pair_string2pair_step_1 = private_key_pair_string.split(', ')
    private_key_pair_string2pair_step_2_a = private_key_pair_string2pair_step_1[0].removeprefix('(')
    private_key_pair_string2pair_step_2_b = private_key_pair_string2pair_step_1[1].removesuffix(')')
    private_key_pair = int(private_key_pair_string2pair_step_2_a), int(private_key_pair_string2pair_step_2_b)
    ## print(public_key_pair_string2pair_step_2_a)
    ## print(public_key_pair_string2pair_step_2_b)
    rsa_decrypt_service = RSA(private_key_pair)
    encoded_encrypted_des_pwd = Encode2int(des_pwd_encrypted)
    decrypted_encoded_des_pwd = rsa_decrypt_service.Decrypt(encoded_encrypted_des_pwd)
    return str(Decode2chr(decrypted_encoded_des_pwd))


    ## The function to decrypt the message basing on the symmetric password decrypted (finished May 31 12:13 a.m.)
    def message_decrypt():
    decrypted_lines = ""
    with open('sandbox_sender/desPassword.txt', 'r') as des_password_gotcha:
    des_pwd = des_pwd_decrypt()
    with open('sent/encrypted_msg.txt', 'r') as encrypted_message_container:
    encrypted_message = encrypted_message_container.readline()
    for i in range(int(len(encrypted_message) / 8)):
    newTempText = [encrypted_message[j] for j in range(i * 8, i * 8 + 8)]
    decrypted_lines = "".join([decrypted_lines, DES(newTempText, des_pwd, 1)])
    ## print(decrypted_lines)
    with open('inbox/decrypted_msg.txt', 'w') as container_of_decrypted_contents:
    container_of_decrypted_contents.write(decrypted_lines)
    print("Message Decrypted.")

  2. 使用发送方的非对称加密公钥对encrypted_hash.txt进行解密,得到明文decrypted_hash.txt。随后自己再生成一次解密后的信息和密码对应的散列值,与decrypted_hash.txt进行比较;如果相同则代表信息在传输途中未被修改。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    def hash_decrypt():
    with open('sent/encrypted_hash.txt', 'r') as hash_receipt:
    encrypted_hash = hash_receipt.readline()
    with open('public_key_sender.txt', 'r') as public_key_sender:
    public_key_sender_string = public_key_sender.readline()
    public_key_pair_string2pair_step_1 = public_key_sender_string.split(', ')
    public_key_pair_string2pair_step_2_a = public_key_pair_string2pair_step_1[0].removeprefix('(')
    public_key_pair_string2pair_step_2_b = public_key_pair_string2pair_step_1[1].removesuffix(')')
    public_rsa_key_pair = int(public_key_pair_string2pair_step_2_a), int(public_key_pair_string2pair_step_2_b)
    rsa_decrypt_service = RSA(public_rsa_key_pair)
    encoded_hash = Encode2int(encrypted_hash)
    encrypted_hash = rsa_decrypt_service.Decrypt(encoded_hash)
    encrypted_hash_chr = Decode2chr(encrypted_hash)
    with open('inbox/hash_decrypted.txt', 'w') as decrypted_hash:
    decrypted_hash.write(str(encrypted_hash_chr))
    print("Hash Decrypted.")


    def hash_check():
    with open('inbox/hash_decrypted.txt', 'r') as hash_decrypted:
    hash_received = hash_decrypted.readline()
    hash_received_list = hash_received.split(" ")
    hash_received_1 = hash_received_list[0]
    hash_received_2 = hash_received_list[1]
    with open('sent/encrypted_msg.txt', 'r') as sample:
    message = sample.read()
    with open('sent/encrypted_pwd_des.txt', 'r') as encrypted_pwd:
    password = encrypted_pwd.readline()
    message_hash = MD5(message)
    message_hash.fill_text()
    message_hash_str = message_hash.group_processing()
    if hash_received_1 != message_hash_str:
    print(message_hash_str)
    print("FATAL: Message has been modified half way.")
    password_hash = MD5(password)
    password_hash.fill_text()
    password_hash_str = password_hash.group_processing()
    if hash_received_2 != password_hash_str:
    print("FATAL: Symmetric password has been modified half way.")
    if hash_received_1 == message_hash_str and hash_received_2 == password_hash_str:
    print("Message not vandalized. Transmit success.")

操作记录

根据上述运行一系列程序。首先运行的是initialization.py文件,对整个实验环境进行初始化。初始化完成后的输出结果和文件架构如下图所示;可以看到,目前的文件树只有sandbox_sender中有文件。这两个文件分别是待处理和发送的信息和对称加密密码。

随后运行fileCipher_keyProvider.py。运行后的结果如图所示;此时该程式通过随机生成的质数197和239产生了发送方的非对称加密公私钥,并将前者放置在公共空间,将后者放置在sandbox_sender中;类似地,随机生成的质数157和239产生了接收方的非对称加密公私钥,并将前者放置在公共空间,将后者放置在sandbox_receiver中。

此时运行fileCipher_sender.py。运行后的结果如图所示;此时文件树已经在outbox中出现了信息、对称密码和散列值加密后的文件。

随后运行fileTransmit.py。运行后,outbox中的文件已经全部转移至sent

之后运行fileCipher_receiver.py。运行后,在inbox中得到信息明文和散列值。将信息明文decrypted_msg.txt与原信息sample.txt对比,发现文本内容一致;程式将解密后的散列值文件decrypted_hash.txt中的两段散列值分别与根据解密所得信息和对称加密密码重新生成的散列值进行比较,发现两散列值均一致,故输出信息未被损坏(“Message not vandalized”),进而得到传输成功的结论。

实验心得

实验过程中因为使用python,故不得不使用该语言的文件有关指令。在编写过程中进行调试时发现虽然python可以创建文件但是不能创建文件夹,因此为initialization.py额外增加了确认路径是否存在的操作。

本次实验虽然只进行了两个午后,但加深了我对密码学中比较具有代表性的这些典型加密方式对理解。不难看出,密码之所以“密”,恰因为每时每刻都有机制在进行更新;通过多种加密方式混合使用,可以很大程度上解决并不知道收发信双方约定加/解密顺序的情况下成功破译的潜在隐患。文件加密传输是密码学综合应用的集大成之作;在我们针对此实验集中精力于收发信双方的同时,作为密钥分发者的管理员也起着非常重要的作用。如果分发密钥者没有妥善保管好双方的私钥,纵使收发双方妥善保管了依然存在传输的信息被截获或被篡改的可能。只有整个文件加密传输过程中任何一个节点都能够保证符合流程规定,这个传输的过程才可以确定是安全的。