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([
``], {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 @@