Danger
Nothing here should be used for any security purposes.
OAEP#
Imported with
from toy_crypto import rsa
Primitive RSA is deterministic, so it completely fails to provide IND-CPA security. It is also vulnerable to chosen ciphertext attacks. OAEP (Optimized Assymmetric Encryption Padding) is designed to address both of those when properly implemented. This module does not provide a proper implemenation.
Much of the code here attempts to follow RFC 8017, particularly section 7.1 and appendix B.2. My intention in doing that was to help me better understand OAEP. This is not intended to be interoperable with things out there in the world. To whatever extent it is interoperable with the world must not be taken as an invitation to use it that way.
Examples#
RSA keys used with OAEP need to have moduli large enough to handle a couple of hash digests and a few other bytes, so we will use a 1024-bit key for our examples.
key2048
is a 2048-bit private key already set up in some undisplayed code.
Just showing that the key exists and is the right size.
# key2048 = ...
pub2048 = key2048.pub_key
assert 2048 - 7 < pub2048.N.bit_length() <= 2048
And lets demo an unfortunate (unless you are an attacker) property of primitive RSA. Our primitive encryption and decryption functions take and yield integers
message = b"My hovercraft is full of eels"
i_message = rsa.Oaep.os2ip(message)
prim_ctext1 = pub2048.encrypt(i_message)
assert key2048.decrypt(prim_ctext1)
So far so good, but sadly, the encryption of the same message always yields the same ciphertext.
# same key and message as above
prim_ctext2 = pub2048.encrypt(i_message)
assert prim_ctext2 == prim_ctext1
# same message and keys as above
oaep_ctext1 = pub2048.oaep_encrypt(message)
decrypted1 = key2048.oaep_decrypt(oaep_ctext1)
assert decrypted1 == message
oaep_ctext2 = pub2048.oaep_encrypt(message)
decrypted2 = key2048.oaep_decrypt(oaep_ctext2)
assert decrypted2 == message
# but now we see that the two ciphertexts are different
assert oaep_ctext1 != oaep_ctext2
A (very limted) choice of hashes#
For my purposes, I could have just hardcoded use of
hashlib.sha256()
or a more modern one,
but most of published test vectors
for RSA-OAEP use hashlib.sha1()
.
For reasons I don’t understand, I had difficulty
getting these type definition within the Oaep
class,
so those are given module scope.
- type toy_crypto.rsa.HashFunc = Callable[[bytes], hashlib._Hash]#
Type for hashlib style hash function.
- type toy_crypto.rsa.MgfFunc = Callable[[bytes, int, str], bytes]#
Type for RFC8017 Mask Generation Function.
The (short) lists of supported hash and mask generation functions
are attributes of the Oaep
class, as are the classes to describe them.
Also note that these are more sanely and readably defined than
what may appear in the automatically generated documentation.
-
Oaep.KNOWN_HASHES:
dict
[str
,HashInfo
] Hashes known for OAEP. keys will be hashlib names.
{ 'sha256': HashInfo(hashlib_name='sha256', function=<built-in function openssl_sha256>, digest_size=32, input_limit=2305843009213693951), 'sha1': HashInfo(hashlib_name='sha1', function=<built-in function openssl_sha1>, digest_size=20, input_limit=18446744073709551615)}
-
Oaep.KNOWN_MGFS:
dict
[str
,MgfInfo
] Known Mask Generation Functions.
{ 'mgf1SHA256': MgfInfo(algorithm='id_mgf1', hashAlgorithm='sha256', function=<staticmethod(<function Oaep.mgf1 at 0x7f96b105e700>)>), 'mgf1SHA1': MgfInfo(algorithm='id_mgf1', hashAlgorithm='sha1', function=<staticmethod(<function Oaep.mgf1 at 0x7f96b105e700>)>)}
Integers, octet-streams, and masks#
Primitive RSA operations on integers, but OAEP is designed for
encryption and decryption of sequences of bytes
,
or octet-streams in the parlence of the standards.
The standards define two functions, I2OSP
and OS2IP
for those
conversions. I don’t implement them as in the standards, but use Python standard library utililties. So class methods Oaep.i2osp()
and
Oaep.os2ip()
are wrappers for standard library method
int.to_bytes()
and class method int.from_bytes()
.
- static Oaep.i2osp(n: int, length: int) bytes [source]
Integer to an octet string of length length.
Implements function from RFC 8017 §4.1.
- Parameters:
- Raises:
ValueError – if
n
is negative.ValueError – if
n
cannot fit inlength
bytes
- Return type:
Warning
When called from a decryption operation, exceptions should be caught and handled discretely.
All operations big-endian.
- static Oaep.os2ip(x: bytes) int [source]
octet-stream to unsigned big-endian int.
Implements function from RFC 8017 §4.2.
Returned is a non-negative integer.
All operations are big-endian.

