Encryption

A Note on Security

While jrnl follows best practices, total security is never possible in the real world. There are a number of ways that people can at least partially compromise your jrnl data. See the Privacy and Security page for more information.

There is one relevant security advisory for encryption-related security issues: GHSA-rhx6-37mm-5q9r.

Encryption Behavior Overview

  • Encrypted journals are now written in the jrnl v3 format.
  • jrnl v1 and v2 encrypted files are read-only formats. They can still be opened and decrypted, but new encrypted writes are always v3.

jrnl v3 Encrypted File Format

jrnl v3 uses a random 16-byte salt per encrypted write, stored in a JSON header after a magic prefix. This avoids the static-salt weakness from v2.

File layout:

Field Size Description
JRNLv3 6 bytes Magic prefix
header_len 2 bytes (uint16 BE) Length of the header field, max 64KB
header header_len bytes Base64-encoded JSON, includes salt (base64url-encoded 16-byte salt)
Fernet token Remaining bytes Ciphertext

Additional header fields may be added in future without changing the format version.

Note

Journals encrypted with v3 before this format was base64-encoded store raw JSON in the header field instead. These are still read correctly; the base64-encoded format is written on the next save.

Encrypting and Decrypting from the CLI

To encrypt a journal, run:

jrnl --encrypt [FILENAME]

You can then enter a new password, and the unencrypted file will replaced with the new encrypted file.

This command also works to change the password for a journal file that is already encrypted. jrnl will prompt you for the current password and then new password.

Conversely, run this to decrypt a journal:

jrnl --decrypt [FILENAME]

replaces the encrypted journal file with a plain text file. You can also specify a filename, e.g., jrnl --decrypt plain_text_copy.txt, to leave the original encrypted file untouched and create a new plain text file next to it.

Note

Changing encrypt in your config file to a different value will not encrypt or decrypt your journal file. It merely says whether or not your journal is encrypted. Hence manually changing this option will most likely result in your journal file being impossible to load. This is why the above commands are necessary.

Storing Passwords in Your Keychain

Nobody can recover or reset your jrnl password. If you lose it, your data will be inaccessible forever.

For this reason, when encrypting a journal, jrnl asks whether you would like to store the password in your system's keychain. An added benefit is that you will not need to enter the password when interacting with the journal file.

If you don't initially store the password in your keychain but decide to do so later---or if you want to store it in one computer's keychain but not in another computer's---you can run jrnl --encrypt on an encrypted journal and use the same password again. This will trigger the keychain storage prompt.

Manual Decryption

The easiest way to decrypt your journal is with jrnl --decrypt, but you could also decrypt your journal manually if needed. To do this, you can use any program that supports the AES algorithm (specifically AES-CBC), and you'll need the following relevant information for decryption:

  • Key: The key used for encryption is the SHA-256 hash of your password.
  • Initialization vector (IV): The IV is stored in the first 16 bytes of your encrypted journal file.
  • The actual text of the journal (everything after the first 16 bytes in the encrypted journal file) is encoded in UTF-8 and padded according to PKCS#7 before being encrypted.

If you'd like an example of what this might look like in script form, please see below for some examples of Python scripts that you could use to manually decrypt your journal.

Note

These are only examples, and are only here to illustrate that your journal files will still be recoverable even if jrnl isn't around anymore. Please use jrnl --decrypt if available.

Example for jrnl v2 files:

#!/usr/bin/env python3
"""
Decrypt a jrnl v2 encrypted journal.

Note: the `cryptography` module must be installed (you can do this with
something like `pip3 install crytography`)
"""

import base64
import getpass
from pathlib import Path

from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

filepath = input("journal file path: ")
password = getpass.getpass("Password: ")

with open(Path(filepath), "rb") as f:
    ciphertext = f.read()

password = password.encode("utf-8")
kdf = PBKDF2HMAC(
    algorithm=hashes.SHA256(),
    length=32,
    salt=b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8",
    iterations=100_000,
    backend=default_backend(),
)

key = base64.urlsafe_b64encode(kdf.derive(password))

print(Fernet(key).decrypt(ciphertext).decode("utf-8"))

Example for jrnl v1 files:

#!/usr/bin/env python3
"""
Decrypt a jrnl v1 encrypted journal.

Note: the `pycrypto` module must be installed (you can do this with something
like `pip3 install pycrypto`)
"""

import argparse
import getpass
import hashlib

from Crypto.Cipher import AES

parser = argparse.ArgumentParser()
parser.add_argument("filepath", help="journal file to decrypt")
args = parser.parse_args()

pwd = getpass.getpass()
key = hashlib.sha256(pwd.encode("utf-8")).digest()

with open(args.filepath, "rb") as f:
    ciphertext = f.read()

crypto = AES.new(key, AES.MODE_CBC, ciphertext[:16])
plain = crypto.decrypt(ciphertext[16:])
plain = plain.strip(plain[-1:])
plain = plain.decode("utf-8")
print(plain)