Rewrite with service worker
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
@@ -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([
|
||||
`<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="#ffffff" id="canvas_background" height="202" width="202" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
<title>background</title>
|
||||
<rect fill="#ffffff" id="canvas_background" height="202" width="202" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="36" id="svg_1" y="61.949997" x="22.958336" stroke-width="0" stroke="#000" fill="#7f7f7f">Could not</text>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="36" id="svg_4" y="112.600002" x="65.974998" stroke-width="0" stroke="#000" fill="#7f7f7f">load</text>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="36" id="svg_5" y="162.949997" x="50.983334" stroke-width="0" stroke="#000" fill="#7f7f7f">image</text>
|
||||
<title>Layer 1</title>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="36" id="svg_1" y="61.949997" x="22.958336" stroke-width="0" stroke="#000" fill="#7f7f7f">Could not</text>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="36" id="svg_4" y="112.600002" x="65.974998" stroke-width="0" stroke="#000" fill="#7f7f7f">load</text>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="36" id="svg_5" y="162.949997" x="50.983334" stroke-width="0" stroke="#000" fill="#7f7f7f">image</text>
|
||||
</g>
|
||||
</svg>`], {type: "image/svg+xml"}));
|
||||
</svg>`], {type: "image/svg+xml"});
|
||||
|
||||
Decryptor.errorResponse = new Response(
|
||||
Decryptor.imagePlaceholderBlob,
|
||||
{
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {
|
||||
"content-type": "image/svg+xml"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
1
sigal/plugins/encrypt/static/keycheck.txt
Normal file
1
sigal/plugins/encrypt/static/keycheck.txt
Normal file
@@ -0,0 +1 @@
|
||||
This file will be decrypted to test if the password supplied by the user is correct.
|
||||
7
sigal/plugins/encrypt/static/sw.js
Normal file
7
sigal/plugins/encrypt/static/sw.js
Normal file
@@ -0,0 +1,7 @@
|
||||
"use strict"
|
||||
importScripts("static/decrypt.js");
|
||||
oninstall = Decryptor.onServiceWorkerInstall;
|
||||
onactivate = Decryptor.onServiceWorkerActivate;
|
||||
onfetch = Decryptor.onServiceWorkerFetch;
|
||||
onmessage = Decryptor.onServiceWorkerMesssage;
|
||||
Decryptor.init({});
|
||||
@@ -3,7 +3,7 @@
|
||||
<script>
|
||||
Decryptor.init({
|
||||
password: "{{ encrypt_options.filtered_password }}",
|
||||
worker_script: "{{ theme.url }}/decrypt-worker.js",
|
||||
sw_script: "{{ theme.url }}/../sw.js",
|
||||
galleryId: "{{ encrypt_options.galleryId }}",
|
||||
gcm_tag: "{{ encrypt_options.escaped_gcm_tag }}",
|
||||
kdf_salt: "{{ encrypt_options.escaped_kdf_salt }}",
|
||||
|
||||
Reference in New Issue
Block a user