From: Sergey Matveev Date: Sun, 4 Jun 2017 14:33:49 +0000 (+0300) Subject: 34.13-2015 cipher modes of operation implementation X-Git-Tag: 3.2^0 X-Git-Url: http://www.git.cypherpunks.ru/?p=pygost.git;a=commitdiff_plain;h=43fdce36120844bc0fc38e0d5664dfc7090c119a 34.13-2015 cipher modes of operation implementation --- diff --git a/NEWS b/NEWS index 2fca8c8..7298105 100644 --- 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 ef84b66..60c3864 100644 --- 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 8c50098..a3ec5a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1 +3.2 diff --git a/pygost/Makefile b/pygost/Makefile index 53eebf3..35eda67 100644 --- a/pygost/Makefile +++ b/pygost/Makefile @@ -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 diff --git a/pygost/gost28147.py b/pygost/gost28147.py index 79ef968..2b39844 100644 --- a/pygost/gost28147.py +++ b/pygost/gost28147.py @@ -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) diff --git a/pygost/gost3413.py b/pygost/gost3413.py index dc7339e..bd8cc49 100644 --- a/pygost/gost3413.py +++ b/pygost/gost3413.py @@ -19,9 +19,14 @@ 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, + )) diff --git a/pygost/stubs/pygost/gost3413.pyi b/pygost/stubs/pygost/gost3413.pyi index dddb205..43ef9ee 100644 --- a/pygost/stubs/pygost/gost3413.pyi +++ b/pygost/stubs/pygost/gost3413.pyi @@ -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 index 0000000..5e3a39b --- /dev/null +++ b/pygost/test_gost3413.py @@ -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) diff --git a/www.texi b/www.texi index 6bcf9bb..be2c62c 100644 --- 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.