Skip to content

Commit 2165641

Browse files
committed
Add MODE_KWP (KeyWrap with Padding) for AES
1 parent e013ce2 commit 2165641

File tree

18 files changed

+4338
-19
lines changed

18 files changed

+4338
-19
lines changed

Changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Changelog
44
Under development
55
++++++++++++++++++++++++++
66

7+
New features
8+
---------------
9+
* Added cipher modes Key Wrap (KW, RFC3394) and Key Wrap with Padding (KWP, RFC5649).
10+
Both are defined also in NIST SP 800-38F.
11+
712
Resolved issues
813
---------------
914
* GH#862: For HashEdDSA and Ed448, sign() and verify() modified the state of the XOF.

Doc/index.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Welcome to PyCryptodome's documentation
33

44
.. toctree::
55
:maxdepth: 3
6-
6+
77
src/introduction
88
src/features
99
src/installation
@@ -12,6 +12,5 @@ Welcome to PyCryptodome's documentation
1212
src/examples
1313
src/faq
1414
src/contribute_support
15-
src/future
1615
src/changelog
1716
src/license

Doc/src/cipher/modern.rst

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ This is the state machine for a cipher object:
2828
.. figure:: aead.png
2929
:align: center
3030
:figwidth: 80%
31-
31+
3232
Generic state diagram for a AEAD cipher mode
3333

3434
Beside the usual :meth:`encrypt()` and :meth:`decrypt()` already
@@ -550,3 +550,106 @@ Example (decryption with multiple chunks)::
550550
>>> except (ValueError, KeyError):
551551
>>> print("Incorrect decryption")
552552

