FHE Basics¶

Einführung¶

Veranschaulichung was FHE kann:

  • Encode
  • Encrypt
  • Decrypt
  • Decode

Basisoperationen:

  • Addition
  • Multiplikation
  • Rotation

Step 1. Importiere pyhelayers library¶

In [35]:
import pyhelayers
import utils 
utils.verify_memory()
# Print the ciphertext content for demo purposes
pyhelayers.get_print_options().print_encrypted_content=True 
print("Imported pyhelayers", pyhelayers.VERSION)
Imported pyhelayers 1.5.3.0

Step 2. Initialisiere kryptografische Parameter¶

In [37]:
requirement = pyhelayers.HeConfigRequirement(
    num_slots = 8192, # Number of slots per ciphertext
    multiplication_depth = 2, # Allow 2 levels of multiplications
    fractional_part_precision = 40, # Set the precision to 1/2^40.
    integer_part_precision = 20, # Set the largest number to 2^20.
    security_level = 128)

he_context = pyhelayers.DefaultContext()
he_context.init(requirement)

Step 3. Perform basic Homomorphic Encryption operations¶

image.png

X + Y =~ decode( decrypt( encrypt( encode(X)) + encrypt(encode(Y)) ) )

Encoding, decoding, encrypting and decrypting¶

Bsp: CKKS

Encoding(Klartextvektor) --> Polynom¶

- nur andere Repräsentation
- Encodings gleicher Klartexte sind identisch (deterministisch)
- Encoding fügt schon enormen Overhead hinzu
In [39]:
# Create the Encoder using the context.
encoder = pyhelayers.Encoder(he_context)

# Encode a vector.
v1 = [1.0, 2.0, 3.0]
encode_v1_1 = encoder.encode(v1)
encode_v1_2 = encoder.encode(v1)

print(
      f'1. Encoding: {encode_v1_1} \n'
      f'2. Encoding: {encode_v1_2} \n'
     )

print(f'Typ von Klartext: {type(v1)} \n'
      f'Länge des Klartextes: {len(v1)} \n')

print(f'Typ von Encoding: {type(encode_v1_1)} \n'
      f'Länge des Encodings: {encode_v1_1.slot_count()}\n \n'
      f'--> Das Encoding ist viel länger als der Klartext - großer Overhead')
1. Encoding: PTile CI=2, logscale=40 , bit usage=41.585, max value=3
(1,-4.75e-17) (2,1.11e-18) (3,-1.19e-16) (2.42e-11,4.24e-18) ...  (3.96e-11,-2.7e-18)
 
2. Encoding: PTile CI=2, logscale=40 , bit usage=41.585, max value=3
(1,-4.75e-17) (2,1.11e-18) (3,-1.19e-16) (2.42e-11,4.24e-18) ...  (3.96e-11,-2.7e-18)
 

Typ von Klartext: <class 'list'> 
Länge des Klartextes: 3 

Typ von Encoding: <class 'pyhelayers.PTile'> 
Länge des Encodings: 8192
 
--> Das Encoding ist viel länger als der Klartext - großer Overhead

Encrypt(Encoding) --> Paar von Polynomen¶

- Sicherheit kommt hier her
- jeder Geheimtext sieht anders aus (fügt noise hinzu)
In [4]:
c1_1 = encoder.encode_encrypt(v1)
c1_2 = encoder.encode_encrypt(v1)

print(f'1. Encryption: {c1_1} \n'
      f'2. Encryption: {c1_2} \n'
     )

print(f'Typ von Encryption: {type(c1_1)} \n'
      f'Länge der Verschlüsselung: {c1_1.slot_count()}')
1. Encryption: CIPHERTEXT [[1131135981975755219,1112425318636213092,505430410042946198,667051018363176745 ...  537006492109] ] 
2. Encryption: CIPHERTEXT [[429019767460013127,831004575477499145,566215651600707057,172610751300467944 ...  131783248038] ] 

Typ von Encryption: <class 'pyhelayers.CTile'> 
Länge der Verschlüsselung: 8192

Decrypt(ciphertext) --> Encoding¶

