Part 9 - Intro to Encrypted Programs

Believe it or not, it is possible to compute with encrypted data. In other words, it's possible to run a program where ALL of the variables in the program are encrypted!

In this tutorial, we're going to walk through very basic tools of encrypted computation. In particular, we're going to focus on one popular approach called Secure Multi-Party Computation. In this lesson, we'll learn how to build an encrypted calculator which can perform calculations on encrypted numbers.

Authors:

References:

Step 1: Encryption Using Secure Multi-Party Computation

SMPC is at first glance a rather strange form of "encryption". Instead of using a public/private key to encrypt a variable, each value is split into multiple shares, each of which operates like a private key. Typically, these shares will be distributed amongst 2 or more owners. Thus, in order to decrypt the variable, all owners must agree to allow the decryption. In essence, everyone has a private key.

Encrypt()

So, let's say we wanted to "encrypt" a variable x, we could do so in the following way.

Encryption doesn't use floats or real numbers but happens in a mathematical space called integer quotient ring which is basically the integers between 0 and Q-1, where Q is prime and "big enough" so that the space can contain all the numbers that we use in our experiments. In practice, given a value x integer, we do x % Q to fit in the ring. (That's why we avoid using number x' > Q).


In [1]:
Q = 1234567891011

In [2]:
x = 25

In [3]:
import random

def encrypt(x):
    share_a = random.randint(-Q,Q)
    share_b = random.randint(-Q,Q)
    share_c = (x - share_a - share_b) % Q
    return (share_a, share_b,  share_c)

In [4]:
encrypt(x)


Out[4]:
(467284329259, -652559215416, 185274886182)

As you can see here, we have split our variable x into 3 different shares, which could be sent to 3 different owners.

Decrypt()

If we wanted to decrypt these 3 shares, we could simply sum them together and take the modulus of the result (mod Q)


In [5]:
def decrypt(*shares):
    return sum(shares) % Q

In [6]:
a,b,c = encrypt(25)

In [7]:
decrypt(a, b, c)


Out[7]:
25

Importantly, notice that if we try to decrypt with only two shares, the decryption does not work!


In [8]:
decrypt(a, b)


Out[8]:
346980890071

Thus, we need all of the owners to participate in order to decrypt the value. It is in this way that the shares act like private keys, all of which must be present in order to decrypt a value.

Step 2: Basic Arithmetic Using SMPC

However, the truly extraordinary property of Secure Multi-Party Computation is the ability to perform computation while the variables are still encrypted. Let's demonstrate simple addition below.


In [9]:
x = encrypt(25)
y = encrypt(5)

In [10]:
def add(x, y):
    z = list()
    # the first worker adds their shares together
    z.append((x[0] + y[0]) % Q)
    
    # the second worker adds their shares together
    z.append((x[1] + y[1]) % Q)
    
    # the third worker adds their shares together
    z.append((x[2] + y[2]) % Q)
    
    return z

In [11]:
decrypt(*add(x,y))


Out[11]:
30

Success!!!

And there you have it! If each worker (separately) adds their shares together, then the resulting shares will decrypt to the correct value (25 + 5 == 30).

