diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 16166ebe3..8951265c9 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -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 diff --git a/apps/web/package.json b/apps/web/package.json index 3a244350f..5203f6c07 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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" diff --git a/apps/web/packages/nncrypto/index.ts b/apps/web/packages/nncrypto/index.ts new file mode 100644 index 000000000..f5fb9d341 --- /dev/null +++ b/apps/web/packages/nncrypto/index.ts @@ -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 { + await this.init(); + return Encryption.encrypt(key, plaintext, outputFormat); + } + + async decrypt( + key: SerializedKey, + cipherData: Cipher, + outputFormat: OutputFormat = "text" + ): Promise { + 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); + } +} diff --git a/apps/web/packages/nncrypto/package.json b/apps/web/packages/nncrypto/package.json new file mode 100644 index 000000000..23c63872b --- /dev/null +++ b/apps/web/packages/nncrypto/package.json @@ -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": "" +} diff --git a/apps/web/packages/nncrypto/src/decryption.ts b/apps/web/packages/nncrypto/src/decryption.ts new file mode 100644 index 000000000..32054e53b --- /dev/null +++ b/apps/web/packages/nncrypto/src/decryption.ts @@ -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; + } +} diff --git a/apps/web/packages/nncrypto/src/encryption.ts b/apps/web/packages/nncrypto/src/encryption.ts new file mode 100644 index 000000000..f3137b5ed --- /dev/null +++ b/apps/web/packages/nncrypto/src/encryption.ts @@ -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}`; +} diff --git a/apps/web/packages/nncrypto/src/interfaces.ts b/apps/web/packages/nncrypto/src/interfaces.ts new file mode 100644 index 000000000..f85ed63dd --- /dev/null +++ b/apps/web/packages/nncrypto/src/interfaces.ts @@ -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>; +} diff --git a/apps/web/packages/nncrypto/src/keyutils.ts b/apps/web/packages/nncrypto/src/keyutils.ts new file mode 100644 index 000000000..af27c4565 --- /dev/null +++ b/apps/web/packages/nncrypto/src/keyutils.ts @@ -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."); + } +} diff --git a/apps/web/packages/nncrypto/src/password.ts b/apps/web/packages/nncrypto/src/password.ts new file mode 100644 index 000000000..c2af55082 --- /dev/null +++ b/apps/web/packages/nncrypto/src/password.ts @@ -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; + } +} diff --git a/apps/web/packages/nncrypto/src/types.ts b/apps/web/packages/nncrypto/src/types.ts new file mode 100644 index 000000000..c73dc8e93 --- /dev/null +++ b/apps/web/packages/nncrypto/src/types.ts @@ -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; +}; diff --git a/apps/web/packages/nncrypto/tsconfig.json b/apps/web/packages/nncrypto/tsconfig.json new file mode 100644 index 000000000..37bff4060 --- /dev/null +++ b/apps/web/packages/nncrypto/tsconfig.json @@ -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/"] +} diff --git a/apps/web/packages/nncrypto/webpack.config.js b/apps/web/packages/nncrypto/webpack.config.js new file mode 100644 index 000000000..26ba8a348 --- /dev/null +++ b/apps/web/packages/nncrypto/webpack.config.js @@ -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", + }, + }, +}; diff --git a/apps/web/packages/nncryptoworker/index.ts b/apps/web/packages/nncryptoworker/index.ts new file mode 100644 index 000000000..3670960aa --- /dev/null +++ b/apps/web/packages/nncryptoworker/index.ts @@ -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."); + // } +} diff --git a/apps/web/packages/nncryptoworker/package.json b/apps/web/packages/nncryptoworker/package.json new file mode 100644 index 000000000..dc0a0efe1 --- /dev/null +++ b/apps/web/packages/nncryptoworker/package.json @@ -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" + } +} diff --git a/apps/web/packages/nncryptoworker/src/utils.ts b/apps/web/packages/nncryptoworker/src/utils.ts new file mode 100644 index 000000000..22ed78195 --- /dev/null +++ b/apps/web/packages/nncryptoworker/src/utils.ts @@ -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 }); + }); +} diff --git a/apps/web/packages/nncryptoworker/src/worker.ts b/apps/web/packages/nncryptoworker/src/worker.ts new file mode 100644 index 000000000..fe416c17b --- /dev/null +++ b/apps/web/packages/nncryptoworker/src/worker.ts @@ -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); diff --git a/apps/web/packages/nncryptoworker/src/workerstream.ts b/apps/web/packages/nncryptoworker/src/workerstream.ts new file mode 100644 index 000000000..a2bd0d99f --- /dev/null +++ b/apps/web/packages/nncryptoworker/src/workerstream.ts @@ -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(); + } +} diff --git a/apps/web/packages/nncryptoworker/tsconfig.json b/apps/web/packages/nncryptoworker/tsconfig.json new file mode 100644 index 000000000..b7103a824 --- /dev/null +++ b/apps/web/packages/nncryptoworker/tsconfig.json @@ -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/"] +} diff --git a/apps/web/packages/streamablefs/index.ts b/apps/web/packages/streamablefs/index.ts new file mode 100644 index 000000000..eeacc0e65 --- /dev/null +++ b/apps/web/packages/streamablefs/index.ts @@ -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(); + } +} diff --git a/apps/web/packages/streamablefs/package.json b/apps/web/packages/streamablefs/package.json new file mode 100644 index 000000000..3cbcc01af --- /dev/null +++ b/apps/web/packages/streamablefs/package.json @@ -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" + } +} diff --git a/apps/web/packages/streamablefs/src/filehandle.ts b/apps/web/packages/streamablefs/src/filehandle.ts new file mode 100644 index 000000000..8ec63958c --- /dev/null +++ b/apps/web/packages/streamablefs/src/filehandle.ts @@ -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}`; + } +} diff --git a/apps/web/packages/streamablefs/src/filestreamsource.ts b/apps/web/packages/streamablefs/src/filestreamsource.ts new file mode 100644 index 000000000..56a35ad58 --- /dev/null +++ b/apps/web/packages/streamablefs/src/filestreamsource.ts @@ -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}`; + } +} diff --git a/apps/web/packages/streamablefs/src/interfaces.ts b/apps/web/packages/streamablefs/src/interfaces.ts new file mode 100644 index 000000000..e4a4beb82 --- /dev/null +++ b/apps/web/packages/streamablefs/src/interfaces.ts @@ -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>; +} diff --git a/apps/web/packages/streamablefs/src/types.ts b/apps/web/packages/streamablefs/src/types.ts new file mode 100644 index 000000000..2e1a2910f --- /dev/null +++ b/apps/web/packages/streamablefs/src/types.ts @@ -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; +}; diff --git a/apps/web/packages/streamablefs/tsconfig.json b/apps/web/packages/streamablefs/tsconfig.json new file mode 100644 index 000000000..37bff4060 --- /dev/null +++ b/apps/web/packages/streamablefs/tsconfig.json @@ -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/"] +} diff --git a/apps/web/public/crypto.worker.js b/apps/web/public/crypto.worker.js index ad2e156b0..0ffdd5a8e 100644 --- a/apps/web/public/crypto.worker.js +++ b/apps/web/public/crypto.worker.js @@ -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 diff --git a/apps/web/src/components/editor/plugins/picker.js b/apps/web/src/components/editor/plugins/picker.js index 85e34cd84..bc3594175 100644 --- a/apps/web/src/components/editor/plugins/picker.js +++ b/apps/web/src/components/editor/plugins/picker.js @@ -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, diff --git a/apps/web/src/interfaces/fs.js b/apps/web/src/interfaces/fs.js index 2787c5f86..a96b9f962 100644 --- a/apps/web/src/interfaces/fs.js +++ b/apps/web/src/interfaces/fs.js @@ -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; diff --git a/apps/web/src/interfaces/nncrypto/index.js b/apps/web/src/interfaces/nncrypto.stub.js similarity index 51% rename from apps/web/src/interfaces/nncrypto/index.js rename to apps/web/src/interfaces/nncrypto.stub.js index 281b46a96..1d226455f 100644 --- a/apps/web/src/interfaces/nncrypto/index.js +++ b/apps/web/src/interfaces/nncrypto.stub.js @@ -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 diff --git a/apps/web/src/interfaces/nncrypto/nncrypto.js b/apps/web/src/interfaces/nncrypto/nncrypto.js deleted file mode 100644 index 9e05930e4..000000000 --- a/apps/web/src/interfaces/nncrypto/nncrypto.js +++ /dev/null @@ -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); - }); -} diff --git a/apps/web/src/interfaces/nncrypto/nncryptoworker.js b/apps/web/src/interfaces/nncrypto/nncryptoworker.js deleted file mode 100644 index b980caaaf..000000000 --- a/apps/web/src/interfaces/nncrypto/nncryptoworker.js +++ /dev/null @@ -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 }); - }; -} diff --git a/apps/web/src/interfaces/storage.js b/apps/web/src/interfaces/storage.js index 86e1166d2..38d6ffe65 100644 --- a/apps/web/src/interfaces/storage.js +++ b/apps/web/src/interfaces/storage.js @@ -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",