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: dict = None, operations: dict = None, threshold: Optional[int] = None, seed: Union[bytes, bytearray, str] = None) SecretKey[source]

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

>>> 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}, 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 sum operation
>>> 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 a positive integer not larger than the cluster size
>>> SecretKey.generate({'nodes': [{}, {}]}, {'sum': True}, threshold=3)
Traceback (most recent call last):
  ...
ValueError: threshold must 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 derivation of summation-compatible keys is not supported for single-node clusters
dump() dict[source]

Return a JSON-compatible dictionary representation of this key instance.

>>> import json
>>> sk = SecretKey.generate({'nodes': [{}]}, {'store': True})
>>> isinstance(json.dumps(sk.dump()), str)
True
static load(dictionary: dict) SecretKey[source]

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

>>> sk = SecretKey.generate({'nodes': [{}]}, {'store': True})
>>> sk == SecretKey.load(sk.dump())
True
class ClusterKey[source]

Bases: SecretKey

Data structure for representing all categories of cluster key instances.

static generate(cluster: dict = None, operations: dict = None, threshold: Optional[int] = None) ClusterKey[source]

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

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

Cluster keys can only be created for clusters that have two or more nodes.

>>> ClusterKey.generate({'nodes': [{}]}, {'store': True})
Traceback (most recent call last):
  ...
ValueError: cluster configuration must have at least two nodes
dump() dict[source]

Return a JSON-compatible dictionary representation of this key instance.

>>> import json
>>> cluster = {'nodes': [{}, {}, {}]}
>>> ck = ClusterKey.generate(cluster, {'sum': True}, threshold=2)
>>> isinstance(json.dumps(ck.dump()), str)
True
static load(dictionary: dict) ClusterKey[source]

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

>>> cluster = {'nodes': [{}, {}, {}]}
>>> ck = ClusterKey.generate(cluster, {'sum': True}, threshold=2)
>>> ck == ClusterKey.load(ck.dump())
True
class PublicKey[source]

Bases: dict

Data structure for representing all categories of public key instances.

static generate(secret_key: SecretKey) PublicKey[source]

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

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

Return a JSON-compatible dictionary representation of this key instance.

>>> import json
>>> sk = SecretKey.generate({'nodes': [{}]}, {'sum': True})
>>> pk = PublicKey.generate(sk)
>>> isinstance(json.dumps(pk.dump()), str)
True
static load(dictionary: PublicKey) dict[source]

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

>>> sk = SecretKey.generate({'nodes': [{}]}, {'sum': True})
>>> pk = PublicKey.generate(sk)
>>> pk == PublicKey.load(pk.dump())
True
encrypt(key: Union[SecretKey, PublicKey], plaintext: Union[int, str, bytes]) Union[str, Sequence[str], Sequence[int], Sequence[Sequence[int]]][source]

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

>>> key = SecretKey.generate({'nodes': [{}]}, {'store': True})
>>> isinstance(encrypt(key, 123), str)
True

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

>>> key = SecretKey.generate({'nodes': [{}]}, {'sum': True})
>>> encrypt(key, [])
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: SecretKey, ciphertext: Union[str, Sequence[str], Sequence[int], Sequence[Sequence[int]]]) Union[int, str, bytes][source]

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

>>> 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

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).

>>> 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: Union[int, bool, str, list, dict]) Sequence[Union[int, bool, str, list, dict]][source]

Convert a document that may contain ciphertexts intended for multiple-node clusters into secret shares of that 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(secret_key: SecretKey, documents: Sequence[Union[int, bool, str, list, dict]], ignore: Sequence[str] = None) Union[int, bool, str, list, dict][source]

Convert an object that may contain ciphertexts intended for multiple-node clusters into secret shares of that object. Shallow copies are created whenever possible.

>>> 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 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(sk, [123, 'abc'])
Traceback (most recent call last):
  ...
TypeError: array of compatible document shares expected