Danger

Nothing here should be used for any security purposes.

  • If you need cryptographic tools in a Python environment use pyca or PyNaCl.

  • If you need efficient and reliable abstract math utilities in a Python-like environment consider using SageMath.

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:
  • n (int) – A non-negative integer

  • length (int) – Length of returned bytes object

Raises:
Return type:

bytes

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.

Parameters:

x (bytes) – The octet-stream (bytes) you want to make an int from.

Return type:

int

Returned is a non-negative integer.

All operations are big-endian.

A dog with a big rear-end

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 CSPRNG

  • length (int) – Length in bytes of the mask to generate.

  • hash_id (str) – The name hash function in KNOWN_HASHES.

Raises:
Return type:

bytes

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:

bytes

RFC 8017 Section 7.1.1

PrivateKey.oaep_decrypt(ciphertext: bytes, label: bytes = b'', hash_id: str = 'sha256', mgf_id: str = 'mgf1SHA256') bytes[source]

RSA OAEP decryption.

Parameters:
  • ciphertext (bytes) – The message to encrypt.

  • label (bytes, default: b'') – Rarely used.

  • hash_id (str, default: 'sha256') – Name of the hash function.

  • mgf_id (str, default: 'mgf1SHA256') – Name of the MGF function (with hash).

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:

bytes

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:
  • hashlib_name (str) – Name as known by hashlib

  • function (TypeAliasType) – The callable function

  • digest_size (int) – in bytes

  • input_limit (int) – in bytes

Note that names and identifiers here do not conform to RFCs. These are not mean to be interoperable with anything out in the world.

digest_size: int#

in bytes

function: TypeAliasType#

The callable function itself

hashlib_name: str#

Name as known by hashlib

input_limit: int#

maximum input, in bytes

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.

Parameters:
  • algorithm (str) – Name of the algorithm.

  • hashAlgorithm (str) – Key in KNOWN_HASHES.

  • function (TypeAliasType) – A Callable mask generation function

classmethod allow_unsafe_messages(allow: bool = True) None[source]#

Allow (or disallow) verbose DecryptionError messages.

Parameters:

allow (bool, default: True)

Return type:

None

classmethod are_unsafe_messages_allowed() bool[source]#

Does what it says on the tin.

Return type:

bool

static i2osp(n: int, length: int) bytes[source]#

Integer to an octet string of length length.

Implements function from RFC 8017 §4.1.

Parameters:
  • n (int) – A non-negative integer

  • length (int) – Length of returned bytes object

Raises:
Return type:

bytes

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 CSPRNG

  • length (int) – Length in bytes of the mask to generate.

  • hash_id (str) – The name hash function in KNOWN_HASHES.

Raises:
Return type:

bytes

static os2ip(x: bytes) int[source]#

octet-stream to unsigned big-endian int.

Implements function from RFC 8017 §4.2.

Parameters:

x (bytes) – The octet-stream (bytes) you want to make an int from.

Return type:

int

Returned is a non-negative integer.

All operations are big-endian.