- geht nur mit secret key (Passwort)
- die Dechiffrierung ist korrekt
- die Dechiffrierung hat einen Overhead
- die Dechiffrierung ist approximativ
In [5]:
d1 = encoder.decrypt_decode_double(c1_1)
d2 = encoder.decrypt_decode_double(c1_2)
print(f'1. Decryption: {d1} \n'
      f'2. Decryption: {d2} \n'
     )

print(f'Typ der Dechiffrierung: {type(d1)} \n'
      f'Länge der Dechiffrierung: {len(d1)} \n')

print(f'Vergleich Klartext vs. Dechiffrierung \n'
      f'Klartext:       {v1} \n'
      f'Dechiffrierung: {d1[0:3]} \n'
      f'Rest des Vektors: {d1[3:6]}\n')
1. Decryption: [ 1.00000000e+00  2.00000000e+00  3.00000000e+00 ...  2.23855968e-10
 -2.97007349e-09 -5.71343740e-10] 
2. Decryption: [ 9.99999999e-01  2.00000000e+00  3.00000000e+00 ...  1.11710537e-11
 -7.50825423e-10 -1.66492654e-09] 

Typ der Dechiffrierung: <class 'numpy.ndarray'> 
Länge der Dechiffrierung: 8192 

Vergleich Klartext vs. Dechiffrierung 
Klartext:       [1.0, 2.0, 3.0] 
Dechiffrierung: [1. 2. 3.] 
Rest des Vektors: [ 3.31488864e-09 -5.84605247e-10 -2.36247259e-09]

Zusammenfassung zu Encoding, Encryption, Decoding, Decryption¶

In [6]:
print('Initial vector:   {}'.format(v1))
print('Encoded vector:   {}'.format(encode_v1_1))
print('Encrypted vector: {}'.format(c1_1))
print('Decoded vector:   {}'.format(d1[:3]))
Initial vector:   [1.0, 2.0, 3.0]
Encoded vector:   PTile CI=2, logscale=40 , bit usage=41.585, max value=3
(1,-4.75e-17) (2,1.11e-18) (3,-1.19e-16) (2.42e-11,4.24e-18) ...  (3.96e-11,-2.7e-18)

Encrypted vector: CIPHERTEXT [[1131135981975755219,1112425318636213092,505430410042946198,667051018363176745 ...  537006492109] ]
Decoded vector:   [1. 2. 3.]

Rechnen auf verschlüsselten Daten¶

Addition/ Subtraktion¶

Addieren mittels .add() Methode auf dem ersten CTile:

In [21]:
plain_1 = [1, 2, 3]
plain_2 = [4, 5, 6.5]

cipher_1 = encoder.encode_encrypt(plain_1)
cipher_2 = encoder.encode_encrypt(plain_2)

print(f'geheimtext 1: {cipher_1} \ngeheimtext 2: {cipher_2}')
# !!! Wir addieren auf verschlüsselten Daten!!!
cipher_1.add(cipher_2)
print(f'ergebnis:     {cipher_1} \n')
print('Result Addition:    {}'.format(encoder.decrypt_decode_double(cipher_1)[:3]))

# analog mit subtraktion
plain_3 = [1, 2, 3]
plain_4 = [-4, -3, -6.5]

cipher_3 = encoder.encode_encrypt(plain_3)
cipher_4 = encoder.encode_encrypt(plain_4)

# !!! Wir subtrahieren auf verschlüsselten Daten!!!
cipher_3.add(cipher_4)
print('Result Subtraktion: {}'.format(encoder.decrypt_decode_double(cipher_3)[:3]))
geheimtext 1: CIPHERTEXT [[779118373764567359,777002830475277794,487170086735964006,607831202537423885 ...  28626693460] ] 
geheimtext 2: CIPHERTEXT [[636452704892897235,18073020191828212,252724995129612565,475442727712704636 ...  741442868876] ]
ergebnis:     CIPHERTEXT [[262649574050781457,795075850667106006,739895081865576571,1083273930250128521 ...  770069562336] ] 