553+
.. _kw_mode:
554+
555+
KW mode
556+
--------
557+
`Key Wrapping <https://datatracker.ietf.org/doc/html/rfc3394>`_
558+
(or `NIST SP 800-38F <https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf>`_)
559+
is an ad-hoc authenticated cipher mode designed to wrap cryptographic keys,
560+
and not other types of data.
561+
It is deterministic (it doesn't use nonces or IVs) and
562+
only works with ciphers with a block size of 128 bits (like AES).
563+
564+
The cryptographic key to wrap must have a length multiple of 8.
565+
566+
There is no reason to prefer this ad-hoc mode to one like SIV.
567+
568+
The :func:`new` function at the module level under ``Crypto.Cipher`` instantiates
569+
a new KW cipher object for the relevant base algorithm.
570+
In the following definition, ``<algorithm>`` can only be ``AES`` today:
571+
572+
.. function:: Crypto.Cipher.<algorithm>.new(key, <algorithm>.MODE_KW)
573+
574+
Create a new Key Wrapping (KW) cipher object,
575+
using <algorithm> as the base block cipher.
576+
577+
:param bytes key: the cryptographic key
578+
:return: a KW cipher object
579+
580+
The cipher object has one read-only attribute :attr:`block_size`,
581+
and two methods (``seal`` and ``unseal``).
582+
583+
Example of encryption::
584+
585+
>>> from Crypto.Cipher import AES
586+
>>> from Crypto.Random import get_random_bytes
587+
>>>
588+
>>> key_to_wrap = b"1234567890123456"
589+
>>> wrapping_key = get_random_bytes(16)
590+
>>> cipher = AES.new(wrapping_key, AES.MODE_KW)
591+
>>> ciphertext = cipher.seal(key_to_wrap)
592+
593+
Example of decryption::
594+
595+
>>> from Crypto.Cipher import AES
596+
>>> from Crypto.Random import get_random_bytes
597+
>>>
598+
>>> wrapping_key = get_random_bytes(16)
599+
>>> cipher = AES.new(wrapping_key, AES.MODE_KW)
600+
>>> try:
601+
>>> unwrapped_key = cipher.unseal(ciphertext)
602+
>>> except ValueError:
603+
>>> print("Invalid decryption")
604+
605+
.. _kwp_mode:
606+
607+
KWP mode
608+
--------
609+
`Key Wrapping with Padding <https://datatracker.ietf.org/doc/html/rfc5649>`_
610+
(or `NIST SP 800-38F <https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf>`_)
611+
is an ad-hoc authenticated cipher mode designed to wrap cryptographic keys,
612+
and not other types of data.
613+
It is deterministic (it doesn't use nonces or IVs) and
614+
only works with ciphers with a block size of 128 bits (like AES).
615+
616+
The cryptographic key to wrap can have any length.
617+
618+
There is no reason to prefer this ad-hoc mode to one like SIV.
619+
620+
The :func:`new` function at the module level under ``Crypto.Cipher`` instantiates
621+
a new KWP cipher object for the relevant base algorithm.
622+
In the following definition, ``<algorithm>`` can only be ``AES`` today:
623+
624+
.. function:: Crypto.Cipher.<algorithm>.new(key, <algorithm>.MODE_KWP)
625+
626+
Create a new Key Wrapping with Padding (KWP) cipher object,
627+
using <algorithm> as the base block cipher.
628+
629+
:param bytes key: the cryptographic key
630+
:return: a KWP cipher object
631+
632+
The cipher object has one read-only attribute :attr:`block_size`,
633+
and two methods (``seal`` and ``unseal``).
634+
635+
Example of encryption::
636+
637+
>>> from Crypto.Cipher import AES
638+
>>> from Crypto.Random import get_random_bytes
639+
>>>
640+
>>> key_to_wrap = b"1234567890123456"
641+
>>> wrapping_key = get_random_bytes(16)
642+
>>> cipher = AES.new(wrapping_key, AES.MODE_KWP)
643+
>>> ciphertext = cipher.seal(key_to_wrap)
644+
645+
Example of decryption::
646+
647+
>>> from Crypto.Cipher import AES
648+
>>> from Crypto.Random import get_random_bytes
649+
>>>
650+
>>> wrapping_key = get_random_bytes(16)
651+
>>> cipher = AES.new(wrapping_key, AES.MODE_KWP)
652+
>>> try:
653+
>>> unwrapped_key = cipher.unseal(ciphertext)
654+
>>> except ValueError:
655+
>>> print("Invalid decryption")

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ For faster public key operations in Unix, you should install `GMP`_ in your syst
4444
PyCryptodome is a fork of PyCrypto. It brings the following enhancements
4545
with respect to the last official version of PyCrypto (2.6.1):
4646

47-
* Authenticated encryption modes (GCM, CCM, EAX, SIV, OCB)
47+
* Authenticated encryption modes (GCM, CCM, EAX, SIV, OCB, KW, KWP)
4848
* Hybrid Public Key Encryption (HPKE)
4949
* Accelerated AES on Intel platforms via AES-NI
5050
* First class support for PyPy

lib/Crypto/Cipher/AES.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
MODE_SIV = 10 #: Synthetic Initialization Vector (:ref:`siv_mode`)
4242
MODE_GCM = 11 #: Galois Counter Mode (:ref:`gcm_mode`)
4343
MODE_OCB = 12 #: Offset Code Book (:ref:`ocb_mode`)
44-
MODE_KW = 13 #: Key Wrap
45-
MODE_KWP = 14 #: Key Wrap with Padding
44+
MODE_KW = 13 #: Key Wrap (:ref:`kw_mode`)
45+
MODE_KWP = 14 #: Key Wrap with Padding (:ref:`kwp_mode`)
4646

4747
_cproto = """
4848
int AES_start_operation(const uint8_t key[],

lib/Crypto/Cipher/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ def _create_cipher(factory, key, mode, *args, **kwargs):
8181
elif mode == 13:
8282
from Crypto.Cipher._mode_kw import _create_kw_cipher
8383
res = _create_kw_cipher(factory, **kwargs)
84+
elif mode == 14:
85+
from Crypto.Cipher._mode_kwp import _create_kwp_cipher
86+
res = _create_kwp_cipher(factory, **kwargs)
8487

8588
if res is None:
8689
raise ValueError("Mode not supported")

lib/Crypto/Cipher/_mode_kw.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def __init__(self,
7171

7272
self._factory = factory
7373
self._cipher = factory.new(key, factory.MODE_ECB)
74+
self._done = False
7475

7576
def seal(self, plaintext: Union[bytes, bytearray]) -> bytes:
7677
"""Encrypt and authenticate (wrap) a cryptographic key.
@@ -85,13 +86,20 @@ def seal(self, plaintext: Union[bytes, bytearray]) -> bytes:
8586
The wrapped key.
8687
"""
8788

89+
if self._done:
90+
raise ValueError("The cipher cannot be used more than once")
91+
8892
if len(plaintext) % 8:
8993
raise ValueError("The plaintext must have length multiple of 8 bytes")
9094

9195
if len(plaintext) < 16:
9296
raise ValueError("The plaintext must be at least 16 bytes long")
9397

98+
if len(plaintext) >= 2**32:
99+
raise ValueError("The plaintext is too long")
100+
94101
res = W(self._cipher, b'\xA6\xA6\xA6\xA6\xA6\xA6\xA6\xA6' + plaintext)
102+
self._done = True
95103
return res
96104

97105
def unseal(self, ciphertext: Union[bytes, bytearray]) -> bytes:
@@ -110,6 +118,8 @@ def unseal(self, ciphertext: Union[bytes, bytearray]) -> bytes:
110118
If the ciphertext or the key are not valid.
111119
"""
112120

121+
if self._done:
122+
raise ValueError("The cipher cannot be used more than once")
113123

114124
if len(ciphertext) % 8:
115125
raise ValueError("The ciphertext must have length multiple of 8 bytes")
@@ -121,6 +131,7 @@ def unseal(self, ciphertext: Union[bytes, bytearray]) -> bytes:
121131

122132
if pt[:8] != b'\xA6\xA6\xA6\xA6\xA6\xA6\xA6\xA6':
123133
raise ValueError("Incorrect integrity check value")
134+
self._done = True
124135

125136
return pt[8:]
126137

lib/Crypto/Cipher/_mode_kwp.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import struct
2+
3+
from types import ModuleType
4+
from typing import Union
5+
6+
from ._mode_kw import W, W_inverse
7+
8+
9+
class KWPMode(object):
10+
"""Key Wrap with Padding (KWP) mode.
11+
12+
This is a deterministic Authenticated Encryption (AE) mode
13+
for protecting cryptographic keys. See `NIST SP800-38F`_.
14+
15+
It provides both confidentiality and authenticity, and it designed
16+
so that any bit of the ciphertext depends on all bits of the plaintext.
17+
18+
This mode is only available for ciphers that operate on 128 bits blocks
19+
(e.g., AES).
20+
21+
.. _`NIST SP800-38F`: http://csrc.nist.gov/publications/nistpubs/800-38F/SP-800-38F.pdf
22+
23+
:undocumented: __init__
24+
"""
25+
26+
def __init__(self,
27+
factory: ModuleType,
28+
key: Union[bytes, bytearray]):
29+
30+
self.block_size = factory.block_size
31+
if self.block_size != 16:
32+
raise ValueError("Key Wrap with Padding mode is only available for ciphers"
33+
" that operate on 128 bits blocks")
34+
35+
self._factory = factory
36+
self._cipher = factory.new(key, factory.MODE_ECB)
37+
self._done = False
38+
39+
def seal(self, plaintext: Union[bytes, bytearray]) -> bytes:
40+
"""Encrypt and authenticate (wrap) a cryptographic key.
41+
42+
Args:
43+
plaintext:
44+
The cryptographic key to wrap.
45+
46+
Returns:
47+
The wrapped key.
48+
"""
49+
50+
if self._done:
51+
raise ValueError("The cipher cannot be used more than once")
52+
53+
if len(plaintext) == 0:
54+
raise ValueError("The plaintext must be at least 1 byte")
55+
56+
if len(plaintext) >= 2 ** 32:
57+
raise ValueError("The plaintext is too long")
58+
59+
padlen = (8 - len(plaintext)) % 8
60+
padded = plaintext + b'\x00' * padlen
61+
62+
AIV = b'\xA6\x59\x59\xA6' + struct.pack('>I', len(plaintext))
63+
64+
if len(padded) == 8:
65+
res = self._cipher.encrypt(AIV + padded)
66+
else:
67+
res = W(self._cipher, AIV + padded)
68+
69+
return res
70+
71+
def unseal(self, ciphertext: Union[bytes, bytearray]) -> bytes:
72+
"""Decrypt and authenticate (unwrap) a cryptographic key.
73+
74+
Args:
75+
ciphertext:
76+
The cryptographic key to unwrap.
77+
It must be at least 16 bytes long, and its length
78+
must be a multiple of 8.
79+
80+
Returns:
81+
The original key.
82+
83+
Raises: ValueError
84+
If the ciphertext or the key are not valid.
85+
"""
86+
87+
if self._done:
88+
raise ValueError("The cipher cannot be used more than once")
89+
90+
if len(ciphertext) % 8:
91+
raise ValueError("The ciphertext must have length multiple of 8 bytes")
92+
93+
if len(ciphertext) < 16:
94+
raise ValueError("The ciphertext must be at least 24 bytes long")
95+
96+
if len(ciphertext) == 16:
97+
S = self._cipher.decrypt(ciphertext)
98+
else:
99+
S = W_inverse(self._cipher, ciphertext)
100+
101+
if S[:4] != b'\xA6\x59\x59\xA6':
102+
raise ValueError("Incorrect decryption")
103+
104+
Plen = struct.unpack('>I', S[4:8])[0]
105+
106+
padlen = len(S) - 8 - Plen
107+
if padlen < 0 or padlen > 7:
108+
raise ValueError("Incorrect decryption")
109+
110+
if S[len(S) - padlen:] != b'\x00' * padlen:
111+
raise ValueError("Incorrect decryption")
112+
113+
return S[8:len(S) - padlen]
114+
115+
116+
def _create_kwp_cipher(factory: ModuleType,
117+
**kwargs: Union[bytes, bytearray]) -> KWPMode:
118+
"""Create a new block cipher in Key Wrap with Padding mode.
119+
120+
Args:
121+
factory:
122+
A block cipher module, taken from `Crypto.Cipher`.
123+
The cipher must have block length of 16 bytes, such as AES.
124+
125+
Keywords:
126+
key:
127+
The secret key to use to seal or unseal.
128+
"""
129+
130+
try:
131+
key = kwargs["key"]
132+
except KeyError as e:
133+
raise TypeError("Missing parameter:" + str(e))
134+
135+
return KWPMode(factory, key)

0 commit comments

Comments
 (0)