Session variables encryption in Play framework

Main motivation for this work was to counter security issues caused by Play implementation of session variables. Session variable is a store that allows a web application to set variables related to a particular user's session. In the traditional Java HttpServlet a session variable can be set using setAttribute() and getAttribute(). These values never leave the web application - they are stored on server side.

Play also offers session variables which may be set using session() (actual session variables) or flash() methods (short lived messages). They are stored in HTTP cookie in the client's browser. This is clearly explained in the documentation and was motivated by stateless architecture of Play apps but nonetheless it's causing security problems:

  • Integrity.Whatever is sent to the user's browser may come back tampered with. Server-side variables don't really have this problem because they never leave the trusted web applicaiton environment. Fortunately, Play provides integrity protection for variables set with session() but not flash() variables.
  • Confidentiality. Whatever is stored in the cookie is sent over HTTP and persisted in the user's browser. While confidentiality in transit can be protected to some extent with TLS and cookie flags such as Secure, it's creates a number of challenges and introduces additional complexity caused by a need to protect something that shouldn't probably ever leave the server.

Play has a built-in Crypto library that offers basic cryptographic functions such as hashing, digital signature and encryption.

The integrity protection for session() uses this library sign() method, which is basically a HMAC over the session data using SHA-1 and constant `application.secret` as secret key, which isperfectly sufficient in most cases. The hex string in the below sample cookie is the HMAC over plaintext `variable` that follows it:

PLAY_SESSION="a71463aa0bddc8edc9e7e694f11ad6f4b2b8aa6a-variable=value"; Path=/; HTTPOnly

If the HMAC doesn't match the data, the cookie is discarded and session() call returns null (this is implemented in Http.scala module).

While the integrity protection is quite robust, most application architects will also face the problem of confidentiality and Play's Crypto library is not really production-ready in this aspect. The encryptAES() function in Play has two major issues:

  • it always uses the same key for encryption (again application.secret), which means the same plaintext will be always encrypted to the same ciphertext;
  • it uses AES in ECB mode, which results in the same blocks of data producing the same ciphertexts.

While the module theoretically allows other encryption modes via a configuration variable, it doesn't have any interface for setting encryption parameters, which makes it impossible to use anything apart from ECB.

StringEnvelope

As a quick fix I have created [StringEnvelope](https://github.com/kravietz/StringEnvelope) class that reimplements the cryptographic and can be easily integrated with any Play application. The interface is simple:

 StringEnvelope env = new StringEnvelope();
 String ciphertext = env.wrap("plaintext", "key");

The `wrap()` methods returns a BASE64 encoded object with integrity and authenticity protection embedded. Example:

 Mt2sTCX+FbKeIxCRLFHY5A==-hSQPO5vOZgJpo3X/OqrqWGulP905BQlA3Ued9xa0LAo=-LJECPots4J/DX+im2b4wWA==

International scripts are fully supported with UTF-8 encoding:

 wrap("комплекс карательных мер", "key")
 hwSh9urBMkH5vw09J22l2A==-AA2Pbyyylqpvl7TBvtG+l98FYYr7EooGpZG6k56A2sM=-IsUMNpIcxVO+XxPpK265NUtZQ1N9U9dMvu77Nj9TF9P9f6Mo6Yn4W8q3iZ9p3uKe
 unwrap(...) = "комплекс карательных мер"

The unwrap() method provides decryption with identity and authenticity validation:

String plaintext = env.unwrap(ciphertext, "key");

On integrity error an exception will be thrown, so `try/catch` should be used. Details on why decryption
failed are returned in the exception, but it's usually because the encrypted block was modified or malformed:
    try {
        String plaintext = env.unwrap(ciphertext, "key");
    catch (IllegalArgumentException e) {
        System.out.println("Decryption failed: " + e);
    }

The class also has a built-in self-test method that returns `true` if the implementation works as expected.
    if (!env.selfTest())
            throw new InternalError("self test failed");

The self-test should be normally called when the class is initialised so that possible buggy or old Java
implementations can be detected before they impair the encryption.

StringEnvelope in Play

Installation of StringEnvelope into Play is also reasy:

  • Place StringEnvelope.jar inside lib directory of your Play application
  • import `org.owasp.StringEnvelope
  • initialize StringEnvelope env = new StringEnvelope()
  • use env.wrap(plaintext, key) to encrypt and env.unwrap(ciphertext, key) to decrypt

For more detailed usage example see app/controllers/Application.java.