Skip to content

Commit

Permalink
Add RFC 3394 keywrap
Browse files Browse the repository at this point in the history
This is needed for compatibility with OpenSSL's default CMS KDF,
and pretty much nothing else that I could find.
  • Loading branch information
James-E-A committed May 16, 2023
1 parent d64618b commit 3d312ef
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 7 deletions.
8 changes: 8 additions & 0 deletions Doc/src/cipher/classic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,11 @@ A variant of CFB, with two differences:

Like for CTR, an OpenPGP cipher object has a read-only attribute :attr:`iv`.

.. _wrap_mode:

Key Wrap mode
--------
The `Key Wrap Algorithm <https://tools.ietf.org/html/rfc4880>`_,
defined in `NIST SP 800-38F, section 6 <https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf>`_.
It is a historical precursor to SIV mode, allowing securely wrapping multiple
keys by the same KEK without needing a nonce.
1 change: 1 addition & 0 deletions lib/Crypto/Cipher/AES.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
MODE_SIV = 10 #: Synthetic Initialization Vector (:ref:`siv_mode`)
MODE_GCM = 11 #: Galois Counter Mode (:ref:`gcm_mode`)
MODE_OCB = 12 #: Offset Code Book (:ref:`ocb_mode`)
MODE_WRAP = 13 #: NIST Key Wrap (:ref:`wrap_mode`)


_cproto = """
Expand Down
16 changes: 9 additions & 7 deletions lib/Crypto/Cipher/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@
from Crypto.Cipher._mode_siv import _create_siv_cipher
from Crypto.Cipher._mode_gcm import _create_gcm_cipher
from Crypto.Cipher._mode_ocb import _create_ocb_cipher
from Crypto.Cipher._mode_wrap import _create_wrap_cipher

_modes = { 1:_create_ecb_cipher,
2:_create_cbc_cipher,
3:_create_cfb_cipher,
5:_create_ofb_cipher,
6:_create_ctr_cipher,
7:_create_openpgp_cipher,
9:_create_eax_cipher
_modes = { 1:_create_ecb_cipher,
2:_create_cbc_cipher,
3:_create_cfb_cipher,
5:_create_ofb_cipher,
6:_create_ctr_cipher,
7:_create_openpgp_cipher,
9:_create_eax_cipher,
13:_create_wrap_cipher
}

_extra_modes = { 8:_create_ccm_cipher,
Expand Down
180 changes: 180 additions & 0 deletions lib/Crypto/Cipher/_mode_wrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# ===================================================================
#
# Copyright (c) 2023, James Edington <[email protected]>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ===================================================================

"""
NIST SP800-38F Key Wrap mode.
"""

__all__ = ['WrapMode']

from Crypto.Util.number import long_to_bytes
from Crypto.Util.py3compat import _copy_bytes
from Crypto.Util.strxor import strxor
from Crypto.Random import get_random_bytes

class WrapMode(object):
"""Key Wrap mode.
This mode is a historic authenticated construction
mainly used in S/MIME and PKCS#7 applications.
See `NIST SP800-38F`_ , Section 6 .
.. _`NIST SP800-38F` : https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf
:undocumented: __init__
"""

def __init__(self, factory, key, icv, cipher_params):
self.block_size = factory.block_size
self.key_size = factory.block_size // 2
self._icv = _copy_bytes(None, None, icv)

if len(self._icv) != (self.key_size):
raise ValueError("Length of ICV must be %d"
" for MODE_WRAP"
% (self.key_size, ))

# Instantiate the underlying ECB cipher
self._cipher = factory.new(
key,
factory.MODE_ECB,
**cipher_params)

self._done = False # True after the first encryption

def encrypt(self, plaintext):
"""Encrypt (wrap) a key with the key and the parameters set at initialization.
A cipher object is stateful: once you have encrypted a key
you cannot encrypt (or decrypt) another key using the same
object.
The data to encrypt cannot be broken up in two or
more pieces; `encrypt` can only be called once.
:Parameters:
plaintext : bytes/bytearray/memoryview
The key to encrypt.
:Return:
the encrypted key, as a byte string.
It is exactly 8 bytes longer than *plaintext*.
"""

klen = self.key_size

if len(plaintext) % klen != 0:
raise ValueError("MODE_WRAP used with a key that needs padding (consider MODE_WRAP_PADDED)")

if self._done:
raise TypeError("cannot wrap multiple keys with the same cipher")

A = self._icv
R = bytearray(_copy_bytes(None, None, plaintext))
n = len(plaintext) // klen
ctr = 0
for j in range(6):
for i in range(n):
ctr += 1
B = self._cipher.encrypt(A + R[i*klen:(i+1)*klen])
A, R[i*klen:(i+1)*klen] = strxor(B[:klen], long_to_bytes(ctr, klen)), B[klen:]

self._done = True
return A + R

def decrypt(self, ciphertext):
"""Decrypt wrapped key with the key and the parameters set at initialization.
A cipher object is stateful: once you have decrypted a key
you cannot decrypt (or encrypt) another key using the same
object.
The data to decrypt cannot be broken up in two or
more pieces; `decrypt` can only be called once.
:Parameters:
ciphertext : bytes/bytearray/memoryview
The piece of data to decrypt.
:Return: the decrypted key (byte string).
:Raises ValueError:
if the ICV does not match. The message has been tampered with
or the kek is incorrect.
"""

klen = self.key_size

if len(ciphertext) % l != 0:
raise ValueError("bad key length for MODE_WRAP")

if self._done:
raise TypeError("cannot unwrap multiple keys with the same cipher")

A = _copy_bytes(None, klen, ciphertext)
R = bytearray(_copy_bytes(klen, None, ciphertext))
n = len(ciphertext) // klen - 1
ctr = 6*n
for j in reversed(range(6)):
for i in reversed(range(n)):
B = self._cipher.decrypt(strxor(A, long_to_bytes(ctr, klen)) + R[i*klen:(i+1)*klen])
A, R[i*klen:(i+1)*klen] = B[:klen], B[klen:]
ctr -= 1

if A != self._icv:
raise ValueError("integrity check failed")

self._done = True
return _copy_bytes(None, None, R)


def _create_wrap_cipher(factory, **kwargs):
"""Create a new block cipher, configured in Key Wrap mode.
:Parameters:
factory : module
The module.
:Keywords:
key : bytes/bytearray/memoryview
The secret key to use in the symmetric cipher.
icv : bytes/bytearray/memoryview
The "Integrity Check Value" to use.
"""

icv = kwargs.pop("icv", b'\xa6\xa6\xa6\xa6\xa6\xa6\xa6\xa6')

try:
key = kwargs.pop("key")
except KeyError as e:
raise TypeError("Missing component: " + str(e))

return WrapMode(factory, key, icv, kwargs)

0 comments on commit 3d312ef

Please sign in to comment.