SBTB 2023: Sinan Pehlivanoglu, Oblivious Privacy in a Statically Typed World
Recording: SBTB 2023: Sinan Pehlivanoglu, Oblivious Privacy in a Statically Typed World
[Music] all right hello everyone um so for those who don't know me I'm sinan uh my background is mainly in programming language Theory and uh distributed systems but uh for the past year or two uh I've been working in Quantum Computing and quantum mechanics um things like uh how do we represent quantum mechanics using concurrency as well as um working with Quantum processes where we have multiple simultaneous processes where we have to pass data orders of magnitude in hundreds of gigabytes sometimes petabytes of data um so if that kind of thing interests you uh come talk to me afterwards uh but today's talk uh will not be about this uh today's talk will be about privacy um so so I find it important to Define what privacy is in a technical setting because there are multiple different definitions so in the scope of this talk the Privacy will be uh more concerned with the developer experience and sort of automatically asserting uh certain constraints brought on by laws like gdpr uh and not about the more academic definition which is differential privacy uh where you have a attacker model that has access to your memory uh that is not what we're dealing with today um if anyone's interesting I wrote a technical paper about this uh it goes into more detail about um transforming the ests and the compiler magic uh behind all this uh you can read it uh but I'll try to keep it a little bit more brief today so uh let's say we have some social media represented by our little bird there uh and we have a customer let's say Coke um this social media app provides a streaming API this may or may not be a real API uh that you're able to subscribe to and say okay I want uh data about my followers and uh people who interact with hasht Coke in some way it can be Impressions engagements likes reposts uh whatever and as you get new data about this demographic I want you to keep sending it to me so um the social media Aggregates the user data uh with some different um uh characteristics like the age distributions the gender distribution uh deallocation stats and all of that and sends it to Coke and sometime passes we get new data and we send it again so now let's say that we have a user who doesn't like Coke I don't know their taste bus got burned they like Pepsi instead uh so they say I don't want my data shared with Coke now according to gdpr the user is able to dictate with this fine gr scope uh who their dat goes to and who not uh so the social media platform legally cannot send this user data whether anonymized or not uh to Coke so the next time you send this data you essentially have to exclude this user's data this is a difficult problem first of all user preferences um can change at any time if you have some pipeline where the especially data is anonymized it will be very difficult to figure out um whose data you should be sending and who not if the user changes their settings like when you're about to send the data uh it is also difficult because the user preferences live outside of your application they live in a database somewhere in a table and you essentially have to go to that database to fet fetch the user uh preferences on like who these blacklisted brands are every time uh so now let's zoom out a little bit and let's say we have an architecture like this we have some user service and let's say that this user service defines the user schema what it means to be a user and they publish this and as a library uh to multiple Services we have the Enterprise data service which is a service that shares data with third parties and we have some uh machine learning and internal analytics things so when it comes to legal terms uh what you can do with user data internally versus what you can share with third parties highly differs uh so like there's pretty much no restrictions on the internal analytics but there's a lot of restrictions on what you can sell um then there's the platform service this is where the user goes to see their own data and obviously there should not be any restrictions on what they can see about themselves or not um and whenever some privacy change take effect it has to take effect in the entire architecture but it has to take effect in different ways um so here are observations uh the decision whether a user's data should be sent to a third party should be deferred as much as possible because these preferences can uh continue to change at any given time two our the user preference data lives outside of our service uh and three the user data should not exist in its Ro form because essentially what we're trying to avoid is human error and if we allow the data to exist in its raw form then all bets are off human error still applies uh and four privacy controls should be centralized so you it should not be all over your application such that when something changes in law something changes in your implementation you should shouldn't have to go to multiple files and change a million things it should be localized to one place where you can make the change and it will take effect everywhere so how do we do it now you know one idea is just root Force if statements all over your application but as I just mentioned this is bad uh other than that we have what we call IFC uh this is information flow control uh that dictates uh essentially where the data can flow and where not um so these have some limitations uh mainly they are very annotation heavy and they have very significant runtime and uh compile time overheads it is also the case that so some of these work are nearly 20 years old but they haven't really been used in the industry and the reason for that is when these were implemented they were implemented in these small toy dsls right where you say okay I have my primitive types I have integers I have strings and my logic works for that but it turns out that when you scale this to a full-fledged general purpose language a lot of these uh systems stop working they don't play well with more complex types uh so we essentially need something that will work with a general uh purpose language like Scala uh so here's the core observation instead of preventing where the data can go like in IFC we're going to let the data travel freely inside some secure container but when you actually try to read the data we will impose some set of restrictions um so for those who know me I love functional programming I love my mon ads uh and you might say why not use uh something like that for this and we can uh we can essentially implement this privacy container uh that whenever you try to map it or whenever you try to read it it executes this validate function uh otherwise it raises some error uh which is all good but this raises a f isues first of all some might argue that map is not valid because uh it is now effectful there's a side effect in that map it's not pure anymore uh and the second problem is when you do this you will have to go through your entire application and fix the types this isn't too bad in a statically type compile language because the compiler will direct you uh wherever you need to go but it's still a lot of work if you go to a large Enterprise and if you tell them okay here's my privacy solution but we're going to essentially need to fix up all of our code base uh they will just laugh and say no um it also raised another issue um when implementing something like a flat map or a product if this is an applicative uh there is this interesting problem of propagation problem if we have a user type uh they have a name they have email and they have these list of interests what should the wrapper of. email be I the restrictions I impose on the entire user object will still be valid for the email object but the email object may have extra restrictions because email is considered personally identifiable information and there are extra laws governing that so how do you write this monatic container that is able to automatically add these restrictions on the go and this turns out to be a very difficult thing so the inspiration of this work is record contracts um record contracts are essentially these lightweight containers around data uh that allows you to write um restrictions on how the data can be read and how it can be mutated so if you look to the bottom we have you know these accessor Pro procedure the mutator procedure uh that allows us to sort of write these restrictions this is all good in racket because the core racket is untyped so these contracts are higher uh order citizens and they are able to travel freely across the application but how do we get something like this in a typed setting so let's go through our goals again our data should not exist in a raw form this means that the apply function The Constructor should automatically be modified uh in a way that it does not allow raw data okay so we can do something like this I have uh my user uh object and for each of the fields I can say okay it enforces uh user personally identifiable information policy or for the interest I can say it uh enforces this user interest policy and then I can also bind a total um policy to my user object so when you actually do this user construction it will automatically be in the type of this user privacy policy of user but of course doing just this is not enough because if you just do this you'll get something like that you will have all these misaligned types and your compiler will complain and you will have to go around and fixing all of them and as we just mentioned uh we don't want that but all hope is not lost if we can prove some sort of equivalence to opton isomorphism between our user object and user privacy policy object then the compiler can automatically fix up the types uh prior to type checking so what this means is that if I have a field on my user object or a method uh let's call it name uh and it compiles as this then when I wrap this inside a user privacy policy container it should still compiler comp pile uh and this is going to be true for all the fields and all the methods okay well uh for those of you who like programming languages uh this might be familiar this is just structural typing with some quirks um scolar technically has structural typing but uh the syntax is very bloated and it it has some uh restrictions so I went with a different approach um but before that we're going to add a few more obser ation uh first um we have to observe that all of our arguments that existed on the original type as well as the return type may or may not be uh also policy protected so if I have some method M that takes two integers the user should be able to pass a policy wrapped integer to this method or a regular integer and uh similarly on The Operators uh the non-p poliy protected methods should still be able to accept policy protected arguments this is important because things like the plus operator comes from the standard Library I don't have access to them to to modify their signatures and what types they accept and same applies for any functionality that I import from an external Library so if I have uh five plus a policy protected five that should be the same thing as if both of those integers are policy protected and all of these should evaluate to a policy protector 10 um so when we're what we're going to do is essentially we're going to go to all of the fields and all the methods on our types and we are going to copy them in before the type checking happens to our uh policy objects that are in scope and this happens two ways if the compiler can decide with absolute certainty that this function f is pure then it can just uh copy the function change the arguments and return type to a policy protected type and uh keep on going because the function has no side effects we are sure that this data will not leave the application boundary thus it will not leak but otherwise we need to wrap it inside this check function to make sure that wherever in case the data leaves our application it is safe for it to go wherever it is going let it be standard out let it be a socket let it be some uh other API um so we can Implement uh this user interest policy object uh that extends the policy interface uh it has some internal state it has a check function that returns a bulling and in this case it fetches the brands that the user blacklisted from an external service it checks if the brand that I'm trying to send this data to contains um is contained in this bad Brands list and if so it returns false otherwise it's true I can also implement this default modifier uh that says when whenever you try to read this data exclude some parts of it because uh for example legally speaking religious and political affiliations are considered like very very critical data and you cannot under any circumstances send them anywhere uh so whenever you're trying to read this you should be um essentially excluding those um but when we are evaluating these check functions uh that we have in our policy objects we may sometimes need extra arguments and in this specifically case uh we need to know which brand is requesting the data which brand I'm sending my data to but when you're calling Whenever Wherever this check function is called uh there may be multiple objects of type brand in scope and this is actually ambiguous it is very difficult to um figure out automatically uh which brand uh is the correct brand so this is where we need the user to annotate a symbol saying okay this is where my argument value resides uh and they don't have to reside in near the function call they can essentially reside anywhere in the flute scope of that function call and the compiler will automatically uh accumulate that and uh wire those arguments to the correct call um so if I'm sending if I have a function that is send data uh I can say okay this argument brand is my policy argument and this user is also my policy argument and they will automat atically get wired into that check function call uh so you can use it in your check function uh as you as you see it fits so in this case uh when we're fetching the the blacklisted Brands we need the user ID uh and that gets automatically wired in we also need the brand that is requesting the data that also gets automatically wired in so in summary essentially the only thing that the developer needs to do to essentially um migrate their application to to this framework is Define their policy classes which are centralized and can be reused all over the application uh update their Constructors with the policy enforcement uh keyword and the correct policies and annotate their parameters um this is significantly less work than any of the other Solutions out there that that I'm aware of um now I want to talk a little bit more about this policy propagation problem because this is a really difficult and really interesting problem in my opinion um one thing we need to note is if we have a user object and at some point you do field access member access on this object and say user. email the policy that propagated to the email field may still need access to the complete user object because if you remember here our check function needs the user ID and user ID resides in the user object when you just get the email string you no longer longer have that ID in scope um so we need to figure out a way to essentially carry that extra data that we need around as as the policy membranes get propagated um and two in order for the compiler to be able to move these uh functions to the policy object it needs to exist um before uh before we can actually copy that and three uh we also need to think a little bit about how the policies compos when we do these member accesses so problem one is easy uh because case classes in scholar are immutable uh we don't ever have the problem of the data going out of sync essentially the views going out of sync so whenever I am doing propagation I can take the original data that I have I can copy it and place it in the lexical scope of the check function and it will uh it will be able to get used uh whenever it needs to uh we essentially just copy that data bind it to a newly generated symbol and um we're all done problem number two the policy objects all the policy objects needs to exist uh statically when the compilation starts and in order to do this this compiler plugin traverses the ASD for all the method invocations on policy of a it will generate a new policy for the result type of the method call and insert a new object into the syntax tree uh for for that specific type so whenever we're trying to find where to copy our methods uh we will have that object in scope last but not least how do policies compose on member access so if I have a user object and a field a list of interests on it it may be the case that the parent policy is more restrictive than the fields policy it may be more restrictive it may be less restrictive or it is it might be the case that they're completely disjoint so I cannot automatically say that okay I want my parents uh policy automatically applying to Me Maybe I want something more restrictive in order to do this we use uh we take advantage of Scholars implicits we can just Define the scope controller and in essentially the top level of your application wherever your end points are you can Define this uh implicit scope controller and say okay my scope is owner what this means that I am the user who owns this data therefore there should not be any restrictions and if that is the case in my check function I can just use this implicit argument and match against the value of it and say that okay if the owner is requesting this data I don't actually have to run this check function so I'll just return true but if it is someone else then okay I will run the check and um this is since this this just uses the default uh implicit objects in Scala you can uh Define as many values as you want uh for for the scope scope object as it's just a algebraic data type you can just extend that trade and Define your own values so I am going to talk about Vision which is a company that that I founded with these two gentlemen uh a while back uh the customer pitch for this um compan is essentially we say it is the one place for musicians to do everything uh it gives you a bunch of tools for uh writing to recording to touring and we integrated this privacy approach to this code base because it was the largest code base that I have access to where I don't have to go through bunch of letters to get approval uh to to try something out um but the advantage of having all of these in one place is that we have tremendous amount of data to help musicians optimize their their uh project Cycles so if you're booking a tour now your merchandising is on our platform your ticket sales are on our platform and uh your social media stats are on platform so we can say something like okay um people in um Berlin Germany uh spend more money on your music on your products but uh people in col uh spend less so we can essentially optimize the best place for you to book your tour uh so that you will maximize attendance and when we have this much data flowing to music labels we have data flowing to managers we have data flowing to PR agents we need to be very careful about who sees what and we integrated uh this privacy approach into Vision uh so the compiler plug-in has a total of 15 phases and we saw a total of 12. .4% compile time overhead over the raw application what raw application means in this case is that you don't care about anything uh there are no privacy checks no Brute Force if statements you'll just accept paying all the fines uh so you don't do anything and uh 7.8% uh compile time overhead over the boot Force approach uh where you have just a bunch of uh if statements distributed all of your application uh and we saw 39% runtime overhead over the Raw application and just 11% runtime overhead Over The Brute Force application uh which is significantly lower uh than all the information flow control approaches um in terms of how much work the developer needs to do um this code base initially had uh almost 11,000 lines of code uh in order to integrate this fully into this code base we just had to add 180 lines of code and change 251 lines of code and the total change is essentially around 4% which is a very very small amount um I'm going to quickly talk about uh the limitations um there are a few small limitations like you cannot rename your Imports uh this is because of how the uh copying and the policy exploration Works uh this is technically possible I just didn't have time to do it I might eventually uh we don't support uh private Fields when things get cop they automatically get copied as public Fields again this is something that can be easily fixed um there is an interesting question of uh serializability of these containers uh it would be amazing to be able to serialize these and then uh just send them over the wire so whatever Services receiving this data receive them in uh essentially wrapped form uh this gets into the open problem of function sterilization uh how do we sterilize Services how do we uh figure out the correct variables and things like that uh if you were at the Unison talk uh they have a great approach uh for doing this uh it is not very easy to replicate this in Scala yet but uh maybe one day um when the language evolves a little bit more uh I will give it a shot and I'm going to leave you with an interesting question about acing policies so we said that our user data user preferences live outside of our application so we need to go and fetch it every single time so technically if we're being more correct check function doesn't return a bullying it returns some like IO of bullying because it is concurrent we don't know when that data will come back now if I have a method that returns for those familiar with the taglist Final Approach returns F of a and at some point this method the result of this method will get policy protected how do I know what f is more precisely how do I know what I should be protecting do I wrap the entire F in a policy or do I wrap just uh the data that this computation will eventually return because F can be a list and if it's a list uh then I can just wrap the entire thing because I want to protect the entire data but if it's an effect like an IO then what what does it even mean to protect the entire computation uh this is an open question in uh both security and programming languages research uh ret people are looking into it like what what is a contract of a computation uh what what do we even want to restrict on a computation um this is something interesting to think about um in in our compiler plugin we use a sort of hecky method uh like previously knowing what the values of that F can be you know it can be a monic STX it can be a catso it can be a a future uh so we do some we have some hard Cod logic to work around this but in a generalized sense uh this is actually very difficult to do that is all I have um I think I have two or three minutes so if there are any questions I can take them here or I will be upstairs for questions later uh is it open source can we find it on GitHub uh um I can make it it's a private report I can make it public after I uh remove my delightful comments wrestling with the the compiler so just send me an email and I'll uh make it public all right thank you very [Applause] much