]> Cypherpunks.ru repositories - pygost.git/commitdiff
MGM mode 4.8
authorSergey Matveev <stargrave@stargrave.org>
Fri, 24 Jul 2020 16:22:20 +0000 (19:22 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Fri, 24 Jul 2020 16:55:38 +0000 (19:55 +0300)
README
VERSION
install.texi
news.texi
pygost/__init__.py
pygost/mgm.py [new file with mode: 0644]
pygost/stubs/pygost/mgm.pyi [new file with mode: 0644]
pygost/test.do
pygost/test_mgm.py [new file with mode: 0644]
www.texi

diff --git a/README b/README
index 5a5035b23f6ddb4cffd1efcdb536da2204134603..24b28082b2cb63276fdb07e47b78c343c062335a 100644 (file)
--- a/README
+++ b/README
@@ -23,6 +23,7 @@ GOST is GOvernment STandard of Russian Federation (and Soviet Union).
 * 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)
+* MGM AEAD mode for 64 and 128 bit ciphers
 * PEP247-compatible hash/MAC functions
 
 Known problems: low performance and non time-constant calculations.
diff --git a/VERSION b/VERSION
index 4f8c639658e5402d16ccf26e328856c263c891f8..ef216a53f54ef766b2415c07b489089de9c3914f 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-4.7
+4.8
index edfa486f669b4b905f9c281946ca44054da2595c..2a518a7a09067232d1e29fbb4a48848f961ed1f5 100644 (file)
@@ -1,7 +1,7 @@
 @node Download
 @unnumbered Download
 
-@set VERSION 4.7
+@set VERSION 4.8
 
 No additional dependencies except Python 2.7/3.x interpreter are required.
 
index f8c0e82675d7baa0a3687b7e087697e9b9b41bc6..cccd4f3601c2e72f78d968967cecf7de8297b023 100644 (file)
--- a/news.texi
+++ b/news.texi
@@ -3,6 +3,10 @@
 
 @table @strong
 
+@anchor{Release 4.8}
+@item 4.8
+MGM AEAD mode for 64 and 128 bit ciphers.
+
 @anchor{Release 4.7}
 @item 4.7
 Removed @code{gost28147.addmod} for simplicity.
