diff --git a/configure.ac b/configure.ac
index 86522b3..54f8743 100644
--- a/configure.ac
+++ b/configure.ac
@@ -208,7 +208,7 @@ AC_CHECK_FUNCS([ceil floor round])
 
 # Search for the required modules.
 
-VTE_PKGS="glib-2.0 >= $GLIB_REQUIRED gobject-2.0 pango >= $PANGO_REQUIRED gtk+-$GTK_API_VERSION >= $GTK_REQUIRED gobject-2.0 gio-2.0 gio-unix-2.0 zlib"
+VTE_PKGS="glib-2.0 >= $GLIB_REQUIRED gobject-2.0 pango >= $PANGO_REQUIRED gtk+-$GTK_API_VERSION >= $GTK_REQUIRED gobject-2.0 gio-2.0 gio-unix-2.0 zlib gnutls >= 3.2.0"
 PKG_CHECK_MODULES([VTE],[$VTE_PKGS])
 AC_SUBST([VTE_PKGS])
 
diff --git a/src/vtestream-file.h b/src/vtestream-file.h
index 78d3cd0..474c63c 100644
--- a/src/vtestream-file.h
+++ b/src/vtestream-file.h
@@ -47,24 +47,25 @@
  *   the tail is kinda like a snake, and the mapping to file offsets reminds
  *   me of the well-known game on old mobile phones.
  *
- * o The middle layer is called VteBoa. It does compression, and is planned to
- *   do encryption along with integrity check. It has (almost) the same API as
- *   the snake, but the blocksize is a bit smaller to leave room for the
- *   required overhead.
+ * o The middle layer is called VteBoa. It does compression and encryption
+ *   along with integrity check. It has (almost) the same API as the snake,
+ *   but the blocksize is a bit smaller to leave room for the required
+ *   overhead.
  *
  *   The name was chosen because the world of encryption is full of three
- *   letter abbreviations. At this moment we're planning use GNU TLS's method
- *   for doing AES GCM. Also, because grown-ups might think it's a hat, when
- *   actually it's a boa constrictor digesting an elephant :)
+ *   letter abbreviations. At this moment we use GNU TLS's method for doing
+ *   AES GCM. Also, because grown-ups might think it's a hat, when actually
+ *   it's a boa constrictor digesting an elephant :)
  *
  * o The top layer is VteFileStream. Its basic goal is to do buffering and
  *   caching. As opposed to the previous layers, this one provides methods on
  *   arbitrary amount of data. It doesn't offer random-access-writes, instead,
  *   it offers appending data, and truncating the head (undoing the latest
  *   appends). Write requests are batched up until there's a complete block to
- *   be compressed and written to disk. Read requests are answered by reading
- *   and uncompressing possibly more underlying blocks, and sped up by caching
- *   the most recently read and uncompressed block.
+ *   be compressed, encrypted and written to disk. Read requests are answered
+ *   by reading, decrypting and uncompressing possibly more underlying blocks,
+ *   and sped up by caching the most recently read and decrypted+uncompressed
+ *   block.
  *
  * Design discussions: https://bugzilla.gnome.org/show_bug.cgi?id=738601
  */
@@ -75,19 +76,27 @@
 #include <string.h>
 #include <unistd.h>
 #include <zlib.h>
+#include <gnutls/gnutls.h>
+#include <gnutls/crypto.h>
 
 #include "vteutils.h"
 
 #define ALIGN_FLOOR(x, y) ((x) / (y) * (y))
 #define ALIGN_CEIL(x, y) (((x) + (y) - 1) / (y) * (y))
 
+#define VTE_CIPHER_ALGORITHM  GNUTLS_CIPHER_AES_256_GCM
+#define VTE_CIPHER_KEY_SIZE   32
+#define VTE_CIPHER_BLOCK_SIZE 16
+#define VTE_CIPHER_IV_SIZE    12
+#define VTE_CIPHER_TAG_SIZE   16
+
 #ifndef VTESTREAM_MAIN
