Skip to content

Commit e013ce2

Browse files
committed
Add MODE_KW (KeyWrap) for AES
1 parent 9b8b516 commit e013ce2

File tree

6 files changed

+236
-11
lines changed

6 files changed

+236
-11
lines changed

lib/Crypto/Cipher/AES.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +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-
44+
MODE_KW = 13 #: Key Wrap
45+
MODE_KWP = 14 #: Key Wrap with Padding
4546

4647
_cproto = """
4748
int AES_start_operation(const uint8_t key[],

lib/Crypto/Cipher/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,18 @@ def _create_cipher(factory, key, mode, *args, **kwargs):
6969
if mode == 8:
7070
from Crypto.Cipher._mode_ccm import _create_ccm_cipher
7171
res = _create_ccm_cipher(factory, **kwargs)
72-
if mode == 10:
72+
elif mode == 10:
7373
from Crypto.Cipher._mode_siv import _create_siv_cipher
7474
res = _create_siv_cipher(factory, **kwargs)
75-
if mode == 11:
75+
elif mode == 11:
7676
from Crypto.Cipher._mode_gcm import _create_gcm_cipher
7777
res = _create_gcm_cipher(factory, **kwargs)
78-
if mode == 12:
78+
elif mode == 12:
7979
from Crypto.Cipher._mode_ocb import _create_ocb_cipher
8080
res = _create_ocb_cipher(factory, **kwargs)
81+
elif mode == 13:
82+
from Crypto.Cipher._mode_kw import _create_kw_cipher
83+
res = _create_kw_cipher(factory, **kwargs)
8184

8285
if res is None:
8386
raise ValueError("Mode not supported")

lib/Crypto/Cipher/_mode_kw.py

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

lib/Crypto/SelfTest/Cipher/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424

2525
"""Self-test for cipher modules"""
2626

27-
__revision__ = "$Id$"
27+
import sys
28+
2829

2930
def get_tests(config={}):
3031
tests = []
@@ -50,11 +51,14 @@ def get_tests(config={}):
5051
from Crypto.SelfTest.Cipher import test_EAX; tests += test_EAX.get_tests(config=config)
5152
from Crypto.SelfTest.Cipher import test_GCM; tests += test_GCM.get_tests(config=config)
5253
from Crypto.SelfTest.Cipher import test_SIV; tests += test_SIV.get_tests(config=config)
54+
55+
if sys.version_info >= (3, 9):
56+
from Crypto.SelfTest.Cipher import test_KW
57+
tests += test_KW.get_tests(config=config)
58+
5359
return tests
5460

5561
if __name__ == '__main__':
5662
import unittest
5763
suite = lambda: unittest.TestSuite(get_tests())
5864
unittest.main(defaultTest='suite')
59-
60-
# vim:set ts=4 sw=4 sts=4 expandtab:
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import unittest
2+
3+
from Crypto.SelfTest.st_common import list_test_cases
4+
5+
from Crypto.Cipher import AES
6+
7+
8+
class KW_Tests(unittest.TestCase):
9+
10+
# From RFC3394
11+
tvs = [
12+
("000102030405060708090A0B0C0D0E0F",
13+
"00112233445566778899AABBCCDDEEFF",
14+
"1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5"),
15+
("000102030405060708090A0B0C0D0E0F1011121314151617",
16+
"00112233445566778899AABBCCDDEEFF",
17+
"96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D"),
18+
("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
19+
"00112233445566778899AABBCCDDEEFF",
20+
"64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7"),
21+
("000102030405060708090A0B0C0D0E0F1011121314151617",
22+
"00112233445566778899AABBCCDDEEFF0001020304050607",
23+
"031D33264E15D33268F24EC260743EDCE1C6C7DDEE725A936BA814915C6762D2"),
24+
("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
25+
"00112233445566778899AABBCCDDEEFF0001020304050607",
26+
"A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1"),
27+
("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
28+
"00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F",
29+
"28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21"),
30+
]
31+
32+
def test_rfc3394(self):
33+
for tv in self.tvs:
34+
kek, pt, ct = [bytes.fromhex(x) for x in tv]
35+
36+
cipher = AES.new(kek, AES.MODE_KW)
37+
ct2 = cipher.seal(pt)
38+
39+
self.assertEqual(ct, ct2)
40+
41+
pt2 = cipher.unseal(ct)
42+
self.assertEqual(pt, pt2)
43+
44+
def test_neg1(self):
45+
46+
cipher = AES.new(b'-' * 16, AES.MODE_KW)
47+
48+
with self.assertRaises(ValueError):
49+
cipher.seal(b'')
50+
51+
with self.assertRaises(ValueError):
52+
cipher.seal(b'8' * 17)
53+
54+
def test_neg2(self):
55+
56+
cipher = AES.new(b'-' * 16, AES.MODE_KW)
57+
ct = bytearray(cipher.seal(b'7' * 16))
58+
cipher.unseal(ct)
59+
60+
ct[0] ^= 0xFF
61+
with self.assertRaises(ValueError):
62+
cipher.unseal(ct)
63+
64+
65+
def get_tests(config={}):
66+
tests = []
67+
tests += list_test_cases(KW_Tests)
68+
return tests
69+
70+
71+
if __name__ == '__main__':
72+
def suite():
73+
return unittest.TestSuite(get_tests())
74+
unittest.main(defaultTest='suite')

lib/Crypto/SelfTest/Protocol/test_HPKE.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,10 +474,6 @@ def test_hpke_unseal(self):
474474
print(".", end="", flush=True)
475475

476476

477-
if __name__ == "__main__":
478-
unittest.main()
479-
480-
481477
def get_tests(config={}):
482478

483479
tests = []

0 commit comments

Comments
 (0)