]> Cypherpunks.ru repositories - pygost.git/commitdiff
34.13-2015 cipher modes of operation implementation 3.2
authorSergey Matveev <stargrave@stargrave.org>
Sun, 4 Jun 2017 14:33:49 +0000 (17:33 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sun, 4 Jun 2017 15:04:27 +0000 (18:04 +0300)
NEWS
README
VERSION
pygost/Makefile
pygost/gost28147.py
pygost/gost3413.py
pygost/stubs/pygost/gost3413.pyi
pygost/test_gost3413.py [new file with mode: 0644]
www.texi

diff --git a/NEWS b/NEWS
index 2fca8c897e52de29b6bbd063f62c6c7ec36276fa..7298105b970cab46cb9ef6cb438685f1bcb4ccb8 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,6 @@
+3.2:
+    34.13-2015 block cipher modes of operation implementations.
+
 3.1:
     Fixed mypy stubs related to PEP247-successors.
 
diff --git a/README b/README
index ef84b66aac0915c69e40a86641bcee7d9a481663..60c3864e70c01ed40d6846011887e6fdb3a05703 100644 (file)
--- a/README
+++ b/README
@@ -17,7 +17,8 @@ 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.13-2015 padding methods
+* GOST R 34.13-2015 padding methods and block cipher modes of operation
+  (ECB, CTR, OFB, CBC, CFB, MAC)
 * PEP247-compatible hash/MAC functions
 
 Known problems: low performance and non time-constant calculations.
@@ -45,7 +46,7 @@ Example 34.10-2012 keypair generation, signing and verifying:
     >>> verify(curve, pub, dgst, signature, mode=2012)
     True
 
-Other examples can be found in docstrings.
+Other examples can be found in docstrings and unittests.
 
 PyGOST is free software: see the file COPYING for copying conditions.
 
diff --git a/VERSION b/VERSION
index 8c50098d8aed57b02fd10f40a670a7c673b7c5a5..a3ec5a4bd3d7209b4a687a77cad49b945339994b 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1
+3.2
index 53eebf3276ccf98ca96f558080029aa9d99108af..35eda67372bbd042f10d28dd97a28c2cdca12b9b 100644 (file)
@@ -9,3 +9,4 @@ test:
        PYTHONPATH=.. $(PYTHON) -m unittest test_gost3410_vko
        PYTHONPATH=.. $(PYTHON) -m unittest test_wrap
        PYTHONPATH=.. $(PYTHON) -m unittest test_gost3412
+       PYTHONPATH=.. $(PYTHON) -m unittest test_gost3413
index 79ef968f7bbc964baf3c8941a5fb4e86a806f934..2b39844694a39a8a1a9864e5ea23f25132ef27c9 100644 (file)
@@ -26,6 +26,7 @@ from functools import partial
 
 from pygost.gost3413 import pad2
 from pygost.gost3413 import pad_size
+from pygost.gost3413 import unpad2
 from pygost.utils import hexdec
 from pygost.utils import strxor
 from pygost.utils import xrange  # pylint: disable=redefined-builtin
@@ -347,14 +348,7 @@ def cbc_decrypt(key, data, pad=True, sbox=DEFAULT_SBOX):
             data[i - BLOCKSIZE:i],
         ))
     if pad:
-        last_block = bytearray(plaintext[-1])
-        pad_index = last_block.rfind(b"\x80")
-        if pad_index == -1:
-            raise ValueError("Invalid padding")
-        for c in last_block[pad_index + 1:]:
-            if c != 0:
-                raise ValueError("Invalid padding")
-        plaintext[-1] = bytes(last_block[:pad_index])
+        plaintext[-1] = unpad2(plaintext[-1], BLOCKSIZE)
     return b"".join(plaintext)
 
 
index dc7339e864b21c2534ef9788e89489c8cabbd2ab..bd8cc49ad7e8c2ae20fa6cf5f6ced910b60a68a9 100644 (file)
 This module currently includes only padding methods.
 """
 
+from pygost.utils import bytes2long
+from pygost.utils import long2bytes
+from pygost.utils import strxor
+from pygost.utils import xrange
+
 
 def pad_size(data_size, blocksize):
-    """Calculate required pad size to full up BLOCKSIZE
+    """Calculate required pad size to full up blocksize
     """
     if data_size < blocksize:
         return blocksize - data_size