-#define VTE_SNAKE_BLOCKSIZE 65536
-/* Subtract compression overhead */
-#define VTE_BOA_BLOCKSIZE   (VTE_SNAKE_BLOCKSIZE - 4)
+#define VTE_SNAKE_BLOCKSIZE    65536
+/* Subtract encryption overhead, round down to a multiple of encryption blocksize, subtract compression overhead */
+#define VTE_BOA_BLOCKSIZE      (ALIGN_FLOOR(VTE_SNAKE_BLOCKSIZE - 4 - VTE_CIPHER_TAG_SIZE, VTE_CIPHER_BLOCK_SIZE) - 4)
 #else
-#define VTE_SNAKE_BLOCKSIZE 4  /* Small block size for unit testing */
-#define VTE_BOA_BLOCKSIZE   3
+#define VTE_SNAKE_BLOCKSIZE    4  /* Small block size for unit testing */
+#define VTE_BOA_BLOCKSIZE      3
 #endif
 
 #define OFFSET_BOA_TO_SNAKE(x) ((x) / VTE_BOA_BLOCKSIZE * VTE_SNAKE_BLOCKSIZE)
@@ -549,17 +558,59 @@ _vte_snake_class_init (VteSnakeClass *klass)
 /******************************************************************************************/
 
 /*
- * VteBoa: Compress the data
+ * VteBoa: Compress and encrypt an elephant to make it look like a hat.
+ *
+ *                 _.----._
+ *                /         \._______
+ *               /                    \.
+ *              |                       \
+ *              |                        \
+ *             /                          \
+ *   ________/                             \_________________
+ *  '------------------------------------------------------'-'
+ *
+ * A boa block is converted to a snake block as follows:
+ *
+ *              snake block
+ * ├────────────────────────────────────┤
+ *
+ *   enc                            enc
+ * 0 len 4     encrypted data     E tag T
+ * └─────┼─────┬────────────┬─────┼─────┘
+ *       4 com 8 compressed C pad E
+ *         len      data
+ *
+ * ┌ ─ ─ ─ ─ ─ ┘            └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
+ *
+ * ├────────────────────────────────────────────────────────┤
+ *                          boa block
  *
- * The data is stored as 4 bytes denoting the length of the compressed block,
- * followed by the compressed data. The data is stored uncompressed if compression
- * didn't result in a smaller size.
+ * Encryption layer:
+ * - 0..4: The value of T-4 (4 bytes)
+ * - 4..E: The encrypted data (length is multiple of VTE_CIPHER_BLOCK_SIZE)
+ * - E..T: Encryption verification tag (VTE_CIPHER_TAG_SIZE bytes)
+ *
+ * Compression layer:
+ * - 4..8: The value of C-8 (4 bytes)
+ * - 8..C: The compressed (or uncompressed if uncompressable) data
+ * - C..E: Padding to make E-4 a multiple of VTE_CIPHER_BLOCK_SIZE
  */
 
 typedef struct _VteBoa {
         VteSnake parent;
         gsize tail, head;
 
+        gnutls_cipher_hd_t cipher_hd;
+        /* The IV (nonce) consists of the offset within the stream, and a reset counter so that
+         * we don't reuse the same IVs when the offset is reset back to 0.
+         * The padding is there to make sure the structure is at least VTE_CIPHER_IV_SIZE bytes large.
+         * Assertion is made later that the real data fits in its first VTE_CIPHER_IV_SIZE bytes.
+         */
+        struct _VteIv {
+                gsize offset;
+                guint32 reset_counter;
+                unsigned char padding[VTE_CIPHER_IV_SIZE];
+        } iv;
         char *buf;
         uLong compressBound;
 } VteBoa;
