169 lines
6.0 KiB
Python
169 lines
6.0 KiB
Python
# coding: utf-8
|
|
# PyGOST -- Pure Python GOST cryptographic functions library
|
|
# Copyright (C) 2015-2023 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 tag_size
|
|
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)
|