How does Kyber Encryption Work

Table of Contents

1. 简介

Kyber 是一种后量子(Post-Quantum Cryptography)公钥加密算法,能够抵抗量子计算机的攻击,本文将介绍它。

2. Baby Kyber

下面我们将介绍 Baby Kyber,它和常规 Kyber 相比,安全参数更小,只保留了最核心的过程,比如省略了对密文的压缩等。这样使得 Baby Kyber 的可读性更好,方便理解。

与其他公钥系统一样,Kyber 也需要一个模数 q 。也就是说,每次我们用数字进行计算时,我们都会对结果取模。对于 Baby Kyber,我们将此模数指定为 q=17

对于 Kyber,我们必须处理多项式。由于我们将多项式相乘和相加,我们将对多项式进行模多项式运算,否则他们的次数会变得太大而我们无法处理。我们将使用的多项式模数是 f=x4+1 ,这将保证多项式的次数(最高指数)小于 4。

下面的所有计算都隐含着 1. 对多项式的系数进行模数 q=17 计算,2. 对多项式本身进行模 f=x4+1 多项式运算。

注:如果您不了解多项式模多项式的运算,可以参考 Cryptography and Network Security - Principles and Practice, 8th edition, 5.5 Polynomial Arithmetic

2.1. Key Generation

Kyber 的私钥 s 由一些随机多项式组成,且要求它们的系数值很小。关于要求系数值很小的原因可参见节 2.3.1

对于 Baby Kyber 来说,假设私钥 s 为两个多项式 s=(x3x2+xx3x) 它们是随机产生的多项式系数很小的多项式。注: “系数很小”的意思是“和 0 或者系数模数 q=17 比较接近”。 上面的私钥 s 中多次出来的 1 系数做个模 17 运算后就是 16 ,尽管和 0 比不是很接近,但它和模数 17 很接近,所以也符合“系数很小”的要求。

Kyber 的公钥由两部分组成:随机矩阵多项式 A 和另一个矩阵 t 在 Baby Kyber 例子中,将使用 A=(6x3+16x2+16x+119x3+4x2+6x+35x3+3x2+10x+16x3+x2+9x+15) 前面提到过 Bady Kyber 中,对多项式的系数进行模数 q=17 计算,对多项式本身进行模 f=x4+1 多项式运算,所以 A 的多项式系数都小于 17 ,且多项式次数都小于 4

Kyber 公钥中另一部分 t 的计算公式为 tAs+e 其中 e 是错误矩阵,要求是随机产生,且多项式的系数比较小。在 Baby Kyber 例子中,将使用 e=(x2x2x)

从而,我们可以计算出公钥 ttAs+e=(16x3+15x2+710x3+12x2+11x+6)

2.1.1. 依赖的数学难题(Module-LWE)

前面介绍了 Kyber 私钥 s 和公钥 A,t 符合关系 t=As+e

已知 A,t ,很难恢复出 s ,这被称为 Module Learning With Errors(Module-LWE)问题,这对于量子计算机也很难,这就是 Kyber 所依赖的数学难题。

2.2. 加密

对于非对称加密系统来说,使用公钥对消息进行加密,使用私钥对消息进行解密。

Kyber 加密时,会使用 2 个一次一用的系数比较小的随机矩阵 r,e1 ,和 1 个一次一用的系数比较小的随机多项式 e2 在 Baby Kyber 例子中,将使用 r=(x3+x2x3+x21)e1=(x2+xx2)e2=x3x2

在加密消息前,我们需要先把消息编码为某个多项式。 我们可以使用消息的二进制表达作为多项式的系数。比如我们想加密数字 11 ,它的二进制表达为 (1011)2 ,所以待加密的数字 11 可以编码为多项式 mb=1x3+0x2+1x1+1x0=x3+x+1

在加密前,还需要把 mb 的系数放大,这是通过乘以 q2 来实现的(为什么在加密前要把 mb 放大呢?其原因可参考节 2.3.1)。其中记号 x 表示最接近实数 x 的整数,如果实数 x 恰好位于两个连续整数中间,则上向取整,比如 1.4=1,1.5=2 。在我们的例子中,有 q2=172=8.5=9

也就是说待加密的数字 11 最终编码为了 mq2mb=9x3+9x+9

使用公钥 A,t 对消息 m 进行加密,其密文由两个部分组成 (u,v) ,它的定义为 uATr+e1vtTr+e2+m

代入各个值,我们可以计算出数字 11 的密文的两个部分 (u,v) 分别为 u=(11x3+11x2+10x+34x3+4x2+13x+11)v=8x3+6x2+9x+16

2.3. 解密

使用私钥 s 可对密文 (u,v) 进行解密,解密步骤如下。

第一步,使用下面公式计算出带有噪声的解密结果(noisy result) mnoisyvsTu 代入私钥和密文的具体值,可得 mnoisy=8x3+142+8x+6

第二步,检查 mn 的各个系数,看它是更接近于 q2=9 ,还是更接近于 0 (或者 q=17 )。如果更接近于 q2 ,则把系数改为 q2 ;如果更接近于 0 (或者 q=17 ),则把系数改为 0 。下面依次检测一下 mn 的各个系数。

  • 系数 8 更接近于 9 ,所以改为 9
  • 系数 14 更接近于 0 (或者 q=17 ),所以改为 0
  • 系数 8 更接近于 9 ,所以改为 9
  • 系数 6 更接近于 9 ,所以改为 9

所以,得到解密多项式 9x3+0x2+9x+9 ,可以发现它就是加密前的多项式。

第三步,把多项式的缩小 q2=9 倍,就得到原始加密数据的二进制表达 mb=1q2(9x3+0x2+9x+9)=x3+0x2+x+1
所以,我们得到二进制数据 (1011)2 ,换算为十进制就是 11 ,它就是原始加密数据。

2.3.1. 解密的原理

为什么 mnoisy 是带有噪声的解密结果呢?我们按照 mnoisy 定义展开一下 mnoisyvsTu=tTr+e2+msT(ATr+e1)=(As+e)Tr+e2+msT(ATr+e1)=eTr+e2系数很小+msTe1系数很小

前面提到过,生成 e,r,e2,s,e1 时,要求多项式的系数很小;而对于消息 m 特意把它系数放大了。

所以, mnoisy 中的大头是 m ,其它项全面加起来也只是微不足道的噪声。

3. Kyber 总结

Kyber 加解密的总结如图 1 所示(改编自 https://youtu.be/gp7KFOs7y3g?t=3818 中截图)。

kyber.gif

Figure 1: Kyber 总结

需要注意的是,图 1 中采用的定义是 u=rA+e1,v=rt+e2+m,mnoisy=vus ,而 Kyber 论文(以及前面的例子)采用的定义是 u=ATr+e1,v=tTr+e2+m,mnoisy=vsTu ,为什么会有区别呢?其实它们是完全一样的,只是因为图 1r,e1 是“行向量”定义形式,而 Kyber 论文(以及前面的例子)中 r,e1 是“列向量”定义形式,行向量和列向量之间有个“矩阵转置操作”的区别,这导致 u,v,mnoisy 的形式有小的区别。

4. Kyber 和 ElGamal 的相似之处

从形式上看,Kyber 和 ElGamal 有相似之处,它们的密文都由两部分构成,而且每部分的产生过程也比较类似,如表 1 所示。从形式上说,可以认为 Kyber 是一种 noisy ElGamal 算法。

Table 1: Kyber 和 ElGamal 加密过程的相似之处
ElGamal 加密 Kyber 加密
密文第 1 部分 c1=gr 密文第 1 部分 u=ATr+e1
密文第 2 部分 c2=mhr 密文第 2 部分 v=m+tTr+e2
r 是一次一用随机数 r 是一次一用随机数
h 是公钥 t 是公钥

5. 参考

  1. 原论文 CRYSTALS – Kyber: a CCA-secure module-lattice-based KEM
  2. Kyber - How does it work?(本文 Bady Kyber 的例子来源这个文章,注:本文修正了这个文章中的几处错误)
  3. Cryptography and Network Security - Principles and Practice, 8th edition, 5.5 Polynomial Arithmetic
  4. CRYSTALS – Kyber and Dilithium, by Peter Schwabe

6. 附录

6.1. Baby Kyber 示例代码

下面代码演示了前面介绍的 Baby Kyber 例子。

# polynomials/modules from: https://github.com/GiacomoPope/kyber-py

from polynomials import *
from modules import *

q = 17
n = 4  # x^4 + 1
R = PolynomialRing(q, n)
M = Module(R)

s0 = R([0,1,-1,-1])
s1 = R([0,-1,0,-1])
s = M([s0,s1]).transpose()

A00 = R([11,16,16,6])
A01 = R([3,6,4,9])
A10 = R([1,10,3,5])
A11 = R([15,9,1,6])
A = M([[A00, A01],[A10, A11]])

e0 = R([0,0,1,0])
e1 = R([0,-1,1,0])
e = M([e0,e1]).transpose()

t = A @ s + e

print("s:",s)
print("A:",A)
print("e:",e)
print("t:",t)

r0 = R([0,0,1,-1])
r1 = R([-1,0,1,1])
r = M([r0, r1]).transpose()
print("r:",r)

e1_0 = R([0,1,1,0])
e1_1 = R([0,0,1,0])
e1 = M([e1_0, e1_1]).transpose()
print("e1:",e1)

e2 = M([R([0,0,-1,-1])])
print("e2:",e2)

m = M([R([9,9,0,9])])

# Encryption
u = A.transpose() @ r + e1
v = t.transpose() @ r + e2 + m
print("u:", u)
print("v:", v)

# Decryption
m_noisy = v -  s.transpose() @ u
print("m_noisy:", m_noisy)         # m_noisy: [6 + 8*x + 14*x^2 + 8*x^3]

Author: cig01

Created: <2023-08-06 Sun>

Last updated: <2023-08-07 Mon>

Creator: Emacs 27.1 (Org mode 9.4)