@@ -584,8 +635,35 @@ G_DEFINE_TYPE (VteBoa, _vte_boa, VTE_TYPE_SNAKE)
 static void
 _vte_boa_init (VteBoa *boa)
 {
+        unsigned char key[VTE_CIPHER_KEY_SIZE];
+        gnutls_datum_t datum_key;
+
         boa->compressBound = compressBound(VTE_BOA_BLOCKSIZE);
-        boa->buf = g_malloc(MAX(VTE_SNAKE_BLOCKSIZE, 4 + boa->compressBound));
+        boa->buf = g_malloc(MAX(VTE_SNAKE_BLOCKSIZE, 8 + boa->compressBound));
+
+        gnutls_global_init ();
+
+        /* Assert that VTE_CIPHER_* constants are defined correctly. Should happen compile-time, nevermind. */
+        g_assert_cmpuint (gnutls_cipher_get_block_size(VTE_CIPHER_ALGORITHM), ==, VTE_CIPHER_BLOCK_SIZE);
+        g_assert_cmpuint (gnutls_cipher_get_iv_size(VTE_CIPHER_ALGORITHM), ==, VTE_CIPHER_IV_SIZE);
+        g_assert_cmpuint (gnutls_cipher_get_tag_size(VTE_CIPHER_ALGORITHM), ==, VTE_CIPHER_TAG_SIZE);
+
+        /* Assert that IV does indeed include all the data we want to use (offset and reset_counter). */
+        g_assert_cmpuint (offsetof(struct _VteIv, padding), <=, VTE_CIPHER_IV_SIZE);
+
+        /* Strong random for the key. */
+        gnutls_rnd(GNUTLS_RND_KEY, key, VTE_CIPHER_KEY_SIZE);
+
+        datum_key.data = key;
+        datum_key.size = VTE_CIPHER_KEY_SIZE;
+        gnutls_cipher_init(&boa->cipher_hd, VTE_CIPHER_ALGORITHM, &datum_key, NULL);
+        /* FIXME: 738601#c52 the compiler might optimize this away, how to make sure it's erased?
+         * It's on the stack so maybe we can rest assured it'll be overwritten pretty soon. */
+        memset(key, 0, VTE_CIPHER_KEY_SIZE);
+
+        /* Weak random for IV's reset_counter, zeros for the rest. */
+        memset(&boa->iv, 0, sizeof(boa->iv));
+        gnutls_rnd(GNUTLS_RND_NONCE, &boa->iv.reset_counter, sizeof(boa->iv.reset_counter));
 }
 
 static void
@@ -595,6 +673,11 @@ _vte_boa_finalize (GObject *object)
 
         g_free(boa->buf);
 
+        memset(&boa->iv, 0, sizeof(boa->iv));
+
+        gnutls_cipher_deinit (boa->cipher_hd);
+        gnutls_global_deinit ();
+
         G_OBJECT_CLASS (_vte_boa_parent_class)->finalize(object);
 }
 
