Read-Once object implementation in Python

Shahriyar Rzayev
Dev Genius
Published in
4 min readDec 6, 2022

--

What is a Read-Once object?

This concept is defined and explained in the Secure by Design book.

It is also exposed in this link LiveBook.

The overall characteristics of Read-Once objects, grabbed from Book Review: Secure by Design

Read-once objects

A read-once object is an object designed to be read once (or a limited number of times). This object usually represents a value or concept in your domain that’s considered to be sensitive (for example, passport numbers, credit card numbers, or passwords). The main purpose of the read-once object is to facilitate detection of unintentional use of the data it encapsulates.

Here’s a list of the key aspects of a read-once object:

Its main purpose is to facilitate detection of unintentional use.
It represents a sensitive value or concept.
It’s often a domain primitive.
Its value can be read once, and once only.
It prevents serialization of sensitive data.
It prevents sub-classing and extension.

About the Usage

Imagine that you need to pass a password to some service, which will log in to your user. The Login service will only require this password once, so why not restrict it to be read, and used only Once?

Install using pip:

pip install readonce

GitHub repo -> https://github.com/ShahriyarR/py-read-once

Then just inherit from the ReadOnce:

Here the password string is added as a secret. From our definition, it can be read only once and only using get_secret(), no direct access to the secret.

  • You can not expose the object properties as well:
  • Trying to read the password twice:
  • If someone tries to add its own secret to an already instantiated object and then gets back already defined secret data(original secret), it will get only a new secret.
  • You cannot create a subclass from a sensitive class, it is a way of exposing parent class data, but no success:
  • If somebody tries to access secrets directly:
  • You can not pickle it:
  • You can not JSON serialize it:

With default encoder:

With custom encoder:

  • At some points the class itself can be silently dumped to logs, but not here:

How about Python Dataclasses?

Regarding dataclasses, it is prohibited to directly define a field and then add it to the secret:

The result will be:

The better way either to use fields as a “descriptor” way. Imagine you have an idea to share your database credentials in whole chunks. We can create separate sensitive data holders or secrets for each piece of information:

Then we can combine them in one dataclass:

In this way, further, we can get our secrets back, again using get_secret() and only once:

Printing or dumping credentials object will not give any valuable information as well:

Okay, this is not a full “descriptors” in terms of Python(no __get__ and __set__), but I did not open this door intentionally.

  • Another way of using dataclasses is just declaring the fields:

Then initialize the fields in the future. This approach is similar to DTOs(data transfer objects).

  • Is it possible to JSON serialize DBCredentials? Impossible if you decided to dump sensitive fields: Trying with custom encoder:

The same applies to pickling:

Relation with Pydantic

As we know the Pydantic models is a de-facto standard for data validation based on type annotations, we can easily use ReadOnce objects with Pydantic. In this section, I am going to share some tests.

The simplest way to declare Pydantic models with ReadOnce objects is to allow arbitrary types:

Creating credentials:

Again the sensitive data is not exposed:

It can not be serialized in a default way:

Unfortunately, the nature of the ReadOnce object prevents using powerful validation mechanics in the model class. In its core, the sensitive object can not be used twice if it was already consumed:

  • You can call arbitrary time add_secret() if no get_secret() was called before it.
  • Whenever you called get_secret() the sensitive object is considered exhausted.

Imagine we want to validate the password length and try to add a custom validator inside the Pydantic model:

As you can expect, we need first to get the secret data and then validate it, if validation is okay we need to put that secret back into the sensitive object, which is not possible.

Therefore, it is better to push the validation logic toward Password sensitive classes instead. We will explore the validation in-depth in the future.

If we test this InvalidDBCredentialsModel it should fail with: readonce.UnsupportedOperationException: ('Not allowed on sensitive value', 'Sensitive object exhausted; you can not use it twice')

If you have any further Pydantic ideas please open an issue, we can explore and figure out the best usage

Applying best practices from Design by Contract

In order to further ensure data(secret) integrity and security, we can use DbC ideas as it gives us a cleaner way of defining reusable constraints.

I like icontract package which is quite a handy tool. I have tried to explain this YouTube tutorial as well Design-by-Contract programming with Python.

Let’s redefine our sensitive class as:

The current password validation is quite naive, it just checks the length of the string: this is our pre-condition and it is marked as @icontract.require.

But what is @icontract.ensure then? This is our so-called, post-condition: after adding a secret the length of the secrets storage must be equal to one.

We can add more sophisticated password validation using regex, it is up to your business needs.

The question should be asked here: “What is a password for our application?”

After writing down password requirements you can convert them to pre-conditions as part of your DbC approach.

  • I used these ideas in the ReadOnce implementation as well, such as:

Here I make myself to be sure that everything was reset properly.

Another important topic is the invariants.

Thinking about ReadOnce object, at its lifecycle there can be either zero secret or only and only one secret:

If somebody tries to inject more than one piece of data into the secret storage, it will fail as it is a clear invariant violation.

--

--

Azerbaijan Python User Group lead — True Pythonista. Open Source enthusiast.