diff --git a/sigal/plugins/encrypt/encrypt.py b/sigal/plugins/encrypt/encrypt.py index 2886400..f3e1f5c 100644 --- a/sigal/plugins/encrypt/encrypt.py +++ b/sigal/plugins/encrypt/encrypt.py @@ -196,8 +196,8 @@ def encrypt_gallery(gallery): # gallery.encryptCache = cache logger.info("starting encryption") - encrypt_files(settings, config, cache, albums, gallery.progressbar_target) copy_assets(settings) + encrypt_files(settings, config, cache, albums, gallery.progressbar_target) save_cache(settings, cache) except Abort: pass @@ -209,6 +209,8 @@ def encrypt_files(settings, config, cache, albums, progressbar_target): raise Abort key = kdf_gen_key(config["password"].encode("utf-8"), config["kdf_salt"].encode("utf-8"), config["kdf_iters"]) + gcm_tag = config["gcm_tag"].encode("utf-8") + medias = list(chain.from_iterable(albums.values())) with progressbar(medias, label="%16s" % "Encrypting files", file=progressbar_target, show_eta=True) as medias: for media in medias: @@ -226,25 +228,38 @@ def encrypt_files(settings, config, cache, albums, progressbar_target): continue full_path = os.path.join(settings["destination"], f) - with BytesIO() as outBuffer: - try: - with open(full_path, "rb") as infile: - encrypt(key, infile, outBuffer, config["gcm_tag"].encode("utf-8")) - except Exception as e: - logger.error("Encryption failed for %s: %s", f, e) - else: - logger.info("Encrypting %s...", f) - try: - with open(full_path, "wb") as outfile: - outfile.write(outBuffer.getbuffer()) - cacheEntry.add(f) - except Exception as e: - logger.error("Could not write to file %s: %s", f, e) + if encrypt_file(f, full_path, key, gcm_tag): + cacheEntry.add(f) + + key_check_path = os.path.join( + os.path.join(settings["destination"], 'static'), + 'keycheck.txt' + ) + encrypt_file("keycheck.txt", key_check_path, key, gcm_tag) + +def encrypt_file(filename, full_path, key, gcm_tag): + with BytesIO() as outBuffer: + try: + with open(full_path, "rb") as infile: + encrypt(key, infile, outBuffer, gcm_tag) + except Exception as e: + logger.error("Encryption failed for %s: %s", filename, e) + return False + else: + logger.info("Encrypting %s...", filename) + try: + with open(full_path, "wb") as outfile: + outfile.write(outBuffer.getbuffer()) + except Exception as e: + logger.error("Could not write to file %s: %s", filename, e) + return False + return True def copy_assets(settings): theme_path = os.path.join(settings["destination"], 'static') - copy(ASSETS_PATH + "/decrypt.js", theme_path, symlink=False, rellink=False) - copy(ASSETS_PATH + "/decrypt-worker.js", theme_path, symlink=False, rellink=False) + copy(os.path.join(ASSETS_PATH, "decrypt.js"), theme_path, symlink=False, rellink=False) + copy(os.path.join(ASSETS_PATH, "keycheck.txt"), theme_path, symlink=False, rellink=False) + copy(os.path.join(ASSETS_PATH, "sw.js"), settings["destination"], symlink=False, rellink=False) def inject_scripts(context): try: diff --git a/sigal/plugins/encrypt/endec.py b/sigal/plugins/encrypt/endec.py index 37ca8d8..9316fc8 100644 --- a/sigal/plugins/encrypt/endec.py +++ b/sigal/plugins/encrypt/endec.py @@ -32,6 +32,7 @@ from cryptography.exceptions import InvalidTag from typing import BinaryIO backend = default_backend() +MAGIC_STRING = "_e_n_c_r_y_p_t_e_d_" def kdf_gen_key(password: bytes, salt:bytes, iters: int) -> bytes: kdf = PBKDF2HMAC( @@ -74,6 +75,7 @@ def encrypt(key: bytes, infile: BinaryIO, outfile: BinaryIO, tag: bytes): ciphertext = outfile rawbytes = plaintext.read() encrypted = aesgcm.encrypt(iv, rawbytes, tag) + ciphertext.write(MAGIC_STRING.encode("utf-8")) ciphertext.write(iv) ciphertext.write(encrypted) @@ -83,6 +85,9 @@ def decrypt(key: bytes, infile: BinaryIO, outfile: BinaryIO, tag: bytes): aesgcm = AESGCM(key) ciphertext = infile plaintext = outfile + magicstring = ciphertext.read(len(MAGIC_STRING)) + if magicstring != MAGIC_STRING.encode("utf-8"): + raise ValueError("Data is not encrypted") iv = ciphertext.read(12) rawbytes = ciphertext.read() try: diff --git a/sigal/plugins/encrypt/static/decrypt-worker.js b/sigal/plugins/encrypt/static/decrypt-worker.js deleted file mode 100644 index f86c329..0000000 --- a/sigal/plugins/encrypt/static/decrypt-worker.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * copyright (c) 2020 Bowen Ding - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. -*/ - -"use strict" -importScripts("decrypt.js"); - -onmessage = Decryptor.onWorkerMessage; diff --git a/sigal/plugins/encrypt/static/decrypt.js b/sigal/plugins/encrypt/static/decrypt.js index 7a0c3b8..740e781 100644 --- a/sigal/plugins/encrypt/static/decrypt.js +++ b/sigal/plugins/encrypt/static/decrypt.js @@ -23,51 +23,154 @@ "use strict" class Decryptor { constructor(config) { - const c = Decryptor._getCrypto(); - if (Decryptor.isWorker()) { - this._role = "worker"; - const encoder = new TextEncoder("utf-8"); - const salt = encoder.encode(config.kdf_salt); - const iters = config.kdf_iters; - const shared_key = encoder.encode(config.password); - const gcm_tag = encoder.encode(config.gcm_tag); + this._jobCount = 0; + this._jobMap = new Map(); + this._workerReady = false; - Decryptor - ._initAesKey(c, salt, iters, shared_key) - .then((aes_key) => { - this._decrypt = (encrypted_blob_arraybuffer) => - Decryptor.decrypt(c, encrypted_blob_arraybuffer, - aes_key, gcm_tag); - }) - .then(() => { - Decryptor._sendEvent(self, "DecryptWorkerReady"); - }); - } else { - this._role = "main"; - this._jobCount = 0; - this._numWorkers = Math.min(4, navigator.hardwareConcurrency); - this._jobMap = new Map(); - this._workerReady = false; - this._galleryId = config.galleryId; - Decryptor._initPage(); - if (!("password" in config && config.password)) { - this._askPassword() - .then((password) => { - config.password = password; - this._createWorkerPool(config); - }) - } else { - this._createWorkerPool(config); + if (Decryptor.isServiceWorker()) { + this._role = "service_worker"; + } else if (!Decryptor.isWorker()) { + if (!Decryptor.featureTest()) { + alert("This page cannot function properly because your browser does not support some critical features. Please update your browser."); + return; } + this._role = "main"; + this._galleryId = config.galleryId; + this._mSetupServiceWorker(config); } console.info("Decryptor initialized"); } - /* main thread only */ static init(config) { - if (Decryptor.isWorker()) return; - window.decryptor = new Decryptor(config); + if (Decryptor.isServiceWorker()) { + self.decryptor = new Decryptor(config); + } else { + window.decryptor = new Decryptor(config); + } + } + + static featureTest() { + let features = [ + typeof crypto, + typeof TextEncoder, + typeof navigator.serviceWorker, + typeof Proxy, + typeof fetch, + typeof Blob.prototype.arrayBuffer, + typeof Response.prototype.clone, + typeof caches + ]; + return features.every((e) => e !== "undefined"); + } + + async _swInitServiceWorker(config) { + const crypto = Decryptor._getCrypto(); + const encoder = new TextEncoder("utf-8"); + const salt = encoder.encode(config.kdf_salt); + const iters = config.kdf_iters; + const shared_key = encoder.encode(config.password); + const gcm_tag = encoder.encode(config.gcm_tag); + + const aes_key = await Decryptor._initAesKey(crypto, salt, iters, shared_key); + if (await this._swCheckAesKey(aes_key, gcm_tag)) { + this.workerReady = true; + this._decrypt = (encrypted_blob_arraybuffer) => + Decryptor.decrypt(crypto, encrypted_blob_arraybuffer, aes_key, gcm_tag); + this._swNotifyWorkerReady(); + } else { + const array_clients = await self.clients.matchAll({includeUncontrolled: true}); + for (let client of array_clients) { + this._proxyWrap(client)._mNotifyIncorrectPassword(); + } + } + } + + async _swCheckAesKey(aes_key, gcm_tag) { + let response; + try { + response = await fetch(Decryptor.keyCheckURL); + } catch (error) { + throw new Error("Fetched failed when checking encryption key"); + } + try { + await Decryptor.decrypt( + Decryptor._getCrypto(), + await response.blob(), + aes_key, + gcm_tag, + true + ); + } catch (error) { + console.warn("Password is incorrect!"); + return false; + } + return true; + } + + async _swNotifyWorkerReady() { + const array_clients = await self.clients.matchAll({includeUncontrolled: true}); + for (let client of array_clients) { + this._proxyWrap(client)._mReload(); + } + } + + static isInitialized() { + if (Decryptor.isServiceWorker()) { + return 'decryptor' in self && self.decryptor.workerReady; + } else { + return 'decryptor' in window && window.decryptor.workerReady; + } + } + + get workerReady() { + return this._workerReady; + } + + set workerReady(val) { + this._workerReady = (val ? true : false); + if (this._workerReady) { + const eventTarget = (Decryptor.isWorker() ? self : document); + Decryptor._sendEvent(eventTarget, "DecryptWorkerReady"); + } + } + + _mReload() { + window.location.reload(); + } + + _mNotifyIncorrectPassword() { + localStorage.removeItem(this._galleryId); + } + + async _mSetupServiceWorker(config) { + if (!('serviceWorker' in navigator)) { + console.error("Fatal: Your browser does not support service worker"); + throw new Error("no service worker support"); + } + + if (navigator.serviceWorker.controller) { + this.serviceWorker = navigator.serviceWorker.controller; + } else { + navigator.serviceWorker.register(config.sw_script); + const registration = await navigator.serviceWorker.ready; + this.serviceWorker = registration.active; + } + + navigator.serviceWorker.onmessage = + (e) => Decryptor.onMessage(this.serviceWorker, e); + this.serviceWorker = this._proxyWrap(this.serviceWorker); + + if (!(await this.serviceWorker.Decryptor.isInitialized())) { + if (!('password' in config && config.password)) { + config.password = await this._mAskPassword(); + } + this.serviceWorker._swInitServiceWorker(config); + } + } + + static isServiceWorker() { + return ('undefined' !== typeof ServiceWorkerGlobalScope) && ("function" === typeof importScripts) && (navigator instanceof WorkerNavigator); } static isWorker() { @@ -75,7 +178,7 @@ class Decryptor { } static _getCrypto() { - if(crypto && crypto.subtle) { + if('undefined' !== typeof crypto && crypto.subtle) { return crypto.subtle; } else { throw new Error("Fatal: Browser does not support Web Crypto"); @@ -83,24 +186,16 @@ class Decryptor { } /* main thread only */ - async _askPassword() { - let password = sessionStorage.getItem(this._galleryId); + async _mAskPassword() { + let password = localStorage.getItem(this._galleryId); if (!password) { - return new Promise((s, e) => { - window.addEventListener( - "load", - s, - { once: true, passive: true } - ); - }).then((e) => { - const password = prompt("Input password to view this gallery:"); - if (password) { - sessionStorage.setItem(this._galleryId, password); - return password; - } else { - return "__wrong_password__"; - } - }); + const password = prompt("Input password to view this gallery:"); + if (password) { + localStorage.setItem(this._galleryId, password); + return password; + } else { + return "__wrong_password__"; + } } else { return password; } @@ -129,78 +224,6 @@ class Decryptor { ); } - async _doReload(url, img) { - const proceed = Decryptor._sendEvent(img, "DecryptImageBeforeLoad", {oldSrc: url}); - if (proceed) { - let old_src = url; - try { - const blobUrl = await this.dispatchJob("reloadImage", [old_src, null]); - img.addEventListener( - "load", - (e) => Decryptor._sendEvent(e.target, "DecryptImageLoaded", {oldSrc: old_src}), - {once: true, passive: true} - ); - img.src = blobUrl; - } catch (error) { - img.addEventListener( - "load", - (e) => Decryptor._sendEvent(e.target, "DecryptImageError", {oldSrc: old_src, error: error}), - {once: true, passive: true} - ); - img.src = Decryptor.imagePlaceholderURL; - // password is incorrect - if (error.message.indexOf("decryption failed") >= 0) { - sessionStorage.removeItem(this._galleryId); - } - throw new Error(`Image reload failed: ${error.message}`); - } - } - } - - async reloadImage(url, img) { - if (this._role === "main") { - const full_url = (new URL(url, window.location)).toString(); - if (!this.isWorkerReady()) { - document.addEventListener( - "DecryptWorkerReady", - (e) => { this._doReload(full_url, img); }, - {once: true, passive: true} - ); - } else { - this._doReload(full_url, img); - } - } else if (this._role === "worker") { - let r; - try { - r = await fetch(url); - } catch (e) { - throw new Error("fetch failed"); - } - if (r && r.ok) { - const encrypted_blob = await r.blob(); - try { - const decrypted_blob = await this._decrypt(encrypted_blob); - return URL.createObjectURL(decrypted_blob); - } catch (e) { - throw new Error(`decryption failed: ${e.message}`); - } - } else { - throw new Error("fetch failed"); - } - } - } - - /* main thread only */ - static onNewImageError(e) { - if (e.target.src.startsWith("blob")) return; - if (!window.decryptor) return; - - window.decryptor.reloadImage(e.target.src, e.target); - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - } - static _sendEvent(target, type, detail = null) { const eventInit = { detail: detail, @@ -210,186 +233,303 @@ class Decryptor { return target.dispatchEvent(new CustomEvent(type, eventInit)); } - /* main thread only */ - static _initPage() { - document.addEventListener( - "error", - e => { - if (e.target instanceof HTMLImageElement) { - Decryptor.onNewImageError(e); - } - }, - {capture: true} + static async checkMagicString(arraybuffer) { + const sample = new DataView( + arraybuffer, + 0, + Decryptor.MAGIC_STRING_ARRAYBUFFER.byteLength ); - - Image = (function (oldImage) { - function Image(...args) { - let img = new oldImage(...args); - img.addEventListener( - "error", - Decryptor.onNewImageError - ); - return img; + for (let i = 0; i < Decryptor.MAGIC_STRING_ARRAYBUFFER.byteLength; i++) { + if (Decryptor.MAGIC_STRING_ARRAYBUFFER[i] !== sample.getUint8(i)) { + return false; } - Image.prototype = oldImage.prototype; - Image.prototype.constructor = Image; - return Image; - })(Image); - - document.createElement = (function(create) { - return function() { - let ret = create.apply(this, arguments); - if (ret.tagName.toLowerCase() === "img") { - ret.addEventListener( - "error", - Decryptor.onNewImageError - ); - } - return ret; - }; - })(document.createElement); + } + return true; } - static async decrypt(crypto, blob, aes_key, gcm_tag) { - const iv = await blob.slice(0, 12).arrayBuffer(); - const ciphertext = await blob.slice(12).arrayBuffer(); + static async decrypt(crypto, blob_or_arraybuffer, aes_key, gcm_tag, check_magic_string=false) { + let arraybuffer, return_blob; + if (blob_or_arraybuffer instanceof Blob) { + arraybuffer = await blob_or_arraybuffer.arrayBuffer(); + return_blob = true; + } else if (blob_or_arraybuffer instanceof ArrayBuffer) { + arraybuffer = blob_or_arraybuffer + return_blob = false; + } else { + throw new TypeError("decrypt accepts either a Blob or an ArrayBuffer"); + } + + // make sure there is enough data to decrypt + // although 1 byte of data seems not acceptable for some browsers + // in which case crypto.decrypt will throw an error + // "The provided data is too small" + if (arraybuffer.byteLength < + Decryptor.MAGIC_STRING_ARRAYBUFFER.byteLength + + Decryptor.IV_LENGTH + + 1) { + throw new Error("not enough data to decrypt"); + } + + if (check_magic_string && !(await Decryptor.checkMagicString(arraybuffer))) { + // data is not encrypted + return blob; + } + + const iv = new DataView( + arraybuffer, + Decryptor.MAGIC_STRING_ARRAYBUFFER.byteLength, + Decryptor.IV_LENGTH + ); + const ciphertext = new DataView( + arraybuffer, + Decryptor.MAGIC_STRING_ARRAYBUFFER.byteLength + Decryptor.IV_LENGTH + ); const decrypted = await crypto.decrypt( - { - name: "AES-GCM", - iv: iv, - additionalData: gcm_tag - }, - aes_key, - ciphertext - ); - return new Blob([decrypted], {type: blob.type}); + { + name: "AES-GCM", + iv: iv, + additionalData: gcm_tag + }, + aes_key, + ciphertext + ); + if (return_blob) { + return new Blob([decrypted], {type: blob_or_arraybuffer.type}); + } else { + return decrypted; + } } - isWorkerReady() { - return this._workerReady; + _proxyWrap(target) { + const decryptor = this; + const handler = { + get: (wrappedObj, prop) => { + if (prop in wrappedObj) { + if (wrappedObj[prop] instanceof Function) { + return (...args) => wrappedObj[prop].apply(wrappedObj, args); + } else { + return wrappedObj[prop]; + } + } + if (prop === "Decryptor") { + return new Proxy(target, { + get: (wrappedObj, prop) => { + return decryptor._rpcCall(wrappedObj, prop, true); + } + }); + } + return decryptor._rpcCall(wrappedObj, prop, false); + } + } + return new Proxy(target, handler); } - _createWorkerPool(config) { - if (this._role !== "main") return; - if (this._workerReady) return; + _rpcCall(target, method, static_) { + const decryptor = this; + const dummyFunction = () => {}; + const handler = { + apply: (wrappedFunc, thisArg, args) => { + return new Promise((success, error) => { + const jobId = decryptor._jobCount++; + decryptor._jobMap.set(jobId, {success: success, error: error}); + Decryptor._rpcPostJob(jobId, target, method, args, static_); + }); + } + }; + return new Proxy(dummyFunction, handler); + } - let callback = (e) => { - const callbacks = this._jobMap.get(e.data.id); + static _rpcPostJob(jobId, messagePort, method, args, static_=false) { + const job = { + type: "job", + id: jobId, + method: method, + args: args, + static: static_ + }; + messagePort.postMessage(job); + } + + static _asyncReturn(instance, method, args) { + if (!(instance instanceof Object)) { + return Promise.reject(new Error("calling method on a primitive")); + } + if (!(method in instance && instance[method] instanceof Function)) { + return Promise.reject(new Error(`no such method: ${method}`)) + } + + try { + let promise_or_value = instance[method].apply(instance, args); + if (promise_or_value instanceof Promise) { + return promise_or_value; + } else { + return Promise.resolve(promise_or_value); + } + } catch (e) { + return Promise.reject(e); + } + } + + static onMessage(replyPort, e) { + const type = e.data.type; + const id = e.data.id; + const method = e.data.method; + const args = e.data.args; + const instance = e.data.static ? Decryptor : (Decryptor.isWorker() ? self : window).decryptor; + + if (type === "job") { + Decryptor._asyncReturn(instance, method, args) + .then( + (result) => { return {type: "reply", success: true, result: result}; }, + (error) => { return {type: "reply", success: false, result: error.message}; } + ) + .then((reply) => { + reply.id = id; + replyPort.postMessage(reply); + }); + } else if (type === "reply") { + const callbacks = decryptor._jobMap.get(e.data.id); if (e.data.success) { if (callbacks.success) callbacks.success(e.data.result); } else { if (callbacks.error) callbacks.error(new Error(e.data.result)); } - this._jobMap.delete(e.data.id); - }; - - let pool = Array(); - - for (let i = 0; i < this._numWorkers; i++) { - let worker = new Worker(config.worker_script); - worker.onmessage = callback; - pool.push(worker); - } - this._workerPool = pool; - - let notReadyWorkers = this._numWorkers; - for (let i = 0; i < this._numWorkers; i++) { - this.dispatchJob("new", [config]) - .then(() => { - if (--notReadyWorkers <= 0) { - this._workerReady = true; - Decryptor._sendEvent(document, "DecryptWorkerReady"); - } - }); + decryptor._jobMap.delete(e.data.id); } } - /* - * method: string - * args: Array - */ - dispatchJob(method, args) { - if (this._role === "main") { - return new Promise((success, error) => { - const jobId = this._jobCount++; - const worker = this._workerPool[jobId % this._numWorkers]; - this._jobMap.set(jobId, {success: success, error: error}); - Decryptor._postJobToWorker(jobId, worker, method, args); - }); - } else if (this._role === "worker") { - return Decryptor._asyncReturn(this, method, args) - .then( - (result) => { return {success: true, result: result}; }, - (error) => { return {success: false, result: error.message}; } - ); - } + static async onServiceWorkerInstall(e) { + console.log("service worker on install: ", e); + e.waitUntil(self.skipWaiting()); } - static _asyncReturn(instance, method, args) { - if (method in instance && instance[method] instanceof Function) { - try { - let promise_or_value = instance[method].apply(instance, args); - if (promise_or_value instanceof Promise) { - return promise_or_value; - } else { - return Promise.resolve(promise_or_value); - } - } catch (e) { - return Promise.reject(e); + static onServiceWorkerActivate(e) { + console.log("service worker on activate: ", e); + e.waitUntil(self.clients.claim()); + } + + static onServiceWorkerMesssage(e) { + return Decryptor.onMessage(e.source, e); + } + + static async _swHandleFetch(e) { + const request = e.request; + try { + const cached_response = await caches.match(request); + if (cached_response) { + // TODO: handle cache expiration + console.debug(`Found cached response for ${request.url}`); + return cached_response; } - } else { - return Promise.reject(new Error(`no such method: ${method}`)) + } catch (error) { + console.error("Caches.match error!"); } - } - static _postJobToWorker(jobId, worker, method, args) { - const job = { - id: jobId, - method: method, - args: args - }; - worker.postMessage(job); - } + let response; + try { + response = await fetch(request); + } catch (error) { + console.debug(`Fetch failed when trying for ${request.url}: ${error}`); + throw error; + } - /* worker thread only */ - static onWorkerMessage(e) { - const id = e.data.id; - const method = e.data.method; - const args = e.data.args; + if (!response.ok) { + console.debug(`Fetch succeeded but server returned non-2xx: ${request.url}`); + return response; + } - if (method === "new") { - self.decryptor = new Decryptor(...args); - self.addEventListener( - "DecryptWorkerReady", - (e) => self.postMessage({id: id, success: true, result: "worker ready"}), - {once: true, passive: true} + const is_image = [ + request.destination === "image", + (() => { + const content_type = response.headers.get("content-type"); + return content_type && content_type.startsWith("image"); + })() + ]; + + if (!is_image.some((e) => e)) { + console.debug(`Fetch succeeded but response is likely not an image ${request.url}`); + return response; + } + + const response_clone = response.clone(); + const encrypted_blob = await response.blob(); + const encrypted_arraybuffer = await encrypted_blob.arrayBuffer(); + if (!(await Decryptor.checkMagicString(encrypted_arraybuffer))) { + console.debug(`Response image is not encrypted: ${request.url}`); + return response_clone; + } + console.debug(`Fetch succeeded with encrypted image ${request.url}, trying to decrypt`); + + if (!Decryptor.isInitialized()) { + console.debug(`Service worker not initialized on fetch event`); + return Decryptor.errorResponse.clone(); + } + + let decrypted_blob; + try { + decrypted_blob = new Blob( + [await self.decryptor._decrypt(encrypted_arraybuffer)], + {type: encrypted_blob.type} ); - } else { - self.decryptor - .dispatchJob(method, args) - .then((reply) => { - reply.id = id; - self.postMessage(reply); - }); + } catch (error) { + console.debug(`Decryption failed for ${request.url}: ${error.message}`); + console.error("Corrupted data??? This shouldn't occur."); + return Decryptor.errorResponse.clone(); } + + const decrypted_response = new Response( + decrypted_blob, + { + status: response.status, + statusText: response.statusText, + headers: response.headers + } + ); + decrypted_response.headers.set("content-length", decrypted_blob.size); + + const decrypted_response_clone = decrypted_response.clone(); + const cache = await caches.open("v1"); + cache.put(request, decrypted_response_clone); + + console.debug(`Responding with decrypted response ${request.url}`); + return decrypted_response; + } + + static onServiceWorkerFetch(e) { + e.respondWith(Decryptor._swHandleFetch(e)); } } -Decryptor.imagePlaceholderURL = URL.createObjectURL(new Blob([ +Decryptor.MAGIC_STRING = "_e_n_c_r_y_p_t_e_d_"; +Decryptor.MAGIC_STRING_ARRAYBUFFER = (new TextEncoder("utf-8")).encode(Decryptor.MAGIC_STRING); +Decryptor.IV_LENGTH = 12; +Decryptor.keyCheckURL = "static/keycheck.txt"; +Decryptor.imagePlaceholderBlob = new Blob([ ` - background - - - - + background + + + + - Layer 1 - Could not - load - image + Layer 1 + Could not + load + image -`], {type: "image/svg+xml"})); +`], {type: "image/svg+xml"}); + +Decryptor.errorResponse = new Response( + Decryptor.imagePlaceholderBlob, + { + status: 200, + statusText: "OK", + headers: { + "content-type": "image/svg+xml" + } + } +); diff --git a/sigal/plugins/encrypt/static/keycheck.txt b/sigal/plugins/encrypt/static/keycheck.txt new file mode 100644 index 0000000..227ac90 --- /dev/null +++ b/sigal/plugins/encrypt/static/keycheck.txt @@ -0,0 +1 @@ +This file will be decrypted to test if the password supplied by the user is correct. diff --git a/sigal/plugins/encrypt/static/sw.js b/sigal/plugins/encrypt/static/sw.js new file mode 100644 index 0000000..cf389d3 --- /dev/null +++ b/sigal/plugins/encrypt/static/sw.js @@ -0,0 +1,7 @@ +"use strict" +importScripts("static/decrypt.js"); +oninstall = Decryptor.onServiceWorkerInstall; +onactivate = Decryptor.onServiceWorkerActivate; +onfetch = Decryptor.onServiceWorkerFetch; +onmessage = Decryptor.onServiceWorkerMesssage; +Decryptor.init({}); \ No newline at end of file diff --git a/sigal/themes/default/templates/decrypt.html b/sigal/themes/default/templates/decrypt.html index 39146ab..0d7faf5 100644 --- a/sigal/themes/default/templates/decrypt.html +++ b/sigal/themes/default/templates/decrypt.html @@ -3,7 +3,7 @@