Key Takeaways
- Application-layer cryptography is part of a trend to move more infrastructure and IT accountabilities into developer or DevOps roles.
- End-to-end encryption is an increasingly popular type of application-layer cryptography.
- This type of encryption lets organizations enforce access control using key management as well as policy.
- Delivering code securely to end-users is very different depending on the platform; browser-based JavaScript is the most challenging.
- Privacy and security can be greatly improved with these approaches, so despite the challenges, it is well worth it.
Anyone handling sensitive user data lives in fear of a data breach. We know that encryption can reduce the negative consequences, but most encryption is relegated to infrastructure-level elements like TLS and VPNs rather than at the application layer. Application-layer and end-to-end encryption can be a powerful tool in our toolkit, but as developers, how can we safely add encryption to our applications without introducing bugs or reducing the utility of the data?
In this article, we discuss the pros and cons of application-layer encryption. We will cover the attack surface of application-layer encryption in the browser, how it is very different from native clients, and how WebCrypto helps.
The Threat Landscape
The reputation, financial, and human impact of breaches can be extremely high. New laws that help protect end-user privacy are an important step forward, but they come with potentially ruinous fines.
Studies show that encryption is one of the most effective technical security measures to reduce the impact and cost of a data breach. When attackers get encrypted datasets, they either have to attack a different system to get the key or have to settle with metadata and side-channel information instead of “the good stuff.”
Encryption is typically focused on “infrastructure-layer” elements, like TLS, VPNs, database encryption flags, and full-disk encryption. These are important tools in our toolbox, but they rely on assumptions about the infrastructure instead of the application code itself.
In fact, if you consider most recent data breaches, at least among established companies, they were certainly using TLS and at-rest database encryption, and yet the leaks happened anyway. For instance, Capital One was recently hacked and sensitive financial information stolen. Google Photos accidentally gave the wrong users access to photos and videos from other users. These mistakes could have been prevented, or at least mitigated, by application-layer or end-to-end encryption.
As developers, infrastructure isn’t our strength, and sometimes it’s not our job, so encryption takes a back seat to features. But for those of us who do care about defense in-depth, it makes good sense to add encryption to the application itself. Application-layer encryption can insulate our systems from infrastructure-level failures, known weaknesses of TLS, and some server-side vulnerabilities.
Shift-Left Cryptography: What is Application-Layer Encryption?
The practice of moving more security, operations, and testing into the development process (known as shift-left) is improving software agility, reliability, and efficiency. It also means that security best practices need to be implemented as part of application development—not as an afterthought when things go wrong. However, the vast majority of developers are not security or cryptography experts, and at the same time, the security team has less control over the security posture of IT and development than ever before.
Application-layer encryption, or shift-left cryptography, is part of this trend. It means giving developers more control over what gets encrypted and who gets the keys for decryption. In some cases, the users themselves may be the only parties with the keys. In other cases, application-layer encryption can be an added access control layer on data management, providing defense-in-depth.
As implied by the name, application-layer encryption gets added directly to the codebase of your application, and access to key material is controlled by your application logic. As a result, you can think of the data itself as being encrypted throughout its lifecycle, rather than relying on it being on an encrypted network or disk.
The most widely-understood application-layer encryption is end-to-end encrypted chat like Signal and WhatsApp provide, so let’s think through how those applications work. It’s a bit over-simplified, but it basically works like this:
End-user action |
Access Control Logic (Server) |
App-layer Cryptographic Operation (Client) |
Add a friend |
Create an access control rule where users are allowed to send each-other messages |
Trust the friend’s cryptographic key |
Write the friend a message |
Create an access control rule where the friend can read the message |
Encrypt the message with the friend’s key (and sign it) |
Read a message from a friend |
Check for permission to download the message |
Decrypt a message with end user’s key (and check the signature) |
In this simple example, we can already see some of the power of application-layer encryption:
- The user thinks in terms of who they want to talk to, not about access control or encryption, but in fact, the user is making policy decisions.
- The access control logic is enforced with cryptography (and defined by who has the keys), not just rules on a server that an attacker might work around.
- The server doesn’t have access to the plain text data, so attacks against the server, like SQL injection, insider threats, and root-level compromise, can’t reveal the data.
Note that this is an example of end-to-end encryption, but not all application-layer encryption is end-to-end. Also, applications like this still need TLS and other infrastructure-layer encryption to enforce things like authentication, prevent replay attacks, and address a host of other issues.
Why TLS is Not Enough
When we think about TLS, we picture data getting encrypted at its source and decrypted on the server. But this over-simplification hides the practical limits of TLS.
The reality of encryption in transit leaves out encryption of data at rest, which impacts the security of both ends of the transmission. It also completely ignores what happens to the data after HTTPS termination which may be further out on the edge of your network than you know; at your load balancer for instance.
So what about encryption at other points in the application? If you’re doing an above-average job of crypto, you’ve written robust, well-tested code in your app to encrypt data at rest, you’ve used HTTPS and IPSec on your network, and you’ve enabled transparent database crypto.
We’re pretty much “encrypting everywhere” with this approach, but as the data moves through the system, it gets decrypted and re-encrypted at each step. Each point that touches plain text data is a potential vulnerability, resulting in a large attack surface, and you have to ask yourself, “why the heck do these intermediate services need the data in plain text anyway?” They probably don’t.
Infrastructure-layer encryption also lends itself to gaps in security because unanticipated parts of the infrastructure might get the data. For instance, your database and disk backups might not get encrypted, even if your database is. Or your health monitoring system might be logging sensitive data in plain text, and (horror of horrors) maybe even sending it to a third party. These security gaps happen because different individuals or departments are accountable for security at these various points:
- On the mobile side, your development team or vendor had to write some code (or at least implement HTTPS right). Or your Mobile Device Management (MDM) system encapsulates the data, or maybe you’re relying on the user to check the “encrypt phone” box and the OS vendor to do something sensible there.
- In the network, IT or DevOps is accountable for provisioning certificates and ensuring that HTTPS is well configured, which isn’t always that easy.
- On your server, you’re counting on IT and DevOps to secure internal access to your systems, and you’re counting on the cloud provider and database vendor to implement “transparent” database crypto.
Each one of these solutions uses different ciphers, libraries, and key sizes. You’re counting on a lot of people to get a lot of things right. That’s a problem.
Delivering Trustworthy Code
Encryption is about communication; data is written and encrypted by one party, then received and decrypted by another party. The sender and receiver both have to have an application that knows how to do the encryption and decryption, and can be trusted to do it correctly. But that is easier said than done.
What if the encryption code is malicious? What could an attacker do? The simplest attack would be for the application to work exactly as expected, but also send the unencrypted messages to the bad guys. More subtle attacks are possible of course; adding hidden vulnerabilities to weaken the encryption, messing with the public keys, etc. But they all amount to the same thing: A bit of code that helps the bad guy get the secret message.
So let’s talk about code delivery. For two people communicating using apps on their mobile phones, the trust chain goes something like this: A good programmer writes good encryption code, compiles it into an app, signs the app with a digital signature, and uploads it to an app store via TLS. The user downloads an app over TLS, the operating system checks whether the digital signature is “trusted,” and the user runs the app to have their encrypted communication. Note that this protocol is itself an application-layer cryptographic data exchange. Systems like Debian Linux have similar protocols for installing and upgrading the server and desktop applications.
There are a number of things that can go wrong with the trusted app download: The user could download a malicious version of the app. The OS vendor could undermine the check of the digital signature on the app. An attacker could trick the user into installing an old and vulnerable version of the app (or not upgrading it). Any of these types of attacks would make the end-to-end encrypted communication suspect. But for the most part, this works well.
Application-level cryptography is typically implemented in native code running on mobile, laptops, or servers, and can use a protocol like this to deliver trustworthy code. But modern applications very often have a major browser-based component, even for critically sensitive information.
So what about the web?
The code delivery model on the web looks quite different from an app. When users decide that they want to have a secure conversation, they visit a web page. The browser downloads some JavaScript over TLS on-demand. Beyond warning the user about bad TLS connections, that’s the end of the standard protocol for code delivery. It relies completely on TLS. The JavaScript that gets delivered needs to perform the application-layer encryption and to not have any malicious code that just sends the unencrypted text to the bad guys.
Why is this a problem? Let’s say for instance that our security claim is that the data gets encrypted in one browser, decrypted in another browser, and the webserver in between cannot see the data without warning flags and fireworks going off. To undermine this claim, the server simply needs to deliver malicious JavaScript at the application start time. So an attacker that can control the server that delivers code or various aspects of DNS and TLS could pull off this attack without breaking any crypto. The bad code can be sent only to a specific target, making it hard to detect for security researchers.
In fact, with the speed of application updates and continuous integration, similar attacks are possible against mobile apps and desktops. Many modern apps use dynamic code techniques to deliver at least some code to an app in real-time; many desktop apps update their own code at will. This gives attackers the ability to hijack code updates at various points but also gives security teams the ability to patch quickly. That said, the browser-based attacks are a lot better understood.
Some people in the security and cryptography community point to this issue to say that you shouldn’t do browser-based encryption, or if you do, you can’t claim that it’s end-to-end secure. Or at the very least, that it creates a false sense of security. We disagree. There are indeed weaknesses, but as developers, we should be doing it anyway, because simply put, people use the web for security-critical purposes.
Why app-layer encryption in the browser is still good security
Despite the code delivery problem, doing application-layer encryption in the browser significantly improves the overall security of any system. The reason for this is that security isn’t all-or-nothing. Very rarely in modern server infrastructure is a single browser talking only to a single web server that performs every task; modern systems are just more complex than that.
For instance, let’s say your web application uses HTTPS and does browser-based end-to-end encryption, but that it has an SQL injection vulnerability. The nature of this vulnerability is that the attacker tricks the application into tricking the database into dumping out sensitive data (over HTTPS, ironically). But in our example, the data is end-to-end encrypted, so the database only contains encrypted messages. Without application-layer encryption, the bad guy would get something much more sensitive: the plain text messages. Note that with this vulnerability alone, the attacker cannot change the code to inject malicious JavaScript; the browser-based encryption code is still sound.
On the other hand, if the attacker has a remote code execution exploit on the API server, and can modify the JavaScript or inject malicious code into it on the fly, they can undermine the end-to-end encryption, again by simply adding code that sends the plain text data to themselves.
These are only two examples, one where application-layer encryption can be undermined and one where it cannot, but there are innumerable other attacks that can be prevented with end-to-end encryption: Perhaps you have a too-nosey employee who is looking for the private information on celebrities, but who doesn’t have access to the code. Perhaps you backed up your Postgres database to an S3 bucket and accidentally left it open on the web. Perhaps an attacker can undermine TLS, but they only act passively; they can eavesdrop but they cannot do code injection.
As we can see, application-layer encryption in the browser provides defense-in-depth, even though there are challenges to code delivery. In the next section, we will talk about approaches that mitigate those challenges.
Improving trusted code delivery to the browser
There are a number of ways to improve the security of application-layer encryption in the browser. The first line of defense is to use good, trusted code. Modern application development is much faster because we reuse a lot of code we find on the web, but if any of the code that runs in the user’s browser is malicious or vulnerable, it undermines the encryption significantly.
Protecting the server that delivers the code is also vital. Use the principle of least privilege when assigning access control rights on that server. Use multi-party control for administration and code deployment. This will significantly reduce the risk of insider attacks.
There are also under-used code-delivery settings that instruct the browser to take extra precautions. These aren’t the default because they somewhat reduce the flexibility of the development and integration process, but the security they provide is worth the work, whether your application does encryption or not:
- HTTP Strict Transport Security (HSTS): Instructs the browser to always load this page over HTTPS. This prevents downgrade attacks, for instance, if the attacker can redirect your DNS to a malicious service they can’t downgrade the connection to unencrypted HTTP.
- Strict Content Security Policies (CSP): Whitelist safe sources for loading code. In the complex world of modern JavaScript, this prevents the application from dynamically loading code from a remote resource that you don’t know about.
- Subresource Integrity (SRI): Only load scripts that you know you can trust. This uses cryptographic hashes to mark trusted scripts. If an attacker modifies the JavaScript for an encryption application, this will change the hash and the code won’t load.
In addition, there is a relatively new browser API that helps with efficient and secure delivery of cryptographic primitives. The WebCrypto API provides low-level ciphers, hashes, and other encryption components. This helps because you don’t have to include those ciphers in your JavaScript. The browser implements them directly and can take advantage of local native execution and even hardware acceleration. It doesn’t prevent certain attacks, like just sending an unencrypted copy of the data to the bad guys, but WebCrypto does make browser-based encryption more standard and accessible.
Other Pitfalls
Secure code delivery isn’t the only challenge for implementing application-layer encryption. The biggest problem is that most encryption libraries are relatively hard to use securely and difficult to implement consistently in different programming languages and platforms. When you encrypt something in a browser and decrypt it on an app, you probably need three different implementations in different languages (Android, iOS, and JavaScript) that all use the exact same ciphers and modes.
The secure operation of these modes is not very easy to understand. For instance, the well-beloved cipher AES is secure, but pairing it with an insecure mode like ECB (the default mode in Java) is insecure. Pairing AES with GCM is considered a best practice, but even GCM has its flaws; if you encrypt too much data with the same key, or make a mistake with the initialization vector/nonce, you could actually leak key material, which is a flaw that some other modes do not have.
One mistake can make your encrypted data unrecoverable, or even worse, recoverable by a bad guy.
Another challenge is that if you put encrypted data in your database, it’s no longer as searchable. You have to plan ahead for what kinds of queries and downselects you want the database to do or that you want your application to do. If you encrypt a user’s home address, for instance, you can’t simply SELECT * for all the rows with the string “Oregon.” If downselecting by state is part of your application workflow, you can instead encrypt the user’s entire address, but add an unencrypted metadata field with their state so that you can still perform this query. From there, you can potentially use application-layer logic to decrypt the record and perform the rest of the search, but the database won’t be of much help.
People I talk to are often concerned about performance for application-layer encryption, but this isn’t a significant concern. Encryption is fast, and often hardware accelerated these days. After all, we use HTTPS for streaming entire social networks with photos and videos and don’t really notice much of a performance hit. It’s similar at the application layer, and you are simply unlikely to find encryption to be a bottleneck.
To be sure, there are still attacks against application-layer encryption. Various governments have made it illegal or legally impractical to operate an encryption service or install an encrypted app. Users selecting weak or reused passwords can completely undermine encryption. Users forgetting passwords is a challenge to address as well; what should happen in that case? Should the user be able to recover their data via a password reset email? That itself weakens the end-to-end encryption argument.
And of course, once the data is decrypted, attackers can attack the end device itself. This happened to WhatsApp in 2019, causing some to wonder if end-to-end encryption is worthwhile or important. But the fact that attackers had to target specific individuals with zero-day attacks against WhatsApp is proof enough to me that end-to-end encryption helps.
How to be Successful
When implementing encryption in your application, you will need to consider your specific security goals, any compliance rules you might have to follow, and who you need to have the key material. Cryptography is very specific to your application. A trained cryptographer can help you understand the strengths and weaknesses of your approach, and no magazine article can tell you what’s right or wrong. There are, however, a few choices you can make that will get you closer to “good cryptography,” and you can often safely use them.
First a bit of brief background on the three major cryptographic systems—symmetric, asymmetric, and hashing. Symmetric (shared key) is fast and efficient, these algorithms are usually your baseline for encrypting data. AES is usually what you want. Symmetric encryption suffers from challenges with key management. You need a way to get the shared key to both parties, which is why you need asymmetric encryption. Symmetric multi-block modes vary in their confidentiality and integrity properties, and some work better with different types of data or different system constraints (such as a lack of a random number generator): ECB, GCM, CBC, SIV, etc.
Asymmetric (public/private key) cryptography is slower and more complex than symmetric encryption, these algorithms are typically used for exchanging symmetric keys. RSA is the “classic” choice here; ECC is more modern and efficient, and almost as widely supported. Roughly speaking, public keys are used for encrypting data and verifying signatures. Private keys are used for decrypting data and generating signatures.
Hashing, cryptographic signatures, and message authentication codes (MACs) provide integrity. Hashing generates a short string that proves the data was either unchanged or in the case of message authentication codes, proves that the person holding a secret key “signed” the data. Many people think that encryption implies integrity, but it does not. For instance, AES doesn’t provide integrity by default. Algorithms like SHA2, Poly1305, and GCM help.
Managing keys is a very big topic in itself, but a few important things to consider:
- Generation: How random are the keys, what size are they, symmetric vs. asymmetric, etc.
- Storage: Whether to derive keys from a user-generated password or store them for later lookup. If storing, do you have a safe place to put them like a keychain or a Hardware Security Module (HSM)? Many operating systems and platforms now have support for secure key management.
- Communication: How to agree on a key between a client and a server or two users. This is very hard with symmetric keys, but also challenging for public keys. Public keys don’t need to be secret, but you have to trust that they are really from the person you think they’re from. For that, you need to already have something to trust, which can be a chicken-and-egg problem.
Beyond key material, there are other elements of randomness or uniqueness that are associated with encrypted messages. Initialization Vector, salt, and nonces fall in this category. These need to be communicated to the decrypting party as well, so they need to be stored or transmitted. Typically, it’s safe to transmit these unencrypted along with the ciphertext, but you should be careful not to let the attacker modify them.
You also need to pad, encode, serialize, and sign your messages. Believe it or not, even bad padding can undermine the confidentiality of the encrypted message. For signing of structured data like a JSON object or HTTP headers, you need an identical way for both sides to serialize and deserialize the data, or the signatures won’t match.
If you’ve done all of this right, you now have an encrypted and signed message. It’s likely at this point that you’ll want to send this message to another party, who will check the signature and decrypt the message. That means you need to communicate all of your choices: key id, size, cipher, mode, IV, hashing algorithm, etc. This communication itself is a fraught weakness in many cryptography systems. For instance, attackers have been able to trick some symmetric systems into behaving like asymmetric systems and sending their shared key directly to the attacker. Oops.
A few recommendations we have, particularly if you need to or want to stick with the NIST/FIPS-140 ciphers that are sometimes required for compliance in government work or banking:
- Symmetric encryption: AES-GCM is a nice mode of operation because it provides multi-block confidentiality (unlike ECC) and authentication/integrity (unlike CBC). It’s broadly available, so you can usually count on it being there when you need it. You have to be very careful with the GCM nonce, though, because nonce reuse (or if the attacker can choose it) can leak key material. That is no good.
- Authentication: This verifies that the person with the private key encrypted the data. Very important. Our recommendation is the same as above using the tag added by GCM.
- Key Exchange: Elliptic Curve Diffie-Hellman (ECDH) over curve P-384 is a good choice.
- Hashing: SHA256 is pretty standard across the board nowadays.
- Don’t use old/broken stuff: While this is not an exhaustive list, the most commonly used “old or broken” stuff includes: DES, MD4, MD5, SHA1, RC4, AES-ECB, (RSA is old, but not broken. It’s fine to use if that’s what’s available, but prefer ECC if you can.)
- libSodium: If you don’t need NIST/FIPS compliance, you should definitely look into libSodium. It’s very well regarded and the libraries are typically easier to use than the libraries that implement similar FIPS ciphers.
Conclusion
Encryption is an exceptionally effective way to protect data, but most encryption deployed today is part of the IT infrastructure, and not part of applications. As developers, we have a unique opportunity to improve privacy and security of our users by making application-layer encryption a part of our toolbox. There are challenges to be sure; encrypted data can be harder to manage, and most encryption libraries are very hard to use for untrained developers, but the benefit to our users is worth it!
Glossary of Terms
The following are not the formal definition of these terms, but color commentary to help you understand how these terms and technologies fit into application-layer encryption.
- Advanced Encryption Standard (AES): One of the most common symmetric encryption standards.
- Asymmetric encryption: Also known as public-key cryptography. Slower, but more flexible in terms of key management than symmetric encryption. Algorithms include ECC and RSA. Typically used for negotiating symmetric encryption keys.
- Block Cipher Mode: Since symmetric ciphers like AES only handle a fixed number of bits (e.g. 128), secure methods for handling multiple blocks must be used. Incorrect use of such modes is a common vulnerability. Commonly used block modes are GCM, ECB (although it is insecure), CBC, SIV, among many others.
- Domain Name System (DNS): The protocol for identifying servers (typically) based on their name. DNS is a critical piece of security infrastructure since control over DNS can allow an attacker to impersonate a server to an end user.
- FIPS 140 / NIST ciphers: A collection of ciphers vetted by the US federal government for various uses. Some industries require the use of vetted ciphers and implementations. The National Institute of Standards and Technology (NIST) standardizes these.
- Hardware Security Model (HSM): Toolkits for managing encryption keys and operations securely. The hardware layer provides added protection e.g. against privileged exploits.
- Initialization Vector, salt, and nonces: Random numbers used as components within cryptographic algorithms. Depending on the algorithm and mode, each of these can have different security properties and uses.
- Integrity: The security property that a piece of data cannot be changed, or if it has been changed, that the change can be detected, or proof that a message was generated by an authorized party. MACs, and algorithms like SHA2, GCM, and Poly1305 can help provide this property.
- Symmetric encryption: Algorithms like AES, where both parties use the same key for encryption and decryption. Faster than symmetric encryption, but more difficult to manage keys since the key needs to be exchanged on a secure channel.
- Transport Layer Security (TLS): The widespread standard for encryption of communications “in transit”. Sometimes referred to as HTTPS, but also applicable to other protocols like VPNs.
- Virtual Private Network (VPN): Encryption of network-based communications. It secures more aspects of user behavior than just HTTPS, and often uses similar technology.
About the Author
Isaac Potoczny-Jones is the founder and CEO of Tozny, LLC, a privacy and security company specializing in identity management and encryption. Isaac’s work in cybersecurity spans open source, the public sector, and commercial companies. His projects have included end-to-end encryption for privacy in human subject research, secure cross-domain collaboration, identity management, anonymous authorization, mobile password-free authentication, anti-forgery in hardware devices, and privacy-preserving authentication. He has worked with agencies including DARPA, the Navy, Air Force Research Laboratory, the Department of Homeland Security, the National Institute of Standards and Technologies, and other elements of the DoD and intelligence communities. Isaac is an active open source developer in the areas of cryptography and programming languages. Education: B.S. in computer science, M.S. in Cybersecurity.