blindfold module

Python library for working with encrypted data within nilDB queries and replies.

class SecretKey[source]

Bases: dict

Data structure for representing all categories of secret key instances.

static generate(cluster, operations, threshold=None, seed=None)[source]

Return a secret key built according to what is specified in the supplied cluster configuration, operation specification, and other parameters.

Parameters:
  • cluster (dict) – Cluster configuration for this key.

  • operations (dict) – Specification of supported operations on ciphertexts.

  • threshold (Optional[int]) – Minimum number of parties required to decrypt a ciphertext.

  • seed (Union[bytes, bytearray, str, None]) – Seed from which to deterministically derive cryptographic material.

Return type:

SecretKey

The supplied arguments determine which encryption protocol is used when encrypting ciphertexts with this key.

>>> sk = SecretKey.generate({'nodes': [{}]}, {'sum': True})
>>> isinstance(sk, SecretKey)
True

Supplying an invalid combination of configurations and/or parameters raises a corresponding exception.

>>> SecretKey.generate({'nodes': [{}]}, {'sum': True}, seed={})
Traceback (most recent call last):
  ...
TypeError: seed must be a bytes-like object or a string
>>> SecretKey.generate({'nodes': [{}]}, {'sum': True}, threshold='abc')
Traceback (most recent call last):
  ...
TypeError: threshold must be an integer
>>> SecretKey.generate({'nodes': [{}, {}]}, {'match': True}, threshold=1)
Traceback (most recent call last):
  ...
ValueError: thresholds are only supported for the store and sum operations
>>> SecretKey.generate({'nodes': [{}]}, {'sum': True}, threshold=1)
Traceback (most recent call last):
  ...
ValueError: thresholds are only supported for multiple-node clusters
>>> SecretKey.generate({'nodes': [{}]}, {'sum': True}, threshold=-1)
Traceback (most recent call last):
  ...
ValueError: threshold must be a positive integer not larger than the cluster size
>>> SecretKey.generate({'nodes': [{}, {}]}, {'sum': True}, threshold=3)
Traceback (most recent call last):
  ...
ValueError: threshold must be a positive integer not larger than the cluster size
>>> SecretKey.generate({'nodes': [{}]}, {'sum': True}, seed=bytes([123]))
Traceback (most recent call last):
  ...
ValueError: seed-based ... summation-compatible ... not supported for single-node ...
dump()[source]

Return a JSON-compatible dictionary representation of this key instance. This method complements the load method.

Return type:

dict

static load(dictionary)[source]

Return an instance built from a JSON-compatible dictionary representation.

Parameters:

dictionary (dict) – Dictionary representation of a secret key.

Return type:

SecretKey

This method complements the dump method and also makes it possible to work with JSON representations of keys.

>>> sk = SecretKey.generate({'nodes': [{}]}, {'store': True})
>>> import json
>>> sk_json = json.dumps(sk.dump())
>>> sk == SecretKey.load(json.loads(sk_json))
True

Any attempt to supply an invalid input raises an exception.

>>> SecretKey.load('abc')
Traceback (most recent call last):
  ...
TypeError: dictionary expected
class ClusterKey[source]

Bases: SecretKey

Data structure for representing all categories of cluster key instances.

static generate(cluster, operations, threshold=None)[source]

Return a cluster key built according to what is specified in the supplied cluster configuration and operation specification.

Parameters:
  • cluster (dict) – Cluster configuration for this key.

  • operations (dict) – Specification of supported operations on ciphertexts.

  • threshold (Optional[int]) – Minimum number of parties required to decrypt a ciphertext.

Return type:

ClusterKey

The supplied arguments determine which encryption protocol is used when encrypting ciphertexts with this key.

>>> ck = ClusterKey.generate({'nodes': [{}, {}, {}]}, {'sum': True})
>>> isinstance(ck, ClusterKey)
True

Cluster keys can only be created for clusters that have two or more nodes and can only enable encryption for storage and summation compatibility.

>>> ClusterKey.generate({'nodes': [{}]}, {'store': True})
Traceback (most recent call last):
  ...
ValueError: cluster configuration must have at least two nodes
>>> ClusterKey.generate({'nodes': [{}, {}, {}]}, {'match': True})
Traceback (most recent call last):
  ...
ValueError: creation of matching-compatible cluster keys is not supported
dump()[source]

Return a JSON-compatible dictionary representation of this key instance. This method complements the load method.

Return type:

dict

static load(dictionary)[source]

Return an instance built from a JSON-compatible dictionary representation.

Parameters:

dictionary (dict) – Dictionary representation of a cluster key.

Return type:

ClusterKey

This method complements the dump method and also makes it possible to work with JSON representations of keys.

