Imagine a regular non Offline First app: a user enters data, which is sent to a server, which validates it before putting it in the database. The setup would look something like this:
user enters data → send to server → server validates → server adds to database
A naïve Offline First app built with PouchDB or similar might have a setup where a user enters data, the app validates the data and then adds it to the local database which is synced to the remote database, e.g.:
user enters data → app validates → app adds to local database → sync local database to remote database
The reason this is naïve is because you cannot trust the client. Anybody can add things to that local database and if they do it will be synced to the remote database where it could wreak any amount of havoc.
The solution is to treat data on the client as untrusted and to add a validation step to the replication to ignore or reject any document that doesn’t conform to the schema or does something it’s not supposed to.
Adding validation to the sync process adds an additional complication: the client side state can diverge from the server held state; this could theoretically be used for nefarious means to screw up a local install in a way that won’t sync in order to trick somebody to do something.
Sometimes you need to log into an app from your parents’ house, on a sketchy computer at the library, or at work, and you neither want nor need the data saved locally. We’re not going to cover the likely usability issue of attempting to sync the whole database when a user quickly needs a specific piece of information; instead we’ll focus on breadcrumbs left behind.
No matter how you handle logging out (see next), if you store stuff in IndexedDB you have to write your app like the databases will still be there when the user leaves and that is going to leak anywhere from just the username to all of the data depending on the setup.
Just have the user use incognito mode might be your first reaction. But there are 2 problems with that: First, most users won’t ever notice how your bank’s website explicitly makes you choose public or private with no default. That’s because most users will probably 90% of the time want it to be saved, but if you just had a checkbox or similar most users would check the box on autopilot the other 10% of the time.
The other issue with incognito mode is Safari. Instead of having all storage APIs (e.g. local storage, IndexedDB) be empty when opening incognito mode and cleared when closing incognito mode, Safari takes the jort sort approach and removes all storage APIs when in incognito mode, breaking your app instead of making it ephemeral.
It is probably a good idea to have an online first version of your app which doesn’t save any data locally especially if your app has a lot of data or sensitive data. You could do this in PouchDB easily by just swapping in a memory back end or directly using the remote DB with the HTTP back end.
Put another way, the workflow in Offline First apps where you haven’t made an account is usually to make it local only, and then if they want to make an account, sync it to the remote server. I’d suggest that when you sign into an account on a computer where you haven’t done anything with the app yet it by default be remote only until the user explicitly syncs it locally.
Whenever I’m using our laptop and my wife asks for it I always hit logout on the laptop before giving it to her. This isn’t because I’m afraid of her seeing anything, but to prevent another incident like when Taylor Swift showed up in my Rdio favorites.
For the rest of us who either have more paranoid significant others, a computer that only has one account, or actual things they need to hide, your app needs some way to log out and allow somebody else to log in, preferably being able to do this while offline (if both users have already used the app). But first a digression…
There is no such thing as perfect or ‘the best’ when it comes to security. There are always trade-offs. For instance, using one time pads to encrypt messages has been mathematically proven to be unbreakable assuming the pad is kept secure and only used once. The drawbacks, such as the pad having to be as long or longer then the message and the requirements that the pad be delivered via secure channels mean that in practice if you have those secure channels, you can just use those for your message and otherwise use a crypt scheme, which instead of being unbreakable ever, is merely unbreakable before the heat death of the universe.
A more concrete example would be cumbersome password requirements (e.g. password must be changed every 2 weeks or password must contain an uppercase and lowercase letter, a symbol, a real number, a Unicode rune such as ᛃ or ᚸ, and a picture of a marsupial or monotreme); while these policies sometimes lead to more complex passwords, they also lead to passwords which are harder to remember, making people more likely to write them down on post-it notes stuck to their monitor. To put it another way, it doesn’t matter how strong your locks are if somebody props the door open because constantly unlocking it was too time consuming.
Why am I bringing this up? To explain why I’m about to disappoint anyone who was looking to be told a simple rule they could apply blindly without thinking. Instead we’re going to come up with a set of different strategies to fit apps ranging from ‘Sync Pictures of Shiba Inus’ to ‘Grindr for Saudi Arabian Atheists’. Some things to bear in mind when we discuss them:
Having an app without security features is only a problem if people use it like it does have security features. Do you think all men on Ashley Madison would have used their real names if they had known the level of security on that site? In other words, if your app stores stuff in plain text in local storage, tell the user this, it may be ok (the user likely stores documents in plane text on their hard drive anyway), but if it’s not, they now know they need to rely on the operating system’s security measures, not the app’s.
People are fundamentally lazy. This is not a judgment against people, it’s just that there isn’t enough time to do everything you want to do, so people either cut corners or get nothing done. Ergo people tend to value their time rather highly so time-consuming security measures should only be required if they are actually necessary.
Your app is not special, so don’t expect people to treat it like it is. Your app is not Facebook, or Instagram, or Snapchat, or whatever tweens are using these days, so don’t expect the stunning popularity of your app to cause people to jump through hoops. Your app is probably not even Lastpass or Keybase so don’t expect people to give up features or convenience for security features they don’t really need.
Just delete the database
I’d like to contrast this from actually logging out of the app. Logging out of an app implies you should be able to log back into it. Your app isn’t much of an Offline First app if logging out of it when offline prevents you from using it again until you get the internet again.
While this is better then nothing (i.e. having to go into the bowels of your browser’s storage preferences to delete the data), it’s not optimal. Besides the aforementioned fact that it really wouldn’t be an Offline First app, there is the fact that this would make logging off asynchronous, i.e. you could click logout and then close your browser fast enough to prevent it from finishing. This is opposed to just removing a key from local storage which is synchronous and should have less of a problem with this.
No password, Database name based on username
In the least secure way to do it, you base the database name on the username (i.e. appname_username or something) and then save the username in local storage. To sign them out you’d just delete the username from local storage. If you do something like this, you probably want to have a list of users with data and allow people to switch between them, the idea being that if people are not comfortable with users doing that from the UI, then this app doesn’t have enough security for them.
With a setup like this you’d only need to require a password when you sync the data (to get a token or cookie for use with auth) and you’d want to delete the token or cookie when the person signs out.
Encrypted local database, key not saved
The idea here is that you use something like crypto-pouch to encrypt the database locally with a key derived from the user’s password, and you don’t save the key (and never ever save the user’s password). The advantage here is that the safety is pretty intuitive: Is the tab open and signed in? Then they have access (you’d also probably want to do an inactive timeout). The big downside here is that it requires the user to constantly reenter their password which means they are likely to either choose a weak password, save the password somewhere else, or stop using your app.
This method is a legitimate choice for certain types of apps that require high security like Lastpass and Keybase (Lastpass I use and can attest to this; for Keybase I’ve been told this as I’m still on the wait list like a pleb). Unless your app involves cryptographic or other sensitive data (e.g. the Saudi Grindr example) this is probably a bad idea for your app as it’s probably going to turn people off or have them use bad security measures that defeat the purpose.
Encrypt local database, key is saved
The idea here is that the user enters a password, it’s strengthened into a key used to decrypt the database, and that derived key is stored in local storage until the user logs off. With a properly strengthened password, this should be approximately as secure as a normal app (it does allow for offline password cracking attempts, though that is only a threat if a user picks a very easy password or the app author does not have a difficult enough work factor for strengthening the password, as finding good documentation on what appropriate work factors are is hard; this is something to bear in mind). Which means that intuitively this is more or less the same security level as standard ways of logging in: if you’re logged in, people can mess with your stuff; if you’re logged out, they can’t.
This is not supported by crypto-pouch yet, but I’m working on a pull.
For apps that require high security, not only can the data can be locally encrypted but also encrypted on the server leaving you, the app developer, with no way to decrypt the data. This has advantages for the user — they know you the developer can’t snoop on their sensitive business documents — and has advantages for you the developer — your liability is limited should you be hacked as all the hacker will get is the encrypted data, not plain text data (see the blase reaction when Lastpass, a company that practices very good security, got hacked).
That being said, there are some pretty steep challenges, the biggest being that it is impossible to do proper end-to-end encryption and have normal password resets. This is a deal breaker in most circumstances and should be; for most apps I personally use, me forgetting the password is a far greater threat than my data being viewed by an employee of the developer or my data getting leaked. In many apps the threat is of impersonation, not leaking of data (e.g. Twitter and Facebook); the exception to this is apps primarily dedicated to storage (e.g. Lastpass or Dropbox), but even then the competing threats of forgetting your password and data being leaked need to be balanced.
The next challenge is double edged, it means that the app developer can’t read your data. But reading your data is one way developers improve apps, to see if people are using features or not and seeing if people are using the feature as intended. With big data you get things like the way google mines your Gmail account to remind you about upcoming flights, which on one hand is creepy but on the other is damn convenient. There is no right answer here, and this particular question isn’t restricted to end to end encryption. Julian Simioni from Mapzen talked in a different session about the struggle they have in developing their geocoder and balancing not spying on their users with using machine learning to improve the geocoder. (The quick version of geocoders is just that 90% of addresses are easy but the last 10% are all weird things like addresses in Queens and Japan, the various points of interest that often have many different names, and the creative ways people misspell things).
The last issue to think about is supporting users from shared computers with online-first version of the app; if your app has a lot of data then decryption client side may be slow. Plus when on a hostile computer (aka one at the library which might have some malware) having a key of some sort in memory the entire time the app is open is much more vulnerable then a password which is only remembered locally briefly when getting a token.
Currently crypto-pouch doesn’t support end-to-end encryption as it saves the salt for deriving the encryption key locally and doesn’t support accessing the documents sometimes encrypted (for syncing) but sometimes decrypted (for using the app), but there is nothing preventing it from doing so in the future.
Two-factor authentication can be used with Offline First apps, but not necessarily how you’d think. Two-factor auth can’t be used for offline apps when actually offline, but they can be used to authorize synchronization. The fact that two-factor auth can’t be used offline is a bit unintuitive to some people, but here goes…
A password can be used to prove that you are who you say you are; this is how a password is used when you log into Facebook and how an Offline First app uses your password when it syncs to the server. A password may also be used to derive the encryption key of a file. While both processes use key derivation algorithms on keys and do something with the result, only the local decryption version actually needs the result to proceed. When logging into Facebook, the process that uses the password in the end just returns true or false about whether it was valid. This is because the server side process is trusted so we can believe it if it says this user is okay.
There are a couple different methods of two-factor authentication, but the most common is the kind that gives you a short code that changes every minute or two. The way this works is that you have an algorithm that takes an initial seed and the time rounded to some unit and outputs a code. The client can send in the code it calculated and the server can check it against the one it calculated but some adversary can’t save it and resend it later when it tries to do something nefarious.
You can’t use two-factor authentication methods like this to decrypt a file because these methods are built around providing a way to show a server you are who you say you are, which doesn’t help you when you are trying to decrypt data which is stored somewhere an untrusted person might get it. What you can use it for is to authorize people who want to sync user data, which would prevent adversaries who steal (or guess) your password but don’t have access to the encrypted data from getting your data, and preventing those same people from modifying or deleting your data.
In theory somebody could develop (or has developed) a smart card which performs some decryption and encryption on the card and a spec which integrates it with a web app, but (as far as I know) that hasn’t happened (yet).
You could use some sort of second factor to help derive the local key from the password, but from the app’s perspective the resulting key isn’t all that different from a password, so you could do that without the app having to opt in with a password manager (have I mentioned I like Lastpass?).
Writing secure apps is hard in general and even harder in the browser, as documented by others. Additionally, the cryptographic options available for use in browsers is somewhat uneven; there is a native API, but its support is uneven. You have some older, more mature libraries that are super insecure due to not relying on some of the native crypto APIs that have good cross-browser support like the browser function to get random bytes via crypto.getRandomValues (see forge and SJCL). These libraries have a tendency to not incorporate widely used high performance buffer options in favor of home grown implementation (see forge and SJCL). The other options are less well tested libraries with better APIs, but I’m not sure which I’d trust if I was a gay Saudi Arabian. Probably none. I’d just write a shiba inu picture app with one and try to hack it until I was sure.
Editor’s Note: This article is part of series of unconference session recaps submitted by the awesome folks who participated in our first ever Offline Camp. You can find more coverage of our initial discussions in our Medium publication or sign up for Offline Camp California (November 4–7, 2016) to continue the conversation.