In Multi-stage docker build of Haskell webapp blog post I briefly mentioned data-files
. They are problematic. A simpler way is to use e.g. file-embed-lzma
or similar functionality to embed data into the final binary.
You can also embed secret data if you first encrypt it. This would reduce the pain when dealing with (large) secrets. I personally favor configuration (of running Docker containers) through environment variables. Injecting extra data into containers is inelegant: that's another way to "configure" running container, when one would be enough.
In this blog post, I'll show that dealing with encrypted data in Haskell is not too complicated. The code is in the same repository as the previous post. This post is based on Tutorial: AES Encryption and Decryption with OpenSSL, but is updated and adapted for Haskell.
To encrypt a plaintext using AES with OpenSSL, the enc
command is used. The following command will prompt you for a password, encrypt a file called plaintext.txt
and Base64 encode the output. The output will be written to encrypted.txt
.
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 -base64 -md sha1 -in plaintext.txt -out encrypted.txt
This will result in a different output each time it is run. This is because a different (random) salt is used. The Salt is written as part of the output, and we will read it back in the next section. I used HaskellCurry
as a password, and placed an encrypted file in the repository.
Note that we use -pbkdf2
flag. It's available since OpenSSL 1.1.1, which is available in Ubuntu 18.04 at the time of writing. Update your systems! We use 100000 iterations.
The choice of SHA1 digest is done because pkcs5_pbkdf2_hmac_sha1
exists directly in HsOpenSSL
. We will use it to derive key and IV from a password in Haskell. Alternatively, you could use -p
flag, so openssl
prints the used Key and IV and provide these to the running service.
To decrypt file on command line, we'll use -d
option:
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 -base64 -md sha1 -d -in encrypted.txt
This command is useful to check "what's there". Next, the Haskell version.
To decrypt the output of an AES encryption (aes-256-cbc) we will use the HsOpenSSL
library. Unlike the command line, each step must be explicitly performed. Luckily, it's a lot nice that using C. There 6 steps:
To embed file we use Template Haskell, embedByteString
from file-embed-lzma
library.
{-# LANGUAGE TemplateHaskell #-}
import Data.ByteString (ByteString)
import FileEmbedLzma (embedByteString)
encrypted :: ByteString
encrypted = $(embedByteString "encrypted.txt")
Decoding Base64 is an one-liner in Haskell. We use decodeLenient
because we are quite sure input is valid.
import Data.ByteString.Base64 (decodeLenient)
encrypted' :: ByteString
encrypted' = decodeLenient encrypted
Note: HsOpenSSL
can also handle Base64, but doesn't seem to provide lenient variant. HsOpenSSL
throws exceptions on errors.
Once we have decoded the cipher, we can read the salt. The Salt is identified by the 8 byte header (Salted__
), followed by the 8 byte salt. We start by ensuring the header exists, and then we extract the following 8 bytes:
extract
:: ByteString -- ^ password
-> ByteString -- ^ encrypted data
-> IO ByteString -- ^ decrypted data
extract password bs0 = do
when (BS.length bs0 < 16) $ fail "Too small input"
let (magic, bs1) = BS.splitAt 8 bs0
(salt, enc) = BS.splitAt 8 bs1
when (magic /= "Salted__") $ fail "No Salted__ header"
...
Probably you have some setup to extract configuration from environment variables. The following is a very simple way, which is enough for us.
We use unix
package, and System.Posix.Env.ByteString.getEnv
to get environment variable as ByteString
directly. The program will run in Docker in Linux: depending on unix
is not a problem.
{-# LANGUAGE OverloadedStrings #-}
import System.Posix.Env.ByteString (getEnv)
import OpenSSL (withOpenSSL)
main :: IO ()
main = withOpenSSL $ do
password <- getEnv "PASSWORD" >>= maybe (fail "PASSWORD not set") return
...
We also initialize the OpenSSL library using withOpenSSL
.
Once we have extracted the salt, we can use the salt and password to generate the Key and Initialization Vector (IV). To determine the Key and IV from the password we use the pkcs5_pbkdf2_hmac_sha1
function. PBKDF2 (Password-Based Key Derivation Function 2) is a key derivation function. We (as openssl
) derive both key and IV simultaneously:
import OpenSSL.EVP.Digest (pkcs5_pbkdf2_hmac_sha1)
iters :: Int
iters = 100000
...
let (key, iv) = BS.splitAt 32
$ pkcs5_pbkdf2_hmac_sha1 password salt iters 48
...
With the Key and IV computed, and the ciphertext decoded from Base64, we are now ready to decrypt the message.
import OpenSSL.EVP.Cipher (getCipherByName, CryptoMode(Decrypt), cipherBS)
...
cipher <- getCipherByName "aes-256-cbc"
>>= maybe (fail "no cipher") return
plain <- cipherBS cipher key iv Decrypt enc
...
In this post we embedded an encrypted file into Haskell application, which is then decrypted at run time. The complete copy of the code is at same repository, and changes done for this post are visible in a pull request.