blindfold module
Python library for working with encrypted data within nilDB queries and replies.
- class SecretKey[source]
Bases:
dictData 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:
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
loadmethod.- Return type:
- static load(dictionary)[source]
Return an instance built from a JSON-compatible dictionary representation.
This method complements the
dumpmethod 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:
SecretKeyData 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:
- Return type:
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
loadmethod.- Return type:
- 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:
This method complements the
dumpmethod 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:
dictData 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:
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
loadmethod.- Return type:
- static load(dictionary)[source]
Return an instance built from a JSON-compatible dictionary representation.
This method complements the
dumpmethod 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.
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:
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
1is 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
1incurs 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 that80>68and that28>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:
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:
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
ignoreparameter 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