Publishing "private" blog posts with Emacs and org-mode

A coworker of mine had a really clever idea for sending out invitations: Encrypt the invitation, upload the invitation on some website that allows hosting static content (e.g., github pages) and let the visitors enter the decryption key.

This blog post is really just about that, but automated a bit using a bit of python.

How it works

Conceptually, the idea is very simple:

  • Use org-mode's publishing functionality to convert a page from .org to .html.
  • Use a small python script to encrypt and move the .html file.

When a visitor goes to the site, use a small piece of Javascript to decrypt the content and overwrite the page so that the original HTML gets displaced.

But less see how it works a bit more in detail.

From org to HTML

The first step simply involves publishing the org file as one normally would. The only difference is that we will publish the file into a temporary directory, instead of directly into the repository that goes online.

For example, if we place the org files that we wish to publish encrypted in a directory hidden then we could place the published HTML files in another directory hidden_temp, or something.

Encrypting the HTML file

The next step is to encrypt the HTML file. While the encryption itself is done using a small python script, calling it can again be done through emacs as part of org-mode's publishing functionality.

To this end, we will create a new component which uses a custom publishing function that simply calls the python program. The component I use looks like this

("B-secret-publish"
	 :base-directory ,(blog::make-directory "hidden_temp")
	 :publishing-directory ,(blog::make-publishing-directory "hidden")
	 :base-extension "html"
	 :recursive t
	 :secrets-file ,(blog::make-directory "secrets.txt")
	 :publishing-function blog::publish-encrypted)

The blog::publish-encrypted function is quite simple. All it does is call a python script with arguments received from the component above, namely the filename publishing directory and the secrets-file file, which is where the encryption key gets stored.

(defun blog::publish-encrypted (plist filename pub-dir)
  (let ((output-filename (concat pub-dir (file-name-nondirectory filename)))
	(secrets-filename (plist-get plist :secrets-file))
	(encrypt-py (blog::make-directory "scripts" "encrypt.py")))
    (message (format "Encrypting %s %s %s" filename output-filename secrets-filename))
    (start-process "encrypt-blog-post" nil encrypt-py filename output-filename secrets-filename)))

Finally, the python script encrypt.py takes care of the actual encryption.

#!/usr/bin/python

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import os
from sys import argv
from base64 import b64encode

if len(argv) < 4:
    print(f"usage: {argv[0]} [input.html] [output.html] [secrets.txt]")
    exit(1)

key = os.urandom(32)
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CTR(iv))
encryptor = cipher.encryptor()

input_filename = argv[1]
output_filename = argv[2]
secrets_txt = argv[3]

ct = b''

with open(input_filename, 'rb') as f:
    for line in f:
	ct += encryptor.update(line)
    ct += encryptor.finalize()

ctxt = b64encode(ct)

with open(output_filename, 'wb') as f:
    f.write(b'<!DOCTYPE html><html lang="en"><head></head><body>\n')
    f.write(b'<div id="c">')
    f.write(ctxt)
    f.write(b'</div>')
    f.write(b'<script src="/res/js/decrypt.js"></script></body></html>')

with open(secrets_txt, 'ab') as f:
    f.write(output_filename.encode())
    f.write(b', ')
    f.write(b64encode(key + iv))
    f.write(b'\n')

The script simple encrypts the whole file using AES-CBC, and outputs a new HTML page that (1) contains the original encrypted file, and (2) includes the script that will be used to decrypt the file.

The last thing the script does, is write the key and IV to a file so that it's actually possible to see the page.

Decrypting and displaying the page

The last step is to decrypt and display the page—that is, reverting the steps above, but at the visitors end.

This is done through a very simple piece of javascript (the decrypt.js included in the file generated by the python code above). This script does three things:

  1. First it reads the key and IV from the URL fragment.
  2. Then it decrypts the base64 encoded string in the div with tag "c".
  3. Finally it overwrites the whole page with the result of the decryption.
(function() {
    let k_iv = Uint8Array.from(atob(window.location.hash.substring(1)), c => c.charCodeAt(0))

    let key = k_iv.slice(0, 32)
    let iv = k_iv.slice(32, 48)

    let ctxt = Uint8Array.from(atob(document.getElementById('c').innerHTML), c => c.charCodeAt(0))

    window.crypto.subtle.importKey(
	"raw",
	key,
	{
	    name: "AES-CTR"
	},
	false,
	["decrypt"]
    ).then(function (k) {
	window.crypto.subtle.decrypt(
	    {
		name: "AES-CTR",
		counter: iv,
		length: 128
	    },
	    k,
	    ctxt
	).then(function(decrypted) {
	    let td = new TextDecoder()
	    let actual_page_content = td.decode(decrypted)
	    document.open()
	    document.write(actual_page_content)
	    document.close()
	}).catch(function(err){
	    console.log(err)
	})
    }).catch(function(err) {
	console.log(err)
    });
})();

Limitations

This whole thing does not do anything else besides encrypt a static HTML page, which in particular means that it wont encrypt any content that is included from external sources. So don't expect it to hide e.g., the content of an img tag.1

The encrypted page is also not authenticated. But that really shouldn't be an issue, since it will be sent on authenticated channel anyway (github uses TLS, after all).

Concluding

Anyway. This is pretty neat, I think. I'm not sure what I'll use it for, but now it exists and it seems to work.

A example page can be seen here.

The key is: +nR3AKqEZqlqakavJyoCaj7ZFRdRAh4ZccNZsDCIL/q+AWop9CssObbgiZrGT5oU.

Footnotes:

1

Although one can include the image inline as a base64 encoded string, which would then result in the image getting encrypted.


CC BY-SA

Published: 2024-01-01

Last modified: 2024-08-01 Thu 15:13