@@ -362,7 +362,7 @@ def generate(
362362
363363 if (
364364 (not isinstance (operations , dict )) or
365- (not set (operations .keys ()).issubset ({'store' , 'match' , 'sum' }))
365+ (not set (operations .keys ()).issubset ({'store' , 'seed' , ' match' , 'sum' }))
366366 ):
367367 raise ValueError ('valid operations specification is required' )
368368
@@ -393,6 +393,14 @@ def generate(
393393 bytes .__new__ (bcl .secret , _random_bytes (32 , seed ))
394394 )
395395
396+ if secret_key ['operations' ].get ('seed' ):
397+ # Symmetric key for encrypting the plaintext or the shares of a plaintext.
398+ secret_key ['material' ] = (
399+ bcl .symmetric .secret ()
400+ if seed is None else
401+ bytes .__new__ (bcl .secret , _random_bytes (32 , seed ))
402+ )
403+
396404 if secret_key ['operations' ].get ('match' ):
397405 # Salt for deterministic hashing of the plaintext.
398406 secret_key ['material' ] = _random_bytes (64 , seed )
@@ -743,6 +751,33 @@ def encrypt(
743751 ))
744752 return list (map (_pack , shares ))
745753
754+ # Encrypt a plaintext for storage and retrieval.
755+ if key ['operations' ].get ('seed' ):
756+ # For single-node clusters, the data is encrypted using a symmetric key.
757+ if len (key ['cluster' ]['nodes' ]) == 1 :
758+ return _pack (
759+ bcl .symmetric .encrypt (key ['material' ], bcl .plain (buffer ))
760+ )
761+
762+ # For multiple-node clusters, the ciphertext is secret-shared using XOR
763+ # (with each share symmetrically encrypted in the case of a secret key).
764+ optional_enc = (
765+ (lambda s : bcl .symmetric .encrypt (key ['material' ], bcl .plain (s )))
766+ if 'material' in key else
767+ (lambda s : s )
768+ )
769+ seeds = []
770+ aggregate = bytes (len (buffer ))
771+ for _ in range (len (key ['cluster' ]['nodes' ]) - 1 ):
772+ seed = _random_bytes (64 )
773+ seeds .append (optional_enc (seed ))
774+ mask = _random_bytes (len (buffer ), seed )
775+ aggregate = bytes (a ^ b for (a , b ) in zip (aggregate , mask ))
776+ share = optional_enc (
777+ bytes (a ^ b for (a , b ) in zip (aggregate , buffer ))
778+ )
779+ return list (map (_pack , seeds )) + [_pack (share )]
780+
746781 # Encrypt (i.e., hash) a plaintext for matching.
747782 if key ['operations' ].get ('match' ):
748783 # The deterministic salted hash of the encoded plaintext is the ciphertext.
@@ -949,6 +984,40 @@ def decrypt(
949984
950985 return _decode (bytes_ )
951986
987+ # Decrypt a value that was encrypted for storage and retrieval.
988+ if key ['operations' ].get ('seed' ):
989+ # For single-node clusters, the plaintext is encrypted using a symmetric key.
990+ if len (key ['cluster' ]['nodes' ]) == 1 :
991+ try :
992+ return _decode (
993+ bcl .symmetric .decrypt (
994+ key ['material' ],
995+ bcl .cipher (_unpack (ciphertext ))
996+ )
997+ )
998+ except Exception as exc :
999+ raise error from exc
1000+
1001+ # For multiple-node clusters, the ciphertext is secret-shared using XOR
1002+ # (with each share symmetrically encrypted in the case of a secret key).
1003+ shares = [_unpack (share ) for share in ciphertext ]
1004+ if 'material' in key :
1005+ try :
1006+ shares = [
1007+ bcl .symmetric .decrypt (key ['material' ], bcl .cipher (share ))
1008+ for share in shares
1009+ ]
1010+ except Exception as exc :
1011+ raise error from exc
1012+
1013+ bytes_ = bytes (len (shares [- 1 ]))
1014+ for share_ in shares [:- 1 ]:
1015+ share_ = _random_bytes (len (bytes_ ), share_ )
1016+ bytes_ = bytes (a ^ b for (a , b ) in zip (bytes_ , share_ ))
1017+ bytes_ = bytes (a ^ b for (a , b ) in zip (bytes_ , shares [- 1 ]))
1018+
1019+ return _decode (bytes_ )
1020+
9521021 # Decrypt a value that was encrypted in a summation-compatible way.
9531022 if key ['operations' ].get ('sum' ):
9541023 # For single-node clusters, the Paillier cryptosystem is used.
0 commit comments