As it turns out, SMPC protocols exist which can allow this encrypted computation for the following operations:

  • addition (which we've just seen)
  • multiplication
  • comparison

and using these basic underlying primitives, we can perform arbitrary computation!!!

In the next section, we're going to learn how to use the PySyft library to perform these operations!

Step 3: SMPC Using PySyft

In the previous sections, we outlined some basic intuitions around SMPC is supposed to work. However, in practice we don't want to have to hand-write all of the primitive operations ourselves when writing our encrypted programs. So, in this section we're going to walk through the basics of how to do encrypted computation using PySyft. In particular, we're going to focus on how to do the 3 primitives previously mentioned: addition, multiplication, and comparison.

First, we need to create a few Virtual Workers (which hopefully you're now familiar with given our previous tutorials).


In [12]:
import torch
import syft as sy
hook = sy.TorchHook(torch)

bob = sy.VirtualWorker(hook, id="bob")
alice = sy.VirtualWorker(hook, id="alice")
bill = sy.VirtualWorker(hook, id="bill")

Basic Encryption/Decryption

Encryption is as simple as taking any PySyft tensor and calling .share(). Decryption is as simple as calling .get() on the shared variable


In [13]:
x = torch.tensor([25])

In [14]:
x


Out[14]:
tensor([25])

In [15]:
encrypted_x = x.share(bob, alice, bill)

In [16]:
encrypted_x.get()


Out[16]:
tensor([25])

Introspecting the Encrypted Values

If we look closer at Bob, Alice, and Bill's workers, we can see the shares that get created!


In [17]:
list(bob._tensors.values())


Out[17]:
[]

In [18]:
x = torch.tensor([25]).share(bob, alice, bill)

In [19]:
# Bob's share
bobs_share = list(bob._tensors.values())[0]
bobs_share


Out[19]:
tensor([1061525539398735196])

In [20]:
# Alice's share
alices_share = list(alice._tensors.values())[0]
alices_share


Out[20]:
tensor([424539979954823758])

In [21]:
# Bill's share
bills_share = list(bill._tensors.values())[0]
bills_share


Out[21]:
tensor([3125620499073828975])

And if we wanted to, we could decrypt these values using the SAME approach we talked about earlier!!!


In [22]:
(bobs_share + alices_share + bills_share)


Out[22]:
tensor([25])

As you can see, when we called .share() it simply split the value into 3 shares and sent one share to each of the parties!

Encrypted Arithmetic

And now you see that we can perform arithmetic on the underlying values! The API is constructed so that we can simply perform arithmetic like we would normal PyTorch tensors.


In [23]:
x = torch.tensor([25]).share(bob,alice)
y = torch.tensor([5]).share(bob,alice)

In [24]:
z = x + y
z.get()


Out[24]:
tensor([30])

In [25]:
z = x - y
z.get()


Out[25]:
tensor([20])

Encrypted Multiplication

For multiplication we need an additional party who is responsible for consistently generating random numbers (and not colluding with any of the other parties). We call this person a "crypto provider". For all intensive purposes, the crypto provider is just an additional VirtualWorker, but it's important to acknowledge that the crypto provider is not an "owner" in that he/she doesn't own shares but is someone who needs to be trusted to not collude with any of the existing shareholders.


In [26]:
crypto_provider = sy.VirtualWorker(hook, id="crypto_provider")

In [27]:
x = torch.tensor([25]).share(bob,alice, crypto_provider=crypto_provider)
y = torch.tensor([5]).share(bob,alice, crypto_provider=crypto_provider)

In [28]:
# multiplication

z = x * y
z.get()


Out[28]:
tensor([125])

You can also do matrix multiplication


In [29]:
x = torch.tensor([[1, 2],[3,4]]).share(bob,alice, crypto_provider=crypto_provider)
y = torch.tensor([[2, 0],[0,2]]).share(bob,alice, crypto_provider=crypto_provider)

In [30]:
# matrix multiplication

z = x.mm(y)
z.get()


Out[30]:
tensor([[2, 4],
        [6, 8]])

Encrypted comparison

It is also possible to private comparisons between private values. We rely here on the SecureNN protocol, the details of which can be found here. The result of the comparison is also a private shared tensor.


In [31]:
x = torch.tensor([25]).share(bob,alice, crypto_provider=crypto_provider)
y = torch.tensor([5]).share(bob,alice, crypto_provider=crypto_provider)

In [32]:
z = x > y
z.get()


Out[32]:
tensor([1])

In [33]:
z = x <= y
z.get()


Out[33]:
tensor([0])

In [34]:
z = x == y
z.get()


Out[34]:
tensor([0])

In [35]:
z = x == y + 20
z.get()


Out[35]:
tensor([1])

You can also perform max operations


In [36]:
x = torch.tensor([2, 3, 4, 1]).share(bob,alice, crypto_provider=crypto_provider)
x.max().get()


Out[36]:
tensor([4])

In [37]:
x = torch.tensor([[2, 3], [4, 1]]).share(bob,alice, crypto_provider=crypto_provider)
max_values, max_ids = x.max(dim=0)
max_values.get()


Out[37]:
tensor([4, 3])

Congratulations!!! - Time to Join the Community!

Congratulations on completing this notebook tutorial! If you enjoyed this and would like to join the movement toward privacy preserving, decentralized ownership of AI and the AI supply chain (data), you can do so in the following ways!

Star PySyft on GitHub

The easiest way to help our community is just by starring the Repos! This helps raise awareness of the cool tools we're building.

Join our Slack!

The best way to keep up to date on the latest advancements is to join our community! You can do so by filling out the form at http://slack.openmined.org

Join a Code Project!

The best way to contribute to our community is to become a code contributor! At any time you can go to PySyft GitHub Issues page and filter for "Projects". This will show you all the top level Tickets giving an overview of what projects you can join! If you don't want to join a project, but you would like to do a bit of coding, you can also look for more "one off" mini-projects by searching for GitHub issues marked "good first issue".

If you don't have time to contribute to our codebase, but would still like to lend support, you can also become a Backer on our Open Collective. All donations go toward our web hosting and other community expenses such as hackathons and meetups!

OpenMined's Open Collective Page


In [ ]: