How To Create An Encrypted Digital Journal

Published on
Authors

Keeping a digital journal offers several advantages over a physical one:

  • Editing: You can easily erase and modify text, or insert new sections
  • Usability and Statistics:
    • You have timestamps for creation, modification, and access
    • You can have detailed statistics such as file, line, word, and character counts.
    • You can easily search for specific entries with words or names
  • Privacy: You can make it more secure than a physical journal

Getting Started

This guide is made for developers, so some of the language will be foreign if you are not one. It is also not a step-by-step guide, but rather a general idea on how I set mine up, and how you can set yours up.

The way this works is by using your file system as the primary container, 7zip for the encryption, and git/github for the cloud backup.

The way you set up your folder doesn't really matter, but personally I have mine set at ~/journal with the structure of ~/journal/[year]/[month]/[day].txt and an additional concepts folder adjacent to the years, for anything that's not a daily journal entry (e.g. "My Review on Invincible").

journal/
   concepts/
      why-[x]-is-[y].txt
      my-review-on-[z].txt
   2022/
      8/
         17.txt
   2021/
         ...

You can write to those files however you feel with a text editor. To encrypt this directory with a password you can use 7z:

7z a -p -sdel -t7z journal.7z ~/journal/

This will compress all the files in ~/journal/ into a single file that's password protected. These settings are the bare minimum, but you might want to add some flags for "ultra settings":

$ 7z a -p -t7z -m0=lzma2 -mx=9 -mfb=64 -md=32m -ms=on -mhe=on diary.7z diary/

To unzip it, you can simply execute: 7z x journal.7z

Scripting

While using those commands are pretty simple, you might have other folders you want to encrypt, or you might just want to type less.

The following script is the one I use. There are some things to note before reading it. I have multiple archives, and store all the compressed 7z files in ~/encoded. The other archives I have are:

  • text-archive.7z: for general text files, things like a list of useful websites, lists of songs from artists I enjoy.
  • pics.7z: general pictures and videos (note: months after writing this I removed this archive as it got tedious to encrypt/decrypt gigabytes of media)

You will need to change some code to match your setup.

Usage: encode <archive_name>

#!/bin/sh

set -e

case "$1" in
    text)
        ENCODED_ID="text-archive"
        DO_NOT_DELETE=1
        ;;
    private|pics)
        ENCODED_ID="$1"
        ;;
    *)
        echo "Unknown argument: \"$1\""
        exit
        ;;
esac

if [ ! -d "$HOME/$ENCODED_ID" ]; then
    echo "folder does not exist"
    exit
fi

FILE_PATH="$HOME/encoded/${ENCODED_ID}.7z"

7z t $FILE_PATH
rm $FILE_PATH

if [ "$DO_NOT_DELETE" = "1" ]; then
    7z a -p -t7z -m0=lzma2 -mx=9 -mfb=64 -md=32m -ms=on -mhe=on $FILE_PATH $ENCODED_ID
else
    7z a -p -t7z -m0=lzma2 -mx=9 -mfb=64 -md=32m -ms=on -mhe=on -sdel $FILE_PATH $ENCODED_ID
fi

It works by

  1. taking the argument provided, and matching it to an archive name
  2. checking if the folder for that archive exists (private => ~/private, text => ~/text-archive)
  3. running a test on the archive with 7z t $FILE_PATH. this way if you have your password in your clipboard you can test out it's the correct one before setting the new archive to a false password
  4. deletes the old archive file, and runs 7z a ... $FILE_PATH $ENCODED_ID
  5. deletes the folder unless it's marked as "do not delete" (DO_NOT_DELETE=1)

If you know 7z, you might ask why I don't use the u action rather than deleting the old archive and creating a new one. That is because the u action simply adds new/modified files to the archive. If a file has been deleted or renamed, the old file will not show up in the new archive.

You might also notice how in the switch statement, I map text to text-archive, this is because, the actual archive name is 'text-archive', but I don't want to type that out, so I just use 'text'.

As for the decode command (also decode <archive_name>):

#!/bin/sh

set -e

case "$1" in
    text)
        ENCODED_ID="text-archive"
        ;;
    private|pics)
        ENCODED_ID="$1"
        ;;
    *)
        echo "Unknown argument: \"$1\""
        exit
        ;;
esac

if [ -d "$HOME/$ENCODED_ID" ]; then
    echo "folder already exists"
    exit
fi


7z x "$HOME/encoded/$ENCODED_ID.7z"

Finally, I also have a script that I use to quickly open up a specific file in my journal. Example uses:

alias ej="editjournal"
ej # opens up the current day's journal entry
ej -d 10 # opens up the journal with the current year and month but the 10th day
ej --lastseq # opens up the last journal entry
ej --lastmodified # opens up the last modified journal entry
ej -f # fuzzy find whatever file you want in the journal

The script is a bit long and can be found in this gist.

Completions

If you want completions, you can just hardcode the values:

complete -W "text journal pics" encode
complete -W "text journal pics" decode

and for the editjournal script:

_editjournal_completions() {
    local cur_word="${COMP_WORDS[COMP_CWORD]}"
    local prev_word="${COMP_WORDS[COMP_CWORD - 1]}"

    local generic_options="--year --month --day --skip-prompts lastseq lastmodified"
    local dates=($(editjournal --completions ${COMP_WORDS[@]:1}))

    case "$prev_word" in
        --year|-y)
            local DIRS=$(basename -a $DIARY_FOLDER/* | tr '\n' ' ')
            COMPREPLY=( $(compgen -W "${DIRS[*]}" -- $cur_word ) )
            ;;
        --month|-m)
            local DIRS=$(basename -a $DIARY_FOLDER/${dates[0]}/* | tr '\n' ' ')
            COMPREPLY=( $(compgen -W "${DIRS[*]}" -- $cur_word ) )
            ;;
        --day|-d)
            local FILES=$(basename -a $DIARY_FOLDER/${dates[0]}/${dates[1]}/* | sed 's/.txt//' | tr '\n' ' ')
            COMPREPLY=( $(compgen -W "${FILES[*]}" -- $cur_word ) )
            ;;
        *)
            COMPREPLY=( $(compgen -W "$generic_options" -- $cur_word) )
    esac

    return 0
}

complete -F _editjournal_completions -o default editjournal

Backing Up the Data

You can use whatever storage system you want: a flash drive, Google Drive, Dropbox, but personally for small text file archives, I would recommend GitHub or some other repository hosting service.

My ~/encoded is a git repository that holds all my archives. I have my .gitignore set up so it only includes text archives and not the large archives with images/videos.

*
!.gitignore
!text-archive.7z
!diary.7z

I have an alias set so that I can reference the git repository anywhere and push commits on any directory:

alias enc='/usr/bin/git --git-dir=$HOME/encoded/.git --work-tree=$HOME/encoded'

Now I can simply call enc status, enc add ..., enc commit ..., or whatever anywhere without having to go to the encoded directory.

If you were looking for a way to have version control for the content inside the archive, the steps above won't help. Instead, you can make the archive folder (~/journal) a git repo itself, and since you are encrypting it before pushing it to GitHub, you don't have to worry about sub-repos.

The main issue with this approach is that the inner git repo doesn't sync with the root repo's updates, but you can edit the encode script to add commits to the sub-repos aswell.

Previewing

To finish off, you might want to view your files from time to time. There are many ways to do that of course, I usually just look at them inside the editor I use (neovim/vscode). But if you want a more focused method, you could use my self-hosted local-files-viewer.