>>> cluster = {'nodes': [{}, {}, {}]}
>>> ck = ClusterKey.generate(cluster, {'sum': True}, threshold=2)
>>> import json
>>> ck_json = json.dumps(ck.dump())
>>> ck == ClusterKey.load(json.loads(ck_json))
True

Any attempt to supply an invalid input raises an exception.

>>> ClusterKey.load('abc')
Traceback (most recent call last):
  ...
TypeError: dictionary expected
class PublicKey[source]

Bases: dict

Data structure for representing all categories of public key instances.

static generate(secret_key)[source]

Return a public key built according to what is specified in the supplied secret key.

Parameters:

secret_key (SecretKey) – Secret key from which to derive this public key.

Return type:

PublicKey

A public key can only be derived from a compatible secret key.

>>> sk = SecretKey.generate({'nodes': [{}]}, {'sum': True})
>>> isinstance(PublicKey.generate(sk), PublicKey)
True
>>> ck = SecretKey.generate({'nodes': [{}, {}]}, {'sum': True})
>>> PublicKey.generate(ck)
Traceback (most recent call last):
  ...
ValueError: cannot create public key for supplied secret key
>>> ck = ClusterKey.generate({'nodes': [{}, {}]}, {'sum': True})
>>> PublicKey.generate(ck)
Traceback (most recent call last):
  ...
TypeError: secret key expected
dump()[source]

Return a JSON-compatible dictionary representation of this key instance. This method complements the load method.

Return type:

dict

static load(dictionary)[source]

Return an instance built from a JSON-compatible dictionary representation.

Parameters:

dictionary (dict) – Dictionary representation of a public key.

Return type:

PublicKey

This method complements the dump method and also makes it possible to work with JSON representations of keys.

>>> sk = SecretKey.generate({'nodes': [{}]}, {'sum': True})
>>> pk = PublicKey.generate(sk)
>>> import json
>>> pk_json = json.dumps(pk.dump())
>>> pk == PublicKey.load(pk.dump())
True

Any attempt to supply an invalid input raises an exception.

>>> PublicKey.load('abc')
Traceback (most recent call last):
  ...
TypeError: dictionary expected
encrypt(key, plaintext)[source]

Return the ciphertext obtained by using the supplied key to encrypt the supplied plaintext.

Parameters:
Return type:

Union[str, Sequence[str], Sequence[int], Sequence[Sequence[int]]]

The supplied key determines which protocol is used to perform the encryption.

>>> sk = SecretKey.generate({'nodes': [{}]}, {'store': True})
>>> len(encrypt(sk, 'abc'))
60
>>> sk = SecretKey.generate({'nodes': [{}]}, {'match': True}, seed='xyz')
>>> encrypt(sk, 'abc')[:70]
'Y3V9Nm4o3F5cTEy+oy3utP19m8XA1eMQ2zFfQiEdGpkE92g4X7eXy4T1yH4u1aBtw0FUs0'
>>> sk = SecretKey.generate({'nodes': [{}]}, {'sum': True})
>>> pk = PublicKey.generate(sk)
>>> isinstance(encrypt(pk, 123), str)
True
>>> sk = SecretKey.generate({'nodes': [{}, {}]}, {'store': True})
>>> shares = encrypt(sk, 'abc')
>>> len(shares) == 2 and all(isinstance(share, str) for share in shares)
True
>>> ck = ClusterKey.generate({'nodes': [{}, {}, {}]}, {'sum': True})
>>> shares = encrypt(ck, 123)
>>> len(shares) == 3 and all(isinstance(share, int) for share in shares)
True

Invocations that involve invalid argument values or types may raise an exception.

>>> encrypt('abc', 123)
Traceback (most recent call last):
  ...
TypeError: secret key, cluster key, or public key expected
>>> key = SecretKey.generate({'nodes': [{}]}, {'sum': True})
>>> encrypt(key, {})
Traceback (most recent call last):
  ...
TypeError: plaintext must be string, integer, or bytes-like object
>>> encrypt(key, 'abc')
Traceback (most recent call last):
  ...
TypeError: plaintext to encrypt for sum operation must be an integer
>>> encrypt(key, 2 ** 64)
Traceback (most recent call last):
  ...
ValueError: numeric plaintext must be a valid 32-bit signed integer
>>> del key['operations']['sum']
>>> encrypt(key, 123)
Traceback (most recent call last):
  ...
ValueError: cannot encrypt the supplied plaintext using the supplied key
decrypt(key, ciphertext)[source]

Return the plaintext obtained by using the supplied key to decrypt the supplied ciphertext.

Parameters:
Return type:

Union[int, str, bytes]

The supplied key determines which protocol is used to perform the decryption.

>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'store': True})
>>> decrypt(key, encrypt(key, 123))
123
>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'store': True})
>>> decrypt(key, encrypt(key, -10))
-10
>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'store': True})
>>> decrypt(key, encrypt(key, bytes([1, 2, 3])))
b'\x01\x02\x03'
>>> key = SecretKey.generate({'nodes': [{}]}, {'store': True})
>>> decrypt(key, encrypt(key, 'abc'))
'abc'
>>> key = SecretKey.generate({'nodes': [{}]}, {'store': True})
>>> decrypt(key, encrypt(key, 123))
123
>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'sum': True})
>>> decrypt(key, encrypt(key, 123))
123
>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'sum': True})
>>> decrypt(key, encrypt(key, -10))
-10
>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'sum': True}, threshold=2)
>>> decrypt(key, encrypt(key, 123))
123
>>> key = SecretKey.generate({'nodes': [{}, {}, {}, {}]}, {'sum': True}, threshold=3)
>>> decrypt(key, encrypt(key, 123)[:-1])
123
>>> key = SecretKey.generate({'nodes': [{}, {}, {}, {}]}, {'sum': True}, threshold=2)
>>> decrypt(key, encrypt(key, 123)[2:])
123
>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'sum': True}, threshold=1)
>>> decrypt(key, encrypt(key, 123)[1:])
123
>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'sum': True}, threshold=2)
>>> decrypt(key, encrypt(key, -10))
-10

A decryption threshold of 1 is permitted in order to accommodate seamlessly scenarios in which it may be useful to replicate plaintext or encrypted data across nodes (such as for redundancy).

>>> key = SecretKey.generate({'nodes': [{}, {}, {}]}, {'store': True}, threshold=1)
>>> decrypt(key, encrypt(key, 123)[:1])
123
>>> decrypt(key, encrypt(key, 123)[1:2])
123
>>> decrypt(key, encrypt(key, 123)[2:])
123

However, the use of a threshold of 1 incurs the same representation size overheads (compared to simply keeping copies of the same data on different nodes) as the use of larger threshold values. For example, note below that 80 > 68 and that 28 > 8.

>>> key = SecretKey.generate({'nodes': [{}]}, {'store': True})
>>> len(encrypt(key, 123))
68
>>> key = SecretKey.generate({'nodes': [{}, {}, {}]}, {'store': True}, threshold=1)
>>> len(encrypt(key, 123)[0])
80
>>> key = SecretKey.generate({'nodes': [{}, {}, {}]}, {'store': True}, threshold=3)
>>> len(encrypt(key, 123)[0])
80
>>> import base64
>>> len(base64.b64encode(int(123).to_bytes(4, 'little')))
8
>>> key = ClusterKey.generate({'nodes': [{}, {}, {}]}, {'store': True}, threshold=1)
>>> len(encrypt(key, 123)[0])
28
>>> key = ClusterKey.generate({'nodes': [{}, {}, {}]}, {'store': True}, threshold=3)
>>> len(encrypt(key, 123)[0])
28

An exception is raised if a ciphertext cannot be decrypted using the supplied key (e.g., because one or both are malformed or they are incompatible).

>>> decrypt('abc', 123)
Traceback (most recent call last):
  ...
TypeError: secret key or cluster key expected
>>> key = SecretKey.generate({'nodes': [{}, {}]}, {'store': True})
>>> decrypt(key, 'abc')
Traceback (most recent call last):
  ...
ValueError: secret key requires a valid ciphertext from a multiple-node cluster
>>> decrypt(
...     SecretKey({'cluster': {'nodes': [{}]}, 'operations': {}}),
...     'abc'
... )
Traceback (most recent call last):
  ...
ValueError: cannot decrypt the supplied ciphertext using the supplied key
>>> key_alt = SecretKey.generate({'nodes': [{}, {}]}, {'store': True})
>>> decrypt(key_alt, encrypt(key, 123))
Traceback (most recent call last):
  ...
ValueError: cannot decrypt the supplied ciphertext using the supplied key
allot(document)[source]

Convert a document that may contain ciphertexts intended for multiple-node clusters into secret shares of that document.

Parameters:

document (Union[int, bool, str, list, dict]) – Document to convert into secret shares.

Return type:

Sequence[Union[int, bool, str, list, dict]]

The output consists of a sequence of documents; the number of documents in the sequence is determined by the number of secret shares that appear in ciphertext values found in the document. Shallow copies are created whenever possible.