Chena, like all operations in this class, is big-endian; although in her case it is her rear end is bigger than her bytey end.#
The security that OEAP offers comes from the clevernesss of applying a mask
to the plaintext, while keeping the seed for the mask out of the clear.
The mask is generated by a mask generation function,
MGF1
in the standards.
It is a lot like HKDF,
which probably would have used had it been around when OAEP was first developed.
- static Oaep.mgf1(seed: bytes, length: int, hash_id: str) bytes [source]
Mask generation function.
Generates a unique mask of length
length
as described in appendix B.2.1 of RFC 8017.- Parameters:
seed (
bytes
) – This should come from a CSPRNGlength (
int
) – Length in bytes of the mask to generate.hash_id (
str
) – The name hash function inKNOWN_HASHES
.
- Raises:
ValueError – if
length
\(> 2^{32}\) bytes.ValueError – if
hash_id
is unknown.
- Return type:
The API#
OAEP encryption and decryption performed with method on instances of the
PublicKey
and PrivateKey
respectively.
- PublicKey.oaep_encrypt(message: bytes, label: bytes = b'', hash_id: str = 'sha256', mgf_id: str = 'mgf1SHA256', _seed: bytes | None = None) bytes [source]
RSA OAEP encryption.
- Parameters:
message (
bytes
) – The message to encrypt.label (
bytes
, default:b''
) – Rarely used. Just leave as default.hash_id (
str
, default:'sha256'
) – Name of the hash function.mgf_id (
str
, default:'mgf1SHA256'
) – Name of the MGF function (with hash)._seed (
bytes
|None
, default:None
) – Used for testing only. OAEP is not supposed to be deterministic.
- Raises:
ValueError – if hash or MGF is not recognized.
ValueError – if lengths of inputs exceed what modulus and hash sizes can accommodate.
- Return type:
- PrivateKey.oaep_decrypt(ciphertext: bytes, label: bytes = b'', hash_id: str = 'sha256', mgf_id: str = 'mgf1SHA256') bytes [source]
RSA OAEP decryption.
- Parameters:
- Raises:
ValueError – if hash or MGF is not recognized.
DecryptionError – on various decryption errors. If unsafe error reporting is enabled, details of decryption errors will be provided.
- Return type:
- class toy_crypto.rsa.Oaep[source]#
Tools and data for OAEP.
Although this attempts to follow RFC 8017 in many respects, this is not designed to be interoperable with compliant keys and ciphertext.
- class HashInfo(*, hashlib_name: str, function: HashFunc, digest_size: int, input_limit: int) None [source]#
Information about hash function
- Parameters:
Note that names and identifiers here do not conform to RFCs. These are not mean to be interoperable with anything out in the world.
-
function:
TypeAliasType
# The callable function itself
-
KNOWN_HASHES:
dict
[str
,HashInfo
] = {'sha1': Oaep.HashInfo(hashlib_name='sha1', function=<built-in function openssl_sha1>, digest_size=20, input_limit=18446744073709551615), 'sha256': Oaep.HashInfo(hashlib_name='sha256', function=<built-in function openssl_sha256>, digest_size=32, input_limit=2305843009213693951)}# Hashes known for OAEP. keys will be hashlib names.
-
KNOWN_MGFS:
dict
[str
,MgfInfo
] = {'mgf1SHA1': Oaep.MgfInfo(algorithm='id_mgf1', hashAlgorithm='sha1', function=<staticmethod(<function Oaep.mgf1>)>), 'mgf1SHA256': Oaep.MgfInfo(algorithm='id_mgf1', hashAlgorithm='sha256', function=<staticmethod(<function Oaep.mgf1>)>)}# Known Mask Generation Functions.
- class MgfInfo(*, algorithm: str, hashAlgorithm: str, function: MgfFunc) None [source]#
Information about Mask Generation function.
- classmethod allow_unsafe_messages(allow: bool = True) None [source]#
Allow (or disallow) verbose DecryptionError messages.
- static i2osp(n: int, length: int) bytes [source]#
Integer to an octet string of length length.
Implements function from RFC 8017 §4.1.
- Parameters:
- Raises:
ValueError – if
n
is negative.ValueError – if
n
cannot fit inlength
bytes
- Return type:
Warning
When called from a decryption operation, exceptions should be caught and handled discretely.
All operations big-endian.
- static mgf1(seed: bytes, length: int, hash_id: str) bytes [source]#
Mask generation function.
Generates a unique mask of length
length
as described in appendix B.2.1 of RFC 8017.- Parameters:
seed (
bytes
) – This should come from a CSPRNGlength (
int
) – Length in bytes of the mask to generate.hash_id (
str
) – The name hash function inKNOWN_HASHES
.
- Raises:
ValueError – if
length
\(> 2^{32}\) bytes.ValueError – if
hash_id
is unknown.
- Return type: