Package: kyber-py
CRYSTALS-Kyber Python ImplementationThis repository contains a pure python implementation of CRYSTALS-Kyber
following (at the time of writing) the most recentspecification(v3.02)Disclaimer:warning:Under no circumstances should this be used for a cryptographic application.:warning:I have writtenkyber-pyas a way to learn about the way Kyber works, and to
try and create a clean, well commented implementation which people can learn
from.This code is not constant time, or written to be performant. Rather, it was
written so that reading though Algorithms 1-9 in thespecificationclosely matches the code which is seen inkyber.py.KATsThis implementation currently passes all KAT tests from the reference implementation.
For more information, see the unit tests intest_kyber.py.Note: there is a discrepancy between the specification and reference implementation.
To ensure all KATs pass, I have to generate the public keybeforethe random
bytes $z = \mathcal{B}^{32}$ in algorithm 7 of thespecification(v3.02).DependenciesOriginally this was planned to have zero dependencies, however to make this work
pass the KATs, I needed a deterministic CSRNG. The reference implementation uses
AES256 CTR DRBG. I have implemented this inaes256_ctr_drbg.py.
However, I have not implemented AES itself, instead I import this frompycryptodome.To install dependencies, runpip -r install requirements.If you're happy to use system randomness (os.urandom) then you don't need
this dependency.Using kyber-pyThere are three functions exposed on theKyberclass which are intended
for use:Kyber.keygen(): generate a keypair(pk, sk)Kyber.enc(pk): generate a challenge and a shared key(c, K)Kyber.dec(sk, c): generate the shared keyKTo useKyber()it must be initialised with a dictionary of the
protocol parameters. An example can be seen inDEFAULT_PARAMETERS.Additionally, the class has been initialised with these default parameters,
so you can simply import the NIST level you want to play with:Example>>>fromkyberimportKyber512>>>pk,sk=Kyber512.keygen()>>>c,key=Kyber512.enc(pk)>>>_key=Kyber512.dec(c,sk)>>>assertkey==_keyThe above example would also work withKyber768andKyber1024.BenchmarksTODO: Better benchmarks? Although this was never about speed haha.For now, here are some approximate benchmarks:1000 IterationsKyber512Kyber768Kyber1024KeyGen()6.868s10.820s16.172sEnc()10.677s16.094s22.341sDec()16.822s25.979s33.524sAll times recorded using a Intel Core i7-9750H CPU.Future PlansAdd documentation onNTTtransform for polynomialsAdd documentation for working with DRBG and setting the seedInclude DilithiumUsingpolynomials.pyandmodules.pythis work could be extended to
have a pure python implementation of CRYSTALS-Dilithium too.I suppose then this repo should be calledcrystals-pybut I wont
get ahead of myself.Discussion of ImplementationKyberTODO:

Add some more information about how working with Kyber works with this
library...PolynomialsThe filepolynomials.pycontains the classesPolynomialRingandPolynomial. This implements the univariate polynomial ring$$
R_q = \mathbb{F}_q[X] /(X^n + 1)
$$The implementation is inspired bySageMathand you can create the
ring $R_{11} = \mathbb{F}_{11}[X] /(X^8 + 1)$ in the following way:Example>>>R=PolynomialRing(11,8)>>>x=R.gen()>>>f=3*x**3+4*x**7>>>g=R.random_element();g5+x^2+5*x^3+4*x^4+x^5+3*x^6+8*x^7>>>f*g8+9*x+10*x^3+7*x^4+2*x^5+5*x^6+10*x^7>>>f+f6*x^3+8*x^7>>>g-g0We additionally include functions forPolynomialRingandPolynomialto move from bytes to polynomials (and back again).PolynomialRingparse(bytes)takes $3n$ bytes and produces a random polynomial in $R_q$decode(bytes, l)takes $\ell n$ bits and produces a polynomial in $R_q$cbd(beta, eta)takes $\eta \cdot n / 4$ bytes and produces a polynomial in $R_q$ with coefficents taken from a centered binomial distributionPolynomialself.encode(l)takes the polynomial and returns a length $\ell n / 8$ bytearrayExample>>>R=PolynomialRing(11,8)>>>f=R.random_element()>>># If we do not specify `l` then it is computed for us (minimal value)>>>f_bytes=f.encode()>>>f_bytes.hex()'06258910'>>>R.decode(f_bytes)==fTrue>>># We can also set `l` ourselves>>>f_bytes=f.encode(l=10)>>>f_bytes.hex()'00180201408024010000'>>>R.decode(f_bytes,l=10)==fTrueLastly, we define aself.compress(d)andself.decompress(d)method for
polynomials following page 2 of thespecification$$
\textsf{compress}_q(x, d) = \lceil (2^d / q) \cdot x \rfloor \textrm{mod}^+ 2^d,
$$$$
\textsf{decompress}_q(x, d) = \lceil (q / 2^d) \cdot x \rfloor.
$$The functionscompressanddecompressare defined for the coefficients
of a polynomial and a polynomial is (de)compressed by acting the function
on every coefficient.
Similarly, an element of a module is (de)compressed by acting the
function on every polynomial.Example>>>R=PolynomialRing(11,8)>>>f=R.random_element()>>>f9+3*x+5*x^2+2*x^3+9*x^4+10*x^5+6*x^6+x^7>>>f.compress(1)x+x^2+x^6>>>f.decompress(1)6*x+6*x^2+6*x^6Note: compression is lossy! We do not get the same polynomial back
by computingf.compress(d).decompress(d). They are howeverclose.
See the specification for more information.Number Theoretic TransformTODO:

This is now handled by `NTTHelper` which is passed to `PolynomialRing`
and has functions which are accessed by `Polynomial`.

Talk about what is available, and how they are used.ModulesThe filemodules.pycontains the classesModuleandMatrix.
A module is a generalisation of a vector space, where the field
of scalars is replaced with a ring. In the case of Kyber, we
need the module with the ring $R_q$ as described above.Matrixallows elements of the module to be of size $m \times n$
but for Kyber, we only need vectors of length $k$ and square
matricies of size $k \times k$.As an example of the operations we can perform with outModulelets revisit the ring from the previous example:Example>>>R=PolynomialRing(11,8)>>>x=R.gen()>>>>>>M=Module(R)>>># We create a matrix by feeding the coefficients to M>>>A=M([[x+3*x**2,4+3*x**7],[3*x**3+9*x**7,x**4]])>>>A[x+3*x^2,4+3*x^7][3*x^3+9*x^7,x^4]>>># We can add and subtract matricies of the same size>>>A+A[2*x+6*x^2,8+6*x^7][6*x^3+7*x^7,2*x^4]>>>A-A[0,0][0,0]>>># A vector can be constructed by a list of coefficents>>>v=M([3*x**5,x])>>>v[3*x^5,x]>>># We can compute the transpose>>>v.transpose()[3*x^5][x]>>>v+v[6*x^5,2*x]>>># We can also compute the transpose in place>>>v.transpose_self()[3*x^5][x]>>>v+v[6*x^5][2*x]>>># Matrix multiplication follows python standards and is denoted by @>>>A@v[8+4*x+3*x^6+9*x^7][2+6*x^4+x^5]We also carry throughMatrix.encode()andModule.decode(bytes, n_rows, n_cols)which simply use the above functions defined for polynomials and run for each
element.ExampleWe can see how encoding / decoding a vector works in the following example.
Note that we can swap the rows/columns to decode bytes into the transpose
when working with a vector.>>>R=PolynomialRing(11,8)>>>M=Module(R)>>>v=M([R.random_element()for_inrange(2)])>>>v_bytes=v.encode()>>>v_bytes.hex()'d'>>>M.decode(v_bytes,1,2)==vTrue>>>v_bytes=v.encode(l=10)>>>v_bytes.hex()'a014020100103004000040240a03009030080200'>>>M.decode(v_bytes,1,2,l=10)==vTrue>>>M.decode(v_bytes,2,1,l=10)==v.transpose()True>>># We can also compress and decompress elements of the module>>>v[5+10*x+4*x^2+2*x^3+8*x^4+3*x^5+2*x^6,2+9*x+5*x^2+3*x^3+9*x^4+3*x^5+x^6+x^7]>>>v.compress(1)[1+x^2+x^4+x^5,x^2+x^3+x^5]>>>v.decompress(1)[6+6*x^2+6*x^4+6*x^5,6*x^2+6*x^3+6*x^5]Baby KyberA great resource for learning Kyber is available atApproachable Cryptography.We include code corresponding to their example inbaby_kyber.py.