From 07c62fdaf6ba22a611492ff9ce734d68b5e65a66 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sat, 10 Jun 2017 23:49:16 +0300 Subject: [PATCH] Magma block cipher --- NEWS | 1 + README | 1 + pygost/gost3412.py | 37 ++++++-- pygost/gost3413.py | 30 +++---- pygost/stubs/pygost/gost3412.pyi | 8 ++ pygost/test_gost3412.py | 15 ++++ pygost/test_gost3413.py | 144 +++++++++++++++++++++++++++++++ www.texi | 3 + 8 files changed, 219 insertions(+), 20 deletions(-) diff --git a/NEWS b/NEWS index fd1faa5..7c6ca3f 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,6 @@ 3.3: * GOST3412Kuz renamed to GOST3412Kuznezhik + * GOST3412Magma implements GOST R 34.12-2015 Magma 64-bit block cipher 3.2: 34.13-2015 block cipher modes of operation implementations. diff --git a/README b/README index 60c3864..57009b0 100644 --- a/README +++ b/README @@ -17,6 +17,7 @@ GOST is GOvernment STandard of Russian Federation (and Soviet Union). * 28147-89 CryptoPro key meshing for CFB mode (RFC 4357) * RFC 4491 (using GOST algorithms with X.509) compatibility helpers * GOST R 34.12-2015 128-bit block cipher Кузнечик (Kuznechik) (RFC 7801) +* GOST R 34.12-2015 64-bit block cipher Магма (Magma) * GOST R 34.13-2015 padding methods and block cipher modes of operation (ECB, CTR, OFB, CBC, CFB, MAC) * PEP247-compatible hash/MAC functions diff --git a/pygost/gost3412.py b/pygost/gost3412.py index 6633186..fe96007 100644 --- a/pygost/gost3412.py +++ b/pygost/gost3412.py @@ -14,15 +14,15 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""GOST 34.12-2015 128-bit block cipher Кузнечик (Kuznechik) - -:rfc:`7801`. Pay attention that 34.12-2015 also defines 64-bit block -cipher Магма (Magma) -- it is **not** implemented here, but in gost28147 -module. +"""GOST 34.12-2015 64 and 128 bit block ciphers (:rfc:`7801`) Several precalculations are performed during this module importing. """ +from pygost.gost28147 import block2ns as gost28147_block2ns +from pygost.gost28147 import decrypt as gost28147_decrypt +from pygost.gost28147 import encrypt as gost28147_encrypt +from pygost.gost28147 import ns2block as gost28147_ns2block from pygost.utils import strxor from pygost.utils import xrange # pylint: disable=redefined-builtin @@ -152,3 +152,30 @@ class GOST3412Kuznechik(object): for i in range(9, 0, -1): blk = [PIinv[v] for v in Linv(bytearray(strxor(self.ks[i], blk)))] return bytes(strxor(self.ks[0], blk)) + + +class GOST3412Magma(object): + """GOST 34.12-2015 64-bit block cipher Магма (Magma) + """ + def __init__(self, key): + """ + :param key: encryption/decryption key + :type key: bytes, 32 bytes + """ + # Backward compatibility key preparation for 28147-89 key schedule + self.key = b"".join(key[i * 4:i * 4 + 4][::-1] for i in range(8)) + self.sbox = "Gost28147_tc26_ParamZ" + + def encrypt(self, blk): + return gost28147_ns2block(gost28147_encrypt( + self.sbox, + self.key, + gost28147_block2ns(blk[::-1]), + ))[::-1] + + def decrypt(self, blk): + return gost28147_ns2block(gost28147_decrypt( + self.sbox, + self.key, + gost28147_block2ns(blk[::-1]), + ))[::-1] diff --git a/pygost/gost3413.py b/pygost/gost3413.py index bd8cc49..c0201c9 100644 --- a/pygost/gost3413.py +++ b/pygost/gost3413.py @@ -132,12 +132,12 @@ def ofb(encrypter, bs, data, iv): For decryption you use the same function again. """ - if len(iv) != 2 * bs: + if len(iv) < 2 * bs or len(iv) % bs != 0: raise ValueError("Invalid IV size") - r = [iv[:bs], iv[bs:]] + r = [iv[i:i + bs] for i in range(0, len(iv), bs)] result = [] for i in xrange(0, len(data) + pad_size(len(data), bs), bs): - r = [r[1], encrypter(r[0])] + r = r[1:] + [encrypter(r[0])] result.append(strxor(r[1], data[i:i + bs])) return b"".join(result) @@ -152,13 +152,13 @@ def cbc_encrypt(encrypter, bs, pt, iv): """ if not pt or len(pt) % bs != 0: raise ValueError("Plaintext is not blocksize aligned") - if len(iv) != 2 * bs: + if len(iv) < 2 * bs or len(iv) % bs != 0: raise ValueError("Invalid IV size") - r = [iv[:bs], iv[bs:]] + r = [iv[i:i + bs] for i in range(0, len(iv), bs)] ct = [] for i in xrange(0, len(pt), bs): ct.append(encrypter(strxor(r[0], pt[i:i + bs]))) - r = [r[1], ct[-1]] + r = r[1:] + [ct[-1]] return b"".join(ct) @@ -172,14 +172,14 @@ def cbc_decrypt(decrypter, bs, ct, iv): """ if not ct or len(ct) % bs != 0: raise ValueError("Ciphertext is not blocksize aligned") - if len(iv) != 2 * bs: + if len(iv) < 2 * bs or len(iv) % bs != 0: raise ValueError("Invalid IV size") - r = [iv[:bs], iv[bs:]] + r = [iv[i:i + bs] for i in range(0, len(iv), bs)] pt = [] for i in xrange(0, len(ct), bs): blk = ct[i:i + bs] pt.append(strxor(r[0], decrypter(blk))) - r = [r[1], blk] + r = r[1:] + [blk] return b"".join(pt) @@ -191,13 +191,13 @@ def cfb_encrypt(encrypter, bs, pt, iv): :param bytes pt: plaintext :param bytes iv: double blocksize-sized initialization vector """ - if len(iv) != 2 * bs: + if len(iv) < 2 * bs or len(iv) % bs != 0: raise ValueError("Invalid IV size") - r = [iv[:bs], iv[bs:]] + r = [iv[i:i + bs] for i in range(0, len(iv), bs)] ct = [] for i in xrange(0, len(pt) + pad_size(len(pt), bs), bs): ct.append(strxor(encrypter(r[0]), pt[i:i + bs])) - r = [r[1], ct[-1]] + r = r[1:] + [ct[-1]] return b"".join(ct) @@ -209,14 +209,14 @@ def cfb_decrypt(encrypter, bs, ct, iv): :param bytes ct: ciphertext :param bytes iv: double blocksize-sized initialization vector """ - if len(iv) != 2 * bs: + if len(iv) < 2 * bs or len(iv) % bs != 0: raise ValueError("Invalid IV size") - r = [iv[:bs], iv[bs:]] + r = [iv[i:i + bs] for i in range(0, len(iv), bs)] pt = [] for i in xrange(0, len(ct) + pad_size(len(ct), bs), bs): blk = ct[i:i + bs] pt.append(strxor(encrypter(r[0]), blk)) - r = [r[1], blk] + r = r[1:] + [blk] return b"".join(pt) diff --git a/pygost/stubs/pygost/gost3412.pyi b/pygost/stubs/pygost/gost3412.pyi index c67a7da..1648cf4 100644 --- a/pygost/stubs/pygost/gost3412.pyi +++ b/pygost/stubs/pygost/gost3412.pyi @@ -4,3 +4,11 @@ class GOST3412Kuznechik(object): def encrypt(self, blk: bytes) -> bytes: ... def decrypt(self, blk: bytes) -> bytes: ... + + +class GOST3412Magma(object): + def __init__(self, key: bytes) -> None: ... + + def encrypt(self, blk: bytes) -> bytes: ... + + def decrypt(self, blk: bytes) -> bytes: ... diff --git a/pygost/test_gost3412.py b/pygost/test_gost3412.py index 6ab4c90..e0b0786 100644 --- a/pygost/test_gost3412.py +++ b/pygost/test_gost3412.py @@ -19,6 +19,7 @@ from unittest import TestCase from pygost.gost3412 import C from pygost.gost3412 import GOST3412Kuznechik +from pygost.gost3412 import GOST3412Magma from pygost.gost3412 import L from pygost.gost3412 import PI from pygost.utils import hexdec @@ -121,3 +122,17 @@ class KuznechikTest(TestCase): def test_decrypt(self): ciph = GOST3412Kuznechik(self.key) self.assertEqual(ciph.decrypt(self.ciphertext), self.plaintext) + + +class MagmaTest(TestCase): + key = hexdec("ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") + plaintext = hexdec("fedcba9876543210") + ciphertext = hexdec("4ee901e5c2d8ca3d") + + def test_encrypt(self): + ciph = GOST3412Magma(self.key) + self.assertEqual(ciph.encrypt(self.plaintext), self.ciphertext) + + def test_decrypt(self): + ciph = GOST3412Magma(self.key) + self.assertEqual(ciph.decrypt(self.ciphertext), self.plaintext) diff --git a/pygost/test_gost3413.py b/pygost/test_gost3413.py index 31a56f3..f1574ee 100644 --- a/pygost/test_gost3413.py +++ b/pygost/test_gost3413.py @@ -3,6 +3,7 @@ from random import randint from unittest import TestCase from pygost.gost3412 import GOST3412Kuznechik +from pygost.gost3412 import GOST3412Magma from pygost.gost3413 import _mac_ks from pygost.gost3413 import cbc_decrypt from pygost.gost3413 import cbc_encrypt @@ -169,3 +170,146 @@ class GOST3412KuznechikModesTest(TestCase): data = urandom(randint(0, 16 * 2)) ciph = GOST3412Kuznechik(urandom(32)) mac(ciph.encrypt, 16, data) + + +class GOST3412MagmaModesTest(TestCase): + key = hexdec("ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") + ciph = GOST3412Magma(key) + plaintext = "" + plaintext += "92def06b3c130a59" + plaintext += "db54c704f8189d20" + plaintext += "4a98fb2e67a8024c" + plaintext += "8912409b17b57e41" + iv = hexdec("1234567890abcdef234567890abcdef134567890abcdef12") + + def test_ecb_vectors(self): + ciphtext = "" + ciphtext += "2b073f0494f372a0" + ciphtext += "de70e715d3556e48" + ciphtext += "11d8d9e9eacfbc1e" + ciphtext += "7c68260996c67efb" + self.assertSequenceEqual( + hexenc(ecb_encrypt(self.ciph.encrypt, 8, hexdec(self.plaintext))), + ciphtext, + ) + self.assertSequenceEqual( + hexenc(ecb_decrypt(self.ciph.decrypt, 8, hexdec(ciphtext))), + self.plaintext, + ) + + def test_ecb_symmetric(self): + for _ in range(100): + pt = pad2(urandom(randint(0, 16 * 2)), 16) + ciph = GOST3412Magma(urandom(32)) + ct = ecb_encrypt(ciph.encrypt, 8, pt) + self.assertSequenceEqual(ecb_decrypt(ciph.decrypt, 8, ct), pt) + + def test_ctr_vectors(self): + ciphtext = "" + ciphtext += "4e98110c97b7b93c" + ciphtext += "3e250d93d6e85d69" + ciphtext += "136d868807b2dbef" + ciphtext += "568eb680ab52a12d" + iv = self.iv[:4] + self.assertSequenceEqual( + hexenc(ctr(self.ciph.encrypt, 8, hexdec(self.plaintext), iv)), + ciphtext, + ) + self.assertSequenceEqual( + hexenc(ctr(self.ciph.encrypt, 8, hexdec(ciphtext), iv)), + self.plaintext, + ) + + def test_ctr_symmetric(self): + for _ in range(100): + pt = urandom(randint(0, 16 * 2)) + iv = urandom(4) + ciph = GOST3412Magma(urandom(32)) + ct = ctr(ciph.encrypt, 8, pt, iv) + self.assertSequenceEqual(ctr(ciph.encrypt, 8, ct, iv), pt) + + def test_ofb_vectors(self): + iv = self.iv[:16] + ciphtext = "" + ciphtext += "db37e0e266903c83" + ciphtext += "0d46644c1f9a089c" + ciphtext += "a0f83062430e327e" + ciphtext += "c824efb8bd4fdb05" + self.assertSequenceEqual( + hexenc(ofb(self.ciph.encrypt, 8, hexdec(self.plaintext), iv)), + ciphtext, + ) + self.assertSequenceEqual( + hexenc(ofb(self.ciph.encrypt, 8, hexdec(ciphtext), iv)), + self.plaintext, + ) + + def test_ofb_symmetric(self): + for _ in range(100): + pt = urandom(randint(0, 16 * 2)) + iv = urandom(8 * 2) + ciph = GOST3412Magma(urandom(32)) + ct = ofb(ciph.encrypt, 8, pt, iv) + self.assertSequenceEqual(ofb(ciph.encrypt, 8, ct, iv), pt) + + def test_cbc_vectors(self): + ciphtext = "" + ciphtext += "96d1b05eea683919" + ciphtext += "aff76129abb937b9" + ciphtext += "5058b4a1c4bc0019" + ciphtext += "20b78b1a7cd7e667" + self.assertSequenceEqual( + hexenc(cbc_encrypt(self.ciph.encrypt, 8, hexdec(self.plaintext), self.iv)), + ciphtext, + ) + self.assertSequenceEqual( + hexenc(cbc_decrypt(self.ciph.decrypt, 8, hexdec(ciphtext), self.iv)), + self.plaintext, + ) + + def test_cbc_symmetric(self): + for _ in range(100): + pt = pad2(urandom(randint(0, 16 * 2)), 16) + iv = urandom(8 * 2) + ciph = GOST3412Magma(urandom(32)) + ct = cbc_encrypt(ciph.encrypt, 8, pt, iv) + self.assertSequenceEqual(cbc_decrypt(ciph.decrypt, 8, ct, iv), pt) + + def test_cfb_vectors(self): + iv = self.iv[:16] + ciphtext = "" + ciphtext += "db37e0e266903c83" + ciphtext += "0d46644c1f9a089c" + ciphtext += "24bdd2035315d38b" + ciphtext += "bcc0321421075505" + self.assertSequenceEqual( + hexenc(cfb_encrypt(self.ciph.encrypt, 8, hexdec(self.plaintext), iv)), + ciphtext, + ) + self.assertSequenceEqual( + hexenc(cfb_decrypt(self.ciph.encrypt, 8, hexdec(ciphtext), iv)), + self.plaintext, + ) + + def test_cfb_symmetric(self): + for _ in range(100): + pt = urandom(randint(0, 16 * 2)) + iv = urandom(8 * 2) + ciph = GOST3412Magma(urandom(32)) + ct = cfb_encrypt(ciph.encrypt, 8, pt, iv) + self.assertSequenceEqual(cfb_decrypt(ciph.encrypt, 8, ct, iv), pt) + + def test_mac_vectors(self): + k1, k2 = _mac_ks(self.ciph.encrypt, 8) + self.assertSequenceEqual(hexenc(k1), "5f459b3342521424") + self.assertSequenceEqual(hexenc(k2), "be8b366684a42848") + self.assertSequenceEqual( + hexenc(mac(self.ciph.encrypt, 8, hexdec(self.plaintext))[:4]), + "154e7210", + ) + + def test_mac_applies(self): + for _ in range(100): + data = urandom(randint(0, 16 * 2)) + ciph = GOST3412Magma(urandom(32)) + mac(ciph.encrypt, 8, data) diff --git a/www.texi b/www.texi index 6cb880b..5e9791e 100644 --- a/www.texi +++ b/www.texi @@ -49,6 +49,7 @@ Currently supported algorithms are: (using GOST algorithms with X.509) compatibility helpers @item GOST R 34.12-2015 128-bit block cipher Кузнечик (Kuznechik) (@url{https://tools.ietf.org/html/rfc7801.html, RFC 7801}) +@item GOST R 34.12-2015 64-bit block cipher Магма (Magma) @item GOST R 34.13-2015 padding methods and block cipher modes of operation (ECB, CTR, OFB, CBC, CFB, MAC) @item PEP247-compatible hash/MAC functions @@ -92,6 +93,8 @@ mailing list. Announcements also go to this mailing list. @item 3.3 @itemize @item @code{GOST3412Kuz} renamed to @code{GOST3412Kuznezhik} + @item @code{GOST3412Magma} implements GOST R 34.12-2015 Magma 64-bit + block cipher @end itemize @item 3.2 -- 2.44.0