@@ -603,6 +686,10 @@ _vte_boa_reset (VteBoa *boa, gsize offset)
 {
         g_assert_cmpuint (offset % VTE_BOA_BLOCKSIZE, ==, 0);
 
+        /* The stream is empty, there's no data to decrypt. Offset is likely set back to 0.
+         * To avoid reusing the same IVs, increment reset_counter. */
+        boa->iv.reset_counter++;
+
         _vte_snake_reset (&boa->parent, OFFSET_BOA_TO_SNAKE(offset));
 
         boa->tail = boa->head = offset;
@@ -616,18 +703,32 @@ static void
 _vte_boa_write (VteBoa *boa, gsize offset, const char *data)
 {
         uLongf destLen = boa->compressBound;
+        uLongf destLen2;
 
         g_assert_cmpuint (offset, >=, boa->tail);
         g_assert_cmpuint (offset, <=, boa->head);
         g_assert_cmpuint (offset % VTE_BOA_BLOCKSIZE, ==, 0);
 
 #ifndef VTESTREAM_MAIN
-        g_assert (compress2 ((Bytef *) (boa->buf + 4), &destLen, (Bytef *) data, VTE_BOA_BLOCKSIZE, 1) == Z_OK);
+        g_assert (compress2 ((Bytef *) (boa->buf + 8), &destLen, (Bytef *) data, VTE_BOA_BLOCKSIZE, 1) == Z_OK);
         if (G_UNLIKELY (destLen >= VTE_BOA_BLOCKSIZE)) {
                 /* Could not compress, store the original. */
-                memcpy (boa->buf + 4, data, VTE_BOA_BLOCKSIZE);
+                memcpy (boa->buf + 8, data, VTE_BOA_BLOCKSIZE);
                 destLen = VTE_BOA_BLOCKSIZE;
         }
+        *((guint32 *) (boa->buf + 4)) = (guint32) destLen;
+        destLen += 4;
+
+        /* Add padding to make it a multiple of VTE_CIPHER_BLOCK_SIZE */
+        destLen2 = ALIGN_CEIL(destLen, VTE_CIPHER_BLOCK_SIZE);
+        memset(boa->buf + 4 + destLen2, 0, destLen2 - destLen);
+        destLen = destLen2;
+
+        boa->iv.offset = offset;
+        gnutls_cipher_set_iv (boa->cipher_hd, &boa->iv, VTE_CIPHER_IV_SIZE);
+        gnutls_cipher_encrypt (boa->cipher_hd, boa->buf + 4, destLen);
+        gnutls_cipher_tag (boa->cipher_hd, boa->buf + 4 + destLen, VTE_CIPHER_TAG_SIZE);
+        destLen += VTE_CIPHER_TAG_SIZE;
         *((guint32 *) boa->buf) = (guint32) destLen;
         destLen += 4;
 #else
@@ -636,6 +737,7 @@ _vte_boa_write (VteBoa *boa, gsize offset, const char *data)
         boa->buf[1] = data[1] ^ 0x20;
         boa->buf[2] = data[2] ^ 0x20;
         destLen = 3;
+        (void) destLen2;  /* silence gcc */
 #endif
         _vte_snake_write (&boa->parent, OFFSET_BOA_TO_SNAKE(offset), boa->buf, destLen);
 
@@ -649,7 +751,10 @@ static gboolean
 _vte_boa_read (VteBoa *boa, gsize offset, char *data)
 {
         uLongf destLen = VTE_BOA_BLOCKSIZE;
+        unsigned char tag[VTE_CIPHER_TAG_SIZE];
         guint32 sourceLen;
+        guint8 faulty = 0;
+        unsigned int i, j;
 
         g_assert_cmpuint (offset % VTE_BOA_BLOCKSIZE, ==, 0);
 
@@ -658,19 +763,40 @@ _vte_boa_read (VteBoa *boa, gsize offset, char *data)
 
 #ifndef VTESTREAM_MAIN
         sourceLen = *((guint32 *) boa->buf);
+        g_assert (sourceLen >= VTE_CIPHER_TAG_SIZE);
+        sourceLen -= VTE_CIPHER_TAG_SIZE;
+
+        boa->iv.offset = offset;
+        gnutls_cipher_set_iv (boa->cipher_hd, &boa->iv, VTE_CIPHER_IV_SIZE);
+        gnutls_cipher_decrypt (boa->cipher_hd, boa->buf + 4, sourceLen);
+        gnutls_cipher_tag (boa->cipher_hd, tag, VTE_CIPHER_TAG_SIZE);
+
+        /* Constant time comparison: 738601#c66 */
+        for (i = 0, j = 4 + sourceLen; i < VTE_CIPHER_TAG_SIZE; i++, j++) {
+                faulty |= tag[i] ^ boa->buf[j];
+        }
+        if (faulty) {
+                /*
+                 * Data integrity failure. What to do???
+                 * One possible reason is a previous disk full, leaving a hole in the file.
+                 * As of now, just return a normal failure.
+                 */
+                return FALSE;
+        }
 
+        sourceLen = *((guint32 *) (boa->buf + 4));
         if (G_LIKELY (sourceLen < VTE_BOA_BLOCKSIZE)) {
-                g_assert (uncompress ((Bytef *) data, &destLen, (Bytef *) (boa->buf + 4), sourceLen) == Z_OK);
+                g_assert (uncompress ((Bytef *) data, &destLen, (Bytef *) (boa->buf + 8), sourceLen) == Z_OK);
                 g_assert (destLen == VTE_BOA_BLOCKSIZE);
         } else {
-                memcpy (data, boa->buf + 4, VTE_BOA_BLOCKSIZE);
+                memcpy (data, boa->buf + 8, VTE_BOA_BLOCKSIZE);
         }
 #else
         /* Fake decryption+decompression for unit testing */
         data[0] = boa->buf[0] ^ 0x20;
         data[1] = boa->buf[1] ^ 0x20;
         data[2] = boa->buf[2] ^ 0x20;
-        (void) destLen; (void) sourceLen;  /* silence gcc */
+        (void) tag; (void) faulty; (void) i; (void) j; (void) destLen; (void) sourceLen;  /* silence gcc */
 #endif
         return TRUE;
 }
