Code Experiment: End-to-End encrypted Diary App
This article has been published a while ago.
If this is a technical article some information might be out of date. If something is terribly broken, let me know and I will update the article accordingly.
Update 01.09.2019
This post has been written by naive me in 2017. Back then I knew basically nothing about encryption! The described app has some major design flaws (App encryption key is exposed to the public; App encryption key is not tied to users Master Password; Unlocking the journal sends the users Master Password in plain text to the server; and probably many more β¦)
So yeah, I'm by no means a cryptographic expert! Please, don't use the source code of the mentioned app as the basis of your next project!
Update 11.03.2018
I recently browsed the source code of the app and decided to give it a redesign with Tailwind CSS. You can see the pull request or see a list of updated screenshots at the bottom of this post.
Earlier this year I thought that I should start writing a diary again. In the past I used Day One but since they released version 2.0 of their app I rarely used it anymore. The design didn't resonate with me. They added encryption support in June 2017 but when I started with this project in January 2017 there was still no sign of it.
I thought "How hard can it be? Can I build something similar by myself?". I always wanted to do something with encryption and just like that a little side project was born. This post describes how I've built a diary app with Vue and Laravel. It's merely a collection of notes and thoughts.
TL;DR #
- I've written a small Diary App to experiment with Laravel, Vue, Vuex and End-to-End Encryption
- Testing complex real-world Vue Components is still hard
- The Code is on GitHub
Table of Contents #
- Backend: Laravel, Passport and JSON API
- Frontend: Design
- Frontend: Vue
- Frontend: Encryption
- Frontend: Testing
- Conclusion
- Updated Screenshots
Laravel is my go to framework nowadays. The database structure is quite simple. There are User
, Journal
, Entry
and Media
models. To make the development of the SPA easier I installed Laravel Passport. The packages adds a some nice security features for your APIs (Issuing OAuth Tokens, using your own API for your JavaScript Frontend and much more).
I've also added 2 additional columns to the users
table: master_password
and encryption_key
. The idea was, that the user has to set a master password when he opens the app for the first time (Similar how you protect your 1Password Vault). When creating your master password the application will generate an encryption key and store it in the database. This encryption key will be passed to the frontend and will be used by our Vue application to encrypt and decrypt values in the browser.
I've chosen to generate an encryption key by the application itself, because the encryption library I used should use a fixed length key. If the user could set his own encryption key it might be too weak. The app now creates a secure 100 characters encryption key.
It's also worth noting that I used "Mutators" and "Accessors" in all models. Things like master password, encryption key, journal name, entry title and entry content will not only be encrypted in the frontend but also in the backend. If someone would steal the database, he wouldn't be able to read the journal entries of a user without knowing the global application encryption key (Stored in the APP_KEY
environment variable).
I've also used fractal and the JsonApiSerializer
to return standardized responses from the API. 1
The entire API is also backed by Unit- and Integration tests.
This was the easiest part. Laravel and all those packages make developing applications and APIs a breeze π.
Frontend: Design #
I'm by no means a designer and every time I have to design a complex view I appreciate the hard work all those designers in the world do. It's really hard to make an interface easy to understand and make it visually pleasing.
For this diary app I first had the idea to make it fully responsive. You should be able to use it on your phone and on your desktop. After experimenting with Vue and Vuex I quickly realized that it would be much easier to get a working prototype which works on desktop first. Here are some sketches and screenshots of my design ideas:
In the end I settled for a layout which is similar to Mail.app or Reeder.app. My implementation looks like this:
## Frontend: Vue, Vuex and Vue RouterNow to the fun part: Vue! Since it's 0.x-releases I'm a big fan of the framework and I've already built multiple smaller widgets with it. But I've never built an SPA with routes and global state management with it.
I began by simply reading the docs for vuex and vue-router multiple times and began to outline how my global state object would look like. In the end I landed on the following structure.
{
hasMasterPassword: false,
isUnlocked: false,
// The Entry the user is currently working with
entry: null,
// Key to encrypt and decrypt everything
encryption_password: null,
// All journals
journals: null,
selected_journal: null,
// Entries for the current journal
entries: null,
selected_entry: null,
// The logged in User
user: null,
files: []
}
The names and the comments speak for them self so I won't go into details here.
Next I've added a lot of actions. These reflect all async calls to the API.
To keep this file as lean I moved the code which actually calls the API into "Model" classes (/resources/assets/js/models/*
). The look like this:
import Api from './Api.js';
export default class extends Api {
all(journalId) {
return this.get(`/api/journals/${journalId}/entries`);
}
find(id) {
return this.get(`/api/entries/${id}/`);
}
create(journalId) {
return this.post(`/api/journals/${journalId}/entries`, {});
}
update(entryId, payload) {
return this.patch(`/api/entries/${entryId}/`, payload);
}
destroy(entryId) {
return this.deleteCall(`/api/entries/${entryId}/`);
}
}
Isn't that a nice clean file? Coming from the backend I love these new ES2016 features. The Api
class all models extend looks like this.
import axios from 'axios';
export default class {
get(url) {
return axios.get(url);
}
post(url, payload) {
return axios.post(url, payload);
}
patch(url, payload) {
return axios.patch(url, payload);
}
deleteCall(url) {
return axios.delete(url);
}
}
I've also setup 2 simple getters: isUnlocked
and isLocked
. These will come handy in the next step when we vue-router.
Vue Router #
After countless re-reads of the documentation I finally understood how I could implement my design idea with vue-router. Foreach each "route" I've created a Vue Component. These are:
JournalListView
: The home view. List all available journals for the current user and allows to create a new oneJournalView
: Lists all entrys which belong to a journal. Lets the user create a new entry or delete the entire journal and it's entriesEntryView
: Displays a rendered markdown version of the entryEntryEditorView
: The text editor. Also allows the user to upload files for the current entryUnlockView
: Handles Unlocking the Diary and guides user through the process of setting up a new master password
All Views except the UnlockView
are protected and can only be opened if the diary is unlocked. The "protection" is applied in the meta
object of each route.
{
path: '/j',
name: 'journals.index',
component: (resolve) => {
require(['./views/JournalListView.vue'], resolve)
},
meta: {
requiresUnlock: true,
},
}
I've placed the implementation of this locking-mechanism in the beforeEach
statement of the router.
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresUnlock)) {
if (!store.getters.isUnlocked) {
next({
path: '/unlock',
query: { redirect: to.fullPath },
});
} else {
next();
}
} else {
next();
}
});
If the route which should be opened is protected by the requiresUnlock
setting, the router checks the global state in vuex (! store.getters.isUnlocked
) and redirects to the unlock-screen if the diary is still locked.
The frontend encryption library I used is aes.js. I've created a dedicated crypto.js class which handles the setup of aes.js and gives me two simple methods: encrypt
and decrypt
.
// crypto.js
import aesjs from 'aes-js';
import pbkdf2 from 'pbkdf2';
export default class {
constructor(encryptionKey) {
this.salt = window.Laravel.salt;
this.encryptionKey = encryptionKey;
this.key_256 = pbkdf2.pbkdf2Sync(this.encryptionKey, this.salt, 1, 256 / 8, 'sha512');
}
encrypt(stringToEncrypt) {
let textBytes = aesjs.utils.utf8.toBytes(stringToEncrypt);
let aesCtr = new aesjs.ModeOfOperation.ctr(this.key_256, new aesjs.Counter(5));
let encryptedBytes = aesCtr.encrypt(textBytes);
return aesjs.utils.hex.fromBytes(encryptedBytes);
}
decrypt(stringToDecrypt) {
let encryptedBytes = aesjs.utils.hex.toBytes(stringToDecrypt);
let aesCtr = new aesjs.ModeOfOperation.ctr(this.key_256, new aesjs.Counter(5));
let decryptedBytes = aesCtr.decrypt(encryptedBytes);
return aesjs.utils.utf8.fromBytes(decryptedBytes);
}
}
The class is used throughout the app. When creating a journal, displaying the journal title, creating an entry and displaying the entry title and content.
In my first vision I also wanted to encrypt files. This is a far more difficult than I thought and so I've only added support to upload file by drag and drop.2
### Frontend TestingAnother big topic for me was writing test for my frontend code. This is something I've neglected in the past and finally wanted to start doing ... but it's so hard! I've wanted to test my Vue components with Jest, a Testing Framework built by Facebook, but I couldn't get it to work. I spent countless hours reading awesome guides like this one by Freek Van der Herten but I couldn't get it running with my Vuex actions.
After many frustrating evenings I gave up π. I've only written 1 test for a Vue component, but the test doesn't do something significant. It just asserts the components get's mounted correctly. Here the code for that:
import Vue from 'vue/dist/vue.js';
import UnlockScreen from './../../../resources/assets/js/components/Util/UnlockScreen.vue';
describe('UnlockScreen', () => {
Vue.component('unlock-form', UnlockScreen);
beforeEach(() => {
document.body.innerHTML = `
<div id="app">
<unlock-form></unlock-form>
</div>
`;
});
it('can mount', async () => {
await createVm();
expect(document.body.innerHTML).toMatchSnapshot();
});
it('password is empty when component is initialized', async () => {
const { component } = await createVm();
expect(component.password).toEqual('');
});
});
async function createVm() {
const vm = new Vue({
el: '#app',
store,
});
await Vue.nextTick(() => {});
return { app: vm, component: vm.$children[0] };
}
And heres another test written for my crypto-wrapper:
import Crypto from './../../resources/assets/js/classes/Crypto.js';
describe('crypto-class', function () {
beforeEach(() => {
window.Laravel = {
salt: 'base64:n2xyMxL55nQOMlpIUVgDRtlvKe6zLPCMf3lhZZBKT7M=',
};
});
test('it encrypts a given string', () => {
let crypto = new Crypto('encryption-key');
let string = 'This string will be encrypted';
let encryptedString = crypto.encrypt(string);
expect(string === encryptedString).toBeFalsy();
});
test('it decrypts an encrypted string', () => {
let crypto = new Crypto('encryption-key');
let string = 'This string will be encrypted';
let encryptedString = 'd1daf756780a6c39fa772d3f7e54193386362079aa7d7a6b89ad820ec1';
let decryptedString = crypto.decrypt(encryptedString);
expect(decryptedString).toBe(string);
expect(decryptedString === encryptedString).toBeFalsy();
});
test('it fails to decrypt value if encryption key has changed', () => {
let crypto = new Crypto('encryption-key-is-different');
let string = 'This string will be encrypted';
let encryptedString = 'd1daf756780a6c39fa772d3f7e54193386362079aa7d7a6b89ad820ec1';
let decryptedString = crypto.decrypt(encryptedString);
expect(decryptedString === string).toBeFalsy();
expect(decryptedString === encryptedString).toBeFalsy();
});
test('it fails to decrypt value if decrypted string has been modified', () => {
let crypto = new Crypto('encryption-key');
let encryptedString = 'xxxd1daf756780a6c39fa772d3f7e54193386362079aa7d7a6b89ad820ec1';
expect(() => {
crypto.decrypt(encryptedString);
}).toThrow();
});
test('it fails to decrypt value if salt has been modified', () => {
window.Laravel.salt = 'this-is-a-different-salt-string';
let crypto = new Crypto('encryption-key');
let string = 'This string will be encrypted';
let encryptedString = 'd1daf756780a6c39fa772d3f7e54193386362079aa7d7a6b89ad820ec1';
let decryptedString = crypto.decrypt(encryptedString);
expect(decryptedString === string).toBeFalsy();
expect(decryptedString === encryptedString).toBeFalsy();
});
});
JavaScript testing is something I will definitely have an eye on in the future. I'm sure if I invest more time in reading guides, documentations or talk with other developers about this topic that one day I will be able to master it π.
## ConclusionI've worked on this little app over the course of 7 months now. During this time I've learned a lot. My final conclusion about this project:
- End-to-End encryption for text strings isn't that hard. (Depends heavily on the kind of encryption you choose)
- End-to-End encryption for files is far more difficult (but also something I haven't invested much research time into it)
- Vuex is a great tool but you have to be careful not to put everything in the global state
- Fractal is a nice package to return standardized responses from your APIs
- Writing tests for complex Vue components is harder than I thought
- Coding the Frontend of an application is still far more difficult for me than building the Backend of an application. But I like how the development of JavaScript apps in the last 2-3 years has changed
In the end I won't use this app for to write any entries but it was a cool experiment.
If you've made it this far: Thank's for reading! I've you have any questions or thoughts about this post please let me know on Twitter or send me a message by mail: [email protected].
The code is also available on GitHub.
## Updated Screenshots-
I've heard and read a lot about fractal and how useful it easy, but never had a project actually using it. Now that I've finally installed it I wish I could go back in time and use it in all my projects π ↩
-
I personally found it quite hard to find valuable resources about file encryption. Definitely something I want to learn more (and probably write more too). ↩