Result Addition:    [5.  7.  9.5]
Result Subtraktion: [-3.         -1.         -3.49999999]

Multiplikation/ Division¶

Addieren mittels .multiply() Methode auf dem ersten CTile:

In [23]:
plain_1 = [1, 2, 3]
plain_2 = [4, 5, -6.5]

cipher_1 = encoder.encode_encrypt(plain_1)
cipher_2 = encoder.encode_encrypt(plain_2)

print(f'geheimtext 1: {cipher_1} \ngeheimtext 2: {cipher_2}')
# !!! Wir multiplizieren auf verschlüsselten Daten!!!
cipher_1.multiply(cipher_2)
print(f'ergebnis:     {cipher_1} \n')

print('Result Multiplikation:    {}'.format(encoder.decrypt_decode_double(cipher_1)[:3]))

# analog mit Division
plain_3 = [1, 2, 3]
plain_4 = [1/2, 1/3, -1/5]

cipher_3 = encoder.encode_encrypt(plain_3)
cipher_4 = encoder.encode_encrypt(plain_4)

# !!! Wir dividieren auf verschlüsselten Daten!!!
cipher_3.multiply(cipher_4)
print('Result Division: {}'.format(encoder.decrypt_decode_double(cipher_3)[:3]))
geheimtext 1: CIPHERTEXT [[1097283863602053394,9354795132089334,400621897882819895,326089640398490511 ...  454926127153] ] 
geheimtext 2: CIPHERTEXT [[916575898319111688,1053580958696087342,816237921836376049,138814830951217515 ...  913517383928] ]
ergebnis:     CIPHERTEXT [[654564794388088258,188519724547076812,1139610384326327666,1083455393637216895 ...  74583677062] ] 

Result Multiplikation:    [  4.          10.         -19.49999997]
Result Division: [ 0.5         0.66666667 -0.59999999]

Rotation¶

Rotieren mittels .rotate() Methode auf dem ersten CTile:

In [32]:
m = [1, 2, 3]
c = encoder.encode_encrypt(plain_1)


print(f'geheimtext 1: {c}')
c.rotate(-1)
print(f'ergebnis:     {c} \n')

print(f'm:               {m}')
print('Result Rotation: {}'.format(encoder.decrypt_decode_double(c)[:4]))
geheimtext 1: CIPHERTEXT [[586537068613204090,219016388662698895,1147545213661994966,768152345655487977 ...  179994915353] ]
ergebnis:     CIPHERTEXT [[599926430988645676,727055542217798224,998235849372833756,836812647448946916 ...  409630059191] ] 

m:               [1, 2, 3]
Result Rotation: [2.94314936e-07 1.00000044e+00 2.00000004e+00 3.00000000e+00]

Durchschnitt berechnen¶

Man kann auch komplizierte Funktionen zusammenbauen

In [34]:
v1 = [1.0, 5.0, 13.0]
c1 = encoder.encode_encrypt(v1)

v2 = [2.0, 6.0, 18.0]
c2 = encoder.encode_encrypt(v2)

v3 = [3.0, 1.0, 20.0]
c3 = encoder.encode_encrypt(v3)

print(f'geheimtext 1: {c1} \ngeheimtext 2: {c2}\ngeheimtext 3: {c3}')
c1.add(c2) 
c1.add(c3)
c1.multiply_scalar(1/3)
print(f'ergebnis:     {c1} \n')

print('Result: {}'.format(encoder.decrypt_decode_double(c1)[:3]))
geheimtext 1: CIPHERTEXT [[552070155153608120,805284675234682769,208357715202033589,310653003918033555 ...  1047349113151] ] 
geheimtext 2: CIPHERTEXT [[798657649215876734,860634207456706686,851552505377225521,792274579944754522 ...  468991438378] ]
geheimtext 3: CIPHERTEXT [[385361835998236336,897719448037193841,580809716355919033,769178920464783808 ...  397939367739] ]
ergebnis:     CIPHERTEXT [[396067391478773528,1065516990712473916,888057515677871983,504469227572553077 ...  927220862567] ] 

Result: [ 2.  4. 17.]