index 4d1c86ad4882915ab5f478cae681e0d3b224de01..488683d9899eb82efeed3e345c0c55317d3061cc 100644 (file)
@@ -3,4 +3,4 @@
 PyGOST is free software: see the file COPYING for copying conditions.
 """
 
-__version__ = "4.7"
+__version__ = "4.8"
diff --git a/pygost/mgm.py b/pygost/mgm.py
new file mode 100644 (file)
index 0000000..0321628
--- /dev/null
@@ -0,0 +1,168 @@
+# coding: utf-8
+# PyGOST -- Pure Python GOST cryptographic functions library
+# Copyright (C) 2015-2020 Sergey Matveev <stargrave@stargrave.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+""" Multilinear Galois Mode (MGM) block cipher mode.
+"""
+
+from hmac import compare_digest
+
+from pygost.gost3413 import pad1
+from pygost.utils import bytes2long
+from pygost.utils import long2bytes
+from pygost.utils import strxor
+
+
+def _incr(data, bs):
+    return long2bytes(bytes2long(data) + 1, size=bs // 2)
+
+
+def incr_r(data, bs):
+    return data[:bs // 2] + _incr(data[bs // 2:], bs)
+
+
+def incr_l(data, bs):
+    return _incr(data[:bs // 2], bs) + data[bs // 2:]
+
+
+def nonce_prepare(nonce):
+    """Prepare nonce for MGM usage
+
+    It just clears MSB.
+    """
+    n = bytearray(nonce)
+    n[0] &= 0x7F
+    return bytes(n)
+
+
+class MGM(object):
+    # Implementation is fully based on go.cypherpunks.ru/gogost/mgm
+    def __init__(self, encrypter, bs, tag_size=None):
+        """Multilinear Galois Mode (MGM) block cipher mode
+
+        :param encrypter: encrypting function, that takes block as an input
+        :param int bs: cipher's blocksize
+        :param int tag_size: authentication tag size
+                             (defaults to blocksize if not specified)
+        """
+        if bs not in (8, 16):
+            raise ValueError("only 64/128-bit blocksizes allowed")
+        self.tag_size = bs if tag_size is None else bs
+        if self.tag_size < 4 or self.tag_size > bs:
+            raise ValueError("invalid tag_size")
+        self.encrypter = encrypter
+        self.bs = bs
+        self.max_size = (1 << (bs * 8 // 2)) - 1
+        self.r = 0x1B if bs == 8 else 0x87
+
+    def _validate_nonce(self, nonce):
+        if len(nonce) != self.bs:
+            raise ValueError("nonce length must be equal to cipher's blocksize")
+        if bytearray(nonce)[0] & 0x80 > 0:
+            raise ValueError("nonce must not have higher bit set")
+
+    def _validate_sizes(self, plaintext, additional_data):
+        if len(plaintext) == 0 and len(additional_data) == 0:
+            raise ValueError("at least one of plaintext or additional_data required")
+        if len(plaintext) + len(additional_data) > self.max_size:
+            raise ValueError("plaintext+additional_data are too big")
+
+    def _mul(self, x, y):
+        x = bytes2long(x)
+        y = bytes2long(y)
+        z = 0
+        max_bit = 1 << (self.bs * 8 - 1)
+        while y > 0:
+            if y & 1 == 1:
+                z ^= x
+            if x & max_bit > 0:
+                x = ((x ^ max_bit) << 1) ^ self.r
+            else:
+                x <<= 1
+            y >>= 1
+        return long2bytes(z, size=self.bs)
+
+    def _crypt(self, icn, data):
+        icn[0] &= 0x7F
+        enc = self.encrypter(bytes(icn))
+        res = []
+        while len(data) > 0:
+            res.append(strxor(self.encrypter(enc), data))
+            enc = incr_r(enc, self.bs)
+            data = data[self.bs:]
+        return b"".join(res)
+
+    def _auth(self, icn, text, ad):
+        icn[0] |= 0x80
+        enc = self.encrypter(bytes(icn))
+        _sum = self.bs * b"\x00"
+        ad_len = len(ad)
+        text_len = len(text)
+        while len(ad) > 0:
+            _sum = strxor(_sum, self._mul(
+                self.encrypter(enc),
+                pad1(ad[:self.bs], self.bs),
+            ))
+            enc = incr_l(enc, self.bs)
+            ad = ad[self.bs:]
+        while len(text) > 0:
+            _sum = strxor(_sum, self._mul(
+                self.encrypter(enc),
+                pad1(text[:self.bs], self.bs),
+            ))
+            enc = incr_l(enc, self.bs)
+            text = text[self.bs:]
+        _sum = strxor(_sum, self._mul(self.encrypter(enc), (
+            long2bytes(ad_len * 8, size=self.bs // 2) +
+            long2bytes(text_len * 8, size=self.bs // 2)
+        )))
+        return self.encrypter(_sum)[:self.tag_size]
+
+    def seal(self, nonce, plaintext, additional_data):
+        """Seal plaintext
+
+        :param bytes nonce: blocksize-sized nonce.
+                            Assure that it does not have MSB bit set
+                            (:py:func:`pygost.mgm.nonce_prepare` helps)
+        :param bytes plaintext: plaintext to be encrypted and authenticated
+        :param bytes additional_data: additional data to be authenticated
+        """
+        self._validate_nonce(nonce)
+        self._validate_sizes(plaintext, additional_data)
+        icn = bytearray(nonce)
+        ciphertext = self._crypt(icn, plaintext)
+        tag = self._auth(icn, ciphertext, additional_data)
+        return ciphertext + tag
+
+    def open(self, nonce, ciphertext, additional_data):
+        """Open ciphertext
+
+        :param bytes nonce: blocksize-sized nonce.
+                            Assure that it does not have MSB bit set
+                            (:py:func:`pygost.mgm.nonce_prepare` helps)
+        :param bytes ciphertext: ciphertext to be decrypted and authenticated
+        :param bytes additional_data: additional data to be authenticated
+        :raises ValueError: if ciphertext authentication fails
+        """
+        self._validate_nonce(nonce)
+        self._validate_sizes(ciphertext, additional_data)
+        icn = bytearray(nonce)
+        ciphertext, tag_expected = (
+            ciphertext[:-self.tag_size],
+            ciphertext[-self.tag_size:],
+        )
+        tag = self._auth(icn, ciphertext, additional_data)
+        if not compare_digest(tag_expected, tag):
+            raise ValueError("invalid authentication tag")
+        return self._crypt(icn, ciphertext)
diff --git a/pygost/stubs/pygost/mgm.pyi b/pygost/stubs/pygost/mgm.pyi
new file mode 100644 (file)
index 0000000..81906b7
--- /dev/null
@@ -0,0 +1,17 @@
+from typing import Callable
+
+
+def nonce_prepare(nonce: bytes) -> bytes: ...
+
+
+class MGM(object):
+    def __init__(
+        self,
+        encrypter: Callable[[bytes], bytes],
+        bs: int,
+        tag_size: int = None,
+    ) -> None: ...
+
+    def seal(self, nonce: bytes, plaintext: bytes, additional_data: bytes) -> bytes: ...
+
+    def open(self, nonce: bytes, ciphertext: bytes, additional_data: bytes) -> bytes: ...
index e4fd871dfb4f440b24369930904fcd5936810002..06b9f9763a70ca267d34ae6756584f97006efafc 100644 (file)
@@ -1,13 +1,19 @@
-PYTHON=${PYTHON:-python}
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost28147
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost28147_mac
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost341194
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost34112012
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost3410
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost3410_vko
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_wrap
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost3412
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_gost3413
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_x509
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_cms
-PYTHONPATH=$PYTHONPATH:.. $PYTHON -m unittest test_pfx
+mods="
+gost28147
+gost28147_mac
+gost341194
+gost34112012
+gost3410
+gost3410_vko
+wrap
+gost3412
+gost3413
+mgm
+x509
+cms
+pfx
+"
+
+for mod in $mods ; do
+    PYTHONPATH=$PYTHONPATH:.. ${PYTHON:=python} -m unittest test_$mod
+done
diff --git a/pygost/test_mgm.py b/pygost/test_mgm.py
new file mode 100644 (file)
index 0000000..0d692b6
--- /dev/null
@@ -0,0 +1,57 @@
+# coding: utf-8
+# PyGOST -- Pure Python GOST cryptographic functions library
+# Copyright (C) 2015-2020 Sergey Matveev <stargrave@stargrave.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from os import urandom
+from random import randint
+from unittest import TestCase
+
+from pygost.gost3412 import GOST3412Magma
+from pygost.gost3412 import GOST3412Kuznechik
+from pygost.mgm import MGM
+from pygost.mgm import nonce_prepare
+from pygost.utils import hexdec
+
+
+class TestVector(TestCase):
+    def runTest(self):
+        key = hexdec("8899AABBCCDDEEFF0011223344556677FEDCBA98765432100123456789ABCDEF")
+        ad = hexdec("0202020202020202010101010101010104040404040404040303030303030303EA0505050505050505")
+        plaintext = hexdec("1122334455667700FFEEDDCCBBAA998800112233445566778899AABBCCEEFF0A112233445566778899AABBCCEEFF0A002233445566778899AABBCCEEFF0A0011AABBCC")
+        mgm = MGM(GOST3412Kuznechik(key).encrypt, 16)
+        ciphertext = mgm.seal(plaintext[:16], plaintext, ad)
+        self.assertSequenceEqual(ciphertext[:len(plaintext)], hexdec("A9757B8147956E9055B8A33DE89F42FC8075D2212BF9FD5BD3F7069AADC16B39497AB15915A6BA85936B5D0EA9F6851CC60C14D4D3F883D0AB94420695C76DEB2C7552"))
+        self.assertSequenceEqual(ciphertext[len(plaintext):], hexdec("CF5D656F40C34F5C46E8BB0E29FCDB4C"))
+        self.assertSequenceEqual(mgm.open(plaintext[:16], ciphertext, ad), plaintext)
+
+
+class TestSymmetric(TestCase):
+    def _itself(self, mgm, bs):
+        for _ in range(1000):
+            nonce = nonce_prepare(urandom(bs))
+            ad = urandom(randint(0, 20))
+            pt = urandom(randint(0, 20))
+            if len(ad) + len(pt) == 0:
+                continue
+            ct = mgm.seal(nonce, pt, ad)
+            self.assertSequenceEqual(mgm.open(nonce, ct, ad), pt)
+
+    def test_magma(self):
+        mgm = MGM(GOST3412Magma(urandom(32)).encrypt, 8)
+        self._itself(mgm, 8)
+
+    def test_kuznechik(self):
+        mgm = MGM(GOST3412Kuznechik(urandom(32)).encrypt, 16)
+        self._itself(mgm, 16)
index 9b4e79f901077ff3c4a9ba42b2606ca4d5a7b8de..013780b332a76a620d44978a221077e211d5697f 100644 (file)
--- a/www.texi
+++ b/www.texi
@@ -55,6 +55,7 @@ Currently supported algorithms are:
 @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 MGM AEAD mode for 64 and 128 bit ciphers
 @item PEP247-compatible hash/MAC functions
 @end itemize