@@ -46,9 +51,207 @@ def pad2(data, blocksize):
     return data + b"\x80" + b"\x00" * pad_size(len(data) + 1, blocksize)
 
 
+def unpad2(data, blocksize):
+    """Unpad method 2
+    """
+    last_block = bytearray(data[-blocksize:])
+    pad_index = last_block.rfind(b"\x80")
+    if pad_index == -1:
+        raise ValueError("Invalid padding")
+    for c in last_block[pad_index + 1:]:
+        if c != 0:
+            raise ValueError("Invalid padding")
+    return data[:-(blocksize - pad_index)]
+
+
 def pad3(data, blocksize):
     """Padding method 3
     """
     if pad_size(len(data), blocksize) == 0:
         return data
     return pad2(data, blocksize)
+
+
+def ecb_encrypt(encrypter, bs, pt):
+    """ECB encryption mode of operation
+
+    :param encrypter: Encrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes pt: already padded plaintext
+    """
+    if not pt or len(pt) % bs != 0:
+        raise ValueError("Plaintext is not blocksize aligned")
+    ct = []
+    for i in xrange(0, len(pt), bs):
+        ct.append(encrypter(pt[i:i + bs]))
+    return b"".join(ct)
+
+
+def ecb_decrypt(decrypter, bs, ct):
+    """ECB decryption mode of operation
+
+    :param decrypter: Decrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes ct: ciphertext
+    """
+    if not ct or len(ct) % bs != 0:
+        raise ValueError("Ciphertext is not blocksize aligned")
+    pt = []
+    for i in xrange(0, len(ct), bs):
+        pt.append(decrypter(ct[i:i + bs]))
+    return b"".join(pt)
+
+
+def ctr(encrypter, bs, data, iv):
+    """Counter mode of operation
+
+    :param encrypter: Encrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes data: plaintext/ciphertext
+    :param bytes iv: half blocksize-sized initialization vector
+
+    For decryption you use the same function again.
+    """
+    if len(iv) != bs // 2:
+        raise ValueError("Invalid IV size")
+    stream = []
+    ctr_value = 0
+    for _ in xrange(0, len(data) + pad_size(len(data), bs), bs):
+        stream.append(encrypter(iv + long2bytes(ctr_value, bs // 2)))
+        ctr_value += 1
+    return strxor(b"".join(stream), data)
+
+
+def ofb(encrypter, bs, data, iv):
+    """OFB mode of operation
+
+    :param encrypter: Encrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes data: plaintext/ciphertext
+    :param bytes iv: double blocksize-sized initialization vector
+
+    For decryption you use the same function again.
+    """
+    if len(iv) != 2 * bs:
+        raise ValueError("Invalid IV size")
+    r = [iv[:bs], iv[bs:]]
+    result = []
+    for i in xrange(0, len(data) + pad_size(len(data), bs), bs):
+        r = [r[1], encrypter(r[0])]
+        result.append(strxor(r[1], data[i:i + bs]))
+    return b"".join(result)
+
+
+def cbc_encrypt(encrypter, bs, pt, iv):
+    """CBC encryption mode of operation
+
+    :param encrypter: Encrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes pt: already padded plaintext
+    :param bytes iv: double blocksize-sized initialization vector
+    """
+    if not pt or len(pt) % bs != 0:
+        raise ValueError("Plaintext is not blocksize aligned")
+    if len(iv) != 2 * bs:
+        raise ValueError("Invalid IV size")
+    r = [iv[:bs], 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]]
+    return b"".join(ct)
+
+
+def cbc_decrypt(decrypter, bs, ct, iv):
+    """CBC decryption mode of operation
+
+    :param decrypter: Decrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes ct: ciphertext
+    :param bytes iv: double blocksize-sized initialization vector
+    """
+    if not ct or len(ct) % bs != 0:
+        raise ValueError("Ciphertext is not blocksize aligned")
+    if len(iv) != 2 * bs:
+        raise ValueError("Invalid IV size")
+    r = [iv[:bs], 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]
+    return b"".join(pt)
+
+
+def cfb_encrypt(encrypter, bs, pt, iv):
+    """CFB encryption mode of operation
+
+    :param encrypter: Encrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes pt: plaintext
+    :param bytes iv: double blocksize-sized initialization vector
+    """
+    if len(iv) != 2 * bs:
+        raise ValueError("Invalid IV size")
+    r = [iv[:bs], 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]]
+    return b"".join(ct)
+
+
+def cfb_decrypt(encrypter, bs, ct, iv):
+    """CFB decryption mode of operation
+
+    :param encrypter: Encrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes ct: ciphertext
+    :param bytes iv: double blocksize-sized initialization vector
+    """
+    if len(iv) != 2 * bs:
+        raise ValueError("Invalid IV size")
+    r = [iv[:bs], 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]
+    return b"".join(pt)
+
+
+def _mac_shift(bs, data, xor_lsb=0):
+    num = (bytes2long(data) << 1) ^ xor_lsb
+    return long2bytes(num, bs)[-bs:]
+
+
+def _mac_ks(encrypter, bs):
+    Rb = 0b10000111 if bs == 16 else 0b11011
+    _l = encrypter(bs * b'\x00')
+    k1 = _mac_shift(bs, _l, Rb) if bytearray(_l)[0] & 0x80 > 0 else _mac_shift(bs, _l)
+    k2 = _mac_shift(bs, k1, Rb) if bytearray(k1)[0] & 0x80 > 0 else _mac_shift(bs, k1)
+    return k1, k2
+
+
+def mac(encrypter, bs, data):
+    """MAC (known here as CMAC, OMAC1) mode of operation
+
+    :param encrypter: Encrypting function, that takes block as an input
+    :param int bs: cipher's blocksize
+    :param bytes data: data to authenticate
+
+    Implementation is based on PyCrypto's CMAC one, that is in public domain.
+    """
+    k1, k2 = _mac_ks(encrypter, bs)
+    if len(data) % bs == 0:
+        tail_offset = len(data) - bs
+    else:
+        tail_offset = len(data) - (len(data) % bs)
+    prev = bs * b'\x00'
+    for i in xrange(0, tail_offset, bs):
+        prev = encrypter(strxor(data[i:i + bs], prev))
+    tail = data[tail_offset:]
+    return encrypter(strxor(
+        strxor(pad3(tail, bs), prev),
+        k1 if len(tail) == bs else k2,
+    ))
index dddb205ba94192f8c7959ab4de51415aca2525ba..43ef9ee524dd2a7c56af416b26740473741393c9 100644 (file)
@@ -1,3 +1,6 @@
+from typing import Callable
+
+
 def pad_size(data_size: int, blocksize: int) -> int: ...
 
 
@@ -7,4 +10,34 @@ def pad1(data: bytes, blocksize: int) -> bytes: ...
 def pad2(data: bytes, blocksize: int) -> bytes: ...
 
 
+def unpad2(data: bytes, blocksize: int) -> bytes: ...
+
+
 def pad3(data: bytes, blocksize: int) -> bytes: ...
+
+
+def ecb_encrypt(encrypter: Callable[[bytes], bytes], bs: int, pt: bytes) -> bytes: ...
+
+
+def ecb_decrypt(decrypter: Callable[[bytes], bytes], bs: int, ct: bytes) -> bytes: ...
+
+
+def ctr(encrypter: Callable[[bytes], bytes], bs: int, data: bytes, iv: bytes) -> bytes: ...
+
+
+def ofb(encrypter: Callable[[bytes], bytes], bs: int, data: bytes, iv: bytes) -> bytes: ...
+
+
+def cbc_encrypt(encrypter: Callable[[bytes], bytes], bs: int, pt: bytes, iv: bytes) -> bytes: ...
+
+
+def cbc_decrypt(decrypter: Callable[[bytes], bytes], bs: int, ct: bytes, iv: bytes) -> bytes: ...
+
+
+def cfb_encrypt(encrypter: Callable[[bytes], bytes], bs: int, pt: bytes, iv: bytes) -> bytes: ...
+
+
+def cfb_decrypt(encrypter: Callable[[bytes], bytes], bs: int, ct: bytes, iv: bytes) -> bytes: ...
+
+
+def mac(encrypter: Callable[[bytes], bytes], bs: int, data: bytes) -> bytes: ...
diff --git a/pygost/test_gost3413.py b/pygost/test_gost3413.py
new file mode 100644 (file)
index 0000000..5e3a39b
--- /dev/null
@@ -0,0 +1,171 @@
+from os import urandom
+from random import randint
+from unittest import TestCase
+
+from pygost.gost3412 import GOST3412Kuz
+from pygost.gost3413 import _mac_ks
+from pygost.gost3413 import cbc_decrypt
+from pygost.gost3413 import cbc_encrypt
+from pygost.gost3413 import cfb_decrypt
+from pygost.gost3413 import cfb_encrypt
+from pygost.gost3413 import ctr
+from pygost.gost3413 import ecb_decrypt
+from pygost.gost3413 import ecb_encrypt
+from pygost.gost3413 import mac
+from pygost.gost3413 import ofb
+from pygost.gost3413 import pad2
+from pygost.gost3413 import unpad2
+from pygost.utils import hexdec
+from pygost.utils import hexenc
+
+
+class Pad2Test(TestCase):
+    def test_symmetric(self):
+        for _ in range(100):
+            for blocksize in (8, 16):
+                data = urandom(randint(0, blocksize * 3))
+                self.assertSequenceEqual(
+                    unpad2(pad2(data, blocksize), blocksize),
+                    data,
+                )
+
+
+class GOST3412KuzModesTest(TestCase):
+    key = hexdec("8899aabbccddeeff0011223344556677fedcba98765432100123456789abcdef")
+    ciph = GOST3412Kuz(key)
+    plaintext = ""
+    plaintext += "1122334455667700ffeeddccbbaa9988"
+    plaintext += "00112233445566778899aabbcceeff0a"
+    plaintext += "112233445566778899aabbcceeff0a00"
+    plaintext += "2233445566778899aabbcceeff0a0011"
+    iv = hexdec("1234567890abcef0a1b2c3d4e5f0011223344556677889901213141516171819")
+
+    def test_ecb_vectors(self):
+        ciphtext = ""
+        ciphtext += "7f679d90bebc24305a468d42b9d4edcd"
+        ciphtext += "b429912c6e0032f9285452d76718d08b"
+        ciphtext += "f0ca33549d247ceef3f5a5313bd4b157"
+        ciphtext += "d0b09ccde830b9eb3a02c4c5aa8ada98"
+        self.assertSequenceEqual(
+            hexenc(ecb_encrypt(self.ciph.encrypt, 16, hexdec(self.plaintext))),
+            ciphtext,
+        )
+        self.assertSequenceEqual(
+            hexenc(ecb_decrypt(self.ciph.decrypt, 16, hexdec(ciphtext))),
+            self.plaintext,
+        )
+
+    def test_ecb_symmetric(self):
+        for _ in range(100):
+            pt = pad2(urandom(randint(0, 16 * 2)), 16)
+            ciph = GOST3412Kuz(urandom(32))
+            ct = ecb_encrypt(ciph.encrypt, 16, pt)
+            self.assertSequenceEqual(ecb_decrypt(ciph.decrypt, 16, ct), pt)
+
+    def test_ctr_vectors(self):
+        ciphtext = ""
+        ciphtext += "f195d8bec10ed1dbd57b5fa240bda1b8"
+        ciphtext += "85eee733f6a13e5df33ce4b33c45dee4"
+        ciphtext += "a5eae88be6356ed3d5e877f13564a3a5"
+        ciphtext += "cb91fab1f20cbab6d1c6d15820bdba73"
+        iv = self.iv[:8]
+        self.assertSequenceEqual(
+            hexenc(ctr(self.ciph.encrypt, 16, hexdec(self.plaintext), iv)),
+            ciphtext,
+        )
+        self.assertSequenceEqual(
+            hexenc(ctr(self.ciph.encrypt, 16, hexdec(ciphtext), iv)),
+            self.plaintext,
+        )
+
+    def test_ctr_symmetric(self):
+        for _ in range(100):
+            pt = urandom(randint(0, 16 * 2))
+            iv = urandom(8)
+            ciph = GOST3412Kuz(urandom(32))
+            ct = ctr(ciph.encrypt, 16, pt, iv)
+            self.assertSequenceEqual(ctr(ciph.encrypt, 16, ct, iv), pt)
+
+    def test_ofb_vectors(self):
+        ciphtext = ""
+        ciphtext += "81800a59b1842b24ff1f795e897abd95"
+        ciphtext += "ed5b47a7048cfab48fb521369d9326bf"
+        ciphtext += "66a257ac3ca0b8b1c80fe7fc10288a13"
+        ciphtext += "203ebbc066138660a0292243f6903150"
+        self.assertSequenceEqual(
+            hexenc(ofb(self.ciph.encrypt, 16, hexdec(self.plaintext), self.iv)),
+            ciphtext,
+        )
+        self.assertSequenceEqual(
+            hexenc(ofb(self.ciph.encrypt, 16, hexdec(ciphtext), self.iv)),
+            self.plaintext,
+        )
+
+    def test_ofb_symmetric(self):
+        for _ in range(100):
+            pt = urandom(randint(0, 16 * 2))
+            iv = urandom(16 * 2)
+            ciph = GOST3412Kuz(urandom(32))
+            ct = ofb(ciph.encrypt, 16, pt, iv)
+            self.assertSequenceEqual(ofb(ciph.encrypt, 16, ct, iv), pt)
+
+    def test_cbc_vectors(self):
+        ciphtext = ""
+        ciphtext += "689972d4a085fa4d90e52e3d6d7dcc27"
+        ciphtext += "2826e661b478eca6af1e8e448d5ea5ac"
+        ciphtext += "fe7babf1e91999e85640e8b0f49d90d0"
+        ciphtext += "167688065a895c631a2d9a1560b63970"
+        self.assertSequenceEqual(
+            hexenc(cbc_encrypt(self.ciph.encrypt, 16, hexdec(self.plaintext), self.iv)),
+            ciphtext,
+        )
+        self.assertSequenceEqual(
+            hexenc(cbc_decrypt(self.ciph.decrypt, 16, 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(16 * 2)
+            ciph = GOST3412Kuz(urandom(32))
+            ct = cbc_encrypt(ciph.encrypt, 16, pt, iv)
+            self.assertSequenceEqual(cbc_decrypt(ciph.decrypt, 16, ct, iv), pt)
+
+    def test_cfb_vectors(self):
+        ciphtext = ""
+        ciphtext += "81800a59b1842b24ff1f795e897abd95"
+        ciphtext += "ed5b47a7048cfab48fb521369d9326bf"
+        ciphtext += "79f2a8eb5cc68d38842d264e97a238b5"
+        ciphtext += "4ffebecd4e922de6c75bd9dd44fbf4d1"
+        self.assertSequenceEqual(
+            hexenc(cfb_encrypt(self.ciph.encrypt, 16, hexdec(self.plaintext), self.iv)),
+            ciphtext,
+        )
+        self.assertSequenceEqual(
+            hexenc(cfb_decrypt(self.ciph.encrypt, 16, hexdec(ciphtext), self.iv)),
+            self.plaintext,
+        )
+
+    def test_cfb_symmetric(self):
+        for _ in range(100):
+            pt = urandom(randint(0, 16 * 2))
+            iv = urandom(16 * 2)
+            ciph = GOST3412Kuz(urandom(32))
+            ct = cfb_encrypt(ciph.encrypt, 16, pt, iv)
+            self.assertSequenceEqual(cfb_decrypt(ciph.encrypt, 16, ct, iv), pt)
+
+    def test_mac_vectors(self):
+        k1, k2 = _mac_ks(self.ciph.encrypt, 16)
+        self.assertSequenceEqual(hexenc(k1), "297d82bc4d39e3ca0de0573298151dc7")
+        self.assertSequenceEqual(hexenc(k2), "52fb05789a73c7941bc0ae65302a3b8e")
+        self.assertSequenceEqual(
+            hexenc(mac(self.ciph.encrypt, 16, hexdec(self.plaintext))[:8]),
+            "336f4d296059fbe3",
+        )
+
+    def test_mac_applies(self):
+        for _ in range(100):
+            data = urandom(randint(0, 16 * 2))
+            ciph = GOST3412Kuz(urandom(32))
+            mac(ciph.encrypt, 16, data)
index 6bcf9bb7d986c054093f3aca59e5480a4583fa59..be2c62c552ec73e102a72ac1773bbc1098f4b0bb 100644 (file)
--- a/www.texi
+++ b/www.texi
@@ -49,7 +49,8 @@ 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.13-2015 padding methods
+@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
 @end itemize
 
@@ -88,6 +89,9 @@ mailing list. Announcements also go to this mailing list.
 @unnumbered News
 
 @table @strong
+@item 3.2
+34.13-2015 block cipher modes of operation implementations.
+
 @item 3.1
 Fixed mypy stubs related to PEP247-successors.