2021-09-15 02:16:27 +05:00
|
|
|
import localforage from "localforage";
|
2021-10-10 18:56:05 +05:00
|
|
|
import { createXXHash3, xxhash3 } from "hash-wasm";
|
2021-09-20 12:10:08 +05:00
|
|
|
import axios from "axios";
|
2021-09-26 11:46:50 +05:00
|
|
|
import { AppEventManager, AppEvents } from "../common";
|
2021-10-10 18:56:05 +05:00
|
|
|
// eslint-disable-next-line import/no-webpack-loader-syntax
|
|
|
|
|
import "worker-loader!nncryptoworker/dist/src/worker.js";
|
|
|
|
|
import { StreamableFS } from "streamablefs";
|
|
|
|
|
import NNCrypto from "./nncrypto.stub";
|
2021-09-15 02:16:27 +05:00
|
|
|
|
2021-09-26 11:46:50 +05:00
|
|
|
const PLACEHOLDER = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMzUuNDcgMTM1LjQ3Ij48ZyBmaWxsPSJncmF5Ij48cGF0aCBkPSJNNjUuNjMgNjUuODZhNC40OCA0LjQ4IDAgMSAwLS4wMS04Ljk2IDQuNDggNC40OCAwIDAgMCAwIDguOTZ6bTAtNi4zM2ExLjg1IDEuODUgMCAxIDEgMCAzLjcgMS44NSAxLjg1IDAgMCAxIDAtMy43em0wIDAiLz48cGF0aCBkPSJNODguNDkgNDguNTNINDYuOThjLS45IDAtMS42NC43My0xLjY0IDEuNjRWODUuM2MwIC45Ljc0IDEuNjQgMS42NCAxLjY0aDQxLjVjLjkxIDAgMS42NC0uNzQgMS42NC0xLjY0VjUwLjE3YzAtLjktLjczLTEuNjQtMS42My0xLjY0Wm0tLjk5IDIuNjJ2MjAuNzdsLTguMjUtOC4yNWExLjM4IDEuMzggMCAwIDAtMS45NSAwTDY1LjYzIDc1LjM0bC03LjQ2LTcuNDZhMS4zNyAxLjM3IDAgMCAwLTEuOTUgMGwtOC4yNSA4LjI1VjUxLjE1Wk00Ny45NyA4NC4zMXYtNC40N2w5LjIyLTkuMjIgNy40NiA3LjQ1YTEuMzcgMS4zNyAwIDAgMCAxLjk1IDBMNzguMjcgNjYuNGw5LjIzIDkuMjN2OC42OHptMCAwIi8+PC9nPjwvc3ZnPg==`;
|
2021-10-10 18:56:05 +05:00
|
|
|
const crypto = new NNCrypto("/static/js/bundle.worker.js");
|
|
|
|
|
const streamablefs = new StreamableFS("streamable-fs");
|
2021-09-15 02:16:27 +05:00
|
|
|
const fs = localforage.createInstance({
|
|
|
|
|
storeName: "notesnook-fs",
|
|
|
|
|
name: "NotesnookFS",
|
|
|
|
|
driver: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fs.hasItem = async function (key) {
|
|
|
|
|
const keys = await fs.keys();
|
|
|
|
|
return keys.includes(key);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* We perform 4 steps here:
|
|
|
|
|
* 1. We convert base64 to Uint8Array (if we get base64, that is)
|
|
|
|
|
* 2. We hash the Uint8Array.
|
|
|
|
|
* 3. We encrypt the Uint8Array
|
|
|
|
|
* 4. We save the encrypted Uint8Array
|
2021-10-10 18:56:05 +05:00
|
|
|
* @param {File} file
|
2021-09-15 02:16:27 +05:00
|
|
|
*/
|
2021-10-10 18:56:05 +05:00
|
|
|
async function* writeEncryptedFile(file, key) {
|
|
|
|
|
if (!localforage.supports(localforage.INDEXEDDB))
|
|
|
|
|
throw new Error("This browser does not support IndexedDB.");
|
|
|
|
|
|
|
|
|
|
const reader = file.stream().getReader();
|
|
|
|
|
const { hash, type: hashType } = await hashStream(reader);
|
|
|
|
|
reader.releaseLock();
|
|
|
|
|
|
|
|
|
|
yield { hash, hashType };
|
2021-09-15 02:16:27 +05:00
|
|
|
|
2021-10-10 18:56:05 +05:00
|
|
|
let offset = 0;
|
|
|
|
|
let CHUNK_SIZE = 5 * 1024 * 1024;
|
|
|
|
|
|
|
|
|
|
const fileHandle = await streamablefs.createFile(hash, file.size, file.type);
|
|
|
|
|
|
|
|
|
|
const iv = await crypto.encryptStream(
|
|
|
|
|
key,
|
|
|
|
|
{
|
|
|
|
|
read: async () => {
|
|
|
|
|
let end = Math.min(offset + CHUNK_SIZE, file.size);
|
|
|
|
|
if (offset === end) return;
|
|
|
|
|
const chunk = new Uint8Array(
|
|
|
|
|
await file.slice(offset, end).arrayBuffer()
|
|
|
|
|
);
|
|
|
|
|
offset = end;
|
|
|
|
|
const isFinal = offset === file.size;
|
|
|
|
|
return {
|
|
|
|
|
final: isFinal,
|
|
|
|
|
data: chunk,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
write: (chunk) => fileHandle.write(chunk),
|
|
|
|
|
},
|
|
|
|
|
file.name
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
hash,
|
|
|
|
|
hashType,
|
|
|
|
|
iv: iv,
|
|
|
|
|
length: file.size,
|
|
|
|
|
salt: key.salt,
|
|
|
|
|
alg: "xcha-stream",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* We perform 4 steps here:
|
|
|
|
|
* 1. We convert base64 to Uint8Array (if we get base64, that is)
|
|
|
|
|
* 2. We hash the Uint8Array.
|
|
|
|
|
* 3. We encrypt the Uint8Array
|
|
|
|
|
* 4. We save the encrypted Uint8Array
|
|
|
|
|
*/
|
|
|
|
|
async function writeEncrypted(filename, { data, type, key, hash }) {
|
2021-10-04 14:04:29 +05:00
|
|
|
const saveAsBuffer = localforage.supports(localforage.INDEXEDDB);
|
|
|
|
|
if (type === "base64") data = new Uint8Array(Buffer.from(data, "base64"));
|
2021-09-15 02:16:27 +05:00
|
|
|
|
2021-10-10 18:56:05 +05:00
|
|
|
if (!hash) hash = await hashBuffer(data);
|
|
|
|
|
if (!filename) filename = hash;
|
|
|
|
|
|
|
|
|
|
if (await fs.hasItem(filename)) return {};
|
|
|
|
|
|
|
|
|
|
const output = await crypto.encrypt(
|
|
|
|
|
key,
|
|
|
|
|
{
|
|
|
|
|
data,
|
|
|
|
|
format: "uint8array",
|
|
|
|
|
},
|
|
|
|
|
saveAsBuffer ? "uint8array" : "base64"
|
|
|
|
|
);
|
2021-09-15 02:16:27 +05:00
|
|
|
|
|
|
|
|
await fs.setItem(filename, output.cipher);
|
|
|
|
|
return {
|
|
|
|
|
iv: output.iv,
|
|
|
|
|
length: output.length,
|
|
|
|
|
salt: output.salt,
|
|
|
|
|
alg: output.alg,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-04 14:04:29 +05:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {import("hash-wasm/dist/lib/util").IDataType} data
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2021-09-15 02:16:27 +05:00
|
|
|
async function hashBuffer(data) {
|
|
|
|
|
return {
|
2021-09-23 15:18:11 +05:00
|
|
|
hash: await xxhash3(data),
|
2021-09-21 12:28:11 +05:00
|
|
|
type: "xxh3",
|
2021-09-15 02:16:27 +05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-10 18:56:05 +05:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {ReadableStreamReader<Uint8Array>} reader
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
async function hashStream(reader) {
|
|
|
|
|
const hasher = await createXXHash3();
|
|
|
|
|
hasher.init();
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const { value } = await reader.read();
|
|
|
|
|
if (!value) break;
|
|
|
|
|
hasher.update(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { type: "xxh3", hash: hasher.digest("hex") };
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-15 02:16:27 +05:00
|
|
|
async function readEncrypted(filename, key, cipherData) {
|
2021-09-20 12:10:08 +05:00
|
|
|
console.log("Reading encrypted file", filename);
|
2021-10-10 18:56:05 +05:00
|
|
|
|
2021-09-15 02:16:27 +05:00
|
|
|
const readAsBuffer = localforage.supports(localforage.INDEXEDDB);
|
|
|
|
|
cipherData.cipher = await fs.getItem(filename);
|
2021-10-10 18:56:05 +05:00
|
|
|
cipherData.format = readAsBuffer ? "uint8array" : "base64";
|
|
|
|
|
|
2021-09-26 11:46:50 +05:00
|
|
|
if (!cipherData.cipher) {
|
|
|
|
|
console.error(`File not found. Filename: ${filename}`);
|
2021-10-10 18:56:05 +05:00
|
|
|
return null;
|
2021-09-26 11:46:50 +05:00
|
|
|
}
|
2021-09-15 02:16:27 +05:00
|
|
|
|
2021-10-10 18:56:05 +05:00
|
|
|
return await crypto.decrypt(key, cipherData, cipherData.outputType);
|
2021-09-15 02:16:27 +05:00
|
|
|
}
|
|
|
|
|
|
2021-09-20 12:10:08 +05:00
|
|
|
async function uploadFile(filename, requestOptions) {
|
|
|
|
|
console.log("Request to upload file", filename, requestOptions);
|
2021-09-29 09:54:36 +05:00
|
|
|
const { url, cancellationToken } = requestOptions;
|
2021-09-20 12:10:08 +05:00
|
|
|
|
|
|
|
|
let cipher = await fs.getItem(filename);
|
|
|
|
|
if (!cipher) throw new Error(`File not found. Filename: ${filename}`);
|
|
|
|
|
|
|
|
|
|
const readAsBuffer = localforage.supports(localforage.INDEXEDDB);
|
|
|
|
|
if (!readAsBuffer)
|
|
|
|
|
cipher = Uint8Array.from(window.atob(cipher), (c) => c.charCodeAt(0));
|
|
|
|
|
|
|
|
|
|
const response = await axios.request({
|
|
|
|
|
url: url,
|
|
|
|
|
method: "PUT",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "",
|
|
|
|
|
},
|
2021-09-29 09:54:36 +05:00
|
|
|
cancelToken: cancellationToken,
|
2021-09-20 12:10:08 +05:00
|
|
|
data: new Blob([cipher.buffer]),
|
|
|
|
|
onUploadProgress: (ev) => {
|
|
|
|
|
console.log("Uploading file", filename, ev);
|
2021-09-26 11:46:50 +05:00
|
|
|
AppEventManager.publish(AppEvents.UPDATE_ATTACHMENT_PROGRESS, {
|
|
|
|
|
type: "upload",
|
|
|
|
|
hash: filename,
|
|
|
|
|
total: ev.total,
|
|
|
|
|
loaded: ev.loaded,
|
|
|
|
|
});
|
2021-09-20 12:10:08 +05:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log("File uploaded:", filename, response);
|
|
|
|
|
return isSuccessStatusCode(response.status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function downloadFile(filename, requestOptions) {
|
2021-09-29 09:54:36 +05:00
|
|
|
const { url, headers, cancellationToken } = requestOptions;
|
2021-09-20 12:10:08 +05:00
|
|
|
console.log("Request to download file", filename, url, headers);
|
|
|
|
|
if (await fs.hasItem(filename)) return true;
|
|
|
|
|
|
|
|
|
|
const response = await axios.get(url, {
|
|
|
|
|
headers: headers,
|
|
|
|
|
responseType: "blob",
|
2021-09-29 09:54:36 +05:00
|
|
|
cancelToken: cancellationToken,
|
2021-09-20 12:10:08 +05:00
|
|
|
onDownloadProgress: (ev) => {
|
|
|
|
|
console.log("Downloading file", filename, ev);
|
2021-09-26 11:46:50 +05:00
|
|
|
AppEventManager.publish(AppEvents.UPDATE_ATTACHMENT_PROGRESS, {
|
|
|
|
|
type: "download",
|
|
|
|
|
hash: filename,
|
|
|
|
|
total: ev.total,
|
|
|
|
|
loaded: ev.loaded,
|
|
|
|
|
});
|
2021-09-20 12:10:08 +05:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
console.log("File downloaded", filename, url, response);
|
|
|
|
|
if (!isSuccessStatusCode(response.status)) return false;
|
|
|
|
|
const blob = new Blob([response.data]);
|
|
|
|
|
await fs.setItem(filename, new Uint8Array(await blob.arrayBuffer()));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-21 12:28:11 +05:00
|
|
|
async function deleteFile(filename, requestOptions) {
|
2021-09-29 09:54:36 +05:00
|
|
|
const { url, headers, cancellationToken } = requestOptions;
|
2021-09-21 12:28:11 +05:00
|
|
|
console.log("Request to delete file", filename, url, headers);
|
|
|
|
|
if (!(await fs.hasItem(filename))) return true;
|
|
|
|
|
|
|
|
|
|
const response = await axios.delete(url, {
|
2021-09-29 09:54:36 +05:00
|
|
|
cancelToken: cancellationToken,
|
2021-09-21 12:28:11 +05:00
|
|
|
headers: headers,
|
|
|
|
|
});
|
|
|
|
|
const result = isSuccessStatusCode(response.status);
|
2021-09-29 09:54:36 +05:00
|
|
|
if (result) await fs.removeItem(filename);
|
2021-09-21 12:28:11 +05:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-26 11:46:50 +05:00
|
|
|
function exists(filename) {
|
|
|
|
|
return fs.hasItem(filename);
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-21 12:28:11 +05:00
|
|
|
const FS = {
|
|
|
|
|
writeEncrypted,
|
|
|
|
|
readEncrypted,
|
2021-09-29 09:54:36 +05:00
|
|
|
uploadFile: cancellable(uploadFile),
|
|
|
|
|
downloadFile: cancellable(downloadFile),
|
2021-09-21 12:28:11 +05:00
|
|
|
deleteFile,
|
2021-09-26 11:46:50 +05:00
|
|
|
exists,
|
2021-10-04 14:04:29 +05:00
|
|
|
hashBuffer,
|
2021-10-10 18:56:05 +05:00
|
|
|
hashStream,
|
|
|
|
|
writeEncryptedFile,
|
2021-09-21 12:28:11 +05:00
|
|
|
};
|
2021-09-15 02:16:27 +05:00
|
|
|
export default FS;
|
2021-09-20 12:10:08 +05:00
|
|
|
|
|
|
|
|
function isSuccessStatusCode(statusCode) {
|
|
|
|
|
return statusCode >= 200 && statusCode <= 299;
|
|
|
|
|
}
|
2021-09-29 09:54:36 +05:00
|
|
|
|
|
|
|
|
function cancellable(operation) {
|
|
|
|
|
return function (filename, requestOptions) {
|
2021-10-04 14:04:29 +05:00
|
|
|
const source = axios.CancelToken.source();
|
2021-09-29 09:54:36 +05:00
|
|
|
requestOptions.cancellationToken = source.token;
|
|
|
|
|
return {
|
|
|
|
|
execute: () => operation(filename, requestOptions),
|
|
|
|
|
cancel: (message) => source.cancel(message),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}
|