Skip to content

Commit

Permalink
Unique key per file
Browse files Browse the repository at this point in the history
Each file is now encrypted with a unique key, that is wrapped with the master key using AES-KW
  • Loading branch information
ItalyPaleAle committed Mar 21, 2020
1 parent cf2cd20 commit 1ab107d
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 111 deletions.
4 changes: 2 additions & 2 deletions .eslintignore
@@ -1,5 +1,5 @@
# Vendored dependencies
app/vendor
# Docs
docs-source

# Auth0 rules (they follow a different style)
auth0
Expand Down
12 changes: 6 additions & 6 deletions .eslintrc.js
Expand Up @@ -19,6 +19,10 @@ module.exports = {
processor: 'svelte3/svelte3'
}
],
globals: {
// See https://github.com/eslint/eslint/issues/11524
BigInt: true
},
settings: {
'svelte3/ignore-styles': () => true,
'html': {
Expand Down Expand Up @@ -166,12 +170,8 @@ module.exports = {
'error',
'always'
],
'no-multiple-empty-lines': [
'error',
{
max: 1
}
],
// Need to disable this because it causes issues with Svelte
'no-multiple-empty-lines': 'off',
'operator-linebreak': [
'error',
'after'
Expand Down
1 change: 1 addition & 0 deletions app/components/PassphraseBox.svelte
Expand Up @@ -51,6 +51,7 @@ function handleSubmit() {
replace('/list')
})
.catch((err) => {
// eslint-disable-next-line no-console
console.error('err caught', err)
unlockError = true
})
Expand Down
2 changes: 0 additions & 2 deletions app/layout/App.svelte
@@ -1,13 +1,11 @@
<Navbar />

<div class="container w-full lg:w-3/5 px-2 pt-10 lg:pt-10 mt-10">
<Router {routes}/>
<footer class="text-xs text-gray-600 text-center mt-8 mb-2">Built with <a href="https://hereditas.app">Hereditas</a></footer>
</div>

<script>
// Components
import RequestAuthentication from '../components/RequestAuthentication.svelte'
import Navbar from '../components/Navbar.svelte'
// Router and routes
Expand Down
38 changes: 25 additions & 13 deletions app/lib/Box.js
@@ -1,12 +1,12 @@
import {DeriveKey, Decrypt, buf2str} from './CryptoUtils'
import {DeriveKey, Decrypt, buf2str, UnwrapKey} from './CryptoUtils'
import {DecodeArrayBuffer} from './Base64Utils'

/**
* Manages the Hereditas box
*/
export class Box {
constructor() {
this._key = null
this._masterKey = null
this._contents = null
this._indexFetchingPromise = null
this._encryptedIndex = null
Expand All @@ -18,14 +18,14 @@ export class Box {
* @returns {boolean}
*/
isUnlocked() {
return this._key && this._contents
return this._masterKey && this._contents
}

/**
* Lock the box again, removing the key and the decrypted index from memory
*/
lock() {
this._key = null
this._masterKey = null
this._contents = null
}

Expand Down Expand Up @@ -58,19 +58,27 @@ export class Box {
}

// Return the promise
let iv = null
let data = null
return fetch(info.dist)
// Grab the encrypted contents as ArrayBuffer
.then((response) => response.arrayBuffer())
// Decrypt the data
.then((buffer) => {
// The first 12 bytes are the IV
const iv = buffer.slice(0, 12)
const data = buffer.slice(12)
// The first 40 bytes are the wrapped key, and the next 12 bytes are the IV
const wrappedKey = buffer.slice(0, 40)
iv = buffer.slice(40, 52)
data = buffer.slice(52)

// Un-wrap the key
return UnwrapKey(this._masterKey, wrappedKey)
})

.then((key) => {
// Get the tag
const tag = DecodeArrayBuffer(info.tag)

return Decrypt(this._key, iv, data, tag)
return Decrypt(key, iv, data, tag)
.then((data) => {
// Clone the info object
info = JSON.parse(JSON.stringify(info))
Expand Down Expand Up @@ -112,9 +120,10 @@ export class Box {
.then((buffer) => {
// Read the data from the response
this._encryptedIndex = {
// The first 12 bytes are the IV
iv: buffer.slice(0, 12),
data: buffer.slice(12)
// The first 40 bytes are the wrapped key, and the next 12 bytes are the IV
wrappedKey: buffer.slice(0, 40),
iv: buffer.slice(40, 52),
data: buffer.slice(52)
}

// Request is done
Expand Down Expand Up @@ -152,11 +161,14 @@ export class Box {
// First: derive the encryption key
.then(() => DeriveKey(passphrase + appToken, keySalt))
.then(([key]) => {
this._key = key
this._masterKey = key
})

// Un-wrap the key
.then(() => UnwrapKey(this._masterKey, this._encryptedIndex.wrappedKey))

// Decrypt the index
.then(() => Decrypt(this._key, this._encryptedIndex.iv, this._encryptedIndex.data, indexTag))
.then((key) => Decrypt(key, this._encryptedIndex.iv, this._encryptedIndex.data, indexTag))
.then((data) => {
// Convert the buffer to string
const str = buf2str(new Uint8Array(data))
Expand Down
37 changes: 31 additions & 6 deletions app/lib/CryptoUtils.js
Expand Up @@ -43,15 +43,15 @@ function concatBuffers(buffer1, buffer2) {
* @async
*/
export function DeriveKey(passphrase, salt) {
salt = salt || crypto.getRandomValues(new Uint8Array(64))
return crypto.subtle.importKey('raw', str2buf(passphrase), 'PBKDF2', false, ['deriveKey'])
salt = salt || window.crypto.getRandomValues(new Uint8Array(64))
return window.crypto.subtle.importKey('raw', str2buf(passphrase), 'PBKDF2', false, ['deriveKey'])
.then((key) =>
crypto.subtle.deriveKey(
window.crypto.subtle.deriveKey(
{name: 'PBKDF2', salt, iterations: process.env.PBKDF2_ITERATIONS, hash: 'SHA-512'},
key,
{name: 'AES-GCM', length: 256},
{name: 'AES-KW', length: 256},
false,
['decrypt'],
['unwrapKey'],
)
)
.then((key) => [key, salt])
Expand All @@ -70,5 +70,30 @@ export function DeriveKey(passphrase, salt) {
*/
export function Decrypt(key, iv, data, tag) {
const ciphertext = concatBuffers(data, tag)
return crypto.subtle.decrypt({name: 'AES-GCM', iv}, key, ciphertext)
return window.crypto.subtle.decrypt({name: 'AES-GCM', iv}, key, ciphertext)
}

/**
* Unwraps a key wrapped with AES-KW (per RFC 3349)
*
* @param {CryptoKey} wrappingKey - Key used to wrap/unwrap the key
* @param {ArrayBuffer} ciphertext - Wrapped key
* @returns {Promise<CryptoKey>} Unwrapped key
* @async
* @throws Throws an error if the decryption fails, likely meaning that the key was wrong.
*/
export function UnwrapKey(wrappingKey, ciphertext) {
return window.crypto.subtle.unwrapKey(
'raw',
ciphertext,
wrappingKey,
{name: 'AES-KW'},
{name: 'AES-GCM'},
false,
['decrypt']
)
.then((key) => {
console.log(key)
return key
})
}
3 changes: 0 additions & 3 deletions app/views/ListView.svelte
Expand Up @@ -59,8 +59,6 @@ import {box} from '../stores'
// Params from the route, which includes the prefix
export let params = {}
let i = 0
// List of contents
const list = {
files: [],
Expand Down Expand Up @@ -112,7 +110,6 @@ $: {
list.files = files
list.paths = list.prefix ? list.prefix.split('/') : []
list.i = 0
console.log(list)
}
// Ensure that we have unlocked the box
Expand Down
6 changes: 3 additions & 3 deletions app/webpack.config.js
Expand Up @@ -69,9 +69,9 @@ function webpackConfig(appParams) {
{
test: /\.css$/,
use: [
"style-loader",
{loader: "css-loader", options: {importLoaders: 1}},
"postcss-loader",
'style-loader',
{loader: 'css-loader', options: {importLoaders: 1}},
'postcss-loader',
]
}
]
Expand Down
39 changes: 23 additions & 16 deletions cli/lib/Builder.js
Expand Up @@ -6,6 +6,7 @@ const {Readable} = require('stream')
const util = require('util')
const Content = require('./Content')
const path = require('path')
const kw = require('./aes-kw')

// Webpack
const webpack = util.promisify(require('webpack'))
Expand Down Expand Up @@ -69,14 +70,14 @@ class Builder {
// This needs to be of 64 bytes, which is the length of a SHA-512 hash
this.keySalt = await randomBytesPromise(64)

// Step 4: derive the key
const key = await this._deriveKey(this._passphrase + this._config.get('appToken'), this.keySalt)
// Step 4: derive the master key
const masterKey = await this._deriveKey(this._passphrase + this._config.get('appToken'), this.keySalt)

// Step 5: encrypt all files
content = await this._encryptContent(key, content)
content = await this._encryptContent(masterKey, content)

// Step 6: write an (encrypted) index file
this.indexTag = await this._createIndex(key, content)
this.indexTag = await this._createIndex(masterKey, content)

// Step 7: build the app with webpack
const appParams = {
Expand Down Expand Up @@ -151,12 +152,12 @@ class Builder {
/**
* Creates an index file and encrypts it on disk.
*
* @param {Buffer} key - Encryption key
* @param {Buffer} masterKey - Master encryption key
* @param {HereditasContentFile[]} content - List of content
* @returns {Buffer} Authentication tag
* @async
*/
async _createIndex(key, content) {
async _createIndex(masterKey, content) {
// Creat the index file, and convert it to a Readable Stream
const indexData = JSON.stringify(content)
const inStream = new Readable()
Expand All @@ -168,17 +169,17 @@ class Builder {
const outStream = fs.createWriteStream(path.join(this._config.get('distDir'), '_index'))

// Encrypt the index and write it, returning the tag
return this._encryptStream(key, inStream, outStream)
return this._encryptStream(masterKey, inStream, outStream)
}

/**
* Encrypts all the content
* @param {Buffer} key - Encryption key
* @param {Buffer} masterKey - Master encryption key
* @param {HereditasContentFile[]} content - List of content
* @returns {HereditasContentFile[]} - List of content with the dist and tag properties set
* @async
*/
async _encryptContent(key, content) {
async _encryptContent(masterKey, content) {
// Clone the content object
const result = JSON.parse(JSON.stringify(content))

Expand All @@ -196,7 +197,7 @@ class Builder {
result[i] = content.el

// Encrypt the stream and get the tag
const tagBuf = await this._encryptStream(key, content.inStream, outStream)
const tagBuf = await this._encryptStream(masterKey, content.inStream, outStream)
const tag = tagBuf.toString('base64')

// Add the dist and tag properties to the result object
Expand All @@ -210,22 +211,28 @@ class Builder {
/**
* Encrypts a stream using aes-256-gcm
*
* @param {Buffer} key - Encryption key; must be 256 bit long
* @param {Buffer} masterKey - Master key; must be 256 bit long
* @param {Stream} inStream - Readable stream with the data to encrypt
* @param {Stream} outStream - Writable stream to pipe the data to
* @returns {Buffer} Authentication tag
* @async
*/
async _encryptStream(key, inStream, outStream) {
async _encryptStream(masterKey, inStream, outStream) {
// Generate a key for this specific file
const fileKey = await randomBytesPromise(32)
// Generate an IV
const iv = await randomBytesPromise(12)
const fileIV = await randomBytesPromise(12)

// Wrap the file's key with the master key, using AES-KW (RFC-3394)
const wrappedKey = kw.encrypt(masterKey, fileKey)

return new Promise((resolve, reject) => {
// Write the IV to the outStream, at the beginning
outStream.write(iv)
// Write the wrapped key and IV to the outStream, at the beginning
outStream.write(wrappedKey)
outStream.write(fileIV)

// Create the Cipher, which can be used as a stream transform too
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
const cipher = crypto.createCipheriv('aes-256-gcm', fileKey, fileIV)

// When the encryption is done, get the authentication tag
cipher.on('end', () => {
Expand Down

0 comments on commit 1ab107d

Please sign in to comment.