mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
feat: add support for encrypting large files safely
This commit is contained in:
2
apps/web/.gitignore
vendored
2
apps/web/.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"clipboard": "^2.0.6",
|
||||
"cogo-toast": "^4.2.3",
|
||||
"compressorjs": "^1.0.7",
|
||||
"cryptofs": "file:packages/nncryptoworker",
|
||||
"currency-symbol-map": "^5.0.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"emotion-theming": "^10.0.19",
|
||||
@@ -32,6 +33,8 @@
|
||||
"just-debounce": "^1.1.0",
|
||||
"localforage": "^1.10.0",
|
||||
"localforage-getitems": "https://github.com/thecodrr/localForage-getItems.git",
|
||||
"nncrypto": "file:packages/nncrypto",
|
||||
"nncryptoworker": "file:packages/nncryptoworker",
|
||||
"notes-core": "npm:@streetwriters/notesnook-core@latest",
|
||||
"print-js": "^1.6.0",
|
||||
"qclone": "^1.0.4",
|
||||
@@ -48,10 +51,13 @@
|
||||
"react-virtualized-auto-sizer": "^1.0.4",
|
||||
"react-virtuoso": "^1.9.3",
|
||||
"rebass": "^4.0.7",
|
||||
"streamablefs": "file:packages/streamablefs",
|
||||
"streamsaver": "^2.0.5",
|
||||
"threads": "^1.7.0",
|
||||
"timeago-react": "^3.0.2",
|
||||
"tinymce": "5.8.1",
|
||||
"uzip": "^0.20201231.0",
|
||||
"worker-loader": "^3.0.8",
|
||||
"wouter": "^2.7.3",
|
||||
"xxhash-wasm": "^0.4.2",
|
||||
"zustand": "^3.3.1"
|
||||
|
||||
101
apps/web/packages/nncrypto/index.ts
Normal file
101
apps/web/packages/nncrypto/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { ready } from "libsodium-wrappers";
|
||||
import Decryption from "./src/decryption";
|
||||
import Encryption from "./src/encryption";
|
||||
import { INNCrypto, IStreamable } from "./src/interfaces";
|
||||
import KeyUtils from "./src/keyutils";
|
||||
import Password from "./src/password";
|
||||
import {
|
||||
Cipher,
|
||||
EncryptionKey,
|
||||
OutputFormat,
|
||||
Plaintext,
|
||||
SerializedKey,
|
||||
} from "./src/types";
|
||||
|
||||
export class NNCrypto implements INNCrypto {
|
||||
private isReady: boolean = false;
|
||||
|
||||
private async init() {
|
||||
if (this.isReady) return;
|
||||
await ready;
|
||||
this.isReady = true;
|
||||
}
|
||||
|
||||
async encrypt(
|
||||
key: SerializedKey,
|
||||
plaintext: Plaintext,
|
||||
outputFormat: OutputFormat = "uint8array"
|
||||
): Promise<Cipher> {
|
||||
await this.init();
|
||||
return Encryption.encrypt(key, plaintext, outputFormat);
|
||||
}
|
||||
|
||||
async decrypt(
|
||||
key: SerializedKey,
|
||||
cipherData: Cipher,
|
||||
outputFormat: OutputFormat = "text"
|
||||
): Promise<Plaintext> {
|
||||
await this.init();
|
||||
return Decryption.decrypt(key, cipherData, outputFormat);
|
||||
}
|
||||
|
||||
async hash(password: string, salt: string): Promise<string> {
|
||||
await this.init();
|
||||
return Password.hash(password, salt);
|
||||
}
|
||||
|
||||
async deriveKey(password: string, salt: string): Promise<EncryptionKey> {
|
||||
await this.init();
|
||||
return KeyUtils.deriveKey(password, salt);
|
||||
}
|
||||
|
||||
async exportKey(password: string, salt: string): Promise<string> {
|
||||
await this.init();
|
||||
return KeyUtils.exportKey(password, salt);
|
||||
}
|
||||
|
||||
async createEncryptionStream(
|
||||
key: SerializedKey,
|
||||
stream: IStreamable
|
||||
): Promise<string> {
|
||||
await this.init();
|
||||
const encryptionStream = Encryption.createStream(key);
|
||||
while (true) {
|
||||
const chunk = await stream.read();
|
||||
if (!chunk) break;
|
||||
|
||||
const { data, final } = chunk;
|
||||
await stream.write(encryptionStream.write(data, final));
|
||||
|
||||
if (final) break;
|
||||
}
|
||||
return encryptionStream.header;
|
||||
}
|
||||
|
||||
async createDecryptionStream(
|
||||
iv: string,
|
||||
key: SerializedKey,
|
||||
stream: IStreamable
|
||||
) {
|
||||
await this.init();
|
||||
const decryptionStream = Decryption.createStream(iv, key);
|
||||
while (true) {
|
||||
const chunk = await stream.read();
|
||||
if (!chunk) break;
|
||||
|
||||
const { data, final } = chunk;
|
||||
await stream.write(decryptionStream.read(data));
|
||||
|
||||
if (!final) break;
|
||||
}
|
||||
}
|
||||
|
||||
async encryptStream(
|
||||
key: SerializedKey,
|
||||
stream: IStreamable,
|
||||
_filename?: string
|
||||
): Promise<string> {
|
||||
await this.init();
|
||||
return await this.createEncryptionStream(key, stream);
|
||||
}
|
||||
}
|
||||
25
apps/web/packages/nncrypto/package.json
Normal file
25
apps/web/packages/nncrypto/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "nncrypto",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"es": "esbuild ./js/index.js --external:path --external:crypto --minify --bundle --outdir=./dist --platform=browser",
|
||||
"build": "tsc --declaration --outDir ./dist",
|
||||
"prod": "NODE_ENV=production webpack",
|
||||
"dev": "NODE_ENV=development webpack",
|
||||
"check": "tsc --noEmit",
|
||||
"types": "tsc --declaration --emitDeclarationOnly --outDir ./dist/"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"libsodium-wrappers": "^0.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"streamablefs": "file:../streamablefs",
|
||||
"@types/libsodium-wrappers": "^0.7.9"
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
79
apps/web/packages/nncrypto/src/decryption.ts
Normal file
79
apps/web/packages/nncrypto/src/decryption.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
crypto_aead_xchacha20poly1305_ietf_decrypt,
|
||||
crypto_secretstream_xchacha20poly1305_init_pull,
|
||||
crypto_secretstream_xchacha20poly1305_pull,
|
||||
to_base64,
|
||||
from_base64,
|
||||
base64_variants,
|
||||
StateAddress,
|
||||
to_string,
|
||||
} from "libsodium-wrappers";
|
||||
import KeyUtils from "./keyutils";
|
||||
import {
|
||||
Cipher,
|
||||
EncryptionKey,
|
||||
OutputFormat,
|
||||
Plaintext,
|
||||
SerializedKey,
|
||||
} from "./types";
|
||||
|
||||
export default class Decryption {
|
||||
static decrypt(
|
||||
key: SerializedKey,
|
||||
cipherData: Cipher,
|
||||
outputFormat: OutputFormat = "text"
|
||||
): Plaintext {
|
||||
const encryptionKey = KeyUtils.transform(key);
|
||||
let input: Uint8Array = cipherData.cipher as Uint8Array;
|
||||
if (
|
||||
typeof cipherData.cipher === "string" &&
|
||||
cipherData.format === "base64"
|
||||
) {
|
||||
input = from_base64(
|
||||
cipherData.cipher,
|
||||
base64_variants.URLSAFE_NO_PADDING
|
||||
);
|
||||
}
|
||||
|
||||
const plaintext = crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
null,
|
||||
input,
|
||||
null,
|
||||
from_base64(cipherData.iv),
|
||||
encryptionKey.key
|
||||
);
|
||||
|
||||
return {
|
||||
format: outputFormat,
|
||||
data:
|
||||
outputFormat === "base64"
|
||||
? to_base64(plaintext, base64_variants.ORIGINAL)
|
||||
: outputFormat === "text"
|
||||
? to_string(plaintext)
|
||||
: plaintext,
|
||||
};
|
||||
}
|
||||
|
||||
static createStream(header: string, key: SerializedKey): DecryptionStream {
|
||||
return new DecryptionStream(header, KeyUtils.transform(key));
|
||||
}
|
||||
}
|
||||
|
||||
class DecryptionStream {
|
||||
state: StateAddress;
|
||||
constructor(header: string, key: EncryptionKey) {
|
||||
this.state = crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
from_base64(header),
|
||||
key.key
|
||||
);
|
||||
}
|
||||
|
||||
read(chunk: Uint8Array): Uint8Array {
|
||||
const { message } = crypto_secretstream_xchacha20poly1305_pull(
|
||||
this.state,
|
||||
chunk,
|
||||
null
|
||||
);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
95
apps/web/packages/nncrypto/src/encryption.ts
Normal file
95
apps/web/packages/nncrypto/src/encryption.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
crypto_aead_xchacha20poly1305_ietf_encrypt,
|
||||
crypto_secretstream_xchacha20poly1305_init_push,
|
||||
crypto_secretstream_xchacha20poly1305_push,
|
||||
randombytes_buf,
|
||||
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES,
|
||||
crypto_secretstream_xchacha20poly1305_TAG_FINAL,
|
||||
crypto_secretstream_xchacha20poly1305_TAG_MESSAGE,
|
||||
to_base64,
|
||||
from_base64,
|
||||
base64_variants,
|
||||
StateAddress,
|
||||
} from "libsodium-wrappers";
|
||||
import KeyUtils from "./keyutils";
|
||||
import {
|
||||
Cipher,
|
||||
EncryptionKey,
|
||||
OutputFormat,
|
||||
Plaintext,
|
||||
SerializedKey,
|
||||
} from "./types";
|
||||
|
||||
export default class Encryption {
|
||||
static encrypt(
|
||||
key: SerializedKey,
|
||||
plaintext: Plaintext,
|
||||
outputFormat: OutputFormat = "uint8array"
|
||||
): Cipher {
|
||||
const encryptionKey = KeyUtils.transform(key);
|
||||
|
||||
let data: Uint8Array | string = plaintext.data;
|
||||
if (typeof plaintext.data === "string" && plaintext.format === "base64") {
|
||||
data = from_base64(plaintext.data, base64_variants.ORIGINAL);
|
||||
}
|
||||
|
||||
const nonce = randombytes_buf(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
||||
|
||||
const cipher: string | Uint8Array =
|
||||
crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||
data,
|
||||
null,
|
||||
null,
|
||||
nonce,
|
||||
encryptionKey.key
|
||||
);
|
||||
|
||||
let output: string | Uint8Array = cipher;
|
||||
if (outputFormat === "base64") {
|
||||
output = to_base64(cipher, base64_variants.URLSAFE_NO_PADDING);
|
||||
}
|
||||
|
||||
const iv = to_base64(nonce);
|
||||
return {
|
||||
format: outputFormat,
|
||||
alg: getAlgorithm(base64_variants.URLSAFE_NO_PADDING),
|
||||
cipher: output,
|
||||
iv,
|
||||
salt: encryptionKey.salt,
|
||||
length: data.length,
|
||||
};
|
||||
}
|
||||
|
||||
static createStream(key: SerializedKey): EncryptionStream {
|
||||
return new EncryptionStream(KeyUtils.transform(key));
|
||||
}
|
||||
}
|
||||
|
||||
class EncryptionStream {
|
||||
state: StateAddress;
|
||||
header: string;
|
||||
constructor(key: EncryptionKey) {
|
||||
const { state, header } = crypto_secretstream_xchacha20poly1305_init_push(
|
||||
key.key,
|
||||
"base64"
|
||||
);
|
||||
this.state = state;
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
write(chunk: Uint8Array, final?: boolean): Uint8Array {
|
||||
return crypto_secretstream_xchacha20poly1305_push(
|
||||
this.state,
|
||||
chunk,
|
||||
null,
|
||||
final
|
||||
? crypto_secretstream_xchacha20poly1305_TAG_FINAL
|
||||
: crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getAlgorithm(base64Variant: base64_variants) {
|
||||
//Template: encryptionAlgorithm-kdfAlgorithm-base64variant
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
39
apps/web/packages/nncrypto/src/interfaces.ts
Normal file
39
apps/web/packages/nncrypto/src/interfaces.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Chunk } from "streamablefs/dist/src/types";
|
||||
import {
|
||||
Cipher,
|
||||
EncryptionKey,
|
||||
OutputFormat,
|
||||
Plaintext,
|
||||
SerializedKey,
|
||||
} from "./types";
|
||||
|
||||
export interface IStreamable {
|
||||
read(): Promise<Chunk | undefined>;
|
||||
write(chunk: Uint8Array): Promise<void>;
|
||||
}
|
||||
|
||||
export interface INNCrypto {
|
||||
encrypt(
|
||||
key: SerializedKey,
|
||||
plaintext: Plaintext,
|
||||
outputFormat?: OutputFormat
|
||||
): Promise<Cipher>;
|
||||
|
||||
decrypt(
|
||||
key: SerializedKey,
|
||||
cipherData: Cipher,
|
||||
outputFormat?: OutputFormat
|
||||
): Promise<Plaintext>;
|
||||
|
||||
hash(password: string, salt: string): Promise<string>;
|
||||
|
||||
deriveKey(password: string, salt: string): Promise<EncryptionKey>;
|
||||
|
||||
exportKey(password: string, salt: string): Promise<string>;
|
||||
|
||||
encryptStream(
|
||||
key: SerializedKey,
|
||||
stream: IStreamable,
|
||||
filename?: string
|
||||
): Promise<string>;
|
||||
}
|
||||
59
apps/web/packages/nncrypto/src/keyutils.ts
Normal file
59
apps/web/packages/nncrypto/src/keyutils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
from_base64,
|
||||
to_base64,
|
||||
randombytes_buf,
|
||||
crypto_pwhash,
|
||||
crypto_pwhash_SALTBYTES,
|
||||
crypto_pwhash_ALG_ARGON2I13,
|
||||
crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
|
||||
} from "libsodium-wrappers";
|
||||
import { EncryptionKey, SerializedKey } from "./types";
|
||||
|
||||
type KeyCipherData = { iv: Uint8Array; cipher: Uint8Array };
|
||||
|
||||
export default class KeyUtils {
|
||||
static deriveKey(password: string, salt: string): EncryptionKey {
|
||||
let saltBytes: Uint8Array;
|
||||
if (!salt) saltBytes = randombytes_buf(crypto_pwhash_SALTBYTES);
|
||||
else {
|
||||
saltBytes = from_base64(salt);
|
||||
}
|
||||
|
||||
if (!saltBytes)
|
||||
throw new Error("Could not generate bytes from the given salt.");
|
||||
|
||||
const key = crypto_pwhash(
|
||||
crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
|
||||
password,
|
||||
saltBytes,
|
||||
3, // operations limit
|
||||
1024 * 1024 * 8, // memory limit (8MB)
|
||||
crypto_pwhash_ALG_ARGON2I13
|
||||
);
|
||||
|
||||
return {
|
||||
key,
|
||||
salt: typeof salt === "string" ? salt : to_base64(saltBytes),
|
||||
};
|
||||
}
|
||||
|
||||
static exportKey(password: string, salt: string): string {
|
||||
const { key } = this.deriveKey(password, salt);
|
||||
return to_base64(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in either a password or a serialized encryption key
|
||||
* and spits out a key that can be directly used for encryption/decryption.
|
||||
* @param input
|
||||
*/
|
||||
static transform(input: SerializedKey): EncryptionKey {
|
||||
if ("password" in input && !!input.password) {
|
||||
const { password, salt } = input;
|
||||
return this.deriveKey(password, salt);
|
||||
} else if ("key" in input && !!input.key) {
|
||||
return { key: from_base64(input.key), salt: input.salt };
|
||||
}
|
||||
throw new Error("Invalid input.");
|
||||
}
|
||||
}
|
||||
22
apps/web/packages/nncrypto/src/password.ts
Normal file
22
apps/web/packages/nncrypto/src/password.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
crypto_generichash,
|
||||
crypto_pwhash,
|
||||
crypto_pwhash_ALG_ARGON2ID13,
|
||||
crypto_pwhash_SALTBYTES,
|
||||
} from "libsodium-wrappers";
|
||||
|
||||
export default class Password {
|
||||
static hash(password: string, salt: string): string {
|
||||
const saltBytes = crypto_generichash(crypto_pwhash_SALTBYTES, salt);
|
||||
const hash = crypto_pwhash(
|
||||
32,
|
||||
password,
|
||||
saltBytes,
|
||||
3, // operations limit
|
||||
1024 * 1024 * 64, // memory limit (8MB)
|
||||
crypto_pwhash_ALG_ARGON2ID13,
|
||||
"base64"
|
||||
);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
28
apps/web/packages/nncrypto/src/types.ts
Normal file
28
apps/web/packages/nncrypto/src/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { StringOutputFormat, Uint8ArrayOutputFormat } from "libsodium-wrappers";
|
||||
|
||||
export type OutputFormat = Uint8ArrayOutputFormat | StringOutputFormat;
|
||||
|
||||
export type Cipher = {
|
||||
format: OutputFormat;
|
||||
alg: string;
|
||||
cipher: string | Uint8Array;
|
||||
iv: string;
|
||||
salt: string;
|
||||
length: number;
|
||||
};
|
||||
|
||||
export type Plaintext = {
|
||||
format: OutputFormat;
|
||||
data: string | Uint8Array;
|
||||
};
|
||||
|
||||
export type SerializedKey = {
|
||||
password?: string;
|
||||
key?: string;
|
||||
salt: string;
|
||||
};
|
||||
|
||||
export type EncryptionKey = {
|
||||
key: Uint8Array;
|
||||
salt: string;
|
||||
};
|
||||
105
apps/web/packages/nncrypto/tsconfig.json
Normal file
105
apps/web/packages/nncrypto/tsconfig.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"lib": [
|
||||
"DOM"
|
||||
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs",
|
||||
// "module": "commonjs" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
//"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true /* Create source map files for emitted JavaScript files. */,
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"exclude": ["webpack.config.js", "dist", "node_modules"],
|
||||
"include": ["./index.ts", "./src/"]
|
||||
}
|
||||
28
apps/web/packages/nncrypto/webpack.config.js
Normal file
28
apps/web/packages/nncrypto/webpack.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./index.ts",
|
||||
mode: process.env.NODE_ENV === "production" ? "production" : "development",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
||||
fallback: { path: false, crypto: false },
|
||||
},
|
||||
output: {
|
||||
filename: "index.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
library: {
|
||||
type: "commonjs2",
|
||||
name: "NNCrypto",
|
||||
export: "NNCrypto",
|
||||
},
|
||||
},
|
||||
};
|
||||
114
apps/web/packages/nncryptoworker/index.ts
Normal file
114
apps/web/packages/nncryptoworker/index.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
SerializedKey,
|
||||
Plaintext,
|
||||
OutputFormat,
|
||||
Cipher,
|
||||
EncryptionKey,
|
||||
} from "nncrypto/dist/src/types";
|
||||
import { Chunk } from "streamablefs/dist/src/types";
|
||||
import { spawn, Worker } from "threads";
|
||||
import { NNCryptoWorkerModule } from "./src/worker";
|
||||
import { INNCrypto, IStreamable } from "nncrypto/dist/src/interfaces";
|
||||
|
||||
export class NNCryptoWorker implements INNCrypto {
|
||||
private worker?: Worker;
|
||||
private workermodule?: NNCryptoWorkerModule;
|
||||
private isReady: boolean = false;
|
||||
private path?: string;
|
||||
|
||||
constructor(path?: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
private async init() {
|
||||
if (!this.path) throw new Error("path cannot be undefined.");
|
||||
if (this.isReady) return;
|
||||
|
||||
this.worker = new Worker(this.path);
|
||||
this.workermodule = await spawn<NNCryptoWorkerModule>(this.worker);
|
||||
this.isReady = true;
|
||||
}
|
||||
|
||||
async encrypt(
|
||||
key: SerializedKey,
|
||||
plaintext: Plaintext,
|
||||
outputFormat: OutputFormat = "uint8array"
|
||||
): Promise<Cipher> {
|
||||
await this.init();
|
||||
if (!this.workermodule) throw new Error("Worker module is not ready.");
|
||||
|
||||
return this.workermodule.encrypt(key, plaintext, outputFormat);
|
||||
}
|
||||
|
||||
async decrypt(
|
||||
key: SerializedKey,
|
||||
cipherData: Cipher,
|
||||
outputFormat: OutputFormat = "text"
|
||||
): Promise<Plaintext> {
|
||||
await this.init();
|
||||
if (!this.workermodule) throw new Error("Worker module is not ready.");
|
||||
|
||||
return this.workermodule.decrypt(key, cipherData, outputFormat);
|
||||
}
|
||||
|
||||
async hash(password: string, salt: string): Promise<string> {
|
||||
await this.init();
|
||||
if (!this.workermodule) throw new Error("Worker module is not ready.");
|
||||
|
||||
return this.workermodule.hash(password, salt);
|
||||
}
|
||||
|
||||
async deriveKey(password: string, salt: string): Promise<EncryptionKey> {
|
||||
await this.init();
|
||||
if (!this.workermodule) throw new Error("Worker module is not ready.");
|
||||
|
||||
return this.workermodule.deriveKey(password, salt);
|
||||
}
|
||||
|
||||
async exportKey(password: string, salt: string): Promise<string> {
|
||||
await this.init();
|
||||
if (!this.workermodule) throw new Error("Worker module is not ready.");
|
||||
|
||||
return this.workermodule.exportKey(password, salt);
|
||||
}
|
||||
|
||||
async encryptStream(
|
||||
key: SerializedKey,
|
||||
stream: IStreamable,
|
||||
streamId?: string
|
||||
): Promise<string> {
|
||||
if (!streamId) throw new Error("streamId is required.");
|
||||
await this.init();
|
||||
if (!this.workermodule) throw new Error("Worker module is not ready.");
|
||||
if (!this.worker) throw new Error("Worker is not ready.");
|
||||
|
||||
const readEventType = `${streamId}:read`;
|
||||
const writeEventType = `${streamId}:write`;
|
||||
const eventListener = {
|
||||
handleEvent: async (ev: MessageEvent) => {
|
||||
const { type } = ev.data;
|
||||
if (type === readEventType) {
|
||||
const chunk = await stream.read();
|
||||
if (!chunk || !this.worker) return;
|
||||
this.worker.postMessage({ type, data: chunk }, [chunk.data.buffer]);
|
||||
} else if (type === writeEventType) {
|
||||
const { data } = ev.data as Chunk;
|
||||
await stream.write(data);
|
||||
}
|
||||
},
|
||||
};
|
||||
this.worker.addEventListener("message", eventListener);
|
||||
const iv = await this.workermodule.createEncryptionStream(streamId, key);
|
||||
this.worker.removeEventListener("message", eventListener);
|
||||
return iv;
|
||||
}
|
||||
|
||||
// async createDecryptionStream(
|
||||
// iv: string,
|
||||
// key: SerializedKey,
|
||||
// stream: IStreamable
|
||||
// ) {
|
||||
// await this.init();
|
||||
// if (!this.workermodule) throw new Error("Worker module is not ready.");
|
||||
// }
|
||||
}
|
||||
20
apps/web/packages/nncryptoworker/package.json
Normal file
20
apps/web/packages/nncryptoworker/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "nncryptoworker",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --declaration --outDir ./dist"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"nncrypto": "file:../nncrypto",
|
||||
"streamablefs": "file:../streamablefs",
|
||||
"threads": "^1.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
21
apps/web/packages/nncryptoworker/src/utils.ts
Normal file
21
apps/web/packages/nncryptoworker/src/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
type Message<T> = {
|
||||
type: string;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
export function sendEventWithResult<T>(type: string, data?: any): Promise<T> {
|
||||
return new Promise<T>((resolve) => {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
addEventListener(
|
||||
"message",
|
||||
(ev: MessageEvent<Message<T>>) => {
|
||||
const { type: messageType, data } = ev.data;
|
||||
if (messageType === type && data) {
|
||||
resolve(data);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
postMessage({ type, data });
|
||||
});
|
||||
}
|
||||
24
apps/web/packages/nncryptoworker/src/worker.ts
Normal file
24
apps/web/packages/nncryptoworker/src/worker.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NNCrypto } from "nncrypto";
|
||||
import { SerializedKey } from "nncrypto/dist/src/types";
|
||||
import { expose } from "threads/worker";
|
||||
import WorkerStream from "./workerstream";
|
||||
|
||||
const crypto = new NNCrypto();
|
||||
|
||||
const module = {
|
||||
exportKey: crypto.exportKey.bind(crypto),
|
||||
deriveKey: crypto.deriveKey.bind(crypto),
|
||||
encrypt: crypto.encrypt.bind(crypto),
|
||||
decrypt: crypto.decrypt.bind(crypto),
|
||||
hash: crypto.hash.bind(crypto),
|
||||
createEncryptionStream: (id: string, key: SerializedKey) => {
|
||||
return crypto.createEncryptionStream(key, new WorkerStream(id));
|
||||
},
|
||||
createDecryptionStream: (id: string, iv: string, key: SerializedKey) => {
|
||||
return crypto.createDecryptionStream(iv, key, new WorkerStream(id));
|
||||
},
|
||||
};
|
||||
|
||||
export type NNCryptoWorkerModule = typeof module;
|
||||
|
||||
expose(module);
|
||||
44
apps/web/packages/nncryptoworker/src/workerstream.ts
Normal file
44
apps/web/packages/nncryptoworker/src/workerstream.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { IStreamable } from "nncrypto/dist/src/interfaces";
|
||||
import { Chunk } from "streamablefs/dist/src/types";
|
||||
import { sendEventWithResult } from "./utils";
|
||||
|
||||
export default class WorkerStream
|
||||
extends ReadableStream<Chunk>
|
||||
implements IStreamable
|
||||
{
|
||||
private id: string;
|
||||
private reader?: ReadableStreamReader<Chunk>;
|
||||
|
||||
constructor(streamId: string) {
|
||||
super(new WorkerStreamSource(streamId));
|
||||
this.id = streamId;
|
||||
}
|
||||
|
||||
async read(): Promise<Chunk | undefined> {
|
||||
if (!this.reader) this.reader = this.getReader();
|
||||
const { value } = await this.reader.read();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} chunk
|
||||
*/
|
||||
async write(chunk: Uint8Array): Promise<void> {
|
||||
postMessage({ type: `${this.id}:write`, data: chunk }, [chunk.buffer]);
|
||||
}
|
||||
}
|
||||
|
||||
class WorkerStreamSource implements UnderlyingSource<Chunk> {
|
||||
private id: string;
|
||||
constructor(streamId: string) {
|
||||
this.id = streamId;
|
||||
}
|
||||
|
||||
start(controller: ReadableStreamController<Chunk>) {}
|
||||
|
||||
async pull(controller: ReadableStreamController<Chunk>) {
|
||||
const chunk = await sendEventWithResult<Chunk>(`${this.id}:read`);
|
||||
controller.enqueue(chunk);
|
||||
if (chunk.final) controller.close();
|
||||
}
|
||||
}
|
||||
106
apps/web/packages/nncryptoworker/tsconfig.json
Normal file
106
apps/web/packages/nncryptoworker/tsconfig.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"WebWorker"
|
||||
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs",
|
||||
// "module": "commonjs" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
//"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true /* Create source map files for emitted JavaScript files. */,
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"exclude": ["webpack.config.js", "dist", "node_modules"],
|
||||
"include": ["./index.ts", "./src/"]
|
||||
}
|
||||
52
apps/web/packages/streamablefs/index.ts
Normal file
52
apps/web/packages/streamablefs/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import localforage from "localforage";
|
||||
import FileHandle from "./src/filehandle";
|
||||
import { IStreamableFS } from "./src/interfaces";
|
||||
import { File } from "./src/types";
|
||||
|
||||
export class StreamableFS implements IStreamableFS {
|
||||
private storage: LocalForage;
|
||||
|
||||
/**
|
||||
* @param db name of the indexeddb database
|
||||
*/
|
||||
constructor(db: string) {
|
||||
this.storage = localforage.createInstance({
|
||||
storeName: "streamable-fs",
|
||||
name: db,
|
||||
driver: [localforage.INDEXEDDB],
|
||||
});
|
||||
}
|
||||
|
||||
async createFile(
|
||||
filename: string,
|
||||
size: number,
|
||||
type: string
|
||||
): Promise<FileHandle> {
|
||||
if (await this.exists(filename)) throw new Error("File already exists.");
|
||||
|
||||
const file: File = await this.storage.setItem<File>(filename, {
|
||||
filename,
|
||||
size,
|
||||
type,
|
||||
chunks: 0,
|
||||
});
|
||||
return new FileHandle(this.storage, file);
|
||||
}
|
||||
|
||||
async readFile(filename: string): Promise<FileHandle> {
|
||||
const file = await this.storage.getItem<File>(filename);
|
||||
if (!file) throw new Error("File does not exist.");
|
||||
return new FileHandle(this.storage, file);
|
||||
}
|
||||
|
||||
async exists(filename: string): Promise<boolean> {
|
||||
const file = await this.storage.getItem<File>(filename);
|
||||
return !!file;
|
||||
}
|
||||
|
||||
async deleteFile(filename: string) {
|
||||
const handle = await this.readFile(filename);
|
||||
if (!handle) return;
|
||||
await handle.delete();
|
||||
}
|
||||
}
|
||||
19
apps/web/packages/streamablefs/package.json
Normal file
19
apps/web/packages/streamablefs/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "streamablefs",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc --declaration --outDir ./dist"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"localforage": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/localforage": "^0.0.34",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
40
apps/web/packages/streamablefs/src/filehandle.ts
Normal file
40
apps/web/packages/streamablefs/src/filehandle.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import FileStreamSource from "./filestreamsource";
|
||||
import { File } from "./types";
|
||||
|
||||
export default class FileHandle extends ReadableStream {
|
||||
private storage: LocalForage;
|
||||
private file: File;
|
||||
|
||||
constructor(storage: LocalForage, file: File) {
|
||||
super(new FileStreamSource(storage, file));
|
||||
|
||||
this.file = file;
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Uint8Array} chunk
|
||||
*/
|
||||
async write(chunk: Uint8Array) {
|
||||
await this.storage.setItem(this.getChunkKey(this.file.chunks++), chunk);
|
||||
await this.storage.setItem(this.file.filename, this.file);
|
||||
}
|
||||
|
||||
async addAdditionalData(key: string, value: any) {
|
||||
this.file.additionalData = this.file.additionalData || {};
|
||||
this.file.additionalData[key] = value;
|
||||
await this.storage.setItem(this.file.filename, this.file);
|
||||
}
|
||||
|
||||
async delete() {
|
||||
for (let i = 0; i < this.file.chunks; ++i) {
|
||||
await this.storage.removeItem(this.getChunkKey(i));
|
||||
}
|
||||
await this.storage.removeItem(this.file.filename);
|
||||
}
|
||||
|
||||
private getChunkKey(offset: number): string {
|
||||
return `${this.file.filename}-chunk-${offset}`;
|
||||
}
|
||||
}
|
||||
36
apps/web/packages/streamablefs/src/filestreamsource.ts
Normal file
36
apps/web/packages/streamablefs/src/filestreamsource.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Chunk, File } from "./types";
|
||||
|
||||
export default class FileStreamSource implements UnderlyingSource<Chunk> {
|
||||
private storage: LocalForage;
|
||||
private file: File;
|
||||
private offset: number = 0;
|
||||
|
||||
constructor(storage: LocalForage, file: File) {
|
||||
this.storage = storage;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
start(controller: ReadableStreamController<Chunk>) {}
|
||||
|
||||
async pull(controller: ReadableStreamController<Chunk>) {
|
||||
const data = await this.readChunk(this.offset++);
|
||||
const isFinalChunk = this.offset === this.file.chunks;
|
||||
|
||||
if (data)
|
||||
controller.enqueue({
|
||||
data,
|
||||
final: isFinalChunk,
|
||||
});
|
||||
|
||||
if (isFinalChunk || !data) controller.close();
|
||||
}
|
||||
|
||||
private readChunk(offset: number) {
|
||||
if (offset > this.file.chunks) return;
|
||||
return this.storage.getItem<Uint8Array>(this.getChunkKey(offset));
|
||||
}
|
||||
|
||||
private getChunkKey(offset: number): string {
|
||||
return `${this.file.filename}-chunk-${offset}`;
|
||||
}
|
||||
}
|
||||
8
apps/web/packages/streamablefs/src/interfaces.ts
Normal file
8
apps/web/packages/streamablefs/src/interfaces.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import FileHandle from "./filehandle";
|
||||
|
||||
export interface IStreamableFS {
|
||||
createFile(filename: string, size: number, type: string): Promise<FileHandle>;
|
||||
readFile(filename: string): Promise<FileHandle>;
|
||||
exists(filename: string): Promise<boolean>;
|
||||
deleteFile(filename: string): Promise<void>;
|
||||
}
|
||||
12
apps/web/packages/streamablefs/src/types.ts
Normal file
12
apps/web/packages/streamablefs/src/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type File = {
|
||||
filename: string;
|
||||
size: number;
|
||||
type: string;
|
||||
chunks: number;
|
||||
additionalData?: { [key: string]: any };
|
||||
};
|
||||
|
||||
export type Chunk = {
|
||||
data: Uint8Array;
|
||||
final: boolean;
|
||||
};
|
||||
105
apps/web/packages/streamablefs/tsconfig.json
Normal file
105
apps/web/packages/streamablefs/tsconfig.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"lib": [
|
||||
"DOM"
|
||||
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs",
|
||||
// "module": "commonjs" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
//"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true /* Create source map files for emitted JavaScript files. */,
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"exclude": ["webpack.config.js", "dist", "node_modules"],
|
||||
"include": ["./index.ts", "./src/"]
|
||||
}
|
||||
@@ -18,15 +18,17 @@ function onMessage(ev) {
|
||||
}
|
||||
case "encryptBinary": {
|
||||
const { passwordOrKey, data: _data } = data;
|
||||
const cipher = encrypt.call(
|
||||
context,
|
||||
passwordOrKey,
|
||||
_data,
|
||||
"uint8array"
|
||||
);
|
||||
const cipher = encryptStream.call(context, passwordOrKey, _data);
|
||||
sendMessage("encryptBinary", cipher, messageId, [cipher.cipher.buffer]);
|
||||
break;
|
||||
}
|
||||
case "decryptBinary": {
|
||||
const { passwordOrKey, cipher } = data;
|
||||
const output = decryptStream.call(context, passwordOrKey, cipher);
|
||||
const transferables = cipher.output === "base64" ? [] : [output.buffer];
|
||||
sendMessage("decryptBinary", output, messageId, transferables);
|
||||
break;
|
||||
}
|
||||
case "decrypt": {
|
||||
const { passwordOrKey, cipher } = data;
|
||||
const plainText = decrypt.call(context, passwordOrKey, cipher);
|
||||
@@ -166,10 +168,7 @@ const encrypt = (passwordOrKey, plainData, outputType) => {
|
||||
sodium.memzero(nonce);
|
||||
sodium.memzero(key);
|
||||
return {
|
||||
alg: getAlgorithm(
|
||||
sodium.base64_variants.URLSAFE_NO_PADDING,
|
||||
plainData.compress ? 1 : 0 // TODO: Crude but works (change this to a more exact boolean flag)
|
||||
),
|
||||
alg: getAlgorithm(sodium.base64_variants.URLSAFE_NO_PADDING),
|
||||
cipher,
|
||||
iv,
|
||||
salt,
|
||||
@@ -177,6 +176,117 @@ const encrypt = (passwordOrKey, plainData, outputType) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {{type: "plain" | "uint8array", data: string | Uint8Array}} plainData - the plaintext data
|
||||
*/
|
||||
const encryptStream = (passwordOrKey, plainData) => {
|
||||
const { sodium } = this;
|
||||
|
||||
if (plainData.type === "plain") {
|
||||
plainData.data = enc.encode(plainData.data);
|
||||
} else if (plainData.type === "base64") {
|
||||
plainData.data = sodium.from_base64(
|
||||
plainData.data,
|
||||
sodium.base64_variants.ORIGINAL
|
||||
);
|
||||
}
|
||||
|
||||
const { key, salt } = _getKey(passwordOrKey);
|
||||
|
||||
let res = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
|
||||
|
||||
const BLOCK_SIZE = 8 * 1024; // 8 KB
|
||||
const ENCRYPTED_BLOCK_SIZE =
|
||||
BLOCK_SIZE + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||
|
||||
let dataLength = plainData.data.byteLength;
|
||||
const REMAINDER = BLOCK_SIZE - (dataLength % BLOCK_SIZE);
|
||||
const TOTAL_BLOCKS = (dataLength + REMAINDER) / BLOCK_SIZE;
|
||||
|
||||
const ENCRYPTED_SIZE =
|
||||
dataLength +
|
||||
TOTAL_BLOCKS * sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||
|
||||
let buffer = new Uint8Array(ENCRYPTED_SIZE);
|
||||
|
||||
for (let i = 0; i < TOTAL_BLOCKS; ++i) {
|
||||
const start = i * BLOCK_SIZE;
|
||||
const isFinalBlock = start + BLOCK_SIZE >= dataLength;
|
||||
const end = isFinalBlock
|
||||
? start + (dataLength - start)
|
||||
: start + BLOCK_SIZE;
|
||||
|
||||
const block = plainData.data.slice(start, end);
|
||||
let encryptedBlock = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
res.state,
|
||||
block,
|
||||
null,
|
||||
isFinalBlock
|
||||
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
|
||||
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
|
||||
);
|
||||
buffer.set(encryptedBlock, i * ENCRYPTED_BLOCK_SIZE);
|
||||
}
|
||||
|
||||
const iv = sodium.to_base64(res.header);
|
||||
return {
|
||||
alg: getAlgorithm("nil"),
|
||||
cipher: buffer,
|
||||
iv,
|
||||
salt,
|
||||
length: dataLength,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {{salt: string, iv: string, cipher: Uint8Array}} cipher - the cipher data
|
||||
*/
|
||||
const decryptStream = (passwordOrKey, { iv, cipher, salt, output }) => {
|
||||
const { sodium } = this;
|
||||
const ABYTES = sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||
|
||||
const { key } = _getKey({ salt, ...passwordOrKey });
|
||||
const header = sodium.from_base64(iv);
|
||||
|
||||
const BLOCK_SIZE = 8 * 1024 + ABYTES;
|
||||
const DECRYPTED_BLOCK_SIZE = BLOCK_SIZE - ABYTES;
|
||||
|
||||
let dataLength = cipher.byteLength;
|
||||
const REMAINDER = BLOCK_SIZE - (dataLength % BLOCK_SIZE);
|
||||
const TOTAL_BLOCKS = (dataLength + REMAINDER) / BLOCK_SIZE;
|
||||
|
||||
const DECRYPTED_SIZE = dataLength - TOTAL_BLOCKS * ABYTES;
|
||||
|
||||
let buffer = new Uint8Array(DECRYPTED_SIZE);
|
||||
|
||||
let state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
header,
|
||||
key
|
||||
);
|
||||
for (let i = 0; i < TOTAL_BLOCKS; ++i) {
|
||||
const start = i * BLOCK_SIZE;
|
||||
const isFinalBlock = start + BLOCK_SIZE >= dataLength;
|
||||
const end = isFinalBlock
|
||||
? start + (dataLength - start)
|
||||
: start + BLOCK_SIZE;
|
||||
|
||||
const block = cipher.slice(start, end);
|
||||
let { message } = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
state,
|
||||
block
|
||||
);
|
||||
buffer.set(message, i * DECRYPTED_BLOCK_SIZE);
|
||||
}
|
||||
|
||||
return output === "base64"
|
||||
? sodium.to_base64(buffer, sodium.base64_variants.ORIGINAL)
|
||||
: buffer;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
|
||||
@@ -38,28 +38,23 @@ async function pickFile() {
|
||||
const selectedFile = await showFilePicker({ acceptedFileTypes: "*/*" });
|
||||
if (!selectedFile) return;
|
||||
|
||||
const buffer = await selectedFile.arrayBuffer();
|
||||
const { hash, type: hashType } = await fs.hashBuffer(Buffer.from(buffer));
|
||||
|
||||
const output = db.attachments.exists(hash)
|
||||
? {}
|
||||
: await fs.writeEncrypted(null, {
|
||||
data: new Uint8Array(buffer),
|
||||
type: "buffer",
|
||||
key,
|
||||
hash,
|
||||
});
|
||||
const generator = fs.writeEncryptedFile(selectedFile, key);
|
||||
|
||||
let { value: output } = await generator.next();
|
||||
if (!db.attachments.exists(output.hash)) {
|
||||
const { value } = await generator.next();
|
||||
output = value;
|
||||
}
|
||||
console.log(output);
|
||||
await db.attachments.add({
|
||||
...output,
|
||||
hash,
|
||||
hashType,
|
||||
salt: "sda",
|
||||
filename: selectedFile.name,
|
||||
type: selectedFile.type,
|
||||
});
|
||||
|
||||
return {
|
||||
hash,
|
||||
hash: output.hash,
|
||||
filename: selectedFile.name,
|
||||
type: selectedFile.type,
|
||||
size: selectedFile.size,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import NNCrypto from "./nncrypto/index";
|
||||
import localforage from "localforage";
|
||||
import { xxhash3 } from "hash-wasm";
|
||||
import { createXXHash3, xxhash3 } from "hash-wasm";
|
||||
import axios from "axios";
|
||||
import { AppEventManager, AppEvents } from "../common";
|
||||
// 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";
|
||||
|
||||
const PLACEHOLDER = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMzUuNDcgMTM1LjQ3Ij48ZyBmaWxsPSJncmF5Ij48cGF0aCBkPSJNNjUuNjMgNjUuODZhNC40OCA0LjQ4IDAgMSAwLS4wMS04Ljk2IDQuNDggNC40OCAwIDAgMCAwIDguOTZ6bTAtNi4zM2ExLjg1IDEuODUgMCAxIDEgMCAzLjcgMS44NSAxLjg1IDAgMCAxIDAtMy43em0wIDAiLz48cGF0aCBkPSJNODguNDkgNDguNTNINDYuOThjLS45IDAtMS42NC43My0xLjY0IDEuNjRWODUuM2MwIC45Ljc0IDEuNjQgMS42NCAxLjY0aDQxLjVjLjkxIDAgMS42NC0uNzQgMS42NC0xLjY0VjUwLjE3YzAtLjktLjczLTEuNjQtMS42My0xLjY0Wm0tLjk5IDIuNjJ2MjAuNzdsLTguMjUtOC4yNWExLjM4IDEuMzggMCAwIDAtMS45NSAwTDY1LjYzIDc1LjM0bC03LjQ2LTcuNDZhMS4zNyAxLjM3IDAgMCAwLTEuOTUgMGwtOC4yNSA4LjI1VjUxLjE1Wk00Ny45NyA4NC4zMXYtNC40N2w5LjIyLTkuMjIgNy40NiA3LjQ1YTEuMzcgMS4zNyAwIDAgMCAxLjk1IDBMNzguMjcgNjYuNGw5LjIzIDkuMjN2OC42OHptMCAwIi8+PC9nPjwvc3ZnPg==`;
|
||||
const crypto = new NNCrypto();
|
||||
const crypto = new NNCrypto("/static/js/bundle.worker.js");
|
||||
const streamablefs = new StreamableFS("streamable-fs");
|
||||
const fs = localforage.createInstance({
|
||||
storeName: "notesnook-fs",
|
||||
name: "NotesnookFS",
|
||||
@@ -17,6 +21,60 @@ fs.hasItem = async function (key) {
|
||||
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
|
||||
* @param {File} file
|
||||
*/
|
||||
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 };
|
||||
|
||||
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)
|
||||
@@ -25,15 +83,22 @@ fs.hasItem = async function (key) {
|
||||
* 4. We save the encrypted Uint8Array
|
||||
*/
|
||||
async function writeEncrypted(filename, { data, type, key, hash }) {
|
||||
if (!filename) filename = hash;
|
||||
if (await fs.hasItem(filename)) return {};
|
||||
|
||||
const saveAsBuffer = localforage.supports(localforage.INDEXEDDB);
|
||||
if (type === "base64") data = new Uint8Array(Buffer.from(data, "base64"));
|
||||
|
||||
const output = saveAsBuffer
|
||||
? await crypto.encryptBinary(key, data, "buffer")
|
||||
: await crypto.encrypt(key, data, "buffer");
|
||||
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"
|
||||
);
|
||||
|
||||
await fs.setItem(filename, output.cipher);
|
||||
return {
|
||||
@@ -56,18 +121,37 @@ async function hashBuffer(data) {
|
||||
};
|
||||
}
|
||||
|
||||
async function readEncrypted(filename, key, cipherData) {
|
||||
console.log("Reading encrypted file", filename);
|
||||
const readAsBuffer = localforage.supports(localforage.INDEXEDDB);
|
||||
cipherData.cipher = await fs.getItem(filename);
|
||||
if (!cipherData.cipher) {
|
||||
console.error(`File not found. Filename: ${filename}`);
|
||||
return PLACEHOLDER;
|
||||
/**
|
||||
*
|
||||
* @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 readAsBuffer
|
||||
? await crypto.decryptBinary(key, cipherData, cipherData.outputType)
|
||||
: await crypto.decrypt(key, cipherData, cipherData.outputType);
|
||||
return { type: "xxh3", hash: hasher.digest("hex") };
|
||||
}
|
||||
|
||||
async function readEncrypted(filename, key, cipherData) {
|
||||
console.log("Reading encrypted file", filename);
|
||||
|
||||
const readAsBuffer = localforage.supports(localforage.INDEXEDDB);
|
||||
cipherData.cipher = await fs.getItem(filename);
|
||||
cipherData.format = readAsBuffer ? "uint8array" : "base64";
|
||||
|
||||
if (!cipherData.cipher) {
|
||||
console.error(`File not found. Filename: ${filename}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await crypto.decrypt(key, cipherData, cipherData.outputType);
|
||||
}
|
||||
|
||||
async function uploadFile(filename, requestOptions) {
|
||||
@@ -156,6 +240,8 @@ const FS = {
|
||||
deleteFile,
|
||||
exists,
|
||||
hashBuffer,
|
||||
hashStream,
|
||||
writeEncryptedFile,
|
||||
};
|
||||
export default FS;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import NNCrypto from "./nncrypto";
|
||||
import NNCryptoWorker from "./nncryptoworker";
|
||||
import { NNCrypto } from "nncrypto";
|
||||
import { NNCryptoWorker } from "nncryptoworker";
|
||||
|
||||
export default "Worker" in window || "Worker" in global
|
||||
? NNCryptoWorker
|
||||
@@ -1,87 +0,0 @@
|
||||
export default class NNCrypto {
|
||||
isReady = false;
|
||||
constructor() {
|
||||
this.sodium = undefined;
|
||||
}
|
||||
async _initialize() {
|
||||
if (this.isReady) return;
|
||||
return new Promise(async (resolve) => {
|
||||
window.sodium = {
|
||||
onload: (_sodium) => {
|
||||
if (this.isReady) return;
|
||||
this.isReady = true;
|
||||
this.sodium = _sodium;
|
||||
loadScript("/crypto.worker.js").then(resolve);
|
||||
},
|
||||
};
|
||||
await loadScript("sodium.js");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {string} plainData - the plaintext data
|
||||
* @param {boolean}
|
||||
*/
|
||||
encrypt = async (passwordOrKey, plainData, type = "plain") => {
|
||||
await this._initialize();
|
||||
return global.ncrypto.encrypt.call(this, passwordOrKey, {
|
||||
type,
|
||||
data: plainData,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {string} plainData - the plaintext data
|
||||
* @param {string} type
|
||||
*/
|
||||
encryptBinary = async (passwordOrKey, plainData, type = "plain") => {
|
||||
await this._initialize();
|
||||
return global.ncrypto.encryptBinary.call(this, passwordOrKey, {
|
||||
type,
|
||||
data: plainData,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {{alg: string, salt: string, iv: string, cipher: string}} cipherData - the cipher data
|
||||
*/
|
||||
decrypt = async (passwordOrKey, cipherData) => {
|
||||
await this._initialize();
|
||||
cipherData.output = "text";
|
||||
return global.ncrypto.decrypt.call(this, passwordOrKey, cipherData);
|
||||
};
|
||||
|
||||
deriveKey = async (password, salt, exportKey = false) => {
|
||||
await this._initialize();
|
||||
return global.ncrypto.deriveKey.call(this, password, salt, exportKey);
|
||||
};
|
||||
|
||||
hashPassword = async (password, userId) => {
|
||||
await this._initialize();
|
||||
return global.ncrypto.hashPassword.call(this, password, userId);
|
||||
};
|
||||
}
|
||||
|
||||
function loadScript(url) {
|
||||
return new Promise((resolve) => {
|
||||
// adding the script tag to the head as suggested before
|
||||
var head = document.getElementsByTagName("head")[0];
|
||||
var script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = url;
|
||||
|
||||
// then bind the event to the callback function
|
||||
// there are several events for cross browser compatibility
|
||||
script.onreadystatechange = resolve;
|
||||
script.onload = resolve;
|
||||
|
||||
// fire the loading
|
||||
head.appendChild(script);
|
||||
});
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
export default class NNCryptoWorker {
|
||||
constructor() {
|
||||
this.isReady = false;
|
||||
this.initializing = false;
|
||||
this.promiseQueue = [];
|
||||
}
|
||||
async _initialize() {
|
||||
if (this.isReady) return;
|
||||
if (this.initializing)
|
||||
return await new Promise((resolve, reject) => {
|
||||
this.promiseQueue.push({ resolve, reject });
|
||||
});
|
||||
|
||||
this.initializing = true;
|
||||
this.worker = new Worker("/crypto.worker.js");
|
||||
const buffer = Buffer.allocUnsafe(32);
|
||||
crypto.getRandomValues(buffer);
|
||||
const message = { seed: buffer.buffer };
|
||||
|
||||
try {
|
||||
await this._communicate("load", message, [message.seed], false);
|
||||
this.isReady = true;
|
||||
|
||||
this.promiseQueue.forEach(({ resolve }) => resolve());
|
||||
} catch (e) {
|
||||
this.isReady = false;
|
||||
|
||||
this.promiseQueue.forEach(({ reject }) => reject(e));
|
||||
} finally {
|
||||
this.initializing = false;
|
||||
this.promiseQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
_communicate(type, data, transferables = [], init = true) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (init) await this._initialize();
|
||||
const messageId = Math.random().toString(36).substr(2, 9);
|
||||
const onMessage = (e) => {
|
||||
const { type: _type, messageId: _mId, data } = e.data;
|
||||
if (_type === type && _mId === messageId) {
|
||||
this.worker.removeEventListener("message", onMessage);
|
||||
if (data.error) {
|
||||
console.error(data.error);
|
||||
return reject(data.error);
|
||||
}
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
this.worker.addEventListener("message", onMessage);
|
||||
this.worker.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
messageId,
|
||||
},
|
||||
transferables
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {string} data - the plaintext data
|
||||
* @param {boolean} compress
|
||||
*/
|
||||
encrypt = (passwordOrKey, data, type = "plain") => {
|
||||
const payload = { type, data };
|
||||
const transferables = type === "buffer" ? [payload.data] : [];
|
||||
return this._communicate(
|
||||
"encrypt",
|
||||
{
|
||||
passwordOrKey,
|
||||
data: payload,
|
||||
},
|
||||
transferables
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {string|Uint8Array} data - the plaintext data
|
||||
* @param {string} type
|
||||
*/
|
||||
encryptBinary = (passwordOrKey, data, type = "plain") => {
|
||||
const payload = { type, data };
|
||||
const transferables = type === "buffer" ? [payload.data.buffer] : [];
|
||||
return this._communicate(
|
||||
"encryptBinary",
|
||||
{
|
||||
passwordOrKey,
|
||||
data: payload,
|
||||
},
|
||||
transferables
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {{alg: string, salt: string, iv: string, cipher: Uint8Array}} cipherData - the cipher data
|
||||
*/
|
||||
decryptBinary = (passwordOrKey, cipherData, outputType = "text") => {
|
||||
cipherData.output = outputType;
|
||||
cipherData.inputType = "uint8array";
|
||||
return this._communicate("decrypt", { passwordOrKey, cipher: cipherData }, [
|
||||
cipherData.cipher.buffer,
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
|
||||
* @param {{alg: string, salt: string, iv: string, cipher: string}} cipherData - the cipher data
|
||||
*/
|
||||
decrypt = (passwordOrKey, cipherData, outputType = "text") => {
|
||||
cipherData.output = outputType;
|
||||
cipherData.inputType = "base64";
|
||||
return this._communicate("decrypt", { passwordOrKey, cipher: cipherData });
|
||||
};
|
||||
|
||||
deriveKey = (password, salt, exportKey = false) => {
|
||||
return this._communicate("deriveKey", { password, salt, exportKey });
|
||||
};
|
||||
|
||||
hashPassword = (password, userId) => {
|
||||
return this._communicate("hashPassword", { password, userId });
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import localforage from "localforage";
|
||||
import { extendPrototype } from "localforage-getitems";
|
||||
import sort from "fast-sort";
|
||||
import NNCrypto from "./nncrypto";
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
import "worker-loader!nncryptoworker/dist/src/worker.js";
|
||||
import NNCrypto from "./nncrypto.stub";
|
||||
|
||||
const crypto = new NNCrypto();
|
||||
const crypto = new NNCrypto("/static/js/bundle.worker.js");
|
||||
extendPrototype(localforage);
|
||||
|
||||
localforage.config({
|
||||
@@ -40,7 +42,7 @@ async function deriveCryptoKey(name, data) {
|
||||
const { password, salt } = data;
|
||||
if (!password) throw new Error("Invalid data provided to deriveCryptoKey.");
|
||||
|
||||
const keyData = await crypto.deriveKey(password, salt, true);
|
||||
const keyData = await crypto.exportKey(password, salt);
|
||||
|
||||
if (localforage.supports(localforage.INDEXEDDB) && window?.crypto?.subtle) {
|
||||
const pbkdfKey = await derivePBKDF2Key(password);
|
||||
@@ -73,7 +75,7 @@ const Storage = {
|
||||
getAllKeys,
|
||||
deriveCryptoKey,
|
||||
getCryptoKey,
|
||||
hash: crypto.hashPassword,
|
||||
hash: crypto.hash,
|
||||
encrypt: crypto.encrypt,
|
||||
decrypt: crypto.decrypt,
|
||||
};
|
||||
@@ -81,6 +83,7 @@ export default Storage;
|
||||
|
||||
let enc = new TextEncoder();
|
||||
let dec = new TextDecoder();
|
||||
|
||||
async function derivePBKDF2Key(password) {
|
||||
const key = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
|
||||
Reference in New Issue
Block a user