>>> d = {
...     'id': 0,
...     'age': {'%allot': [1, 2, 3]},
...     'dat': {'loc': {'%allot': [4, 5, 6]}}
... }
>>> for d in allot(d): print(d)
{'id': 0, 'age': {'%share': 1}, 'dat': {'loc': {'%share': 4}}}
{'id': 0, 'age': {'%share': 2}, 'dat': {'loc': {'%share': 5}}}
{'id': 0, 'age': {'%share': 3}, 'dat': {'loc': {'%share': 6}}}

A document with no ciphertexts intended for decentralized clusters is unmodofied; a list containing this document is returned.

>>> allot({'id': 0, 'age': 23})
[{'id': 0, 'age': 23}]

Any attempt to convert a document that has an incorrect structure raises an exception.

>>> allot({1, 2, 3})
Traceback (most recent call last):
  ...
TypeError: boolean, integer, float, string, list, dictionary, or None expected
>>> allot({'id': 0, 'age': {'%allot': [1, 2, 3], 'extra': [1, 2, 3]}})
Traceback (most recent call last):
  ...
ValueError: allotment must only have one key
>>> allot({
...     'id': 0,
...     'age': {'%allot': [1, 2, 3]},
...     'dat': {'loc': {'%allot': [4, 5]}}
... })
Traceback (most recent call last):
  ...
ValueError: number of shares in subdocument is not consistent
>>> allot([
...     0,
...     {'%allot': [1, 2, 3]},
...     {'loc': {'%allot': [4, 5]}}
... ])
Traceback (most recent call last):
  ...
ValueError: number of shares in subdocument is not consistent
unify(key, documents, ignore=None)[source]

Combine a sequence of compatible secret shares of a document into one document.

Parameters:
Return type:

Union[int, bool, str, list, dict]

Corresponding plaintexts acting as leaf values are deduplicated and corresponding secret shares acting as leaf values are used to reconstruct plaintexts that appear in the resulting document.

>>> data = {
...     'a': [True, 'v', 12],
...     'b': [False, 'w', 34],
...     'c': [True, 'x', 56],
...     'd': [False, 'y', 78],
...     'e': [True, 'z', 90],
... }
>>> sk = SecretKey.generate({'nodes': [{}, {}, {}]}, {'store': True})
>>> encrypted = {
...     'a': [True, 'v', {'%allot': encrypt(sk, 12)}],
...     'b': [False, 'w', {'%allot': encrypt(sk, 34)}],
...     'c': [True, 'x', {'%allot': encrypt(sk, 56)}],
...     'd': [False, 'y', {'%allot': encrypt(sk, 78)}],
...     'e': [True, 'z', {'%allot': encrypt(sk, 90)}],
... }
>>> shares = allot(encrypted)
>>> decrypted = unify(sk, shares)
>>> data == decrypted
True

It is possible to wrap nested lists of shares to reduce the overhead associated with the {'%allot': ...} and {'%share': ...} wrappers.

>>> data = {
...     'a': [1, [2, 3]],
...     'b': [4, 5, 6],
...     'c': None,
...     'd': 1.23
... }
>>> sk = SecretKey.generate({'nodes': [{}, {}, {}]}, {'store': True})
>>> encrypted = {
...     'a': {'%allot': [encrypt(sk, 1), [encrypt(sk, 2), encrypt(sk, 3)]]},
...     'b': {'%allot': [encrypt(sk, 4), encrypt(sk, 5), encrypt(sk, 6)]},
...     'c': None,
...     'd': 1.23
... }
>>> shares = allot(encrypted)
>>> decrypted = unify(sk, shares)
>>> data == decrypted
True

The ignore parameter specifies which dictionary keys should be ignored during unification. By default, '_created' and '_updated' are ignored.

>>> shares[0]['_created'] = '123'
>>> shares[1]['_created'] = '456'
>>> shares[2]['_created'] = '789'
>>> shares[0]['_updated'] = 'ABC'
>>> shares[1]['_updated'] = 'DEF'
>>> shares[2]['_updated'] = 'GHI'
>>> decrypted = unify(sk, shares)
>>> data == decrypted
True

Unification returns the sole document when a one-document list is supplied.

>>> 123 == unify(sk, [123])
True

Any attempt to supply incompatible document shares raises an exception.

>>> unify('abc', [])
Traceback (most recent call last):
  ...
TypeError: secret key or cluster key expected
>>> unify(sk, 123)
Traceback (most recent call last):
  ...
TypeError: sequence of documents expected
>>> unify(sk, [123, 'abc'])
Traceback (most recent call last):
  ...
TypeError: sequence of compatible document shares expected
>>> unify(sk, [123, 123], 456)
Traceback (most recent call last):
  ...
TypeError: ignored keys must be supplied as a sequence of strings