diff --git a/.editorconfig b/.editorconfig index 332f3a5d..ec87dc82 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,9 +7,10 @@ root = true [*] end_of_line = lf insert_final_newline = true +trim_trailing_whitespace = true # Set default charset [*.{js,ts,scss,html}] charset = utf-8 indent_style = space -indent_size = 4 +indent_size = 2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e58289c8..ffc8db18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,11 +33,10 @@ jobs: - name: Set up Node uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea with: - node-version: '14' + node-version: '16' - - name: Update NPM + - name: Install node-gyp run: | - npm install -g npm@7 npm install -g node-gyp node-gyp install $(node -v) @@ -52,8 +51,8 @@ jobs: - name: Install Node dependencies run: npm install - - name: Run linter - run: npm run lint + # - name: Run linter + # run: npm run lint - name: Build run: npm run build diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 00000000..31354ec1 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..36af2198 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dist diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..de753c53 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 100 +} diff --git a/README.md b/README.md index 2dca3256..fed8186b 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,14 @@ Common code referenced across Bitwarden JavaScript projects. ## Requirements -* [Node.js](https://nodejs.org) v14.17 or greater -* NPM v7 -* Git -* node-gyp + +- [Node.js](https://nodejs.org) v16.13.1 or greater +- NPM v8 +- Git +- node-gyp ### Windows -* *Microsoft Build Tools 2015* in Visual Studio Installer -* [Windows 10 SDK 17134](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) -either by downloading it seperately or through the Visual Studio Installer. +- _Microsoft Build Tools 2015_ in Visual Studio Installer +- [Windows 10 SDK 17134](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) + either by downloading it seperately or through the Visual Studio Installer. diff --git a/angular/package-lock.json b/angular/package-lock.json index fc5beb76..69362e93 100644 --- a/angular/package-lock.json +++ b/angular/package-lock.json @@ -9,24 +9,25 @@ "version": "0.0.0", "license": "GPL-3.0", "dependencies": { - "@angular/animations": "^11.2.11", - "@angular/cdk": "^11.2.10", - "@angular/common": "^11.2.11", - "@angular/compiler": "^11.2.11", - "@angular/core": "^11.2.11", - "@angular/forms": "^11.2.11", - "@angular/platform-browser": "^11.2.11", - "@angular/platform-browser-dynamic": "^11.2.11", - "@angular/router": "^11.2.11", + "@angular/animations": "^12.2.13", + "@angular/cdk": "^12.2.13", + "@angular/common": "^12.2.13", + "@angular/compiler": "^12.2.13", + "@angular/core": "^12.2.13", + "@angular/forms": "^12.2.13", + "@angular/platform-browser": "^12.2.13", + "@angular/platform-browser-dynamic": "^12.2.13", + "@angular/router": "^12.2.13", "@bitwarden/jslib-common": "file:../common", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", + "tldjs": "^2.3.1", "zone.js": "0.11.4" }, "devDependencies": { "@types/duo_web_sdk": "^2.7.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "../common": { @@ -41,7 +42,7 @@ "lunr": "^2.3.9", "node-forge": "^0.10.0", "papaparse": "^5.3.0", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldts": "5.7.52", "zxcvbn": "^4.4.2" }, @@ -52,92 +53,111 @@ "@types/papaparse": "^5.2.5", "@types/zxcvbn": "^4.4.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "node_modules/@angular/animations": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-11.2.14.tgz", - "integrity": "sha512-Heq/nNrCmb3jbkusu+BQszOecfFI/31Oxxj+CDQkqqYpBcswk6bOJLoEE472o+vmgxaXbgeflU9qbIiCQhpMFA==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-12.2.14.tgz", + "integrity": "sha512-1BR5u32auVePvXNNP96DB2008V+Ku0OGqeZQl2h4XA9xzES/Zk5WllIJZXqRmWMRBVARfXsfb0RdMty9gcaVjA==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/core": "11.2.14" + "@angular/core": "12.2.14" } }, "node_modules/@angular/cdk": { - "version": "11.2.13", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-11.2.13.tgz", - "integrity": "sha512-FkE4iCwoLbQxLDUOjV1I7M/6hmpyb7erAjEdWgch7nGRNxF1hqX5Bqf1lvLFKPNCbx5NRI5K7YVAdIUQUR8vug==", + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.13.tgz", + "integrity": "sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "optionalDependencies": { "parse5": "^5.0.0" }, "peerDependencies": { - "@angular/common": "^11.0.0 || ^12.0.0-0", - "@angular/core": "^11.0.0 || ^12.0.0-0" + "@angular/common": "^12.0.0 || ^13.0.0-0", + "@angular/core": "^12.0.0 || ^13.0.0-0", + "rxjs": "^6.5.3 || ^7.0.0" } }, "node_modules/@angular/common": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-11.2.14.tgz", - "integrity": "sha512-ZSLV/3j7eCTyLf/8g4yBFLWySjiLz3vLJAGWscYoUpnJWMnug1VRu6zoF/COxCbtORgE+Wz6K0uhfS6MziBGVw==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-12.2.14.tgz", + "integrity": "sha512-ffYUYdwZETmFJw0AcWY30WsaWBhJxj/zSmFXWjgEGEGZH56zwbbNwfMZOYZ1jz4haAVxGu+TdXsOl2yMGzN7jQ==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/core": "11.2.14", - "rxjs": "^6.5.3" + "@angular/core": "12.2.14", + "rxjs": "^6.5.3 || ^7.0.0" } }, "node_modules/@angular/compiler": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-11.2.14.tgz", - "integrity": "sha512-XBOK3HgA+/y6Cz7kOX4zcJYmgJ264XnfcbXUMU2cD7Ac+mbNhLPKohWrEiSWalfcjnpf5gRfufQrQP7lpAGu0A==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-12.2.14.tgz", + "integrity": "sha512-dwmZi+n66IUzRFlGWu9mjXq170ZEsaDvlNLZzaPgs6vZTa4Kt7PWvIF/Y7TMvnVv/uqNG6kOhfmOkf6rfz1Gjg==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" } }, "node_modules/@angular/core": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-11.2.14.tgz", - "integrity": "sha512-vpR4XqBGitk1Faph37CSpemwIYTmJ3pdIVNoHKP6jLonpWu+0azkchf0f7oD8/2ivj2F81opcIw0tcsy/D/5Vg==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-12.2.14.tgz", + "integrity": "sha512-dlVk7yqUHL2R/eCmM8LsWuxhEBfzg0y1zHt0UqCuFwlCoiw+IG4HFy4OlZEUw9NUEZJSv0aDv3sWqxLkvK5vvg==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "rxjs": "^6.5.3", - "zone.js": "^0.10.2 || ^0.11.3" + "rxjs": "^6.5.3 || ^7.0.0", + "zone.js": "~0.11.4" } }, "node_modules/@angular/forms": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-11.2.14.tgz", - "integrity": "sha512-4LWqY6KEIk1AZQFnk+4PJSOCamlD4tumuVN06gO4D0dZo9Cx+GcvW6pM6N0CPubRvPs3sScCnu20WT11HNWC1w==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-12.2.14.tgz", + "integrity": "sha512-/9/gSJUBXVRVdRnzgJnALAQZYJATuGDMkFC9ms9DEMG4PMAhe9x4if1lJjN6noz5RAom3qNuVBNWaYAPUxlcBQ==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/common": "11.2.14", - "@angular/core": "11.2.14", - "@angular/platform-browser": "11.2.14", - "rxjs": "^6.5.3" + "@angular/common": "12.2.14", + "@angular/core": "12.2.14", + "@angular/platform-browser": "12.2.14", + "rxjs": "^6.5.3 || ^7.0.0" } }, "node_modules/@angular/platform-browser": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-11.2.14.tgz", - "integrity": "sha512-fb7b7ss/gRoP8wLAN17W62leMgjynuyjEPU2eUoAAazsG9f2cgM+z3rK29GYncDVyYQxZUZYnjSqvL6GSXx86A==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.2.14.tgz", + "integrity": "sha512-fWcE2rnJ3ZCISa1oPfsIDV7FBZBoLFEdDuMXAiDYqDPKvF/E5U5nHrS+K4SlLAi094bMobtTOReNWl/Ienniyw==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/animations": "11.2.14", - "@angular/common": "11.2.14", - "@angular/core": "11.2.14" + "@angular/animations": "12.2.14", + "@angular/common": "12.2.14", + "@angular/core": "12.2.14" }, "peerDependenciesMeta": { "@angular/animations": { @@ -146,31 +166,37 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.14.tgz", - "integrity": "sha512-TWTPdFs6iBBcp+/YMsgCRQwdHpWGq8KjeJDJ2tfatGgBD3Gqt2YaHOMST1zPW6RkrmupytTejuVqXzeaKWFxuw==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.2.14.tgz", + "integrity": "sha512-0NPF7mS91Tct8rBmOLZPmnLSuS4kbLHXo6eTgrg80OC0vlzBiQwGDVW4X3KncCoX9CpevaGJCdSMc+uPNsFOUQ==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/common": "11.2.14", - "@angular/compiler": "11.2.14", - "@angular/core": "11.2.14", - "@angular/platform-browser": "11.2.14" + "@angular/common": "12.2.14", + "@angular/compiler": "12.2.14", + "@angular/core": "12.2.14", + "@angular/platform-browser": "12.2.14" } }, "node_modules/@angular/router": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-11.2.14.tgz", - "integrity": "sha512-3aYBmj+zrEL9yf/ntIQxHIYaWShZOBKP3U07X2mX+TPMpGlvHDnR7L6bWhQVZwewzMMz7YVR16ldg50IFuAlfA==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-12.2.14.tgz", + "integrity": "sha512-yP5grSnqBvc4vNhtYdcxDgDYIebUKs5f0xyFkUJM5030UnQ0CV45tBsSxHMkQbPZucIfOuxpRy8xy5+4GizuwQ==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/common": "11.2.14", - "@angular/core": "11.2.14", - "@angular/platform-browser": "11.2.14", - "rxjs": "^6.5.3" + "@angular/common": "12.2.14", + "@angular/core": "12.2.14", + "@angular/platform-browser": "12.2.14", + "rxjs": "^6.5.3 || ^7.0.0" } }, "node_modules/@bitwarden/jslib-common": { @@ -304,20 +330,17 @@ } }, "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "~2.1.0" } }, "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, "node_modules/tslib": { "version": "2.3.1", @@ -325,9 +348,9 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -354,76 +377,76 @@ }, "dependencies": { "@angular/animations": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-11.2.14.tgz", - "integrity": "sha512-Heq/nNrCmb3jbkusu+BQszOecfFI/31Oxxj+CDQkqqYpBcswk6bOJLoEE472o+vmgxaXbgeflU9qbIiCQhpMFA==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-12.2.14.tgz", + "integrity": "sha512-1BR5u32auVePvXNNP96DB2008V+Ku0OGqeZQl2h4XA9xzES/Zk5WllIJZXqRmWMRBVARfXsfb0RdMty9gcaVjA==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@angular/cdk": { - "version": "11.2.13", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-11.2.13.tgz", - "integrity": "sha512-FkE4iCwoLbQxLDUOjV1I7M/6hmpyb7erAjEdWgch7nGRNxF1hqX5Bqf1lvLFKPNCbx5NRI5K7YVAdIUQUR8vug==", + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.13.tgz", + "integrity": "sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg==", "requires": { "parse5": "^5.0.0", - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@angular/common": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-11.2.14.tgz", - "integrity": "sha512-ZSLV/3j7eCTyLf/8g4yBFLWySjiLz3vLJAGWscYoUpnJWMnug1VRu6zoF/COxCbtORgE+Wz6K0uhfS6MziBGVw==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-12.2.14.tgz", + "integrity": "sha512-ffYUYdwZETmFJw0AcWY30WsaWBhJxj/zSmFXWjgEGEGZH56zwbbNwfMZOYZ1jz4haAVxGu+TdXsOl2yMGzN7jQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@angular/compiler": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-11.2.14.tgz", - "integrity": "sha512-XBOK3HgA+/y6Cz7kOX4zcJYmgJ264XnfcbXUMU2cD7Ac+mbNhLPKohWrEiSWalfcjnpf5gRfufQrQP7lpAGu0A==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-12.2.14.tgz", + "integrity": "sha512-dwmZi+n66IUzRFlGWu9mjXq170ZEsaDvlNLZzaPgs6vZTa4Kt7PWvIF/Y7TMvnVv/uqNG6kOhfmOkf6rfz1Gjg==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@angular/core": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-11.2.14.tgz", - "integrity": "sha512-vpR4XqBGitk1Faph37CSpemwIYTmJ3pdIVNoHKP6jLonpWu+0azkchf0f7oD8/2ivj2F81opcIw0tcsy/D/5Vg==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-12.2.14.tgz", + "integrity": "sha512-dlVk7yqUHL2R/eCmM8LsWuxhEBfzg0y1zHt0UqCuFwlCoiw+IG4HFy4OlZEUw9NUEZJSv0aDv3sWqxLkvK5vvg==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@angular/forms": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-11.2.14.tgz", - "integrity": "sha512-4LWqY6KEIk1AZQFnk+4PJSOCamlD4tumuVN06gO4D0dZo9Cx+GcvW6pM6N0CPubRvPs3sScCnu20WT11HNWC1w==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-12.2.14.tgz", + "integrity": "sha512-/9/gSJUBXVRVdRnzgJnALAQZYJATuGDMkFC9ms9DEMG4PMAhe9x4if1lJjN6noz5RAom3qNuVBNWaYAPUxlcBQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@angular/platform-browser": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-11.2.14.tgz", - "integrity": "sha512-fb7b7ss/gRoP8wLAN17W62leMgjynuyjEPU2eUoAAazsG9f2cgM+z3rK29GYncDVyYQxZUZYnjSqvL6GSXx86A==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.2.14.tgz", + "integrity": "sha512-fWcE2rnJ3ZCISa1oPfsIDV7FBZBoLFEdDuMXAiDYqDPKvF/E5U5nHrS+K4SlLAi094bMobtTOReNWl/Ienniyw==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@angular/platform-browser-dynamic": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.14.tgz", - "integrity": "sha512-TWTPdFs6iBBcp+/YMsgCRQwdHpWGq8KjeJDJ2tfatGgBD3Gqt2YaHOMST1zPW6RkrmupytTejuVqXzeaKWFxuw==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.2.14.tgz", + "integrity": "sha512-0NPF7mS91Tct8rBmOLZPmnLSuS4kbLHXo6eTgrg80OC0vlzBiQwGDVW4X3KncCoX9CpevaGJCdSMc+uPNsFOUQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@angular/router": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-11.2.14.tgz", - "integrity": "sha512-3aYBmj+zrEL9yf/ntIQxHIYaWShZOBKP3U07X2mX+TPMpGlvHDnR7L6bWhQVZwewzMMz7YVR16ldg50IFuAlfA==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-12.2.14.tgz", + "integrity": "sha512-yP5grSnqBvc4vNhtYdcxDgDYIebUKs5f0xyFkUJM5030UnQ0CV45tBsSxHMkQbPZucIfOuxpRy8xy5+4GizuwQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@bitwarden/jslib-common": { @@ -442,9 +465,9 @@ "node-forge": "^0.10.0", "papaparse": "^5.3.0", "rimraf": "^3.0.2", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldts": "5.7.52", - "typescript": "4.1.5", + "typescript": "4.3.5", "zxcvbn": "^4.4.2" } }, @@ -556,17 +579,17 @@ } }, "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "requires": { - "tslib": "^1.9.0" + "tslib": "~2.1.0" }, "dependencies": { "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" } } }, @@ -576,9 +599,9 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true }, "wrappy": { diff --git a/angular/package.json b/angular/package.json index a5a88a25..d1d5f64c 100644 --- a/angular/package.json +++ b/angular/package.json @@ -22,21 +22,22 @@ "devDependencies": { "@types/duo_web_sdk": "^2.7.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" }, "dependencies": { - "@angular/animations": "^11.2.11", - "@angular/cdk": "^11.2.10", - "@angular/common": "^11.2.11", - "@angular/compiler": "^11.2.11", - "@angular/core": "^11.2.11", - "@angular/forms": "^11.2.11", - "@angular/platform-browser": "^11.2.11", - "@angular/platform-browser-dynamic": "^11.2.11", - "@angular/router": "^11.2.11", + "@angular/animations": "^12.2.13", + "@angular/cdk": "^12.2.13", + "@angular/common": "^12.2.13", + "@angular/compiler": "^12.2.13", + "@angular/core": "^12.2.13", + "@angular/forms": "^12.2.13", + "@angular/platform-browser": "^12.2.13", + "@angular/platform-browser-dynamic": "^12.2.13", + "@angular/router": "^12.2.13", "@bitwarden/jslib-common": "file:../common", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", + "tldjs": "^2.3.1", "zone.js": "0.11.4" } } diff --git a/angular/src/components/add-edit.component.ts b/angular/src/components/add-edit.component.ts index edb0b4ae..4c19588c 100644 --- a/angular/src/components/add-edit.component.ts +++ b/angular/src/components/add-edit.component.ts @@ -1,509 +1,566 @@ -import { - Directive, - EventEmitter, - Input, - OnInit, - Output, -} from '@angular/core'; +import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; -import { CipherType } from 'jslib-common/enums/cipherType'; -import { EventType } from 'jslib-common/enums/eventType'; -import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType'; -import { PolicyType } from 'jslib-common/enums/policyType'; -import { SecureNoteType } from 'jslib-common/enums/secureNoteType'; -import { UriMatchType } from 'jslib-common/enums/uriMatchType'; +import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType"; +import { CipherType } from "jslib-common/enums/cipherType"; +import { EventType } from "jslib-common/enums/eventType"; +import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType"; +import { PolicyType } from "jslib-common/enums/policyType"; +import { SecureNoteType } from "jslib-common/enums/secureNoteType"; +import { UriMatchType } from "jslib-common/enums/uriMatchType"; -import { AuditService } from 'jslib-common/abstractions/audit.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { EventService } from 'jslib-common/abstractions/event.service'; -import { FolderService } from 'jslib-common/abstractions/folder.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { AuditService } from "jslib-common/abstractions/audit.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { EventService } from "jslib-common/abstractions/event.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; +import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { Cipher } from 'jslib-common/models/domain/cipher'; +import { Cipher } from "jslib-common/models/domain/cipher"; -import { CardView } from 'jslib-common/models/view/cardView'; -import { CipherView } from 'jslib-common/models/view/cipherView'; -import { CollectionView } from 'jslib-common/models/view/collectionView'; -import { FolderView } from 'jslib-common/models/view/folderView'; -import { IdentityView } from 'jslib-common/models/view/identityView'; -import { LoginUriView } from 'jslib-common/models/view/loginUriView'; -import { LoginView } from 'jslib-common/models/view/loginView'; -import { SecureNoteView } from 'jslib-common/models/view/secureNoteView'; +import { CardView } from "jslib-common/models/view/cardView"; +import { CipherView } from "jslib-common/models/view/cipherView"; +import { CollectionView } from "jslib-common/models/view/collectionView"; +import { FolderView } from "jslib-common/models/view/folderView"; +import { IdentityView } from "jslib-common/models/view/identityView"; +import { LoginUriView } from "jslib-common/models/view/loginUriView"; +import { LoginView } from "jslib-common/models/view/loginView"; +import { SecureNoteView } from "jslib-common/models/view/secureNoteView"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; @Directive() export class AddEditComponent implements OnInit { - @Input() cloneMode: boolean = false; - @Input() folderId: string = null; - @Input() cipherId: string; - @Input() type: CipherType; - @Input() collectionIds: string[]; - @Input() organizationId: string = null; - @Output() onSavedCipher = new EventEmitter(); - @Output() onDeletedCipher = new EventEmitter(); - @Output() onRestoredCipher = new EventEmitter(); - @Output() onCancelled = new EventEmitter(); - @Output() onEditAttachments = new EventEmitter(); - @Output() onShareCipher = new EventEmitter(); - @Output() onEditCollections = new EventEmitter(); - @Output() onGeneratePassword = new EventEmitter(); + @Input() cloneMode: boolean = false; + @Input() folderId: string = null; + @Input() cipherId: string; + @Input() type: CipherType; + @Input() collectionIds: string[]; + @Input() organizationId: string = null; + @Output() onSavedCipher = new EventEmitter(); + @Output() onDeletedCipher = new EventEmitter(); + @Output() onRestoredCipher = new EventEmitter(); + @Output() onCancelled = new EventEmitter(); + @Output() onEditAttachments = new EventEmitter(); + @Output() onShareCipher = new EventEmitter(); + @Output() onEditCollections = new EventEmitter(); + @Output() onGeneratePassword = new EventEmitter(); - editMode: boolean = false; - cipher: CipherView; - folders: FolderView[]; - collections: CollectionView[] = []; - title: string; - formPromise: Promise; - deletePromise: Promise; - restorePromise: Promise; - checkPasswordPromise: Promise; - showPassword: boolean = false; - showCardNumber: boolean = false; - showCardCode: boolean = false; - cipherType = CipherType; - typeOptions: any[]; - cardBrandOptions: any[]; - cardExpMonthOptions: any[]; - identityTitleOptions: any[]; - uriMatchOptions: any[]; - ownershipOptions: any[] = []; - autofillOnPageLoadOptions: any[]; - currentDate = new Date(); - allowPersonal = true; - reprompt: boolean = false; - canUseReprompt: boolean = true; + editMode: boolean = false; + cipher: CipherView; + folders: FolderView[]; + collections: CollectionView[] = []; + title: string; + formPromise: Promise; + deletePromise: Promise; + restorePromise: Promise; + checkPasswordPromise: Promise; + showPassword: boolean = false; + showCardNumber: boolean = false; + showCardCode: boolean = false; + cipherType = CipherType; + typeOptions: any[]; + cardBrandOptions: any[]; + cardExpMonthOptions: any[]; + identityTitleOptions: any[]; + uriMatchOptions: any[]; + ownershipOptions: any[] = []; + autofillOnPageLoadOptions: any[]; + currentDate = new Date(); + allowPersonal = true; + reprompt: boolean = false; + canUseReprompt: boolean = true; - protected writeableCollections: CollectionView[]; - private previousCipherId: string; + protected writeableCollections: CollectionView[]; + private previousCipherId: string; - constructor(protected cipherService: CipherService, protected folderService: FolderService, - protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected auditService: AuditService, protected stateService: StateService, - protected userService: UserService, protected collectionService: CollectionService, - protected messagingService: MessagingService, protected eventService: EventService, - protected policyService: PolicyService, protected passwordRepromptService: PasswordRepromptService, - private logService: LogService) { - this.typeOptions = [ - { name: i18nService.t('typeLogin'), value: CipherType.Login }, - { name: i18nService.t('typeCard'), value: CipherType.Card }, - { name: i18nService.t('typeIdentity'), value: CipherType.Identity }, - { name: i18nService.t('typeSecureNote'), value: CipherType.SecureNote }, - ]; - this.cardBrandOptions = [ - { name: '-- ' + i18nService.t('select') + ' --', value: null }, - { name: 'Visa', value: 'Visa' }, - { name: 'Mastercard', value: 'Mastercard' }, - { name: 'American Express', value: 'Amex' }, - { name: 'Discover', value: 'Discover' }, - { name: 'Diners Club', value: 'Diners Club' }, - { name: 'JCB', value: 'JCB' }, - { name: 'Maestro', value: 'Maestro' }, - { name: 'UnionPay', value: 'UnionPay' }, - { name: i18nService.t('other'), value: 'Other' }, - ]; - this.cardExpMonthOptions = [ - { name: '-- ' + i18nService.t('select') + ' --', value: null }, - { name: '01 - ' + i18nService.t('january'), value: '1' }, - { name: '02 - ' + i18nService.t('february'), value: '2' }, - { name: '03 - ' + i18nService.t('march'), value: '3' }, - { name: '04 - ' + i18nService.t('april'), value: '4' }, - { name: '05 - ' + i18nService.t('may'), value: '5' }, - { name: '06 - ' + i18nService.t('june'), value: '6' }, - { name: '07 - ' + i18nService.t('july'), value: '7' }, - { name: '08 - ' + i18nService.t('august'), value: '8' }, - { name: '09 - ' + i18nService.t('september'), value: '9' }, - { name: '10 - ' + i18nService.t('october'), value: '10' }, - { name: '11 - ' + i18nService.t('november'), value: '11' }, - { name: '12 - ' + i18nService.t('december'), value: '12' }, - ]; - this.identityTitleOptions = [ - { name: '-- ' + i18nService.t('select') + ' --', value: null }, - { name: i18nService.t('mr'), value: i18nService.t('mr') }, - { name: i18nService.t('mrs'), value: i18nService.t('mrs') }, - { name: i18nService.t('ms'), value: i18nService.t('ms') }, - { name: i18nService.t('dr'), value: i18nService.t('dr') }, - ]; - this.uriMatchOptions = [ - { name: i18nService.t('defaultMatchDetection'), value: null }, - { name: i18nService.t('baseDomain'), value: UriMatchType.Domain }, - { name: i18nService.t('host'), value: UriMatchType.Host }, - { name: i18nService.t('startsWith'), value: UriMatchType.StartsWith }, - { name: i18nService.t('regEx'), value: UriMatchType.RegularExpression }, - { name: i18nService.t('exact'), value: UriMatchType.Exact }, - { name: i18nService.t('never'), value: UriMatchType.Never }, - ]; - this.autofillOnPageLoadOptions = [ - { name: i18nService.t('autoFillOnPageLoadUseDefault'), value: null }, - { name: i18nService.t('autoFillOnPageLoadYes'), value: true }, - { name: i18nService.t('autoFillOnPageLoadNo'), value: false }, - ]; + constructor( + protected cipherService: CipherService, + protected folderService: FolderService, + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + protected auditService: AuditService, + protected stateService: StateService, + protected collectionService: CollectionService, + protected messagingService: MessagingService, + protected eventService: EventService, + protected policyService: PolicyService, + private logService: LogService, + protected passwordRepromptService: PasswordRepromptService, + private organizationService: OrganizationService + ) { + this.typeOptions = [ + { name: i18nService.t("typeLogin"), value: CipherType.Login }, + { name: i18nService.t("typeCard"), value: CipherType.Card }, + { name: i18nService.t("typeIdentity"), value: CipherType.Identity }, + { name: i18nService.t("typeSecureNote"), value: CipherType.SecureNote }, + ]; + this.cardBrandOptions = [ + { name: "-- " + i18nService.t("select") + " --", value: null }, + { name: "Visa", value: "Visa" }, + { name: "Mastercard", value: "Mastercard" }, + { name: "American Express", value: "Amex" }, + { name: "Discover", value: "Discover" }, + { name: "Diners Club", value: "Diners Club" }, + { name: "JCB", value: "JCB" }, + { name: "Maestro", value: "Maestro" }, + { name: "UnionPay", value: "UnionPay" }, + { name: i18nService.t("other"), value: "Other" }, + ]; + this.cardExpMonthOptions = [ + { name: "-- " + i18nService.t("select") + " --", value: null }, + { name: "01 - " + i18nService.t("january"), value: "1" }, + { name: "02 - " + i18nService.t("february"), value: "2" }, + { name: "03 - " + i18nService.t("march"), value: "3" }, + { name: "04 - " + i18nService.t("april"), value: "4" }, + { name: "05 - " + i18nService.t("may"), value: "5" }, + { name: "06 - " + i18nService.t("june"), value: "6" }, + { name: "07 - " + i18nService.t("july"), value: "7" }, + { name: "08 - " + i18nService.t("august"), value: "8" }, + { name: "09 - " + i18nService.t("september"), value: "9" }, + { name: "10 - " + i18nService.t("october"), value: "10" }, + { name: "11 - " + i18nService.t("november"), value: "11" }, + { name: "12 - " + i18nService.t("december"), value: "12" }, + ]; + this.identityTitleOptions = [ + { name: "-- " + i18nService.t("select") + " --", value: null }, + { name: i18nService.t("mr"), value: i18nService.t("mr") }, + { name: i18nService.t("mrs"), value: i18nService.t("mrs") }, + { name: i18nService.t("ms"), value: i18nService.t("ms") }, + { name: i18nService.t("dr"), value: i18nService.t("dr") }, + ]; + this.uriMatchOptions = [ + { name: i18nService.t("defaultMatchDetection"), value: null }, + { name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, + { name: i18nService.t("host"), value: UriMatchType.Host }, + { name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, + { name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, + { name: i18nService.t("exact"), value: UriMatchType.Exact }, + { name: i18nService.t("never"), value: UriMatchType.Never }, + ]; + this.autofillOnPageLoadOptions = [ + { name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null }, + { name: i18nService.t("autoFillOnPageLoadYes"), value: true }, + { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, + ]; + } + + async ngOnInit() { + await this.init(); + } + + async init() { + if (this.ownershipOptions.length) { + this.ownershipOptions = []; + } + if (await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership)) { + this.allowPersonal = false; + } else { + const myEmail = await this.stateService.getEmail(); + this.ownershipOptions.push({ name: myEmail, value: null }); } - async ngOnInit() { - await this.init(); + const orgs = await this.organizationService.getAll(); + orgs.sort(Utils.getSortFunction(this.i18nService, "name")).forEach((o) => { + if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { + this.ownershipOptions.push({ name: o.name, value: o.id }); + } + }); + if (!this.allowPersonal) { + this.organizationId = this.ownershipOptions[0].value; } - async init() { - if (this.ownershipOptions.length) { - this.ownershipOptions = []; - } - if (await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership)) { - this.allowPersonal = false; - } else { - const myEmail = await this.userService.getEmail(); - this.ownershipOptions.push({ name: myEmail, value: null }); - } + this.writeableCollections = await this.loadCollections(); - const orgs = await this.userService.getAllOrganizations(); - orgs.sort(Utils.getSortFunction(this.i18nService, 'name')).forEach(o => { - if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { - this.ownershipOptions.push({ name: o.name, value: o.id }); - } - }); - if (!this.allowPersonal) { - this.organizationId = this.ownershipOptions[0].value; - } + this.canUseReprompt = await this.passwordRepromptService.enabled(); + } - this.writeableCollections = await this.loadCollections(); - - this.canUseReprompt = await this.passwordRepromptService.enabled(); + async load() { + this.editMode = this.cipherId != null; + if (this.editMode) { + this.editMode = true; + if (this.cloneMode) { + this.cloneMode = true; + this.title = this.i18nService.t("addItem"); + } else { + this.title = this.i18nService.t("editItem"); + } + } else { + this.title = this.i18nService.t("addItem"); } - async load() { - this.editMode = this.cipherId != null; - if (this.editMode) { - this.editMode = true; - if (this.cloneMode) { - this.cloneMode = true; - this.title = this.i18nService.t('addItem'); - } else { - this.title = this.i18nService.t('editItem'); - } - } else { - this.title = this.i18nService.t('addItem'); - } - - const addEditCipherInfo: any = await this.stateService.get('addEditCipherInfo'); - if (addEditCipherInfo != null) { - this.cipher = addEditCipherInfo.cipher; - this.collectionIds = addEditCipherInfo.collectionIds; - } - await this.stateService.remove('addEditCipherInfo'); - - if (this.cipher == null) { - if (this.editMode) { - const cipher = await this.loadCipher(); - this.cipher = await cipher.decrypt(); - - // Adjust Cipher Name if Cloning - if (this.cloneMode) { - this.cipher.name += ' - ' + this.i18nService.t('clone'); - // If not allowing personal ownership, update cipher's org Id to prompt downstream changes - if (this.cipher.organizationId == null && !this.allowPersonal) { - this.cipher.organizationId = this.organizationId; - } - } - } else { - this.cipher = new CipherView(); - this.cipher.organizationId = this.organizationId == null ? null : this.organizationId; - this.cipher.folderId = this.folderId; - this.cipher.type = this.type == null ? CipherType.Login : this.type; - this.cipher.login = new LoginView(); - this.cipher.login.uris = [new LoginUriView()]; - this.cipher.card = new CardView(); - this.cipher.identity = new IdentityView(); - this.cipher.secureNote = new SecureNoteView(); - this.cipher.secureNote.type = SecureNoteType.Generic; - this.cipher.reprompt = CipherRepromptType.None; - } - } - - if (this.cipher != null && (!this.editMode || addEditCipherInfo != null || this.cloneMode)) { - await this.organizationChanged(); - if (this.collectionIds != null && this.collectionIds.length > 0 && this.collections.length > 0) { - this.collections.forEach(c => { - if (this.collectionIds.indexOf(c.id) > -1) { - (c as any).checked = true; - } - }); - } - } - - this.folders = await this.folderService.getAllDecrypted(); - - if (this.editMode && this.previousCipherId !== this.cipherId) { - this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId); - } - this.previousCipherId = this.cipherId; - this.reprompt = this.cipher.reprompt !== CipherRepromptType.None; + const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo(); + if (addEditCipherInfo != null) { + this.cipher = addEditCipherInfo.cipher; + this.collectionIds = addEditCipherInfo.collectionIds; } + await this.stateService.setAddEditCipherInfo(null); - async submit(): Promise { - if (this.cipher.isDeleted) { - return this.restore(); - } + if (this.cipher == null) { + if (this.editMode) { + const cipher = await this.loadCipher(); + this.cipher = await cipher.decrypt(); - if (this.cipher.name == null || this.cipher.name === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('nameRequired')); - return false; - } - - if ((!this.editMode || this.cloneMode) && !this.allowPersonal && this.cipher.organizationId == null) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('personalOwnershipSubmitError')); - return false; - } - - if ((!this.editMode || this.cloneMode) && this.cipher.type === CipherType.Login && - this.cipher.login.uris != null && this.cipher.login.uris.length === 1 && - (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { - this.cipher.login.uris = null; - } - - // Allows saving of selected collections during "Add" and "Clone" flows - if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) { - this.cipher.collectionIds = this.collections == null ? [] : - this.collections.filter(c => (c as any).checked).map(c => c.id); - } - - // Clear current Cipher Id to trigger "Add" cipher flow + // Adjust Cipher Name if Cloning if (this.cloneMode) { - this.cipher.id = null; + this.cipher.name += " - " + this.i18nService.t("clone"); + // If not allowing personal ownership, update cipher's org Id to prompt downstream changes + if (this.cipher.organizationId == null && !this.allowPersonal) { + this.cipher.organizationId = this.organizationId; + } } + } else { + this.cipher = new CipherView(); + this.cipher.organizationId = this.organizationId == null ? null : this.organizationId; + this.cipher.folderId = this.folderId; + this.cipher.type = this.type == null ? CipherType.Login : this.type; + this.cipher.login = new LoginView(); + this.cipher.login.uris = [new LoginUriView()]; + this.cipher.card = new CardView(); + this.cipher.identity = new IdentityView(); + this.cipher.secureNote = new SecureNoteView(); + this.cipher.secureNote.type = SecureNoteType.Generic; + this.cipher.reprompt = CipherRepromptType.None; + } + } - const cipher = await this.encryptCipher(); - try { - this.formPromise = this.saveCipher(cipher); - await this.formPromise; - this.cipher.id = cipher.id; - this.platformUtilsService.showToast('success', null, - this.i18nService.t(this.editMode && !this.cloneMode ? 'editedItem' : 'addedItem')); - this.onSavedCipher.emit(this.cipher); - this.messagingService.send(this.editMode && !this.cloneMode ? 'editedCipher' : 'addedCipher'); - return true; - } catch (e) { - this.logService.error(e); - } + if (this.cipher != null && (!this.editMode || addEditCipherInfo != null || this.cloneMode)) { + await this.organizationChanged(); + if ( + this.collectionIds != null && + this.collectionIds.length > 0 && + this.collections.length > 0 + ) { + this.collections.forEach((c) => { + if (this.collectionIds.indexOf(c.id) > -1) { + (c as any).checked = true; + } + }); + } + } + this.folders = await this.folderService.getAllDecrypted(); + + if (this.editMode && this.previousCipherId !== this.cipherId) { + this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId); + } + this.previousCipherId = this.cipherId; + this.reprompt = this.cipher.reprompt !== CipherRepromptType.None; + } + + async submit(): Promise { + if (this.cipher.isDeleted) { + return this.restore(); + } + + if (this.cipher.name == null || this.cipher.name === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nameRequired") + ); + return false; + } + + if ( + (!this.editMode || this.cloneMode) && + !this.allowPersonal && + this.cipher.organizationId == null + ) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("personalOwnershipSubmitError") + ); + return false; + } + + if ( + (!this.editMode || this.cloneMode) && + this.cipher.type === CipherType.Login && + this.cipher.login.uris != null && + this.cipher.login.uris.length === 1 && + (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "") + ) { + this.cipher.login.uris = null; + } + + // Allows saving of selected collections during "Add" and "Clone" flows + if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) { + this.cipher.collectionIds = + this.collections == null + ? [] + : this.collections.filter((c) => (c as any).checked).map((c) => c.id); + } + + // Clear current Cipher Id to trigger "Add" cipher flow + if (this.cloneMode) { + this.cipher.id = null; + } + + const cipher = await this.encryptCipher(); + try { + this.formPromise = this.saveCipher(cipher); + await this.formPromise; + this.cipher.id = cipher.id; + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem") + ); + this.onSavedCipher.emit(this.cipher); + this.messagingService.send(this.editMode && !this.cloneMode ? "editedCipher" : "addedCipher"); + return true; + } catch (e) { + this.logService.error(e); + } + + return false; + } + + addUri() { + if (this.cipher.type !== CipherType.Login) { + return; + } + + if (this.cipher.login.uris == null) { + this.cipher.login.uris = []; + } + + this.cipher.login.uris.push(new LoginUriView()); + } + + removeUri(uri: LoginUriView) { + if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) { + return; + } + + const i = this.cipher.login.uris.indexOf(uri); + if (i > -1) { + this.cipher.login.uris.splice(i, 1); + } + } + + trackByFunction(index: number, item: any) { + return index; + } + + cancel() { + this.onCancelled.emit(this.cipher); + } + + attachments() { + this.onEditAttachments.emit(this.cipher); + } + + share() { + this.onShareCipher.emit(this.cipher); + } + + editCollections() { + this.onEditCollections.emit(this.cipher); + } + + async delete(): Promise { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation" + ), + this.i18nService.t("deleteItem"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; + } + + try { + this.deletePromise = this.deleteCipher(); + await this.deletePromise; + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem") + ); + this.onDeletedCipher.emit(this.cipher); + this.messagingService.send( + this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher" + ); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("restoreItemConfirmation"), + this.i18nService.t("restoreItem"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; + } + + try { + this.restorePromise = this.restoreCipher(); + await this.restorePromise; + this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.onRestoredCipher.emit(this.cipher); + this.messagingService.send("restoredCipher"); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + async generatePassword(): Promise { + if ( + this.cipher.login != null && + this.cipher.login.password != null && + this.cipher.login.password.length + ) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("overwritePasswordConfirmation"), + this.i18nService.t("overwritePassword"), + this.i18nService.t("yes"), + this.i18nService.t("no") + ); + if (!confirmed) { return false; + } } - addUri() { - if (this.cipher.type !== CipherType.Login) { - return; - } + this.onGeneratePassword.emit(); + return true; + } - if (this.cipher.login.uris == null) { - this.cipher.login.uris = []; - } + togglePassword() { + this.showPassword = !this.showPassword; + document.getElementById("loginPassword").focus(); + if (this.editMode && this.showPassword) { + this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId); + } + } - this.cipher.login.uris.push(new LoginUriView()); + async toggleCardNumber() { + this.showCardNumber = !this.showCardNumber; + if (this.showCardNumber) { + this.eventService.collect(EventType.Cipher_ClientToggledCardNumberVisible, this.cipherId); + } + } + + toggleCardCode() { + this.showCardCode = !this.showCardCode; + document.getElementById("cardCode").focus(); + if (this.editMode && this.showCardCode) { + this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId); + } + } + + toggleUriOptions(uri: LoginUriView) { + const u = uri as any; + u.showOptions = u.showOptions == null && uri.match != null ? false : !u.showOptions; + } + + loginUriMatchChanged(uri: LoginUriView) { + const u = uri as any; + u.showOptions = u.showOptions == null ? true : u.showOptions; + } + + async organizationChanged() { + if (this.writeableCollections != null) { + this.writeableCollections.forEach((c) => ((c as any).checked = false)); + } + if (this.cipher.organizationId != null) { + this.collections = this.writeableCollections.filter( + (c) => c.organizationId === this.cipher.organizationId + ); + const org = await this.organizationService.get(this.cipher.organizationId); + if (org != null) { + this.cipher.organizationUseTotp = org.useTotp; + } + } else { + this.collections = []; + } + } + + async checkPassword() { + if (this.checkPasswordPromise != null) { + return; } - removeUri(uri: LoginUriView) { - if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) { - return; - } - - const i = this.cipher.login.uris.indexOf(uri); - if (i > -1) { - this.cipher.login.uris.splice(i, 1); - } + if ( + this.cipher.login == null || + this.cipher.login.password == null || + this.cipher.login.password === "" + ) { + return; } - trackByFunction(index: number, item: any) { - return index; + this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); + const matches = await this.checkPasswordPromise; + this.checkPasswordPromise = null; + + if (matches > 0) { + this.platformUtilsService.showToast( + "warning", + null, + this.i18nService.t("passwordExposed", matches.toString()) + ); + } else { + this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe")); } + } - cancel() { - this.onCancelled.emit(this.cipher); + repromptChanged() { + this.reprompt = !this.reprompt; + if (this.reprompt) { + this.cipher.reprompt = CipherRepromptType.Password; + } else { + this.cipher.reprompt = CipherRepromptType.None; } + } - attachments() { - this.onEditAttachments.emit(this.cipher); - } + protected async loadCollections() { + const allCollections = await this.collectionService.getAllDecrypted(); + return allCollections.filter((c) => !c.readOnly); + } - share() { - this.onShareCipher.emit(this.cipher); - } + protected loadCipher() { + return this.cipherService.get(this.cipherId); + } - editCollections() { - this.onEditCollections.emit(this.cipher); - } + protected encryptCipher() { + return this.cipherService.encrypt(this.cipher); + } - async delete(): Promise { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'), - this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return false; - } + protected saveCipher(cipher: Cipher) { + return this.cipherService.saveWithServer(cipher); + } - try { - this.deletePromise = this.deleteCipher(); - await this.deletePromise; - this.platformUtilsService.showToast('success', null, - this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeletedItem' : 'deletedItem')); - this.onDeletedCipher.emit(this.cipher); - this.messagingService.send(this.cipher.isDeleted ? 'permanentlyDeletedCipher' : 'deletedCipher'); - } catch (e) { - this.logService.error(e); - } + protected deleteCipher() { + return this.cipher.isDeleted + ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } - return true; - } - - async restore(): Promise { - if (!this.cipher.isDeleted) { - return false; - } - - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('restoreItemConfirmation'), this.i18nService.t('restoreItem'), - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return false; - } - - try { - this.restorePromise = this.restoreCipher(); - await this.restorePromise; - this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem')); - this.onRestoredCipher.emit(this.cipher); - this.messagingService.send('restoredCipher'); - } catch (e) { - this.logService.error(e); - } - - return true; - } - - async generatePassword(): Promise { - if (this.cipher.login != null && this.cipher.login.password != null && this.cipher.login.password.length) { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('overwritePasswordConfirmation'), this.i18nService.t('overwritePassword'), - this.i18nService.t('yes'), this.i18nService.t('no')); - if (!confirmed) { - return false; - } - } - - this.onGeneratePassword.emit(); - return true; - } - - togglePassword() { - this.showPassword = !this.showPassword; - document.getElementById('loginPassword').focus(); - if (this.editMode && this.showPassword) { - this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId); - } - } - - async toggleCardNumber() { - this.showCardNumber = !this.showCardNumber; - if (this.showCardNumber) { - this.eventService.collect(EventType.Cipher_ClientToggledCardNumberVisible, this.cipherId); - } - } - - toggleCardCode() { - this.showCardCode = !this.showCardCode; - document.getElementById('cardCode').focus(); - if (this.editMode && this.showCardCode) { - this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId); - } - } - - toggleUriOptions(uri: LoginUriView) { - const u = (uri as any); - u.showOptions = u.showOptions == null && uri.match != null ? false : !u.showOptions; - } - - loginUriMatchChanged(uri: LoginUriView) { - const u = (uri as any); - u.showOptions = u.showOptions == null ? true : u.showOptions; - } - - async organizationChanged() { - if (this.writeableCollections != null) { - this.writeableCollections.forEach(c => (c as any).checked = false); - } - if (this.cipher.organizationId != null) { - this.collections = this.writeableCollections.filter(c => c.organizationId === this.cipher.organizationId); - const org = await this.userService.getOrganization(this.cipher.organizationId); - if (org != null) { - this.cipher.organizationUseTotp = org.useTotp; - } - } else { - this.collections = []; - } - } - - async checkPassword() { - if (this.checkPasswordPromise != null) { - return; - } - - if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') { - return; - } - - this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); - const matches = await this.checkPasswordPromise; - this.checkPasswordPromise = null; - - if (matches > 0) { - this.platformUtilsService.showToast('warning', null, - this.i18nService.t('passwordExposed', matches.toString())); - } else { - this.platformUtilsService.showToast('success', null, this.i18nService.t('passwordSafe')); - } - } - - repromptChanged() { - this.reprompt = !this.reprompt; - if (this.reprompt) { - this.cipher.reprompt = CipherRepromptType.Password; - } else { - this.cipher.reprompt = CipherRepromptType.None; - } - } - - protected async loadCollections() { - const allCollections = await this.collectionService.getAllDecrypted(); - return allCollections.filter(c => !c.readOnly); - } - - protected loadCipher() { - return this.cipherService.get(this.cipherId); - } - - protected encryptCipher() { - return this.cipherService.encrypt(this.cipher); - } - - protected saveCipher(cipher: Cipher) { - return this.cipherService.saveWithServer(cipher); - } - - protected deleteCipher() { - return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id) - : this.cipherService.softDeleteWithServer(this.cipher.id); - } - - protected restoreCipher() { - return this.cipherService.restoreWithServer(this.cipher.id); - } + protected restoreCipher() { + return this.cipherService.restoreWithServer(this.cipher.id); + } } diff --git a/angular/src/components/attachments.component.ts b/angular/src/components/attachments.component.ts index dcbaf19b..bff508e2 100644 --- a/angular/src/components/attachments.component.ts +++ b/angular/src/components/attachments.component.ts @@ -1,250 +1,291 @@ -import { - Directive, - EventEmitter, - Input, - OnInit, - Output, -} from '@angular/core'; +import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { Cipher } from 'jslib-common/models/domain/cipher'; -import { ErrorResponse } from 'jslib-common/models/response/errorResponse'; +import { Cipher } from "jslib-common/models/domain/cipher"; +import { ErrorResponse } from "jslib-common/models/response/errorResponse"; -import { AttachmentView } from 'jslib-common/models/view/attachmentView'; -import { CipherView } from 'jslib-common/models/view/cipherView'; +import { AttachmentView } from "jslib-common/models/view/attachmentView"; +import { CipherView } from "jslib-common/models/view/cipherView"; @Directive() export class AttachmentsComponent implements OnInit { - @Input() cipherId: string; - @Output() onUploadedAttachment = new EventEmitter(); - @Output() onDeletedAttachment = new EventEmitter(); - @Output() onReuploadedAttachment = new EventEmitter(); + @Input() cipherId: string; + @Output() onUploadedAttachment = new EventEmitter(); + @Output() onDeletedAttachment = new EventEmitter(); + @Output() onReuploadedAttachment = new EventEmitter(); - cipher: CipherView; - cipherDomain: Cipher; - hasUpdatedKey: boolean; - canAccessAttachments: boolean; - formPromise: Promise; - deletePromises: { [id: string]: Promise; } = {}; - reuploadPromises: { [id: string]: Promise; } = {}; - emergencyAccessId?: string = null; + cipher: CipherView; + cipherDomain: Cipher; + hasUpdatedKey: boolean; + canAccessAttachments: boolean; + formPromise: Promise; + deletePromises: { [id: string]: Promise } = {}; + reuploadPromises: { [id: string]: Promise } = {}; + emergencyAccessId?: string = null; - constructor(protected cipherService: CipherService, protected i18nService: I18nService, - protected cryptoService: CryptoService, protected userService: UserService, - protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService, - protected win: Window, private logService: LogService) { } + constructor( + protected cipherService: CipherService, + protected i18nService: I18nService, + protected cryptoService: CryptoService, + protected platformUtilsService: PlatformUtilsService, + protected apiService: ApiService, + protected win: Window, + protected logService: LogService, + protected stateService: StateService + ) {} - async ngOnInit() { - await this.init(); + async ngOnInit() { + await this.init(); + } + + async submit() { + if (!this.hasUpdatedKey) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("updateKey") + ); + return; } - async submit() { - if (!this.hasUpdatedKey) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('updateKey')); - return; - } - - const fileEl = document.getElementById('file') as HTMLInputElement; - const files = fileEl.files; - if (files == null || files.length === 0) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('selectFile')); - return; - } - - if (files[0].size > 524288000) { // 500 MB - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('maxFileSize')); - return; - } - - try { - this.formPromise = this.saveCipherAttachment(files[0]); - this.cipherDomain = await this.formPromise; - this.cipher = await this.cipherDomain.decrypt(); - this.platformUtilsService.showToast('success', null, this.i18nService.t('attachmentSaved')); - this.onUploadedAttachment.emit(); - } catch (e) { - this.logService.error(e); - } - - // reset file input - // ref: https://stackoverflow.com/a/20552042 - fileEl.type = ''; - fileEl.type = 'file'; - fileEl.value = ''; + const fileEl = document.getElementById("file") as HTMLInputElement; + const files = fileEl.files; + if (files == null || files.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("selectFile") + ); + return; } - async delete(attachment: AttachmentView) { - if (this.deletePromises[attachment.id] != null) { - return; - } - - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('deleteAttachmentConfirmation'), this.i18nService.t('deleteAttachment'), - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return; - } - - try { - this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); - await this.deletePromises[attachment.id]; - this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedAttachment')); - const i = this.cipher.attachments.indexOf(attachment); - if (i > -1) { - this.cipher.attachments.splice(i, 1); - } - } catch (e) { - this.logService.error(e); - } - - this.deletePromises[attachment.id] = null; - this.onDeletedAttachment.emit(); + if (files[0].size > 524288000) { + // 500 MB + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("maxFileSize") + ); + return; } - async download(attachment: AttachmentView) { - const a = (attachment as any); - if (a.downloading) { - return; - } + try { + this.formPromise = this.saveCipherAttachment(files[0]); + this.cipherDomain = await this.formPromise; + this.cipher = await this.cipherDomain.decrypt(); + this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved")); + this.onUploadedAttachment.emit(); + } catch (e) { + this.logService.error(e); + } - if (!this.canAccessAttachments) { - this.platformUtilsService.showToast('error', this.i18nService.t('premiumRequired'), - this.i18nService.t('premiumRequiredDesc')); - return; - } + // reset file input + // ref: https://stackoverflow.com/a/20552042 + fileEl.type = ""; + fileEl.type = "file"; + fileEl.value = ""; + } - let url: string; - try { - const attachmentDownloadResponse = await this.apiService.getAttachmentData(this.cipher.id, attachment.id, - this.emergencyAccessId); - url = attachmentDownloadResponse.url; - } catch (e) { - if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { - url = attachment.url; - } else if (e instanceof ErrorResponse) { - throw new Error((e as ErrorResponse).getSingleMessage()); - } else { - throw e; - } - } + async delete(attachment: AttachmentView) { + if (this.deletePromises[attachment.id] != null) { + return; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("deleteAttachmentConfirmation"), + this.i18nService.t("deleteAttachment"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return; + } + + try { + this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); + await this.deletePromises[attachment.id]; + this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedAttachment")); + const i = this.cipher.attachments.indexOf(attachment); + if (i > -1) { + this.cipher.attachments.splice(i, 1); + } + } catch (e) { + this.logService.error(e); + } + + this.deletePromises[attachment.id] = null; + this.onDeletedAttachment.emit(); + } + + async download(attachment: AttachmentView) { + const a = attachment as any; + if (a.downloading) { + return; + } + + if (!this.canAccessAttachments) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("premiumRequired"), + this.i18nService.t("premiumRequiredDesc") + ); + return; + } + + let url: string; + try { + const attachmentDownloadResponse = await this.apiService.getAttachmentData( + this.cipher.id, + attachment.id, + this.emergencyAccessId + ); + url = attachmentDownloadResponse.url; + } catch (e) { + if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { + url = attachment.url; + } else if (e instanceof ErrorResponse) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } else { + throw e; + } + } + + a.downloading = true; + const response = await fetch(new Request(url, { cache: "no-store" })); + if (response.status !== 200) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + a.downloading = false; + return; + } + + try { + const buf = await response.arrayBuffer(); + const key = + attachment.key != null + ? attachment.key + : await this.cryptoService.getOrgKey(this.cipher.organizationId); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); + } catch (e) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + } + + a.downloading = false; + } + + protected async init() { + this.cipherDomain = await this.loadCipher(); + this.cipher = await this.cipherDomain.decrypt(); + + this.hasUpdatedKey = await this.cryptoService.hasEncKey(); + const canAccessPremium = await this.stateService.getCanAccessPremium(); + this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; + + if (!this.canAccessAttachments) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("premiumRequiredDesc"), + this.i18nService.t("premiumRequired"), + this.i18nService.t("learnMore"), + this.i18nService.t("cancel") + ); + if (confirmed) { + this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=purchase"); + } + } else if (!this.hasUpdatedKey) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("updateKey"), + this.i18nService.t("featureUnavailable"), + this.i18nService.t("learnMore"), + this.i18nService.t("cancel"), + "warning" + ); + if (confirmed) { + this.platformUtilsService.launchUri( + "https://help.bitwarden.com/article/update-encryption-key/" + ); + } + } + } + + protected async reuploadCipherAttachment(attachment: AttachmentView, admin: boolean) { + const a = attachment as any; + if (attachment.key != null || a.downloading || this.reuploadPromises[attachment.id] != null) { + return; + } + + try { + this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => { + // 1. Download a.downloading = true; - const response = await fetch(new Request(url, { cache: 'no-store' })); + const response = await fetch(new Request(attachment.url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); - a.downloading = false; - return; + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + a.downloading = false; + return; } try { - const buf = await response.arrayBuffer(); - const key = attachment.key != null ? attachment.key : - await this.cryptoService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.cryptoService.decryptFromBytes(buf, key); - this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); + // 2. Resave + const buf = await response.arrayBuffer(); + const key = + attachment.key != null + ? attachment.key + : await this.cryptoService.getOrgKey(this.cipher.organizationId); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer( + this.cipherDomain, + attachment.fileName, + decBuf, + admin + ); + this.cipher = await this.cipherDomain.decrypt(); + + // 3. Delete old + this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); + await this.deletePromises[attachment.id]; + const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id); + if (foundAttachment.length > 0) { + const i = this.cipher.attachments.indexOf(foundAttachment[0]); + if (i > -1) { + this.cipher.attachments.splice(i, 1); + } + } + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("attachmentSaved") + ); + this.onReuploadedAttachment.emit(); } catch (e) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } a.downloading = false; + }); + await this.reuploadPromises[attachment.id]; + } catch (e) { + this.logService.error(e); } + } - protected async init() { - this.cipherDomain = await this.loadCipher(); - this.cipher = await this.cipherDomain.decrypt(); + protected loadCipher() { + return this.cipherService.get(this.cipherId); + } - this.hasUpdatedKey = await this.cryptoService.hasEncKey(); - const canAccessPremium = await this.userService.canAccessPremium(); - this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; + protected saveCipherAttachment(file: File) { + return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file); + } - if (!this.canAccessAttachments) { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'), - this.i18nService.t('learnMore'), this.i18nService.t('cancel')); - if (confirmed) { - this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); - } - } else if (!this.hasUpdatedKey) { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('updateKey'), this.i18nService.t('featureUnavailable'), - this.i18nService.t('learnMore'), this.i18nService.t('cancel'), 'warning'); - if (confirmed) { - this.platformUtilsService.launchUri('https://help.bitwarden.com/article/update-encryption-key/'); - } - } - } - - protected async reuploadCipherAttachment(attachment: AttachmentView, admin: boolean) { - const a = (attachment as any); - if (attachment.key != null || a.downloading || this.reuploadPromises[attachment.id] != null) { - return; - } - - try { - this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => { - // 1. Download - a.downloading = true; - const response = await fetch(new Request(attachment.url, { cache: 'no-store' })); - if (response.status !== 200) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); - a.downloading = false; - return; - } - - try { - // 2. Resave - const buf = await response.arrayBuffer(); - const key = attachment.key != null ? attachment.key : - await this.cryptoService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.cryptoService.decryptFromBytes(buf, key); - this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer( - this.cipherDomain, attachment.fileName, decBuf, admin); - this.cipher = await this.cipherDomain.decrypt(); - - // 3. Delete old - this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); - await this.deletePromises[attachment.id]; - const foundAttachment = this.cipher.attachments.filter(a2 => a2.id === attachment.id); - if (foundAttachment.length > 0) { - const i = this.cipher.attachments.indexOf(foundAttachment[0]); - if (i > -1) { - this.cipher.attachments.splice(i, 1); - } - } - - this.platformUtilsService.showToast('success', null, this.i18nService.t('attachmentSaved')); - this.onReuploadedAttachment.emit(); - } catch (e) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); - } - - a.downloading = false; - }); - await this.reuploadPromises[attachment.id]; - } catch (e) { - this.logService.error(e); - } - } - - protected loadCipher() { - return this.cipherService.get(this.cipherId); - } - - protected saveCipherAttachment(file: File) { - return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file); - } - - protected deleteCipherAttachment(attachmentId: string) { - return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId); - } + protected deleteCipherAttachment(attachmentId: string) { + return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId); + } } diff --git a/angular/src/components/avatar.component.ts b/angular/src/components/avatar.component.ts index 1760c521..e00a68a8 100644 --- a/angular/src/components/avatar.component.ts +++ b/angular/src/components/avatar.component.ts @@ -1,138 +1,143 @@ -import { - Component, - Input, - OnChanges, - OnInit, -} from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; +import { Component, Input, OnChanges, OnInit } from "@angular/core"; +import { DomSanitizer } from "@angular/platform-browser"; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; @Component({ - selector: 'app-avatar', - template: '', + selector: "app-avatar", + template: + '", }) export class AvatarComponent implements OnChanges, OnInit { - @Input() data: string; - @Input() email: string; - @Input() size = 45; - @Input() charCount = 2; - @Input() textColor = '#ffffff'; - @Input() fontSize = 20; - @Input() fontWeight = 300; - @Input() dynamic = false; - @Input() circle = false; + @Input() data: string; + @Input() email: string; + @Input() size = 45; + @Input() charCount = 2; + @Input() textColor = "#ffffff"; + @Input() fontSize = 20; + @Input() fontWeight = 300; + @Input() dynamic = false; + @Input() circle = false; - src: string; + src: string; - constructor(public sanitizer: DomSanitizer, private cryptoFunctionService: CryptoFunctionService, - private stateService: StateService) { } + constructor( + public sanitizer: DomSanitizer, + private cryptoFunctionService: CryptoFunctionService, + private stateService: StateService + ) {} - ngOnInit() { - if (!this.dynamic) { - this.generate(); - } + ngOnInit() { + if (!this.dynamic) { + this.generate(); } + } - ngOnChanges() { - if (this.dynamic) { - this.generate(); - } + ngOnChanges() { + if (this.dynamic) { + this.generate(); } + } - private async generate() { - const enableGravatars = await this.stateService.get('enableGravatars'); - if (enableGravatars && this.email != null) { - const hashBytes = await this.cryptoFunctionService.hash(this.email.toLowerCase().trim(), 'md5'); - const hash = Utils.fromBufferToHex(hashBytes).toLowerCase(); - this.src = 'https://www.gravatar.com/avatar/' + hash + '?s=' + this.size + '&r=pg&d=retro'; - } else { - let chars: string = null; - const upperData = this.data.toUpperCase(); + private async generate() { + const enableGravatars = await this.stateService.getEnableGravitars(); + if (enableGravatars && this.email != null) { + const hashBytes = await this.cryptoFunctionService.hash( + this.email.toLowerCase().trim(), + "md5" + ); + const hash = Utils.fromBufferToHex(hashBytes).toLowerCase(); + this.src = "https://www.gravatar.com/avatar/" + hash + "?s=" + this.size + "&r=pg&d=retro"; + } else { + let chars: string = null; + const upperData = this.data.toUpperCase(); - if (this.charCount > 1) { - chars = this.getFirstLetters(upperData, this.charCount); - } - if (chars == null) { - chars = this.unicodeSafeSubstring(upperData, this.charCount); - } + if (this.charCount > 1) { + chars = this.getFirstLetters(upperData, this.charCount); + } + if (chars == null) { + chars = this.unicodeSafeSubstring(upperData, this.charCount); + } - // If the chars contain an emoji, only show it. - if (chars.match(Utils.regexpEmojiPresentation)) { - chars = chars.match(Utils.regexpEmojiPresentation)[0]; - } + // If the chars contain an emoji, only show it. + if (chars.match(Utils.regexpEmojiPresentation)) { + chars = chars.match(Utils.regexpEmojiPresentation)[0]; + } - const charObj = this.getCharText(chars); - const color = this.stringToColor(upperData); - const svg = this.getSvg(this.size, color); - svg.appendChild(charObj); - const html = window.document.createElement('div').appendChild(svg).outerHTML; - const svgHtml = window.btoa(unescape(encodeURIComponent(html))); - this.src = 'data:image/svg+xml;base64,' + svgHtml; - } + const charObj = this.getCharText(chars); + const color = this.stringToColor(upperData); + const svg = this.getSvg(this.size, color); + svg.appendChild(charObj); + const html = window.document.createElement("div").appendChild(svg).outerHTML; + const svgHtml = window.btoa(unescape(encodeURIComponent(html))); + this.src = "data:image/svg+xml;base64," + svgHtml; } + } - private stringToColor(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - // tslint:disable-next-line - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - let color = '#'; - for (let i = 0; i < 3; i++) { - // tslint:disable-next-line - const value = (hash >> (i * 8)) & 0xFF; - color += ('00' + value.toString(16)).substr(-2); - } - return color; + private stringToColor(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + // tslint:disable-next-line + hash = str.charCodeAt(i) + ((hash << 5) - hash); } + let color = "#"; + for (let i = 0; i < 3; i++) { + // tslint:disable-next-line + const value = (hash >> (i * 8)) & 0xff; + color += ("00" + value.toString(16)).substr(-2); + } + return color; + } - private getFirstLetters(data: string, count: number): string { - const parts = data.split(' '); - if (parts.length > 1) { - let text = ''; - for (let i = 0; i < count; i++) { - text += this.unicodeSafeSubstring(parts[i], 1); - } - return text; - } - return null; + private getFirstLetters(data: string, count: number): string { + const parts = data.split(" "); + if (parts.length > 1) { + let text = ""; + for (let i = 0; i < count; i++) { + text += this.unicodeSafeSubstring(parts[i], 1); + } + return text; } + return null; + } - private getSvg(size: number, color: string): HTMLElement { - const svgTag = window.document.createElement('svg'); - svgTag.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); - svgTag.setAttribute('pointer-events', 'none'); - svgTag.setAttribute('width', size.toString()); - svgTag.setAttribute('height', size.toString()); - svgTag.style.backgroundColor = color; - svgTag.style.width = size + 'px'; - svgTag.style.height = size + 'px'; - return svgTag; - } + private getSvg(size: number, color: string): HTMLElement { + const svgTag = window.document.createElement("svg"); + svgTag.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + svgTag.setAttribute("pointer-events", "none"); + svgTag.setAttribute("width", size.toString()); + svgTag.setAttribute("height", size.toString()); + svgTag.style.backgroundColor = color; + svgTag.style.width = size + "px"; + svgTag.style.height = size + "px"; + return svgTag; + } - private getCharText(character: string): HTMLElement { - const textTag = window.document.createElement('text'); - textTag.setAttribute('text-anchor', 'middle'); - textTag.setAttribute('y', '50%'); - textTag.setAttribute('x', '50%'); - textTag.setAttribute('dy', '0.35em'); - textTag.setAttribute('pointer-events', 'auto'); - textTag.setAttribute('fill', this.textColor); - textTag.setAttribute('font-family', '"Open Sans","Helvetica Neue",Helvetica,Arial,' + - 'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'); - textTag.textContent = character; - textTag.style.fontWeight = this.fontWeight.toString(); - textTag.style.fontSize = this.fontSize + 'px'; - return textTag; - } + private getCharText(character: string): HTMLElement { + const textTag = window.document.createElement("text"); + textTag.setAttribute("text-anchor", "middle"); + textTag.setAttribute("y", "50%"); + textTag.setAttribute("x", "50%"); + textTag.setAttribute("dy", "0.35em"); + textTag.setAttribute("pointer-events", "auto"); + textTag.setAttribute("fill", this.textColor); + textTag.setAttribute( + "font-family", + '"Open Sans","Helvetica Neue",Helvetica,Arial,' + + 'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"' + ); + textTag.textContent = character; + textTag.style.fontWeight = this.fontWeight.toString(); + textTag.style.fontSize = this.fontSize + "px"; + return textTag; + } - private unicodeSafeSubstring(str: string, count: number) { - const characters = str.match(/./ug); - return characters != null ? characters.slice(0, count).join('') : ''; - } + private unicodeSafeSubstring(str: string, count: number) { + const characters = str.match(/./gu); + return characters != null ? characters.slice(0, count).join("") : ""; + } } diff --git a/angular/src/components/change-password.component.ts b/angular/src/components/change-password.component.ts index 5af79115..6ca81494 100644 --- a/angular/src/components/change-password.component.ts +++ b/angular/src/components/change-password.component.ts @@ -1,152 +1,197 @@ -import { Directive, OnInit } from '@angular/core'; +import { Directive, OnInit } from "@angular/core"; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { EncString } from 'jslib-common/models/domain/encString'; -import { MasterPasswordPolicyOptions } from 'jslib-common/models/domain/masterPasswordPolicyOptions'; -import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; +import { EncString } from "jslib-common/models/domain/encString"; +import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions"; +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; -import { KdfType } from 'jslib-common/enums/kdfType'; +import { KdfType } from "jslib-common/enums/kdfType"; @Directive() export class ChangePasswordComponent implements OnInit { - masterPassword: string; - masterPasswordRetype: string; - formPromise: Promise; - masterPasswordScore: number; - enforcedPolicyOptions: MasterPasswordPolicyOptions; + masterPassword: string; + masterPasswordRetype: string; + formPromise: Promise; + masterPasswordScore: number; + enforcedPolicyOptions: MasterPasswordPolicyOptions; - protected email: string; - protected kdf: KdfType; - protected kdfIterations: number; + protected email: string; + protected kdf: KdfType; + protected kdfIterations: number; - private masterPasswordStrengthTimeout: any; + private masterPasswordStrengthTimeout: any; - constructor(protected i18nService: I18nService, protected cryptoService: CryptoService, - protected messagingService: MessagingService, protected userService: UserService, - protected passwordGenerationService: PasswordGenerationService, - protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService) { } + constructor( + protected i18nService: I18nService, + protected cryptoService: CryptoService, + protected messagingService: MessagingService, + protected passwordGenerationService: PasswordGenerationService, + protected platformUtilsService: PlatformUtilsService, + protected policyService: PolicyService, + protected stateService: StateService + ) {} - async ngOnInit() { - this.email = await this.userService.getEmail(); - this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); + async ngOnInit() { + this.email = await this.stateService.getEmail(); + this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); + } + + async submit() { + if (!(await this.strongPassword())) { + return; } - async submit() { - if (!await this.strongPassword()) { - return; - } - - if (!await this.setupSubmitActions()) { - return; - } - - const email = await this.userService.getEmail(); - if (this.kdf == null) { - this.kdf = await this.userService.getKdf(); - } - if (this.kdfIterations == null) { - this.kdfIterations = await this.userService.getKdfIterations(); - } - const key = await this.cryptoService.makeKey(this.masterPassword, email.trim().toLowerCase(), - this.kdf, this.kdfIterations); - const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key); - - let encKey: [SymmetricCryptoKey, EncString] = null; - const existingEncKey = await this.cryptoService.getEncKey(); - if (existingEncKey == null) { - encKey = await this.cryptoService.makeEncKey(key); - } else { - encKey = await this.cryptoService.remakeEncKey(key); - } - - await this.performSubmitActions(masterPasswordHash, key, encKey); + if (!(await this.setupSubmitActions())) { + return; } - async setupSubmitActions(): Promise { - // Override in sub-class - // Can be used for additional validation and/or other processes the should occur before changing passwords - return true; + const email = await this.stateService.getEmail(); + if (this.kdf == null) { + this.kdf = await this.stateService.getKdfType(); + } + if (this.kdfIterations == null) { + this.kdfIterations = await this.stateService.getKdfIterations(); + } + const key = await this.cryptoService.makeKey( + this.masterPassword, + email.trim().toLowerCase(), + this.kdf, + this.kdfIterations + ); + const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key); + + let encKey: [SymmetricCryptoKey, EncString] = null; + const existingEncKey = await this.cryptoService.getEncKey(); + if (existingEncKey == null) { + encKey = await this.cryptoService.makeEncKey(key); + } else { + encKey = await this.cryptoService.remakeEncKey(key); } - async performSubmitActions(masterPasswordHash: string, key: SymmetricCryptoKey, - encKey: [SymmetricCryptoKey, EncString]) { - // Override in sub-class + await this.performSubmitActions(masterPasswordHash, key, encKey); + } + + async setupSubmitActions(): Promise { + // Override in sub-class + // Can be used for additional validation and/or other processes the should occur before changing passwords + return true; + } + + async performSubmitActions( + masterPasswordHash: string, + key: SymmetricCryptoKey, + encKey: [SymmetricCryptoKey, EncString] + ) { + // Override in sub-class + } + + async strongPassword(): Promise { + if (this.masterPassword == null || this.masterPassword === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPassRequired") + ); + return false; + } + if (this.masterPassword.length < 8) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPassLength") + ); + return false; + } + if (this.masterPassword !== this.masterPasswordRetype) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPassDoesntMatch") + ); + return false; } - async strongPassword(): Promise { - if (this.masterPassword == null || this.masterPassword === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassRequired')); - return false; - } - if (this.masterPassword.length < 8) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassLength')); - return false; - } - if (this.masterPassword !== this.masterPasswordRetype) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassDoesntMatch')); - return false; - } + const strengthResult = this.passwordGenerationService.passwordStrength( + this.masterPassword, + this.getPasswordStrengthUserInput() + ); - const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, - this.getPasswordStrengthUserInput()); - - if (this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - strengthResult.score, - this.masterPassword, - this.enforcedPolicyOptions)) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPasswordPolicyRequirementsNotMet')); - return false; - } - - if (strengthResult != null && strengthResult.score < 3) { - const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), - this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), - 'warning'); - if (!result) { - return false; - } - } - - return true; + if ( + this.enforcedPolicyOptions != null && + !this.policyService.evaluateMasterPassword( + strengthResult.score, + this.masterPassword, + this.enforcedPolicyOptions + ) + ) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPasswordPolicyRequirementsNotMet") + ); + return false; } - updatePasswordStrength() { - if (this.masterPasswordStrengthTimeout != null) { - clearTimeout(this.masterPasswordStrengthTimeout); - } - this.masterPasswordStrengthTimeout = setTimeout(() => { - const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, - this.getPasswordStrengthUserInput()); - this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; - }, 300); + if (strengthResult != null && strengthResult.score < 3) { + const result = await this.platformUtilsService.showDialog( + this.i18nService.t("weakMasterPasswordDesc"), + this.i18nService.t("weakMasterPassword"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!result) { + return false; + } } - async logOut() { - const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'), - this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel')); - if (confirmed) { - this.messagingService.send('logout'); - } - } + return true; + } - private getPasswordStrengthUserInput() { - let userInput: string[] = []; - const atPosition = this.email.indexOf('@'); - if (atPosition > -1) { - userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/)); - } - return userInput; + updatePasswordStrength() { + if (this.masterPasswordStrengthTimeout != null) { + clearTimeout(this.masterPasswordStrengthTimeout); } + this.masterPasswordStrengthTimeout = setTimeout(() => { + const strengthResult = this.passwordGenerationService.passwordStrength( + this.masterPassword, + this.getPasswordStrengthUserInput() + ); + this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; + }, 300); + } + + async logOut() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("logOutConfirmation"), + this.i18nService.t("logOut"), + this.i18nService.t("logOut"), + this.i18nService.t("cancel") + ); + if (confirmed) { + this.messagingService.send("logout"); + } + } + + private getPasswordStrengthUserInput() { + let userInput: string[] = []; + const atPosition = this.email.indexOf("@"); + if (atPosition > -1) { + userInput = userInput.concat( + this.email + .substr(0, atPosition) + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/) + ); + } + return userInput; + } } diff --git a/angular/src/components/groupings.component.ts b/angular/src/components/groupings.component.ts index 1c3c27a1..e8a97c6f 100644 --- a/angular/src/components/groupings.component.ts +++ b/angular/src/components/groupings.component.ts @@ -1,168 +1,160 @@ -import { - Directive, - EventEmitter, - Input, - Output, -} from '@angular/core'; +import { Directive, EventEmitter, Input, Output } from "@angular/core"; -import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherType } from "jslib-common/enums/cipherType"; -import { CollectionView } from 'jslib-common/models/view/collectionView'; -import { FolderView } from 'jslib-common/models/view/folderView'; +import { CollectionView } from "jslib-common/models/view/collectionView"; +import { FolderView } from "jslib-common/models/view/folderView"; -import { TreeNode } from 'jslib-common/models/domain/treeNode'; +import { TreeNode } from "jslib-common/models/domain/treeNode"; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { FolderService } from 'jslib-common/abstractions/folder.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; - -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { StateService } from "jslib-common/abstractions/state.service"; @Directive() export class GroupingsComponent { - @Input() showFolders = true; - @Input() showCollections = true; - @Input() showFavorites = true; - @Input() showTrash = true; + @Input() showFolders = true; + @Input() showCollections = true; + @Input() showFavorites = true; + @Input() showTrash = true; - @Output() onAllClicked = new EventEmitter(); - @Output() onFavoritesClicked = new EventEmitter(); - @Output() onTrashClicked = new EventEmitter(); - @Output() onCipherTypeClicked = new EventEmitter(); - @Output() onFolderClicked = new EventEmitter(); - @Output() onAddFolder = new EventEmitter(); - @Output() onEditFolder = new EventEmitter(); - @Output() onCollectionClicked = new EventEmitter(); + @Output() onAllClicked = new EventEmitter(); + @Output() onFavoritesClicked = new EventEmitter(); + @Output() onTrashClicked = new EventEmitter(); + @Output() onCipherTypeClicked = new EventEmitter(); + @Output() onFolderClicked = new EventEmitter(); + @Output() onAddFolder = new EventEmitter(); + @Output() onEditFolder = new EventEmitter(); + @Output() onCollectionClicked = new EventEmitter(); - folders: FolderView[]; - nestedFolders: TreeNode[]; - collections: CollectionView[]; - nestedCollections: TreeNode[]; - loaded: boolean = false; - cipherType = CipherType; - selectedAll: boolean = false; - selectedFavorites: boolean = false; - selectedTrash: boolean = false; - selectedType: CipherType = null; - selectedFolder: boolean = false; - selectedFolderId: string = null; - selectedCollectionId: string = null; + folders: FolderView[]; + nestedFolders: TreeNode[]; + collections: CollectionView[]; + nestedCollections: TreeNode[]; + loaded: boolean = false; + cipherType = CipherType; + selectedAll: boolean = false; + selectedFavorites: boolean = false; + selectedTrash: boolean = false; + selectedType: CipherType = null; + selectedFolder: boolean = false; + selectedFolderId: string = null; + selectedCollectionId: string = null; - private collapsedGroupings: Set; - private collapsedGroupingsKey: string; + private collapsedGroupings: Set; - constructor(protected collectionService: CollectionService, protected folderService: FolderService, - protected storageService: StorageService, protected userService: UserService) { } + constructor( + protected collectionService: CollectionService, + protected folderService: FolderService, + protected stateService: StateService + ) {} - async load(setLoaded = true) { - const userId = await this.userService.getUserId(); - this.collapsedGroupingsKey = ConstantsService.collapsedGroupingsKey + '_' + userId; - const collapsedGroupings = await this.storageService.get(this.collapsedGroupingsKey); - if (collapsedGroupings == null) { - this.collapsedGroupings = new Set(); - } else { - this.collapsedGroupings = new Set(collapsedGroupings); - } - - await this.loadFolders(); - await this.loadCollections(); - - if (setLoaded) { - this.loaded = true; - } + async load(setLoaded = true) { + const collapsedGroupings = await this.stateService.getCollapsedGroupings(); + if (collapsedGroupings == null) { + this.collapsedGroupings = new Set(); + } else { + this.collapsedGroupings = new Set(collapsedGroupings); } - async loadCollections(organizationId?: string) { - if (!this.showCollections) { - return; - } - const collections = await this.collectionService.getAllDecrypted(); - if (organizationId != null) { - this.collections = collections.filter(c => c.organizationId === organizationId); - } else { - this.collections = collections; - } - this.nestedCollections = await this.collectionService.getAllNested(this.collections); - } + await this.loadFolders(); + await this.loadCollections(); - async loadFolders() { - if (!this.showFolders) { - return; - } - this.folders = await this.folderService.getAllDecrypted(); - this.nestedFolders = await this.folderService.getAllNested(); + if (setLoaded) { + this.loaded = true; } + } - selectAll() { - this.clearSelections(); - this.selectedAll = true; - this.onAllClicked.emit(); + async loadCollections(organizationId?: string) { + if (!this.showCollections) { + return; } + const collections = await this.collectionService.getAllDecrypted(); + if (organizationId != null) { + this.collections = collections.filter((c) => c.organizationId === organizationId); + } else { + this.collections = collections; + } + this.nestedCollections = await this.collectionService.getAllNested(this.collections); + } - selectFavorites() { - this.clearSelections(); - this.selectedFavorites = true; - this.onFavoritesClicked.emit(); + async loadFolders() { + if (!this.showFolders) { + return; } + this.folders = await this.folderService.getAllDecrypted(); + this.nestedFolders = await this.folderService.getAllNested(); + } - selectTrash() { - this.clearSelections(); - this.selectedTrash = true; - this.onTrashClicked.emit(); - } + selectAll() { + this.clearSelections(); + this.selectedAll = true; + this.onAllClicked.emit(); + } - selectType(type: CipherType) { - this.clearSelections(); - this.selectedType = type; - this.onCipherTypeClicked.emit(type); - } + selectFavorites() { + this.clearSelections(); + this.selectedFavorites = true; + this.onFavoritesClicked.emit(); + } - selectFolder(folder: FolderView) { - this.clearSelections(); - this.selectedFolder = true; - this.selectedFolderId = folder.id; - this.onFolderClicked.emit(folder); - } + selectTrash() { + this.clearSelections(); + this.selectedTrash = true; + this.onTrashClicked.emit(); + } - addFolder() { - this.onAddFolder.emit(); - } + selectType(type: CipherType) { + this.clearSelections(); + this.selectedType = type; + this.onCipherTypeClicked.emit(type); + } - editFolder(folder: FolderView) { - this.onEditFolder.emit(folder); - } + selectFolder(folder: FolderView) { + this.clearSelections(); + this.selectedFolder = true; + this.selectedFolderId = folder.id; + this.onFolderClicked.emit(folder); + } - selectCollection(collection: CollectionView) { - this.clearSelections(); - this.selectedCollectionId = collection.id; - this.onCollectionClicked.emit(collection); - } + addFolder() { + this.onAddFolder.emit(); + } - clearSelections() { - this.selectedAll = false; - this.selectedFavorites = false; - this.selectedTrash = false; - this.selectedType = null; - this.selectedFolder = false; - this.selectedFolderId = null; - this.selectedCollectionId = null; - } + editFolder(folder: FolderView) { + this.onEditFolder.emit(folder); + } - collapse(grouping: FolderView | CollectionView, idPrefix = '') { - if (grouping.id == null) { - return; - } - const id = idPrefix + grouping.id; - if (this.isCollapsed(grouping, idPrefix)) { - this.collapsedGroupings.delete(id); - } else { - this.collapsedGroupings.add(id); - } - this.storageService.save(this.collapsedGroupingsKey, this.collapsedGroupings); - } + selectCollection(collection: CollectionView) { + this.clearSelections(); + this.selectedCollectionId = collection.id; + this.onCollectionClicked.emit(collection); + } - isCollapsed(grouping: FolderView | CollectionView, idPrefix = '') { - return this.collapsedGroupings.has(idPrefix + grouping.id); + clearSelections() { + this.selectedAll = false; + this.selectedFavorites = false; + this.selectedTrash = false; + this.selectedType = null; + this.selectedFolder = false; + this.selectedFolderId = null; + this.selectedCollectionId = null; + } + + async collapse(grouping: FolderView | CollectionView, idPrefix = "") { + if (grouping.id == null) { + return; } + const id = idPrefix + grouping.id; + if (this.isCollapsed(grouping, idPrefix)) { + this.collapsedGroupings.delete(id); + } else { + this.collapsedGroupings.add(id); + } + await this.stateService.setCollapsedGroupings(this.collapsedGroupings); + } + + isCollapsed(grouping: FolderView | CollectionView, idPrefix = "") { + return this.collapsedGroupings.has(idPrefix + grouping.id); + } } diff --git a/angular/src/components/icon.component.ts b/angular/src/components/icon.component.ts index b3a75d5a..a32948cb 100644 --- a/angular/src/components/icon.component.ts +++ b/angular/src/components/icon.component.ts @@ -1,107 +1,105 @@ -import { - Component, - Input, - OnChanges, -} from '@angular/core'; +import { Component, Input, OnChanges } from "@angular/core"; -import { CipherType } from 'jslib-common/enums/cipherType'; +import { CipherType } from "jslib-common/enums/cipherType"; -import { CipherView } from 'jslib-common/models/view/cipherView'; +import { CipherView } from "jslib-common/models/view/cipherView"; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { ConstantsService } from 'jslib-common/services/constants.service'; - -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; const IconMap: any = { - 'fa-globe': String.fromCharCode(0xf0ac), - 'fa-sticky-note-o': String.fromCharCode(0xf24a), - 'fa-id-card-o': String.fromCharCode(0xf2c3), - 'fa-credit-card': String.fromCharCode(0xf09d), - 'fa-android': String.fromCharCode(0xf17b), - 'fa-apple': String.fromCharCode(0xf179), + "fa-globe": String.fromCharCode(0xf0ac), + "fa-sticky-note-o": String.fromCharCode(0xf24a), + "fa-id-card-o": String.fromCharCode(0xf2c3), + "fa-credit-card": String.fromCharCode(0xf09d), + "fa-android": String.fromCharCode(0xf17b), + "fa-apple": String.fromCharCode(0xf179), }; @Component({ - selector: 'app-vault-icon', - templateUrl: 'icon.component.html', + selector: "app-vault-icon", + templateUrl: "icon.component.html", }) export class IconComponent implements OnChanges { - @Input() cipher: CipherView; - icon: string; - image: string; - fallbackImage: string; - imageEnabled: boolean; + @Input() cipher: CipherView; + icon: string; + image: string; + fallbackImage: string; + imageEnabled: boolean; - private iconsUrl: string; + private iconsUrl: string; - constructor(environmentService: EnvironmentService, protected stateService: StateService) { - this.iconsUrl = environmentService.getIconsUrl(); + constructor(environmentService: EnvironmentService, private stateService: StateService) { + this.iconsUrl = environmentService.getIconsUrl(); + } + + async ngOnChanges() { + // Components may be re-used when using cdk-virtual-scroll. Which puts the component in a weird state, + // to avoid this we reset all state variables. + this.image = null; + this.fallbackImage = null; + this.imageEnabled = !(await this.stateService.getDisableFavicon()); + this.load(); + } + + get iconCode(): string { + return IconMap[this.icon]; + } + + protected load() { + switch (this.cipher.type) { + case CipherType.Login: + this.icon = "fa-globe"; + this.setLoginIcon(); + break; + case CipherType.SecureNote: + this.icon = "fa-sticky-note-o"; + break; + case CipherType.Card: + this.icon = "fa-credit-card"; + break; + case CipherType.Identity: + this.icon = "fa-id-card-o"; + break; + default: + break; } + } - async ngOnChanges() { - // Components may be re-used when using cdk-virtual-scroll. Which puts the component in a weird state, - // to avoid this we reset all state variables. + private setLoginIcon() { + if (this.cipher.login.uri) { + let hostnameUri = this.cipher.login.uri; + let isWebsite = false; + + if (hostnameUri.indexOf("androidapp://") === 0) { + this.icon = "fa-android"; this.image = null; - this.fallbackImage = null; - this.imageEnabled = !(await this.stateService.get(ConstantsService.disableFaviconKey)); - this.load(); - } + } else if (hostnameUri.indexOf("iosapp://") === 0) { + this.icon = "fa-apple"; + this.image = null; + } else if ( + this.imageEnabled && + hostnameUri.indexOf("://") === -1 && + hostnameUri.indexOf(".") > -1 + ) { + hostnameUri = "http://" + hostnameUri; + isWebsite = true; + } else if (this.imageEnabled) { + isWebsite = hostnameUri.indexOf("http") === 0 && hostnameUri.indexOf(".") > -1; + } - get iconCode(): string { - return IconMap[this.icon]; - } - - protected load() { - switch (this.cipher.type) { - case CipherType.Login: - this.icon = 'fa-globe'; - this.setLoginIcon(); - break; - case CipherType.SecureNote: - this.icon = 'fa-sticky-note-o'; - break; - case CipherType.Card: - this.icon = 'fa-credit-card'; - break; - case CipherType.Identity: - this.icon = 'fa-id-card-o'; - break; - default: - break; - } - } - - private setLoginIcon() { - if (this.cipher.login.uri) { - let hostnameUri = this.cipher.login.uri; - let isWebsite = false; - - if (hostnameUri.indexOf('androidapp://') === 0) { - this.icon = 'fa-android'; - this.image = null; - } else if (hostnameUri.indexOf('iosapp://') === 0) { - this.icon = 'fa-apple'; - this.image = null; - } else if (this.imageEnabled && hostnameUri.indexOf('://') === -1 && hostnameUri.indexOf('.') > -1) { - hostnameUri = 'http://' + hostnameUri; - isWebsite = true; - } else if (this.imageEnabled) { - isWebsite = hostnameUri.indexOf('http') === 0 && hostnameUri.indexOf('.') > -1; - } - - if (this.imageEnabled && isWebsite) { - try { - this.image = this.iconsUrl + '/' + Utils.getHostname(hostnameUri) + '/icon.png'; - this.fallbackImage = 'images/fa-globe.png'; - } catch (e) { - // Ignore error since the fallback icon will be shown if image is null. - } - } - } else { - this.image = null; + if (this.imageEnabled && isWebsite) { + try { + this.image = this.iconsUrl + "/" + Utils.getHostname(hostnameUri) + "/icon.png"; + this.fallbackImage = "images/fa-globe.png"; + } catch (e) { + // Ignore error since the fallback icon will be shown if image is null. } + } + } else { + this.image = null; } + } } diff --git a/angular/src/components/lock.component.ts b/angular/src/components/lock.component.ts index b192ce6c..04672dcd 100644 --- a/angular/src/components/lock.component.ts +++ b/angular/src/components/lock.component.ts @@ -1,214 +1,279 @@ -import { Directive, NgZone, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { take } from 'rxjs/operators'; +import { Directive, NgZone, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { take } from "rxjs/operators"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { EncString } from "jslib-common/models/domain/encString"; +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; -import { EncString } from 'jslib-common/models/domain/encString'; -import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; +import { SecretVerificationRequest } from "jslib-common/models/request/secretVerificationRequest"; -import { SecretVerificationRequest } from 'jslib-common/models/request/secretVerificationRequest'; +import { Utils } from "jslib-common/misc/utils"; -import { Utils } from 'jslib-common/misc/utils'; - -import { HashPurpose } from 'jslib-common/enums/hashPurpose'; +import { HashPurpose } from "jslib-common/enums/hashPurpose"; +import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions"; @Directive() export class LockComponent implements OnInit { - masterPassword: string = ''; - pin: string = ''; - showPassword: boolean = false; - email: string; - pinLock: boolean = false; - webVaultHostname: string = ''; - formPromise: Promise; - supportsBiometric: boolean; - biometricLock: boolean; - biometricText: string; - hideInput: boolean; + masterPassword: string = ""; + pin: string = ""; + showPassword: boolean = false; + email: string; + pinLock: boolean = false; + webVaultHostname: string = ""; + formPromise: Promise; + supportsBiometric: boolean; + biometricLock: boolean; + biometricText: string; + hideInput: boolean; - protected successRoute: string = 'vault'; - protected onSuccessfulSubmit: () => void; + protected successRoute: string = "vault"; + protected onSuccessfulSubmit: () => Promise; - private invalidPinAttempts = 0; - private pinSet: [boolean, boolean]; + private invalidPinAttempts = 0; + private pinSet: [boolean, boolean]; - constructor(protected router: Router, protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, - protected userService: UserService, protected cryptoService: CryptoService, - protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService, - protected environmentService: EnvironmentService, protected stateService: StateService, - protected apiService: ApiService, private logService: LogService, - private keyConnectorService: KeyConnectorService, protected ngZone: NgZone) { } + constructor( + protected router: Router, + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + protected messagingService: MessagingService, + protected cryptoService: CryptoService, + protected vaultTimeoutService: VaultTimeoutService, + protected environmentService: EnvironmentService, + protected stateService: StateService, + protected apiService: ApiService, + private logService: LogService, + private keyConnectorService: KeyConnectorService, + protected ngZone: NgZone + ) {} - async ngOnInit() { - this.pinSet = await this.vaultTimeoutService.isPinLockSet(); - this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1]; - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); - this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() && - (await this.cryptoService.hasKeyStored('biometric') || !this.platformUtilsService.supportsSecureStorage()); - this.biometricText = await this.storageService.get(ConstantsService.biometricText); - this.email = await this.userService.getEmail(); - const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); - this.hideInput = usesKeyConnector && !this.pinLock; + async ngOnInit() { + this.stateService.activeAccount.subscribe(async (_userId) => { + await this.load(); + }); + } - // Users with key connector and without biometric or pin has no MP to unlock using - if (usesKeyConnector && !(this.biometricLock || this.pinLock)) { - await this.vaultTimeoutService.logOut(); - } - - const webVaultUrl = this.environmentService.getWebVaultUrl(); - const vaultUrl = webVaultUrl === 'https://vault.bitwarden.com' ? 'https://bitwarden.com' : webVaultUrl; - this.webVaultHostname = Utils.getHostname(vaultUrl); + async submit() { + if (this.pinLock && (this.pin == null || this.pin === "")) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("pinRequired") + ); + return; + } + if (!this.pinLock && (this.masterPassword == null || this.masterPassword === "")) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPassRequired") + ); + return; } - async submit() { - if (this.pinLock && (this.pin == null || this.pin === '')) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('pinRequired')); - return; - } - if (!this.pinLock && (this.masterPassword == null || this.masterPassword === '')) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassRequired')); - return; - } + const kdf = await this.stateService.getKdfType(); + const kdfIterations = await this.stateService.getKdfIterations(); - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); - - if (this.pinLock) { - let failed = true; - try { - if (this.pinSet[0]) { - const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations, - this.vaultTimeoutService.pinProtectedKey); - const encKey = await this.cryptoService.getEncKey(key); - const protectedPin = await this.storageService.get(ConstantsService.protectedPin); - const decPin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), encKey); - failed = decPin !== this.pin; - if (!failed) { - await this.setKeyAndContinue(key); - } - } else { - const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations); - failed = false; - await this.setKeyAndContinue(key); - } - } catch { - failed = true; - } - - if (failed) { - this.invalidPinAttempts++; - if (this.invalidPinAttempts >= 5) { - this.messagingService.send('logout'); - return; - } - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('invalidPin')); - } + if (this.pinLock) { + let failed = true; + try { + if (this.pinSet[0]) { + const key = await this.cryptoService.makeKeyFromPin( + this.pin, + this.email, + kdf, + kdfIterations, + await this.stateService.getDecryptedPinProtected() + ); + const encKey = await this.cryptoService.getEncKey(key); + const protectedPin = await this.stateService.getProtectedPin(); + const decPin = await this.cryptoService.decryptToUtf8( + new EncString(protectedPin), + encKey + ); + failed = decPin !== this.pin; + if (!failed) { + await this.setKeyAndContinue(key); + } } else { - const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); - const storedKeyHash = await this.cryptoService.getKeyHash(); - - let passwordValid = false; - - if (storedKeyHash != null) { - passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, key); - } else { - const request = new SecretVerificationRequest(); - const serverKeyHash = await this.cryptoService.hashPassword(this.masterPassword, key, - HashPurpose.ServerAuthorization); - request.masterPasswordHash = serverKeyHash; - try { - this.formPromise = this.apiService.postAccountVerifyPassword(request); - await this.formPromise; - passwordValid = true; - const localKeyHash = await this.cryptoService.hashPassword(this.masterPassword, key, - HashPurpose.LocalAuthorization); - await this.cryptoService.setKeyHash(localKeyHash); - } catch (e) { - this.logService.error(e); - } - } - - if (passwordValid) { - if (this.pinSet[0]) { - const protectedPin = await this.storageService.get(ConstantsService.protectedPin); - const encKey = await this.cryptoService.getEncKey(key); - const decPin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), encKey); - const pinKey = await this.cryptoService.makePinKey(decPin, this.email, kdf, kdfIterations); - this.vaultTimeoutService.pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey); - } - this.setKeyAndContinue(key); - } else { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('invalidMasterPassword')); - } + const key = await this.cryptoService.makeKeyFromPin( + this.pin, + this.email, + kdf, + kdfIterations + ); + failed = false; + await this.setKeyAndContinue(key); } + } catch { + failed = true; + } + + if (failed) { + this.invalidPinAttempts++; + if (this.invalidPinAttempts >= 5) { + this.messagingService.send("logout"); + return; + } + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("invalidPin") + ); + } + } else { + const key = await this.cryptoService.makeKey( + this.masterPassword, + this.email, + kdf, + kdfIterations + ); + const storedKeyHash = await this.cryptoService.getKeyHash(); + + let passwordValid = false; + + if (storedKeyHash != null) { + passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, key); + } else { + const request = new SecretVerificationRequest(); + const serverKeyHash = await this.cryptoService.hashPassword( + this.masterPassword, + key, + HashPurpose.ServerAuthorization + ); + request.masterPasswordHash = serverKeyHash; + try { + this.formPromise = this.apiService.postAccountVerifyPassword(request); + await this.formPromise; + passwordValid = true; + const localKeyHash = await this.cryptoService.hashPassword( + this.masterPassword, + key, + HashPurpose.LocalAuthorization + ); + await this.cryptoService.setKeyHash(localKeyHash); + } catch (e) { + this.logService.error(e); + } + } + + if (passwordValid) { + if (this.pinSet[0]) { + const protectedPin = await this.stateService.getProtectedPin(); + const encKey = await this.cryptoService.getEncKey(key); + const decPin = await this.cryptoService.decryptToUtf8( + new EncString(protectedPin), + encKey + ); + const pinKey = await this.cryptoService.makePinKey( + decPin, + this.email, + kdf, + kdfIterations + ); + await this.stateService.setDecryptedPinProtected( + await this.cryptoService.encrypt(key.key, pinKey) + ); + } + await this.setKeyAndContinue(key); + } else { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("invalidMasterPassword") + ); + } + } + } + + async logOut() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("logOutConfirmation"), + this.i18nService.t("logOut"), + this.i18nService.t("logOut"), + this.i18nService.t("cancel") + ); + if (confirmed) { + this.messagingService.send("logout"); + } + } + + async unlockBiometric(): Promise { + if (!this.biometricLock) { + return; } - async logOut() { - const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'), - this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel')); - if (confirmed) { - this.messagingService.send('logout'); - } + const success = (await this.cryptoService.getKey(KeySuffixOptions.Biometric)) != null; + + if (success) { + await this.doContinue(); } - async unlockBiometric(): Promise { - if (!this.biometricLock) { - return; - } + return success; + } - const success = (await this.cryptoService.getKey('biometric')) != null; + togglePassword() { + this.showPassword = !this.showPassword; + const input = document.getElementById(this.pinLock ? "pin" : "masterPassword"); + if (this.ngZone.isStable) { + input.focus(); + } else { + this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus()); + } + } - if (success) { - await this.doContinue(); - } + private async setKeyAndContinue(key: SymmetricCryptoKey) { + await this.cryptoService.setKey(key); + await this.doContinue(); + } - return success; + private async doContinue() { + await this.stateService.setBiometricLocked(false); + await this.stateService.setEverBeenUnlocked(true); + const disableFavicon = await this.stateService.getDisableFavicon(); + await this.stateService.setDisableFavicon(!!disableFavicon); + this.messagingService.send("unlocked"); + if (this.onSuccessfulSubmit != null) { + await this.onSuccessfulSubmit(); + } else if (this.router != null) { + this.router.navigate([this.successRoute]); + } + } + + private async load() { + this.pinSet = await this.vaultTimeoutService.isPinLockSet(); + this.pinLock = + (this.pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) || + this.pinSet[1]; + this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.biometricLock = + (await this.vaultTimeoutService.isBiometricLockSet()) && + ((await this.cryptoService.hasKeyStored(KeySuffixOptions.Biometric)) || + !this.platformUtilsService.supportsSecureStorage()); + this.biometricText = await this.stateService.getBiometricText(); + this.email = await this.stateService.getEmail(); + const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); + this.hideInput = usesKeyConnector && !this.pinLock; + + // Users with key connector and without biometric or pin has no MP to unlock using + if (usesKeyConnector && !(this.biometricLock || this.pinLock)) { + await this.vaultTimeoutService.logOut(); } - togglePassword() { - this.showPassword = !this.showPassword; - const input = document.getElementById(this.pinLock ? 'pin' : 'masterPassword'); - if (this.ngZone.isStable) { - input.focus(); - } else { - this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus()); - } - } - - private async setKeyAndContinue(key: SymmetricCryptoKey) { - await this.cryptoService.setKey(key); - this.doContinue(); - } - - private async doContinue() { - this.vaultTimeoutService.biometricLocked = false; - this.vaultTimeoutService.everBeenUnlocked = true; - const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); - await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); - this.messagingService.send('unlocked'); - if (this.onSuccessfulSubmit != null) { - this.onSuccessfulSubmit(); - } else if (this.router != null) { - this.router.navigate([this.successRoute]); - } - } + const webVaultUrl = this.environmentService.getWebVaultUrl(); + const vaultUrl = + webVaultUrl === "https://vault.bitwarden.com" ? "https://bitwarden.com" : webVaultUrl; + this.webVaultHostname = Utils.getHostname(vaultUrl); + } } diff --git a/angular/src/components/login.component.ts b/angular/src/components/login.component.ts index 34c0b8fd..5cfd6a6d 100644 --- a/angular/src/components/login.component.ts +++ b/angular/src/components/login.component.ts @@ -1,176 +1,186 @@ -import { - Directive, - Input, - NgZone, - OnInit, -} from '@angular/core'; +import { Directive, Input, NgZone, OnInit } from "@angular/core"; -import { Router } from '@angular/router'; +import { Router } from "@angular/router"; -import { take } from 'rxjs/operators'; +import { take } from "rxjs/operators"; -import { AuthResult } from 'jslib-common/models/domain/authResult'; +import { AuthResult } from "jslib-common/models/domain/authResult"; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { Utils } from "jslib-common/misc/utils"; -import { Utils } from 'jslib-common/misc/utils'; - -import { CaptchaProtectedComponent } from './captchaProtected.component'; - -const Keys = { - rememberedEmail: 'rememberedEmail', - rememberEmail: 'rememberEmail', -}; +import { CaptchaProtectedComponent } from "./captchaProtected.component"; @Directive() export class LoginComponent extends CaptchaProtectedComponent implements OnInit { - @Input() email: string = ''; - @Input() rememberEmail = true; + @Input() email: string = ""; + @Input() rememberEmail = true; - masterPassword: string = ''; - showPassword: boolean = false; - formPromise: Promise; - onSuccessfulLogin: () => Promise; - onSuccessfulLoginNavigate: () => Promise; - onSuccessfulLoginTwoFactorNavigate: () => Promise; - onSuccessfulLoginForceResetNavigate: () => Promise; + masterPassword: string = ""; + showPassword: boolean = false; + formPromise: Promise; + onSuccessfulLogin: () => Promise; + onSuccessfulLoginNavigate: () => Promise; + onSuccessfulLoginTwoFactorNavigate: () => Promise; + onSuccessfulLoginForceResetNavigate: () => Promise; - protected twoFactorRoute = '2fa'; - protected successRoute = 'vault'; - protected forcePasswordResetRoute = 'update-temp-password'; + protected twoFactorRoute = "2fa"; + protected successRoute = "vault"; + protected forcePasswordResetRoute = "update-temp-password"; - constructor(protected authService: AuthService, protected router: Router, - platformUtilsService: PlatformUtilsService, i18nService: I18nService, - protected stateService: StateService, environmentService: EnvironmentService, - protected passwordGenerationService: PasswordGenerationService, - protected cryptoFunctionService: CryptoFunctionService, private storageService: StorageService, - protected logService: LogService, protected ngZone: NgZone) { - super(environmentService, i18nService, platformUtilsService); + constructor( + protected authService: AuthService, + protected router: Router, + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + protected stateService: StateService, + environmentService: EnvironmentService, + protected passwordGenerationService: PasswordGenerationService, + protected cryptoFunctionService: CryptoFunctionService, + protected logService: LogService, + protected ngZone: NgZone + ) { + super(environmentService, i18nService, platformUtilsService); + } + + async ngOnInit() { + if (this.email == null || this.email === "") { + this.email = await this.stateService.getRememberedEmail(); + if (this.email == null) { + this.email = ""; + } + } + this.rememberEmail = (await this.stateService.getRememberedEmail()) != null; + if (Utils.isBrowser && !Utils.isNode) { + this.focusInput(); + } + } + + async submit() { + await this.setupCaptcha(); + + if (this.email == null || this.email === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("emailRequired") + ); + return; + } + if (this.email.indexOf("@") === -1) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("invalidEmail") + ); + return; + } + if (this.masterPassword == null || this.masterPassword === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPassRequired") + ); + return; } - async ngOnInit() { - if (this.email == null || this.email === '') { - this.email = await this.storageService.get(Keys.rememberedEmail); - if (this.email == null) { - this.email = ''; - } - } - this.rememberEmail = await this.storageService.get(Keys.rememberEmail); - if (this.rememberEmail == null) { - this.rememberEmail = true; - } - if (Utils.isBrowser && !Utils.isNode) { - this.focusInput(); - } - } - - async submit() { - await this.setupCaptcha(); - - if (this.email == null || this.email === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('emailRequired')); - return; - } - if (this.email.indexOf('@') === -1) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('invalidEmail')); - return; - } - if (this.masterPassword == null || this.masterPassword === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassRequired')); - return; - } - - try { - this.formPromise = this.authService.logIn(this.email, this.masterPassword, this.captchaToken); - const response = await this.formPromise; - await this.storageService.save(Keys.rememberEmail, this.rememberEmail); - if (this.rememberEmail) { - await this.storageService.save(Keys.rememberedEmail, this.email); - } else { - await this.storageService.remove(Keys.rememberedEmail); - } - if (this.handleCaptchaRequired(response)) { - return; - } else if (response.twoFactor) { - if (this.onSuccessfulLoginTwoFactorNavigate != null) { - this.onSuccessfulLoginTwoFactorNavigate(); - } else { - this.router.navigate([this.twoFactorRoute]); - } - } else if (response.forcePasswordReset) { - if (this.onSuccessfulLoginForceResetNavigate != null) { - this.onSuccessfulLoginForceResetNavigate(); - } else { - this.router.navigate([this.forcePasswordResetRoute]); - } - } else { - const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); - await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); - if (this.onSuccessfulLogin != null) { - this.onSuccessfulLogin(); - } - if (this.onSuccessfulLoginNavigate != null) { - this.onSuccessfulLoginNavigate(); - } else { - this.router.navigate([this.successRoute]); - } - } - } catch (e) { - this.logService.error(e); - } - } - - togglePassword() { - this.showPassword = !this.showPassword; - if (this.ngZone.isStable) { - document.getElementById('masterPassword').focus(); + try { + this.formPromise = this.authService.logIn(this.email, this.masterPassword, this.captchaToken); + const response = await this.formPromise; + if (this.rememberEmail) { + await this.stateService.setRememberedEmail(this.email); + } else { + await this.stateService.setRememberedEmail(null); + } + if (this.handleCaptchaRequired(response)) { + return; + } else if (response.twoFactor) { + if (this.onSuccessfulLoginTwoFactorNavigate != null) { + this.onSuccessfulLoginTwoFactorNavigate(); } else { - this.ngZone.onStable.pipe(take(1)).subscribe(() => document.getElementById('masterPassword').focus()); + this.router.navigate([this.twoFactorRoute]); } + } else if (response.forcePasswordReset) { + if (this.onSuccessfulLoginForceResetNavigate != null) { + this.onSuccessfulLoginForceResetNavigate(); + } else { + this.router.navigate([this.forcePasswordResetRoute]); + } + } else { + const disableFavicon = await this.stateService.getDisableFavicon(); + await this.stateService.setDisableFavicon(!!disableFavicon); + if (this.onSuccessfulLogin != null) { + this.onSuccessfulLogin(); + } + if (this.onSuccessfulLoginNavigate != null) { + this.onSuccessfulLoginNavigate(); + } else { + this.router.navigate([this.successRoute]); + } + } + } catch (e) { + this.logService.error(e); } + } - async launchSsoBrowser(clientId: string, ssoRedirectUri: string) { - // Generate necessary sso params - const passwordOptions: any = { - type: 'password', - length: 64, - uppercase: true, - lowercase: true, - numbers: true, - special: false, - }; - const state = await this.passwordGenerationService.generatePassword(passwordOptions); - const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256'); - const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); - - // Save sso params - await this.storageService.save(ConstantsService.ssoStateKey, state); - await this.storageService.save(ConstantsService.ssoCodeVerifierKey, ssoCodeVerifier); - - // Build URI - const webUrl = this.environmentService.getWebVaultUrl(); - - // Launch browser - this.platformUtilsService.launchUri(webUrl + '/#/sso?clientId=' + clientId + - '&redirectUri=' + encodeURIComponent(ssoRedirectUri) + - '&state=' + state + '&codeChallenge=' + codeChallenge); + togglePassword() { + this.showPassword = !this.showPassword; + if (this.ngZone.isStable) { + document.getElementById("masterPassword").focus(); + } else { + this.ngZone.onStable + .pipe(take(1)) + .subscribe(() => document.getElementById("masterPassword").focus()); } + } - protected focusInput() { - document.getElementById(this.email == null || this.email === '' ? 'email' : 'masterPassword').focus(); - } + async launchSsoBrowser(clientId: string, ssoRedirectUri: string) { + // Generate necessary sso params + const passwordOptions: any = { + type: "password", + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + const state = await this.passwordGenerationService.generatePassword(passwordOptions); + const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256"); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + + // Save sso params + await this.stateService.setSsoState(state); + await this.stateService.setSsoCodeVerifier(ssoCodeVerifier); + + // Build URI + const webUrl = this.environmentService.getWebVaultUrl(); + + // Launch browser + this.platformUtilsService.launchUri( + webUrl + + "/#/sso?clientId=" + + clientId + + "&redirectUri=" + + encodeURIComponent(ssoRedirectUri) + + "&state=" + + state + + "&codeChallenge=" + + codeChallenge + ); + } + + protected focusInput() { + document + .getElementById(this.email == null || this.email === "" ? "email" : "masterPassword") + .focus(); + } } diff --git a/angular/src/components/modal/modal.ref.ts b/angular/src/components/modal/modal.ref.ts index 116a6057..a80acebb 100644 --- a/angular/src/components/modal/modal.ref.ts +++ b/angular/src/components/modal/modal.ref.ts @@ -1,51 +1,50 @@ -import { Observable, Subject } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { Observable, Subject } from "rxjs"; +import { first } from "rxjs/operators"; export class ModalRef { + onCreated: Observable; // Modal added to the DOM. + onClose: Observable; // Initiated close. + onClosed: Observable; // Modal was closed (Remove element from DOM) + onShow: Observable; // Start showing modal + onShown: Observable; // Modal is fully visible - onCreated: Observable; // Modal added to the DOM. - onClose: Observable; // Initiated close. - onClosed: Observable; // Modal was closed (Remove element from DOM) - onShow: Observable; // Start showing modal - onShown: Observable; // Modal is fully visible + private readonly _onCreated = new Subject(); + private readonly _onClose = new Subject(); + private readonly _onClosed = new Subject(); + private readonly _onShow = new Subject(); + private readonly _onShown = new Subject(); + private lastResult: any; - private readonly _onCreated = new Subject(); - private readonly _onClose = new Subject(); - private readonly _onClosed = new Subject(); - private readonly _onShow = new Subject(); - private readonly _onShown = new Subject(); - private lastResult: any; + constructor() { + this.onCreated = this._onCreated.asObservable(); + this.onClose = this._onClose.asObservable(); + this.onClosed = this._onClosed.asObservable(); + this.onShow = this._onShow.asObservable(); + this.onShown = this._onShow.asObservable(); + } - constructor() { - this.onCreated = this._onCreated.asObservable(); - this.onClose = this._onClose.asObservable(); - this.onClosed = this._onClosed.asObservable(); - this.onShow = this._onShow.asObservable(); - this.onShown = this._onShow.asObservable(); - } + show() { + this._onShow.next(); + } - show() { - this._onShow.next(); - } + shown() { + this._onShown.next(); + } - shown() { - this._onShown.next(); - } + close(result?: any) { + this.lastResult = result; + this._onClose.next(result); + } - close(result?: any) { - this.lastResult = result; - this._onClose.next(result); - } + closed() { + this._onClosed.next(this.lastResult); + } - closed() { - this._onClosed.next(this.lastResult); - } + created(el: HTMLElement) { + this._onCreated.next(el); + } - created(el: HTMLElement) { - this._onCreated.next(el); - } - - onClosedPromise(): Promise { - return this.onClosed.pipe(first()).toPromise(); - } + onClosedPromise(): Promise { + return this.onClosed.pipe(first()).toPromise(); + } } diff --git a/angular/src/components/premium.component.ts b/angular/src/components/premium.component.ts index 5b5b4c89..62e5d8f1 100644 --- a/angular/src/components/premium.component.ts +++ b/angular/src/components/premium.component.ts @@ -1,48 +1,61 @@ -import { Directive, OnInit } from '@angular/core'; +import { Directive, OnInit } from "@angular/core"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; @Directive() export class PremiumComponent implements OnInit { - isPremium: boolean = false; - price: number = 10; - refreshPromise: Promise; + isPremium: boolean = false; + price: number = 10; + refreshPromise: Promise; - constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected apiService: ApiService, protected userService: UserService, private logService: LogService) { } + constructor( + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + protected apiService: ApiService, + private logService: LogService, + protected stateService: StateService + ) {} - async ngOnInit() { - this.isPremium = await this.userService.canAccessPremium(); + async ngOnInit() { + this.isPremium = await this.stateService.getCanAccessPremium(); + } + + async refresh() { + try { + this.refreshPromise = this.apiService.refreshIdentityToken(); + await this.refreshPromise; + this.platformUtilsService.showToast("success", null, this.i18nService.t("refreshComplete")); + this.isPremium = await this.stateService.getCanAccessPremium(); + } catch (e) { + this.logService.error(e); } + } - async refresh() { - try { - this.refreshPromise = this.apiService.refreshIdentityToken(); - await this.refreshPromise; - this.platformUtilsService.showToast('success', null, this.i18nService.t('refreshComplete')); - this.isPremium = await this.userService.canAccessPremium(); - } catch (e) { - this.logService.error(e); - } + async purchase() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("premiumPurchaseAlert"), + this.i18nService.t("premiumPurchase"), + this.i18nService.t("yes"), + this.i18nService.t("cancel") + ); + if (confirmed) { + this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=purchase"); } + } - async purchase() { - const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumPurchaseAlert'), - this.i18nService.t('premiumPurchase'), this.i18nService.t('yes'), this.i18nService.t('cancel')); - if (confirmed) { - this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); - } - } - - async manage() { - const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumManageAlert'), - this.i18nService.t('premiumManage'), this.i18nService.t('yes'), this.i18nService.t('cancel')); - if (confirmed) { - this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=manage'); - } + async manage() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("premiumManageAlert"), + this.i18nService.t("premiumManage"), + this.i18nService.t("yes"), + this.i18nService.t("cancel") + ); + if (confirmed) { + this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/?premium=manage"); } + } } diff --git a/angular/src/components/register.component.ts b/angular/src/components/register.component.ts index fd91af29..16ccb6ea 100644 --- a/angular/src/components/register.component.ts +++ b/angular/src/components/register.component.ts @@ -1,195 +1,251 @@ -import { Directive, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { Directive, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; -import { KeysRequest } from 'jslib-common/models/request/keysRequest'; -import { ReferenceEventRequest } from 'jslib-common/models/request/referenceEventRequest'; -import { RegisterRequest } from 'jslib-common/models/request/registerRequest'; +import { KeysRequest } from "jslib-common/models/request/keysRequest"; +import { ReferenceEventRequest } from "jslib-common/models/request/referenceEventRequest"; +import { RegisterRequest } from "jslib-common/models/request/registerRequest"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { KdfType } from 'jslib-common/enums/kdfType'; +import { KdfType } from "jslib-common/enums/kdfType"; -import { CaptchaProtectedComponent } from './captchaProtected.component'; +import { CaptchaProtectedComponent } from "./captchaProtected.component"; @Directive() export class RegisterComponent extends CaptchaProtectedComponent implements OnInit { - name: string = ''; - email: string = ''; - masterPassword: string = ''; - confirmMasterPassword: string = ''; - hint: string = ''; - showPassword: boolean = false; - formPromise: Promise; - masterPasswordScore: number; - referenceData: ReferenceEventRequest; - showTerms = true; - acceptPolicies: boolean = false; + name: string = ""; + email: string = ""; + masterPassword: string = ""; + confirmMasterPassword: string = ""; + hint: string = ""; + showPassword: boolean = false; + formPromise: Promise; + masterPasswordScore: number; + referenceData: ReferenceEventRequest; + showTerms = true; + acceptPolicies: boolean = false; - protected successRoute = 'login'; - private masterPasswordStrengthTimeout: any; + protected successRoute = "login"; + private masterPasswordStrengthTimeout: any; - constructor(protected authService: AuthService, protected router: Router, - i18nService: I18nService, protected cryptoService: CryptoService, - protected apiService: ApiService, protected stateService: StateService, - platformUtilsService: PlatformUtilsService, - protected passwordGenerationService: PasswordGenerationService, environmentService: EnvironmentService, - protected logService: LogService) { - super(environmentService, i18nService, platformUtilsService); - this.showTerms = !platformUtilsService.isSelfHost(); + constructor( + protected authService: AuthService, + protected router: Router, + i18nService: I18nService, + protected cryptoService: CryptoService, + protected apiService: ApiService, + protected stateService: StateService, + platformUtilsService: PlatformUtilsService, + protected passwordGenerationService: PasswordGenerationService, + environmentService: EnvironmentService, + protected logService: LogService + ) { + super(environmentService, i18nService, platformUtilsService); + this.showTerms = !platformUtilsService.isSelfHost(); + } + + async ngOnInit() { + this.setupCaptcha(); + } + + get masterPasswordScoreWidth() { + return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; + } + + get masterPasswordScoreColor() { + switch (this.masterPasswordScore) { + case 4: + return "success"; + case 3: + return "primary"; + case 2: + return "warning"; + default: + return "danger"; + } + } + + get masterPasswordScoreText() { + switch (this.masterPasswordScore) { + case 4: + return this.i18nService.t("strong"); + case 3: + return this.i18nService.t("good"); + case 2: + return this.i18nService.t("weak"); + default: + return this.masterPasswordScore != null ? this.i18nService.t("weak") : null; + } + } + + async submit() { + if (!this.acceptPolicies && this.showTerms) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("acceptPoliciesError") + ); + return; } - async ngOnInit() { - this.setupCaptcha(); + if (this.email == null || this.email === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("emailRequired") + ); + return; + } + if (this.email.indexOf("@") === -1) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("invalidEmail") + ); + return; + } + if (this.masterPassword == null || this.masterPassword === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPassRequired") + ); + return; + } + if (this.masterPassword.length < 8) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPassLength") + ); + return; + } + if (this.masterPassword !== this.confirmMasterPassword) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("masterPassDoesntMatch") + ); + return; } - get masterPasswordScoreWidth() { - return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; + const strengthResult = this.passwordGenerationService.passwordStrength( + this.masterPassword, + this.getPasswordStrengthUserInput() + ); + if (strengthResult != null && strengthResult.score < 3) { + const result = await this.platformUtilsService.showDialog( + this.i18nService.t("weakMasterPasswordDesc"), + this.i18nService.t("weakMasterPassword"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!result) { + return; + } } - get masterPasswordScoreColor() { - switch (this.masterPasswordScore) { - case 4: - return 'success'; - case 3: - return 'primary'; - case 2: - return 'warning'; - default: - return 'danger'; - } + if (this.hint === this.masterPassword) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("hintEqualsPassword") + ); + return; } - get masterPasswordScoreText() { - switch (this.masterPasswordScore) { - case 4: - return this.i18nService.t('strong'); - case 3: - return this.i18nService.t('good'); - case 2: - return this.i18nService.t('weak'); - default: - return this.masterPasswordScore != null ? this.i18nService.t('weak') : null; - } + this.name = this.name === "" ? null : this.name; + this.email = this.email.trim().toLowerCase(); + const kdf = KdfType.PBKDF2_SHA256; + const useLowerKdf = this.platformUtilsService.isIE(); + const kdfIterations = useLowerKdf ? 10000 : 100000; + const key = await this.cryptoService.makeKey( + this.masterPassword, + this.email, + kdf, + kdfIterations + ); + const encKey = await this.cryptoService.makeEncKey(key); + const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); + const keys = await this.cryptoService.makeKeyPair(encKey[0]); + const request = new RegisterRequest( + this.email, + this.name, + hashedPassword, + this.hint, + encKey[1].encryptedString, + kdf, + kdfIterations, + this.referenceData, + this.captchaToken + ); + request.keys = new KeysRequest(keys[0], keys[1].encryptedString); + const orgInvite = await this.stateService.getOrganizationInvitation(); + if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { + request.token = orgInvite.token; + request.organizationUserId = orgInvite.organizationUserId; } - async submit() { - if (!this.acceptPolicies && this.showTerms) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('acceptPoliciesError')); - return; - } - - if (this.email == null || this.email === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('emailRequired')); - return; - } - if (this.email.indexOf('@') === -1) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('invalidEmail')); - return; - } - if (this.masterPassword == null || this.masterPassword === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassRequired')); - return; - } - if (this.masterPassword.length < 8) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassLength')); - return; - } - if (this.masterPassword !== this.confirmMasterPassword) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassDoesntMatch')); - return; - } - - const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, - this.getPasswordStrengthUserInput()); - if (strengthResult != null && strengthResult.score < 3) { - const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), - this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), - 'warning'); - if (!result) { - return; - } - } - - if (this.hint === this.masterPassword) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('hintEqualsPassword')); - return; - } - - this.name = this.name === '' ? null : this.name; - this.email = this.email.trim().toLowerCase(); - const kdf = KdfType.PBKDF2_SHA256; - const useLowerKdf = this.platformUtilsService.isIE(); - const kdfIterations = useLowerKdf ? 10000 : 100000; - const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); - const encKey = await this.cryptoService.makeEncKey(key); - const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); - const keys = await this.cryptoService.makeKeyPair(encKey[0]); - const request = new RegisterRequest(this.email, this.name, hashedPassword, - this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceData, this.captchaToken); - request.keys = new KeysRequest(keys[0], keys[1].encryptedString); - const orgInvite = await this.stateService.get('orgInvitation'); - if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { - request.token = orgInvite.token; - request.organizationUserId = orgInvite.organizationUserId; - } - - try { - this.formPromise = this.apiService.postRegister(request); - try { - await this.formPromise; - } catch (e) { - if (this.handleCaptchaRequired(e)) { - return; - } else { - throw e; - } - } - this.platformUtilsService.showToast('success', null, this.i18nService.t('newAccountCreated')); - this.router.navigate([this.successRoute], { queryParams: { email: this.email } }); - } catch (e) { - this.logService.error(e); + try { + this.formPromise = this.apiService.postRegister(request); + try { + await this.formPromise; + } catch (e) { + if (this.handleCaptchaRequired(e)) { + return; + } else { + throw e; } + } + this.platformUtilsService.showToast("success", null, this.i18nService.t("newAccountCreated")); + this.router.navigate([this.successRoute], { queryParams: { email: this.email } }); + } catch (e) { + this.logService.error(e); } + } - togglePassword(confirmField: boolean) { - this.showPassword = !this.showPassword; - document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); - } + togglePassword(confirmField: boolean) { + this.showPassword = !this.showPassword; + document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus(); + } - updatePasswordStrength() { - if (this.masterPasswordStrengthTimeout != null) { - clearTimeout(this.masterPasswordStrengthTimeout); - } - this.masterPasswordStrengthTimeout = setTimeout(() => { - const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, - this.getPasswordStrengthUserInput()); - this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; - }, 300); + updatePasswordStrength() { + if (this.masterPasswordStrengthTimeout != null) { + clearTimeout(this.masterPasswordStrengthTimeout); } + this.masterPasswordStrengthTimeout = setTimeout(() => { + const strengthResult = this.passwordGenerationService.passwordStrength( + this.masterPassword, + this.getPasswordStrengthUserInput() + ); + this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; + }, 300); + } - private getPasswordStrengthUserInput() { - let userInput: string[] = []; - const atPosition = this.email.indexOf('@'); - if (atPosition > -1) { - userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/)); - } - if (this.name != null && this.name !== '') { - userInput = userInput.concat(this.name.trim().toLowerCase().split(' ')); - } - return userInput; + private getPasswordStrengthUserInput() { + let userInput: string[] = []; + const atPosition = this.email.indexOf("@"); + if (atPosition > -1) { + userInput = userInput.concat( + this.email + .substr(0, atPosition) + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/) + ); } + if (this.name != null && this.name !== "") { + userInput = userInput.concat(this.name.trim().toLowerCase().split(" ")); + } + return userInput; + } } diff --git a/angular/src/components/remove-password.component.ts b/angular/src/components/remove-password.component.ts index 91c35312..0359ad35 100644 --- a/angular/src/components/remove-password.component.ts +++ b/angular/src/components/remove-password.component.ts @@ -1,77 +1,83 @@ -import { - Directive, - OnInit, -} from '@angular/core'; -import { Router } from '@angular/router'; +import { Directive, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { ConstantsService } from 'jslib-common/services/constants.service'; - -import { Organization } from 'jslib-common/models/domain/organization'; +import { Organization } from "jslib-common/models/domain/organization"; @Directive() export class RemovePasswordComponent implements OnInit { + actionPromise: Promise; + continuing: boolean = false; + leaving: boolean = false; - actionPromise: Promise; - continuing: boolean = false; - leaving: boolean = false; + loading: boolean = true; + organization: Organization; + email: string; - loading: boolean = true; - organization: Organization; - email: string; + constructor( + private router: Router, + private stateService: StateService, + private apiService: ApiService, + private syncService: SyncService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private keyConnectorService: KeyConnectorService + ) {} - constructor(private router: Router, private userService: UserService, - private apiService: ApiService, private syncService: SyncService, - private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - private keyConnectorService: KeyConnectorService, private storageService: StorageService) { } + async ngOnInit() { + this.organization = await this.keyConnectorService.getManagingOrganization(); + this.email = await this.stateService.getEmail(); + await this.syncService.fullSync(false); + this.loading = false; + } - async ngOnInit() { - this.organization = await this.keyConnectorService.getManagingOrganization(); - this.email = await this.userService.getEmail(); - await this.syncService.fullSync(false); - this.loading = false; + async convert() { + this.continuing = true; + this.actionPromise = this.keyConnectorService.migrateUser(); + + try { + await this.actionPromise; + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("removedMasterPassword") + ); + await this.keyConnectorService.removeConvertAccountRequired(); + this.router.navigate([""]); + } catch (e) { + this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message); + } + } + + async leave() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("leaveOrganizationConfirmation"), + this.organization.name, + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; } - async convert() { - this.continuing = true; - this.actionPromise = this.keyConnectorService.migrateUser(); - - try { - await this.actionPromise; - this.platformUtilsService.showToast('success', null, this.i18nService.t('removedMasterPassword')); - await this.keyConnectorService.removeConvertAccountRequired(); - this.router.navigate(['']); - } catch (e) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), e.message); - } - } - - async leave() { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('leaveOrganizationConfirmation'), this.organization.name, - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return false; - } - - try { - this.leaving = true; - this.actionPromise = this.apiService.postLeaveOrganization(this.organization.id).then(() => { - return this.syncService.fullSync(true); - }); - await this.actionPromise; - this.platformUtilsService.showToast('success', null, this.i18nService.t('leftOrganization')); - await this.keyConnectorService.removeConvertAccountRequired(); - this.router.navigate(['']); - } catch (e) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), e); - } + try { + this.leaving = true; + this.actionPromise = this.apiService.postLeaveOrganization(this.organization.id).then(() => { + return this.syncService.fullSync(true); + }); + await this.actionPromise; + this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization")); + await this.keyConnectorService.removeConvertAccountRequired(); + this.router.navigate([""]); + } catch (e) { + this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e); } + } } diff --git a/angular/src/components/send/add-edit.component.ts b/angular/src/components/send/add-edit.component.ts index 70bd87ba..3573260c 100644 --- a/angular/src/components/send/add-edit.component.ts +++ b/angular/src/components/send/add-edit.component.ts @@ -1,274 +1,296 @@ -import { DatePipe } from '@angular/common'; -import { - Directive, - EventEmitter, - Input, - OnInit, - Output -} from '@angular/core'; +import { DatePipe } from "@angular/common"; +import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { PolicyType } from 'jslib-common/enums/policyType'; -import { SendType } from 'jslib-common/enums/sendType'; +import { PolicyType } from "jslib-common/enums/policyType"; +import { SendType } from "jslib-common/enums/sendType"; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SendService } from 'jslib-common/abstractions/send.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { SendService } from "jslib-common/abstractions/send.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { SendFileView } from 'jslib-common/models/view/sendFileView'; -import { SendTextView } from 'jslib-common/models/view/sendTextView'; -import { SendView } from 'jslib-common/models/view/sendView'; +import { SendFileView } from "jslib-common/models/view/sendFileView"; +import { SendTextView } from "jslib-common/models/view/sendTextView"; +import { SendView } from "jslib-common/models/view/sendView"; -import { EncArrayBuffer } from 'jslib-common/models/domain/encArrayBuffer'; -import { Send } from 'jslib-common/models/domain/send'; +import { EncArrayBuffer } from "jslib-common/models/domain/encArrayBuffer"; +import { Send } from "jslib-common/models/domain/send"; @Directive() export class AddEditComponent implements OnInit { - @Input() sendId: string; - @Input() type: SendType; + @Input() sendId: string; + @Input() type: SendType; - @Output() onSavedSend = new EventEmitter(); - @Output() onDeletedSend = new EventEmitter(); - @Output() onCancelled = new EventEmitter(); + @Output() onSavedSend = new EventEmitter(); + @Output() onDeletedSend = new EventEmitter(); + @Output() onCancelled = new EventEmitter(); - copyLink = false; - disableSend = false; - disableHideEmail = false; - send: SendView; - deletionDate: string; - expirationDate: string; - hasPassword: boolean; - password: string; - showPassword = false; - formPromise: Promise; - deletePromise: Promise; - sendType = SendType; - typeOptions: any[]; - canAccessPremium = true; - emailVerified = true; - alertShown = false; - showOptions = false; + copyLink = false; + disableSend = false; + disableHideEmail = false; + send: SendView; + deletionDate: string; + expirationDate: string; + hasPassword: boolean; + password: string; + showPassword = false; + formPromise: Promise; + deletePromise: Promise; + sendType = SendType; + typeOptions: any[]; + canAccessPremium = true; + emailVerified = true; + alertShown = false; + showOptions = false; - private sendLinkBaseUrl: string; + private sendLinkBaseUrl: string; - constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected environmentService: EnvironmentService, protected datePipe: DatePipe, - protected sendService: SendService, protected userService: UserService, - protected messagingService: MessagingService, protected policyService: PolicyService, - private logService: LogService) { - this.typeOptions = [ - { name: i18nService.t('sendTypeFile'), value: SendType.File }, - { name: i18nService.t('sendTypeText'), value: SendType.Text }, - ]; - this.sendLinkBaseUrl = this.environmentService.getSendUrl(); + constructor( + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + protected environmentService: EnvironmentService, + protected datePipe: DatePipe, + protected sendService: SendService, + protected messagingService: MessagingService, + protected policyService: PolicyService, + private logService: LogService, + protected stateService: StateService + ) { + this.typeOptions = [ + { name: i18nService.t("sendTypeFile"), value: SendType.File }, + { name: i18nService.t("sendTypeText"), value: SendType.Text }, + ]; + this.sendLinkBaseUrl = this.environmentService.getSendUrl(); + } + + get link(): string { + if (this.send.id != null && this.send.accessId != null) { + return this.sendLinkBaseUrl + this.send.accessId + "/" + this.send.urlB64Key; + } + return null; + } + + get isSafari() { + return this.platformUtilsService.isSafari(); + } + + get isDateTimeLocalSupported(): boolean { + return !(this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()); + } + + async ngOnInit() { + await this.load(); + } + + get editMode(): boolean { + return this.sendId != null; + } + + get title(): string { + return this.i18nService.t(this.editMode ? "editSend" : "createSend"); + } + + setDates(event: { deletionDate: string; expirationDate: string }) { + this.deletionDate = event.deletionDate; + this.expirationDate = event.expirationDate; + } + + async load() { + this.disableSend = await this.policyService.policyAppliesToUser(PolicyType.DisableSend); + this.disableHideEmail = await this.policyService.policyAppliesToUser( + PolicyType.SendOptions, + (p) => p.data.disableHideEmail + ); + + this.canAccessPremium = await this.stateService.getCanAccessPremium(); + this.emailVerified = await this.stateService.getEmailVerified(); + if (!this.canAccessPremium || !this.emailVerified) { + this.type = SendType.Text; } - get link(): string { - if (this.send.id != null && this.send.accessId != null) { - return this.sendLinkBaseUrl + this.send.accessId + '/' + this.send.urlB64Key; - } - return null; + if (this.send == null) { + if (this.editMode) { + const send = await this.loadSend(); + this.send = await send.decrypt(); + } else { + this.send = new SendView(); + this.send.type = this.type == null ? SendType.File : this.type; + this.send.file = new SendFileView(); + this.send.text = new SendTextView(); + this.send.deletionDate = new Date(); + this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7); + } } - get isSafari() { - return this.platformUtilsService.isSafari(); + this.hasPassword = this.send.password != null && this.send.password.trim() !== ""; + } + + async submit(): Promise { + if (this.disableSend) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("sendDisabledWarning") + ); + return false; } - get isDateTimeLocalSupported(): boolean { - return !(this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()); + if (this.send.name == null || this.send.name === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nameRequired") + ); + return false; } - async ngOnInit() { - await this.load(); - } - - get editMode(): boolean { - return this.sendId != null; - } - - get title(): string { - return this.i18nService.t( - this.editMode ? - 'editSend' : - 'createSend' + let file: File = null; + if (this.send.type === SendType.File && !this.editMode) { + const fileEl = document.getElementById("file") as HTMLInputElement; + const files = fileEl.files; + if (files == null || files.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("selectFile") ); + return; + } + + file = files[0]; + if (files[0].size > 524288000) { + // 500 MB + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("maxFileSize") + ); + return; + } } - setDates(event: {deletionDate: string, expirationDate: string}) { - this.deletionDate = event.deletionDate; - this.expirationDate = event.expirationDate; + if (this.password != null && this.password.trim() === "") { + this.password = null; } - async load() { - this.disableSend = await this.policyService.policyAppliesToUser(PolicyType.DisableSend); - this.disableHideEmail = await this.policyService.policyAppliesToUser(PolicyType.SendOptions, - p => p.data.disableHideEmail); - - this.canAccessPremium = await this.userService.canAccessPremium(); - this.emailVerified = await this.userService.getEmailVerified(); - if (!this.canAccessPremium || !this.emailVerified) { - this.type = SendType.Text; + this.formPromise = this.encryptSend(file).then(async (encSend) => { + const uploadPromise = this.sendService.saveWithServer(encSend); + await uploadPromise; + if (this.send.id == null) { + this.send.id = encSend[0].id; + } + if (this.send.accessId == null) { + this.send.accessId = encSend[0].accessId; + } + this.onSavedSend.emit(this.send); + if (this.copyLink && this.link != null) { + const copySuccess = await this.copyLinkToClipboard(this.link); + if (copySuccess ?? true) { + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t(this.editMode ? "editedSend" : "createdSend") + ); + } else { + await this.platformUtilsService.showDialog( + this.i18nService.t(this.editMode ? "editedSend" : "createdSend"), + null, + this.i18nService.t("ok"), + null, + "success", + null + ); + await this.copyLinkToClipboard(this.link); } + } + }); + try { + await this.formPromise; + return true; + } catch (e) { + this.logService.error(e); + } + return false; + } - if (this.send == null) { - if (this.editMode) { - const send = await this.loadSend(); - this.send = await send.decrypt(); - } else { - this.send = new SendView(); - this.send.type = this.type == null ? SendType.File : this.type; - this.send.file = new SendFileView(); - this.send.text = new SendTextView(); - this.send.deletionDate = new Date(); - this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7); - } - } + async copyLinkToClipboard(link: string): Promise { + return Promise.resolve(this.platformUtilsService.copyToClipboard(link)); + } - this.hasPassword = this.send.password != null && this.send.password.trim() !== ''; + async delete(): Promise { + if (this.deletePromise != null) { + return false; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("deleteSendConfirmation"), + this.i18nService.t("deleteSend"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; } - async submit(): Promise { - if (this.disableSend) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('sendDisabledWarning')); - return false; - } - - if (this.send.name == null || this.send.name === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('nameRequired')); - return false; - } - - let file: File = null; - if (this.send.type === SendType.File && !this.editMode) { - const fileEl = document.getElementById('file') as HTMLInputElement; - const files = fileEl.files; - if (files == null || files.length === 0) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('selectFile')); - return; - } - - file = files[0]; - if (files[0].size > 524288000) { // 500 MB - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('maxFileSize')); - return; - } - } - - if (this.password != null && this.password.trim() === '') { - this.password = null; - } - - this.formPromise = this.encryptSend(file) - .then(async encSend => { - const uploadPromise = this.sendService.saveWithServer(encSend); - await uploadPromise; - if (this.send.id == null) { - this.send.id = encSend[0].id; - } - if (this.send.accessId == null) { - this.send.accessId = encSend[0].accessId; - } - this.onSavedSend.emit(this.send); - if (this.copyLink && this.link != null) { - const copySuccess = await this.copyLinkToClipboard(this.link); - if (copySuccess ?? true) { - this.platformUtilsService.showToast('success', null, - this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend')); - } else { - await this.platformUtilsService.showDialog( - this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'), null, - this.i18nService.t('ok'), null, 'success', null); - await this.copyLinkToClipboard(this.link); - } - } - }); - try { - await this.formPromise; - return true; - } catch (e) { - this.logService.error(e); - } - return false; + try { + this.deletePromise = this.sendService.deleteWithServer(this.send.id); + await this.deletePromise; + this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedSend")); + await this.load(); + this.onDeletedSend.emit(this.send); + return true; + } catch (e) { + this.logService.error(e); } - async copyLinkToClipboard(link: string): Promise { - return Promise.resolve(this.platformUtilsService.copyToClipboard(link)); + return false; + } + + typeChanged() { + if (this.send.type === SendType.File && !this.alertShown) { + if (!this.canAccessPremium) { + this.alertShown = true; + this.messagingService.send("premiumRequired"); + } else if (!this.emailVerified) { + this.alertShown = true; + this.messagingService.send("emailVerificationRequired"); + } + } + } + + toggleOptions() { + this.showOptions = !this.showOptions; + } + + protected async loadSend(): Promise { + return this.sendService.get(this.sendId); + } + + protected async encryptSend(file: File): Promise<[Send, EncArrayBuffer]> { + const sendData = await this.sendService.encrypt(this.send, file, this.password, null); + + // Parse dates + try { + sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate); + } catch { + sendData[0].deletionDate = null; + } + try { + sendData[0].expirationDate = + this.expirationDate == null ? null : new Date(this.expirationDate); + } catch { + sendData[0].expirationDate = null; } - async delete(): Promise { - if (this.deletePromise != null) { - return false; - } - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('deleteSendConfirmation'), - this.i18nService.t('deleteSend'), - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return false; - } + return sendData; + } - try { - this.deletePromise = this.sendService.deleteWithServer(this.send.id); - await this.deletePromise; - this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); - await this.load(); - this.onDeletedSend.emit(this.send); - return true; - } catch (e) { - this.logService.error(e); - } - - return false; - } - - typeChanged() { - if (this.send.type === SendType.File && !this.alertShown) { - if (!this.canAccessPremium) { - this.alertShown = true; - this.messagingService.send('premiumRequired'); - } else if (!this.emailVerified) { - this.alertShown = true; - this.messagingService.send('emailVerificationRequired'); - } - } - } - - toggleOptions() { - this.showOptions = !this.showOptions; - } - - protected async loadSend(): Promise { - return this.sendService.get(this.sendId); - } - - protected async encryptSend(file: File): Promise<[Send, EncArrayBuffer]> { - const sendData = await this.sendService.encrypt(this.send, file, this.password, null); - - // Parse dates - try { - sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate); - } catch { - sendData[0].deletionDate = null; - } - try { - sendData[0].expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate); - } catch { - sendData[0].expirationDate = null; - } - - return sendData; - } - - protected togglePasswordVisible() { - this.showPassword = !this.showPassword; - document.getElementById('password').focus(); - } + protected togglePasswordVisible() { + this.showPassword = !this.showPassword; + document.getElementById("password").focus(); + } } diff --git a/angular/src/components/send/send.component.ts b/angular/src/components/send/send.component.ts index 77f36459..20b2c095 100644 --- a/angular/src/components/send/send.component.ts +++ b/angular/src/components/send/send.component.ts @@ -1,203 +1,212 @@ -import { - Directive, - NgZone, - OnInit, -} from '@angular/core'; +import { Directive, NgZone, OnInit } from "@angular/core"; -import { PolicyType } from 'jslib-common/enums/policyType'; -import { SendType } from 'jslib-common/enums/sendType'; +import { PolicyType } from "jslib-common/enums/policyType"; +import { SendType } from "jslib-common/enums/sendType"; -import { SendView } from 'jslib-common/models/view/sendView'; +import { SendView } from "jslib-common/models/view/sendView"; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SearchService } from 'jslib-common/abstractions/search.service'; -import { SendService } from 'jslib-common/abstractions/send.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { SearchService } from "jslib-common/abstractions/search.service"; +import { SendService } from "jslib-common/abstractions/send.service"; @Directive() export class SendComponent implements OnInit { + disableSend = false; + sendType = SendType; + loaded = false; + loading = true; + refreshing = false; + expired: boolean = false; + type: SendType = null; + sends: SendView[] = []; + filteredSends: SendView[] = []; + searchText: string; + selectedType: SendType; + selectedAll: boolean; + searchPlaceholder: string; + filter: (cipher: SendView) => boolean; + searchPending = false; + hasSearched = false; // search() function called - returns true if text qualifies for search - disableSend = false; - sendType = SendType; - loaded = false; - loading = true; - refreshing = false; - expired: boolean = false; - type: SendType = null; - sends: SendView[] = []; - filteredSends: SendView[] = []; - searchText: string; - selectedType: SendType; - selectedAll: boolean; - searchPlaceholder: string; - filter: (cipher: SendView) => boolean; - searchPending = false; - hasSearched = false; // search() function called - returns true if text qualifies for search + actionPromise: any; + onSuccessfulRemovePassword: () => Promise; + onSuccessfulDelete: () => Promise; + onSuccessfulLoad: () => Promise; - actionPromise: any; - onSuccessfulRemovePassword: () => Promise; - onSuccessfulDelete: () => Promise; - onSuccessfulLoad: () => Promise; + private searchTimeout: any; - private searchTimeout: any; + constructor( + protected sendService: SendService, + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + protected environmentService: EnvironmentService, + protected ngZone: NgZone, + protected searchService: SearchService, + protected policyService: PolicyService, + private logService: LogService + ) {} - constructor(protected sendService: SendService, protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, - protected ngZone: NgZone, protected searchService: SearchService, - protected policyService: PolicyService, protected userService: UserService, - private logService: LogService) { } + async ngOnInit() { + this.disableSend = await this.policyService.policyAppliesToUser(PolicyType.DisableSend); + } - async ngOnInit() { - this.disableSend = await this.policyService.policyAppliesToUser(PolicyType.DisableSend); + async load(filter: (send: SendView) => boolean = null) { + this.loading = true; + const sends = await this.sendService.getAllDecrypted(); + this.sends = sends; + if (this.onSuccessfulLoad != null) { + await this.onSuccessfulLoad(); + } else { + // Default action + this.selectAll(); + } + this.loading = false; + this.loaded = true; + } + + async reload(filter: (send: SendView) => boolean = null) { + this.loaded = false; + this.sends = []; + await this.load(filter); + } + + async refresh() { + try { + this.refreshing = true; + await this.reload(this.filter); + } finally { + this.refreshing = false; + } + } + + async applyFilter(filter: (send: SendView) => boolean = null) { + this.filter = filter; + await this.search(null); + } + + async search(timeout: number = null) { + this.searchPending = false; + if (this.searchTimeout != null) { + clearTimeout(this.searchTimeout); + } + if (timeout == null) { + this.hasSearched = this.searchService.isSearchable(this.searchText); + this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + this.applyTextSearch(); + return; + } + this.searchPending = true; + this.searchTimeout = setTimeout(async () => { + this.hasSearched = this.searchService.isSearchable(this.searchText); + this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + this.applyTextSearch(); + this.searchPending = false; + }, timeout); + } + + async removePassword(s: SendView): Promise { + if (this.actionPromise != null || s.password == null) { + return; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("removePasswordConfirmation"), + this.i18nService.t("removePassword"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; } - async load(filter: (send: SendView) => boolean = null) { - this.loading = true; - const sends = await this.sendService.getAllDecrypted(); - this.sends = sends; - if (this.onSuccessfulLoad != null) { - await this.onSuccessfulLoad(); - } else { - // Default action - this.selectAll(); - } - this.loading = false; - this.loaded = true; + try { + this.actionPromise = this.sendService.removePasswordWithServer(s.id); + await this.actionPromise; + if (this.onSuccessfulRemovePassword != null) { + this.onSuccessfulRemovePassword(); + } else { + // Default actions + this.platformUtilsService.showToast("success", null, this.i18nService.t("removedPassword")); + await this.load(); + } + } catch (e) { + this.logService.error(e); + } + this.actionPromise = null; + } + + async delete(s: SendView): Promise { + if (this.actionPromise != null) { + return false; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("deleteSendConfirmation"), + this.i18nService.t("deleteSend"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; } - async reload(filter: (send: SendView) => boolean = null) { - this.loaded = false; - this.sends = []; - await this.load(filter); + try { + this.actionPromise = this.sendService.deleteWithServer(s.id); + await this.actionPromise; + + if (this.onSuccessfulDelete != null) { + this.onSuccessfulDelete(); + } else { + // Default actions + this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedSend")); + await this.refresh(); + } + } catch (e) { + this.logService.error(e); } + this.actionPromise = null; + return true; + } - async refresh() { - try { - this.refreshing = true; - await this.reload(this.filter); - } finally { - this.refreshing = false; - } - } - - async applyFilter(filter: (send: SendView) => boolean = null) { - this.filter = filter; - await this.search(null); - } - - async search(timeout: number = null) { - this.searchPending = false; - if (this.searchTimeout != null) { - clearTimeout(this.searchTimeout); - } - if (timeout == null) { - this.hasSearched = this.searchService.isSearchable(this.searchText); - this.filteredSends = this.sends.filter(s => this.filter == null || this.filter(s)); - this.applyTextSearch(); - return; - } - this.searchPending = true; - this.searchTimeout = setTimeout(async () => { - this.hasSearched = this.searchService.isSearchable(this.searchText); - this.filteredSends = this.sends.filter(s => this.filter == null || this.filter(s)); - this.applyTextSearch(); - this.searchPending = false; - }, timeout); - } - - async removePassword(s: SendView): Promise { - if (this.actionPromise != null || s.password == null) { - return; - } - const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'), - this.i18nService.t('removePassword'), - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return false; - } - - try { - this.actionPromise = this.sendService.removePasswordWithServer(s.id); - await this.actionPromise; - if (this.onSuccessfulRemovePassword != null) { - this.onSuccessfulRemovePassword(); - } else { - // Default actions - this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword')); - await this.load(); - } - } catch (e) { - this.logService.error(e); - } - this.actionPromise = null; - } - - async delete(s: SendView): Promise { - if (this.actionPromise != null) { - return false; - } - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('deleteSendConfirmation'), - this.i18nService.t('deleteSend'), - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return false; - } - - try { - this.actionPromise = this.sendService.deleteWithServer(s.id); - await this.actionPromise; - - if (this.onSuccessfulDelete != null) { - this.onSuccessfulDelete(); - } else { - // Default actions - this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); - await this.refresh(); - } - } catch (e) { - this.logService.error(e); - } - this.actionPromise = null; - return true; - } - - copy(s: SendView) { - const sendLinkBaseUrl = this.environmentService.getSendUrl(); - const link = sendLinkBaseUrl + s.accessId + '/' + s.urlB64Key; - this.platformUtilsService.copyToClipboard(link); - this.platformUtilsService.showToast('success', null, - this.i18nService.t('valueCopied', this.i18nService.t('sendLink'))); - } - - searchTextChanged() { - this.search(200); - } - - selectAll() { - this.clearSelections(); - this.selectedAll = true; - this.applyFilter(null); - } - - selectType(type: SendType) { - this.clearSelections(); - this.selectedType = type; - this.applyFilter(s => s.type === type); - } - - clearSelections() { - this.selectedAll = false; - this.selectedType = null; - } - - private applyTextSearch() { - if (this.searchText != null) { - this.filteredSends = this.searchService.searchSends(this.filteredSends, this.searchText); - } + copy(s: SendView) { + const sendLinkBaseUrl = this.environmentService.getSendUrl(); + const link = sendLinkBaseUrl + s.accessId + "/" + s.urlB64Key; + this.platformUtilsService.copyToClipboard(link); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("valueCopied", this.i18nService.t("sendLink")) + ); + } + + searchTextChanged() { + this.search(200); + } + + selectAll() { + this.clearSelections(); + this.selectedAll = true; + this.applyFilter(null); + } + + selectType(type: SendType) { + this.clearSelections(); + this.selectedType = type; + this.applyFilter((s) => s.type === type); + } + + clearSelections() { + this.selectedAll = false; + this.selectedType = null; + } + + private applyTextSearch() { + if (this.searchText != null) { + this.filteredSends = this.searchService.searchSends(this.filteredSends, this.searchText); } + } } diff --git a/angular/src/components/set-password.component.ts b/angular/src/components/set-password.component.ts index c12e32e6..e651eca5 100644 --- a/angular/src/components/set-password.component.ts +++ b/angular/src/components/set-password.component.ts @@ -1,151 +1,184 @@ -import { Directive } from '@angular/core'; -import { - ActivatedRoute, - Router -} from '@angular/router'; +import { Directive } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { EncString } from 'jslib-common/models/domain/encString'; -import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; +import { EncString } from "jslib-common/models/domain/encString"; +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; -import { KeysRequest } from 'jslib-common/models/request/keysRequest'; -import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest'; -import { SetPasswordRequest } from 'jslib-common/models/request/setPasswordRequest'; +import { KeysRequest } from "jslib-common/models/request/keysRequest"; +import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest"; +import { SetPasswordRequest } from "jslib-common/models/request/setPasswordRequest"; -import { ChangePasswordComponent as BaseChangePasswordComponent } from './change-password.component'; +import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; -import { HashPurpose } from 'jslib-common/enums/hashPurpose'; -import { KdfType } from 'jslib-common/enums/kdfType'; +import { HashPurpose } from "jslib-common/enums/hashPurpose"; +import { KdfType } from "jslib-common/enums/kdfType"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; @Directive() export class SetPasswordComponent extends BaseChangePasswordComponent { - syncLoading: boolean = true; - showPassword: boolean = false; - hint: string = ''; - identifier: string = null; - orgId: string; - resetPasswordAutoEnroll = false; + syncLoading: boolean = true; + showPassword: boolean = false; + hint: string = ""; + identifier: string = null; + orgId: string; + resetPasswordAutoEnroll = false; - onSuccessfulChangePassword: () => Promise; - successRoute = 'vault'; + onSuccessfulChangePassword: () => Promise; + successRoute = "vault"; - constructor(i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, - userService: UserService, passwordGenerationService: PasswordGenerationService, - platformUtilsService: PlatformUtilsService, policyService: PolicyService, protected router: Router, - private apiService: ApiService, private syncService: SyncService, private route: ActivatedRoute) { - super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, - platformUtilsService, policyService); + constructor( + i18nService: I18nService, + cryptoService: CryptoService, + messagingService: MessagingService, + passwordGenerationService: PasswordGenerationService, + platformUtilsService: PlatformUtilsService, + policyService: PolicyService, + protected router: Router, + private apiService: ApiService, + private syncService: SyncService, + private route: ActivatedRoute, + stateService: StateService + ) { + super( + i18nService, + cryptoService, + messagingService, + passwordGenerationService, + platformUtilsService, + policyService, + stateService + ); + } + + async ngOnInit() { + await this.syncService.fullSync(true); + this.syncLoading = false; + + this.route.queryParams.pipe(first()).subscribe(async (qParams) => { + if (qParams.identifier != null) { + this.identifier = qParams.identifier; + } + }); + + // Automatic Enrollment Detection + if (this.identifier != null) { + try { + const response = await this.apiService.getOrganizationAutoEnrollStatus(this.identifier); + this.orgId = response.id; + this.resetPasswordAutoEnroll = response.resetPasswordEnabled; + } catch { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + } } - async ngOnInit() { - await this.syncService.fullSync(true); - this.syncLoading = false; + super.ngOnInit(); + } - this.route.queryParams.pipe(first()).subscribe(async qParams => { - if (qParams.identifier != null) { - this.identifier = qParams.identifier; + async setupSubmitActions() { + this.kdf = KdfType.PBKDF2_SHA256; + const useLowerKdf = this.platformUtilsService.isIE(); + this.kdfIterations = useLowerKdf ? 10000 : 100000; + return true; + } + + async performSubmitActions( + masterPasswordHash: string, + key: SymmetricCryptoKey, + encKey: [SymmetricCryptoKey, EncString] + ) { + const keys = await this.cryptoService.makeKeyPair(encKey[0]); + const request = new SetPasswordRequest( + masterPasswordHash, + encKey[1].encryptedString, + this.hint, + this.kdf, + this.kdfIterations, + this.identifier, + new KeysRequest(keys[0], keys[1].encryptedString) + ); + try { + if (this.resetPasswordAutoEnroll) { + this.formPromise = this.apiService + .setPassword(request) + .then(async () => { + await this.onSetPasswordSuccess(key, encKey, keys); + return this.apiService.getOrganizationKeys(this.orgId); + }) + .then(async (response) => { + if (response == null) { + throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); } + const userId = await this.stateService.getUserId(); + const publicKey = Utils.fromB64ToArray(response.publicKey); + + // RSA Encrypt user's encKey.key with organization public key + const userEncKey = await this.cryptoService.getEncKey(); + const encryptedKey = await this.cryptoService.rsaEncrypt( + userEncKey.key, + publicKey.buffer + ); + + const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + resetRequest.resetPasswordKey = encryptedKey.encryptedString; + + return this.apiService.putOrganizationUserResetPasswordEnrollment( + this.orgId, + userId, + resetRequest + ); + }); + } else { + this.formPromise = this.apiService.setPassword(request).then(async () => { + await this.onSetPasswordSuccess(key, encKey, keys); }); + } - // Automatic Enrollment Detection - if (this.identifier != null) { - try { - const response = await this.apiService.getOrganizationAutoEnrollStatus(this.identifier); - this.orgId = response.id; - this.resetPasswordAutoEnroll = response.resetPasswordEnabled; - } catch { - this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); - } - } + await this.formPromise; - super.ngOnInit(); + if (this.onSuccessfulChangePassword != null) { + this.onSuccessfulChangePassword(); + } else { + this.router.navigate([this.successRoute]); + } + } catch { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } + } - async setupSubmitActions() { - this.kdf = KdfType.PBKDF2_SHA256; - const useLowerKdf = this.platformUtilsService.isIE(); - this.kdfIterations = useLowerKdf ? 10000 : 100000; - return true; - } + togglePassword(confirmField: boolean) { + this.showPassword = !this.showPassword; + document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus(); + } - async performSubmitActions(masterPasswordHash: string, key: SymmetricCryptoKey, - encKey: [SymmetricCryptoKey, EncString]) { - const keys = await this.cryptoService.makeKeyPair(encKey[0]); - const request = new SetPasswordRequest( - masterPasswordHash, - encKey[1].encryptedString, - this.hint, - this.kdf, - this.kdfIterations, - this.identifier, - new KeysRequest(keys[0], keys[1].encryptedString) - ); - try { - if (this.resetPasswordAutoEnroll) { - this.formPromise = this.apiService.setPassword(request).then(async () => { - await this.onSetPasswordSuccess(key, encKey, keys); - return this.apiService.getOrganizationKeys(this.orgId); - }).then(async response => { - if (response == null) { - throw new Error(this.i18nService.t('resetPasswordOrgKeysError')); - } - const userId = await this.userService.getUserId(); - const publicKey = Utils.fromB64ToArray(response.publicKey); + private async onSetPasswordSuccess( + key: SymmetricCryptoKey, + encKey: [SymmetricCryptoKey, EncString], + keys: [string, EncString] + ) { + await this.stateService.setKdfType(this.kdf); + await this.stateService.setKdfIterations(this.kdfIterations); + await this.cryptoService.setKey(key); + await this.cryptoService.setEncKey(encKey[1].encryptedString); + await this.cryptoService.setEncPrivateKey(keys[1].encryptedString); - // RSA Encrypt user's encKey.key with organization public key - const userEncKey = await this.cryptoService.getEncKey(); - const encryptedKey = await this.cryptoService.rsaEncrypt(userEncKey.key, publicKey.buffer); - - const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); - resetRequest.resetPasswordKey = encryptedKey.encryptedString; - - return this.apiService.putOrganizationUserResetPasswordEnrollment(this.orgId, userId, resetRequest); - }); - } else { - this.formPromise = this.apiService.setPassword(request).then(async () => { - await this.onSetPasswordSuccess(key, encKey, keys); - }); - } - - await this.formPromise; - - if (this.onSuccessfulChangePassword != null) { - this.onSuccessfulChangePassword(); - } else { - this.router.navigate([this.successRoute]); - } - } catch { - this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); - } - } - - togglePassword(confirmField: boolean) { - this.showPassword = !this.showPassword; - document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); - } - - private async onSetPasswordSuccess(key: SymmetricCryptoKey, encKey: [SymmetricCryptoKey, EncString], keys: [string, EncString]) { - await this.userService.setInformation(await this.userService.getUserId(), await this.userService.getEmail(), - this.kdf, this.kdfIterations); - await this.cryptoService.setKey(key); - await this.cryptoService.setEncKey(encKey[1].encryptedString); - await this.cryptoService.setEncPrivateKey(keys[1].encryptedString); - - const localKeyHash = await this.cryptoService.hashPassword(this.masterPassword, key, - HashPurpose.LocalAuthorization); - await this.cryptoService.setKeyHash(localKeyHash); - } + const localKeyHash = await this.cryptoService.hashPassword( + this.masterPassword, + key, + HashPurpose.LocalAuthorization + ); + await this.cryptoService.setKeyHash(localKeyHash); + } } diff --git a/angular/src/components/set-pin.component.ts b/angular/src/components/set-pin.component.ts index d2cfae3a..bc1e804e 100644 --- a/angular/src/components/set-pin.component.ts +++ b/angular/src/components/set-pin.component.ts @@ -1,59 +1,55 @@ -import { - Directive, - OnInit -} from '@angular/core'; +import { Directive, OnInit } from "@angular/core"; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { Utils } from "jslib-common/misc/utils"; -import { Utils } from 'jslib-common/misc/utils'; - -import { ModalRef } from './modal/modal.ref'; +import { ModalRef } from "./modal/modal.ref"; @Directive() export class SetPinComponent implements OnInit { + pin = ""; + showPin = false; + masterPassOnRestart = true; + showMasterPassOnRestart = true; - pin = ''; - showPin = false; - masterPassOnRestart = true; - showMasterPassOnRestart = true; + constructor( + private modalRef: ModalRef, + private cryptoService: CryptoService, + private keyConnectorService: KeyConnectorService, + private stateService: StateService + ) {} - constructor(private modalRef: ModalRef, private cryptoService: CryptoService, private userService: UserService, - private storageService: StorageService, private vaultTimeoutService: VaultTimeoutService, - private keyConnectorService: KeyConnectorService) { } + async ngOnInit() { + this.showMasterPassOnRestart = this.masterPassOnRestart = + !(await this.keyConnectorService.getUsesKeyConnector()); + } - async ngOnInit() { - this.showMasterPassOnRestart = this.masterPassOnRestart = !await this.keyConnectorService.getUsesKeyConnector(); + toggleVisibility() { + this.showPin = !this.showPin; + } + + async submit() { + if (Utils.isNullOrWhitespace(this.pin)) { + this.modalRef.close(false); } - toggleVisibility() { - this.showPin = !this.showPin; + const kdf = await this.stateService.getKdfType(); + const kdfIterations = await this.stateService.getKdfIterations(); + const email = await this.stateService.getEmail(); + const pinKey = await this.cryptoService.makePinKey(this.pin, email, kdf, kdfIterations); + const key = await this.cryptoService.getKey(); + const pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey); + if (this.masterPassOnRestart) { + const encPin = await this.cryptoService.encrypt(this.pin); + await this.stateService.setProtectedPin(encPin.encryptedString); + await this.stateService.setDecryptedPinProtected(pinProtectedKey); + } else { + await this.stateService.setEncryptedPinProtected(pinProtectedKey.encryptedString); } - async submit() { - if (Utils.isNullOrWhitespace(this.pin)) { - this.modalRef.close(false); - } - - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); - const email = await this.userService.getEmail(); - const pinKey = await this.cryptoService.makePinKey(this.pin, email, kdf, kdfIterations); - const key = await this.cryptoService.getKey(); - const pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey); - if (this.masterPassOnRestart) { - const encPin = await this.cryptoService.encrypt(this.pin); - await this.storageService.save(ConstantsService.protectedPin, encPin.encryptedString); - this.vaultTimeoutService.pinProtectedKey = pinProtectedKey; - } else { - await this.storageService.save(ConstantsService.pinProtectedKey, pinProtectedKey.encryptedString); - } - - this.modalRef.close(true); - } + this.modalRef.close(true); + } } diff --git a/angular/src/components/share.component.ts b/angular/src/components/share.component.ts index e5badc37..6a13e0b2 100644 --- a/angular/src/components/share.component.ts +++ b/angular/src/components/share.component.ts @@ -1,108 +1,119 @@ -import { - Directive, - EventEmitter, - Input, - OnInit, - Output, -} from '@angular/core'; +import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType'; +import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType"; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService } from 'jslib-common/abstractions/collection.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { Organization } from 'jslib-common/models/domain/organization'; -import { CipherView } from 'jslib-common/models/view/cipherView'; -import { CollectionView } from 'jslib-common/models/view/collectionView'; +import { Organization } from "jslib-common/models/domain/organization"; +import { CipherView } from "jslib-common/models/view/cipherView"; +import { CollectionView } from "jslib-common/models/view/collectionView"; -import { Utils } from 'jslib-common/misc/utils'; +import { Utils } from "jslib-common/misc/utils"; @Directive() export class ShareComponent implements OnInit { - @Input() cipherId: string; - @Input() organizationId: string; - @Output() onSharedCipher = new EventEmitter(); + @Input() cipherId: string; + @Input() organizationId: string; + @Output() onSharedCipher = new EventEmitter(); - formPromise: Promise; - cipher: CipherView; - collections: CollectionView[] = []; - organizations: Organization[] = []; + formPromise: Promise; + cipher: CipherView; + collections: CollectionView[] = []; + organizations: Organization[] = []; - protected writeableCollections: CollectionView[] = []; + protected writeableCollections: CollectionView[] = []; - constructor(protected collectionService: CollectionService, protected platformUtilsService: PlatformUtilsService, - protected i18nService: I18nService, protected userService: UserService, - protected cipherService: CipherService, private logService: LogService) { } + constructor( + protected collectionService: CollectionService, + protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, + protected cipherService: CipherService, + private logService: LogService, + protected organizationService: OrganizationService + ) {} - async ngOnInit() { - await this.load(); + async ngOnInit() { + await this.load(); + } + + async load() { + const allCollections = await this.collectionService.getAllDecrypted(); + this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); + const orgs = await this.organizationService.getAll(); + this.organizations = orgs + .sort(Utils.getSortFunction(this.i18nService, "name")) + .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed); + + const cipherDomain = await this.cipherService.get(this.cipherId); + this.cipher = await cipherDomain.decrypt(); + if (this.organizationId == null && this.organizations.length > 0) { + this.organizationId = this.organizations[0].id; + } + this.filterCollections(); + } + + filterCollections() { + this.writeableCollections.forEach((c) => ((c as any).checked = false)); + if (this.organizationId == null || this.writeableCollections.length === 0) { + this.collections = []; + } else { + this.collections = this.writeableCollections.filter( + (c) => c.organizationId === this.organizationId + ); + } + } + + async submit(): Promise { + const selectedCollectionIds = this.collections + .filter((c) => !!(c as any).checked) + .map((c) => c.id); + if (selectedCollectionIds.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("selectOneCollection") + ); + return; } - async load() { - const allCollections = await this.collectionService.getAllDecrypted(); - this.writeableCollections = allCollections.map(c => c).filter(c => !c.readOnly); - const orgs = await this.userService.getAllOrganizations(); - this.organizations = orgs.sort(Utils.getSortFunction(this.i18nService, 'name')) - .filter(o => o.enabled && o.status === OrganizationUserStatusType.Confirmed); + const cipherDomain = await this.cipherService.get(this.cipherId); + const cipherView = await cipherDomain.decrypt(); + const orgName = + this.organizations.find((o) => o.id === this.organizationId)?.name ?? + this.i18nService.t("organization"); - const cipherDomain = await this.cipherService.get(this.cipherId); - this.cipher = await cipherDomain.decrypt(); - if (this.organizationId == null && this.organizations.length > 0) { - this.organizationId = this.organizations[0].id; - } - this.filterCollections(); + try { + this.formPromise = this.cipherService + .shareWithServer(cipherView, this.organizationId, selectedCollectionIds) + .then(async () => { + this.onSharedCipher.emit(); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("movedItemToOrg", cipherView.name, orgName) + ); + }); + await this.formPromise; + return true; + } catch (e) { + this.logService.error(e); } + return false; + } - filterCollections() { - this.writeableCollections.forEach(c => (c as any).checked = false); - if (this.organizationId == null || this.writeableCollections.length === 0) { - this.collections = []; - } else { - this.collections = this.writeableCollections.filter(c => c.organizationId === this.organizationId); + get canSave() { + if (this.collections != null) { + for (let i = 0; i < this.collections.length; i++) { + if ((this.collections[i] as any).checked) { + return true; } + } } - - async submit(): Promise { - const selectedCollectionIds = this.collections - .filter(c => !!(c as any).checked) - .map(c => c.id); - if (selectedCollectionIds.length === 0) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('selectOneCollection')); - return; - } - - const cipherDomain = await this.cipherService.get(this.cipherId); - const cipherView = await cipherDomain.decrypt(); - const orgName = this.organizations.find(o => o.id === this.organizationId)?.name ?? this.i18nService.t('organization'); - - try { - this.formPromise = this.cipherService.shareWithServer(cipherView, this.organizationId, - selectedCollectionIds).then(async () => { - this.onSharedCipher.emit(); - this.platformUtilsService.showToast('success', null, - this.i18nService.t('movedItemToOrg', cipherView.name, orgName)); - }); - await this.formPromise; - return true; - } catch (e) { - this.logService.error(e); - } - return false; - } - - get canSave() { - if (this.collections != null) { - for (let i = 0; i < this.collections.length; i++) { - if ((this.collections[i] as any).checked) { - return true; - } - } - } - return false; - } + return false; + } } diff --git a/angular/src/components/sso.component.ts b/angular/src/components/sso.component.ts index 1ab8e2f4..649f298c 100644 --- a/angular/src/components/sso.component.ts +++ b/angular/src/components/sso.component.ts @@ -1,214 +1,254 @@ -import { Directive } from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { Directive } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { Utils } from "jslib-common/misc/utils"; -import { Utils } from 'jslib-common/misc/utils'; - -import { AuthResult } from 'jslib-common/models/domain/authResult'; +import { AuthResult } from "jslib-common/models/domain/authResult"; @Directive() export class SsoComponent { - identifier: string; - loggingIn = false; + identifier: string; + loggingIn = false; - formPromise: Promise; - initiateSsoFormPromise: Promise; - onSuccessfulLogin: () => Promise; - onSuccessfulLoginNavigate: () => Promise; - onSuccessfulLoginTwoFactorNavigate: () => Promise; - onSuccessfulLoginChangePasswordNavigate: () => Promise; - onSuccessfulLoginForceResetNavigate: () => Promise; + formPromise: Promise; + initiateSsoFormPromise: Promise; + onSuccessfulLogin: () => Promise; + onSuccessfulLoginNavigate: () => Promise; + onSuccessfulLoginTwoFactorNavigate: () => Promise; + onSuccessfulLoginChangePasswordNavigate: () => Promise; + onSuccessfulLoginForceResetNavigate: () => Promise; - protected twoFactorRoute = '2fa'; - protected successRoute = 'lock'; - protected changePasswordRoute = 'set-password'; - protected forcePasswordResetRoute = 'update-temp-password'; - protected clientId: string; - protected redirectUri: string; - protected state: string; - protected codeChallenge: string; + protected twoFactorRoute = "2fa"; + protected successRoute = "lock"; + protected changePasswordRoute = "set-password"; + protected forcePasswordResetRoute = "update-temp-password"; + protected clientId: string; + protected redirectUri: string; + protected state: string; + protected codeChallenge: string; - constructor(protected authService: AuthService, protected router: Router, - protected i18nService: I18nService, protected route: ActivatedRoute, - protected storageService: StorageService, protected stateService: StateService, - protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService, - protected cryptoFunctionService: CryptoFunctionService, protected environmentService: EnvironmentService, - protected passwordGenerationService: PasswordGenerationService, protected logService: LogService) { } + constructor( + protected authService: AuthService, + protected router: Router, + protected i18nService: I18nService, + protected route: ActivatedRoute, + protected stateService: StateService, + protected platformUtilsService: PlatformUtilsService, + protected apiService: ApiService, + protected cryptoFunctionService: CryptoFunctionService, + protected environmentService: EnvironmentService, + protected passwordGenerationService: PasswordGenerationService, + protected logService: LogService + ) {} - async ngOnInit() { - this.route.queryParams.pipe(first()).subscribe(async qParams => { - if (qParams.code != null && qParams.state != null) { - const codeVerifier = await this.storageService.get(ConstantsService.ssoCodeVerifierKey); - const state = await this.storageService.get(ConstantsService.ssoStateKey); - await this.storageService.remove(ConstantsService.ssoCodeVerifierKey); - await this.storageService.remove(ConstantsService.ssoStateKey); - if (qParams.code != null && codeVerifier != null && state != null && this.checkState(state, qParams.state)) { - await this.logIn(qParams.code, codeVerifier, this.getOrgIdentifierFromState(qParams.state)); - } - } else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null && - qParams.codeChallenge != null) { - this.redirectUri = qParams.redirectUri; - this.state = qParams.state; - this.codeChallenge = qParams.codeChallenge; - this.clientId = qParams.clientId; - } - }); + async ngOnInit() { + this.route.queryParams.pipe(first()).subscribe(async (qParams) => { + if (qParams.code != null && qParams.state != null) { + const codeVerifier = await this.stateService.getSsoCodeVerifier(); + const state = await this.stateService.getSsoState(); + await this.stateService.setSsoCodeVerifier(null); + await this.stateService.setSsoState(null); + if ( + qParams.code != null && + codeVerifier != null && + state != null && + this.checkState(state, qParams.state) + ) { + await this.logIn( + qParams.code, + codeVerifier, + this.getOrgIdentifierFromState(qParams.state) + ); + } + } else if ( + qParams.clientId != null && + qParams.redirectUri != null && + qParams.state != null && + qParams.codeChallenge != null + ) { + this.redirectUri = qParams.redirectUri; + this.state = qParams.state; + this.codeChallenge = qParams.codeChallenge; + this.clientId = qParams.clientId; + } + }); + } + + async submit(returnUri?: string, includeUserIdentifier?: boolean) { + this.initiateSsoFormPromise = this.preValidate(); + if (await this.initiateSsoFormPromise) { + const authorizeUrl = await this.buildAuthorizeUrl(returnUri, includeUserIdentifier); + this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + } + } + + async preValidate(): Promise { + if (this.identifier == null || this.identifier === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("ssoValidationFailed"), + this.i18nService.t("ssoIdentifierRequired") + ); + return false; + } + return await this.apiService.preValidateSso(this.identifier); + } + + protected async buildAuthorizeUrl( + returnUri?: string, + includeUserIdentifier?: boolean + ): Promise { + let codeChallenge = this.codeChallenge; + let state = this.state; + + const passwordOptions: any = { + type: "password", + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + + if (codeChallenge == null) { + const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256"); + codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + await this.stateService.setSsoCodeVerifier(codeVerifier); } - async submit(returnUri?: string, includeUserIdentifier?: boolean) { - this.initiateSsoFormPromise = this.preValidate(); - if (await this.initiateSsoFormPromise) { - const authorizeUrl = await this.buildAuthorizeUrl(returnUri, includeUserIdentifier); - this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); - } + if (state == null) { + state = await this.passwordGenerationService.generatePassword(passwordOptions); + if (returnUri) { + state += `_returnUri='${returnUri}'`; + } } - async preValidate(): Promise { - if (this.identifier == null || this.identifier === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('ssoValidationFailed'), - this.i18nService.t('ssoIdentifierRequired')); - return false; - } - return await this.apiService.preValidateSso(this.identifier); + // Add Organization Identifier to state + state += `_identifier=${this.identifier}`; + + // Save state (regardless of new or existing) + await this.stateService.setSsoState(state); + + let authorizeUrl = + this.environmentService.getIdentityUrl() + + "/connect/authorize?" + + "client_id=" + + this.clientId + + "&redirect_uri=" + + encodeURIComponent(this.redirectUri) + + "&" + + "response_type=code&scope=api offline_access&" + + "state=" + + state + + "&code_challenge=" + + codeChallenge + + "&" + + "code_challenge_method=S256&response_mode=query&" + + "domain_hint=" + + encodeURIComponent(this.identifier); + + if (includeUserIdentifier) { + const userIdentifier = await this.apiService.getSsoUserIdentifier(); + authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`; } - protected async buildAuthorizeUrl(returnUri?: string, includeUserIdentifier?: boolean): Promise { - let codeChallenge = this.codeChallenge; - let state = this.state; + return authorizeUrl; + } - const passwordOptions: any = { - type: 'password', - length: 64, - uppercase: true, - lowercase: true, - numbers: true, - special: false, - }; - - if (codeChallenge == null) { - const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); - codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); - await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); + private async logIn(code: string, codeVerifier: string, orgIdFromState: string) { + this.loggingIn = true; + try { + this.formPromise = this.authService.logInSso( + code, + codeVerifier, + this.redirectUri, + orgIdFromState + ); + const response = await this.formPromise; + if (response.twoFactor) { + if (this.onSuccessfulLoginTwoFactorNavigate != null) { + this.onSuccessfulLoginTwoFactorNavigate(); + } else { + this.router.navigate([this.twoFactorRoute], { + queryParams: { + identifier: orgIdFromState, + sso: "true", + }, + }); } - - if (state == null) { - state = await this.passwordGenerationService.generatePassword(passwordOptions); - if (returnUri) { - state += `_returnUri='${returnUri}'`; - } + } else if (response.resetMasterPassword) { + if (this.onSuccessfulLoginChangePasswordNavigate != null) { + this.onSuccessfulLoginChangePasswordNavigate(); + } else { + this.router.navigate([this.changePasswordRoute], { + queryParams: { + identifier: orgIdFromState, + }, + }); } - - // Add Organization Identifier to state - state += `_identifier=${this.identifier}`; - - // Save state (regardless of new or existing) - await this.storageService.save(ConstantsService.ssoStateKey, state); - - let authorizeUrl = this.environmentService.getIdentityUrl() + '/connect/authorize?' + - 'client_id=' + this.clientId + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' + - 'response_type=code&scope=api offline_access&' + - 'state=' + state + '&code_challenge=' + codeChallenge + '&' + - 'code_challenge_method=S256&response_mode=query&' + - 'domain_hint=' + encodeURIComponent(this.identifier); - - if (includeUserIdentifier) { - const userIdentifier = await this.apiService.getSsoUserIdentifier(); - authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`; + } else if (response.forcePasswordReset) { + if (this.onSuccessfulLoginForceResetNavigate != null) { + this.onSuccessfulLoginForceResetNavigate(); + } else { + this.router.navigate([this.forcePasswordResetRoute]); } + } else { + const disableFavicon = await this.stateService.getDisableFavicon(); + await this.stateService.setDisableFavicon(!!disableFavicon); + if (this.onSuccessfulLogin != null) { + this.onSuccessfulLogin(); + } + if (this.onSuccessfulLoginNavigate != null) { + this.onSuccessfulLoginNavigate(); + } else { + this.router.navigate([this.successRoute]); + } + } + } catch (e) { + this.logService.error(e); + if (e.message === "Unable to reach key connector") { + this.platformUtilsService.showToast( + "error", + null, + this.i18nService.t("ssoKeyConnectorUnavailable") + ); + } + } + this.loggingIn = false; + } - return authorizeUrl; + private getOrgIdentifierFromState(state: string): string { + if (state === null || state === undefined) { + return null; } - private async logIn(code: string, codeVerifier: string, orgIdFromState: string) { - this.loggingIn = true; - try { - this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri, orgIdFromState); - const response = await this.formPromise; - if (response.twoFactor) { - if (this.onSuccessfulLoginTwoFactorNavigate != null) { - this.onSuccessfulLoginTwoFactorNavigate(); - } else { - this.router.navigate([this.twoFactorRoute], { - queryParams: { - identifier: orgIdFromState, - sso: 'true', - }, - }); - } - } else if (response.resetMasterPassword) { - if (this.onSuccessfulLoginChangePasswordNavigate != null) { - this.onSuccessfulLoginChangePasswordNavigate(); - } else { - this.router.navigate([this.changePasswordRoute], { - queryParams: { - identifier: orgIdFromState, - }, - }); - } - } else if (response.forcePasswordReset) { - if (this.onSuccessfulLoginForceResetNavigate != null) { - this.onSuccessfulLoginForceResetNavigate(); - } else { - this.router.navigate([this.forcePasswordResetRoute]); - } - } else { - const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); - await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); - if (this.onSuccessfulLogin != null) { - this.onSuccessfulLogin(); - } - if (this.onSuccessfulLoginNavigate != null) { - this.onSuccessfulLoginNavigate(); - } else { - this.router.navigate([this.successRoute]); - } - } - } catch (e) { - this.logService.error(e); - if (e.message === 'Unable to reach key connector') { - this.platformUtilsService.showToast('error', null, this.i18nService.t('ssoKeyConnectorUnavailable')); - } - } - this.loggingIn = false; + const stateSplit = state.split("_identifier="); + return stateSplit.length > 1 ? stateSplit[1] : null; + } + + private checkState(state: string, checkState: string): boolean { + if (state === null || state === undefined) { + return false; + } + if (checkState === null || checkState === undefined) { + return false; } - private getOrgIdentifierFromState(state: string): string { - if (state === null || state === undefined) { - return null; - } - - const stateSplit = state.split('_identifier='); - return stateSplit.length > 1 ? stateSplit[1] : null; - } - - private checkState(state: string, checkState: string): boolean { - if (state === null || state === undefined) { - return false; - } - if (checkState === null || checkState === undefined) { - return false; - } - - const stateSplit = state.split('_identifier='); - const checkStateSplit = checkState.split('_identifier='); - return stateSplit[0] === checkStateSplit[0]; - } + const stateSplit = state.split("_identifier="); + const checkStateSplit = checkState.split("_identifier="); + return stateSplit[0] === checkStateSplit[0]; + } } diff --git a/angular/src/components/toastr.component.ts b/angular/src/components/toastr.component.ts new file mode 100644 index 00000000..0e4ea4ea --- /dev/null +++ b/angular/src/components/toastr.component.ts @@ -0,0 +1,95 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { CommonModule } from "@angular/common"; +import { Component, ModuleWithProviders, NgModule } from "@angular/core"; +import { + DefaultNoComponentGlobalConfig, + GlobalConfig, + Toast as BaseToast, + ToastPackage, + ToastrService, + TOAST_CONFIG, +} from "ngx-toastr"; + +@Component({ + selector: "[toast-component2]", + template: ` + +
+ +
+
+
+ {{ title }} [{{ duplicatesCount + 1 }}] +
+
+
+ {{ message }} +
+
+
+
+
+ `, + animations: [ + trigger("flyInOut", [ + state("inactive", style({ opacity: 0 })), + state("active", style({ opacity: 1 })), + state("removed", style({ opacity: 0 })), + transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), + transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), + ]), + ], + preserveWhitespaces: false, +}) +export class BitwardenToast extends BaseToast { + constructor(protected toastrService: ToastrService, public toastPackage: ToastPackage) { + super(toastrService, toastPackage); + } +} + +export const BitwardenToastGlobalConfig: GlobalConfig = { + ...DefaultNoComponentGlobalConfig, + toastComponent: BitwardenToast, +}; + +@NgModule({ + imports: [CommonModule], + declarations: [BitwardenToast], + exports: [BitwardenToast], +}) +export class BitwardenToastModule { + static forRoot(config: Partial = {}): ModuleWithProviders { + return { + ngModule: BitwardenToastModule, + providers: [ + { + provide: TOAST_CONFIG, + useValue: { + default: BitwardenToastGlobalConfig, + config: config, + }, + }, + ], + }; + } +} diff --git a/angular/src/components/two-factor.component.ts b/angular/src/components/two-factor.component.ts index 5ad45a25..f3813a96 100644 --- a/angular/src/components/two-factor.component.ts +++ b/angular/src/components/two-factor.component.ts @@ -1,254 +1,280 @@ -import { Directive, OnDestroy, OnInit } from '@angular/core'; +import { Directive, OnDestroy, OnInit } from "@angular/core"; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { ActivatedRoute, Router } from "@angular/router"; -import { first } from 'rxjs/operators'; +import { first } from "rxjs/operators"; -import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType'; +import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; -import { TwoFactorEmailRequest } from 'jslib-common/models/request/twoFactorEmailRequest'; +import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest"; -import { AuthResult } from 'jslib-common/models/domain/authResult'; +import { AuthResult } from "jslib-common/models/domain/authResult"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { TwoFactorProviders } from 'jslib-common/services/auth.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { TwoFactorProviders } from "jslib-common/services/auth.service"; -import * as DuoWebSDK from 'duo_web_sdk'; -import { WebAuthnIFrame } from 'jslib-common/misc/webauthn_iframe'; +import * as DuoWebSDK from "duo_web_sdk"; +import { WebAuthnIFrame } from "jslib-common/misc/webauthn_iframe"; @Directive() export class TwoFactorComponent implements OnInit, OnDestroy { - token: string = ''; - remember: boolean = false; - webAuthnReady: boolean = false; - webAuthnNewTab: boolean = false; - providers = TwoFactorProviders; - providerType = TwoFactorProviderType; - selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator; - webAuthnSupported: boolean = false; - webAuthn: WebAuthnIFrame = null; - title: string = ''; - twoFactorEmail: string = null; - formPromise: Promise; - emailPromise: Promise; - identifier: string = null; - onSuccessfulLogin: () => Promise; - onSuccessfulLoginNavigate: () => Promise; + token: string = ""; + remember: boolean = false; + webAuthnReady: boolean = false; + webAuthnNewTab: boolean = false; + providers = TwoFactorProviders; + providerType = TwoFactorProviderType; + selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator; + webAuthnSupported: boolean = false; + webAuthn: WebAuthnIFrame = null; + title: string = ""; + twoFactorEmail: string = null; + formPromise: Promise; + emailPromise: Promise; + identifier: string = null; + onSuccessfulLogin: () => Promise; + onSuccessfulLoginNavigate: () => Promise; - get webAuthnAllow(): string { - return `publickey-credentials-get ${this.environmentService.getWebVaultUrl()}`; + get webAuthnAllow(): string { + return `publickey-credentials-get ${this.environmentService.getWebVaultUrl()}`; + } + + protected loginRoute = "login"; + protected successRoute = "vault"; + + constructor( + protected authService: AuthService, + protected router: Router, + protected i18nService: I18nService, + protected apiService: ApiService, + protected platformUtilsService: PlatformUtilsService, + protected win: Window, + protected environmentService: EnvironmentService, + protected stateService: StateService, + protected route: ActivatedRoute, + protected logService: LogService + ) { + this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); + } + + async ngOnInit() { + if (!this.authing || this.authService.twoFactorProvidersData == null) { + this.router.navigate([this.loginRoute]); + return; } - protected loginRoute = 'login'; - protected successRoute = 'vault'; + this.route.queryParams.pipe(first()).subscribe((qParams) => { + if (qParams.identifier != null) { + this.identifier = qParams.identifier; + } + }); - constructor(protected authService: AuthService, protected router: Router, - protected i18nService: I18nService, protected apiService: ApiService, - protected platformUtilsService: PlatformUtilsService, protected win: Window, - protected environmentService: EnvironmentService, protected stateService: StateService, - protected storageService: StorageService, protected route: ActivatedRoute, - protected logService: LogService) { - this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); + if (this.needsLock) { + this.successRoute = "lock"; } - async ngOnInit() { - if (!this.authing || this.authService.twoFactorProvidersData == null) { - this.router.navigate([this.loginRoute]); - return; + if (this.win != null && this.webAuthnSupported) { + const webVaultUrl = this.environmentService.getWebVaultUrl(); + this.webAuthn = new WebAuthnIFrame( + this.win, + webVaultUrl, + this.webAuthnNewTab, + this.platformUtilsService, + this.i18nService, + (token: string) => { + this.token = token; + this.submit(); + }, + (error: string) => { + this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error); + }, + (info: string) => { + if (info === "ready") { + this.webAuthnReady = true; + } } - - this.route.queryParams.pipe(first()).subscribe(qParams => { - if (qParams.identifier != null) { - this.identifier = qParams.identifier; - } - }); - - if (this.needsLock) { - this.successRoute = 'lock'; - } - - if (this.win != null && this.webAuthnSupported) { - const webVaultUrl = this.environmentService.getWebVaultUrl(); - this.webAuthn = new WebAuthnIFrame(this.win, webVaultUrl, this.webAuthnNewTab, this.platformUtilsService, - this.i18nService, (token: string) => { - this.token = token; - this.submit(); - }, (error: string) => { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), error); - }, (info: string) => { - if (info === 'ready') { - this.webAuthnReady = true; - } - } - ); - } - - this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.webAuthnSupported); - await this.init(); + ); } - ngOnDestroy(): void { - this.cleanupWebAuthn(); - this.webAuthn = null; + this.selectedProviderType = this.authService.getDefaultTwoFactorProvider( + this.webAuthnSupported + ); + await this.init(); + } + + ngOnDestroy(): void { + this.cleanupWebAuthn(); + this.webAuthn = null; + } + + async init() { + if (this.selectedProviderType == null) { + this.title = this.i18nService.t("loginUnavailable"); + return; } - async init() { - if (this.selectedProviderType == null) { - this.title = this.i18nService.t('loginUnavailable'); - return; + this.cleanupWebAuthn(); + this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; + const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType); + switch (this.selectedProviderType) { + case TwoFactorProviderType.WebAuthn: + if (!this.webAuthnNewTab) { + setTimeout(() => { + this.authWebAuthn(); + }, 500); } + break; + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.OrganizationDuo: + setTimeout(() => { + DuoWebSDK.init({ + iframe: undefined, + host: providerData.Host, + sig_request: providerData.Signature, + submit_callback: async (f: HTMLFormElement) => { + const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; + if (sig != null) { + this.token = sig.value; + await this.submit(); + } + }, + }); + }, 0); + break; + case TwoFactorProviderType.Email: + this.twoFactorEmail = providerData.Email; + if (this.authService.twoFactorProvidersData.size > 1) { + await this.sendEmail(false); + } + break; + default: + break; + } + } - this.cleanupWebAuthn(); - this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; - const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType); - switch (this.selectedProviderType) { - case TwoFactorProviderType.WebAuthn: - if (!this.webAuthnNewTab) { - setTimeout(() => { - this.authWebAuthn(); - }, 500); - } - break; - case TwoFactorProviderType.Duo: - case TwoFactorProviderType.OrganizationDuo: - setTimeout(() => { - DuoWebSDK.init({ - iframe: undefined, - host: providerData.Host, - sig_request: providerData.Signature, - submit_callback: async (f: HTMLFormElement) => { - const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; - if (sig != null) { - this.token = sig.value; - await this.submit(); - } - }, - }); - }, 0); - break; - case TwoFactorProviderType.Email: - this.twoFactorEmail = providerData.Email; - if (this.authService.twoFactorProvidersData.size > 1) { - await this.sendEmail(false); - } - break; - default: - break; - } + async submit() { + if (this.token == null || this.token === "") { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("verificationCodeRequired") + ); + return; } - async submit() { - if (this.token == null || this.token === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('verificationCodeRequired')); - return; - } - - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn) { - if (this.webAuthn != null) { - this.webAuthn.stop(); - } else { - return; - } - } else if (this.selectedProviderType === TwoFactorProviderType.Email || - this.selectedProviderType === TwoFactorProviderType.Authenticator) { - this.token = this.token.replace(' ', '').trim(); - } - - try { - await this.doSubmit(); - } catch { - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && this.webAuthn != null) { - this.webAuthn.start(); - } - } + if (this.selectedProviderType === TwoFactorProviderType.WebAuthn) { + if (this.webAuthn != null) { + this.webAuthn.stop(); + } else { + return; + } + } else if ( + this.selectedProviderType === TwoFactorProviderType.Email || + this.selectedProviderType === TwoFactorProviderType.Authenticator + ) { + this.token = this.token.replace(" ", "").trim(); } - async doSubmit() { - this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); - const response: AuthResult = await this.formPromise; - const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); - await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); - if (this.onSuccessfulLogin != null) { - this.onSuccessfulLogin(); - } - if (response.resetMasterPassword) { - this.successRoute = 'set-password'; - } - if (response.forcePasswordReset) { - this.successRoute = 'update-temp-password'; - } - if (this.onSuccessfulLoginNavigate != null) { - this.onSuccessfulLoginNavigate(); - } else { - this.router.navigate([this.successRoute], { - queryParams: { - identifier: this.identifier, - }, - }); - } + try { + await this.doSubmit(); + } catch { + if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && this.webAuthn != null) { + this.webAuthn.start(); + } + } + } + + async doSubmit() { + this.formPromise = this.authService.logInTwoFactor( + this.selectedProviderType, + this.token, + this.remember + ); + const response: AuthResult = await this.formPromise; + const disableFavicon = await this.stateService.getDisableFavicon(); + await this.stateService.setDisableFavicon(!!disableFavicon); + if (this.onSuccessfulLogin != null) { + this.onSuccessfulLogin(); + } + if (response.resetMasterPassword) { + this.successRoute = "set-password"; + } + if (response.forcePasswordReset) { + this.successRoute = "update-temp-password"; + } + if (this.onSuccessfulLoginNavigate != null) { + this.onSuccessfulLoginNavigate(); + } else { + this.router.navigate([this.successRoute], { + queryParams: { + identifier: this.identifier, + }, + }); + } + } + + async sendEmail(doToast: boolean) { + if (this.selectedProviderType !== TwoFactorProviderType.Email) { + return; } - async sendEmail(doToast: boolean) { - if (this.selectedProviderType !== TwoFactorProviderType.Email) { - return; - } - - if (this.emailPromise != null) { - return; - } - - try { - const request = new TwoFactorEmailRequest(); - request.email = this.authService.email; - request.masterPasswordHash = this.authService.masterPasswordHash; - this.emailPromise = this.apiService.postTwoFactorEmail(request); - await this.emailPromise; - if (doToast) { - this.platformUtilsService.showToast('success', null, - this.i18nService.t('verificationCodeEmailSent', this.twoFactorEmail)); - } - } catch (e) { - this.logService.error(e); - } - - this.emailPromise = null; + if (this.emailPromise != null) { + return; } - authWebAuthn() { - const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType); - - if (!this.webAuthnSupported || this.webAuthn == null) { - return; - } - - this.webAuthn.init(providerData); + try { + const request = new TwoFactorEmailRequest(); + request.email = this.authService.email; + request.masterPasswordHash = this.authService.masterPasswordHash; + this.emailPromise = this.apiService.postTwoFactorEmail(request); + await this.emailPromise; + if (doToast) { + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail) + ); + } + } catch (e) { + this.logService.error(e); } - private cleanupWebAuthn() { - if (this.webAuthn != null) { - this.webAuthn.stop(); - this.webAuthn.cleanup(); - } + this.emailPromise = null; + } + + authWebAuthn() { + const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType); + + if (!this.webAuthnSupported || this.webAuthn == null) { + return; } - get authing(): boolean { - return this.authService.authingWithPassword() || this.authService.authingWithSso() || this.authService.authingWithApiKey(); - } + this.webAuthn.init(providerData); + } - get needsLock(): boolean { - return this.authService.authingWithSso() || this.authService.authingWithApiKey(); + private cleanupWebAuthn() { + if (this.webAuthn != null) { + this.webAuthn.stop(); + this.webAuthn.cleanup(); } + } + + get authing(): boolean { + return ( + this.authService.authingWithPassword() || + this.authService.authingWithSso() || + this.authService.authingWithApiKey() + ); + } + + get needsLock(): boolean { + return this.authService.authingWithSso() || this.authService.authingWithApiKey(); + } } diff --git a/angular/src/components/update-temp-password.component.ts b/angular/src/components/update-temp-password.component.ts index 29e54521..bff997e7 100644 --- a/angular/src/components/update-temp-password.component.ts +++ b/angular/src/components/update-temp-password.component.ts @@ -1,109 +1,134 @@ -import { Directive } from '@angular/core'; +import { Directive } from "@angular/core"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { ChangePasswordComponent as BaseChangePasswordComponent } from './change-password.component'; +import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; -import { EncString } from 'jslib-common/models/domain/encString'; -import { MasterPasswordPolicyOptions } from 'jslib-common/models/domain/masterPasswordPolicyOptions'; -import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; +import { EncString } from "jslib-common/models/domain/encString"; +import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions"; +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; -import { UpdateTempPasswordRequest } from 'jslib-common/models/request/updateTempPasswordRequest'; +import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest"; @Directive() export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { - hint: string; - key: string; - enforcedPolicyOptions: MasterPasswordPolicyOptions; - showPassword: boolean = false; + hint: string; + key: string; + enforcedPolicyOptions: MasterPasswordPolicyOptions; + showPassword: boolean = false; - onSuccessfulChangePassword: () => Promise; + onSuccessfulChangePassword: () => Promise; - constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, - passwordGenerationService: PasswordGenerationService, policyService: PolicyService, - cryptoService: CryptoService, userService: UserService, - messagingService: MessagingService, private apiService: ApiService, - private syncService: SyncService, private logService: LogService) { - super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, - platformUtilsService, policyService); + constructor( + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + passwordGenerationService: PasswordGenerationService, + policyService: PolicyService, + cryptoService: CryptoService, + messagingService: MessagingService, + private apiService: ApiService, + stateService: StateService, + private syncService: SyncService, + private logService: LogService + ) { + super( + i18nService, + cryptoService, + messagingService, + passwordGenerationService, + platformUtilsService, + policyService, + stateService + ); + } + + async ngOnInit() { + await this.syncService.fullSync(true); + super.ngOnInit(); + } + + togglePassword(confirmField: boolean) { + this.showPassword = !this.showPassword; + document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus(); + } + + async setupSubmitActions(): Promise { + this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); + this.email = await this.stateService.getEmail(); + this.kdf = await this.stateService.getKdfType(); + this.kdfIterations = await this.stateService.getKdfIterations(); + return true; + } + + async submit() { + // Validation + if (!(await this.strongPassword())) { + return; } - async ngOnInit() { - await this.syncService.fullSync(true); - super.ngOnInit(); + if (!(await this.setupSubmitActions())) { + return; } - togglePassword(confirmField: boolean) { - this.showPassword = !this.showPassword; - document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); + try { + // Create new key and hash new password + const newKey = await this.cryptoService.makeKey( + this.masterPassword, + this.email.trim().toLowerCase(), + this.kdf, + this.kdfIterations + ); + const newPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey); + + // Grab user's current enc key + const userEncKey = await this.cryptoService.getEncKey(); + + // Create new encKey for the User + const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey); + + await this.performSubmitActions(newPasswordHash, newKey, newEncKey); + } catch (e) { + this.logService.error(e); } + } - async setupSubmitActions(): Promise { - this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); - this.email = await this.userService.getEmail(); - this.kdf = await this.userService.getKdf(); - this.kdfIterations = await this.userService.getKdfIterations(); - return true; - } - - async submit() { - // Validation - if (!await this.strongPassword()) { - return; - } - - if (!await this.setupSubmitActions()) { - return; - } - - try { - // Create new key and hash new password - const newKey = await this.cryptoService.makeKey(this.masterPassword, this.email.trim().toLowerCase(), - this.kdf, this.kdfIterations); - const newPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey); - - // Grab user's current enc key - const userEncKey = await this.cryptoService.getEncKey(); - - // Create new encKey for the User - const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey); - - await this.performSubmitActions(newPasswordHash, newKey, newEncKey); - } catch (e) { - this.logService.error(e); - } - } - - async performSubmitActions(masterPasswordHash: string, key: SymmetricCryptoKey, - encKey: [SymmetricCryptoKey, EncString]) { - try { - // Create request - const request = new UpdateTempPasswordRequest(); - request.key = encKey[1].encryptedString; - request.newMasterPasswordHash = masterPasswordHash; - request.masterPasswordHint = this.hint; - - // Update user's password - this.formPromise = this.apiService.putUpdateTempPassword(request); - await this.formPromise; - this.platformUtilsService.showToast('success', null, this.i18nService.t('updatedMasterPassword')); - - if (this.onSuccessfulChangePassword != null) { - this.onSuccessfulChangePassword(); - } else { - this.messagingService.send('logout'); - } - } catch (e) { - this.logService.error(e); - } + async performSubmitActions( + masterPasswordHash: string, + key: SymmetricCryptoKey, + encKey: [SymmetricCryptoKey, EncString] + ) { + try { + // Create request + const request = new UpdateTempPasswordRequest(); + request.key = encKey[1].encryptedString; + request.newMasterPasswordHash = masterPasswordHash; + request.masterPasswordHint = this.hint; + + // Update user's password + this.formPromise = this.apiService.putUpdateTempPassword(request); + await this.formPromise; + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("updatedMasterPassword") + ); + + if (this.onSuccessfulChangePassword != null) { + this.onSuccessfulChangePassword(); + } else { + this.messagingService.send("logout"); + } + } catch (e) { + this.logService.error(e); } + } } diff --git a/angular/src/components/view.component.ts b/angular/src/components/view.component.ts index 8e12634f..9e2ea818 100644 --- a/angular/src/components/view.component.ts +++ b/angular/src/components/view.component.ts @@ -1,394 +1,447 @@ import { - ChangeDetectorRef, - Directive, - EventEmitter, - Input, - NgZone, - OnDestroy, - OnInit, - Output, -} from '@angular/core'; + ChangeDetectorRef, + Directive, + EventEmitter, + Input, + NgZone, + OnDestroy, + OnInit, + Output, +} from "@angular/core"; -import { CipherRepromptType } from 'jslib-common/enums/cipherRepromptType'; -import { CipherType } from 'jslib-common/enums/cipherType'; -import { EventType } from 'jslib-common/enums/eventType'; -import { FieldType } from 'jslib-common/enums/fieldType'; +import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType"; +import { CipherType } from "jslib-common/enums/cipherType"; +import { EventType } from "jslib-common/enums/eventType"; +import { FieldType } from "jslib-common/enums/fieldType"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AuditService } from 'jslib-common/abstractions/audit.service'; -import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; -import { CipherService } from 'jslib-common/abstractions/cipher.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { EventService } from 'jslib-common/abstractions/event.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PasswordRepromptService } from 'jslib-common/abstractions/passwordReprompt.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { TokenService } from 'jslib-common/abstractions/token.service'; -import { TotpService } from 'jslib-common/abstractions/totp.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AuditService } from "jslib-common/abstractions/audit.service"; +import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EventService } from "jslib-common/abstractions/event.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; +import { TotpService } from "jslib-common/abstractions/totp.service"; -import { ErrorResponse } from 'jslib-common/models/response/errorResponse'; +import { ErrorResponse } from "jslib-common/models/response/errorResponse"; -import { AttachmentView } from 'jslib-common/models/view/attachmentView'; -import { CipherView } from 'jslib-common/models/view/cipherView'; -import { LoginUriView } from 'jslib-common/models/view/loginUriView'; +import { AttachmentView } from "jslib-common/models/view/attachmentView"; +import { CipherView } from "jslib-common/models/view/cipherView"; +import { LoginUriView } from "jslib-common/models/view/loginUriView"; -const BroadcasterSubscriptionId = 'ViewComponent'; +const BroadcasterSubscriptionId = "ViewComponent"; @Directive() export class ViewComponent implements OnDestroy, OnInit { - @Input() cipherId: string; - @Output() onEditCipher = new EventEmitter(); - @Output() onCloneCipher = new EventEmitter(); - @Output() onShareCipher = new EventEmitter(); - @Output() onDeletedCipher = new EventEmitter(); - @Output() onRestoredCipher = new EventEmitter(); + @Input() cipherId: string; + @Output() onEditCipher = new EventEmitter(); + @Output() onCloneCipher = new EventEmitter(); + @Output() onShareCipher = new EventEmitter(); + @Output() onDeletedCipher = new EventEmitter(); + @Output() onRestoredCipher = new EventEmitter(); - cipher: CipherView; - showPassword: boolean; - showCardNumber: boolean; - showCardCode: boolean; - canAccessPremium: boolean; - totpCode: string; - totpCodeFormatted: string; - totpDash: number; - totpSec: number; - totpLow: boolean; - fieldType = FieldType; - checkPasswordPromise: Promise; + cipher: CipherView; + showPassword: boolean; + showCardNumber: boolean; + showCardCode: boolean; + canAccessPremium: boolean; + totpCode: string; + totpCodeFormatted: string; + totpDash: number; + totpSec: number; + totpLow: boolean; + fieldType = FieldType; + checkPasswordPromise: Promise; - private totpInterval: any; - private previousCipherId: string; - private passwordReprompted: boolean = false; + private totpInterval: any; + private previousCipherId: string; + private passwordReprompted: boolean = false; - constructor(protected cipherService: CipherService, protected totpService: TotpService, - protected tokenService: TokenService, protected i18nService: I18nService, - protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, - protected auditService: AuditService, protected win: Window, - protected broadcasterService: BroadcasterService, protected ngZone: NgZone, - protected changeDetectorRef: ChangeDetectorRef, protected userService: UserService, - protected eventService: EventService, protected apiService: ApiService, - protected passwordRepromptService: PasswordRepromptService, private logService: LogService) { } + constructor( + protected cipherService: CipherService, + protected totpService: TotpService, + protected tokenService: TokenService, + protected i18nService: I18nService, + protected cryptoService: CryptoService, + protected platformUtilsService: PlatformUtilsService, + protected auditService: AuditService, + protected win: Window, + protected broadcasterService: BroadcasterService, + protected ngZone: NgZone, + protected changeDetectorRef: ChangeDetectorRef, + protected eventService: EventService, + protected apiService: ApiService, + protected passwordRepromptService: PasswordRepromptService, + private logService: LogService, + protected stateService: StateService + ) {} - ngOnInit() { - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'syncCompleted': - if (message.successfully) { - await this.load(); - this.changeDetectorRef.detectChanges(); - } - break; - } - }); - }); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - this.cleanUp(); - } - - async load() { - this.cleanUp(); - - const cipher = await this.cipherService.get(this.cipherId); - this.cipher = await cipher.decrypt(); - this.canAccessPremium = await this.userService.canAccessPremium(); - - if (this.cipher.type === CipherType.Login && this.cipher.login.totp && - (cipher.organizationUseTotp || this.canAccessPremium)) { - await this.totpUpdateCode(); - const interval = this.totpService.getTimeInterval(this.cipher.login.totp); - await this.totpTick(interval); - - this.totpInterval = setInterval(async () => { - await this.totpTick(interval); - }, 1000); - } - - if (this.previousCipherId !== this.cipherId) { - this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId); - } - this.previousCipherId = this.cipherId; - } - - async edit() { - if (await this.promptPassword()) { - this.onEditCipher.emit(this.cipher); - return true; - } - - return false; - } - - async clone() { - if (await this.promptPassword()) { - this.onCloneCipher.emit(this.cipher); - return true; - } - - return false; - } - - async share() { - if (await this.promptPassword()) { - this.onShareCipher.emit(this.cipher); - return true; - } - - return false; - } - - async delete(): Promise { - if (!await this.promptPassword()) { - return; - } - - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'), - this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return false; - } - - try { - await this.deleteCipher(); - this.platformUtilsService.showToast('success', null, - this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeletedItem' : 'deletedItem')); - this.onDeletedCipher.emit(this.cipher); - } catch (e) { - this.logService.error(e); - } - - return true; - } - - async restore(): Promise { - if (!this.cipher.isDeleted) { - return false; - } - - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('restoreItemConfirmation'), this.i18nService.t('restoreItem'), - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); - if (!confirmed) { - return false; - } - - try { - await this.restoreCipher(); - this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem')); - this.onRestoredCipher.emit(this.cipher); - } catch (e) { - this.logService.error(e); - } - - return true; - } - - async togglePassword() { - if (!await this.promptPassword()) { - return; - } - - this.showPassword = !this.showPassword; - if (this.showPassword) { - this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId); - } - } - - async toggleCardNumber() { - if (!await this.promptPassword()) { - return; - } - - this.showCardNumber = !this.showCardNumber; - if (this.showCardNumber) { - this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId); - } - } - - async toggleCardCode() { - if (!await this.promptPassword()) { - return; - } - - this.showCardCode = !this.showCardCode; - if (this.showCardCode) { - this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId); - } - } - - async checkPassword() { - if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') { - return; - } - - this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); - const matches = await this.checkPasswordPromise; - - if (matches > 0) { - this.platformUtilsService.showToast('warning', null, - this.i18nService.t('passwordExposed', matches.toString())); - } else { - this.platformUtilsService.showToast('success', null, this.i18nService.t('passwordSafe')); - } - } - - launch(uri: LoginUriView, cipherId?: string) { - if (!uri.canLaunch) { - return; - } - - if (cipherId) { - this.cipherService.updateLastLaunchedDate(cipherId); - } - - this.platformUtilsService.launchUri(uri.launchUri); - } - - async copy(value: string, typeI18nKey: string, aType: string) { - if (value == null) { - return; - } - - if (this.passwordRepromptService.protectedFields().includes(aType) && !await this.promptPassword()) { - return; - } - - const copyOptions = this.win != null ? { window: this.win } : null; - this.platformUtilsService.copyToClipboard(value, copyOptions); - this.platformUtilsService.showToast('info', null, - this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); - - if (typeI18nKey === 'password') { - this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId); - } else if (typeI18nKey === 'securityCode') { - this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId); - } else if (aType === 'H_Field') { - this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId); - } - } - - setTextDataOnDrag(event: DragEvent, data: string) { - event.dataTransfer.setData('text', data); - } - - async downloadAttachment(attachment: AttachmentView) { - if (!await this.promptPassword()) { - return; - } - const a = (attachment as any); - if (a.downloading) { - return; - } - - if (this.cipher.organizationId == null && !this.canAccessPremium) { - this.platformUtilsService.showToast('error', this.i18nService.t('premiumRequired'), - this.i18nService.t('premiumRequiredDesc')); - return; - } - - let url: string; - try { - const attachmentDownloadResponse = await this.apiService.getAttachmentData(this.cipher.id, attachment.id); - url = attachmentDownloadResponse.url; - } catch (e) { - if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { - url = attachment.url; - } else if (e instanceof ErrorResponse) { - throw new Error((e as ErrorResponse).getSingleMessage()); - } else { - throw e; + ngOnInit() { + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncCompleted": + if (message.successfully) { + await this.load(); + this.changeDetectorRef.detectChanges(); } + break; } + }); + }); + } - a.downloading = true; - const response = await fetch(new Request(url, { cache: 'no-store' })); - if (response.status !== 200) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); - a.downloading = false; - return; - } + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.cleanUp(); + } - try { - const buf = await response.arrayBuffer(); - const key = attachment.key != null ? attachment.key : - await this.cryptoService.getOrgKey(this.cipher.organizationId); - const decBuf = await this.cryptoService.decryptFromBytes(buf, key); - this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); - } catch (e) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); - } + async load() { + this.cleanUp(); - a.downloading = false; + const cipher = await this.cipherService.get(this.cipherId); + this.cipher = await cipher.decrypt(); + this.canAccessPremium = await this.stateService.getCanAccessPremium(); + + if ( + this.cipher.type === CipherType.Login && + this.cipher.login.totp && + (cipher.organizationUseTotp || this.canAccessPremium) + ) { + await this.totpUpdateCode(); + const interval = this.totpService.getTimeInterval(this.cipher.login.totp); + await this.totpTick(interval); + + this.totpInterval = setInterval(async () => { + await this.totpTick(interval); + }, 1000); } - protected deleteCipher() { - return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id) - : this.cipherService.softDeleteWithServer(this.cipher.id); + if (this.previousCipherId !== this.cipherId) { + this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId); + } + this.previousCipherId = this.cipherId; + } + + async edit() { + if (await this.promptPassword()) { + this.onEditCipher.emit(this.cipher); + return true; } - protected restoreCipher() { - return this.cipherService.restoreWithServer(this.cipher.id); + return false; + } + + async clone() { + if (await this.promptPassword()) { + this.onCloneCipher.emit(this.cipher); + return true; } - protected async promptPassword() { - if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) { - return true; - } + return false; + } - return this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt(); + async share() { + if (await this.promptPassword()) { + this.onShareCipher.emit(this.cipher); + return true; } - private cleanUp() { - this.totpCode = null; - this.cipher = null; - this.showPassword = false; - this.showCardNumber = false; - this.showCardCode = false; - this.passwordReprompted = false; - if (this.totpInterval) { - clearInterval(this.totpInterval); - } + return false; + } + + async delete(): Promise { + if (!(await this.promptPassword())) { + return; } - private async totpUpdateCode() { - if (this.cipher == null || this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) { - if (this.totpInterval) { - clearInterval(this.totpInterval); - } - return; - } - - this.totpCode = await this.totpService.getCode(this.cipher.login.totp); - if (this.totpCode != null) { - if (this.totpCode.length > 4) { - const half = Math.floor(this.totpCode.length / 2); - this.totpCodeFormatted = this.totpCode.substring(0, half) + ' ' + this.totpCode.substring(half); - } else { - this.totpCodeFormatted = this.totpCode; - } - } else { - this.totpCodeFormatted = null; - if (this.totpInterval) { - clearInterval(this.totpInterval); - } - } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation" + ), + this.i18nService.t("deleteItem"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; } - private async totpTick(intervalSeconds: number) { - const epoch = Math.round(new Date().getTime() / 1000.0); - const mod = epoch % intervalSeconds; - - this.totpSec = intervalSeconds - mod; - this.totpDash = +(Math.round((((78.6 / intervalSeconds) * mod) + 'e+2') as any) + 'e-2'); - this.totpLow = this.totpSec <= 7; - if (mod === 0) { - await this.totpUpdateCode(); - } + try { + await this.deleteCipher(); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem") + ); + this.onDeletedCipher.emit(this.cipher); + } catch (e) { + this.logService.error(e); } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("restoreItemConfirmation"), + this.i18nService.t("restoreItem"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; + } + + try { + await this.restoreCipher(); + this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.onRestoredCipher.emit(this.cipher); + } catch (e) { + this.logService.error(e); + } + + return true; + } + + async togglePassword() { + if (!(await this.promptPassword())) { + return; + } + + this.showPassword = !this.showPassword; + if (this.showPassword) { + this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId); + } + } + + async toggleCardNumber() { + if (!(await this.promptPassword())) { + return; + } + + this.showCardNumber = !this.showCardNumber; + if (this.showCardNumber) { + this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId); + } + } + + async toggleCardCode() { + if (!(await this.promptPassword())) { + return; + } + + this.showCardCode = !this.showCardCode; + if (this.showCardCode) { + this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId); + } + } + + async checkPassword() { + if ( + this.cipher.login == null || + this.cipher.login.password == null || + this.cipher.login.password === "" + ) { + return; + } + + this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); + const matches = await this.checkPasswordPromise; + + if (matches > 0) { + this.platformUtilsService.showToast( + "warning", + null, + this.i18nService.t("passwordExposed", matches.toString()) + ); + } else { + this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe")); + } + } + + launch(uri: LoginUriView, cipherId?: string) { + if (!uri.canLaunch) { + return; + } + + if (cipherId) { + this.cipherService.updateLastLaunchedDate(cipherId); + } + + this.platformUtilsService.launchUri(uri.launchUri); + } + + async copy(value: string, typeI18nKey: string, aType: string) { + if (value == null) { + return; + } + + if ( + this.passwordRepromptService.protectedFields().includes(aType) && + !(await this.promptPassword()) + ) { + return; + } + + const copyOptions = this.win != null ? { window: this.win } : null; + this.platformUtilsService.copyToClipboard(value, copyOptions); + this.platformUtilsService.showToast( + "info", + null, + this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)) + ); + + if (typeI18nKey === "password") { + this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId); + } else if (typeI18nKey === "securityCode") { + this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId); + } else if (aType === "H_Field") { + this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId); + } + } + + setTextDataOnDrag(event: DragEvent, data: string) { + event.dataTransfer.setData("text", data); + } + + async downloadAttachment(attachment: AttachmentView) { + if (!(await this.promptPassword())) { + return; + } + const a = attachment as any; + if (a.downloading) { + return; + } + + if (this.cipher.organizationId == null && !this.canAccessPremium) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("premiumRequired"), + this.i18nService.t("premiumRequiredDesc") + ); + return; + } + + let url: string; + try { + const attachmentDownloadResponse = await this.apiService.getAttachmentData( + this.cipher.id, + attachment.id + ); + url = attachmentDownloadResponse.url; + } catch (e) { + if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { + url = attachment.url; + } else if (e instanceof ErrorResponse) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } else { + throw e; + } + } + + a.downloading = true; + const response = await fetch(new Request(url, { cache: "no-store" })); + if (response.status !== 200) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + a.downloading = false; + return; + } + + try { + const buf = await response.arrayBuffer(); + const key = + attachment.key != null + ? attachment.key + : await this.cryptoService.getOrgKey(this.cipher.organizationId); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); + } catch (e) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + } + + a.downloading = false; + } + + protected deleteCipher() { + return this.cipher.isDeleted + ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } + + protected restoreCipher() { + return this.cipherService.restoreWithServer(this.cipher.id); + } + + protected async promptPassword() { + if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) { + return true; + } + + return (this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt()); + } + + private cleanUp() { + this.totpCode = null; + this.cipher = null; + this.showPassword = false; + this.showCardNumber = false; + this.showCardCode = false; + this.passwordReprompted = false; + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + } + + private async totpUpdateCode() { + if ( + this.cipher == null || + this.cipher.type !== CipherType.Login || + this.cipher.login.totp == null + ) { + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + return; + } + + this.totpCode = await this.totpService.getCode(this.cipher.login.totp); + if (this.totpCode != null) { + if (this.totpCode.length > 4) { + const half = Math.floor(this.totpCode.length / 2); + this.totpCodeFormatted = + this.totpCode.substring(0, half) + " " + this.totpCode.substring(half); + } else { + this.totpCodeFormatted = this.totpCode; + } + } else { + this.totpCodeFormatted = null; + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + } + } + + private async totpTick(intervalSeconds: number) { + const epoch = Math.round(new Date().getTime() / 1000.0); + const mod = epoch % intervalSeconds; + + this.totpSec = intervalSeconds - mod; + this.totpDash = +(Math.round(((78.6 / intervalSeconds) * mod + "e+2") as any) + "e-2"); + this.totpLow = this.totpSec <= 7; + if (mod === 0) { + await this.totpUpdateCode(); + } + } } diff --git a/angular/src/services/auth-guard.service.ts b/angular/src/services/auth-guard.service.ts index 56561577..8c06e438 100644 --- a/angular/src/services/auth-guard.service.ts +++ b/angular/src/services/auth-guard.service.ts @@ -1,42 +1,45 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - CanActivate, - Router, - RouterStateSnapshot, -} from '@angular/router'; +import { Injectable } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; @Injectable() export class AuthGuardService implements CanActivate { - constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService, - private router: Router, private messagingService: MessagingService, private keyConnectorService: KeyConnectorService) { } + constructor( + private vaultTimeoutService: VaultTimeoutService, + private router: Router, + private messagingService: MessagingService, + private keyConnectorService: KeyConnectorService, + private stateService: StateService + ) {} - async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { - const isAuthed = await this.userService.isAuthenticated(); - if (!isAuthed) { - this.messagingService.send('authBlocked'); - return false; - } - - const locked = await this.vaultTimeoutService.isLocked(); - if (locked) { - if (routerState != null) { - this.messagingService.send('lockedUrl', { url: routerState.url }); - } - this.router.navigate(['lock'], { queryParams: { promptBiometric: true }}); - return false; - } - - if (!routerState.url.includes('remove-password') && await this.keyConnectorService.getConvertAccountRequired()) { - this.router.navigate(['/remove-password']); - return false; - } - - return true; + async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { + const isAuthed = await this.stateService.getIsAuthenticated(); + if (!isAuthed) { + this.messagingService.send("authBlocked"); + return false; } + + const locked = await this.vaultTimeoutService.isLocked(); + if (locked) { + if (routerState != null) { + this.messagingService.send("lockedUrl", { url: routerState.url }); + } + this.router.navigate(["lock"], { queryParams: { promptBiometric: true } }); + return false; + } + + if ( + !routerState.url.includes("remove-password") && + (await this.keyConnectorService.getConvertAccountRequired()) + ) { + this.router.navigate(["/remove-password"]); + return false; + } + + return true; + } } diff --git a/angular/src/services/jslib-services.module.ts b/angular/src/services/jslib-services.module.ts index d398755f..8523818a 100644 --- a/angular/src/services/jslib-services.module.ts +++ b/angular/src/services/jslib-services.module.ts @@ -1,395 +1,452 @@ -import { - Injector, - LOCALE_ID, - NgModule, -} from '@angular/core'; -import { ToasterModule } from 'angular2-toaster'; +import { Injector, LOCALE_ID, NgModule } from "@angular/core"; -import { ApiService } from 'jslib-common/services/api.service'; -import { AppIdService } from 'jslib-common/services/appId.service'; -import { AuditService } from 'jslib-common/services/audit.service'; -import { AuthService } from 'jslib-common/services/auth.service'; -import { CipherService } from 'jslib-common/services/cipher.service'; -import { CollectionService } from 'jslib-common/services/collection.service'; -import { ConsoleLogService } from 'jslib-common/services/consoleLog.service'; -import { CryptoService } from 'jslib-common/services/crypto.service'; -import { EnvironmentService } from 'jslib-common/services/environment.service'; -import { EventService } from 'jslib-common/services/event.service'; -import { ExportService } from 'jslib-common/services/export.service'; -import { FileUploadService } from 'jslib-common/services/fileUpload.service'; -import { FolderService } from 'jslib-common/services/folder.service'; -import { KeyConnectorService } from 'jslib-common/services/keyConnector.service'; -import { NotificationsService } from 'jslib-common/services/notifications.service'; -import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service'; -import { PolicyService } from 'jslib-common/services/policy.service'; -import { SearchService } from 'jslib-common/services/search.service'; -import { SendService } from 'jslib-common/services/send.service'; -import { SettingsService } from 'jslib-common/services/settings.service'; -import { StateService } from 'jslib-common/services/state.service'; -import { SyncService } from 'jslib-common/services/sync.service'; -import { TokenService } from 'jslib-common/services/token.service'; -import { TotpService } from 'jslib-common/services/totp.service'; -import { UserService } from 'jslib-common/services/user.service'; -import { UserVerificationService } from 'jslib-common/services/userVerification.service'; -import { VaultTimeoutService } from 'jslib-common/services/vaultTimeout.service'; -import { WebCryptoFunctionService } from 'jslib-common/services/webCryptoFunction.service'; +import { ApiService } from "jslib-common/services/api.service"; +import { AppIdService } from "jslib-common/services/appId.service"; +import { AuditService } from "jslib-common/services/audit.service"; +import { AuthService } from "jslib-common/services/auth.service"; +import { CipherService } from "jslib-common/services/cipher.service"; +import { CollectionService } from "jslib-common/services/collection.service"; +import { ConsoleLogService } from "jslib-common/services/consoleLog.service"; +import { CryptoService } from "jslib-common/services/crypto.service"; +import { EnvironmentService } from "jslib-common/services/environment.service"; +import { EventService } from "jslib-common/services/event.service"; +import { ExportService } from "jslib-common/services/export.service"; +import { FileUploadService } from "jslib-common/services/fileUpload.service"; +import { FolderService } from "jslib-common/services/folder.service"; +import { KeyConnectorService } from "jslib-common/services/keyConnector.service"; +import { NotificationsService } from "jslib-common/services/notifications.service"; +import { OrganizationService } from "jslib-common/services/organization.service"; +import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service"; +import { PolicyService } from "jslib-common/services/policy.service"; +import { ProviderService } from "jslib-common/services/provider.service"; +import { SearchService } from "jslib-common/services/search.service"; +import { SendService } from "jslib-common/services/send.service"; +import { SettingsService } from "jslib-common/services/settings.service"; +import { StateService } from "jslib-common/services/state.service"; +import { StateMigrationService } from "jslib-common/services/stateMigration.service"; +import { SyncService } from "jslib-common/services/sync.service"; +import { TokenService } from "jslib-common/services/token.service"; +import { TotpService } from "jslib-common/services/totp.service"; +import { UserVerificationService } from "jslib-common/services/userVerification.service"; +import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service"; +import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service"; -import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.service'; -import { AppIdService as AppIdServiceAbstraction } from 'jslib-common/abstractions/appId.service'; -import { AuditService as AuditServiceAbstraction } from 'jslib-common/abstractions/audit.service'; -import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service'; -import { BroadcasterService as BroadcasterServiceAbstraction } from 'jslib-common/abstractions/broadcaster.service'; -import { CipherService as CipherServiceAbstraction } from 'jslib-common/abstractions/cipher.service'; -import { CollectionService as CollectionServiceAbstraction } from 'jslib-common/abstractions/collection.service'; -import { CryptoService as CryptoServiceAbstraction } from 'jslib-common/abstractions/crypto.service'; -import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService as EnvironmentServiceAbstraction, Urls } from 'jslib-common/abstractions/environment.service'; -import { EventService as EventServiceAbstraction } from 'jslib-common/abstractions/event.service'; -import { ExportService as ExportServiceAbstraction } from 'jslib-common/abstractions/export.service'; -import { FileUploadService as FileUploadServiceAbstraction } from 'jslib-common/abstractions/fileUpload.service'; -import { FolderService as FolderServiceAbstraction } from 'jslib-common/abstractions/folder.service'; -import { I18nService as I18nServiceAbstraction } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService as KeyConnectorServiceAbstraction } from 'jslib-common/abstractions/keyConnector.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { MessagingService as MessagingServiceAbstraction } from 'jslib-common/abstractions/messaging.service'; -import { NotificationsService as NotificationsServiceAbstraction } from 'jslib-common/abstractions/notifications.service'; -import { - PasswordGenerationService as PasswordGenerationServiceAbstraction, -} from 'jslib-common/abstractions/passwordGeneration.service'; -import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from 'jslib-common/abstractions/passwordReprompt.service'; -import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService as PolicyServiceAbstraction } from 'jslib-common/abstractions/policy.service'; -import { SearchService as SearchServiceAbstraction } from 'jslib-common/abstractions/search.service'; -import { SendService as SendServiceAbstraction } from 'jslib-common/abstractions/send.service'; -import { SettingsService as SettingsServiceAbstraction } from 'jslib-common/abstractions/settings.service'; -import { StateService as StateServiceAbstraction } from 'jslib-common/abstractions/state.service'; -import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service'; -import { SyncService as SyncServiceAbstraction } from 'jslib-common/abstractions/sync.service'; -import { TokenService as TokenServiceAbstraction } from 'jslib-common/abstractions/token.service'; -import { TotpService as TotpServiceAbstraction } from 'jslib-common/abstractions/totp.service'; -import { UserService as UserServiceAbstraction } from 'jslib-common/abstractions/user.service'; -import { UserVerificationService as UserVerificationServiceAbstraction } from 'jslib-common/abstractions/userVerification.service'; -import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from 'jslib-common/abstractions/vaultTimeout.service'; +import { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service"; +import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service"; +import { AuditService as AuditServiceAbstraction } from "jslib-common/abstractions/audit.service"; +import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service"; +import { BroadcasterService as BroadcasterServiceAbstraction } from "jslib-common/abstractions/broadcaster.service"; +import { CipherService as CipherServiceAbstraction } from "jslib-common/abstractions/cipher.service"; +import { CollectionService as CollectionServiceAbstraction } from "jslib-common/abstractions/collection.service"; +import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service"; +import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService as EnvironmentServiceAbstraction } from "jslib-common/abstractions/environment.service"; +import { EventService as EventServiceAbstraction } from "jslib-common/abstractions/event.service"; +import { ExportService as ExportServiceAbstraction } from "jslib-common/abstractions/export.service"; +import { FileUploadService as FileUploadServiceAbstraction } from "jslib-common/abstractions/fileUpload.service"; +import { FolderService as FolderServiceAbstraction } from "jslib-common/abstractions/folder.service"; +import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService as KeyConnectorServiceAbstraction } from "jslib-common/abstractions/keyConnector.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService as MessagingServiceAbstraction } from "jslib-common/abstractions/messaging.service"; +import { NotificationsService as NotificationsServiceAbstraction } from "jslib-common/abstractions/notifications.service"; +import { OrganizationService as OrganizationServiceAbstraction } from "jslib-common/abstractions/organization.service"; +import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "jslib-common/abstractions/passwordGeneration.service"; +import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "jslib-common/abstractions/passwordReprompt.service"; +import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService as PolicyServiceAbstraction } from "jslib-common/abstractions/policy.service"; +import { ProviderService as ProviderServiceAbstraction } from "jslib-common/abstractions/provider.service"; +import { SearchService as SearchServiceAbstraction } from "jslib-common/abstractions/search.service"; +import { SendService as SendServiceAbstraction } from "jslib-common/abstractions/send.service"; +import { SettingsService as SettingsServiceAbstraction } from "jslib-common/abstractions/settings.service"; +import { StateService as StateServiceAbstraction } from "jslib-common/abstractions/state.service"; +import { StateMigrationService as StateMigrationServiceAbstraction } from "jslib-common/abstractions/stateMigration.service"; +import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service"; +import { SyncService as SyncServiceAbstraction } from "jslib-common/abstractions/sync.service"; +import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service"; +import { TotpService as TotpServiceAbstraction } from "jslib-common/abstractions/totp.service"; +import { UserVerificationService as UserVerificationServiceAbstraction } from "jslib-common/abstractions/userVerification.service"; +import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service"; -import { AuthGuardService } from './auth-guard.service'; -import { BroadcasterService } from './broadcaster.service'; -import { LockGuardService } from './lock-guard.service'; -import { ModalService } from './modal.service'; -import { PasswordRepromptService } from './passwordReprompt.service'; -import { UnauthGuardService } from './unauth-guard.service'; -import { ValidationService } from './validation.service'; +import { AuthGuardService } from "./auth-guard.service"; +import { BroadcasterService } from "./broadcaster.service"; +import { LockGuardService } from "./lock-guard.service"; +import { ModalService } from "./modal.service"; +import { PasswordRepromptService } from "./passwordReprompt.service"; +import { UnauthGuardService } from "./unauth-guard.service"; +import { ValidationService } from "./validation.service"; @NgModule({ - imports: [ - ToasterModule, - ], - declarations: [], - providers: [ - { provide: 'WINDOW', useValue: window }, - { - provide: LOCALE_ID, - useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale, - deps: [I18nServiceAbstraction], - }, - ValidationService, - AuthGuardService, - UnauthGuardService, - LockGuardService, - ModalService, - { - provide: AppIdServiceAbstraction, - useClass: AppIdService, - deps: [StorageServiceAbstraction], - }, - { - provide: AuditServiceAbstraction, - useClass: AuditService, - deps: [CryptoFunctionServiceAbstraction, ApiServiceAbstraction], - }, - { - provide: AuthServiceAbstraction, - useClass: AuthService, - deps: [ - CryptoServiceAbstraction, - ApiServiceAbstraction, - UserServiceAbstraction, - TokenServiceAbstraction, - AppIdServiceAbstraction, - I18nServiceAbstraction, - PlatformUtilsServiceAbstraction, - MessagingServiceAbstraction, - VaultTimeoutServiceAbstraction, - LogService, - CryptoFunctionServiceAbstraction, - EnvironmentServiceAbstraction, - KeyConnectorServiceAbstraction, - ], - }, - { - provide: CipherServiceAbstraction, - useFactory: (cryptoService: CryptoServiceAbstraction, userService: UserServiceAbstraction, - settingsService: SettingsServiceAbstraction, apiService: ApiServiceAbstraction, - fileUploadService: FileUploadServiceAbstraction, storageService: StorageServiceAbstraction, - i18nService: I18nServiceAbstraction, injector: Injector, logService: LogService) => - new CipherService(cryptoService, userService, settingsService, apiService, fileUploadService, - storageService, i18nService, () => injector.get(SearchServiceAbstraction), logService), - deps: [ - CryptoServiceAbstraction, - UserServiceAbstraction, - SettingsServiceAbstraction, - ApiServiceAbstraction, - FileUploadServiceAbstraction, - StorageServiceAbstraction, - I18nServiceAbstraction, - Injector, // TODO: Get rid of this circular dependency! - LogService, - ], - }, - { - provide: FolderServiceAbstraction, - useClass: FolderService, - deps: [ - CryptoServiceAbstraction, - UserServiceAbstraction, - ApiServiceAbstraction, - StorageServiceAbstraction, - I18nServiceAbstraction, - CipherServiceAbstraction, - ], - }, - { provide: LogService, useFactory: () => new ConsoleLogService(false) }, - { - provide: CollectionServiceAbstraction, - useClass: CollectionService, - deps: [ - CryptoServiceAbstraction, - UserServiceAbstraction, - StorageServiceAbstraction, - I18nServiceAbstraction, - ], - }, - { - provide: EnvironmentServiceAbstraction, - useClass: EnvironmentService, - deps: [StorageServiceAbstraction], - }, - { - provide: TotpServiceAbstraction, - useClass: TotpService, - deps: [ - StorageServiceAbstraction, - CryptoFunctionServiceAbstraction, - LogService, - ], - }, - { provide: TokenServiceAbstraction, useClass: TokenService, deps: [StorageServiceAbstraction] }, - { - provide: CryptoServiceAbstraction, - useClass: CryptoService, - deps: [ - StorageServiceAbstraction, - 'SECURE_STORAGE', - CryptoFunctionServiceAbstraction, - PlatformUtilsServiceAbstraction, - LogService, - ], - }, - { - provide: PasswordGenerationServiceAbstraction, - useClass: PasswordGenerationService, - deps: [ - CryptoServiceAbstraction, - StorageServiceAbstraction, - PolicyServiceAbstraction, - ], - }, - { - provide: ApiServiceAbstraction, - useFactory: (tokenService: TokenServiceAbstraction, platformUtilsService: PlatformUtilsServiceAbstraction, - environmentService: EnvironmentServiceAbstraction, messagingService: MessagingServiceAbstraction) => - new ApiService(tokenService, platformUtilsService, environmentService, - async (expired: boolean) => messagingService.send('logout', { expired: expired })), - deps: [ - TokenServiceAbstraction, - PlatformUtilsServiceAbstraction, - EnvironmentServiceAbstraction, - MessagingServiceAbstraction, - ], - }, - { - provide: FileUploadServiceAbstraction, - useClass: FileUploadService, - deps: [ - LogService, - ApiServiceAbstraction, - ], - }, - { - provide: SyncServiceAbstraction, - useFactory: (userService: UserServiceAbstraction, apiService: ApiServiceAbstraction, - settingsService: SettingsServiceAbstraction, folderService: FolderServiceAbstraction, - cipherService: CipherServiceAbstraction, cryptoService: CryptoServiceAbstraction, - collectionService: CollectionServiceAbstraction, storageService: StorageServiceAbstraction, - messagingService: MessagingServiceAbstraction, policyService: PolicyServiceAbstraction, - sendService: SendServiceAbstraction, logService: LogService, tokenService: TokenService, - keyConnectorService: KeyConnectorServiceAbstraction) => new SyncService(userService, apiService, - settingsService, folderService, cipherService, cryptoService, collectionService, storageService, - messagingService, policyService, sendService, logService, tokenService, keyConnectorService, - async (expired: boolean) => messagingService.send('logout', { expired: expired })), - deps: [ - UserServiceAbstraction, - ApiServiceAbstraction, - SettingsServiceAbstraction, - FolderServiceAbstraction, - CipherServiceAbstraction, - CryptoServiceAbstraction, - CollectionServiceAbstraction, - StorageServiceAbstraction, - MessagingServiceAbstraction, - PolicyServiceAbstraction, - SendServiceAbstraction, - LogService, - TokenServiceAbstraction, - KeyConnectorServiceAbstraction, - ], - }, - { - provide: UserServiceAbstraction, - useClass: UserService, - deps: [TokenServiceAbstraction, StorageServiceAbstraction], - }, - { provide: BroadcasterServiceAbstraction, useClass: BroadcasterService }, - { - provide: SettingsServiceAbstraction, - useClass: SettingsService, - deps: [UserServiceAbstraction, StorageServiceAbstraction], - }, - { - provide: VaultTimeoutServiceAbstraction, - useFactory: (cipherService: CipherServiceAbstraction, folderService: FolderServiceAbstraction, - collectionService: CollectionServiceAbstraction, cryptoService: CryptoServiceAbstraction, - platformUtilsService: PlatformUtilsServiceAbstraction, storageService: StorageServiceAbstraction, - messagingService: MessagingServiceAbstraction, searchService: SearchServiceAbstraction, - userService: UserServiceAbstraction, tokenService: TokenServiceAbstraction, - policyService: PolicyServiceAbstraction, keyConnectorService: KeyConnectorServiceAbstraction) => - new VaultTimeoutService(cipherService, folderService, collectionService, cryptoService, - platformUtilsService, storageService, messagingService, searchService, userService, tokenService, - policyService, keyConnectorService, null, - async () => messagingService.send('logout', { expired: false })), - deps: [ - CipherServiceAbstraction, - FolderServiceAbstraction, - CollectionServiceAbstraction, - CryptoServiceAbstraction, - PlatformUtilsServiceAbstraction, - StorageServiceAbstraction, - MessagingServiceAbstraction, - SearchServiceAbstraction, - UserServiceAbstraction, - TokenServiceAbstraction, - PolicyServiceAbstraction, - ], - }, - { provide: StateServiceAbstraction, useClass: StateService }, - { - provide: ExportServiceAbstraction, - useClass: ExportService, - deps: [ - FolderServiceAbstraction, - CipherServiceAbstraction, - ApiServiceAbstraction, - CryptoServiceAbstraction, - ], - }, - { - provide: SearchServiceAbstraction, - useClass: SearchService, - deps: [ - CipherServiceAbstraction, - LogService, - I18nServiceAbstraction, - ], - }, - { - provide: NotificationsServiceAbstraction, - useFactory: (userService: UserServiceAbstraction, syncService: SyncServiceAbstraction, - appIdService: AppIdServiceAbstraction, apiService: ApiServiceAbstraction, - vaultTimeoutService: VaultTimeoutServiceAbstraction, environmentService: EnvironmentServiceAbstraction, - messagingService: MessagingServiceAbstraction, logService: LogService) => - new NotificationsService(userService, syncService, appIdService, apiService, vaultTimeoutService, - environmentService, async () => messagingService.send('logout', { expired: true }), logService), - deps: [ - UserServiceAbstraction, - SyncServiceAbstraction, - AppIdServiceAbstraction, - ApiServiceAbstraction, - VaultTimeoutServiceAbstraction, - EnvironmentServiceAbstraction, - MessagingServiceAbstraction, - LogService, - ], - }, - { - provide: CryptoFunctionServiceAbstraction, - useClass: WebCryptoFunctionService, - deps: ['WINDOW', PlatformUtilsServiceAbstraction], - }, - { - provide: EventServiceAbstraction, - useClass: EventService, - deps: [ - StorageServiceAbstraction, - ApiServiceAbstraction, - UserServiceAbstraction, - CipherServiceAbstraction, - LogService, - ], - }, - { - provide: PolicyServiceAbstraction, - useClass: PolicyService, - deps: [ - UserServiceAbstraction, - StorageServiceAbstraction, - ApiServiceAbstraction, - ], - }, - { - provide: SendServiceAbstraction, - useClass: SendService, - deps: [ - CryptoServiceAbstraction, - UserServiceAbstraction, - ApiServiceAbstraction, - FileUploadServiceAbstraction, - StorageServiceAbstraction, - I18nServiceAbstraction, - CryptoFunctionServiceAbstraction, - ], - }, - { - provide: KeyConnectorServiceAbstraction, - useClass: KeyConnectorService, - deps: [ - StorageServiceAbstraction, - UserServiceAbstraction, - CryptoServiceAbstraction, - ApiServiceAbstraction, - TokenServiceAbstraction, - LogService, - ], - }, - { - provide: UserVerificationServiceAbstraction, - useClass: UserVerificationService, - deps: [ - CryptoServiceAbstraction, - I18nServiceAbstraction, - ApiServiceAbstraction, - ], - }, - { provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, - ], + declarations: [], + providers: [ + { provide: "WINDOW", useValue: window }, + { + provide: LOCALE_ID, + useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale, + deps: [I18nServiceAbstraction], + }, + ValidationService, + AuthGuardService, + UnauthGuardService, + LockGuardService, + ModalService, + { + provide: AppIdServiceAbstraction, + useClass: AppIdService, + deps: [StorageServiceAbstraction], + }, + { + provide: AuditServiceAbstraction, + useClass: AuditService, + deps: [CryptoFunctionServiceAbstraction, ApiServiceAbstraction], + }, + { + provide: AuthServiceAbstraction, + useClass: AuthService, + deps: [ + CryptoServiceAbstraction, + ApiServiceAbstraction, + TokenServiceAbstraction, + AppIdServiceAbstraction, + I18nServiceAbstraction, + PlatformUtilsServiceAbstraction, + MessagingServiceAbstraction, + VaultTimeoutServiceAbstraction, + LogService, + CryptoFunctionServiceAbstraction, + KeyConnectorServiceAbstraction, + EnvironmentServiceAbstraction, + StateServiceAbstraction, + ], + }, + { + provide: CipherServiceAbstraction, + useFactory: ( + cryptoService: CryptoServiceAbstraction, + settingsService: SettingsServiceAbstraction, + apiService: ApiServiceAbstraction, + fileUploadService: FileUploadServiceAbstraction, + i18nService: I18nServiceAbstraction, + injector: Injector, + logService: LogService, + stateService: StateServiceAbstraction + ) => + new CipherService( + cryptoService, + settingsService, + apiService, + fileUploadService, + i18nService, + () => injector.get(SearchServiceAbstraction), + logService, + stateService + ), + deps: [ + CryptoServiceAbstraction, + SettingsServiceAbstraction, + ApiServiceAbstraction, + FileUploadServiceAbstraction, + I18nServiceAbstraction, + Injector, // TODO: Get rid of this circular dependency! + LogService, + StateServiceAbstraction, + ], + }, + { + provide: FolderServiceAbstraction, + useClass: FolderService, + deps: [ + CryptoServiceAbstraction, + ApiServiceAbstraction, + I18nServiceAbstraction, + CipherServiceAbstraction, + StateServiceAbstraction, + ], + }, + { provide: LogService, useFactory: () => new ConsoleLogService(false) }, + { + provide: CollectionServiceAbstraction, + useClass: CollectionService, + deps: [CryptoServiceAbstraction, I18nServiceAbstraction, StateServiceAbstraction], + }, + { + provide: EnvironmentServiceAbstraction, + useClass: EnvironmentService, + deps: [StateServiceAbstraction], + }, + { + provide: TotpServiceAbstraction, + useClass: TotpService, + deps: [CryptoFunctionServiceAbstraction, LogService, StateServiceAbstraction], + }, + { provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] }, + { + provide: CryptoServiceAbstraction, + useClass: CryptoService, + deps: [ + CryptoFunctionServiceAbstraction, + PlatformUtilsServiceAbstraction, + LogService, + StateServiceAbstraction, + ], + }, + { + provide: PasswordGenerationServiceAbstraction, + useClass: PasswordGenerationService, + deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction], + }, + { + provide: ApiServiceAbstraction, + useFactory: ( + tokenService: TokenServiceAbstraction, + platformUtilsService: PlatformUtilsServiceAbstraction, + environmentService: EnvironmentServiceAbstraction, + messagingService: MessagingServiceAbstraction + ) => + new ApiService( + tokenService, + platformUtilsService, + environmentService, + async (expired: boolean) => messagingService.send("logout", { expired: expired }) + ), + deps: [ + TokenServiceAbstraction, + PlatformUtilsServiceAbstraction, + EnvironmentServiceAbstraction, + MessagingServiceAbstraction, + ], + }, + { + provide: FileUploadServiceAbstraction, + useClass: FileUploadService, + deps: [LogService, ApiServiceAbstraction], + }, + { + provide: SyncServiceAbstraction, + useFactory: ( + apiService: ApiServiceAbstraction, + settingsService: SettingsServiceAbstraction, + folderService: FolderServiceAbstraction, + cipherService: CipherServiceAbstraction, + cryptoService: CryptoServiceAbstraction, + collectionService: CollectionServiceAbstraction, + messagingService: MessagingServiceAbstraction, + policyService: PolicyServiceAbstraction, + sendService: SendServiceAbstraction, + logService: LogService, + keyConnectorService: KeyConnectorServiceAbstraction, + stateService: StateServiceAbstraction, + organizationService: OrganizationServiceAbstraction, + providerService: ProviderServiceAbstraction + ) => + new SyncService( + apiService, + settingsService, + folderService, + cipherService, + cryptoService, + collectionService, + messagingService, + policyService, + sendService, + logService, + keyConnectorService, + stateService, + organizationService, + providerService, + async (expired: boolean) => messagingService.send("logout", { expired: expired }) + ), + deps: [ + ApiServiceAbstraction, + SettingsServiceAbstraction, + FolderServiceAbstraction, + CipherServiceAbstraction, + CryptoServiceAbstraction, + CollectionServiceAbstraction, + MessagingServiceAbstraction, + PolicyServiceAbstraction, + SendServiceAbstraction, + LogService, + KeyConnectorServiceAbstraction, + StateServiceAbstraction, + OrganizationServiceAbstraction, + ProviderServiceAbstraction, + ], + }, + { provide: BroadcasterServiceAbstraction, useClass: BroadcasterService }, + { + provide: SettingsServiceAbstraction, + useClass: SettingsService, + deps: [StateServiceAbstraction], + }, + { + provide: VaultTimeoutServiceAbstraction, + useFactory: ( + cipherService: CipherServiceAbstraction, + folderService: FolderServiceAbstraction, + collectionService: CollectionServiceAbstraction, + cryptoService: CryptoServiceAbstraction, + platformUtilsService: PlatformUtilsServiceAbstraction, + messagingService: MessagingServiceAbstraction, + searchService: SearchServiceAbstraction, + tokenService: TokenServiceAbstraction, + policyService: PolicyServiceAbstraction, + keyConnectorService: KeyConnectorServiceAbstraction, + stateService: StateServiceAbstraction + ) => + new VaultTimeoutService( + cipherService, + folderService, + collectionService, + cryptoService, + platformUtilsService, + messagingService, + searchService, + tokenService, + policyService, + keyConnectorService, + stateService, + null, + async () => messagingService.send("logout", { expired: false }) + ), + deps: [ + CipherServiceAbstraction, + FolderServiceAbstraction, + CollectionServiceAbstraction, + CryptoServiceAbstraction, + PlatformUtilsServiceAbstraction, + MessagingServiceAbstraction, + SearchServiceAbstraction, + TokenServiceAbstraction, + PolicyServiceAbstraction, + KeyConnectorServiceAbstraction, + StateServiceAbstraction, + ], + }, + { + provide: StateServiceAbstraction, + useClass: StateService, + deps: [ + StorageServiceAbstraction, + "SECURE_STORAGE", + LogService, + StateMigrationServiceAbstraction, + ], + }, + { + provide: StateMigrationServiceAbstraction, + useClass: StateMigrationService, + deps: [StorageServiceAbstraction, "SECURE_STORAGE"], + }, + { + provide: ExportServiceAbstraction, + useClass: ExportService, + deps: [ + FolderServiceAbstraction, + CipherServiceAbstraction, + ApiServiceAbstraction, + CryptoServiceAbstraction, + ], + }, + { + provide: SearchServiceAbstraction, + useClass: SearchService, + deps: [CipherServiceAbstraction, LogService, I18nServiceAbstraction], + }, + { + provide: NotificationsServiceAbstraction, + useFactory: ( + syncService: SyncServiceAbstraction, + appIdService: AppIdServiceAbstraction, + apiService: ApiServiceAbstraction, + vaultTimeoutService: VaultTimeoutServiceAbstraction, + environmentService: EnvironmentServiceAbstraction, + messagingService: MessagingServiceAbstraction, + logService: LogService, + stateService: StateServiceAbstraction + ) => + new NotificationsService( + syncService, + appIdService, + apiService, + vaultTimeoutService, + environmentService, + async () => messagingService.send("logout", { expired: true }), + logService, + stateService + ), + deps: [ + SyncServiceAbstraction, + AppIdServiceAbstraction, + ApiServiceAbstraction, + VaultTimeoutServiceAbstraction, + EnvironmentServiceAbstraction, + MessagingServiceAbstraction, + LogService, + StateServiceAbstraction, + ], + }, + { + provide: CryptoFunctionServiceAbstraction, + useClass: WebCryptoFunctionService, + deps: ["WINDOW", PlatformUtilsServiceAbstraction], + }, + { + provide: EventServiceAbstraction, + useClass: EventService, + deps: [ + ApiServiceAbstraction, + CipherServiceAbstraction, + StateServiceAbstraction, + LogService, + OrganizationServiceAbstraction, + ], + }, + { + provide: PolicyServiceAbstraction, + useClass: PolicyService, + deps: [StateServiceAbstraction, OrganizationServiceAbstraction, ApiServiceAbstraction], + }, + { + provide: SendServiceAbstraction, + useClass: SendService, + deps: [ + CryptoServiceAbstraction, + ApiServiceAbstraction, + FileUploadServiceAbstraction, + I18nServiceAbstraction, + CryptoFunctionServiceAbstraction, + StateServiceAbstraction, + ], + }, + { + provide: KeyConnectorServiceAbstraction, + useClass: KeyConnectorService, + deps: [ + StateServiceAbstraction, + CryptoServiceAbstraction, + ApiServiceAbstraction, + TokenServiceAbstraction, + LogService, + OrganizationServiceAbstraction, + ], + }, + { + provide: UserVerificationServiceAbstraction, + useClass: UserVerificationService, + deps: [CryptoServiceAbstraction, I18nServiceAbstraction, ApiServiceAbstraction], + }, + { provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, + { + provide: OrganizationServiceAbstraction, + useClass: OrganizationService, + deps: [StateServiceAbstraction], + }, + { + provide: ProviderServiceAbstraction, + useClass: ProviderService, + deps: [StateServiceAbstraction], + }, + ], }) -export class JslibServicesModule { -} +export class JslibServicesModule {} diff --git a/angular/src/services/lock-guard.service.ts b/angular/src/services/lock-guard.service.ts index 400eedc5..b0598c34 100644 --- a/angular/src/services/lock-guard.service.ts +++ b/angular/src/services/lock-guard.service.ts @@ -1,32 +1,29 @@ -import { Injectable } from '@angular/core'; -import { - CanActivate, - Router, -} from '@angular/router'; +import { Injectable } from "@angular/core"; +import { CanActivate, Router } from "@angular/router"; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { StateService } from "jslib-common/abstractions/state.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; @Injectable() export class LockGuardService implements CanActivate { + protected homepage = "vault"; + constructor( + private vaultTimeoutService: VaultTimeoutService, + private router: Router, + private stateService: StateService + ) {} - protected homepage = 'vault'; - constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService, - private router: Router) { } - - async canActivate() { - const isAuthed = await this.userService.isAuthenticated(); - if (isAuthed) { - const locked = await this.vaultTimeoutService.isLocked(); - if (locked) { - return true; - } else { - this.router.navigate([this.homepage]); - return false; - } - } - - this.router.navigate(['']); - return false; + async canActivate() { + if (!(await this.stateService.getIsAuthenticated())) { + this.router.navigate(["login"]); + return false; } + + if (!(await this.vaultTimeoutService.isLocked())) { + this.router.navigate([this.homepage]); + return false; + } + + return true; + } } diff --git a/angular/src/services/unauth-guard.service.ts b/angular/src/services/unauth-guard.service.ts index 786241b8..ee2da045 100644 --- a/angular/src/services/unauth-guard.service.ts +++ b/angular/src/services/unauth-guard.service.ts @@ -1,32 +1,29 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - CanActivate, - Router, -} from '@angular/router'; +import { Injectable } from "@angular/core"; +import { CanActivate, Router } from "@angular/router"; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { StateService } from "jslib-common/abstractions/state.service"; +import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; @Injectable() export class UnauthGuardService implements CanActivate { + protected homepage = "vault"; + constructor( + private vaultTimeoutService: VaultTimeoutService, + private router: Router, + private stateService: StateService + ) {} - protected homepage = 'vault'; - constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService, - private router: Router) { } - - async canActivate() { - const isAuthed = await this.userService.isAuthenticated(); - if (isAuthed) { - const locked = await this.vaultTimeoutService.isLocked(); - if (locked) { - this.router.navigate(['lock']); - } else { - this.router.navigate([this.homepage]); - } - return false; - } - - return true; + async canActivate() { + const isAuthed = await this.stateService.getIsAuthenticated(); + if (isAuthed) { + const locked = await this.vaultTimeoutService.isLocked(); + if (locked) { + this.router.navigate(["lock"]); + } else { + this.router.navigate([this.homepage]); + } + return false; } + return true; + } } diff --git a/common/package-lock.json b/common/package-lock.json index 455d90b7..399c3a47 100644 --- a/common/package-lock.json +++ b/common/package-lock.json @@ -16,18 +16,18 @@ "lunr": "^2.3.9", "node-forge": "^0.10.0", "papaparse": "^5.3.0", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldts": "5.7.52", "zxcvbn": "^4.4.2" }, "devDependencies": { "@types/lunr": "^2.3.3", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-forge": "^0.9.7", "@types/papaparse": "^5.2.5", "@types/zxcvbn": "^4.4.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "node_modules/@microsoft/signalr": { @@ -58,9 +58,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "14.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", - "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==", + "version": "16.11.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", + "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", "dev": true }, "node_modules/@types/node-forge": { @@ -73,9 +73,9 @@ } }, "node_modules/@types/papaparse": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.2.6.tgz", - "integrity": "sha512-xGKSd0UTn58N1h0+zf8mW863Rv8BvXcGibEgKFtBIXZlcDXAmX/T4RdDO2mwmrmOypUDt5vRgo2v32a78JdqUA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.1.tgz", + "integrity": "sha512-1lbngk9wty2kCyQB42LjqSa12SEop3t9wcEC7/xYr3ujTSTmv7HWKjKYXly0GkMfQ42PRb2lFPFEibDOiMXS0g==", "dev": true, "dependencies": { "@types/node": "*" @@ -260,9 +260,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -364,14 +364,11 @@ } }, "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "~2.1.0" } }, "node_modules/safe-buffer": { @@ -448,14 +445,14 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, "node_modules/typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -542,9 +539,9 @@ "dev": true }, "@types/node": { - "version": "14.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", - "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==", + "version": "16.11.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", + "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", "dev": true }, "@types/node-forge": { @@ -557,9 +554,9 @@ } }, "@types/papaparse": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.2.6.tgz", - "integrity": "sha512-xGKSd0UTn58N1h0+zf8mW863Rv8BvXcGibEgKFtBIXZlcDXAmX/T4RdDO2mwmrmOypUDt5vRgo2v32a78JdqUA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.1.tgz", + "integrity": "sha512-1lbngk9wty2kCyQB42LjqSa12SEop3t9wcEC7/xYr3ujTSTmv7HWKjKYXly0GkMfQ42PRb2lFPFEibDOiMXS0g==", "dev": true, "requires": { "@types/node": "*" @@ -723,9 +720,9 @@ } }, "node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "requires": { "whatwg-url": "^5.0.0" } @@ -814,11 +811,11 @@ } }, "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "requires": { - "tslib": "^1.9.0" + "tslib": "~2.1.0" } }, "safe-buffer": { @@ -876,14 +873,14 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, "typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true }, "url-parse": { diff --git a/common/package.json b/common/package.json index 7afe12c8..8901f668 100644 --- a/common/package.json +++ b/common/package.json @@ -21,12 +21,12 @@ }, "devDependencies": { "@types/lunr": "^2.3.3", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-forge": "^0.9.7", "@types/papaparse": "^5.2.5", "@types/zxcvbn": "^4.4.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" }, "dependencies": { "@microsoft/signalr": "5.0.10", @@ -36,7 +36,7 @@ "lunr": "^2.3.9", "node-forge": "^0.10.0", "papaparse": "^5.3.0", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldts": "5.7.52", "zxcvbn": "^4.4.2" } diff --git a/common/src/abstractions/apiKey.service.ts b/common/src/abstractions/apiKey.service.ts deleted file mode 100644 index bc4b8a44..00000000 --- a/common/src/abstractions/apiKey.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -export abstract class ApiKeyService { - setInformation: (clientId: string, clientSecret: string) => Promise; - clear: () => Promise; - getClientId: () => Promise; - getClientSecret: () => Promise; - getEntityType: () => Promise; - getEntityId: () => Promise; - isAuthenticated: () => Promise; -} diff --git a/common/src/abstractions/cipher.service.ts b/common/src/abstractions/cipher.service.ts index 941c3826..c1c1437a 100644 --- a/common/src/abstractions/cipher.service.ts +++ b/common/src/abstractions/cipher.service.ts @@ -1,59 +1,82 @@ -import { CipherType } from '../enums/cipherType'; -import { UriMatchType } from '../enums/uriMatchType'; +import { CipherType } from "../enums/cipherType"; +import { UriMatchType } from "../enums/uriMatchType"; -import { CipherData } from '../models/data/cipherData'; +import { CipherData } from "../models/data/cipherData"; -import { Cipher } from '../models/domain/cipher'; -import { Field } from '../models/domain/field'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { Cipher } from "../models/domain/cipher"; +import { Field } from "../models/domain/field"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { CipherView } from '../models/view/cipherView'; -import { FieldView } from '../models/view/fieldView'; +import { CipherView } from "../models/view/cipherView"; +import { FieldView } from "../models/view/fieldView"; export abstract class CipherService { - decryptedCipherCache: CipherView[]; - - clearCache: () => void; - encrypt: (model: CipherView, key?: SymmetricCryptoKey, originalCipher?: Cipher) => Promise; - encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; - encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - getAllDecrypted: () => Promise; - getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; - getAllDecryptedForUrl: (url: string, includeOtherTypes?: CipherType[], - defaultMatch?: UriMatchType) => Promise; - getAllFromApiForOrganization: (organizationId: string) => Promise; - getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise; - getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise; - getNextCipherForUrl: (url: string) => Promise; - updateLastUsedIndexForUrl: (url: string) => void; - updateLastUsedDate: (id: string) => Promise; - updateLastLaunchedDate: (id: string) => Promise; - saveNeverDomain: (domain: string) => Promise; - saveWithServer: (cipher: Cipher) => Promise; - shareWithServer: (cipher: CipherView, organizationId: string, collectionIds: string[]) => Promise; - shareManyWithServer: (ciphers: CipherView[], organizationId: string, collectionIds: string[]) => Promise; - saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any, admin?: boolean) => Promise; - saveAttachmentRawWithServer: (cipher: Cipher, filename: string, data: ArrayBuffer, - admin?: boolean) => Promise; - saveCollectionsWithServer: (cipher: Cipher) => Promise; - upsert: (cipher: CipherData | CipherData[]) => Promise; - replace: (ciphers: { [id: string]: CipherData; }) => Promise; - clear: (userId: string) => Promise; - moveManyWithServer: (ids: string[], folderId: string) => Promise; - delete: (id: string | string[]) => Promise; - deleteWithServer: (id: string) => Promise; - deleteManyWithServer: (ids: string[]) => Promise; - deleteAttachment: (id: string, attachmentId: string) => Promise; - deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; - sortCiphersByLastUsed: (a: any, b: any) => number; - sortCiphersByLastUsedThenName: (a: any, b: any) => number; - getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number; - softDelete: (id: string | string[]) => Promise; - softDeleteWithServer: (id: string) => Promise; - softDeleteManyWithServer: (ids: string[]) => Promise; - restore: (cipher: { id: string, revisionDate: string; } | { id: string, revisionDate: string; }[]) => Promise; - restoreWithServer: (id: string) => Promise; - restoreManyWithServer: (ids: string[]) => Promise; + clearCache: (userId?: string) => Promise; + encrypt: ( + model: CipherView, + key?: SymmetricCryptoKey, + originalCipher?: Cipher + ) => Promise; + encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; + encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; + getAllDecryptedForUrl: ( + url: string, + includeOtherTypes?: CipherType[], + defaultMatch?: UriMatchType + ) => Promise; + getAllFromApiForOrganization: (organizationId: string) => Promise; + getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise; + getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise; + getNextCipherForUrl: (url: string) => Promise; + updateLastUsedIndexForUrl: (url: string) => void; + updateLastUsedDate: (id: string) => Promise; + updateLastLaunchedDate: (id: string) => Promise; + saveNeverDomain: (domain: string) => Promise; + saveWithServer: (cipher: Cipher) => Promise; + shareWithServer: ( + cipher: CipherView, + organizationId: string, + collectionIds: string[] + ) => Promise; + shareManyWithServer: ( + ciphers: CipherView[], + organizationId: string, + collectionIds: string[] + ) => Promise; + saveAttachmentWithServer: ( + cipher: Cipher, + unencryptedFile: any, + admin?: boolean + ) => Promise; + saveAttachmentRawWithServer: ( + cipher: Cipher, + filename: string, + data: ArrayBuffer, + admin?: boolean + ) => Promise; + saveCollectionsWithServer: (cipher: Cipher) => Promise; + upsert: (cipher: CipherData | CipherData[]) => Promise; + replace: (ciphers: { [id: string]: CipherData }) => Promise; + clear: (userId: string) => Promise; + moveManyWithServer: (ids: string[], folderId: string) => Promise; + delete: (id: string | string[]) => Promise; + deleteWithServer: (id: string) => Promise; + deleteManyWithServer: (ids: string[]) => Promise; + deleteAttachment: (id: string, attachmentId: string) => Promise; + deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; + sortCiphersByLastUsed: (a: any, b: any) => number; + sortCiphersByLastUsedThenName: (a: any, b: any) => number; + getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number; + softDelete: (id: string | string[]) => Promise; + softDeleteWithServer: (id: string) => Promise; + softDeleteManyWithServer: (ids: string[]) => Promise; + restore: ( + cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[] + ) => Promise; + restoreWithServer: (id: string) => Promise; + restoreManyWithServer: (ids: string[]) => Promise; } diff --git a/common/src/abstractions/collection.service.ts b/common/src/abstractions/collection.service.ts index 09045537..f5880e47 100644 --- a/common/src/abstractions/collection.service.ts +++ b/common/src/abstractions/collection.service.ts @@ -1,23 +1,21 @@ -import { CollectionData } from '../models/data/collectionData'; +import { CollectionData } from "../models/data/collectionData"; -import { Collection } from '../models/domain/collection'; -import { TreeNode } from '../models/domain/treeNode'; +import { Collection } from "../models/domain/collection"; +import { TreeNode } from "../models/domain/treeNode"; -import { CollectionView } from '../models/view/collectionView'; +import { CollectionView } from "../models/view/collectionView"; export abstract class CollectionService { - decryptedCollectionCache: CollectionView[]; - - clearCache: () => void; - encrypt: (model: CollectionView) => Promise; - decryptMany: (collections: Collection[]) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - getAllDecrypted: () => Promise; - getAllNested: (collections?: CollectionView[]) => Promise[]>; - getNested: (id: string) => Promise>; - upsert: (collection: CollectionData | CollectionData[]) => Promise; - replace: (collections: { [id: string]: CollectionData; }) => Promise; - clear: (userId: string) => Promise; - delete: (id: string | string[]) => Promise; + clearCache: (userId?: string) => Promise; + encrypt: (model: CollectionView) => Promise; + decryptMany: (collections: Collection[]) => Promise; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + getAllNested: (collections?: CollectionView[]) => Promise[]>; + getNested: (id: string) => Promise>; + upsert: (collection: CollectionData | CollectionData[]) => Promise; + replace: (collections: { [id: string]: CollectionData }) => Promise; + clear: (userId: string) => Promise; + delete: (id: string | string[]) => Promise; } diff --git a/common/src/abstractions/crypto.service.ts b/common/src/abstractions/crypto.service.ts index 841e61ab..4e861952 100644 --- a/common/src/abstractions/crypto.service.ts +++ b/common/src/abstractions/crypto.service.ts @@ -1,64 +1,88 @@ -import { EncArrayBuffer } from '../models/domain/encArrayBuffer'; -import { EncString } from '../models/domain/encString'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { EncArrayBuffer } from "../models/domain/encArrayBuffer"; +import { EncString } from "../models/domain/encString"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; -import { ProfileProviderOrganizationResponse } from '../models/response/profileProviderOrganizationResponse'; -import { ProfileProviderResponse } from '../models/response/profileProviderResponse'; +import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse"; +import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse"; +import { ProfileProviderResponse } from "../models/response/profileProviderResponse"; -import { HashPurpose } from '../enums/hashPurpose'; -import { KdfType } from '../enums/kdfType'; - -import { KeySuffixOptions } from './storage.service'; +import { HashPurpose } from "../enums/hashPurpose"; +import { KdfType } from "../enums/kdfType"; +import { KeySuffixOptions } from "../enums/keySuffixOptions"; export abstract class CryptoService { - setKey: (key: SymmetricCryptoKey) => Promise; - setKeyHash: (keyHash: string) => Promise<{}>; - setEncKey: (encKey: string) => Promise<{}>; - setEncPrivateKey: (encPrivateKey: string) => Promise<{}>; - setOrgKeys: (orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[]) => Promise<{}>; - setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<{}>; - getKey: (keySuffix?: KeySuffixOptions) => Promise; - getKeyFromStorage: (keySuffix: KeySuffixOptions) => Promise; - getKeyHash: () => Promise; - compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise; - getEncKey: (key?: SymmetricCryptoKey) => Promise; - getPublicKey: () => Promise; - getPrivateKey: () => Promise; - getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise; - getOrgKeys: () => Promise>; - getOrgKey: (orgId: string) => Promise; - getProviderKey: (providerId: string) => Promise; - hasKey: () => Promise; - hasKeyInMemory: () => boolean; - hasKeyStored: (keySuffix?: KeySuffixOptions) => Promise; - hasEncKey: () => Promise; - clearKey: (clearSecretStorage?: boolean) => Promise; - clearKeyHash: () => Promise; - clearEncKey: (memoryOnly?: boolean) => Promise; - clearKeyPair: (memoryOnly?: boolean) => Promise; - clearOrgKeys: (memoryOnly?: boolean) => Promise; - clearProviderKeys: (memoryOnly?: boolean) => Promise; - clearPinProtectedKey: () => Promise; - clearKeys: () => Promise; - toggleKey: () => Promise; - makeKey: (password: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; - makeKeyFromPin: (pin: string, salt: string, kdf: KdfType, kdfIterations: number, - protectedKeyCs?: EncString) => Promise; - makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>; - makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>; - makePinKey: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; - makeSendKey: (keyMaterial: ArrayBuffer) => Promise; - hashPassword: (password: string, key: SymmetricCryptoKey, hashPurpose?: HashPurpose) => Promise; - makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>; - remakeEncKey: (key: SymmetricCryptoKey, encKey?: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>; - encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; - encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; - rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise; - rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise; - decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise; - decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise; - decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; - randomNumber: (min: number, max: number) => Promise; - validateKey: (key: SymmetricCryptoKey) => Promise; + setKey: (key: SymmetricCryptoKey) => Promise; + setKeyHash: (keyHash: string) => Promise; + setEncKey: (encKey: string) => Promise; + setEncPrivateKey: (encPrivateKey: string) => Promise; + setOrgKeys: ( + orgs: ProfileOrganizationResponse[], + providerOrgs: ProfileProviderOrganizationResponse[] + ) => Promise; + setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise; + getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise; + getKeyFromStorage: (keySuffix: KeySuffixOptions) => Promise; + getKeyHash: () => Promise; + compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise; + getEncKey: (key?: SymmetricCryptoKey) => Promise; + getPublicKey: () => Promise; + getPrivateKey: () => Promise; + getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise; + getOrgKeys: () => Promise>; + getOrgKey: (orgId: string) => Promise; + getProviderKey: (providerId: string) => Promise; + hasKey: () => Promise; + hasKeyInMemory: (userId?: string) => Promise; + hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise; + hasEncKey: () => Promise; + clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise; + clearKeyHash: () => Promise; + clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise; + clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise; + clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise; + clearProviderKeys: (memoryOnly?: boolean) => Promise; + clearPinProtectedKey: () => Promise; + clearKeys: (userId?: string) => Promise; + toggleKey: () => Promise; + makeKey: ( + password: string, + salt: string, + kdf: KdfType, + kdfIterations: number + ) => Promise; + makeKeyFromPin: ( + pin: string, + salt: string, + kdf: KdfType, + kdfIterations: number, + protectedKeyCs?: EncString + ) => Promise; + makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>; + makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>; + makePinKey: ( + pin: string, + salt: string, + kdf: KdfType, + kdfIterations: number + ) => Promise; + makeSendKey: (keyMaterial: ArrayBuffer) => Promise; + hashPassword: ( + password: string, + key: SymmetricCryptoKey, + hashPurpose?: HashPurpose + ) => Promise; + makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>; + remakeEncKey: ( + key: SymmetricCryptoKey, + encKey?: SymmetricCryptoKey + ) => Promise<[SymmetricCryptoKey, EncString]>; + encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; + encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; + rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise; + rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise; + decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise; + decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise; + decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; + randomNumber: (min: number, max: number) => Promise; + validateKey: (key: SymmetricCryptoKey) => Promise; } diff --git a/common/src/abstractions/event.service.ts b/common/src/abstractions/event.service.ts index 40c08027..2f7660fb 100644 --- a/common/src/abstractions/event.service.ts +++ b/common/src/abstractions/event.service.ts @@ -1,7 +1,7 @@ -import { EventType } from '../enums/eventType'; +import { EventType } from "../enums/eventType"; export abstract class EventService { - collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise; - uploadEvents: () => Promise; - clearEvents: () => Promise; + collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise; + uploadEvents: (userId?: string) => Promise; + clearEvents: (userId?: string) => Promise; } diff --git a/common/src/abstractions/folder.service.ts b/common/src/abstractions/folder.service.ts index f1997ffe..139525e0 100644 --- a/common/src/abstractions/folder.service.ts +++ b/common/src/abstractions/folder.service.ts @@ -1,25 +1,23 @@ -import { FolderData } from '../models/data/folderData'; +import { FolderData } from "../models/data/folderData"; -import { Folder } from '../models/domain/folder'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { TreeNode } from '../models/domain/treeNode'; +import { Folder } from "../models/domain/folder"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; +import { TreeNode } from "../models/domain/treeNode"; -import { FolderView } from '../models/view/folderView'; +import { FolderView } from "../models/view/folderView"; export abstract class FolderService { - decryptedFolderCache: FolderView[]; - - clearCache: () => void; - encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - getAllDecrypted: () => Promise; - getAllNested: () => Promise[]>; - getNested: (id: string) => Promise>; - saveWithServer: (folder: Folder) => Promise; - upsert: (folder: FolderData | FolderData[]) => Promise; - replace: (folders: { [id: string]: FolderData; }) => Promise; - clear: (userId: string) => Promise; - delete: (id: string | string[]) => Promise; - deleteWithServer: (id: string) => Promise; + clearCache: (userId?: string) => Promise; + encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + getAllNested: () => Promise[]>; + getNested: (id: string) => Promise>; + saveWithServer: (folder: Folder) => Promise; + upsert: (folder: FolderData | FolderData[]) => Promise; + replace: (folders: { [id: string]: FolderData }) => Promise; + clear: (userId: string) => Promise; + delete: (id: string | string[]) => Promise; + deleteWithServer: (id: string) => Promise; } diff --git a/common/src/abstractions/organization.service.ts b/common/src/abstractions/organization.service.ts new file mode 100644 index 00000000..a878335d --- /dev/null +++ b/common/src/abstractions/organization.service.ts @@ -0,0 +1,11 @@ +import { OrganizationData } from "../models/data/organizationData"; + +import { Organization } from "../models/domain/organization"; + +export abstract class OrganizationService { + get: (id: string) => Promise; + getByIdentifier: (identifier: string) => Promise; + getAll: (userId?: string) => Promise; + save: (orgs: { [id: string]: OrganizationData }) => Promise; + canManageSponsorships: () => Promise; +} diff --git a/common/src/abstractions/passwordGeneration.service.ts b/common/src/abstractions/passwordGeneration.service.ts index 52d18dde..82bc021f 100644 --- a/common/src/abstractions/passwordGeneration.service.ts +++ b/common/src/abstractions/passwordGeneration.service.ts @@ -1,18 +1,20 @@ -import * as zxcvbn from 'zxcvbn'; +import * as zxcvbn from "zxcvbn"; -import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; -import { PasswordGeneratorPolicyOptions } from '../models/domain/passwordGeneratorPolicyOptions'; +import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; +import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions"; export abstract class PasswordGenerationService { - generatePassword: (options: any) => Promise; - generatePassphrase: (options: any) => Promise; - getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>; - enforcePasswordGeneratorPoliciesOnOptions: (options: any) => Promise<[any, PasswordGeneratorPolicyOptions]>; - getPasswordGeneratorPolicyOptions: () => Promise; - saveOptions: (options: any) => Promise; - getHistory: () => Promise; - addHistory: (password: string) => Promise; - clear: () => Promise; - passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult; - normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void; + generatePassword: (options: any) => Promise; + generatePassphrase: (options: any) => Promise; + getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>; + enforcePasswordGeneratorPoliciesOnOptions: ( + options: any + ) => Promise<[any, PasswordGeneratorPolicyOptions]>; + getPasswordGeneratorPolicyOptions: () => Promise; + saveOptions: (options: any) => Promise; + getHistory: () => Promise; + addHistory: (password: string) => Promise; + clear: (userId?: string) => Promise; + passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult; + normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void; } diff --git a/common/src/abstractions/platformUtils.service.ts b/common/src/abstractions/platformUtils.service.ts index 7f792e61..f5a6203d 100644 --- a/common/src/abstractions/platformUtils.service.ts +++ b/common/src/abstractions/platformUtils.service.ts @@ -1,36 +1,52 @@ -import { DeviceType } from '../enums/deviceType'; -import { ThemeType } from '../enums/themeType'; +import { DeviceType } from "../enums/deviceType"; +import { ThemeType } from "../enums/themeType"; + +interface ToastOptions { + timeout?: number; +} export abstract class PlatformUtilsService { - identityClientId: string; - getDevice: () => DeviceType; - getDeviceString: () => string; - isFirefox: () => boolean; - isChrome: () => boolean; - isEdge: () => boolean; - isOpera: () => boolean; - isVivaldi: () => boolean; - isSafari: () => boolean; - isIE: () => boolean; - isMacAppStore: () => boolean; - isViewOpen: () => Promise; - launchUri: (uri: string, options?: any) => void; - saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; - getApplicationVersion: () => Promise; - supportsWebAuthn: (win: Window) => boolean; - supportsDuo: () => boolean; - showToast: (type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], - options?: any) => void; - showDialog: (body: string, title?: string, confirmText?: string, cancelText?: string, - type?: string, bodyIsHtml?: boolean) => Promise; - isDev: () => boolean; - isSelfHost: () => boolean; - copyToClipboard: (text: string, options?: any) => void | boolean; - readFromClipboard: (options?: any) => Promise; - supportsBiometric: () => Promise; - authenticateBiometric: () => Promise; - getDefaultSystemTheme: () => Promise; - onDefaultSystemThemeChange: (callback: ((theme: ThemeType.Light | ThemeType.Dark) => unknown)) => unknown; - getEffectiveTheme: () => Promise; - supportsSecureStorage: () => boolean; + identityClientId: string; + getDevice: () => DeviceType; + getDeviceString: () => string; + isFirefox: () => boolean; + isChrome: () => boolean; + isEdge: () => boolean; + isOpera: () => boolean; + isVivaldi: () => boolean; + isSafari: () => boolean; + isIE: () => boolean; + isMacAppStore: () => boolean; + isViewOpen: () => Promise; + launchUri: (uri: string, options?: any) => void; + saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; + getApplicationVersion: () => Promise; + supportsWebAuthn: (win: Window) => boolean; + supportsDuo: () => boolean; + showToast: ( + type: "error" | "success" | "warning" | "info", + title: string, + text: string | string[], + options?: ToastOptions + ) => void; + showDialog: ( + body: string, + title?: string, + confirmText?: string, + cancelText?: string, + type?: string, + bodyIsHtml?: boolean + ) => Promise; + isDev: () => boolean; + isSelfHost: () => boolean; + copyToClipboard: (text: string, options?: any) => void | boolean; + readFromClipboard: (options?: any) => Promise; + supportsBiometric: () => Promise; + authenticateBiometric: () => Promise; + getDefaultSystemTheme: () => Promise; + onDefaultSystemThemeChange: ( + callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown + ) => unknown; + getEffectiveTheme: () => Promise; + supportsSecureStorage: () => boolean; } diff --git a/common/src/abstractions/policy.service.ts b/common/src/abstractions/policy.service.ts index 6acc8253..97dcb4ad 100644 --- a/common/src/abstractions/policy.service.ts +++ b/common/src/abstractions/policy.service.ts @@ -1,26 +1,34 @@ -import { PolicyData } from '../models/data/policyData'; +import { PolicyData } from "../models/data/policyData"; -import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions'; -import { Policy } from '../models/domain/policy'; -import { ResetPasswordPolicyOptions } from '../models/domain/resetPasswordPolicyOptions'; +import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions"; +import { Policy } from "../models/domain/policy"; +import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions"; -import { ListResponse } from '../models/response/listResponse'; -import { PolicyResponse } from '../models/response/policyResponse'; +import { ListResponse } from "../models/response/listResponse"; +import { PolicyResponse } from "../models/response/policyResponse"; -import { PolicyType } from '../enums/policyType'; +import { PolicyType } from "../enums/policyType"; export abstract class PolicyService { - policyCache: Policy[]; - - clearCache: () => void; - getAll: (type?: PolicyType) => Promise; - getPolicyForOrganization: (policyType: PolicyType, organizationId: string) => Promise; - replace: (policies: { [id: string]: PolicyData; }) => Promise; - clear: (userId: string) => Promise; - getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise; - evaluateMasterPassword: (passwordStrength: number, newPassword: string, - enforcedPolicyOptions?: MasterPasswordPolicyOptions) => boolean; - getResetPasswordPolicyOptions: (policies: Policy[], orgId: string) => [ResetPasswordPolicyOptions, boolean]; - mapPoliciesFromToken: (policiesResponse: ListResponse) => Policy[]; - policyAppliesToUser: (policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) => Promise; + clearCache: () => void; + getAll: (type?: PolicyType, userId?: string) => Promise; + getPolicyForOrganization: (policyType: PolicyType, organizationId: string) => Promise; + replace: (policies: { [id: string]: PolicyData }) => Promise; + clear: (userId?: string) => Promise; + getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise; + evaluateMasterPassword: ( + passwordStrength: number, + newPassword: string, + enforcedPolicyOptions?: MasterPasswordPolicyOptions + ) => boolean; + getResetPasswordPolicyOptions: ( + policies: Policy[], + orgId: string + ) => [ResetPasswordPolicyOptions, boolean]; + mapPoliciesFromToken: (policiesResponse: ListResponse) => Policy[]; + policyAppliesToUser: ( + policyType: PolicyType, + policyFilter?: (policy: Policy) => boolean, + userId?: string + ) => Promise; } diff --git a/common/src/abstractions/provider.service.ts b/common/src/abstractions/provider.service.ts new file mode 100644 index 00000000..e3e86a1a --- /dev/null +++ b/common/src/abstractions/provider.service.ts @@ -0,0 +1,9 @@ +import { ProviderData } from "../models/data/providerData"; + +import { Provider } from "../models/domain/provider"; + +export abstract class ProviderService { + get: (id: string) => Promise; + getAll: () => Promise; + save: (providers: { [id: string]: ProviderData }) => Promise; +} diff --git a/common/src/abstractions/send.service.ts b/common/src/abstractions/send.service.ts index f7ede223..37b806f2 100644 --- a/common/src/abstractions/send.service.ts +++ b/common/src/abstractions/send.service.ts @@ -1,24 +1,27 @@ -import { SendData } from '../models/data/sendData'; +import { SendData } from "../models/data/sendData"; -import { EncArrayBuffer } from '../models/domain/encArrayBuffer'; -import { Send } from '../models/domain/send'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { EncArrayBuffer } from "../models/domain/encArrayBuffer"; +import { Send } from "../models/domain/send"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { SendView } from '../models/view/sendView'; +import { SendView } from "../models/view/sendView"; export abstract class SendService { - decryptedSendCache: SendView[]; - - clearCache: () => void; - encrypt: (model: SendView, file: File | ArrayBuffer, password: string, key?: SymmetricCryptoKey) => Promise<[Send, EncArrayBuffer]>; - get: (id: string) => Promise; - getAll: () => Promise; - getAllDecrypted: () => Promise; - saveWithServer: (sendData: [Send, EncArrayBuffer]) => Promise; - upsert: (send: SendData | SendData[]) => Promise; - replace: (sends: { [id: string]: SendData; }) => Promise; - clear: (userId: string) => Promise; - delete: (id: string | string[]) => Promise; - deleteWithServer: (id: string) => Promise; - removePasswordWithServer: (id: string) => Promise; + clearCache: () => Promise; + encrypt: ( + model: SendView, + file: File | ArrayBuffer, + password: string, + key?: SymmetricCryptoKey + ) => Promise<[Send, EncArrayBuffer]>; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + saveWithServer: (sendData: [Send, EncArrayBuffer]) => Promise; + upsert: (send: SendData | SendData[]) => Promise; + replace: (sends: { [id: string]: SendData }) => Promise; + clear: (userId: string) => Promise; + delete: (id: string | string[]) => Promise; + deleteWithServer: (id: string) => Promise; + removePasswordWithServer: (id: string) => Promise; } diff --git a/common/src/abstractions/settings.service.ts b/common/src/abstractions/settings.service.ts index 6104c1d1..e7886585 100644 --- a/common/src/abstractions/settings.service.ts +++ b/common/src/abstractions/settings.service.ts @@ -1,6 +1,6 @@ export abstract class SettingsService { - clearCache: () => void; - getEquivalentDomains: () => Promise; - setEquivalentDomains: (equivalentDomains: string[][]) => Promise; - clear: (userId: string) => Promise; + clearCache: () => Promise; + getEquivalentDomains: () => Promise; + setEquivalentDomains: (equivalentDomains: string[][]) => Promise; + clear: (userId?: string) => Promise; } diff --git a/common/src/abstractions/state.service.ts b/common/src/abstractions/state.service.ts index 78658882..55707b7f 100644 --- a/common/src/abstractions/state.service.ts +++ b/common/src/abstractions/state.service.ts @@ -1,6 +1,303 @@ +import { BehaviorSubject } from "rxjs"; + +import { KdfType } from "../enums/kdfType"; +import { UriMatchType } from "../enums/uriMatchType"; + +import { CipherData } from "../models/data/cipherData"; +import { CollectionData } from "../models/data/collectionData"; +import { EventData } from "../models/data/eventData"; +import { FolderData } from "../models/data/folderData"; +import { OrganizationData } from "../models/data/organizationData"; +import { PolicyData } from "../models/data/policyData"; +import { ProviderData } from "../models/data/providerData"; +import { SendData } from "../models/data/sendData"; + +import { Account } from "../models/domain/account"; +import { EncString } from "../models/domain/encString"; +import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; +import { Policy } from "../models/domain/policy"; +import { StorageOptions } from "../models/domain/storageOptions"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; + +import { CipherView } from "../models/view/cipherView"; +import { CollectionView } from "../models/view/collectionView"; +import { FolderView } from "../models/view/folderView"; +import { SendView } from "../models/view/sendView"; + export abstract class StateService { - get: (key: string) => Promise; - save: (key: string, obj: any) => Promise; - remove: (key: string) => Promise; - purge: () => Promise; + accounts: BehaviorSubject<{ [userId: string]: Account }>; + activeAccount: BehaviorSubject; + + addAccount: (account: Account) => Promise; + setActiveUser: (userId: string) => Promise; + clean: (options?: StorageOptions) => Promise; + init: () => Promise; + + getAccessToken: (options?: StorageOptions) => Promise; + setAccessToken: (value: string, options?: StorageOptions) => Promise; + getAddEditCipherInfo: (options?: StorageOptions) => Promise; + setAddEditCipherInfo: (value: any, options?: StorageOptions) => Promise; + getAlwaysShowDock: (options?: StorageOptions) => Promise; + setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise; + getApiKeyClientId: (options?: StorageOptions) => Promise; + setApiKeyClientId: (value: string, options?: StorageOptions) => Promise; + getApiKeyClientSecret: (options?: StorageOptions) => Promise; + setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise; + getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise; + setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise; + getAutoFillOnPageLoadDefault: (options?: StorageOptions) => Promise; + setAutoFillOnPageLoadDefault: (value: boolean, options?: StorageOptions) => Promise; + getBiometricAwaitingAcceptance: (options?: StorageOptions) => Promise; + setBiometricAwaitingAcceptance: (value: boolean, options?: StorageOptions) => Promise; + getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; + setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise; + getBiometricLocked: (options?: StorageOptions) => Promise; + setBiometricLocked: (value: boolean, options?: StorageOptions) => Promise; + getBiometricText: (options?: StorageOptions) => Promise; + setBiometricText: (value: string, options?: StorageOptions) => Promise; + getBiometricUnlock: (options?: StorageOptions) => Promise; + setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise; + getCanAccessPremium: (options?: StorageOptions) => Promise; + getClearClipboard: (options?: StorageOptions) => Promise; + setClearClipboard: (value: number, options?: StorageOptions) => Promise; + getCollapsedGroupings: (options?: StorageOptions) => Promise>; + setCollapsedGroupings: (value: Set, options?: StorageOptions) => Promise; + getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; + setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise; + getCryptoMasterKey: (options?: StorageOptions) => Promise; + setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise; + getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise; + setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise; + getCryptoMasterKeyB64: (options?: StorageOptions) => Promise; + setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise; + getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; + hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; + setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise; + getDecodedToken: (options?: StorageOptions) => Promise; + setDecodedToken: (value: any, options?: StorageOptions) => Promise; + getDecryptedCiphers: (options?: StorageOptions) => Promise; + setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise; + getDecryptedCollections: (options?: StorageOptions) => Promise; + setDecryptedCollections: (value: CollectionView[], options?: StorageOptions) => Promise; + getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; + setDecryptedCryptoSymmetricKey: ( + value: SymmetricCryptoKey, + options?: StorageOptions + ) => Promise; + getDecryptedFolders: (options?: StorageOptions) => Promise; + setDecryptedFolders: (value: FolderView[], options?: StorageOptions) => Promise; + getDecryptedOrganizationKeys: ( + options?: StorageOptions + ) => Promise>; + setDecryptedOrganizationKeys: ( + value: Map, + options?: StorageOptions + ) => Promise; + getDecryptedPasswordGenerationHistory: ( + options?: StorageOptions + ) => Promise; + setDecryptedPasswordGenerationHistory: ( + value: GeneratedPasswordHistory[], + options?: StorageOptions + ) => Promise; + getDecryptedPinProtected: (options?: StorageOptions) => Promise; + setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise; + getDecryptedPolicies: (options?: StorageOptions) => Promise; + setDecryptedPolicies: (value: Policy[], options?: StorageOptions) => Promise; + getDecryptedPrivateKey: (options?: StorageOptions) => Promise; + setDecryptedPrivateKey: (value: ArrayBuffer, options?: StorageOptions) => Promise; + getDecryptedProviderKeys: (options?: StorageOptions) => Promise>; + setDecryptedProviderKeys: ( + value: Map, + options?: StorageOptions + ) => Promise; + getDecryptedSends: (options?: StorageOptions) => Promise; + setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise; + getDefaultUriMatch: (options?: StorageOptions) => Promise; + setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise; + getDisableAddLoginNotification: (options?: StorageOptions) => Promise; + setDisableAddLoginNotification: (value: boolean, options?: StorageOptions) => Promise; + getDisableAutoBiometricsPrompt: (options?: StorageOptions) => Promise; + setDisableAutoBiometricsPrompt: (value: boolean, options?: StorageOptions) => Promise; + getDisableAutoTotpCopy: (options?: StorageOptions) => Promise; + setDisableAutoTotpCopy: (value: boolean, options?: StorageOptions) => Promise; + getDisableBadgeCounter: (options?: StorageOptions) => Promise; + setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise; + getDisableChangedPasswordNotification: (options?: StorageOptions) => Promise; + setDisableChangedPasswordNotification: ( + value: boolean, + options?: StorageOptions + ) => Promise; + getDisableContextMenuItem: (options?: StorageOptions) => Promise; + setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise; + getDisableFavicon: (options?: StorageOptions) => Promise; + setDisableFavicon: (value: boolean, options?: StorageOptions) => Promise; + getDisableGa: (options?: StorageOptions) => Promise; + setDisableGa: (value: boolean, options?: StorageOptions) => Promise; + getDontShowCardsCurrentTab: (options?: StorageOptions) => Promise; + setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise; + getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise; + setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise; + getEmail: (options?: StorageOptions) => Promise; + setEmail: (value: string, options?: StorageOptions) => Promise; + getEmailVerified: (options?: StorageOptions) => Promise; + setEmailVerified: (value: boolean, options?: StorageOptions) => Promise; + getEnableAlwaysOnTop: (options?: StorageOptions) => Promise; + setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise; + getEnableAutoFillOnPageLoad: (options?: StorageOptions) => Promise; + setEnableAutoFillOnPageLoad: (value: boolean, options?: StorageOptions) => Promise; + getEnableBiometric: (options?: StorageOptions) => Promise; + setEnableBiometric: (value: boolean, options?: StorageOptions) => Promise; + getEnableBrowserIntegration: (options?: StorageOptions) => Promise; + setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise; + getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise; + setEnableBrowserIntegrationFingerprint: ( + value: boolean, + options?: StorageOptions + ) => Promise; + getEnableCloseToTray: (options?: StorageOptions) => Promise; + setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise; + getEnableFullWidth: (options?: StorageOptions) => Promise; + setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise; + getEnableGravitars: (options?: StorageOptions) => Promise; + setEnableGravitars: (value: boolean, options?: StorageOptions) => Promise; + getEnableMinimizeToTray: (options?: StorageOptions) => Promise; + setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise; + getEnableStartToTray: (options?: StorageOptions) => Promise; + setEnableStartToTray: (value: boolean, options?: StorageOptions) => Promise; + getEnableTray: (options?: StorageOptions) => Promise; + setEnableTray: (value: boolean, options?: StorageOptions) => Promise; + getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>; + setEncryptedCiphers: ( + value: { [id: string]: CipherData }, + options?: StorageOptions + ) => Promise; + getEncryptedCollections: (options?: StorageOptions) => Promise<{ [id: string]: CollectionData }>; + setEncryptedCollections: ( + value: { [id: string]: CollectionData }, + options?: StorageOptions + ) => Promise; + getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; + setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise; + getEncryptedFolders: (options?: StorageOptions) => Promise<{ [id: string]: FolderData }>; + setEncryptedFolders: ( + value: { [id: string]: FolderData }, + options?: StorageOptions + ) => Promise; + getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise; + setEncryptedOrganizationKeys: ( + value: Map, + options?: StorageOptions + ) => Promise; + getEncryptedPasswordGenerationHistory: ( + options?: StorageOptions + ) => Promise; + setEncryptedPasswordGenerationHistory: ( + value: GeneratedPasswordHistory[], + options?: StorageOptions + ) => Promise; + getEncryptedPinProtected: (options?: StorageOptions) => Promise; + setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; + getEncryptedPolicies: (options?: StorageOptions) => Promise<{ [id: string]: PolicyData }>; + setEncryptedPolicies: ( + value: { [id: string]: PolicyData }, + options?: StorageOptions + ) => Promise; + getEncryptedPrivateKey: (options?: StorageOptions) => Promise; + setEncryptedPrivateKey: (value: string, options?: StorageOptions) => Promise; + getEncryptedProviderKeys: (options?: StorageOptions) => Promise; + setEncryptedProviderKeys: (value: any, options?: StorageOptions) => Promise; + getEncryptedSends: (options?: StorageOptions) => Promise<{ [id: string]: SendData }>; + setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise; + getEntityId: (options?: StorageOptions) => Promise; + setEntityId: (value: string, options?: StorageOptions) => Promise; + getEntityType: (options?: StorageOptions) => Promise; + setEntityType: (value: string, options?: StorageOptions) => Promise; + getEnvironmentUrls: (options?: StorageOptions) => Promise; + setEnvironmentUrls: (value: any, options?: StorageOptions) => Promise; + getEquivalentDomains: (options?: StorageOptions) => Promise; + setEquivalentDomains: (value: string, options?: StorageOptions) => Promise; + getEventCollection: (options?: StorageOptions) => Promise; + setEventCollection: (value: EventData[], options?: StorageOptions) => Promise; + getEverBeenUnlocked: (options?: StorageOptions) => Promise; + setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise; + getForcePasswordReset: (options?: StorageOptions) => Promise; + setForcePasswordReset: (value: boolean, options?: StorageOptions) => Promise; + getInstalledVersion: (options?: StorageOptions) => Promise; + setInstalledVersion: (value: string, options?: StorageOptions) => Promise; + getIsAuthenticated: (options?: StorageOptions) => Promise; + getKdfIterations: (options?: StorageOptions) => Promise; + setKdfIterations: (value: number, options?: StorageOptions) => Promise; + getKdfType: (options?: StorageOptions) => Promise; + setKdfType: (value: KdfType, options?: StorageOptions) => Promise; + getKeyHash: (options?: StorageOptions) => Promise; + setKeyHash: (value: string, options?: StorageOptions) => Promise; + getLastActive: (options?: StorageOptions) => Promise; + setLastActive: (value: number, options?: StorageOptions) => Promise; + getLastSync: (options?: StorageOptions) => Promise; + setLastSync: (value: string, options?: StorageOptions) => Promise; + getLegacyEtmKey: (options?: StorageOptions) => Promise; + setLegacyEtmKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise; + getLocalData: (options?: StorageOptions) => Promise; + setLocalData: (value: string, options?: StorageOptions) => Promise; + getLocale: (options?: StorageOptions) => Promise; + setLocale: (value: string, options?: StorageOptions) => Promise; + getLoginRedirect: (options?: StorageOptions) => Promise; + setLoginRedirect: (value: any, options?: StorageOptions) => Promise; + getMainWindowSize: (options?: StorageOptions) => Promise; + setMainWindowSize: (value: number, options?: StorageOptions) => Promise; + getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; + setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; + getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: any }>; + setNeverDomains: (value: { [id: string]: any }, options?: StorageOptions) => Promise; + getNoAutoPromptBiometrics: (options?: StorageOptions) => Promise; + setNoAutoPromptBiometrics: (value: boolean, options?: StorageOptions) => Promise; + getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise; + setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise; + getOpenAtLogin: (options?: StorageOptions) => Promise; + setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise; + getOrganizationInvitation: (options?: StorageOptions) => Promise; + setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise; + getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>; + setOrganizations: ( + value: { [id: string]: OrganizationData }, + options?: StorageOptions + ) => Promise; + getPasswordGenerationOptions: (options?: StorageOptions) => Promise; + setPasswordGenerationOptions: (value: any, options?: StorageOptions) => Promise; + getProtectedPin: (options?: StorageOptions) => Promise; + setProtectedPin: (value: string, options?: StorageOptions) => Promise; + getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>; + setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise; + getPublicKey: (options?: StorageOptions) => Promise; + setPublicKey: (value: ArrayBuffer, options?: StorageOptions) => Promise; + getRefreshToken: (options?: StorageOptions) => Promise; + setRefreshToken: (value: string, options?: StorageOptions) => Promise; + getRememberedEmail: (options?: StorageOptions) => Promise; + setRememberedEmail: (value: string, options?: StorageOptions) => Promise; + getSecurityStamp: (options?: StorageOptions) => Promise; + setSecurityStamp: (value: string, options?: StorageOptions) => Promise; + getSettings: (options?: StorageOptions) => Promise; + setSettings: (value: string, options?: StorageOptions) => Promise; + getSsoCodeVerifier: (options?: StorageOptions) => Promise; + setSsoCodeVerifier: (value: string, options?: StorageOptions) => Promise; + getSsoOrgIdentifier: (options?: StorageOptions) => Promise; + setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise; + getSsoState: (options?: StorageOptions) => Promise; + setSsoState: (value: string, options?: StorageOptions) => Promise; + getTheme: (options?: StorageOptions) => Promise; + setTheme: (value: string, options?: StorageOptions) => Promise; + getTwoFactorToken: (options?: StorageOptions) => Promise; + setTwoFactorToken: (value: string, options?: StorageOptions) => Promise; + getUserId: (options?: StorageOptions) => Promise; + getUsesKeyConnector: (options?: StorageOptions) => Promise; + setUsesKeyConnector: (vaule: boolean, options?: StorageOptions) => Promise; + getVaultTimeout: (options?: StorageOptions) => Promise; + setVaultTimeout: (value: number, options?: StorageOptions) => Promise; + getVaultTimeoutAction: (options?: StorageOptions) => Promise; + setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; + getStateVersion: () => Promise; + setStateVersion: (value: number) => Promise; + getWindow: () => Promise>; + setWindow: (value: Map) => Promise; } diff --git a/common/src/abstractions/stateMigration.service.ts b/common/src/abstractions/stateMigration.service.ts new file mode 100644 index 00000000..f16777a1 --- /dev/null +++ b/common/src/abstractions/stateMigration.service.ts @@ -0,0 +1,4 @@ +export abstract class StateMigrationService { + needsMigration: () => Promise; + migrate: () => Promise; +} diff --git a/common/src/abstractions/storage.service.ts b/common/src/abstractions/storage.service.ts index cfedb2d0..f522d3cf 100644 --- a/common/src/abstractions/storage.service.ts +++ b/common/src/abstractions/storage.service.ts @@ -1,12 +1,8 @@ +import { StorageOptions } from "../models/domain/storageOptions"; + export abstract class StorageService { - get: (key: string, options?: StorageServiceOptions) => Promise; - has: (key: string, options?: StorageServiceOptions) => Promise; - save: (key: string, obj: any, options?: StorageServiceOptions) => Promise; - remove: (key: string, options?: StorageServiceOptions) => Promise; + get: (key: string, options?: StorageOptions) => Promise; + has: (key: string, options?: StorageOptions) => Promise; + save: (key: string, obj: any, options?: StorageOptions) => Promise; + remove: (key: string, options?: StorageOptions) => Promise; } - -export interface StorageServiceOptions { - keySuffix: KeySuffixOptions; -} - -export type KeySuffixOptions = 'auto' | 'biometric'; diff --git a/common/src/abstractions/sync.service.ts b/common/src/abstractions/sync.service.ts index 5d35cf20..936d027f 100644 --- a/common/src/abstractions/sync.service.ts +++ b/common/src/abstractions/sync.service.ts @@ -1,19 +1,19 @@ import { - SyncCipherNotification, - SyncFolderNotification, - SyncSendNotification, -} from '../models/response/notificationResponse'; + SyncCipherNotification, + SyncFolderNotification, + SyncSendNotification, +} from "../models/response/notificationResponse"; export abstract class SyncService { - syncInProgress: boolean; + syncInProgress: boolean; - getLastSync: () => Promise; - setLastSync: (date: Date) => Promise; - fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise; - syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise; - syncDeleteFolder: (notification: SyncFolderNotification) => Promise; - syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise; - syncDeleteCipher: (notification: SyncFolderNotification) => Promise; - syncUpsertSend: (notification: SyncSendNotification, isEdit: boolean) => Promise; - syncDeleteSend: (notification: SyncSendNotification) => Promise; + getLastSync: () => Promise; + setLastSync: (date: Date, userId?: string) => Promise; + fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise; + syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise; + syncDeleteFolder: (notification: SyncFolderNotification) => Promise; + syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise; + syncDeleteCipher: (notification: SyncFolderNotification) => Promise; + syncUpsertSend: (notification: SyncSendNotification, isEdit: boolean) => Promise; + syncDeleteSend: (notification: SyncSendNotification) => Promise; } diff --git a/common/src/abstractions/system.service.ts b/common/src/abstractions/system.service.ts index c8948bc1..b5c1e595 100644 --- a/common/src/abstractions/system.service.ts +++ b/common/src/abstractions/system.service.ts @@ -1,6 +1,6 @@ export abstract class SystemService { - startProcessReload: () => void; - cancelProcessReload: () => void; - clearClipboard: (clipboardValue: string, timeoutMs?: number) => void; - clearPendingClipboard: () => Promise; + startProcessReload: () => Promise; + cancelProcessReload: () => void; + clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise; + clearPendingClipboard: () => Promise; } diff --git a/common/src/abstractions/token.service.ts b/common/src/abstractions/token.service.ts index 73c960ec..6006ba10 100644 --- a/common/src/abstractions/token.service.ts +++ b/common/src/abstractions/token.service.ts @@ -1,30 +1,31 @@ export abstract class TokenService { - token: string; - decodedToken: any; - refreshToken: string; - setTokens: (accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]) => Promise; - setToken: (token: string) => Promise; - getToken: () => Promise; - setRefreshToken: (refreshToken: string) => Promise; - getRefreshToken: () => Promise; - setClientId: (clientId: string) => Promise; - getClientId: () => Promise; - setClientSecret: (clientSecret: string) => Promise; - getClientSecret: () => Promise; - toggleTokens: () => Promise; - setTwoFactorToken: (token: string, email: string) => Promise; - getTwoFactorToken: (email: string) => Promise; - clearTwoFactorToken: (email: string) => Promise; - clearToken: () => Promise; - decodeToken: () => any; - getTokenExpirationDate: () => Date; - tokenSecondsRemaining: (offsetSeconds?: number) => number; - tokenNeedsRefresh: (minutes?: number) => boolean; - getUserId: () => string; - getEmail: () => string; - getEmailVerified: () => boolean; - getName: () => string; - getPremium: () => boolean; - getIssuer: () => string; - getIsExternal: () => boolean; + setTokens: ( + accessToken: string, + refreshToken: string, + clientIdClientSecret: [string, string] + ) => Promise; + setToken: (token: string) => Promise; + getToken: () => Promise; + setRefreshToken: (refreshToken: string) => Promise; + getRefreshToken: () => Promise; + setClientId: (clientId: string) => Promise; + getClientId: () => Promise; + setClientSecret: (clientSecret: string) => Promise; + getClientSecret: () => Promise; + toggleTokens: () => Promise; + setTwoFactorToken: (token: string, email: string) => Promise; + getTwoFactorToken: (email: string) => Promise; + clearTwoFactorToken: (email: string) => Promise; + clearToken: (userId?: string) => Promise; + decodeToken: (token?: string) => any; + getTokenExpirationDate: () => Promise; + tokenSecondsRemaining: (offsetSeconds?: number) => Promise; + tokenNeedsRefresh: (minutes?: number) => Promise; + getUserId: () => Promise; + getEmail: () => Promise; + getEmailVerified: () => Promise; + getName: () => Promise; + getPremium: () => Promise; + getIssuer: () => Promise; + getIsExternal: () => Promise; } diff --git a/common/src/abstractions/user.service.ts b/common/src/abstractions/user.service.ts deleted file mode 100644 index 8170ae73..00000000 --- a/common/src/abstractions/user.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { OrganizationData } from '../models/data/organizationData'; -import { ProviderData } from '../models/data/providerData'; - -import { Organization } from '../models/domain/organization'; -import { Provider } from '../models/domain/provider'; - -import { KdfType } from '../enums/kdfType'; - -export abstract class UserService { - setInformation: (userId: string, email: string, kdf: KdfType, kdfIterations: number) => Promise; - setEmailVerified: (emailVerified: boolean) => Promise; - setSecurityStamp: (stamp: string) => Promise; - setForcePasswordReset: (forcePasswordReset: boolean) => Promise; - getUserId: () => Promise; - getEmail: () => Promise; - getSecurityStamp: () => Promise; - getKdf: () => Promise; - getKdfIterations: () => Promise; - getEmailVerified: () => Promise; - getForcePasswordReset: () => Promise; - clear: () => Promise; - isAuthenticated: () => Promise; - canAccessPremium: () => Promise; - canManageSponsorships: () => Promise; - getOrganization: (id: string) => Promise; - getOrganizationByIdentifier: (identifier: string) => Promise; - getAllOrganizations: () => Promise; - replaceOrganizations: (organizations: { [id: string]: OrganizationData; }) => Promise; - clearOrganizations: (userId: string) => Promise; - getProvider: (id: string) => Promise; - getAllProviders: () => Promise; - replaceProviders: (providers: { [id: string]: ProviderData; }) => Promise; - clearProviders: (userId: string) => Promise; -} diff --git a/common/src/abstractions/vaultTimeout.service.ts b/common/src/abstractions/vaultTimeout.service.ts index a5944c48..2c2b2521 100644 --- a/common/src/abstractions/vaultTimeout.service.ts +++ b/common/src/abstractions/vaultTimeout.service.ts @@ -1,16 +1,11 @@ -import { EncString } from '../models/domain/encString'; - export abstract class VaultTimeoutService { - biometricLocked: boolean; - everBeenUnlocked: boolean; - pinProtectedKey: EncString; - isLocked: () => Promise; - checkVaultTimeout: () => Promise; - lock: (allowSoftLock?: boolean) => Promise; - logOut: () => Promise; - setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise; - getVaultTimeout: () => Promise; - isPinLockSet: () => Promise<[boolean, boolean]>; - isBiometricLockSet: () => Promise; - clear: () => Promise; + isLocked: (userId?: string) => Promise; + checkVaultTimeout: () => Promise; + lock: (allowSoftLock?: boolean, userId?: string) => Promise; + logOut: (userId?: string) => Promise; + setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise; + getVaultTimeout: () => Promise; + isPinLockSet: () => Promise<[boolean, boolean]>; + isBiometricLockSet: () => Promise; + clear: (userId?: string) => Promise; } diff --git a/common/src/enums/authenticationStatus.ts b/common/src/enums/authenticationStatus.ts new file mode 100644 index 00000000..8e1db548 --- /dev/null +++ b/common/src/enums/authenticationStatus.ts @@ -0,0 +1,6 @@ +export enum AuthenticationStatus { + Locked = "locked", + Unlocked = "unlocked", + LoggedOut = "loggedOut", + Active = "active", +} diff --git a/common/src/enums/htmlStorageLocation.ts b/common/src/enums/htmlStorageLocation.ts new file mode 100644 index 00000000..19c84fe8 --- /dev/null +++ b/common/src/enums/htmlStorageLocation.ts @@ -0,0 +1,5 @@ +export enum HtmlStorageLocation { + Local = "local", + Memory = "memory", + Session = "session", +} diff --git a/common/src/enums/keySuffixOptions.ts b/common/src/enums/keySuffixOptions.ts new file mode 100644 index 00000000..2ae98d8e --- /dev/null +++ b/common/src/enums/keySuffixOptions.ts @@ -0,0 +1,4 @@ +export enum KeySuffixOptions { + Auto = "auto", + Biometric = "biometric", +} diff --git a/common/src/enums/storageLocation.ts b/common/src/enums/storageLocation.ts new file mode 100644 index 00000000..46d50d1d --- /dev/null +++ b/common/src/enums/storageLocation.ts @@ -0,0 +1,5 @@ +export enum StorageLocation { + Both = "both", + Disk = "disk", + Memory = "memory", +} diff --git a/common/src/importers/baseImporter.ts b/common/src/importers/baseImporter.ts index 194d5843..65253c71 100644 --- a/common/src/importers/baseImporter.ts +++ b/common/src/importers/baseImporter.ts @@ -1,377 +1,457 @@ -import * as papa from 'papaparse'; +import * as papa from "papaparse"; -import { LogService } from '../abstractions/log.service'; +import { LogService } from "../abstractions/log.service"; -import { ImportResult } from '../models/domain/importResult'; +import { ImportResult } from "../models/domain/importResult"; -import { CipherView } from '../models/view/cipherView'; -import { CollectionView } from '../models/view/collectionView'; -import { LoginUriView } from '../models/view/loginUriView'; +import { CipherView } from "../models/view/cipherView"; +import { CollectionView } from "../models/view/collectionView"; +import { LoginUriView } from "../models/view/loginUriView"; -import { Utils } from '../misc/utils'; +import { Utils } from "../misc/utils"; -import { FieldView } from '../models/view/fieldView'; -import { FolderView } from '../models/view/folderView'; -import { LoginView } from '../models/view/loginView'; -import { SecureNoteView } from '../models/view/secureNoteView'; +import { FieldView } from "../models/view/fieldView"; +import { FolderView } from "../models/view/folderView"; +import { LoginView } from "../models/view/loginView"; +import { SecureNoteView } from "../models/view/secureNoteView"; -import { CipherRepromptType } from '../enums/cipherRepromptType'; -import { CipherType } from '../enums/cipherType'; -import { FieldType } from '../enums/fieldType'; -import { SecureNoteType } from '../enums/secureNoteType'; +import { CipherRepromptType } from "../enums/cipherRepromptType"; +import { CipherType } from "../enums/cipherType"; +import { FieldType } from "../enums/fieldType"; +import { SecureNoteType } from "../enums/secureNoteType"; -import { ConsoleLogService } from '../services/consoleLog.service'; +import { ConsoleLogService } from "../services/consoleLog.service"; export abstract class BaseImporter { - organizationId: string = null; + organizationId: string = null; - protected logService: LogService = new ConsoleLogService(false); + protected logService: LogService = new ConsoleLogService(false); - protected newLineRegex = /(?:\r\n|\r|\n)/; + protected newLineRegex = /(?:\r\n|\r|\n)/; - protected passwordFieldNames = [ - 'password', 'pass word', 'passphrase', 'pass phrase', - 'pass', 'code', 'code word', 'codeword', - 'secret', 'secret word', 'personpwd', - 'key', 'keyword', 'key word', 'keyphrase', 'key phrase', - 'form_pw', 'wppassword', 'pin', 'pwd', 'pw', 'pword', 'passwd', - 'p', 'serial', 'serial#', 'license key', 'reg #', + protected passwordFieldNames = [ + "password", + "pass word", + "passphrase", + "pass phrase", + "pass", + "code", + "code word", + "codeword", + "secret", + "secret word", + "personpwd", + "key", + "keyword", + "key word", + "keyphrase", + "key phrase", + "form_pw", + "wppassword", + "pin", + "pwd", + "pw", + "pword", + "passwd", + "p", + "serial", + "serial#", + "license key", + "reg #", - // Non-English names - 'passwort', - ]; + // Non-English names + "passwort", + ]; - protected usernameFieldNames = [ - 'user', 'name', 'user name', 'username', 'login name', - 'email', 'e-mail', 'id', 'userid', 'user id', - 'login', 'form_loginname', 'wpname', 'mail', - 'loginid', 'login id', 'log', 'personlogin', - 'first name', 'last name', 'card#', 'account #', - 'member', 'member #', + protected usernameFieldNames = [ + "user", + "name", + "user name", + "username", + "login name", + "email", + "e-mail", + "id", + "userid", + "user id", + "login", + "form_loginname", + "wpname", + "mail", + "loginid", + "login id", + "log", + "personlogin", + "first name", + "last name", + "card#", + "account #", + "member", + "member #", - // Non-English names - 'nom', 'benutzername', - ]; + // Non-English names + "nom", + "benutzername", + ]; - protected notesFieldNames = [ - 'note', 'notes', 'comment', 'comments', 'memo', - 'description', 'free form', 'freeform', - 'free text', 'freetext', 'free', + protected notesFieldNames = [ + "note", + "notes", + "comment", + "comments", + "memo", + "description", + "free form", + "freeform", + "free text", + "freetext", + "free", - // Non-English names - 'kommentar', - ]; + // Non-English names + "kommentar", + ]; - protected uriFieldNames: string[] = [ - 'url', 'hyper link', 'hyperlink', 'link', - 'host', 'hostname', 'host name', 'server', 'address', - 'hyper ref', 'href', 'web', 'website', 'web site', 'site', - 'web-site', 'uri', + protected uriFieldNames: string[] = [ + "url", + "hyper link", + "hyperlink", + "link", + "host", + "hostname", + "host name", + "server", + "address", + "hyper ref", + "href", + "web", + "website", + "web site", + "site", + "web-site", + "uri", - // Non-English names - 'ort', 'adresse', - ]; + // Non-English names + "ort", + "adresse", + ]; - protected parseCsvOptions = { - encoding: 'UTF-8', - skipEmptyLines: false, - }; + protected parseCsvOptions = { + encoding: "UTF-8", + skipEmptyLines: false, + }; - protected get organization() { - return this.organizationId != null; + protected get organization() { + return this.organizationId != null; + } + + protected parseXml(data: string): Document { + const parser = new DOMParser(); + const doc = parser.parseFromString(data, "application/xml"); + return doc != null && doc.querySelector("parsererror") == null ? doc : null; + } + + protected parseCsv(data: string, header: boolean, options: any = {}): any[] { + const parseOptions: papa.ParseConfig = Object.assign( + { header: header }, + this.parseCsvOptions, + options + ); + data = this.splitNewLine(data).join("\n").trim(); + const result = papa.parse(data, parseOptions); + if (result.errors != null && result.errors.length > 0) { + result.errors.forEach((e) => { + if (e.row != null) { + // tslint:disable-next-line + this.logService.warning("Error parsing row " + e.row + ": " + e.message); + } + }); + } + return result.data && result.data.length > 0 ? result.data : null; + } + + protected parseSingleRowCsv(rowData: string) { + if (this.isNullOrWhitespace(rowData)) { + return null; + } + const parsedRow = this.parseCsv(rowData, false); + if (parsedRow != null && parsedRow.length > 0 && parsedRow[0].length > 0) { + return parsedRow[0]; + } + return null; + } + + protected makeUriArray(uri: string | string[]): LoginUriView[] { + if (uri == null) { + return null; } - protected parseXml(data: string): Document { - const parser = new DOMParser(); - const doc = parser.parseFromString(data, 'application/xml'); - return doc != null && doc.querySelector('parsererror') == null ? doc : null; - } - - protected parseCsv(data: string, header: boolean, options: any = {}): any[] { - const parseOptions = Object.assign({ header: header }, this.parseCsvOptions, options); - data = this.splitNewLine(data).join('\n').trim(); - const result = papa.parse(data, parseOptions); - if (result.errors != null && result.errors.length > 0) { - result.errors.forEach(e => { - if (e.row != null) { - // tslint:disable-next-line - this.logService.warning('Error parsing row ' + e.row + ': ' + e.message); - } - }); - } - return result.data && result.data.length > 0 ? result.data : null; - } - - protected parseSingleRowCsv(rowData: string) { - if (this.isNullOrWhitespace(rowData)) { - return null; - } - const parsedRow = this.parseCsv(rowData, false); - if (parsedRow != null && parsedRow.length > 0 && parsedRow[0].length > 0) { - return parsedRow[0]; - } + if (typeof uri === "string") { + const loginUri = new LoginUriView(); + loginUri.uri = this.fixUri(uri); + if (this.isNullOrWhitespace(loginUri.uri)) { return null; + } + loginUri.match = null; + return [loginUri]; } - protected makeUriArray(uri: string | string[]): LoginUriView[] { - if (uri == null) { - return null; + if (uri.length > 0) { + const returnArr: LoginUriView[] = []; + uri.forEach((u) => { + const loginUri = new LoginUriView(); + loginUri.uri = this.fixUri(u); + if (this.isNullOrWhitespace(loginUri.uri)) { + return; } - - if (typeof uri === 'string') { - const loginUri = new LoginUriView(); - loginUri.uri = this.fixUri(uri); - if (this.isNullOrWhitespace(loginUri.uri)) { - return null; - } - loginUri.match = null; - return [loginUri]; - } - - if (uri.length > 0) { - const returnArr: LoginUriView[] = []; - uri.forEach(u => { - const loginUri = new LoginUriView(); - loginUri.uri = this.fixUri(u); - if (this.isNullOrWhitespace(loginUri.uri)) { - return; - } - loginUri.match = null; - returnArr.push(loginUri); - }); - return returnArr.length === 0 ? null : returnArr; - } - - return null; + loginUri.match = null; + returnArr.push(loginUri); + }); + return returnArr.length === 0 ? null : returnArr; } - protected fixUri(uri: string) { - if (uri == null) { - return null; - } - uri = uri.trim(); - if (uri.indexOf('://') === -1 && uri.indexOf('.') >= 0) { - uri = 'http://' + uri; - } - if (uri.length > 1000) { - return uri.substring(0, 1000); - } - return uri; + return null; + } + + protected fixUri(uri: string) { + if (uri == null) { + return null; + } + uri = uri.trim(); + if (uri.indexOf("://") === -1 && uri.indexOf(".") >= 0) { + uri = "http://" + uri; + } + if (uri.length > 1000) { + return uri.substring(0, 1000); + } + return uri; + } + + protected nameFromUrl(url: string) { + const hostname = Utils.getHostname(url); + if (this.isNullOrWhitespace(hostname)) { + return null; + } + return hostname.startsWith("www.") ? hostname.replace("www.", "") : hostname; + } + + protected isNullOrWhitespace(str: string): boolean { + return Utils.isNullOrWhitespace(str); + } + + protected getValueOrDefault(str: string, defaultValue: string = null): string { + if (this.isNullOrWhitespace(str)) { + return defaultValue; + } + return str; + } + + protected splitNewLine(str: string): string[] { + return str.split(this.newLineRegex); + } + + // ref https://stackoverflow.com/a/5911300 + protected getCardBrand(cardNum: string) { + if (this.isNullOrWhitespace(cardNum)) { + return null; } - protected nameFromUrl(url: string) { - const hostname = Utils.getHostname(url); - if (this.isNullOrWhitespace(hostname)) { - return null; - } - return hostname.startsWith('www.') ? hostname.replace('www.', '') : hostname; + // Visa + let re = new RegExp("^4"); + if (cardNum.match(re) != null) { + return "Visa"; } - protected isNullOrWhitespace(str: string): boolean { - return Utils.isNullOrWhitespace(str); + // Mastercard + // Updated for Mastercard 2017 BINs expansion + if ( + /^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test( + cardNum + ) + ) { + return "Mastercard"; } - protected getValueOrDefault(str: string, defaultValue: string = null): string { - if (this.isNullOrWhitespace(str)) { - return defaultValue; - } - return str; + // AMEX + re = new RegExp("^3[47]"); + if (cardNum.match(re) != null) { + return "Amex"; } - protected splitNewLine(str: string): string[] { - return str.split(this.newLineRegex); + // Discover + re = new RegExp( + "^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)" + ); + if (cardNum.match(re) != null) { + return "Discover"; } - // ref https://stackoverflow.com/a/5911300 - protected getCardBrand(cardNum: string) { - if (this.isNullOrWhitespace(cardNum)) { - return null; - } - - // Visa - let re = new RegExp('^4'); - if (cardNum.match(re) != null) { - return 'Visa'; - } - - // Mastercard - // Updated for Mastercard 2017 BINs expansion - if (/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/ - .test(cardNum)) { - return 'Mastercard'; - } - - // AMEX - re = new RegExp('^3[47]'); - if (cardNum.match(re) != null) { - return 'Amex'; - } - - // Discover - re = new RegExp('^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)'); - if (cardNum.match(re) != null) { - return 'Discover'; - } - - // Diners - re = new RegExp('^36'); - if (cardNum.match(re) != null) { - return 'Diners Club'; - } - - // Diners - Carte Blanche - re = new RegExp('^30[0-5]'); - if (cardNum.match(re) != null) { - return 'Diners Club'; - } - - // JCB - re = new RegExp('^35(2[89]|[3-8][0-9])'); - if (cardNum.match(re) != null) { - return 'JCB'; - } - - // Visa Electron - re = new RegExp('^(4026|417500|4508|4844|491(3|7))'); - if (cardNum.match(re) != null) { - return 'Visa'; - } - - return null; + // Diners + re = new RegExp("^36"); + if (cardNum.match(re) != null) { + return "Diners Club"; } - protected setCardExpiration(cipher: CipherView, expiration: string): boolean { - if (!this.isNullOrWhitespace(expiration)) { - expiration = expiration.replace(/\s/g, ''); - const parts = expiration.split('/'); - if (parts.length === 2) { - let month: string = null; - let year: string = null; - if (parts[0].length === 1 || parts[0].length === 2) { - month = parts[0]; - if (month.length === 2 && month[0] === '0') { - month = month.substr(1, 1); - } - } - if (parts[1].length === 2 || parts[1].length === 4) { - year = month.length === 2 ? '20' + parts[1] : parts[1]; - } - if (month != null && year != null) { - cipher.card.expMonth = month; - cipher.card.expYear = year; - return true; - } - } + // Diners - Carte Blanche + re = new RegExp("^30[0-5]"); + if (cardNum.match(re) != null) { + return "Diners Club"; + } + + // JCB + re = new RegExp("^35(2[89]|[3-8][0-9])"); + if (cardNum.match(re) != null) { + return "JCB"; + } + + // Visa Electron + re = new RegExp("^(4026|417500|4508|4844|491(3|7))"); + if (cardNum.match(re) != null) { + return "Visa"; + } + + return null; + } + + protected setCardExpiration(cipher: CipherView, expiration: string): boolean { + if (!this.isNullOrWhitespace(expiration)) { + expiration = expiration.replace(/\s/g, ""); + const parts = expiration.split("/"); + if (parts.length === 2) { + let month: string = null; + let year: string = null; + if (parts[0].length === 1 || parts[0].length === 2) { + month = parts[0]; + if (month.length === 2 && month[0] === "0") { + month = month.substr(1, 1); + } } - return false; + if (parts[1].length === 2 || parts[1].length === 4) { + year = month.length === 2 ? "20" + parts[1] : parts[1]; + } + if (month != null && year != null) { + cipher.card.expMonth = month; + cipher.card.expYear = year; + return true; + } + } } + return false; + } - protected moveFoldersToCollections(result: ImportResult) { - result.folderRelationships.forEach(r => result.collectionRelationships.push(r)); - result.collections = result.folders.map(f => { - const collection = new CollectionView(); - collection.name = f.name; - return collection; - }); - result.folderRelationships = []; - result.folders = []; + protected moveFoldersToCollections(result: ImportResult) { + result.folderRelationships.forEach((r) => result.collectionRelationships.push(r)); + result.collections = result.folders.map((f) => { + const collection = new CollectionView(); + collection.name = f.name; + return collection; + }); + result.folderRelationships = []; + result.folders = []; + } + + protected querySelectorDirectChild(parentEl: Element, query: string) { + const els = this.querySelectorAllDirectChild(parentEl, query); + return els.length === 0 ? null : els[0]; + } + + protected querySelectorAllDirectChild(parentEl: Element, query: string) { + return Array.from(parentEl.querySelectorAll(query)).filter((el) => el.parentNode === parentEl); + } + + protected initLoginCipher() { + const cipher = new CipherView(); + cipher.favorite = false; + cipher.notes = ""; + cipher.fields = []; + cipher.login = new LoginView(); + cipher.type = CipherType.Login; + return cipher; + } + + protected cleanupCipher(cipher: CipherView) { + if (cipher == null) { + return; } - - protected querySelectorDirectChild(parentEl: Element, query: string) { - const els = this.querySelectorAllDirectChild(parentEl, query); - return els.length === 0 ? null : els[0]; + if (cipher.type !== CipherType.Login) { + cipher.login = null; } - - protected querySelectorAllDirectChild(parentEl: Element, query: string) { - return Array.from(parentEl.querySelectorAll(query)).filter(el => el.parentNode === parentEl); + if (this.isNullOrWhitespace(cipher.name)) { + cipher.name = "--"; } + if (this.isNullOrWhitespace(cipher.notes)) { + cipher.notes = null; + } else { + cipher.notes = cipher.notes.trim(); + } + if (cipher.fields != null && cipher.fields.length === 0) { + cipher.fields = null; + } + } - protected initLoginCipher() { - const cipher = new CipherView(); - cipher.favorite = false; - cipher.notes = ''; + protected processKvp( + cipher: CipherView, + key: string, + value: string, + type: FieldType = FieldType.Text + ) { + if (this.isNullOrWhitespace(value)) { + return; + } + if (this.isNullOrWhitespace(key)) { + key = ""; + } + if (value.length > 200 || value.trim().search(this.newLineRegex) > -1) { + if (cipher.notes == null) { + cipher.notes = ""; + } + cipher.notes += key + ": " + this.splitNewLine(value).join("\n") + "\n"; + } else { + if (cipher.fields == null) { cipher.fields = []; - cipher.login = new LoginView(); - cipher.type = CipherType.Login; - return cipher; + } + const field = new FieldView(); + field.type = type; + field.name = key; + field.value = value; + cipher.fields.push(field); + } + } + + protected processFolder(result: ImportResult, folderName: string) { + let folderIndex = result.folders.length; + const hasFolder = !this.isNullOrWhitespace(folderName); + let addFolder = hasFolder; + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === folderName) { + addFolder = false; + folderIndex = i; + break; + } + } } - protected cleanupCipher(cipher: CipherView) { - if (cipher == null) { - return; - } - if (cipher.type !== CipherType.Login) { - cipher.login = null; - } - if (this.isNullOrWhitespace(cipher.name)) { - cipher.name = '--'; - } - if (this.isNullOrWhitespace(cipher.notes)) { - cipher.notes = null; - } else { - cipher.notes = cipher.notes.trim(); - } - if (cipher.fields != null && cipher.fields.length === 0) { - cipher.fields = null; - } + if (addFolder) { + const f = new FolderView(); + f.name = folderName; + result.folders.push(f); } - - protected processKvp(cipher: CipherView, key: string, value: string, type: FieldType = FieldType.Text) { - if (this.isNullOrWhitespace(value)) { - return; - } - if (this.isNullOrWhitespace(key)) { - key = ''; - } - if (value.length > 200 || value.trim().search(this.newLineRegex) > -1) { - if (cipher.notes == null) { - cipher.notes = ''; - } - cipher.notes += (key + ': ' + this.splitNewLine(value).join('\n') + '\n'); - } else { - if (cipher.fields == null) { - cipher.fields = []; - } - const field = new FieldView(); - field.type = type; - field.name = key; - field.value = value; - cipher.fields.push(field); - } + if (hasFolder) { + result.folderRelationships.push([result.ciphers.length, folderIndex]); } + } - protected processFolder(result: ImportResult, folderName: string) { - let folderIndex = result.folders.length; - const hasFolder = !this.isNullOrWhitespace(folderName); - let addFolder = hasFolder; - - if (hasFolder) { - for (let i = 0; i < result.folders.length; i++) { - if (result.folders[i].name === folderName) { - addFolder = false; - folderIndex = i; - break; - } - } - } - - if (addFolder) { - const f = new FolderView(); - f.name = folderName; - result.folders.push(f); - } - if (hasFolder) { - result.folderRelationships.push([result.ciphers.length, folderIndex]); - } - } - - protected convertToNoteIfNeeded(cipher: CipherView) { - if (cipher.type === CipherType.Login && this.isNullOrWhitespace(cipher.login.username) && - this.isNullOrWhitespace(cipher.login.password) && - (cipher.login.uris == null || cipher.login.uris.length === 0)) { - cipher.type = CipherType.SecureNote; - cipher.secureNote = new SecureNoteView(); - cipher.secureNote.type = SecureNoteType.Generic; - } + protected convertToNoteIfNeeded(cipher: CipherView) { + if ( + cipher.type === CipherType.Login && + this.isNullOrWhitespace(cipher.login.username) && + this.isNullOrWhitespace(cipher.login.password) && + (cipher.login.uris == null || cipher.login.uris.length === 0) + ) { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; } + } } diff --git a/common/src/importers/fsecureFskImporter.ts b/common/src/importers/fsecureFskImporter.ts index 1aac8932..91564fe1 100644 --- a/common/src/importers/fsecureFskImporter.ts +++ b/common/src/importers/fsecureFskImporter.ts @@ -1,60 +1,60 @@ -import { BaseImporter } from './baseImporter'; -import { Importer } from './importer'; +import { BaseImporter } from "./baseImporter"; +import { Importer } from "./importer"; -import { ImportResult } from '../models/domain/importResult'; +import { ImportResult } from "../models/domain/importResult"; -import { CardView } from '../models/view/cardView'; +import { CardView } from "../models/view/cardView"; -import { CipherType } from '../enums/cipherType'; +import { CipherType } from "../enums/cipherType"; export class FSecureFskImporter extends BaseImporter implements Importer { - parse(data: string): Promise { - const result = new ImportResult(); - const results = JSON.parse(data); - if (results == null || results.data == null) { - result.success = false; - return Promise.resolve(result); - } - - for (const key in results.data) { - if (!results.data.hasOwnProperty(key)) { - continue; - } - - const value = results.data[key]; - const cipher = this.initLoginCipher(); - cipher.name = this.getValueOrDefault(value.service); - cipher.notes = this.getValueOrDefault(value.notes); - - if (value.style === 'website') { - cipher.login.username = this.getValueOrDefault(value.username); - cipher.login.password = this.getValueOrDefault(value.password); - cipher.login.uris = this.makeUriArray(value.url); - } else if (value.style === 'creditcard') { - cipher.type = CipherType.Card; - cipher.card = new CardView(); - cipher.card.cardholderName = this.getValueOrDefault(value.username); - cipher.card.number = this.getValueOrDefault(value.creditNumber); - cipher.card.brand = this.getCardBrand(cipher.card.number); - cipher.card.code = this.getValueOrDefault(value.creditCvv); - if (!this.isNullOrWhitespace(value.creditExpiry)) { - if (!this.setCardExpiration(cipher, value.creditExpiry)) { - this.processKvp(cipher, 'Expiration', value.creditExpiry); - } - } - if (!this.isNullOrWhitespace(value.password)) { - this.processKvp(cipher, 'PIN', value.password); - } - } else { - continue; - } - - this.convertToNoteIfNeeded(cipher); - this.cleanupCipher(cipher); - result.ciphers.push(cipher); - } - - result.success = true; - return Promise.resolve(result); + parse(data: string): Promise { + const result = new ImportResult(); + const results = JSON.parse(data); + if (results == null || results.data == null) { + result.success = false; + return Promise.resolve(result); } + + for (const key in results.data) { + if (!results.data.hasOwnProperty(key)) { + continue; + } + + const value = results.data[key]; + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.service); + cipher.notes = this.getValueOrDefault(value.notes); + + if (value.style === "website" || value.style === "globe") { + cipher.login.username = this.getValueOrDefault(value.username); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.uris = this.makeUriArray(value.url); + } else if (value.style === "creditcard") { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + cipher.card.cardholderName = this.getValueOrDefault(value.username); + cipher.card.number = this.getValueOrDefault(value.creditNumber); + cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.code = this.getValueOrDefault(value.creditCvv); + if (!this.isNullOrWhitespace(value.creditExpiry)) { + if (!this.setCardExpiration(cipher, value.creditExpiry)) { + this.processKvp(cipher, "Expiration", value.creditExpiry); + } + } + if (!this.isNullOrWhitespace(value.password)) { + this.processKvp(cipher, "PIN", value.password); + } + } else { + continue; + } + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + } + + result.success = true; + return Promise.resolve(result); + } } diff --git a/common/src/models/domain/account.ts b/common/src/models/domain/account.ts new file mode 100644 index 00000000..22ab5ddf --- /dev/null +++ b/common/src/models/domain/account.ts @@ -0,0 +1,189 @@ +import { OrganizationData } from "../data/organizationData"; + +import { AuthenticationStatus } from "../../enums/authenticationStatus"; +import { KdfType } from "../../enums/kdfType"; +import { UriMatchType } from "../../enums/uriMatchType"; + +import { CipherView } from "../view/cipherView"; +import { CollectionView } from "../view/collectionView"; +import { FolderView } from "../view/folderView"; +import { SendView } from "../view/sendView"; + +import { EncString } from "./encString"; +import { GeneratedPasswordHistory } from "./generatedPasswordHistory"; +import { Policy } from "./policy"; +import { SymmetricCryptoKey } from "./symmetricCryptoKey"; + +import { CipherData } from "../data/cipherData"; +import { CollectionData } from "../data/collectionData"; +import { EventData } from "../data/eventData"; +import { FolderData } from "../data/folderData"; +import { PolicyData } from "../data/policyData"; +import { ProviderData } from "../data/providerData"; +import { SendData } from "../data/sendData"; + +export class EncryptionPair { + encrypted?: TEncrypted; + decrypted?: TDecrypted; +} + +export class DataEncryptionPair { + encrypted?: { [id: string]: TEncrypted }; + decrypted?: TDecrypted[]; +} + +export class AccountData { + ciphers?: DataEncryptionPair = new DataEncryptionPair< + CipherData, + CipherView + >(); + folders?: DataEncryptionPair = new DataEncryptionPair< + FolderData, + FolderView + >(); + localData?: any; + sends?: DataEncryptionPair = new DataEncryptionPair(); + collections?: DataEncryptionPair = new DataEncryptionPair< + CollectionData, + CollectionView + >(); + policies?: DataEncryptionPair = new DataEncryptionPair(); + passwordGenerationHistory?: EncryptionPair< + GeneratedPasswordHistory[], + GeneratedPasswordHistory[] + > = new EncryptionPair(); + addEditCipherInfo?: any; + collapsedGroupings?: Set; + eventCollection?: EventData[]; + organizations?: { [id: string]: OrganizationData }; + providers?: { [id: string]: ProviderData }; +} + +export class AccountKeys { + cryptoMasterKey?: SymmetricCryptoKey; + cryptoMasterKeyAuto?: string; + cryptoMasterKeyB64?: string; + cryptoMasterKeyBiometric?: string; + cryptoSymmetricKey?: EncryptionPair = new EncryptionPair< + string, + SymmetricCryptoKey + >(); + organizationKeys?: EncryptionPair> = new EncryptionPair< + any, + Map + >(); + providerKeys?: EncryptionPair> = new EncryptionPair< + any, + Map + >(); + privateKey?: EncryptionPair = new EncryptionPair(); + legacyEtmKey?: SymmetricCryptoKey; + publicKey?: ArrayBuffer; + apiKeyClientSecret?: string; +} + +export class AccountProfile { + apiKeyClientId?: string; + authenticationStatus?: AuthenticationStatus; + convertAccountToKeyConnector?: boolean; + email?: string; + emailVerified?: boolean; + entityId?: string; + entityType?: string; + everBeenUnlocked?: boolean; + forcePasswordReset?: boolean; + hasPremiumPersonally?: boolean; + lastActive?: number; + lastSync?: string; + ssoCodeVerifier?: string; + ssoOrganizationIdentifier?: string; + ssoState?: string; + userId?: string; + usesKeyConnector?: boolean; + keyHash?: string; + kdfIterations?: number; + kdfType?: KdfType; +} + +export class AccountSettings { + alwaysShowDock?: boolean; + autoConfirmFingerPrints?: boolean; + autoFillOnPageLoadDefault?: boolean; + biometricLocked?: boolean; + biometricUnlock?: boolean; + clearClipboard?: number; + defaultUriMatch?: UriMatchType; + disableAddLoginNotification?: boolean; + disableAutoBiometricsPrompt?: boolean; + disableAutoTotpCopy?: boolean; + disableBadgeCounter?: boolean; + disableChangedPasswordNotification?: boolean; + disableContextMenuItem?: boolean; + disableGa?: boolean; + dontShowCardsCurrentTab?: boolean; + dontShowIdentitiesCurrentTab?: boolean; + enableAlwaysOnTop?: boolean; + enableAutoFillOnPageLoad?: boolean; + enableBiometric?: boolean; + enableBrowserIntegration?: boolean; + enableBrowserIntegrationFingerprint?: boolean; + enableCloseToTray?: boolean; + enableFullWidth?: boolean; + enableGravitars?: boolean; + enableMinimizeToTray?: boolean; + enableStartToTray?: boolean; + enableTray?: boolean; + environmentUrls?: any = { + server: "bitwarden.com", + }; + equivalentDomains?: any; + minimizeOnCopyToClipboard?: boolean; + neverDomains?: { [id: string]: any }; + openAtLogin?: boolean; + passwordGenerationOptions?: any; + pinProtected?: EncryptionPair = new EncryptionPair(); + protectedPin?: string; + settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly + vaultTimeout?: number; + vaultTimeoutAction?: string; +} + +export class AccountTokens { + accessToken?: string; + decodedToken?: any; + refreshToken?: string; + securityStamp?: string; +} + +export class Account { + data?: AccountData = new AccountData(); + keys?: AccountKeys = new AccountKeys(); + profile?: AccountProfile = new AccountProfile(); + settings?: AccountSettings = new AccountSettings(); + tokens?: AccountTokens = new AccountTokens(); + + constructor(init: Partial) { + Object.assign(this, { + data: { + ...new AccountData(), + ...init?.data, + }, + keys: { + ...new AccountKeys(), + ...init?.keys, + }, + profile: { + ...new AccountProfile(), + ...init?.profile, + }, + settings: { + ...new AccountSettings(), + ...init?.settings, + }, + tokens: { + ...new AccountTokens(), + ...init?.tokens, + }, + }); + } +} diff --git a/common/src/models/domain/globalState.ts b/common/src/models/domain/globalState.ts new file mode 100644 index 00000000..ad270bce --- /dev/null +++ b/common/src/models/domain/globalState.ts @@ -0,0 +1,24 @@ +export class GlobalState { + enableAlwaysOnTop?: boolean; + installedVersion?: string; + lastActive?: number; + locale?: string; + openAtLogin?: boolean; + organizationInvitation?: any; + rememberedEmail?: string; + theme?: string; + window?: Map = new Map(); + twoFactorToken?: string; + disableFavicon?: boolean; + biometricAwaitingAcceptance?: boolean; + biometricFingerprintValidated?: boolean; + vaultTimeout?: number; + vaultTimeoutAction?: string; + loginRedirect?: any; + mainWindowSize?: number; + enableBiometrics?: boolean; + biometricText?: string; + noAutoPromptBiometrics?: boolean; + noAutoPromptBiometricsText?: string; + stateVersion: number; +} diff --git a/common/src/models/domain/state.ts b/common/src/models/domain/state.ts new file mode 100644 index 00000000..17889bf4 --- /dev/null +++ b/common/src/models/domain/state.ts @@ -0,0 +1,8 @@ +import { Account } from "./account"; +import { GlobalState } from "./globalState"; + +export class State { + accounts: { [userId: string]: Account } = {}; + globals: GlobalState = new GlobalState(); + activeUserId: string; +} diff --git a/common/src/models/domain/storageOptions.ts b/common/src/models/domain/storageOptions.ts new file mode 100644 index 00000000..2db7e0cc --- /dev/null +++ b/common/src/models/domain/storageOptions.ts @@ -0,0 +1,10 @@ +import { HtmlStorageLocation } from "../../enums/htmlStorageLocation"; +import { StorageLocation } from "../../enums/storageLocation"; + +export type StorageOptions = { + storageLocation?: StorageLocation; + useSecureStorage?: boolean; + userId?: string; + htmlStorageLocation?: HtmlStorageLocation; + keySuffix?: string; +}; diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index 46fdc139..2764108e 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -1,1776 +1,2482 @@ -import { DeviceType } from '../enums/deviceType'; -import { PolicyType } from '../enums/policyType'; +import { DeviceType } from "../enums/deviceType"; +import { PolicyType } from "../enums/policyType"; -import { ApiService as ApiServiceAbstraction } from '../abstractions/api.service'; -import { EnvironmentService } from '../abstractions/environment.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { TokenService } from '../abstractions/token.service'; +import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service"; +import { EnvironmentService } from "../abstractions/environment.service"; +import { PlatformUtilsService } from "../abstractions/platformUtils.service"; +import { TokenService } from "../abstractions/token.service"; -import { AttachmentRequest } from '../models/request/attachmentRequest'; -import { BitPayInvoiceRequest } from '../models/request/bitPayInvoiceRequest'; -import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; -import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; -import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; -import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; -import { CipherCreateRequest } from '../models/request/cipherCreateRequest'; -import { CipherRequest } from '../models/request/cipherRequest'; -import { CipherShareRequest } from '../models/request/cipherShareRequest'; -import { CollectionRequest } from '../models/request/collectionRequest'; -import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; -import { EmailRequest } from '../models/request/emailRequest'; -import { EmailTokenRequest } from '../models/request/emailTokenRequest'; -import { EmergencyAccessAcceptRequest } from '../models/request/emergencyAccessAcceptRequest'; -import { EmergencyAccessConfirmRequest } from '../models/request/emergencyAccessConfirmRequest'; -import { EmergencyAccessInviteRequest } from '../models/request/emergencyAccessInviteRequest'; -import { EmergencyAccessPasswordRequest } from '../models/request/emergencyAccessPasswordRequest'; -import { EmergencyAccessUpdateRequest } from '../models/request/emergencyAccessUpdateRequest'; -import { EventRequest } from '../models/request/eventRequest'; -import { FolderRequest } from '../models/request/folderRequest'; -import { GroupRequest } from '../models/request/groupRequest'; -import { IapCheckRequest } from '../models/request/iapCheckRequest'; -import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; -import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; -import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; -import { KdfRequest } from '../models/request/kdfRequest'; -import { KeysRequest } from '../models/request/keysRequest'; -import { OrganizationSponsorshipCreateRequest } from '../models/request/organization/organizationSponsorshipCreateRequest'; -import { OrganizationSponsorshipRedeemRequest } from '../models/request/organization/organizationSponsorshipRedeemRequest'; -import { OrganizationSsoRequest } from '../models/request/organization/organizationSsoRequest'; -import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; -import { OrganizationImportRequest } from '../models/request/organizationImportRequest'; -import { OrganizationKeysRequest } from '../models/request/organizationKeysRequest'; -import { OrganizationSubscriptionUpdateRequest } from '../models/request/organizationSubscriptionUpdateRequest'; -import { OrganizationTaxInfoUpdateRequest } from '../models/request/organizationTaxInfoUpdateRequest'; -import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; -import { OrganizationUpgradeRequest } from '../models/request/organizationUpgradeRequest'; -import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; -import { OrganizationUserBulkConfirmRequest } from '../models/request/organizationUserBulkConfirmRequest'; -import { OrganizationUserBulkRequest } from '../models/request/organizationUserBulkRequest'; -import { OrganizationUserConfirmRequest } from '../models/request/organizationUserConfirmRequest'; -import { OrganizationUserInviteRequest } from '../models/request/organizationUserInviteRequest'; -import { OrganizationUserResetPasswordEnrollmentRequest } from '../models/request/organizationUserResetPasswordEnrollmentRequest'; -import { OrganizationUserResetPasswordRequest } from '../models/request/organizationUserResetPasswordRequest'; -import { OrganizationUserUpdateGroupsRequest } from '../models/request/organizationUserUpdateGroupsRequest'; -import { OrganizationUserUpdateRequest } from '../models/request/organizationUserUpdateRequest'; -import { PasswordHintRequest } from '../models/request/passwordHintRequest'; -import { PasswordRequest } from '../models/request/passwordRequest'; -import { PaymentRequest } from '../models/request/paymentRequest'; -import { PolicyRequest } from '../models/request/policyRequest'; -import { PreloginRequest } from '../models/request/preloginRequest'; -import { ProviderAddOrganizationRequest } from '../models/request/provider/providerAddOrganizationRequest'; -import { ProviderOrganizationCreateRequest } from '../models/request/provider/providerOrganizationCreateRequest'; -import { ProviderSetupRequest } from '../models/request/provider/providerSetupRequest'; -import { ProviderUpdateRequest } from '../models/request/provider/providerUpdateRequest'; -import { ProviderUserAcceptRequest } from '../models/request/provider/providerUserAcceptRequest'; -import { ProviderUserBulkConfirmRequest } from '../models/request/provider/providerUserBulkConfirmRequest'; -import { ProviderUserBulkRequest } from '../models/request/provider/providerUserBulkRequest'; -import { ProviderUserConfirmRequest } from '../models/request/provider/providerUserConfirmRequest'; -import { ProviderUserInviteRequest } from '../models/request/provider/providerUserInviteRequest'; -import { ProviderUserUpdateRequest } from '../models/request/provider/providerUserUpdateRequest'; -import { RegisterRequest } from '../models/request/registerRequest'; -import { SeatRequest } from '../models/request/seatRequest'; -import { SecretVerificationRequest } from '../models/request/secretVerificationRequest'; -import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; -import { SendAccessRequest } from '../models/request/sendAccessRequest'; -import { SendRequest } from '../models/request/sendRequest'; -import { SetPasswordRequest } from '../models/request/setPasswordRequest'; -import { StorageRequest } from '../models/request/storageRequest'; -import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; -import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; -import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; -import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryRequest'; -import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; -import { UpdateKeyRequest } from '../models/request/updateKeyRequest'; -import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; -import { UpdateTempPasswordRequest } from '../models/request/updateTempPasswordRequest'; -import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; -import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; -import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; -import { UpdateTwoFactorWebAuthnDeleteRequest } from '../models/request/updateTwoFactorWebAuthnDeleteRequest'; -import { UpdateTwoFactorWebAuthnRequest } from '../models/request/updateTwoFactorWebAuthnRequest'; -import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; -import { VerifyBankRequest } from '../models/request/verifyBankRequest'; -import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; -import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; +import { AttachmentRequest } from "../models/request/attachmentRequest"; +import { BitPayInvoiceRequest } from "../models/request/bitPayInvoiceRequest"; +import { CipherBulkDeleteRequest } from "../models/request/cipherBulkDeleteRequest"; +import { CipherBulkMoveRequest } from "../models/request/cipherBulkMoveRequest"; +import { CipherBulkShareRequest } from "../models/request/cipherBulkShareRequest"; +import { CipherCollectionsRequest } from "../models/request/cipherCollectionsRequest"; +import { CipherCreateRequest } from "../models/request/cipherCreateRequest"; +import { CipherRequest } from "../models/request/cipherRequest"; +import { CipherShareRequest } from "../models/request/cipherShareRequest"; +import { CollectionRequest } from "../models/request/collectionRequest"; +import { DeleteRecoverRequest } from "../models/request/deleteRecoverRequest"; +import { EmailRequest } from "../models/request/emailRequest"; +import { EmailTokenRequest } from "../models/request/emailTokenRequest"; +import { EmergencyAccessAcceptRequest } from "../models/request/emergencyAccessAcceptRequest"; +import { EmergencyAccessConfirmRequest } from "../models/request/emergencyAccessConfirmRequest"; +import { EmergencyAccessInviteRequest } from "../models/request/emergencyAccessInviteRequest"; +import { EmergencyAccessPasswordRequest } from "../models/request/emergencyAccessPasswordRequest"; +import { EmergencyAccessUpdateRequest } from "../models/request/emergencyAccessUpdateRequest"; +import { EventRequest } from "../models/request/eventRequest"; +import { FolderRequest } from "../models/request/folderRequest"; +import { GroupRequest } from "../models/request/groupRequest"; +import { IapCheckRequest } from "../models/request/iapCheckRequest"; +import { ImportCiphersRequest } from "../models/request/importCiphersRequest"; +import { ImportDirectoryRequest } from "../models/request/importDirectoryRequest"; +import { ImportOrganizationCiphersRequest } from "../models/request/importOrganizationCiphersRequest"; +import { KdfRequest } from "../models/request/kdfRequest"; +import { KeysRequest } from "../models/request/keysRequest"; +import { OrganizationSponsorshipCreateRequest } from "../models/request/organization/organizationSponsorshipCreateRequest"; +import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organizationSponsorshipRedeemRequest"; +import { OrganizationSsoRequest } from "../models/request/organization/organizationSsoRequest"; +import { OrganizationCreateRequest } from "../models/request/organizationCreateRequest"; +import { OrganizationImportRequest } from "../models/request/organizationImportRequest"; +import { OrganizationKeysRequest } from "../models/request/organizationKeysRequest"; +import { OrganizationSubscriptionUpdateRequest } from "../models/request/organizationSubscriptionUpdateRequest"; +import { OrganizationTaxInfoUpdateRequest } from "../models/request/organizationTaxInfoUpdateRequest"; +import { OrganizationUpdateRequest } from "../models/request/organizationUpdateRequest"; +import { OrganizationUpgradeRequest } from "../models/request/organizationUpgradeRequest"; +import { OrganizationUserAcceptRequest } from "../models/request/organizationUserAcceptRequest"; +import { OrganizationUserBulkConfirmRequest } from "../models/request/organizationUserBulkConfirmRequest"; +import { OrganizationUserBulkRequest } from "../models/request/organizationUserBulkRequest"; +import { OrganizationUserConfirmRequest } from "../models/request/organizationUserConfirmRequest"; +import { OrganizationUserInviteRequest } from "../models/request/organizationUserInviteRequest"; +import { OrganizationUserResetPasswordEnrollmentRequest } from "../models/request/organizationUserResetPasswordEnrollmentRequest"; +import { OrganizationUserResetPasswordRequest } from "../models/request/organizationUserResetPasswordRequest"; +import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizationUserUpdateGroupsRequest"; +import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest"; +import { PasswordHintRequest } from "../models/request/passwordHintRequest"; +import { PasswordRequest } from "../models/request/passwordRequest"; +import { PaymentRequest } from "../models/request/paymentRequest"; +import { PolicyRequest } from "../models/request/policyRequest"; +import { PreloginRequest } from "../models/request/preloginRequest"; +import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest"; +import { ProviderOrganizationCreateRequest } from "../models/request/provider/providerOrganizationCreateRequest"; +import { ProviderSetupRequest } from "../models/request/provider/providerSetupRequest"; +import { ProviderUpdateRequest } from "../models/request/provider/providerUpdateRequest"; +import { ProviderUserAcceptRequest } from "../models/request/provider/providerUserAcceptRequest"; +import { ProviderUserBulkConfirmRequest } from "../models/request/provider/providerUserBulkConfirmRequest"; +import { ProviderUserBulkRequest } from "../models/request/provider/providerUserBulkRequest"; +import { ProviderUserConfirmRequest } from "../models/request/provider/providerUserConfirmRequest"; +import { ProviderUserInviteRequest } from "../models/request/provider/providerUserInviteRequest"; +import { ProviderUserUpdateRequest } from "../models/request/provider/providerUserUpdateRequest"; +import { RegisterRequest } from "../models/request/registerRequest"; +import { SeatRequest } from "../models/request/seatRequest"; +import { SecretVerificationRequest } from "../models/request/secretVerificationRequest"; +import { SelectionReadOnlyRequest } from "../models/request/selectionReadOnlyRequest"; +import { SendAccessRequest } from "../models/request/sendAccessRequest"; +import { SendRequest } from "../models/request/sendRequest"; +import { SetPasswordRequest } from "../models/request/setPasswordRequest"; +import { StorageRequest } from "../models/request/storageRequest"; +import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest"; +import { TokenRequest } from "../models/request/tokenRequest"; +import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest"; +import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest"; +import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest"; +import { UpdateDomainsRequest } from "../models/request/updateDomainsRequest"; +import { UpdateKeyRequest } from "../models/request/updateKeyRequest"; +import { UpdateProfileRequest } from "../models/request/updateProfileRequest"; +import { UpdateTempPasswordRequest } from "../models/request/updateTempPasswordRequest"; +import { UpdateTwoFactorAuthenticatorRequest } from "../models/request/updateTwoFactorAuthenticatorRequest"; +import { UpdateTwoFactorDuoRequest } from "../models/request/updateTwoFactorDuoRequest"; +import { UpdateTwoFactorEmailRequest } from "../models/request/updateTwoFactorEmailRequest"; +import { UpdateTwoFactorWebAuthnDeleteRequest } from "../models/request/updateTwoFactorWebAuthnDeleteRequest"; +import { UpdateTwoFactorWebAuthnRequest } from "../models/request/updateTwoFactorWebAuthnRequest"; +import { UpdateTwoFactorYubioOtpRequest } from "../models/request/updateTwoFactorYubioOtpRequest"; +import { VerifyBankRequest } from "../models/request/verifyBankRequest"; +import { VerifyDeleteRecoverRequest } from "../models/request/verifyDeleteRecoverRequest"; +import { VerifyEmailRequest } from "../models/request/verifyEmailRequest"; -import { Utils } from '../misc/utils'; +import { Utils } from "../misc/utils"; -import { ApiKeyResponse } from '../models/response/apiKeyResponse'; -import { AttachmentResponse } from '../models/response/attachmentResponse'; -import { AttachmentUploadDataResponse } from '../models/response/attachmentUploadDataResponse'; -import { BillingResponse } from '../models/response/billingResponse'; -import { BreachAccountResponse } from '../models/response/breachAccountResponse'; -import { CipherResponse } from '../models/response/cipherResponse'; +import { ApiKeyResponse } from "../models/response/apiKeyResponse"; +import { AttachmentResponse } from "../models/response/attachmentResponse"; +import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse"; +import { BillingResponse } from "../models/response/billingResponse"; +import { BreachAccountResponse } from "../models/response/breachAccountResponse"; +import { CipherResponse } from "../models/response/cipherResponse"; import { - CollectionGroupDetailsResponse, - CollectionResponse, -} from '../models/response/collectionResponse'; -import { DomainsResponse } from '../models/response/domainsResponse'; + CollectionGroupDetailsResponse, + CollectionResponse, +} from "../models/response/collectionResponse"; +import { DomainsResponse } from "../models/response/domainsResponse"; import { - EmergencyAccessGranteeDetailsResponse, - EmergencyAccessGrantorDetailsResponse, - EmergencyAccessTakeoverResponse, - EmergencyAccessViewResponse -} from '../models/response/emergencyAccessResponse'; -import { ErrorResponse } from '../models/response/errorResponse'; -import { EventResponse } from '../models/response/eventResponse'; -import { FolderResponse } from '../models/response/folderResponse'; + EmergencyAccessGranteeDetailsResponse, + EmergencyAccessGrantorDetailsResponse, + EmergencyAccessTakeoverResponse, + EmergencyAccessViewResponse, +} from "../models/response/emergencyAccessResponse"; +import { ErrorResponse } from "../models/response/errorResponse"; +import { EventResponse } from "../models/response/eventResponse"; +import { FolderResponse } from "../models/response/folderResponse"; +import { GroupDetailsResponse, GroupResponse } from "../models/response/groupResponse"; +import { IdentityCaptchaResponse } from "../models/response/identityCaptchaResponse"; +import { IdentityTokenResponse } from "../models/response/identityTokenResponse"; +import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse"; +import { ListResponse } from "../models/response/listResponse"; +import { OrganizationSsoResponse } from "../models/response/organization/organizationSsoResponse"; +import { OrganizationAutoEnrollStatusResponse } from "../models/response/organizationAutoEnrollStatusResponse"; +import { OrganizationKeysResponse } from "../models/response/organizationKeysResponse"; +import { OrganizationResponse } from "../models/response/organizationResponse"; +import { OrganizationSubscriptionResponse } from "../models/response/organizationSubscriptionResponse"; +import { OrganizationUserBulkPublicKeyResponse } from "../models/response/organizationUserBulkPublicKeyResponse"; +import { OrganizationUserBulkResponse } from "../models/response/organizationUserBulkResponse"; import { - GroupDetailsResponse, - GroupResponse, -} from '../models/response/groupResponse'; -import { IdentityCaptchaResponse } from '../models/response/identityCaptchaResponse'; -import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; -import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; -import { ListResponse } from '../models/response/listResponse'; -import { OrganizationSsoResponse } from '../models/response/organization/organizationSsoResponse'; -import { OrganizationAutoEnrollStatusResponse } from '../models/response/organizationAutoEnrollStatusResponse'; -import { OrganizationKeysResponse } from '../models/response/organizationKeysResponse'; -import { OrganizationResponse } from '../models/response/organizationResponse'; -import { OrganizationSubscriptionResponse } from '../models/response/organizationSubscriptionResponse'; -import { OrganizationUserBulkPublicKeyResponse } from '../models/response/organizationUserBulkPublicKeyResponse'; -import { OrganizationUserBulkResponse } from '../models/response/organizationUserBulkResponse'; + OrganizationUserDetailsResponse, + OrganizationUserResetPasswordDetailsReponse, + OrganizationUserUserDetailsResponse, +} from "../models/response/organizationUserResponse"; +import { PaymentResponse } from "../models/response/paymentResponse"; +import { PlanResponse } from "../models/response/planResponse"; +import { PolicyResponse } from "../models/response/policyResponse"; +import { PreloginResponse } from "../models/response/preloginResponse"; +import { ProfileResponse } from "../models/response/profileResponse"; import { - OrganizationUserDetailsResponse, - OrganizationUserResetPasswordDetailsReponse, - OrganizationUserUserDetailsResponse, -} from '../models/response/organizationUserResponse'; -import { PaymentResponse } from '../models/response/paymentResponse'; -import { PlanResponse } from '../models/response/planResponse'; -import { PolicyResponse } from '../models/response/policyResponse'; -import { PreloginResponse } from '../models/response/preloginResponse'; -import { ProfileResponse } from '../models/response/profileResponse'; -import { ProviderOrganizationOrganizationDetailsResponse, ProviderOrganizationResponse } from '../models/response/provider/providerOrganizationResponse'; -import { ProviderResponse } from '../models/response/provider/providerResponse'; -import { ProviderUserBulkPublicKeyResponse } from '../models/response/provider/providerUserBulkPublicKeyResponse'; -import { ProviderUserBulkResponse } from '../models/response/provider/providerUserBulkResponse'; + ProviderOrganizationOrganizationDetailsResponse, + ProviderOrganizationResponse, +} from "../models/response/provider/providerOrganizationResponse"; +import { ProviderResponse } from "../models/response/provider/providerResponse"; +import { ProviderUserBulkPublicKeyResponse } from "../models/response/provider/providerUserBulkPublicKeyResponse"; +import { ProviderUserBulkResponse } from "../models/response/provider/providerUserBulkResponse"; import { - ProviderUserResponse, - ProviderUserUserDetailsResponse -} from '../models/response/provider/providerUserResponse'; -import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; -import { SendAccessResponse } from '../models/response/sendAccessResponse'; -import { SendFileDownloadDataResponse } from '../models/response/sendFileDownloadDataResponse'; -import { SendFileUploadDataResponse } from '../models/response/sendFileUploadDataResponse'; -import { SendResponse } from '../models/response/sendResponse'; -import { SubscriptionResponse } from '../models/response/subscriptionResponse'; -import { SyncResponse } from '../models/response/syncResponse'; -import { TaxInfoResponse } from '../models/response/taxInfoResponse'; -import { TaxRateResponse } from '../models/response/taxRateResponse'; -import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; -import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; -import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; -import { TwoFactorProviderResponse } from '../models/response/twoFactorProviderResponse'; -import { TwoFactorRecoverResponse } from '../models/response/twoFactorRescoverResponse'; -import { TwoFactorWebAuthnResponse } from '../models/response/twoFactorWebAuthnResponse'; -import { ChallengeResponse } from '../models/response/twoFactorWebAuthnResponse'; -import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse'; -import { UserKeyResponse } from '../models/response/userKeyResponse'; - -import { SetKeyConnectorKeyRequest } from '../models/request/account/setKeyConnectorKeyRequest'; -import { VerifyOTPRequest } from '../models/request/account/verifyOTPRequest'; -import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; -import { KeyConnectorUserKeyResponse } from '../models/response/keyConnectorUserKeyResponse'; -import { SendAccessView } from '../models/view/sendAccessView'; - + ProviderUserResponse, + ProviderUserUserDetailsResponse, +} from "../models/response/provider/providerUserResponse"; +import { SelectionReadOnlyResponse } from "../models/response/selectionReadOnlyResponse"; +import { SendAccessResponse } from "../models/response/sendAccessResponse"; +import { SendFileDownloadDataResponse } from "../models/response/sendFileDownloadDataResponse"; +import { SendFileUploadDataResponse } from "../models/response/sendFileUploadDataResponse"; +import { SendResponse } from "../models/response/sendResponse"; +import { SubscriptionResponse } from "../models/response/subscriptionResponse"; +import { SyncResponse } from "../models/response/syncResponse"; +import { TaxInfoResponse } from "../models/response/taxInfoResponse"; +import { TaxRateResponse } from "../models/response/taxRateResponse"; +import { TwoFactorAuthenticatorResponse } from "../models/response/twoFactorAuthenticatorResponse"; +import { TwoFactorDuoResponse } from "../models/response/twoFactorDuoResponse"; +import { TwoFactorEmailResponse } from "../models/response/twoFactorEmailResponse"; +import { TwoFactorProviderResponse } from "../models/response/twoFactorProviderResponse"; +import { TwoFactorRecoverResponse } from "../models/response/twoFactorRescoverResponse"; +import { TwoFactorWebAuthnResponse } from "../models/response/twoFactorWebAuthnResponse"; +import { ChallengeResponse } from "../models/response/twoFactorWebAuthnResponse"; +import { TwoFactorYubiKeyResponse } from "../models/response/twoFactorYubiKeyResponse"; +import { UserKeyResponse } from "../models/response/userKeyResponse"; +import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest"; +import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest"; +import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest"; +import { KeyConnectorUserKeyResponse } from "../models/response/keyConnectorUserKeyResponse"; +import { SendAccessView } from "../models/view/sendAccessView"; export class ApiService implements ApiServiceAbstraction { - protected apiKeyRefresh: (clientId: string, clientSecret: string) => Promise; - private device: DeviceType; - private deviceType: string; - private isWebClient = false; - private isDesktopClient = false; - - constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, - private environmentService: EnvironmentService, private logoutCallback: (expired: boolean) => Promise, - private customUserAgent: string = null) { - this.device = platformUtilsService.getDevice(); - this.deviceType = this.device.toString(); - this.isWebClient = this.device === DeviceType.IEBrowser || this.device === DeviceType.ChromeBrowser || - this.device === DeviceType.EdgeBrowser || this.device === DeviceType.FirefoxBrowser || - this.device === DeviceType.OperaBrowser || this.device === DeviceType.SafariBrowser || - this.device === DeviceType.UnknownBrowser || this.device === DeviceType.VivaldiBrowser; - this.isDesktopClient = this.device === DeviceType.WindowsDesktop || this.device === DeviceType.MacOsDesktop || - this.device === DeviceType.LinuxDesktop; - } - - // Auth APIs - - async postIdentityToken(request: TokenRequest): Promise { - const headers = new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Accept': 'application/json', - 'Device-Type': this.deviceType, - }); - if (this.customUserAgent != null) { - headers.set('User-Agent', this.customUserAgent); - } - request.alterIdentityTokenHeaders(headers); - const response = await this.fetch(new Request(this.environmentService.getIdentityUrl() + '/connect/token', { - body: this.qsStringify(request.toIdentityToken(request.clientId ?? this.platformUtilsService.identityClientId)), - credentials: this.getCredentials(), - cache: 'no-store', - headers: headers, - method: 'POST', - })); - - let responseJson: any = null; - if (this.isJsonResponse(response)) { - responseJson = await response.json(); - } - - if (responseJson != null) { - if (response.status === 200) { - return new IdentityTokenResponse(responseJson); - } else if (response.status === 400 && responseJson.TwoFactorProviders2 && - Object.keys(responseJson.TwoFactorProviders2).length) { - await this.tokenService.clearTwoFactorToken(request.email); - return new IdentityTwoFactorResponse(responseJson); - } else if (response.status === 400 && responseJson.HCaptcha_SiteKey && - Object.keys(responseJson.HCaptcha_SiteKey).length) { - return new IdentityCaptchaResponse(responseJson); - } - } - - return Promise.reject(new ErrorResponse(responseJson, response.status, true)); - } - - async refreshIdentityToken(): Promise { - try { - await this.doAuthRefresh(); - } catch (e) { - return Promise.reject(null); - } - } - - // Account APIs - - async getProfile(): Promise { - const r = await this.send('GET', '/accounts/profile', null, true, true); - return new ProfileResponse(r); - } - - async getUserBilling(): Promise { - const r = await this.send('GET', '/accounts/billing', null, true, true); - return new BillingResponse(r); - } - - async getUserSubscription(): Promise { - const r = await this.send('GET', '/accounts/subscription', null, true, true); - return new SubscriptionResponse(r); - } - - async getTaxInfo(): Promise { - const r = await this.send('GET', '/accounts/tax', null, true, true); - return new TaxInfoResponse(r); - } - - async putProfile(request: UpdateProfileRequest): Promise { - const r = await this.send('PUT', '/accounts/profile', request, true, true); - return new ProfileResponse(r); - } - - putTaxInfo(request: TaxInfoUpdateRequest): Promise { - return this.send('PUT', '/accounts/tax', request, true, false); - } - - async postPrelogin(request: PreloginRequest): Promise { - const r = await this.send('POST', '/accounts/prelogin', request, false, true); - return new PreloginResponse(r); - } - - postEmailToken(request: EmailTokenRequest): Promise { - return this.send('POST', '/accounts/email-token', request, true, false); - } - - postEmail(request: EmailRequest): Promise { - return this.send('POST', '/accounts/email', request, true, false); - } - - postPassword(request: PasswordRequest): Promise { - return this.send('POST', '/accounts/password', request, true, false); - } - - setPassword(request: SetPasswordRequest): Promise { - return this.send('POST', '/accounts/set-password', request, true, false); - } - - postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise { - return this.send('POST', '/accounts/set-key-connector-key', request, true, false); - } - - postSecurityStamp(request: SecretVerificationRequest): Promise { - return this.send('POST', '/accounts/security-stamp', request, true, false); - } - - deleteAccount(request: SecretVerificationRequest): Promise { - return this.send('DELETE', '/accounts', request, true, false); - } - - async getAccountRevisionDate(): Promise { - const r = await this.send('GET', '/accounts/revision-date', null, true, true); - return r as number; - } - - postPasswordHint(request: PasswordHintRequest): Promise { - return this.send('POST', '/accounts/password-hint', request, false, false); - } - - postRegister(request: RegisterRequest): Promise { - return this.send('POST', '/accounts/register', request, false, false); - } - - async postPremium(data: FormData): Promise { - const r = await this.send('POST', '/accounts/premium', data, true, true); - return new PaymentResponse(r); - } - - async postIapCheck(request: IapCheckRequest): Promise { - return this.send('POST', '/accounts/iap-check', request, true, false); - } - - postReinstatePremium(): Promise { - return this.send('POST', '/accounts/reinstate-premium', null, true, false); - } - - postCancelPremium(): Promise { - return this.send('POST', '/accounts/cancel-premium', null, true, false); - } - - async postAccountStorage(request: StorageRequest): Promise { - const r = await this.send('POST', '/accounts/storage', request, true, true); - return new PaymentResponse(r); - } - - postAccountPayment(request: PaymentRequest): Promise { - return this.send('POST', '/accounts/payment', request, true, false); - } - - postAccountLicense(data: FormData): Promise { - return this.send('POST', '/accounts/license', data, true, false); - } - - postAccountKeys(request: KeysRequest): Promise { - return this.send('POST', '/accounts/keys', request, true, false); - } - - postAccountKey(request: UpdateKeyRequest): Promise { - return this.send('POST', '/accounts/key', request, true, false); - } - - postAccountVerifyEmail(): Promise { - return this.send('POST', '/accounts/verify-email', null, true, false); - } - - postAccountVerifyEmailToken(request: VerifyEmailRequest): Promise { - return this.send('POST', '/accounts/verify-email-token', request, false, false); - } - - postAccountVerifyPassword(request: SecretVerificationRequest): Promise { - return this.send('POST', '/accounts/verify-password', request, true, false); - } - - postAccountRecoverDelete(request: DeleteRecoverRequest): Promise { - return this.send('POST', '/accounts/delete-recover', request, false, false); - } - - postAccountRecoverDeleteToken(request: VerifyDeleteRecoverRequest): Promise { - return this.send('POST', '/accounts/delete-recover-token', request, false, false); - } - - postAccountKdf(request: KdfRequest): Promise { - return this.send('POST', '/accounts/kdf', request, true, false); - } - - async deleteSsoUser(organizationId: string): Promise { - return this.send('DELETE', '/accounts/sso/' + organizationId, null, true, false); - } - - async getSsoUserIdentifier(): Promise { - return this.send('GET', '/accounts/sso/user-identifier', null, true, true); - } - - async postUserApiKey(id: string, request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/accounts/api-key', request, true, true); - return new ApiKeyResponse(r); - } - - async postUserRotateApiKey(id: string, request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/accounts/rotate-api-key', request, true, true); - return new ApiKeyResponse(r); - } - - putUpdateTempPassword(request: UpdateTempPasswordRequest): Promise { - return this.send('PUT', '/accounts/update-temp-password', request, true, false); - } - - postAccountRequestOTP(): Promise { - return this.send('POST', '/accounts/request-otp', null, true, false); - } - - postAccountVerifyOTP(request: VerifyOTPRequest): Promise { - return this.send('POST', '/accounts/verify-otp', request, true, false); - } - - postConvertToKeyConnector(): Promise { - return this.send('POST', '/accounts/convert-to-key-connector', null, true, false); - } - - // Folder APIs - - async getFolder(id: string): Promise { - const r = await this.send('GET', '/folders/' + id, null, true, true); - return new FolderResponse(r); - } - - async postFolder(request: FolderRequest): Promise { - const r = await this.send('POST', '/folders', request, true, true); - return new FolderResponse(r); - } - - async putFolder(id: string, request: FolderRequest): Promise { - const r = await this.send('PUT', '/folders/' + id, request, true, true); - return new FolderResponse(r); - } - - deleteFolder(id: string): Promise { - return this.send('DELETE', '/folders/' + id, null, true, false); - } - - // Send APIs - - async getSend(id: string): Promise { - const r = await this.send('GET', '/sends/' + id, null, true, true); - return new SendResponse(r); - } - - async postSendAccess(id: string, request: SendAccessRequest, apiUrl?: string): Promise { - const addSendIdHeader = (headers: Headers) => { - headers.set('Send-Id', id); - }; - const r = await this.send('POST', '/sends/access/' + id, request, false, true, apiUrl, addSendIdHeader); - return new SendAccessResponse(r); - } - - async getSendFileDownloadData(send: SendAccessView, request: SendAccessRequest, apiUrl?: string): Promise { - const addSendIdHeader = (headers: Headers) => { - headers.set('Send-Id', send.id); - }; - const r = await this.send('POST', '/sends/' + send.id + '/access/file/' + send.file.id, request, false, true, - apiUrl, addSendIdHeader); - return new SendFileDownloadDataResponse(r); - } - - async getSends(): Promise> { - const r = await this.send('GET', '/sends', null, true, true); - return new ListResponse(r, SendResponse); - } - - async postSend(request: SendRequest): Promise { - const r = await this.send('POST', '/sends', request, true, true); - return new SendResponse(r); - } - - async postFileTypeSend(request: SendRequest): Promise { - const r = await this.send('POST', '/sends/file/v2', request, true, true); - return new SendFileUploadDataResponse(r); - } - - async renewSendFileUploadUrl(sendId: string, fileId: string): Promise { - const r = await this.send('GET', '/sends/' + sendId + '/file/' + fileId, null, true, true); - return new SendFileUploadDataResponse(r); - } - - postSendFile(sendId: string, fileId: string, data: FormData): Promise { - return this.send('POST', '/sends/' + sendId + '/file/' + fileId, data, true, false); - } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async postSendFileLegacy(data: FormData): Promise { - const r = await this.send('POST', '/sends/file', data, true, true); - return new SendResponse(r); - } - - async putSend(id: string, request: SendRequest): Promise { - const r = await this.send('PUT', '/sends/' + id, request, true, true); - return new SendResponse(r); - } - - async putSendRemovePassword(id: string): Promise { - const r = await this.send('PUT', '/sends/' + id + '/remove-password', null, true, true); - return new SendResponse(r); - } - - deleteSend(id: string): Promise { - return this.send('DELETE', '/sends/' + id, null, true, false); - } - - // Cipher APIs - - async getCipher(id: string): Promise { - const r = await this.send('GET', '/ciphers/' + id, null, true, true); - return new CipherResponse(r); - } - - async getCipherAdmin(id: string): Promise { - const r = await this.send('GET', '/ciphers/' + id + '/admin', null, true, true); - return new CipherResponse(r); - } - - async getCiphersOrganization(organizationId: string): Promise> { - const r = await this.send('GET', '/ciphers/organization-details?organizationId=' + organizationId, - null, true, true); - return new ListResponse(r, CipherResponse); - } - - async postCipher(request: CipherRequest): Promise { - const r = await this.send('POST', '/ciphers', request, true, true); - return new CipherResponse(r); - } - - async postCipherCreate(request: CipherCreateRequest): Promise { - const r = await this.send('POST', '/ciphers/create', request, true, true); - return new CipherResponse(r); - } - - async postCipherAdmin(request: CipherCreateRequest): Promise { - const r = await this.send('POST', '/ciphers/admin', request, true, true); - return new CipherResponse(r); - } - - async putCipher(id: string, request: CipherRequest): Promise { - const r = await this.send('PUT', '/ciphers/' + id, request, true, true); - return new CipherResponse(r); - } - - async putCipherAdmin(id: string, request: CipherRequest): Promise { - const r = await this.send('PUT', '/ciphers/' + id + '/admin', request, true, true); - return new CipherResponse(r); - } - - deleteCipher(id: string): Promise { - return this.send('DELETE', '/ciphers/' + id, null, true, false); - } - - deleteCipherAdmin(id: string): Promise { - return this.send('DELETE', '/ciphers/' + id + '/admin', null, true, false); - } - - deleteManyCiphers(request: CipherBulkDeleteRequest): Promise { - return this.send('DELETE', '/ciphers', request, true, false); - } - - deleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise { - return this.send('DELETE', '/ciphers/admin', request, true, false); - } - - putMoveCiphers(request: CipherBulkMoveRequest): Promise { - return this.send('PUT', '/ciphers/move', request, true, false); - } - - async putShareCipher(id: string, request: CipherShareRequest): Promise { - const r = await this.send('PUT', '/ciphers/' + id + '/share', request, true, true); - return new CipherResponse(r); - } - - putShareCiphers(request: CipherBulkShareRequest): Promise { - return this.send('PUT', '/ciphers/share', request, true, false); - } - - putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { - return this.send('PUT', '/ciphers/' + id + '/collections', request, true, false); - } - - putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise { - return this.send('PUT', '/ciphers/' + id + '/collections-admin', request, true, false); - } - - postPurgeCiphers(request: SecretVerificationRequest, organizationId: string = null): Promise { - let path = '/ciphers/purge'; - if (organizationId != null) { - path += '?organizationId=' + organizationId; - } - return this.send('POST', path, request, true, false); - } - - postImportCiphers(request: ImportCiphersRequest): Promise { - return this.send('POST', '/ciphers/import', request, true, false); - } - - postImportOrganizationCiphers(organizationId: string, request: ImportOrganizationCiphersRequest): Promise { - return this.send('POST', '/ciphers/import-organization?organizationId=' + organizationId, request, true, false); - } - - putDeleteCipher(id: string): Promise { - return this.send('PUT', '/ciphers/' + id + '/delete', null, true, false); - } - - putDeleteCipherAdmin(id: string): Promise { - return this.send('PUT', '/ciphers/' + id + '/delete-admin', null, true, false); - } - - putDeleteManyCiphers(request: CipherBulkDeleteRequest): Promise { - return this.send('PUT', '/ciphers/delete', request, true, false); - } - - putDeleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise { - return this.send('PUT', '/ciphers/delete-admin', request, true, false); - } - - async putRestoreCipher(id: string): Promise { - const r = await this.send('PUT', '/ciphers/' + id + '/restore', null, true, true); - return new CipherResponse(r); - } - - async putRestoreCipherAdmin(id: string): Promise { - const r = await this.send('PUT', '/ciphers/' + id + '/restore-admin', null, true, true); - return new CipherResponse(r); - } - - async putRestoreManyCiphers(request: CipherBulkDeleteRequest): Promise> { - const r = await this.send('PUT', '/ciphers/restore', request, true, true); - return new ListResponse(r, CipherResponse); - } - - // Attachments APIs - - async getAttachmentData(cipherId: string, attachmentId: string, emergencyAccessId?: string): Promise { - const path = (emergencyAccessId != null ? - '/emergency-access/' + emergencyAccessId + '/' : - '/ciphers/') + cipherId + '/attachment/' + attachmentId; - const r = await this.send('GET', path, null, true, true); - return new AttachmentResponse(r); - } - - async postCipherAttachment(id: string, request: AttachmentRequest): Promise { - const r = await this.send('POST', '/ciphers/' + id + '/attachment/v2', request, true, true); - return new AttachmentUploadDataResponse(r); - } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async postCipherAttachmentLegacy(id: string, data: FormData): Promise { - const r = await this.send('POST', '/ciphers/' + id + '/attachment', data, true, true); - return new CipherResponse(r); - } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async postCipherAttachmentAdminLegacy(id: string, data: FormData): Promise { - const r = await this.send('POST', '/ciphers/' + id + '/attachment-admin', data, true, true); - return new CipherResponse(r); - } - - deleteCipherAttachment(id: string, attachmentId: string): Promise { - return this.send('DELETE', '/ciphers/' + id + '/attachment/' + attachmentId, null, true, false); - } - - deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise { - return this.send('DELETE', '/ciphers/' + id + '/attachment/' + attachmentId + '/admin', null, true, false); - } - - postShareCipherAttachment(id: string, attachmentId: string, data: FormData, - organizationId: string): Promise { - return this.send('POST', '/ciphers/' + id + '/attachment/' + - attachmentId + '/share?organizationId=' + organizationId, data, true, false); - } - - async renewAttachmentUploadUrl(id: string, attachmentId: string): Promise { - const r = await this.send('GET', '/ciphers/' + id + '/attachment/' + attachmentId + '/renew', null, true, true); - return new AttachmentUploadDataResponse(r); - } - - postAttachmentFile(id: string, attachmentId: string, data: FormData): Promise { - return this.send('POST', '/ciphers/' + id + '/attachment/' + attachmentId, data, true, false); - } - - // Collections APIs - - async getCollectionDetails(organizationId: string, id: string): Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/collections/' + id + '/details', - null, true, true); - return new CollectionGroupDetailsResponse(r); - } - - async getUserCollections(): Promise> { - const r = await this.send('GET', '/collections', null, true, true); - return new ListResponse(r, CollectionResponse); - } - - async getCollections(organizationId: string): Promise> { - const r = await this.send('GET', '/organizations/' + organizationId + '/collections', null, true, true); - return new ListResponse(r, CollectionResponse); - } - - async getCollectionUsers(organizationId: string, id: string): Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/collections/' + id + '/users', - null, true, true); - return r.map((dr: any) => new SelectionReadOnlyResponse(dr)); - } - - async postCollection(organizationId: string, request: CollectionRequest): Promise { - const r = await this.send('POST', '/organizations/' + organizationId + '/collections', request, true, true); - return new CollectionResponse(r); - } - - async putCollection(organizationId: string, id: string, request: CollectionRequest): Promise { - const r = await this.send('PUT', '/organizations/' + organizationId + '/collections/' + id, - request, true, true); - return new CollectionResponse(r); - } - - async putCollectionUsers(organizationId: string, id: string, request: SelectionReadOnlyRequest[]): Promise { - await this.send('PUT', '/organizations/' + organizationId + '/collections/' + id + '/users', - request, true, false); - } - - deleteCollection(organizationId: string, id: string): Promise { - return this.send('DELETE', '/organizations/' + organizationId + '/collections/' + id, null, true, false); - } - - deleteCollectionUser(organizationId: string, id: string, organizationUserId: string): Promise { - return this.send('DELETE', - '/organizations/' + organizationId + '/collections/' + id + '/user/' + organizationUserId, - null, true, false); - } - - // Groups APIs - - async getGroupDetails(organizationId: string, id: string): Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/groups/' + id + '/details', - null, true, true); - return new GroupDetailsResponse(r); - } - - async getGroups(organizationId: string): Promise> { - const r = await this.send('GET', '/organizations/' + organizationId + '/groups', null, true, true); - return new ListResponse(r, GroupResponse); - } - - async getGroupUsers(organizationId: string, id: string): Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/groups/' + id + '/users', - null, true, true); - return r; - } - - async postGroup(organizationId: string, request: GroupRequest): Promise { - const r = await this.send('POST', '/organizations/' + organizationId + '/groups', request, true, true); - return new GroupResponse(r); - } - - async putGroup(organizationId: string, id: string, request: GroupRequest): Promise { - const r = await this.send('PUT', '/organizations/' + organizationId + '/groups/' + id, request, true, true); - return new GroupResponse(r); - } - - async putGroupUsers(organizationId: string, id: string, request: string[]): Promise { - await this.send('PUT', '/organizations/' + organizationId + '/groups/' + id + '/users', request, true, false); - } - - deleteGroup(organizationId: string, id: string): Promise { - return this.send('DELETE', '/organizations/' + organizationId + '/groups/' + id, null, true, false); - } - - deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise { - return this.send('DELETE', - '/organizations/' + organizationId + '/groups/' + id + '/user/' + organizationUserId, null, true, false); - } - - // Policy APIs - - async getPolicy(organizationId: string, type: PolicyType): Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/policies/' + type, null, true, true); - return new PolicyResponse(r); - } - - async getPolicies(organizationId: string): Promise> { - const r = await this.send('GET', '/organizations/' + organizationId + '/policies', null, true, true); - return new ListResponse(r, PolicyResponse); - } - - async getPoliciesByToken(organizationId: string, token: string, email: string, organizationUserId: string): - Promise> { - const r = await this.send('GET', '/organizations/' + organizationId + '/policies/token?' + - 'token=' + encodeURIComponent(token) + '&email=' + encodeURIComponent(email) + - '&organizationUserId=' + organizationUserId, null, false, true); - return new ListResponse(r, PolicyResponse); - } - - async putPolicy(organizationId: string, type: PolicyType, request: PolicyRequest): Promise { - const r = await this.send('PUT', '/organizations/' + organizationId + '/policies/' + type, request, true, true); - return new PolicyResponse(r); - } - - // Organization User APIs - - async getOrganizationUser(organizationId: string, id: string): Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/users/' + id, null, true, true); - return new OrganizationUserDetailsResponse(r); - } - - async getOrganizationUserGroups(organizationId: string, id: string): Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/users/' + id + '/groups', - null, true, true); - return r; - } - - async getOrganizationUsers(organizationId: string): Promise> { - const r = await this.send('GET', '/organizations/' + organizationId + '/users', null, true, true); - return new ListResponse(r, OrganizationUserUserDetailsResponse); - } - - async getOrganizationUserResetPasswordDetails(organizationId: string, id: string): - Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/users/' + id + - '/reset-password-details', null, true, true); - return new OrganizationUserResetPasswordDetailsReponse(r); - } - - async getOrganizationAutoEnrollStatus(identifier: string): Promise { - const r = await this.send('GET', '/organizations/' + identifier + '/auto-enroll-status', null, true, true); - return new OrganizationAutoEnrollStatusResponse(r); - } - - postOrganizationUserInvite(organizationId: string, request: OrganizationUserInviteRequest): Promise { - return this.send('POST', '/organizations/' + organizationId + '/users/invite', request, true, false); - } - - postOrganizationUserReinvite(organizationId: string, id: string): Promise { - return this.send('POST', '/organizations/' + organizationId + '/users/' + id + '/reinvite', null, true, false); - } - - async postManyOrganizationUserReinvite(organizationId: string, request: OrganizationUserBulkRequest): Promise> { - const r = await this.send('POST', '/organizations/' + organizationId + '/users/reinvite', request, true, true); - return new ListResponse(r, OrganizationUserBulkResponse); - } - - postOrganizationUserAccept(organizationId: string, id: string, - request: OrganizationUserAcceptRequest): Promise { - return this.send('POST', '/organizations/' + organizationId + '/users/' + id + '/accept', request, true, false); - } - - postOrganizationUserConfirm(organizationId: string, id: string, - request: OrganizationUserConfirmRequest): Promise { - return this.send('POST', '/organizations/' + organizationId + '/users/' + id + '/confirm', - request, true, false); - } - - async postOrganizationUsersPublicKey(organizationId: string, request: OrganizationUserBulkRequest): Promise> { - const r = await this.send('POST', '/organizations/' + organizationId + '/users/public-keys', request, true, true); - return new ListResponse(r, OrganizationUserBulkPublicKeyResponse); - } - - async postOrganizationUserBulkConfirm(organizationId: string, request: OrganizationUserBulkConfirmRequest): Promise> { - const r = await this.send('POST', '/organizations/' + organizationId + '/users/confirm', request, true, true); - return new ListResponse(r, OrganizationUserBulkResponse); - } - - putOrganizationUser(organizationId: string, id: string, request: OrganizationUserUpdateRequest): Promise { - return this.send('PUT', '/organizations/' + organizationId + '/users/' + id, request, true, false); - } - - putOrganizationUserGroups(organizationId: string, id: string, - request: OrganizationUserUpdateGroupsRequest): Promise { - return this.send('PUT', '/organizations/' + organizationId + '/users/' + id + '/groups', request, true, false); - } - - putOrganizationUserResetPasswordEnrollment(organizationId: string, userId: string, - request: OrganizationUserResetPasswordEnrollmentRequest): Promise { - return this.send('PUT', '/organizations/' + organizationId + '/users/' + userId + '/reset-password-enrollment', - request, true, false); - } - - putOrganizationUserResetPassword(organizationId: string, id: string, - request: OrganizationUserResetPasswordRequest): Promise { - return this.send('PUT', '/organizations/' + organizationId + '/users/' + id + '/reset-password', - request, true, false); - } - - deleteOrganizationUser(organizationId: string, id: string): Promise { - return this.send('DELETE', '/organizations/' + organizationId + '/users/' + id, null, true, false); - } - - async deleteManyOrganizationUsers(organizationId: string, request: OrganizationUserBulkRequest): Promise> { - const r = await this.send('DELETE', '/organizations/' + organizationId + '/users', request, true, true); - return new ListResponse(r, OrganizationUserBulkResponse); - } - - // Plan APIs - - async getPlans(): Promise> { - const r = await this.send('GET', '/plans/', null, true, true); - return new ListResponse(r, PlanResponse); - } - - async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { - return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); - } - - async postPublicImportDirectory(request: OrganizationImportRequest): Promise { - return this.send('POST', '/public/organization/import', request, true, false); - } - - async getTaxRates(): Promise> { - const r = await this.send('GET', '/plans/sales-tax-rates/', null, true, true); - return new ListResponse(r, TaxRateResponse); - } - - // Settings APIs - - async getSettingsDomains(): Promise { - const r = await this.send('GET', '/settings/domains', null, true, true); - return new DomainsResponse(r); - } - - async putSettingsDomains(request: UpdateDomainsRequest): Promise { - const r = await this.send('PUT', '/settings/domains', request, true, true); - return new DomainsResponse(r); - } - - // Sync APIs - - async getSync(): Promise { - const path = this.isDesktopClient || this.isWebClient ? '/sync?excludeDomains=true' : '/sync'; - const r = await this.send('GET', path, null, true, true); - return new SyncResponse(r); - } - - // Two-factor APIs - - async getTwoFactorProviders(): Promise> { - const r = await this.send('GET', '/two-factor', null, true, true); - return new ListResponse(r, TwoFactorProviderResponse); - } - - async getTwoFactorOrganizationProviders(organizationId: string): Promise> { - const r = await this.send('GET', '/organizations/' + organizationId + '/two-factor', null, true, true); - return new ListResponse(r, TwoFactorProviderResponse); - } - - async getTwoFactorAuthenticator(request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/two-factor/get-authenticator', request, true, true); - return new TwoFactorAuthenticatorResponse(r); - } - - async getTwoFactorEmail(request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/two-factor/get-email', request, true, true); - return new TwoFactorEmailResponse(r); - } - - async getTwoFactorDuo(request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/two-factor/get-duo', request, true, true); - return new TwoFactorDuoResponse(r); - } - - async getTwoFactorOrganizationDuo(organizationId: string, - request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/organizations/' + organizationId + '/two-factor/get-duo', - request, true, true); - return new TwoFactorDuoResponse(r); - } - - async getTwoFactorYubiKey(request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/two-factor/get-yubikey', request, true, true); - return new TwoFactorYubiKeyResponse(r); - } - - async getTwoFactorWebAuthn(request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/two-factor/get-webauthn', request, true, true); - return new TwoFactorWebAuthnResponse(r); - } - - async getTwoFactorWebAuthnChallenge(request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/two-factor/get-webauthn-challenge', request, true, true); - return new ChallengeResponse(r); - } - - async getTwoFactorRecover(request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/two-factor/get-recover', request, true, true); - return new TwoFactorRecoverResponse(r); - } - - async putTwoFactorAuthenticator( - request: UpdateTwoFactorAuthenticatorRequest): Promise { - const r = await this.send('PUT', '/two-factor/authenticator', request, true, true); - return new TwoFactorAuthenticatorResponse(r); - } - - async putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise { - const r = await this.send('PUT', '/two-factor/email', request, true, true); - return new TwoFactorEmailResponse(r); - } - - async putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise { - const r = await this.send('PUT', '/two-factor/duo', request, true, true); - return new TwoFactorDuoResponse(r); - } - - async putTwoFactorOrganizationDuo(organizationId: string, - request: UpdateTwoFactorDuoRequest): Promise { - const r = await this.send('PUT', '/organizations/' + organizationId + '/two-factor/duo', request, true, true); - return new TwoFactorDuoResponse(r); - } - - async putTwoFactorYubiKey(request: UpdateTwoFactorYubioOtpRequest): Promise { - const r = await this.send('PUT', '/two-factor/yubikey', request, true, true); - return new TwoFactorYubiKeyResponse(r); - } - - async putTwoFactorWebAuthn(request: UpdateTwoFactorWebAuthnRequest): Promise { - const response = request.deviceResponse.response as AuthenticatorAttestationResponse; - const data: any = Object.assign({}, request); - - data.deviceResponse = { - id: request.deviceResponse.id, - rawId: btoa(request.deviceResponse.id), - type: request.deviceResponse.type, - extensions: request.deviceResponse.getClientExtensionResults(), - response: { - AttestationObject: Utils.fromBufferToB64(response.attestationObject), - clientDataJson: Utils.fromBufferToB64(response.clientDataJSON), - }, - }; - - const r = await this.send('PUT', '/two-factor/webauthn', data, true, true); - return new TwoFactorWebAuthnResponse(r); - } - - async deleteTwoFactorWebAuthn(request: UpdateTwoFactorWebAuthnDeleteRequest): Promise { - const r = await this.send('DELETE', '/two-factor/webauthn', request, true, true); - return new TwoFactorWebAuthnResponse(r); - } - - async putTwoFactorDisable(request: TwoFactorProviderRequest): Promise { - const r = await this.send('PUT', '/two-factor/disable', request, true, true); - return new TwoFactorProviderResponse(r); - } - - async putTwoFactorOrganizationDisable(organizationId: string, - request: TwoFactorProviderRequest): Promise { - const r = await this.send('PUT', '/organizations/' + organizationId + '/two-factor/disable', - request, true, true); - return new TwoFactorProviderResponse(r); - } - - postTwoFactorRecover(request: TwoFactorRecoveryRequest): Promise { - return this.send('POST', '/two-factor/recover', request, false, false); - } - - postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise { - return this.send('POST', '/two-factor/send-email', request, true, false); - } - - postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { - return this.send('POST', '/two-factor/send-email-login', request, false, false); - } - - // Emergency Access APIs - - async getEmergencyAccessTrusted(): Promise> { - const r = await this.send('GET', '/emergency-access/trusted', null, true, true); - return new ListResponse(r, EmergencyAccessGranteeDetailsResponse); - } - - async getEmergencyAccessGranted(): Promise> { - const r = await this.send('GET', '/emergency-access/granted', null, true, true); - return new ListResponse(r, EmergencyAccessGrantorDetailsResponse); - } - - async getEmergencyAccess(id: string): Promise { - const r = await this.send('GET', '/emergency-access/' + id, null, true, true); - return new EmergencyAccessGranteeDetailsResponse(r); - } - - async getEmergencyGrantorPolicies(id: string): Promise> { - const r = await this.send('GET', '/emergency-access/' + id + '/policies', null, true, true); - return new ListResponse(r, PolicyResponse); - } - - putEmergencyAccess(id: string, request: EmergencyAccessUpdateRequest): Promise { - return this.send('PUT', '/emergency-access/' + id, request, true, false); - } - - deleteEmergencyAccess(id: string): Promise { - return this.send('DELETE', '/emergency-access/' + id, null, true, false); - } - - postEmergencyAccessInvite(request: EmergencyAccessInviteRequest): Promise { - return this.send('POST', '/emergency-access/invite', request, true, false); - } - - postEmergencyAccessReinvite(id: string): Promise { - return this.send('POST', '/emergency-access/' + id + '/reinvite', null, true, false); - } - - postEmergencyAccessAccept(id: string, request: EmergencyAccessAcceptRequest): Promise { - return this.send('POST', '/emergency-access/' + id + '/accept', request, true, false); - } - - postEmergencyAccessConfirm(id: string, request: EmergencyAccessConfirmRequest): Promise { - return this.send('POST', '/emergency-access/' + id + '/confirm', request, true, false); - } - - postEmergencyAccessInitiate(id: string): Promise { - return this.send('POST', '/emergency-access/' + id + '/initiate', null, true, false); - } - - postEmergencyAccessApprove(id: string): Promise { - return this.send('POST', '/emergency-access/' + id + '/approve', null, true, false); - } - - postEmergencyAccessReject(id: string): Promise { - return this.send('POST', '/emergency-access/' + id + '/reject', null, true, false); - } - - async postEmergencyAccessTakeover(id: string): Promise { - const r = await this.send('POST', '/emergency-access/' + id + '/takeover', null, true, true); - return new EmergencyAccessTakeoverResponse(r); - } - - async postEmergencyAccessPassword(id: string, request: EmergencyAccessPasswordRequest): Promise { - const r = await this.send('POST', '/emergency-access/' + id + '/password', request, true, true); - } - - async postEmergencyAccessView(id: string): Promise { - const r = await this.send('POST', '/emergency-access/' + id + '/view', null, true, true); - return new EmergencyAccessViewResponse(r); - } - - // Organization APIs - - async getOrganization(id: string): Promise { - const r = await this.send('GET', '/organizations/' + id, null, true, true); - return new OrganizationResponse(r); - } - - async getOrganizationBilling(id: string): Promise { - const r = await this.send('GET', '/organizations/' + id + '/billing', null, true, true); - return new BillingResponse(r); - } - - async getOrganizationSubscription(id: string): Promise { - const r = await this.send('GET', '/organizations/' + id + '/subscription', null, true, true); - return new OrganizationSubscriptionResponse(r); - } - - async getOrganizationLicense(id: string, installationId: string): Promise { - return this.send('GET', '/organizations/' + id + '/license?installationId=' + installationId, - null, true, true); - } - - async getOrganizationTaxInfo(id: string): Promise { - const r = await this.send('GET', '/organizations/' + id + '/tax', null, true, true); - return new TaxInfoResponse(r); - } - - async getOrganizationSso(id: string): Promise { - const r = await this.send('GET', '/organizations/' + id + '/sso', null, true, true); - return new OrganizationSsoResponse(r); - } - - async postOrganization(request: OrganizationCreateRequest): Promise { - const r = await this.send('POST', '/organizations', request, true, true); - return new OrganizationResponse(r); - } - - async putOrganization(id: string, request: OrganizationUpdateRequest): Promise { - const r = await this.send('PUT', '/organizations/' + id, request, true, true); - return new OrganizationResponse(r); - } - - async putOrganizationTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise { - return this.send('PUT', '/organizations/' + id + '/tax', request, true, false); - } - - postLeaveOrganization(id: string): Promise { - return this.send('POST', '/organizations/' + id + '/leave', null, true, false); - } - - async postOrganizationLicense(data: FormData): Promise { - const r = await this.send('POST', '/organizations/license', data, true, true); - return new OrganizationResponse(r); - } - - async postOrganizationLicenseUpdate(id: string, data: FormData): Promise { - return this.send('POST', '/organizations/' + id + '/license', data, true, false); - } - - async postOrganizationApiKey(id: string, request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/organizations/' + id + '/api-key', request, true, true); - return new ApiKeyResponse(r); - } - - async postOrganizationRotateApiKey(id: string, request: SecretVerificationRequest): Promise { - const r = await this.send('POST', '/organizations/' + id + '/rotate-api-key', request, true, true); - return new ApiKeyResponse(r); - } - - async postOrganizationSso(id: string, request: OrganizationSsoRequest): Promise { - const r = await this.send('POST', '/organizations/' + id + '/sso', request, true, true); - return new OrganizationSsoResponse(r); - } - - async postOrganizationUpgrade(id: string, request: OrganizationUpgradeRequest): Promise { - const r = await this.send('POST', '/organizations/' + id + '/upgrade', request, true, true); - return new PaymentResponse(r); - } - - async postOrganizationUpdateSubscription(id: string, request: OrganizationSubscriptionUpdateRequest): Promise { - return this.send('POST', '/organizations/' + id + '/subscription', request, true, false); - } - - async postOrganizationSeat(id: string, request: SeatRequest): Promise { - const r = await this.send('POST', '/organizations/' + id + '/seat', request, true, true); - return new PaymentResponse(r); - } - - async postOrganizationStorage(id: string, request: StorageRequest): Promise { - const r = await this.send('POST', '/organizations/' + id + '/storage', request, true, true); - return new PaymentResponse(r); - } - - postOrganizationPayment(id: string, request: PaymentRequest): Promise { - return this.send('POST', '/organizations/' + id + '/payment', request, true, false); - } - - postOrganizationVerifyBank(id: string, request: VerifyBankRequest): Promise { - return this.send('POST', '/organizations/' + id + '/verify-bank', request, true, false); - } - - postOrganizationCancel(id: string): Promise { - return this.send('POST', '/organizations/' + id + '/cancel', null, true, false); - } - - postOrganizationReinstate(id: string): Promise { - return this.send('POST', '/organizations/' + id + '/reinstate', null, true, false); - } - - deleteOrganization(id: string, request: SecretVerificationRequest): Promise { - return this.send('DELETE', '/organizations/' + id, request, true, false); - } - - async getOrganizationKeys(id: string): Promise { - const r = await this.send('GET', '/organizations/' + id + '/keys', null, true, true); - return new OrganizationKeysResponse(r); - } - - async postOrganizationKeys(id: string, request: OrganizationKeysRequest): Promise { - const r = await this.send('POST', '/organizations/' + id + '/keys', request, true, true); - return new OrganizationKeysResponse(r); - } - - // Provider APIs - - async postProviderSetup(id: string, request: ProviderSetupRequest) { - const r = await this.send('POST', '/providers/' + id + '/setup', request, true, true); - return new ProviderResponse(r); - } - - async getProvider(id: string) { - const r = await this.send('GET', '/providers/' + id, null, true, true); - return new ProviderResponse(r); - } - - async putProvider(id: string, request: ProviderUpdateRequest) { - const r = await this.send('PUT', '/providers/' + id, request, true, true); - return new ProviderResponse(r); - } - - // Provider User APIs - - async getProviderUsers(providerId: string): Promise> { - const r = await this.send('GET', '/providers/' + providerId + '/users', null, true, true); - return new ListResponse(r, ProviderUserUserDetailsResponse); - } - - async getProviderUser(providerId: string, id: string): Promise { - const r = await this.send('GET', '/providers/' + providerId + '/users/' + id, null, true, true); - return new ProviderUserResponse(r); - } - - postProviderUserInvite(providerId: string, request: ProviderUserInviteRequest): Promise { - return this.send('POST', '/providers/' + providerId + '/users/invite', request, true, false); - } - - postProviderUserReinvite(providerId: string, id: string): Promise { - return this.send('POST', '/providers/' + providerId + '/users/' + id + '/reinvite', null, true, false); - } - - async postManyProviderUserReinvite(providerId: string, request: ProviderUserBulkRequest): Promise> { - const r = await this.send('POST', '/providers/' + providerId + '/users/reinvite', request, true, true); - return new ListResponse(r, ProviderUserBulkResponse); - } - - async postProviderUserBulkConfirm(providerId: string, request: ProviderUserBulkConfirmRequest): Promise> { - const r = await this.send('POST', '/providers/' + providerId + '/users/confirm', request, true, true); - return new ListResponse(r, ProviderUserBulkResponse); - } - - async deleteManyProviderUsers(providerId: string, request: ProviderUserBulkRequest): Promise> { - const r = await this.send('DELETE', '/providers/' + providerId + '/users', request, true, true); - return new ListResponse(r, ProviderUserBulkResponse); - } - - postProviderUserAccept(providerId: string, id: string, request: ProviderUserAcceptRequest): Promise { - return this.send('POST', '/providers/' + providerId + '/users/' + id + '/accept', request, true, false); - } - - postProviderUserConfirm(providerId: string, id: string, request: ProviderUserConfirmRequest): Promise { - return this.send('POST', '/providers/' + providerId + '/users/' + id + '/confirm', - request, true, false); - } - - async postProviderUsersPublicKey(providerId: string, request: ProviderUserBulkRequest): Promise> { - const r = await this.send('POST', '/providers/' + providerId + '/users/public-keys', request, true, true); - return new ListResponse(r, ProviderUserBulkPublicKeyResponse); - } - - - putProviderUser(providerId: string, id: string, request: ProviderUserUpdateRequest): Promise { - return this.send('PUT', '/providers/' + providerId + '/users/' + id, request, true, false); - } - - deleteProviderUser(providerId: string, id: string): Promise { - return this.send('DELETE', '/providers/' + providerId + '/users/' + id, null, true, false); - } - - // Provider Organization APIs - - async getProviderClients(providerId: string): Promise> { - const r = await this.send('GET', '/providers/' + providerId + '/organizations', null, true, true); - return new ListResponse(r, ProviderOrganizationOrganizationDetailsResponse); - } - - postProviderAddOrganization(providerId: string, request: ProviderAddOrganizationRequest): Promise { - return this.send('POST', '/providers/' + providerId + '/organizations/add', request, true, false); - } - - async postProviderCreateOrganization(providerId: string, request: ProviderOrganizationCreateRequest): Promise { - const r = await this.send('POST', '/providers/' + providerId + '/organizations', request, true, true); - return new ProviderOrganizationResponse(r); - } - - deleteProviderOrganization(providerId: string, id: string): Promise { - return this.send('DELETE', '/providers/' + providerId + '/organizations/' + id, null, true, false); - } - - // Event APIs - - async getEvents(start: string, end: string, token: string): Promise> { - const r = await this.send('GET', this.addEventParameters('/events', start, end, token), null, true, true); - return new ListResponse(r, EventResponse); - } - - async getEventsCipher(id: string, start: string, end: string, - token: string): Promise> { - const r = await this.send('GET', this.addEventParameters('/ciphers/' + id + '/events', start, end, token), - null, true, true); - return new ListResponse(r, EventResponse); - } - - async getEventsOrganization(id: string, start: string, end: string, - token: string): Promise> { - const r = await this.send('GET', this.addEventParameters('/organizations/' + id + '/events', start, end, token), - null, true, true); - return new ListResponse(r, EventResponse); - } - - async getEventsOrganizationUser(organizationId: string, id: string, - start: string, end: string, token: string): Promise> { - const r = await this.send('GET', - this.addEventParameters('/organizations/' + organizationId + '/users/' + id + '/events', start, end, token), - null, true, true); - return new ListResponse(r, EventResponse); - } - - async getEventsProvider(id: string, start: string, end: string, token: string): Promise> { - const r = await this.send('GET', this.addEventParameters('/providers/' + id + '/events', start, end, token), null, true, true); - return new ListResponse(r, EventResponse); - } - - async getEventsProviderUser(providerId: string, id: string, - start: string, end: string, token: string): Promise> { - const r = await this.send('GET', - this.addEventParameters('/providers/' + providerId + '/users/' + id + '/events', start, end, token), - null, true, true); - return new ListResponse(r, EventResponse); - } - - async postEventsCollect(request: EventRequest[]): Promise { - const authHeader = await this.getActiveBearerToken(); - const headers = new Headers({ - 'Device-Type': this.deviceType, - 'Authorization': 'Bearer ' + authHeader, - 'Content-Type': 'application/json; charset=utf-8', - }); - if (this.customUserAgent != null) { - headers.set('User-Agent', this.customUserAgent); - } - const response = await this.fetch(new Request(this.environmentService.getEventsUrl() + '/collect', { - cache: 'no-store', - credentials: this.getCredentials(), - method: 'POST', - body: JSON.stringify(request), - headers: headers, - })); - if (response.status !== 200) { - return Promise.reject('Event post failed.'); - } - } - - // User APIs - - async getUserPublicKey(id: string): Promise { - const r = await this.send('GET', '/users/' + id + '/public-key', null, true, true); - return new UserKeyResponse(r); - } - - // HIBP APIs - - async getHibpBreach(username: string): Promise { - const r = await this.send('GET', '/hibp/breach?username=' + username, null, true, true); - return r.map((a: any) => new BreachAccountResponse(a)); - } - - // Misc - - async postBitPayInvoice(request: BitPayInvoiceRequest): Promise { - const r = await this.send('POST', '/bitpay-invoice', request, true, true); - return r as string; - } - - async postSetupPayment(): Promise { - const r = await this.send('POST', '/setup-payment', null, true, true); - return r as string; - } - - // Key Connector - - async getUserKeyFromKeyConnector(keyConnectorUrl: string): Promise { - const authHeader = await this.getActiveBearerToken(); - - const response = await this.fetch(new Request(keyConnectorUrl + '/user-keys', { - cache: 'no-store', - method: 'GET', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': 'Bearer ' + authHeader, - }), - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false, true); - return Promise.reject(error); - } - - return new KeyConnectorUserKeyResponse(await response.json()); - } - - async postUserKeyToKeyConnector(keyConnectorUrl: string, request: KeyConnectorUserKeyRequest): Promise { - const authHeader = await this.getActiveBearerToken(); - - const response = await this.fetch(new Request(keyConnectorUrl + '/user-keys', { - cache: 'no-store', - method: 'POST', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': 'Bearer ' + authHeader, - 'Content-Type': 'application/json; charset=utf-8', - }), - body: JSON.stringify(request), - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false, true); - return Promise.reject(error); - } - } - - async getKeyConnectorAlive(keyConnectorUrl: string) { - const response = await this.fetch(new Request(keyConnectorUrl + '/alive', { - cache: 'no-store', - method: 'GET', - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - }), - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false, true); - return Promise.reject(error); - } - } - - // Helpers - - async getActiveBearerToken(): Promise { - let accessToken = await this.tokenService.getToken(); - if (this.tokenService.tokenNeedsRefresh()) { - await this.doAuthRefresh(); - accessToken = await this.tokenService.getToken(); - } - return accessToken; - } - - fetch(request: Request): Promise { - if (request.method === 'GET') { - request.headers.set('Cache-Control', 'no-store'); - request.headers.set('Pragma', 'no-cache'); - } - return this.nativeFetch(request); - } - - nativeFetch(request: Request): Promise { - return fetch(request); - } - - async preValidateSso(identifier: string): Promise { - if (identifier == null || identifier === '') { - throw new Error('Organization Identifier was not provided.'); - } - const headers = new Headers({ - 'Accept': 'application/json', - 'Device-Type': this.deviceType, - }); - if (this.customUserAgent != null) { - headers.set('User-Agent', this.customUserAgent); - } - - const path = `/account/prevalidate?domainHint=${encodeURIComponent(identifier)}`; - const response = await this.fetch(new Request(this.environmentService.getIdentityUrl() + path, { - cache: 'no-store', - credentials: this.getCredentials(), - headers: headers, - method: 'GET', - })); - - if (response.status === 200) { - return true; + protected apiKeyRefresh: (clientId: string, clientSecret: string) => Promise; + private device: DeviceType; + private deviceType: string; + private isWebClient = false; + private isDesktopClient = false; + + constructor( + private tokenService: TokenService, + private platformUtilsService: PlatformUtilsService, + private environmentService: EnvironmentService, + private logoutCallback: (expired: boolean) => Promise, + private customUserAgent: string = null + ) { + this.device = platformUtilsService.getDevice(); + this.deviceType = this.device.toString(); + this.isWebClient = + this.device === DeviceType.IEBrowser || + this.device === DeviceType.ChromeBrowser || + this.device === DeviceType.EdgeBrowser || + this.device === DeviceType.FirefoxBrowser || + this.device === DeviceType.OperaBrowser || + this.device === DeviceType.SafariBrowser || + this.device === DeviceType.UnknownBrowser || + this.device === DeviceType.VivaldiBrowser; + this.isDesktopClient = + this.device === DeviceType.WindowsDesktop || + this.device === DeviceType.MacOsDesktop || + this.device === DeviceType.LinuxDesktop; + } + + // Auth APIs + + async postIdentityToken( + request: TokenRequest + ): Promise { + const headers = new Headers({ + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + Accept: "application/json", + "Device-Type": this.deviceType, + }); + if (this.customUserAgent != null) { + headers.set("User-Agent", this.customUserAgent); + } + request.alterIdentityTokenHeaders(headers); + const response = await this.fetch( + new Request(this.environmentService.getIdentityUrl() + "/connect/token", { + body: this.qsStringify( + request.toIdentityToken(request.clientId ?? this.platformUtilsService.identityClientId) + ), + credentials: this.getCredentials(), + cache: "no-store", + headers: headers, + method: "POST", + }) + ); + + let responseJson: any = null; + if (this.isJsonResponse(response)) { + responseJson = await response.json(); + } + + if (responseJson != null) { + if (response.status === 200) { + return new IdentityTokenResponse(responseJson); + } else if ( + response.status === 400 && + responseJson.TwoFactorProviders2 && + Object.keys(responseJson.TwoFactorProviders2).length + ) { + await this.tokenService.clearTwoFactorToken(request.email); + return new IdentityTwoFactorResponse(responseJson); + } else if ( + response.status === 400 && + responseJson.HCaptcha_SiteKey && + Object.keys(responseJson.HCaptcha_SiteKey).length + ) { + return new IdentityCaptchaResponse(responseJson); + } + } + + return Promise.reject(new ErrorResponse(responseJson, response.status, true)); + } + + async refreshIdentityToken(): Promise { + try { + await this.doAuthRefresh(); + } catch (e) { + return Promise.reject(null); + } + } + + // Account APIs + + async getProfile(): Promise { + const r = await this.send("GET", "/accounts/profile", null, true, true); + return new ProfileResponse(r); + } + + async getUserBilling(): Promise { + const r = await this.send("GET", "/accounts/billing", null, true, true); + return new BillingResponse(r); + } + + async getUserSubscription(): Promise { + const r = await this.send("GET", "/accounts/subscription", null, true, true); + return new SubscriptionResponse(r); + } + + async getTaxInfo(): Promise { + const r = await this.send("GET", "/accounts/tax", null, true, true); + return new TaxInfoResponse(r); + } + + async putProfile(request: UpdateProfileRequest): Promise { + const r = await this.send("PUT", "/accounts/profile", request, true, true); + return new ProfileResponse(r); + } + + putTaxInfo(request: TaxInfoUpdateRequest): Promise { + return this.send("PUT", "/accounts/tax", request, true, false); + } + + async postPrelogin(request: PreloginRequest): Promise { + const r = await this.send("POST", "/accounts/prelogin", request, false, true); + return new PreloginResponse(r); + } + + postEmailToken(request: EmailTokenRequest): Promise { + return this.send("POST", "/accounts/email-token", request, true, false); + } + + postEmail(request: EmailRequest): Promise { + return this.send("POST", "/accounts/email", request, true, false); + } + + postPassword(request: PasswordRequest): Promise { + return this.send("POST", "/accounts/password", request, true, false); + } + + setPassword(request: SetPasswordRequest): Promise { + return this.send("POST", "/accounts/set-password", request, true, false); + } + + postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise { + return this.send("POST", "/accounts/set-key-connector-key", request, true, false); + } + + postSecurityStamp(request: SecretVerificationRequest): Promise { + return this.send("POST", "/accounts/security-stamp", request, true, false); + } + + deleteAccount(request: SecretVerificationRequest): Promise { + return this.send("DELETE", "/accounts", request, true, false); + } + + async getAccountRevisionDate(): Promise { + const r = await this.send("GET", "/accounts/revision-date", null, true, true); + return r as number; + } + + postPasswordHint(request: PasswordHintRequest): Promise { + return this.send("POST", "/accounts/password-hint", request, false, false); + } + + postRegister(request: RegisterRequest): Promise { + return this.send("POST", "/accounts/register", request, false, false); + } + + async postPremium(data: FormData): Promise { + const r = await this.send("POST", "/accounts/premium", data, true, true); + return new PaymentResponse(r); + } + + async postIapCheck(request: IapCheckRequest): Promise { + return this.send("POST", "/accounts/iap-check", request, true, false); + } + + postReinstatePremium(): Promise { + return this.send("POST", "/accounts/reinstate-premium", null, true, false); + } + + postCancelPremium(): Promise { + return this.send("POST", "/accounts/cancel-premium", null, true, false); + } + + async postAccountStorage(request: StorageRequest): Promise { + const r = await this.send("POST", "/accounts/storage", request, true, true); + return new PaymentResponse(r); + } + + postAccountPayment(request: PaymentRequest): Promise { + return this.send("POST", "/accounts/payment", request, true, false); + } + + postAccountLicense(data: FormData): Promise { + return this.send("POST", "/accounts/license", data, true, false); + } + + postAccountKeys(request: KeysRequest): Promise { + return this.send("POST", "/accounts/keys", request, true, false); + } + + postAccountKey(request: UpdateKeyRequest): Promise { + return this.send("POST", "/accounts/key", request, true, false); + } + + postAccountVerifyEmail(): Promise { + return this.send("POST", "/accounts/verify-email", null, true, false); + } + + postAccountVerifyEmailToken(request: VerifyEmailRequest): Promise { + return this.send("POST", "/accounts/verify-email-token", request, false, false); + } + + postAccountVerifyPassword(request: SecretVerificationRequest): Promise { + return this.send("POST", "/accounts/verify-password", request, true, false); + } + + postAccountRecoverDelete(request: DeleteRecoverRequest): Promise { + return this.send("POST", "/accounts/delete-recover", request, false, false); + } + + postAccountRecoverDeleteToken(request: VerifyDeleteRecoverRequest): Promise { + return this.send("POST", "/accounts/delete-recover-token", request, false, false); + } + + postAccountKdf(request: KdfRequest): Promise { + return this.send("POST", "/accounts/kdf", request, true, false); + } + + async deleteSsoUser(organizationId: string): Promise { + return this.send("DELETE", "/accounts/sso/" + organizationId, null, true, false); + } + + async getSsoUserIdentifier(): Promise { + return this.send("GET", "/accounts/sso/user-identifier", null, true, true); + } + + async postUserApiKey(id: string, request: SecretVerificationRequest): Promise { + const r = await this.send("POST", "/accounts/api-key", request, true, true); + return new ApiKeyResponse(r); + } + + async postUserRotateApiKey( + id: string, + request: SecretVerificationRequest + ): Promise { + const r = await this.send("POST", "/accounts/rotate-api-key", request, true, true); + return new ApiKeyResponse(r); + } + + putUpdateTempPassword(request: UpdateTempPasswordRequest): Promise { + return this.send("PUT", "/accounts/update-temp-password", request, true, false); + } + + postAccountRequestOTP(): Promise { + return this.send("POST", "/accounts/request-otp", null, true, false); + } + + postAccountVerifyOTP(request: VerifyOTPRequest): Promise { + return this.send("POST", "/accounts/verify-otp", request, true, false); + } + + postConvertToKeyConnector(): Promise { + return this.send("POST", "/accounts/convert-to-key-connector", null, true, false); + } + + // Folder APIs + + async getFolder(id: string): Promise { + const r = await this.send("GET", "/folders/" + id, null, true, true); + return new FolderResponse(r); + } + + async postFolder(request: FolderRequest): Promise { + const r = await this.send("POST", "/folders", request, true, true); + return new FolderResponse(r); + } + + async putFolder(id: string, request: FolderRequest): Promise { + const r = await this.send("PUT", "/folders/" + id, request, true, true); + return new FolderResponse(r); + } + + deleteFolder(id: string): Promise { + return this.send("DELETE", "/folders/" + id, null, true, false); + } + + // Send APIs + + async getSend(id: string): Promise { + const r = await this.send("GET", "/sends/" + id, null, true, true); + return new SendResponse(r); + } + + async postSendAccess( + id: string, + request: SendAccessRequest, + apiUrl?: string + ): Promise { + const addSendIdHeader = (headers: Headers) => { + headers.set("Send-Id", id); + }; + const r = await this.send( + "POST", + "/sends/access/" + id, + request, + false, + true, + apiUrl, + addSendIdHeader + ); + return new SendAccessResponse(r); + } + + async getSendFileDownloadData( + send: SendAccessView, + request: SendAccessRequest, + apiUrl?: string + ): Promise { + const addSendIdHeader = (headers: Headers) => { + headers.set("Send-Id", send.id); + }; + const r = await this.send( + "POST", + "/sends/" + send.id + "/access/file/" + send.file.id, + request, + false, + true, + apiUrl, + addSendIdHeader + ); + return new SendFileDownloadDataResponse(r); + } + + async getSends(): Promise> { + const r = await this.send("GET", "/sends", null, true, true); + return new ListResponse(r, SendResponse); + } + + async postSend(request: SendRequest): Promise { + const r = await this.send("POST", "/sends", request, true, true); + return new SendResponse(r); + } + + async postFileTypeSend(request: SendRequest): Promise { + const r = await this.send("POST", "/sends/file/v2", request, true, true); + return new SendFileUploadDataResponse(r); + } + + async renewSendFileUploadUrl( + sendId: string, + fileId: string + ): Promise { + const r = await this.send("GET", "/sends/" + sendId + "/file/" + fileId, null, true, true); + return new SendFileUploadDataResponse(r); + } + + postSendFile(sendId: string, fileId: string, data: FormData): Promise { + return this.send("POST", "/sends/" + sendId + "/file/" + fileId, data, true, false); + } + + /** + * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. + * This method still exists for backward compatibility with old server versions. + */ + async postSendFileLegacy(data: FormData): Promise { + const r = await this.send("POST", "/sends/file", data, true, true); + return new SendResponse(r); + } + + async putSend(id: string, request: SendRequest): Promise { + const r = await this.send("PUT", "/sends/" + id, request, true, true); + return new SendResponse(r); + } + + async putSendRemovePassword(id: string): Promise { + const r = await this.send("PUT", "/sends/" + id + "/remove-password", null, true, true); + return new SendResponse(r); + } + + deleteSend(id: string): Promise { + return this.send("DELETE", "/sends/" + id, null, true, false); + } + + // Cipher APIs + + async getCipher(id: string): Promise { + const r = await this.send("GET", "/ciphers/" + id, null, true, true); + return new CipherResponse(r); + } + + async getCipherAdmin(id: string): Promise { + const r = await this.send("GET", "/ciphers/" + id + "/admin", null, true, true); + return new CipherResponse(r); + } + + async getCiphersOrganization(organizationId: string): Promise> { + const r = await this.send( + "GET", + "/ciphers/organization-details?organizationId=" + organizationId, + null, + true, + true + ); + return new ListResponse(r, CipherResponse); + } + + async postCipher(request: CipherRequest): Promise { + const r = await this.send("POST", "/ciphers", request, true, true); + return new CipherResponse(r); + } + + async postCipherCreate(request: CipherCreateRequest): Promise { + const r = await this.send("POST", "/ciphers/create", request, true, true); + return new CipherResponse(r); + } + + async postCipherAdmin(request: CipherCreateRequest): Promise { + const r = await this.send("POST", "/ciphers/admin", request, true, true); + return new CipherResponse(r); + } + + async putCipher(id: string, request: CipherRequest): Promise { + const r = await this.send("PUT", "/ciphers/" + id, request, true, true); + return new CipherResponse(r); + } + + async putCipherAdmin(id: string, request: CipherRequest): Promise { + const r = await this.send("PUT", "/ciphers/" + id + "/admin", request, true, true); + return new CipherResponse(r); + } + + deleteCipher(id: string): Promise { + return this.send("DELETE", "/ciphers/" + id, null, true, false); + } + + deleteCipherAdmin(id: string): Promise { + return this.send("DELETE", "/ciphers/" + id + "/admin", null, true, false); + } + + deleteManyCiphers(request: CipherBulkDeleteRequest): Promise { + return this.send("DELETE", "/ciphers", request, true, false); + } + + deleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise { + return this.send("DELETE", "/ciphers/admin", request, true, false); + } + + putMoveCiphers(request: CipherBulkMoveRequest): Promise { + return this.send("PUT", "/ciphers/move", request, true, false); + } + + async putShareCipher(id: string, request: CipherShareRequest): Promise { + const r = await this.send("PUT", "/ciphers/" + id + "/share", request, true, true); + return new CipherResponse(r); + } + + putShareCiphers(request: CipherBulkShareRequest): Promise { + return this.send("PUT", "/ciphers/share", request, true, false); + } + + putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { + return this.send("PUT", "/ciphers/" + id + "/collections", request, true, false); + } + + putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise { + return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, false); + } + + postPurgeCiphers( + request: SecretVerificationRequest, + organizationId: string = null + ): Promise { + let path = "/ciphers/purge"; + if (organizationId != null) { + path += "?organizationId=" + organizationId; + } + return this.send("POST", path, request, true, false); + } + + postImportCiphers(request: ImportCiphersRequest): Promise { + return this.send("POST", "/ciphers/import", request, true, false); + } + + postImportOrganizationCiphers( + organizationId: string, + request: ImportOrganizationCiphersRequest + ): Promise { + return this.send( + "POST", + "/ciphers/import-organization?organizationId=" + organizationId, + request, + true, + false + ); + } + + putDeleteCipher(id: string): Promise { + return this.send("PUT", "/ciphers/" + id + "/delete", null, true, false); + } + + putDeleteCipherAdmin(id: string): Promise { + return this.send("PUT", "/ciphers/" + id + "/delete-admin", null, true, false); + } + + putDeleteManyCiphers(request: CipherBulkDeleteRequest): Promise { + return this.send("PUT", "/ciphers/delete", request, true, false); + } + + putDeleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise { + return this.send("PUT", "/ciphers/delete-admin", request, true, false); + } + + async putRestoreCipher(id: string): Promise { + const r = await this.send("PUT", "/ciphers/" + id + "/restore", null, true, true); + return new CipherResponse(r); + } + + async putRestoreCipherAdmin(id: string): Promise { + const r = await this.send("PUT", "/ciphers/" + id + "/restore-admin", null, true, true); + return new CipherResponse(r); + } + + async putRestoreManyCiphers( + request: CipherBulkDeleteRequest + ): Promise> { + const r = await this.send("PUT", "/ciphers/restore", request, true, true); + return new ListResponse(r, CipherResponse); + } + + // Attachments APIs + + async getAttachmentData( + cipherId: string, + attachmentId: string, + emergencyAccessId?: string + ): Promise { + const path = + (emergencyAccessId != null ? "/emergency-access/" + emergencyAccessId + "/" : "/ciphers/") + + cipherId + + "/attachment/" + + attachmentId; + const r = await this.send("GET", path, null, true, true); + return new AttachmentResponse(r); + } + + async postCipherAttachment( + id: string, + request: AttachmentRequest + ): Promise { + const r = await this.send("POST", "/ciphers/" + id + "/attachment/v2", request, true, true); + return new AttachmentUploadDataResponse(r); + } + + /** + * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. + * This method still exists for backward compatibility with old server versions. + */ + async postCipherAttachmentLegacy(id: string, data: FormData): Promise { + const r = await this.send("POST", "/ciphers/" + id + "/attachment", data, true, true); + return new CipherResponse(r); + } + + /** + * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. + * This method still exists for backward compatibility with old server versions. + */ + async postCipherAttachmentAdminLegacy(id: string, data: FormData): Promise { + const r = await this.send("POST", "/ciphers/" + id + "/attachment-admin", data, true, true); + return new CipherResponse(r); + } + + deleteCipherAttachment(id: string, attachmentId: string): Promise { + return this.send("DELETE", "/ciphers/" + id + "/attachment/" + attachmentId, null, true, false); + } + + deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise { + return this.send( + "DELETE", + "/ciphers/" + id + "/attachment/" + attachmentId + "/admin", + null, + true, + false + ); + } + + postShareCipherAttachment( + id: string, + attachmentId: string, + data: FormData, + organizationId: string + ): Promise { + return this.send( + "POST", + "/ciphers/" + id + "/attachment/" + attachmentId + "/share?organizationId=" + organizationId, + data, + true, + false + ); + } + + async renewAttachmentUploadUrl( + id: string, + attachmentId: string + ): Promise { + const r = await this.send( + "GET", + "/ciphers/" + id + "/attachment/" + attachmentId + "/renew", + null, + true, + true + ); + return new AttachmentUploadDataResponse(r); + } + + postAttachmentFile(id: string, attachmentId: string, data: FormData): Promise { + return this.send("POST", "/ciphers/" + id + "/attachment/" + attachmentId, data, true, false); + } + + // Collections APIs + + async getCollectionDetails( + organizationId: string, + id: string + ): Promise { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/collections/" + id + "/details", + null, + true, + true + ); + return new CollectionGroupDetailsResponse(r); + } + + async getUserCollections(): Promise> { + const r = await this.send("GET", "/collections", null, true, true); + return new ListResponse(r, CollectionResponse); + } + + async getCollections(organizationId: string): Promise> { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/collections", + null, + true, + true + ); + return new ListResponse(r, CollectionResponse); + } + + async getCollectionUsers( + organizationId: string, + id: string + ): Promise { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/collections/" + id + "/users", + null, + true, + true + ); + return r.map((dr: any) => new SelectionReadOnlyResponse(dr)); + } + + async postCollection( + organizationId: string, + request: CollectionRequest + ): Promise { + const r = await this.send( + "POST", + "/organizations/" + organizationId + "/collections", + request, + true, + true + ); + return new CollectionResponse(r); + } + + async putCollection( + organizationId: string, + id: string, + request: CollectionRequest + ): Promise { + const r = await this.send( + "PUT", + "/organizations/" + organizationId + "/collections/" + id, + request, + true, + true + ); + return new CollectionResponse(r); + } + + async putCollectionUsers( + organizationId: string, + id: string, + request: SelectionReadOnlyRequest[] + ): Promise { + await this.send( + "PUT", + "/organizations/" + organizationId + "/collections/" + id + "/users", + request, + true, + false + ); + } + + deleteCollection(organizationId: string, id: string): Promise { + return this.send( + "DELETE", + "/organizations/" + organizationId + "/collections/" + id, + null, + true, + false + ); + } + + deleteCollectionUser( + organizationId: string, + id: string, + organizationUserId: string + ): Promise { + return this.send( + "DELETE", + "/organizations/" + organizationId + "/collections/" + id + "/user/" + organizationUserId, + null, + true, + false + ); + } + + // Groups APIs + + async getGroupDetails(organizationId: string, id: string): Promise { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/groups/" + id + "/details", + null, + true, + true + ); + return new GroupDetailsResponse(r); + } + + async getGroups(organizationId: string): Promise> { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/groups", + null, + true, + true + ); + return new ListResponse(r, GroupResponse); + } + + async getGroupUsers(organizationId: string, id: string): Promise { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/groups/" + id + "/users", + null, + true, + true + ); + return r; + } + + async postGroup(organizationId: string, request: GroupRequest): Promise { + const r = await this.send( + "POST", + "/organizations/" + organizationId + "/groups", + request, + true, + true + ); + return new GroupResponse(r); + } + + async putGroup( + organizationId: string, + id: string, + request: GroupRequest + ): Promise { + const r = await this.send( + "PUT", + "/organizations/" + organizationId + "/groups/" + id, + request, + true, + true + ); + return new GroupResponse(r); + } + + async putGroupUsers(organizationId: string, id: string, request: string[]): Promise { + await this.send( + "PUT", + "/organizations/" + organizationId + "/groups/" + id + "/users", + request, + true, + false + ); + } + + deleteGroup(organizationId: string, id: string): Promise { + return this.send( + "DELETE", + "/organizations/" + organizationId + "/groups/" + id, + null, + true, + false + ); + } + + deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise { + return this.send( + "DELETE", + "/organizations/" + organizationId + "/groups/" + id + "/user/" + organizationUserId, + null, + true, + false + ); + } + + // Policy APIs + + async getPolicy(organizationId: string, type: PolicyType): Promise { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/policies/" + type, + null, + true, + true + ); + return new PolicyResponse(r); + } + + async getPolicies(organizationId: string): Promise> { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/policies", + null, + true, + true + ); + return new ListResponse(r, PolicyResponse); + } + + async getPoliciesByToken( + organizationId: string, + token: string, + email: string, + organizationUserId: string + ): Promise> { + const r = await this.send( + "GET", + "/organizations/" + + organizationId + + "/policies/token?" + + "token=" + + encodeURIComponent(token) + + "&email=" + + encodeURIComponent(email) + + "&organizationUserId=" + + organizationUserId, + null, + false, + true + ); + return new ListResponse(r, PolicyResponse); + } + + async putPolicy( + organizationId: string, + type: PolicyType, + request: PolicyRequest + ): Promise { + const r = await this.send( + "PUT", + "/organizations/" + organizationId + "/policies/" + type, + request, + true, + true + ); + return new PolicyResponse(r); + } + + // Organization User APIs + + async getOrganizationUser( + organizationId: string, + id: string + ): Promise { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/users/" + id, + null, + true, + true + ); + return new OrganizationUserDetailsResponse(r); + } + + async getOrganizationUserGroups(organizationId: string, id: string): Promise { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/users/" + id + "/groups", + null, + true, + true + ); + return r; + } + + async getOrganizationUsers( + organizationId: string + ): Promise> { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/users", + null, + true, + true + ); + return new ListResponse(r, OrganizationUserUserDetailsResponse); + } + + async getOrganizationUserResetPasswordDetails( + organizationId: string, + id: string + ): Promise { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/users/" + id + "/reset-password-details", + null, + true, + true + ); + return new OrganizationUserResetPasswordDetailsReponse(r); + } + + async getOrganizationAutoEnrollStatus( + identifier: string + ): Promise { + const r = await this.send( + "GET", + "/organizations/" + identifier + "/auto-enroll-status", + null, + true, + true + ); + return new OrganizationAutoEnrollStatusResponse(r); + } + + postOrganizationUserInvite( + organizationId: string, + request: OrganizationUserInviteRequest + ): Promise { + return this.send( + "POST", + "/organizations/" + organizationId + "/users/invite", + request, + true, + false + ); + } + + postOrganizationUserReinvite(organizationId: string, id: string): Promise { + return this.send( + "POST", + "/organizations/" + organizationId + "/users/" + id + "/reinvite", + null, + true, + false + ); + } + + async postManyOrganizationUserReinvite( + organizationId: string, + request: OrganizationUserBulkRequest + ): Promise> { + const r = await this.send( + "POST", + "/organizations/" + organizationId + "/users/reinvite", + request, + true, + true + ); + return new ListResponse(r, OrganizationUserBulkResponse); + } + + postOrganizationUserAccept( + organizationId: string, + id: string, + request: OrganizationUserAcceptRequest + ): Promise { + return this.send( + "POST", + "/organizations/" + organizationId + "/users/" + id + "/accept", + request, + true, + false + ); + } + + postOrganizationUserConfirm( + organizationId: string, + id: string, + request: OrganizationUserConfirmRequest + ): Promise { + return this.send( + "POST", + "/organizations/" + organizationId + "/users/" + id + "/confirm", + request, + true, + false + ); + } + + async postOrganizationUsersPublicKey( + organizationId: string, + request: OrganizationUserBulkRequest + ): Promise> { + const r = await this.send( + "POST", + "/organizations/" + organizationId + "/users/public-keys", + request, + true, + true + ); + return new ListResponse(r, OrganizationUserBulkPublicKeyResponse); + } + + async postOrganizationUserBulkConfirm( + organizationId: string, + request: OrganizationUserBulkConfirmRequest + ): Promise> { + const r = await this.send( + "POST", + "/organizations/" + organizationId + "/users/confirm", + request, + true, + true + ); + return new ListResponse(r, OrganizationUserBulkResponse); + } + + putOrganizationUser( + organizationId: string, + id: string, + request: OrganizationUserUpdateRequest + ): Promise { + return this.send( + "PUT", + "/organizations/" + organizationId + "/users/" + id, + request, + true, + false + ); + } + + putOrganizationUserGroups( + organizationId: string, + id: string, + request: OrganizationUserUpdateGroupsRequest + ): Promise { + return this.send( + "PUT", + "/organizations/" + organizationId + "/users/" + id + "/groups", + request, + true, + false + ); + } + + putOrganizationUserResetPasswordEnrollment( + organizationId: string, + userId: string, + request: OrganizationUserResetPasswordEnrollmentRequest + ): Promise { + return this.send( + "PUT", + "/organizations/" + organizationId + "/users/" + userId + "/reset-password-enrollment", + request, + true, + false + ); + } + + putOrganizationUserResetPassword( + organizationId: string, + id: string, + request: OrganizationUserResetPasswordRequest + ): Promise { + return this.send( + "PUT", + "/organizations/" + organizationId + "/users/" + id + "/reset-password", + request, + true, + false + ); + } + + deleteOrganizationUser(organizationId: string, id: string): Promise { + return this.send( + "DELETE", + "/organizations/" + organizationId + "/users/" + id, + null, + true, + false + ); + } + + async deleteManyOrganizationUsers( + organizationId: string, + request: OrganizationUserBulkRequest + ): Promise> { + const r = await this.send( + "DELETE", + "/organizations/" + organizationId + "/users", + request, + true, + true + ); + return new ListResponse(r, OrganizationUserBulkResponse); + } + + // Plan APIs + + async getPlans(): Promise> { + const r = await this.send("GET", "/plans/", null, true, true); + return new ListResponse(r, PlanResponse); + } + + async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { + return this.send("POST", "/organizations/" + organizationId + "/import", request, true, false); + } + + async postPublicImportDirectory(request: OrganizationImportRequest): Promise { + return this.send("POST", "/public/organization/import", request, true, false); + } + + async getTaxRates(): Promise> { + const r = await this.send("GET", "/plans/sales-tax-rates/", null, true, true); + return new ListResponse(r, TaxRateResponse); + } + + // Settings APIs + + async getSettingsDomains(): Promise { + const r = await this.send("GET", "/settings/domains", null, true, true); + return new DomainsResponse(r); + } + + async putSettingsDomains(request: UpdateDomainsRequest): Promise { + const r = await this.send("PUT", "/settings/domains", request, true, true); + return new DomainsResponse(r); + } + + // Sync APIs + + async getSync(): Promise { + const path = this.isDesktopClient || this.isWebClient ? "/sync?excludeDomains=true" : "/sync"; + const r = await this.send("GET", path, null, true, true); + return new SyncResponse(r); + } + + // Two-factor APIs + + async getTwoFactorProviders(): Promise> { + const r = await this.send("GET", "/two-factor", null, true, true); + return new ListResponse(r, TwoFactorProviderResponse); + } + + async getTwoFactorOrganizationProviders( + organizationId: string + ): Promise> { + const r = await this.send( + "GET", + "/organizations/" + organizationId + "/two-factor", + null, + true, + true + ); + return new ListResponse(r, TwoFactorProviderResponse); + } + + async getTwoFactorAuthenticator( + request: SecretVerificationRequest + ): Promise { + const r = await this.send("POST", "/two-factor/get-authenticator", request, true, true); + return new TwoFactorAuthenticatorResponse(r); + } + + async getTwoFactorEmail(request: SecretVerificationRequest): Promise { + const r = await this.send("POST", "/two-factor/get-email", request, true, true); + return new TwoFactorEmailResponse(r); + } + + async getTwoFactorDuo(request: SecretVerificationRequest): Promise { + const r = await this.send("POST", "/two-factor/get-duo", request, true, true); + return new TwoFactorDuoResponse(r); + } + + async getTwoFactorOrganizationDuo( + organizationId: string, + request: SecretVerificationRequest + ): Promise { + const r = await this.send( + "POST", + "/organizations/" + organizationId + "/two-factor/get-duo", + request, + true, + true + ); + return new TwoFactorDuoResponse(r); + } + + async getTwoFactorYubiKey(request: SecretVerificationRequest): Promise { + const r = await this.send("POST", "/two-factor/get-yubikey", request, true, true); + return new TwoFactorYubiKeyResponse(r); + } + + async getTwoFactorWebAuthn( + request: SecretVerificationRequest + ): Promise { + const r = await this.send("POST", "/two-factor/get-webauthn", request, true, true); + return new TwoFactorWebAuthnResponse(r); + } + + async getTwoFactorWebAuthnChallenge( + request: SecretVerificationRequest + ): Promise { + const r = await this.send("POST", "/two-factor/get-webauthn-challenge", request, true, true); + return new ChallengeResponse(r); + } + + async getTwoFactorRecover(request: SecretVerificationRequest): Promise { + const r = await this.send("POST", "/two-factor/get-recover", request, true, true); + return new TwoFactorRecoverResponse(r); + } + + async putTwoFactorAuthenticator( + request: UpdateTwoFactorAuthenticatorRequest + ): Promise { + const r = await this.send("PUT", "/two-factor/authenticator", request, true, true); + return new TwoFactorAuthenticatorResponse(r); + } + + async putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise { + const r = await this.send("PUT", "/two-factor/email", request, true, true); + return new TwoFactorEmailResponse(r); + } + + async putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise { + const r = await this.send("PUT", "/two-factor/duo", request, true, true); + return new TwoFactorDuoResponse(r); + } + + async putTwoFactorOrganizationDuo( + organizationId: string, + request: UpdateTwoFactorDuoRequest + ): Promise { + const r = await this.send( + "PUT", + "/organizations/" + organizationId + "/two-factor/duo", + request, + true, + true + ); + return new TwoFactorDuoResponse(r); + } + + async putTwoFactorYubiKey( + request: UpdateTwoFactorYubioOtpRequest + ): Promise { + const r = await this.send("PUT", "/two-factor/yubikey", request, true, true); + return new TwoFactorYubiKeyResponse(r); + } + + async putTwoFactorWebAuthn( + request: UpdateTwoFactorWebAuthnRequest + ): Promise { + const response = request.deviceResponse.response as AuthenticatorAttestationResponse; + const data: any = Object.assign({}, request); + + data.deviceResponse = { + id: request.deviceResponse.id, + rawId: btoa(request.deviceResponse.id), + type: request.deviceResponse.type, + extensions: request.deviceResponse.getClientExtensionResults(), + response: { + AttestationObject: Utils.fromBufferToB64(response.attestationObject), + clientDataJson: Utils.fromBufferToB64(response.clientDataJSON), + }, + }; + + const r = await this.send("PUT", "/two-factor/webauthn", data, true, true); + return new TwoFactorWebAuthnResponse(r); + } + + async deleteTwoFactorWebAuthn( + request: UpdateTwoFactorWebAuthnDeleteRequest + ): Promise { + const r = await this.send("DELETE", "/two-factor/webauthn", request, true, true); + return new TwoFactorWebAuthnResponse(r); + } + + async putTwoFactorDisable(request: TwoFactorProviderRequest): Promise { + const r = await this.send("PUT", "/two-factor/disable", request, true, true); + return new TwoFactorProviderResponse(r); + } + + async putTwoFactorOrganizationDisable( + organizationId: string, + request: TwoFactorProviderRequest + ): Promise { + const r = await this.send( + "PUT", + "/organizations/" + organizationId + "/two-factor/disable", + request, + true, + true + ); + return new TwoFactorProviderResponse(r); + } + + postTwoFactorRecover(request: TwoFactorRecoveryRequest): Promise { + return this.send("POST", "/two-factor/recover", request, false, false); + } + + postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise { + return this.send("POST", "/two-factor/send-email", request, true, false); + } + + postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { + return this.send("POST", "/two-factor/send-email-login", request, false, false); + } + + // Emergency Access APIs + + async getEmergencyAccessTrusted(): Promise> { + const r = await this.send("GET", "/emergency-access/trusted", null, true, true); + return new ListResponse(r, EmergencyAccessGranteeDetailsResponse); + } + + async getEmergencyAccessGranted(): Promise> { + const r = await this.send("GET", "/emergency-access/granted", null, true, true); + return new ListResponse(r, EmergencyAccessGrantorDetailsResponse); + } + + async getEmergencyAccess(id: string): Promise { + const r = await this.send("GET", "/emergency-access/" + id, null, true, true); + return new EmergencyAccessGranteeDetailsResponse(r); + } + + async getEmergencyGrantorPolicies(id: string): Promise> { + const r = await this.send("GET", "/emergency-access/" + id + "/policies", null, true, true); + return new ListResponse(r, PolicyResponse); + } + + putEmergencyAccess(id: string, request: EmergencyAccessUpdateRequest): Promise { + return this.send("PUT", "/emergency-access/" + id, request, true, false); + } + + deleteEmergencyAccess(id: string): Promise { + return this.send("DELETE", "/emergency-access/" + id, null, true, false); + } + + postEmergencyAccessInvite(request: EmergencyAccessInviteRequest): Promise { + return this.send("POST", "/emergency-access/invite", request, true, false); + } + + postEmergencyAccessReinvite(id: string): Promise { + return this.send("POST", "/emergency-access/" + id + "/reinvite", null, true, false); + } + + postEmergencyAccessAccept(id: string, request: EmergencyAccessAcceptRequest): Promise { + return this.send("POST", "/emergency-access/" + id + "/accept", request, true, false); + } + + postEmergencyAccessConfirm(id: string, request: EmergencyAccessConfirmRequest): Promise { + return this.send("POST", "/emergency-access/" + id + "/confirm", request, true, false); + } + + postEmergencyAccessInitiate(id: string): Promise { + return this.send("POST", "/emergency-access/" + id + "/initiate", null, true, false); + } + + postEmergencyAccessApprove(id: string): Promise { + return this.send("POST", "/emergency-access/" + id + "/approve", null, true, false); + } + + postEmergencyAccessReject(id: string): Promise { + return this.send("POST", "/emergency-access/" + id + "/reject", null, true, false); + } + + async postEmergencyAccessTakeover(id: string): Promise { + const r = await this.send("POST", "/emergency-access/" + id + "/takeover", null, true, true); + return new EmergencyAccessTakeoverResponse(r); + } + + async postEmergencyAccessPassword( + id: string, + request: EmergencyAccessPasswordRequest + ): Promise { + const r = await this.send("POST", "/emergency-access/" + id + "/password", request, true, true); + } + + async postEmergencyAccessView(id: string): Promise { + const r = await this.send("POST", "/emergency-access/" + id + "/view", null, true, true); + return new EmergencyAccessViewResponse(r); + } + + // Organization APIs + + async getOrganization(id: string): Promise { + const r = await this.send("GET", "/organizations/" + id, null, true, true); + return new OrganizationResponse(r); + } + + async getOrganizationBilling(id: string): Promise { + const r = await this.send("GET", "/organizations/" + id + "/billing", null, true, true); + return new BillingResponse(r); + } + + async getOrganizationSubscription(id: string): Promise { + const r = await this.send("GET", "/organizations/" + id + "/subscription", null, true, true); + return new OrganizationSubscriptionResponse(r); + } + + async getOrganizationLicense(id: string, installationId: string): Promise { + return this.send( + "GET", + "/organizations/" + id + "/license?installationId=" + installationId, + null, + true, + true + ); + } + + async getOrganizationTaxInfo(id: string): Promise { + const r = await this.send("GET", "/organizations/" + id + "/tax", null, true, true); + return new TaxInfoResponse(r); + } + + async getOrganizationSso(id: string): Promise { + const r = await this.send("GET", "/organizations/" + id + "/sso", null, true, true); + return new OrganizationSsoResponse(r); + } + + async postOrganization(request: OrganizationCreateRequest): Promise { + const r = await this.send("POST", "/organizations", request, true, true); + return new OrganizationResponse(r); + } + + async putOrganization( + id: string, + request: OrganizationUpdateRequest + ): Promise { + const r = await this.send("PUT", "/organizations/" + id, request, true, true); + return new OrganizationResponse(r); + } + + async putOrganizationTaxInfo( + id: string, + request: OrganizationTaxInfoUpdateRequest + ): Promise { + return this.send("PUT", "/organizations/" + id + "/tax", request, true, false); + } + + postLeaveOrganization(id: string): Promise { + return this.send("POST", "/organizations/" + id + "/leave", null, true, false); + } + + async postOrganizationLicense(data: FormData): Promise { + const r = await this.send("POST", "/organizations/license", data, true, true); + return new OrganizationResponse(r); + } + + async postOrganizationLicenseUpdate(id: string, data: FormData): Promise { + return this.send("POST", "/organizations/" + id + "/license", data, true, false); + } + + async postOrganizationApiKey( + id: string, + request: SecretVerificationRequest + ): Promise { + const r = await this.send("POST", "/organizations/" + id + "/api-key", request, true, true); + return new ApiKeyResponse(r); + } + + async postOrganizationRotateApiKey( + id: string, + request: SecretVerificationRequest + ): Promise { + const r = await this.send( + "POST", + "/organizations/" + id + "/rotate-api-key", + request, + true, + true + ); + return new ApiKeyResponse(r); + } + + async postOrganizationSso( + id: string, + request: OrganizationSsoRequest + ): Promise { + const r = await this.send("POST", "/organizations/" + id + "/sso", request, true, true); + return new OrganizationSsoResponse(r); + } + + async postOrganizationUpgrade( + id: string, + request: OrganizationUpgradeRequest + ): Promise { + const r = await this.send("POST", "/organizations/" + id + "/upgrade", request, true, true); + return new PaymentResponse(r); + } + + async postOrganizationUpdateSubscription( + id: string, + request: OrganizationSubscriptionUpdateRequest + ): Promise { + return this.send("POST", "/organizations/" + id + "/subscription", request, true, false); + } + + async postOrganizationSeat(id: string, request: SeatRequest): Promise { + const r = await this.send("POST", "/organizations/" + id + "/seat", request, true, true); + return new PaymentResponse(r); + } + + async postOrganizationStorage(id: string, request: StorageRequest): Promise { + const r = await this.send("POST", "/organizations/" + id + "/storage", request, true, true); + return new PaymentResponse(r); + } + + postOrganizationPayment(id: string, request: PaymentRequest): Promise { + return this.send("POST", "/organizations/" + id + "/payment", request, true, false); + } + + postOrganizationVerifyBank(id: string, request: VerifyBankRequest): Promise { + return this.send("POST", "/organizations/" + id + "/verify-bank", request, true, false); + } + + postOrganizationCancel(id: string): Promise { + return this.send("POST", "/organizations/" + id + "/cancel", null, true, false); + } + + postOrganizationReinstate(id: string): Promise { + return this.send("POST", "/organizations/" + id + "/reinstate", null, true, false); + } + + deleteOrganization(id: string, request: SecretVerificationRequest): Promise { + return this.send("DELETE", "/organizations/" + id, request, true, false); + } + + async getOrganizationKeys(id: string): Promise { + const r = await this.send("GET", "/organizations/" + id + "/keys", null, true, true); + return new OrganizationKeysResponse(r); + } + + async postOrganizationKeys( + id: string, + request: OrganizationKeysRequest + ): Promise { + const r = await this.send("POST", "/organizations/" + id + "/keys", request, true, true); + return new OrganizationKeysResponse(r); + } + + // Provider APIs + + async postProviderSetup(id: string, request: ProviderSetupRequest) { + const r = await this.send("POST", "/providers/" + id + "/setup", request, true, true); + return new ProviderResponse(r); + } + + async getProvider(id: string) { + const r = await this.send("GET", "/providers/" + id, null, true, true); + return new ProviderResponse(r); + } + + async putProvider(id: string, request: ProviderUpdateRequest) { + const r = await this.send("PUT", "/providers/" + id, request, true, true); + return new ProviderResponse(r); + } + + // Provider User APIs + + async getProviderUsers( + providerId: string + ): Promise> { + const r = await this.send("GET", "/providers/" + providerId + "/users", null, true, true); + return new ListResponse(r, ProviderUserUserDetailsResponse); + } + + async getProviderUser(providerId: string, id: string): Promise { + const r = await this.send("GET", "/providers/" + providerId + "/users/" + id, null, true, true); + return new ProviderUserResponse(r); + } + + postProviderUserInvite(providerId: string, request: ProviderUserInviteRequest): Promise { + return this.send("POST", "/providers/" + providerId + "/users/invite", request, true, false); + } + + postProviderUserReinvite(providerId: string, id: string): Promise { + return this.send( + "POST", + "/providers/" + providerId + "/users/" + id + "/reinvite", + null, + true, + false + ); + } + + async postManyProviderUserReinvite( + providerId: string, + request: ProviderUserBulkRequest + ): Promise> { + const r = await this.send( + "POST", + "/providers/" + providerId + "/users/reinvite", + request, + true, + true + ); + return new ListResponse(r, ProviderUserBulkResponse); + } + + async postProviderUserBulkConfirm( + providerId: string, + request: ProviderUserBulkConfirmRequest + ): Promise> { + const r = await this.send( + "POST", + "/providers/" + providerId + "/users/confirm", + request, + true, + true + ); + return new ListResponse(r, ProviderUserBulkResponse); + } + + async deleteManyProviderUsers( + providerId: string, + request: ProviderUserBulkRequest + ): Promise> { + const r = await this.send("DELETE", "/providers/" + providerId + "/users", request, true, true); + return new ListResponse(r, ProviderUserBulkResponse); + } + + postProviderUserAccept( + providerId: string, + id: string, + request: ProviderUserAcceptRequest + ): Promise { + return this.send( + "POST", + "/providers/" + providerId + "/users/" + id + "/accept", + request, + true, + false + ); + } + + postProviderUserConfirm( + providerId: string, + id: string, + request: ProviderUserConfirmRequest + ): Promise { + return this.send( + "POST", + "/providers/" + providerId + "/users/" + id + "/confirm", + request, + true, + false + ); + } + + async postProviderUsersPublicKey( + providerId: string, + request: ProviderUserBulkRequest + ): Promise> { + const r = await this.send( + "POST", + "/providers/" + providerId + "/users/public-keys", + request, + true, + true + ); + return new ListResponse(r, ProviderUserBulkPublicKeyResponse); + } + + putProviderUser( + providerId: string, + id: string, + request: ProviderUserUpdateRequest + ): Promise { + return this.send("PUT", "/providers/" + providerId + "/users/" + id, request, true, false); + } + + deleteProviderUser(providerId: string, id: string): Promise { + return this.send("DELETE", "/providers/" + providerId + "/users/" + id, null, true, false); + } + + // Provider Organization APIs + + async getProviderClients( + providerId: string + ): Promise> { + const r = await this.send( + "GET", + "/providers/" + providerId + "/organizations", + null, + true, + true + ); + return new ListResponse(r, ProviderOrganizationOrganizationDetailsResponse); + } + + postProviderAddOrganization( + providerId: string, + request: ProviderAddOrganizationRequest + ): Promise { + return this.send( + "POST", + "/providers/" + providerId + "/organizations/add", + request, + true, + false + ); + } + + async postProviderCreateOrganization( + providerId: string, + request: ProviderOrganizationCreateRequest + ): Promise { + const r = await this.send( + "POST", + "/providers/" + providerId + "/organizations", + request, + true, + true + ); + return new ProviderOrganizationResponse(r); + } + + deleteProviderOrganization(providerId: string, id: string): Promise { + return this.send( + "DELETE", + "/providers/" + providerId + "/organizations/" + id, + null, + true, + false + ); + } + + // Event APIs + + async getEvents(start: string, end: string, token: string): Promise> { + const r = await this.send( + "GET", + this.addEventParameters("/events", start, end, token), + null, + true, + true + ); + return new ListResponse(r, EventResponse); + } + + async getEventsCipher( + id: string, + start: string, + end: string, + token: string + ): Promise> { + const r = await this.send( + "GET", + this.addEventParameters("/ciphers/" + id + "/events", start, end, token), + null, + true, + true + ); + return new ListResponse(r, EventResponse); + } + + async getEventsOrganization( + id: string, + start: string, + end: string, + token: string + ): Promise> { + const r = await this.send( + "GET", + this.addEventParameters("/organizations/" + id + "/events", start, end, token), + null, + true, + true + ); + return new ListResponse(r, EventResponse); + } + + async getEventsOrganizationUser( + organizationId: string, + id: string, + start: string, + end: string, + token: string + ): Promise> { + const r = await this.send( + "GET", + this.addEventParameters( + "/organizations/" + organizationId + "/users/" + id + "/events", + start, + end, + token + ), + null, + true, + true + ); + return new ListResponse(r, EventResponse); + } + + async getEventsProvider( + id: string, + start: string, + end: string, + token: string + ): Promise> { + const r = await this.send( + "GET", + this.addEventParameters("/providers/" + id + "/events", start, end, token), + null, + true, + true + ); + return new ListResponse(r, EventResponse); + } + + async getEventsProviderUser( + providerId: string, + id: string, + start: string, + end: string, + token: string + ): Promise> { + const r = await this.send( + "GET", + this.addEventParameters( + "/providers/" + providerId + "/users/" + id + "/events", + start, + end, + token + ), + null, + true, + true + ); + return new ListResponse(r, EventResponse); + } + + async postEventsCollect(request: EventRequest[]): Promise { + const authHeader = await this.getActiveBearerToken(); + const headers = new Headers({ + "Device-Type": this.deviceType, + Authorization: "Bearer " + authHeader, + "Content-Type": "application/json; charset=utf-8", + }); + if (this.customUserAgent != null) { + headers.set("User-Agent", this.customUserAgent); + } + const response = await this.fetch( + new Request(this.environmentService.getEventsUrl() + "/collect", { + cache: "no-store", + credentials: this.getCredentials(), + method: "POST", + body: JSON.stringify(request), + headers: headers, + }) + ); + if (response.status !== 200) { + return Promise.reject("Event post failed."); + } + } + + // User APIs + + async getUserPublicKey(id: string): Promise { + const r = await this.send("GET", "/users/" + id + "/public-key", null, true, true); + return new UserKeyResponse(r); + } + + // HIBP APIs + + async getHibpBreach(username: string): Promise { + const r = await this.send("GET", "/hibp/breach?username=" + username, null, true, true); + return r.map((a: any) => new BreachAccountResponse(a)); + } + + // Misc + + async postBitPayInvoice(request: BitPayInvoiceRequest): Promise { + const r = await this.send("POST", "/bitpay-invoice", request, true, true); + return r as string; + } + + async postSetupPayment(): Promise { + const r = await this.send("POST", "/setup-payment", null, true, true); + return r as string; + } + + // Key Connector + + async getUserKeyFromKeyConnector(keyConnectorUrl: string): Promise { + const authHeader = await this.getActiveBearerToken(); + + const response = await this.fetch( + new Request(keyConnectorUrl + "/user-keys", { + cache: "no-store", + method: "GET", + headers: new Headers({ + Accept: "application/json", + Authorization: "Bearer " + authHeader, + }), + }) + ); + + if (response.status !== 200) { + const error = await this.handleError(response, false, true); + return Promise.reject(error); + } + + return new KeyConnectorUserKeyResponse(await response.json()); + } + + async postUserKeyToKeyConnector( + keyConnectorUrl: string, + request: KeyConnectorUserKeyRequest + ): Promise { + const authHeader = await this.getActiveBearerToken(); + + const response = await this.fetch( + new Request(keyConnectorUrl + "/user-keys", { + cache: "no-store", + method: "POST", + headers: new Headers({ + Accept: "application/json", + Authorization: "Bearer " + authHeader, + "Content-Type": "application/json; charset=utf-8", + }), + body: JSON.stringify(request), + }) + ); + + if (response.status !== 200) { + const error = await this.handleError(response, false, true); + return Promise.reject(error); + } + } + + async getKeyConnectorAlive(keyConnectorUrl: string) { + const response = await this.fetch( + new Request(keyConnectorUrl + "/alive", { + cache: "no-store", + method: "GET", + headers: new Headers({ + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }), + }) + ); + + if (response.status !== 200) { + const error = await this.handleError(response, false, true); + return Promise.reject(error); + } + } + + // Helpers + + async getActiveBearerToken(): Promise { + let accessToken = await this.tokenService.getToken(); + if (await this.tokenService.tokenNeedsRefresh()) { + await this.doAuthRefresh(); + accessToken = await this.tokenService.getToken(); + } + return accessToken; + } + + fetch(request: Request): Promise { + if (request.method === "GET") { + request.headers.set("Cache-Control", "no-store"); + request.headers.set("Pragma", "no-cache"); + } + return this.nativeFetch(request); + } + + nativeFetch(request: Request): Promise { + return fetch(request); + } + + async preValidateSso(identifier: string): Promise { + if (identifier == null || identifier === "") { + throw new Error("Organization Identifier was not provided."); + } + const headers = new Headers({ + Accept: "application/json", + "Device-Type": this.deviceType, + }); + if (this.customUserAgent != null) { + headers.set("User-Agent", this.customUserAgent); + } + + const path = `/account/prevalidate?domainHint=${encodeURIComponent(identifier)}`; + const response = await this.fetch( + new Request(this.environmentService.getIdentityUrl() + path, { + cache: "no-store", + credentials: this.getCredentials(), + headers: headers, + method: "GET", + }) + ); + + if (response.status === 200) { + return true; + } else { + const error = await this.handleError(response, false, true); + return Promise.reject(error); + } + } + + async postCreateSponsorship( + sponsoredOrgId: string, + request: OrganizationSponsorshipCreateRequest + ): Promise { + return await this.send( + "POST", + "/organization/sponsorship/" + sponsoredOrgId + "/families-for-enterprise", + request, + true, + false + ); + } + + async deleteRevokeSponsorship(sponsoringOrganizationId: string): Promise { + return await this.send( + "DELETE", + "/organization/sponsorship/" + sponsoringOrganizationId, + null, + true, + false + ); + } + + async deleteRemoveSponsorship(sponsoringOrgId: string): Promise { + return await this.send( + "DELETE", + "/organization/sponsorship/sponsored/" + sponsoringOrgId, + null, + true, + false + ); + } + + async postPreValidateSponsorshipToken(sponsorshipToken: string): Promise { + const r = await this.send( + "POST", + "/organization/sponsorship/validate-token?sponsorshipToken=" + + encodeURIComponent(sponsorshipToken), + null, + true, + true + ); + return r as boolean; + } + + async postRedeemSponsorship( + sponsorshipToken: string, + request: OrganizationSponsorshipRedeemRequest + ): Promise { + return await this.send( + "POST", + "/organization/sponsorship/redeem?sponsorshipToken=" + encodeURIComponent(sponsorshipToken), + request, + true, + false + ); + } + + async postResendSponsorshipOffer(sponsoringOrgId: string): Promise { + return await this.send( + "POST", + "/organization/sponsorship/" + sponsoringOrgId + "/families-for-enterprise/resend", + null, + true, + false + ); + } + + protected async doAuthRefresh(): Promise { + const refreshToken = await this.tokenService.getRefreshToken(); + if (refreshToken != null && refreshToken !== "") { + return this.doRefreshToken(); + } + + const clientId = await this.tokenService.getClientId(); + const clientSecret = await this.tokenService.getClientSecret(); + if (!Utils.isNullOrWhitespace(clientId) && !Utils.isNullOrWhitespace(clientSecret)) { + return this.doApiTokenRefresh(); + } + + throw new Error("Cannot refresh token, no refresh token or api keys are stored"); + } + + protected async doApiTokenRefresh(): Promise { + const clientId = await this.tokenService.getClientId(); + const clientSecret = await this.tokenService.getClientSecret(); + if ( + Utils.isNullOrWhitespace(clientId) || + Utils.isNullOrWhitespace(clientSecret) || + this.apiKeyRefresh == null + ) { + throw new Error(); + } + + await this.apiKeyRefresh(clientId, clientSecret); + } + + protected async doRefreshToken(): Promise { + const refreshToken = await this.tokenService.getRefreshToken(); + if (refreshToken == null || refreshToken === "") { + throw new Error(); + } + const headers = new Headers({ + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + Accept: "application/json", + "Device-Type": this.deviceType, + }); + if (this.customUserAgent != null) { + headers.set("User-Agent", this.customUserAgent); + } + + const decodedToken = await this.tokenService.decodeToken(); + const response = await this.fetch( + new Request(this.environmentService.getIdentityUrl() + "/connect/token", { + body: this.qsStringify({ + grant_type: "refresh_token", + client_id: decodedToken.client_id, + refresh_token: refreshToken, + }), + cache: "no-store", + credentials: this.getCredentials(), + headers: headers, + method: "POST", + }) + ); + + if (response.status === 200) { + const responseJson = await response.json(); + const tokenResponse = new IdentityTokenResponse(responseJson); + await this.tokenService.setTokens( + tokenResponse.accessToken, + tokenResponse.refreshToken, + null + ); + } else { + const error = await this.handleError(response, true, true); + return Promise.reject(error); + } + } + + private async send( + method: "GET" | "POST" | "PUT" | "DELETE", + path: string, + body: any, + authed: boolean, + hasResponse: boolean, + apiUrl?: string, + alterHeaders?: (headers: Headers) => void + ): Promise { + apiUrl = Utils.isNullOrWhitespace(apiUrl) ? this.environmentService.getApiUrl() : apiUrl; + + const requestUrl = apiUrl + path; + // Prevent directory traversal from malicious paths + if (new URL(requestUrl).href !== requestUrl) { + return Promise.reject("Invalid request url path."); + } + + const headers = new Headers({ + "Device-Type": this.deviceType, + }); + if (this.customUserAgent != null) { + headers.set("User-Agent", this.customUserAgent); + } + + const requestInit: RequestInit = { + cache: "no-store", + credentials: this.getCredentials(), + method: method, + }; + + if (authed) { + const authHeader = await this.getActiveBearerToken(); + headers.set("Authorization", "Bearer " + authHeader); + } + if (body != null) { + if (typeof body === "string") { + requestInit.body = body; + headers.set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + } else if (typeof body === "object") { + if (body instanceof FormData) { + requestInit.body = body; } else { - const error = await this.handleError(response, false, true); - return Promise.reject(error); + headers.set("Content-Type", "application/json; charset=utf-8"); + requestInit.body = JSON.stringify(body); } + } + } + if (hasResponse) { + headers.set("Accept", "application/json"); + } + if (alterHeaders != null) { + alterHeaders(headers); } - async postCreateSponsorship(sponsoredOrgId: string, request: OrganizationSponsorshipCreateRequest): Promise { - return await this.send('POST', - '/organization/sponsorship/' + sponsoredOrgId + '/families-for-enterprise', - request, true, false); + requestInit.headers = headers; + const response = await this.fetch(new Request(requestUrl, requestInit)); + + if (hasResponse && response.status === 200) { + const responseJson = await response.json(); + return responseJson; + } else if (response.status !== 200) { + const error = await this.handleError(response, false, authed); + return Promise.reject(error); + } + } + + private async handleError( + response: Response, + tokenError: boolean, + authed: boolean + ): Promise { + if ( + authed && + ((tokenError && response.status === 400) || + response.status === 401 || + response.status === 403) + ) { + await this.logoutCallback(true); + return null; } - async deleteRevokeSponsorship(sponsoringOrganizationId: string): Promise { - return await this.send('DELETE', - '/organization/sponsorship/' + sponsoringOrganizationId, - null, true, false); + let responseJson: any = null; + if (this.isJsonResponse(response)) { + responseJson = await response.json(); + } else if (this.isTextResponse(response)) { + responseJson = { Message: await response.text() }; } - async deleteRemoveSponsorship(sponsoringOrgId: string): Promise { - return await this.send('DELETE', - '/organization/sponsorship/sponsored/' + sponsoringOrgId, - null, true, false); + return new ErrorResponse(responseJson, response.status, tokenError); + } + + private qsStringify(params: any): string { + return Object.keys(params) + .map((key) => { + return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + }) + .join("&"); + } + + private getCredentials(): RequestCredentials { + if (!this.isWebClient || this.environmentService.hasBaseUrl()) { + return "include"; } + return undefined; + } - async postPreValidateSponsorshipToken(sponsorshipToken: string): Promise { - const r = await this.send('POST', '/organization/sponsorship/validate-token?sponsorshipToken=' + encodeURIComponent(sponsorshipToken), - null, true, true); - return r as boolean; + private addEventParameters(base: string, start: string, end: string, token: string) { + if (start != null) { + base += "?start=" + start; } - - async postRedeemSponsorship(sponsorshipToken: string, request: OrganizationSponsorshipRedeemRequest): Promise { - return await this.send('POST', '/organization/sponsorship/redeem?sponsorshipToken=' + encodeURIComponent(sponsorshipToken), - request, true, false); + if (end != null) { + base += base.indexOf("?") > -1 ? "&" : "?"; + base += "end=" + end; } - - async postResendSponsorshipOffer(sponsoringOrgId: string): Promise { - return await this.send('POST', - '/organization/sponsorship/' + sponsoringOrgId + '/families-for-enterprise/resend', - null, true, false); + if (token != null) { + base += base.indexOf("?") > -1 ? "&" : "?"; + base += "continuationToken=" + token; } + return base; + } + private isJsonResponse(response: Response): boolean { + const typeHeader = response.headers.get("content-type"); + return typeHeader != null && typeHeader.indexOf("application/json") > -1; + } - protected async doAuthRefresh(): Promise { - const refreshToken = await this.tokenService.getRefreshToken(); - if (refreshToken != null && refreshToken !== '') { - return this.doRefreshToken(); - } - - const clientId = await this.tokenService.getClientId(); - const clientSecret = await this.tokenService.getClientSecret(); - if (!Utils.isNullOrWhitespace(clientId) && !Utils.isNullOrWhitespace(clientSecret)) { - return this.doApiTokenRefresh(); - } - - throw new Error('Cannot refresh token, no refresh token or api keys are stored'); - } - - protected async doApiTokenRefresh(): Promise { - const clientId = await this.tokenService.getClientId(); - const clientSecret = await this.tokenService.getClientSecret(); - if (Utils.isNullOrWhitespace(clientId) || Utils.isNullOrWhitespace(clientSecret) || this.apiKeyRefresh == null) { - throw new Error(); - } - - await this.apiKeyRefresh(clientId, clientSecret); - } - - protected async doRefreshToken(): Promise { - const refreshToken = await this.tokenService.getRefreshToken(); - if (refreshToken == null || refreshToken === '') { - throw new Error(); - } - const headers = new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Accept': 'application/json', - 'Device-Type': this.deviceType, - }); - if (this.customUserAgent != null) { - headers.set('User-Agent', this.customUserAgent); - } - - const decodedToken = this.tokenService.decodeToken(); - const response = await this.fetch(new Request(this.environmentService.getIdentityUrl() + '/connect/token', { - body: this.qsStringify({ - grant_type: 'refresh_token', - client_id: decodedToken.client_id, - refresh_token: refreshToken, - }), - cache: 'no-store', - credentials: this.getCredentials(), - headers: headers, - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - const tokenResponse = new IdentityTokenResponse(responseJson); - await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken, null); - } else { - const error = await this.handleError(response, true, true); - return Promise.reject(error); - } - } - - private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, - authed: boolean, hasResponse: boolean, apiUrl?: string, - alterHeaders?: (headers: Headers) => void): Promise { - apiUrl = Utils.isNullOrWhitespace(apiUrl) ? this.environmentService.getApiUrl() : apiUrl; - - const requestUrl = apiUrl + path; - // Prevent directory traversal from malicious paths - if (new URL(requestUrl).href !== requestUrl) { - return Promise.reject('Invalid request url path.'); - } - - const headers = new Headers({ - 'Device-Type': this.deviceType, - }); - if (this.customUserAgent != null) { - headers.set('User-Agent', this.customUserAgent); - } - - const requestInit: RequestInit = { - cache: 'no-store', - credentials: this.getCredentials(), - method: method, - }; - - if (authed) { - const authHeader = await this.getActiveBearerToken(); - headers.set('Authorization', 'Bearer ' + authHeader); - } - if (body != null) { - if (typeof body === 'string') { - requestInit.body = body; - headers.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8'); - } else if (typeof body === 'object') { - if (body instanceof FormData) { - requestInit.body = body; - } else { - headers.set('Content-Type', 'application/json; charset=utf-8'); - requestInit.body = JSON.stringify(body); - } - } - } - if (hasResponse) { - headers.set('Accept', 'application/json'); - } - if (alterHeaders != null) { - alterHeaders(headers); - } - - requestInit.headers = headers; - const response = await this.fetch(new Request(requestUrl, requestInit)); - - if (hasResponse && response.status === 200) { - const responseJson = await response.json(); - return responseJson; - } else if (response.status !== 200) { - const error = await this.handleError(response, false, authed); - return Promise.reject(error); - } - } - - private async handleError(response: Response, tokenError: boolean, authed: boolean): Promise { - if (authed && ((tokenError && response.status === 400) || response.status === 401 || response.status === 403)) { - await this.logoutCallback(true); - return null; - } - - let responseJson: any = null; - if (this.isJsonResponse(response)) { - responseJson = await response.json(); - } else if (this.isTextResponse(response)) { - responseJson = { Message: await response.text() }; - } - - return new ErrorResponse(responseJson, response.status, tokenError); - } - - private qsStringify(params: any): string { - return Object.keys(params).map(key => { - return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); - }).join('&'); - } - - private getCredentials(): RequestCredentials { - if (!this.isWebClient || this.environmentService.hasBaseUrl()) { - return 'include'; - } - return undefined; - } - - private addEventParameters(base: string, start: string, end: string, token: string) { - if (start != null) { - base += ('?start=' + start); - } - if (end != null) { - base += (base.indexOf('?') > -1 ? '&' : '?'); - base += ('end=' + end); - } - if (token != null) { - base += (base.indexOf('?') > -1 ? '&' : '?'); - base += ('continuationToken=' + token); - } - return base; - } - - private isJsonResponse(response: Response): boolean { - const typeHeader = response.headers.get('content-type'); - return typeHeader != null && typeHeader.indexOf('application/json') > -1; - } - - private isTextResponse(response: Response): boolean { - const typeHeader = response.headers.get('content-type'); - return typeHeader != null && typeHeader.indexOf('text') > -1; - } + private isTextResponse(response: Response): boolean { + const typeHeader = response.headers.get("content-type"); + return typeHeader != null && typeHeader.indexOf("text") > -1; + } } diff --git a/common/src/services/apiKey.service.ts b/common/src/services/apiKey.service.ts deleted file mode 100644 index 0b67781b..00000000 --- a/common/src/services/apiKey.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ApiKeyService as ApiKeyServiceAbstraction } from '../abstractions/apiKey.service'; -import { StorageService } from '../abstractions/storage.service'; -import { TokenService } from '../abstractions/token.service'; - -import { Utils } from '../misc/utils'; - -const Keys = { - clientId: 'clientId', - clientSecret: 'clientSecret', - entityType: 'entityType', - entityId: 'entityId', -}; - - -export class ApiKeyService implements ApiKeyServiceAbstraction { - private clientId: string; - private clientSecret: string; - private entityType: string; - private entityId: string; - - constructor(private tokenService: TokenService, private storageService: StorageService) { } - - async setInformation(clientId: string, clientSecret: string) { - this.clientId = clientId; - this.clientSecret = clientSecret; - const idParts = clientId.split('.'); - - if (idParts.length !== 2 || !Utils.isGuid(idParts[1])) { - throw Error('Invalid clientId'); - } - this.entityType = idParts[0]; - this.entityId = idParts[1]; - - await this.storageService.save(Keys.clientId, this.clientId); - await this.storageService.save(Keys.entityId, this.entityId); - await this.storageService.save(Keys.entityType, this.entityType); - await this.storageService.save(Keys.clientSecret, this.clientSecret); - } - - async getClientId(): Promise { - if (this.clientId == null) { - this.clientId = await this.storageService.get(Keys.clientId); - } - return this.clientId; - } - - async getClientSecret(): Promise { - if (this.clientSecret == null) { - this.clientSecret = await this.storageService.get(Keys.clientSecret); - } - return this.clientSecret; - } - - async getEntityType(): Promise { - if (this.entityType == null) { - this.entityType = await this.storageService.get(Keys.entityType); - } - return this.entityType; - } - - async getEntityId(): Promise { - if (this.entityId == null) { - this.entityId = await this.storageService.get(Keys.entityId); - } - return this.entityId; - } - - async clear(): Promise { - await this.storageService.remove(Keys.clientId); - await this.storageService.remove(Keys.clientSecret); - await this.storageService.remove(Keys.entityId); - await this.storageService.remove(Keys.entityType); - - this.clientId = this.clientSecret = this.entityId = this.entityType = null; - } - - async isAuthenticated(): Promise { - const token = await this.tokenService.getToken(); - if (token == null) { - return false; - } - - const entityId = await this.getEntityId(); - return entityId != null; - } -} diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index e4f670d7..5e44c90d 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -1,438 +1,658 @@ -import { HashPurpose } from '../enums/hashPurpose'; -import { KdfType } from '../enums/kdfType'; -import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; +import { HashPurpose } from "../enums/hashPurpose"; +import { KdfType } from "../enums/kdfType"; +import { TwoFactorProviderType } from "../enums/twoFactorProviderType"; -import { AuthResult } from '../models/domain/authResult'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { Account, AccountData, AccountProfile, AccountTokens } from "../models/domain/account"; +import { AuthResult } from "../models/domain/authResult"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { SetKeyConnectorKeyRequest } from '../models/request/account/setKeyConnectorKeyRequest'; -import { DeviceRequest } from '../models/request/deviceRequest'; -import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; -import { KeysRequest } from '../models/request/keysRequest'; -import { PreloginRequest } from '../models/request/preloginRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; +import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest"; +import { DeviceRequest } from "../models/request/deviceRequest"; +import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest"; +import { KeysRequest } from "../models/request/keysRequest"; +import { PreloginRequest } from "../models/request/preloginRequest"; +import { TokenRequest } from "../models/request/tokenRequest"; -import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; -import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { IdentityTokenResponse } from "../models/response/identityTokenResponse"; +import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse"; -import { ApiService } from '../abstractions/api.service'; -import { AppIdService } from '../abstractions/appId.service'; -import { AuthService as AuthServiceAbstraction } from '../abstractions/auth.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; -import { EnvironmentService } from '../abstractions/environment.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { KeyConnectorService } from '../abstractions/keyConnector.service'; -import { LogService } from '../abstractions/log.service'; -import { MessagingService } from '../abstractions/messaging.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; -import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; +import { ApiService } from "../abstractions/api.service"; +import { AppIdService } from "../abstractions/appId.service"; +import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; +import { CryptoService } from "../abstractions/crypto.service"; +import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; +import { EnvironmentService } from "../abstractions/environment.service"; +import { I18nService } from "../abstractions/i18n.service"; +import { KeyConnectorService } from "../abstractions/keyConnector.service"; +import { LogService } from "../abstractions/log.service"; +import { MessagingService } from "../abstractions/messaging.service"; +import { PlatformUtilsService } from "../abstractions/platformUtils.service"; +import { StateService } from "../abstractions/state.service"; +import { TokenService } from "../abstractions/token.service"; +import { VaultTimeoutService } from "../abstractions/vaultTimeout.service"; -import { Utils } from '../misc/utils'; +import { Utils } from "../misc/utils"; export const TwoFactorProviders = { - [TwoFactorProviderType.Authenticator]: { - type: TwoFactorProviderType.Authenticator, - name: null as string, - description: null as string, - priority: 1, - sort: 1, - premium: false, - }, - [TwoFactorProviderType.Yubikey]: { - type: TwoFactorProviderType.Yubikey, - name: null as string, - description: null as string, - priority: 3, - sort: 2, - premium: true, - }, - [TwoFactorProviderType.Duo]: { - type: TwoFactorProviderType.Duo, - name: 'Duo', - description: null as string, - priority: 2, - sort: 3, - premium: true, - }, - [TwoFactorProviderType.OrganizationDuo]: { - type: TwoFactorProviderType.OrganizationDuo, - name: 'Duo (Organization)', - description: null as string, - priority: 10, - sort: 4, - premium: false, - }, - [TwoFactorProviderType.Email]: { - type: TwoFactorProviderType.Email, - name: null as string, - description: null as string, - priority: 0, - sort: 6, - premium: false, - }, - [TwoFactorProviderType.WebAuthn]: { - type: TwoFactorProviderType.WebAuthn, - name: null as string, - description: null as string, - priority: 4, - sort: 5, - premium: true, - }, + [TwoFactorProviderType.Authenticator]: { + type: TwoFactorProviderType.Authenticator, + name: null as string, + description: null as string, + priority: 1, + sort: 1, + premium: false, + }, + [TwoFactorProviderType.Yubikey]: { + type: TwoFactorProviderType.Yubikey, + name: null as string, + description: null as string, + priority: 3, + sort: 2, + premium: true, + }, + [TwoFactorProviderType.Duo]: { + type: TwoFactorProviderType.Duo, + name: "Duo", + description: null as string, + priority: 2, + sort: 3, + premium: true, + }, + [TwoFactorProviderType.OrganizationDuo]: { + type: TwoFactorProviderType.OrganizationDuo, + name: "Duo (Organization)", + description: null as string, + priority: 10, + sort: 4, + premium: false, + }, + [TwoFactorProviderType.Email]: { + type: TwoFactorProviderType.Email, + name: null as string, + description: null as string, + priority: 0, + sort: 6, + premium: false, + }, + [TwoFactorProviderType.WebAuthn]: { + type: TwoFactorProviderType.WebAuthn, + name: null as string, + description: null as string, + priority: 4, + sort: 5, + premium: true, + }, }; export class AuthService implements AuthServiceAbstraction { - email: string; - masterPasswordHash: string; - localMasterPasswordHash: string; - code: string; - codeVerifier: string; - ssoRedirectUrl: string; - clientId: string; - clientSecret: string; - twoFactorProvidersData: Map; - selectedTwoFactorProviderType: TwoFactorProviderType = null; - captchaToken: string; + email: string; + masterPasswordHash: string; + localMasterPasswordHash: string; + code: string; + codeVerifier: string; + ssoRedirectUrl: string; + clientId: string; + clientSecret: string; + twoFactorProvidersData: Map; + selectedTwoFactorProviderType: TwoFactorProviderType = null; + captchaToken: string; - private key: SymmetricCryptoKey; + private key: SymmetricCryptoKey; - constructor(private cryptoService: CryptoService, protected apiService: ApiService, - private userService: UserService, protected tokenService: TokenService, - protected appIdService: AppIdService, private i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, - private vaultTimeoutService: VaultTimeoutService, private logService: LogService, - private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService, - private keyConnectorService: KeyConnectorService, private setCryptoKeys = true) { + constructor( + private cryptoService: CryptoService, + protected apiService: ApiService, + protected tokenService: TokenService, + protected appIdService: AppIdService, + private i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + private messagingService: MessagingService, + private vaultTimeoutService: VaultTimeoutService, + private logService: LogService, + protected cryptoFunctionService: CryptoFunctionService, + private keyConnectorService: KeyConnectorService, + protected environmentService: EnvironmentService, + protected stateService: StateService, + private setCryptoKeys = true + ) {} + + init() { + TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t("emailTitle"); + TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t("emailDesc"); + + TwoFactorProviders[TwoFactorProviderType.Authenticator].name = + this.i18nService.t("authenticatorAppTitle"); + TwoFactorProviders[TwoFactorProviderType.Authenticator].description = + this.i18nService.t("authenticatorAppDesc"); + + TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t("duoDesc"); + + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name = + "Duo (" + this.i18nService.t("organization") + ")"; + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description = + this.i18nService.t("duoOrganizationDesc"); + + TwoFactorProviders[TwoFactorProviderType.WebAuthn].name = this.i18nService.t("webAuthnTitle"); + TwoFactorProviders[TwoFactorProviderType.WebAuthn].description = + this.i18nService.t("webAuthnDesc"); + + TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t("yubiKeyTitle"); + TwoFactorProviders[TwoFactorProviderType.Yubikey].description = + this.i18nService.t("yubiKeyDesc"); + } + + async logIn(email: string, masterPassword: string, captchaToken?: string): Promise { + this.selectedTwoFactorProviderType = null; + const key = await this.makePreloginKey(masterPassword, email); + const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); + const localHashedPassword = await this.cryptoService.hashPassword( + masterPassword, + key, + HashPurpose.LocalAuthorization + ); + return await this.logInHelper( + email, + hashedPassword, + localHashedPassword, + null, + null, + null, + null, + null, + key, + null, + null, + null, + captchaToken, + null + ); + } + + async logInSso( + code: string, + codeVerifier: string, + redirectUrl: string, + orgId: string + ): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper( + null, + null, + null, + code, + codeVerifier, + redirectUrl, + null, + null, + null, + null, + null, + null, + null, + orgId + ); + } + + async logInApiKey(clientId: string, clientSecret: string): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper( + null, + null, + null, + null, + null, + null, + clientId, + clientSecret, + null, + null, + null, + null, + null, + null + ); + } + + async logInTwoFactor( + twoFactorProvider: TwoFactorProviderType, + twoFactorToken: string, + remember?: boolean + ): Promise { + return await this.logInHelper( + this.email, + this.masterPasswordHash, + this.localMasterPasswordHash, + this.code, + this.codeVerifier, + this.ssoRedirectUrl, + this.clientId, + this.clientSecret, + this.key, + twoFactorProvider, + twoFactorToken, + remember, + this.captchaToken, + null + ); + } + + async logInComplete( + email: string, + masterPassword: string, + twoFactorProvider: TwoFactorProviderType, + twoFactorToken: string, + remember?: boolean, + captchaToken?: string + ): Promise { + this.selectedTwoFactorProviderType = null; + const key = await this.makePreloginKey(masterPassword, email); + const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); + const localHashedPassword = await this.cryptoService.hashPassword( + masterPassword, + key, + HashPurpose.LocalAuthorization + ); + return await this.logInHelper( + email, + hashedPassword, + localHashedPassword, + null, + null, + null, + null, + null, + key, + twoFactorProvider, + twoFactorToken, + remember, + captchaToken, + null + ); + } + + async logInSsoComplete( + code: string, + codeVerifier: string, + redirectUrl: string, + twoFactorProvider: TwoFactorProviderType, + twoFactorToken: string, + remember?: boolean + ): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper( + null, + null, + null, + code, + codeVerifier, + redirectUrl, + null, + null, + null, + twoFactorProvider, + twoFactorToken, + remember, + null, + null + ); + } + + async logInApiKeyComplete( + clientId: string, + clientSecret: string, + twoFactorProvider: TwoFactorProviderType, + twoFactorToken: string, + remember?: boolean + ): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper( + null, + null, + null, + null, + null, + null, + clientId, + clientSecret, + null, + twoFactorProvider, + twoFactorToken, + remember, + null, + null + ); + } + + logOut(callback: Function) { + callback(); + this.messagingService.send("loggedOut"); + } + + getSupportedTwoFactorProviders(win: Window): any[] { + const providers: any[] = []; + if (this.twoFactorProvidersData == null) { + return providers; } - init() { - TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t('emailTitle'); - TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t('emailDesc'); - - TwoFactorProviders[TwoFactorProviderType.Authenticator].name = this.i18nService.t('authenticatorAppTitle'); - TwoFactorProviders[TwoFactorProviderType.Authenticator].description = - this.i18nService.t('authenticatorAppDesc'); - - TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t('duoDesc'); - - TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name = - 'Duo (' + this.i18nService.t('organization') + ')'; - TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description = - this.i18nService.t('duoOrganizationDesc'); - - TwoFactorProviders[TwoFactorProviderType.WebAuthn].name = this.i18nService.t('webAuthnTitle'); - TwoFactorProviders[TwoFactorProviderType.WebAuthn].description = this.i18nService.t('webAuthnDesc'); - - TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t('yubiKeyTitle'); - TwoFactorProviders[TwoFactorProviderType.Yubikey].description = this.i18nService.t('yubiKeyDesc'); + if ( + this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) && + this.platformUtilsService.supportsDuo() + ) { + providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); } - async logIn(email: string, masterPassword: string, captchaToken?: string): Promise { - this.selectedTwoFactorProviderType = null; - const key = await this.makePreloginKey(masterPassword, email); - const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key, - HashPurpose.LocalAuthorization); - return await this.logInHelper(email, hashedPassword, localHashedPassword, null, null, null, null, null, - key, null, null, null, captchaToken, null); + if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); } - async logInSso(code: string, codeVerifier: string, redirectUrl: string, orgId: string): Promise { - this.selectedTwoFactorProviderType = null; - return await this.logInHelper(null, null, null, code, codeVerifier, redirectUrl, null, null, - null, null, null, null, null, orgId); + if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); } - async logInApiKey(clientId: string, clientSecret: string): Promise { - this.selectedTwoFactorProviderType = null; - return await this.logInHelper(null, null, null, null, null, null, clientId, clientSecret, - null, null, null, null, null, null); + if ( + this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) && + this.platformUtilsService.supportsDuo() + ) { + providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); } - async logInTwoFactor(twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, - remember?: boolean): Promise { - return await this.logInHelper(this.email, this.masterPasswordHash, this.localMasterPasswordHash, this.code, - this.codeVerifier, this.ssoRedirectUrl, this.clientId, this.clientSecret, this.key, twoFactorProvider, - twoFactorToken, remember, this.captchaToken, null); + if ( + this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) && + this.platformUtilsService.supportsWebAuthn(win) + ) { + providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]); } - async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, - twoFactorToken: string, remember?: boolean, captchaToken?: string): Promise { - this.selectedTwoFactorProviderType = null; - const key = await this.makePreloginKey(masterPassword, email); - const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - const localHashedPassword = await this.cryptoService.hashPassword(masterPassword, key, - HashPurpose.LocalAuthorization); - return await this.logInHelper(email, hashedPassword, localHashedPassword, null, null, null, null, null, key, - twoFactorProvider, twoFactorToken, remember, captchaToken, null); + if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); } - async logInSsoComplete(code: string, codeVerifier: string, redirectUrl: string, - twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { - this.selectedTwoFactorProviderType = null; - return await this.logInHelper(null, null, null, code, codeVerifier, redirectUrl, null, - null, null, twoFactorProvider, twoFactorToken, remember, null, null); + return providers; + } + + getDefaultTwoFactorProvider(webAuthnSupported: boolean): TwoFactorProviderType { + if (this.twoFactorProvidersData == null) { + return null; } - async logInApiKeyComplete(clientId: string, clientSecret: string, twoFactorProvider: TwoFactorProviderType, - twoFactorToken: string, remember?: boolean): Promise { - this.selectedTwoFactorProviderType = null; - return await this.logInHelper(null, null, null, null, null, null, clientId, clientSecret, null, - twoFactorProvider, twoFactorToken, remember, null, null); + if ( + this.selectedTwoFactorProviderType != null && + this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType) + ) { + return this.selectedTwoFactorProviderType; } - logOut(callback: Function) { - callback(); - this.messagingService.send('loggedOut'); - } - - getSupportedTwoFactorProviders(win: Window): any[] { - const providers: any[] = []; - if (this.twoFactorProvidersData == null) { - return providers; + let providerType: TwoFactorProviderType = null; + let providerPriority = -1; + this.twoFactorProvidersData.forEach((_value, type) => { + const provider = (TwoFactorProviders as any)[type]; + if (provider != null && provider.priority > providerPriority) { + if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) { + return; } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) && - this.platformUtilsService.supportsDuo()) { - providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); - } + providerType = type; + providerPriority = provider.priority; + } + }); - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) { - providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); - } + return providerType; + } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) { - providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); - } + async makePreloginKey(masterPassword: string, email: string): Promise { + email = email.trim().toLowerCase(); + let kdf: KdfType = null; + let kdfIterations: number = null; + try { + const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); + if (preloginResponse != null) { + kdf = preloginResponse.kdf; + kdfIterations = preloginResponse.kdfIterations; + } + } catch (e) { + if (e == null || e.statusCode !== 404) { + throw e; + } + } + return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); + } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) { - providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); - } + authingWithApiKey(): boolean { + return this.clientId != null && this.clientSecret != null; + } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) && this.platformUtilsService.supportsWebAuthn(win)) { - providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]); - } + authingWithSso(): boolean { + return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null; + } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) { - providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); - } + authingWithPassword(): boolean { + return this.email != null && this.masterPasswordHash != null; + } - return providers; + private async logInHelper( + email: string, + hashedPassword: string, + localHashedPassword: string, + code: string, + codeVerifier: string, + redirectUrl: string, + clientId: string, + clientSecret: string, + key: SymmetricCryptoKey, + twoFactorProvider?: TwoFactorProviderType, + twoFactorToken?: string, + remember?: boolean, + captchaToken?: string, + orgId?: string + ): Promise { + const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); + const appId = await this.appIdService.getAppId(); + const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); + + let emailPassword: string[] = []; + let codeCodeVerifier: string[] = []; + let clientIdClientSecret: [string, string] = [null, null]; + + if (email != null && hashedPassword != null) { + emailPassword = [email, hashedPassword]; + } else { + emailPassword = null; + } + if (code != null && codeVerifier != null && redirectUrl != null) { + codeCodeVerifier = [code, codeVerifier, redirectUrl]; + } else { + codeCodeVerifier = null; + } + if (clientId != null && clientSecret != null) { + clientIdClientSecret = [clientId, clientSecret]; + } else { + clientIdClientSecret = null; } - getDefaultTwoFactorProvider(webAuthnSupported: boolean): TwoFactorProviderType { - if (this.twoFactorProvidersData == null) { - return null; - } - - if (this.selectedTwoFactorProviderType != null && - this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType)) { - return this.selectedTwoFactorProviderType; - } - - let providerType: TwoFactorProviderType = null; - let providerPriority = -1; - this.twoFactorProvidersData.forEach((value, type) => { - const provider = (TwoFactorProviders as any)[type]; - if (provider != null && provider.priority > providerPriority) { - if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) { - return; - } - - providerType = type; - providerPriority = provider.priority; - } - }); - - return providerType; + let request: TokenRequest; + if (twoFactorToken != null && twoFactorProvider != null) { + request = new TokenRequest( + emailPassword, + codeCodeVerifier, + clientIdClientSecret, + twoFactorProvider, + twoFactorToken, + remember, + captchaToken, + deviceRequest + ); + } else if (storedTwoFactorToken != null) { + request = new TokenRequest( + emailPassword, + codeCodeVerifier, + clientIdClientSecret, + TwoFactorProviderType.Remember, + storedTwoFactorToken, + false, + captchaToken, + deviceRequest + ); + } else { + request = new TokenRequest( + emailPassword, + codeCodeVerifier, + clientIdClientSecret, + null, + null, + false, + captchaToken, + deviceRequest + ); } - async makePreloginKey(masterPassword: string, email: string): Promise { - email = email.trim().toLowerCase(); - let kdf: KdfType = null; - let kdfIterations: number = null; + const response = await this.apiService.postIdentityToken(request); + + this.clearState(); + const result = new AuthResult(); + result.captchaSiteKey = (response as any).siteKey; + if (!!result.captchaSiteKey) { + return result; + } + result.twoFactor = !!(response as any).twoFactorProviders2; + + if (result.twoFactor) { + // two factor required + this.email = email; + this.masterPasswordHash = hashedPassword; + this.localMasterPasswordHash = localHashedPassword; + this.code = code; + this.codeVerifier = codeVerifier; + this.ssoRedirectUrl = redirectUrl; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.key = this.setCryptoKeys ? key : null; + const twoFactorResponse = response as IdentityTwoFactorResponse; + this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2; + result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; + this.captchaToken = twoFactorResponse.captchaToken; + return result; + } + + const tokenResponse = response as IdentityTokenResponse; + result.resetMasterPassword = tokenResponse.resetMasterPassword; + result.forcePasswordReset = tokenResponse.forcePasswordReset; + + const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken); + await this.stateService.addAccount({ + profile: { + ...new AccountProfile(), + ...{ + userId: accountInformation.sub, + email: accountInformation.email, + apiKeyClientId: clientId, + apiKeyClientSecret: clientSecret, + hasPremiumPersonally: accountInformation.premium, + kdfIterations: tokenResponse.kdfIterations, + kdfType: tokenResponse.kdf, + }, + }, + tokens: { + ...new AccountTokens(), + ...{ + accessToken: tokenResponse.accessToken, + refreshToken: tokenResponse.refreshToken, + }, + }, + }); + + if (tokenResponse.twoFactorToken != null) { + await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); + } + + if (this.setCryptoKeys) { + if (key != null) { + await this.cryptoService.setKey(key); + } + if (localHashedPassword != null) { + await this.cryptoService.setKeyHash(localHashedPassword); + } + + // Skip this step during SSO new user flow. No key is returned from server. + if (code == null || tokenResponse.key != null) { + if (tokenResponse.keyConnectorUrl != null) { + await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl); + } else if (tokenResponse.apiUseKeyConnector) { + const keyConnectorUrl = this.environmentService.getKeyConnectorUrl(); + await this.keyConnectorService.getAndSetKey(keyConnectorUrl); + } + + await this.cryptoService.setEncKey(tokenResponse.key); + + // User doesn't have a key pair yet (old account), let's generate one for them + if (tokenResponse.privateKey == null) { + try { + const keyPair = await this.cryptoService.makeKeyPair(); + await this.apiService.postAccountKeys( + new KeysRequest(keyPair[0], keyPair[1].encryptedString) + ); + tokenResponse.privateKey = keyPair[1].encryptedString; + } catch (e) { + this.logService.error(e); + } + } + + await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); + } else if (tokenResponse.keyConnectorUrl != null) { + const password = await this.cryptoFunctionService.randomBytes(64); + + const k = await this.cryptoService.makeKey( + Utils.fromBufferToB64(password), + await this.tokenService.getEmail(), + tokenResponse.kdf, + tokenResponse.kdfIterations + ); + const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64); + await this.cryptoService.setKey(k); + + const encKey = await this.cryptoService.makeEncKey(k); + await this.cryptoService.setEncKey(encKey[1].encryptedString); + + const [pubKey, privKey] = await this.cryptoService.makeKeyPair(); + try { - const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); - if (preloginResponse != null) { - kdf = preloginResponse.kdf; - kdfIterations = preloginResponse.kdfIterations; - } + await this.apiService.postUserKeyToKeyConnector( + tokenResponse.keyConnectorUrl, + keyConnectorRequest + ); } catch (e) { - if (e == null || e.statusCode !== 404) { - throw e; - } + throw new Error("Unable to reach key connector"); } - return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); + + const keys = new KeysRequest(pubKey, privKey.encryptedString); + const setPasswordRequest = new SetKeyConnectorKeyRequest( + encKey[1].encryptedString, + tokenResponse.kdf, + tokenResponse.kdfIterations, + orgId, + keys + ); + await this.apiService.postSetKeyConnectorKey(setPasswordRequest); + } } - authingWithApiKey(): boolean { - return this.clientId != null && this.clientSecret != null; + if (this.vaultTimeoutService != null) { + await this.stateService.setBiometricLocked(false); } + this.messagingService.send("loggedIn"); + return result; + } - authingWithSso(): boolean { - return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null; - } - - authingWithPassword(): boolean { - return this.email != null && this.masterPasswordHash != null; - } - - private async logInHelper(email: string, hashedPassword: string, localHashedPassword: string, code: string, - codeVerifier: string, redirectUrl: string, clientId: string, clientSecret: string, key: SymmetricCryptoKey, - twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean, captchaToken?: string, - orgId?: string): Promise { - const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); - const appId = await this.appIdService.getAppId(); - const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); - - let emailPassword: string[] = []; - let codeCodeVerifier: string[] = []; - let clientIdClientSecret: [string, string] = [null, null]; - - if (email != null && hashedPassword != null) { - emailPassword = [email, hashedPassword]; - } else { - emailPassword = null; - } - if (code != null && codeVerifier != null && redirectUrl != null) { - codeCodeVerifier = [code, codeVerifier, redirectUrl]; - } else { - codeCodeVerifier = null; - } - if (clientId != null && clientSecret != null) { - clientIdClientSecret = [clientId, clientSecret]; - } else { - clientIdClientSecret = null; - } - - let request: TokenRequest; - if (twoFactorToken != null && twoFactorProvider != null) { - request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, twoFactorProvider, - twoFactorToken, remember, captchaToken, deviceRequest); - } else if (storedTwoFactorToken != null) { - request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, - TwoFactorProviderType.Remember, storedTwoFactorToken, false, captchaToken, deviceRequest); - } else { - request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, null, - null, false, captchaToken, deviceRequest); - } - - const response = await this.apiService.postIdentityToken(request); - - this.clearState(); - const result = new AuthResult(); - result.captchaSiteKey = (response as any).siteKey; - if (!!result.captchaSiteKey) { - return result; - } - result.twoFactor = !!(response as any).twoFactorProviders2; - - if (result.twoFactor) { - // two factor required - this.email = email; - this.masterPasswordHash = hashedPassword; - this.localMasterPasswordHash = localHashedPassword; - this.code = code; - this.codeVerifier = codeVerifier; - this.ssoRedirectUrl = redirectUrl; - this.clientId = clientId; - this.clientSecret = clientSecret; - this.key = this.setCryptoKeys ? key : null; - const twoFactorResponse = response as IdentityTwoFactorResponse; - this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2; - result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; - this.captchaToken = twoFactorResponse.captchaToken; - return result; - } - - const tokenResponse = response as IdentityTokenResponse; - result.resetMasterPassword = tokenResponse.resetMasterPassword; - result.forcePasswordReset = tokenResponse.forcePasswordReset; - if (tokenResponse.twoFactorToken != null) { - await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); - } - - await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken, clientIdClientSecret); - await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(), - tokenResponse.kdf, tokenResponse.kdfIterations); - if (this.setCryptoKeys) { - if (key != null) { - await this.cryptoService.setKey(key); - } - if (localHashedPassword != null) { - await this.cryptoService.setKeyHash(localHashedPassword); - } - - // Skip this step during SSO new user flow. No key is returned from server. - if (code == null || tokenResponse.key != null) { - - if (tokenResponse.keyConnectorUrl != null) { - await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl); - } else if (tokenResponse.apiUseKeyConnector) { - const keyConnectorUrl = this.environmentService.getKeyConnectorUrl(); - await this.keyConnectorService.getAndSetKey(keyConnectorUrl); - } - - await this.cryptoService.setEncKey(tokenResponse.key); - - // User doesn't have a key pair yet (old account), let's generate one for them - if (tokenResponse.privateKey == null) { - try { - const keyPair = await this.cryptoService.makeKeyPair(); - await this.apiService.postAccountKeys(new KeysRequest(keyPair[0], keyPair[1].encryptedString)); - tokenResponse.privateKey = keyPair[1].encryptedString; - } catch (e) { - this.logService.error(e); - } - } - - await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); - } else if (tokenResponse.keyConnectorUrl != null) { - const password = await this.cryptoFunctionService.randomBytes(64); - - const k = await this.cryptoService.makeKey(Utils.fromBufferToB64(password), this.tokenService.getEmail(), tokenResponse.kdf, tokenResponse.kdfIterations); - const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64); - await this.cryptoService.setKey(k); - - const encKey = await this.cryptoService.makeEncKey(k); - await this.cryptoService.setEncKey(encKey[1].encryptedString); - - const [pubKey, privKey] = await this.cryptoService.makeKeyPair(); - - try { - await this.apiService.postUserKeyToKeyConnector(tokenResponse.keyConnectorUrl, keyConnectorRequest); - } catch (e) { - throw new Error('Unable to reach key connector'); - } - - const keys = new KeysRequest(pubKey, privKey.encryptedString); - const setPasswordRequest = new SetKeyConnectorKeyRequest( - encKey[1].encryptedString, tokenResponse.kdf, tokenResponse.kdfIterations, orgId, keys - ); - await this.apiService.postSetKeyConnectorKey(setPasswordRequest); - } - } - - if (this.vaultTimeoutService != null) { - this.vaultTimeoutService.biometricLocked = false; - } - this.messagingService.send('loggedIn'); - return result; - } - - private clearState(): void { - this.key = null; - this.email = null; - this.masterPasswordHash = null; - this.localMasterPasswordHash = null; - this.code = null; - this.codeVerifier = null; - this.ssoRedirectUrl = null; - this.clientId = null; - this.clientSecret = null; - this.twoFactorProvidersData = null; - this.selectedTwoFactorProviderType = null; - } + private clearState(): void { + this.key = null; + this.email = null; + this.masterPasswordHash = null; + this.localMasterPasswordHash = null; + this.code = null; + this.codeVerifier = null; + this.ssoRedirectUrl = null; + this.clientId = null; + this.clientSecret = null; + this.twoFactorProvidersData = null; + this.selectedTwoFactorProviderType = null; + } } diff --git a/common/src/services/cipher.service.ts b/common/src/services/cipher.service.ts index 421f381a..00f94c91 100644 --- a/common/src/services/cipher.service.ts +++ b/common/src/services/cipher.service.ts @@ -1,1131 +1,1283 @@ -import { CipherType } from '../enums/cipherType'; -import { FieldType } from '../enums/fieldType'; -import { UriMatchType } from '../enums/uriMatchType'; +import { CipherType } from "../enums/cipherType"; +import { FieldType } from "../enums/fieldType"; +import { UriMatchType } from "../enums/uriMatchType"; -import { CipherData } from '../models/data/cipherData'; +import { CipherData } from "../models/data/cipherData"; -import { Attachment } from '../models/domain/attachment'; -import { Card } from '../models/domain/card'; -import { Cipher } from '../models/domain/cipher'; -import Domain from '../models/domain/domainBase'; -import { EncArrayBuffer } from '../models/domain/encArrayBuffer'; -import { EncString } from '../models/domain/encString'; -import { Field } from '../models/domain/field'; -import { Identity } from '../models/domain/identity'; -import { Login } from '../models/domain/login'; -import { LoginUri } from '../models/domain/loginUri'; -import { Password } from '../models/domain/password'; -import { SecureNote } from '../models/domain/secureNote'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { Attachment } from "../models/domain/attachment"; +import { Card } from "../models/domain/card"; +import { Cipher } from "../models/domain/cipher"; +import Domain from "../models/domain/domainBase"; +import { EncArrayBuffer } from "../models/domain/encArrayBuffer"; +import { EncString } from "../models/domain/encString"; +import { Field } from "../models/domain/field"; +import { Identity } from "../models/domain/identity"; +import { Login } from "../models/domain/login"; +import { LoginUri } from "../models/domain/loginUri"; +import { Password } from "../models/domain/password"; +import { SecureNote } from "../models/domain/secureNote"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { AttachmentRequest } from '../models/request/attachmentRequest'; -import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; -import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; -import { CipherBulkRestoreRequest } from '../models/request/cipherBulkRestoreRequest'; -import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; -import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; -import { CipherCreateRequest } from '../models/request/cipherCreateRequest'; -import { CipherRequest } from '../models/request/cipherRequest'; -import { CipherShareRequest } from '../models/request/cipherShareRequest'; +import { AttachmentRequest } from "../models/request/attachmentRequest"; +import { CipherBulkDeleteRequest } from "../models/request/cipherBulkDeleteRequest"; +import { CipherBulkMoveRequest } from "../models/request/cipherBulkMoveRequest"; +import { CipherBulkRestoreRequest } from "../models/request/cipherBulkRestoreRequest"; +import { CipherBulkShareRequest } from "../models/request/cipherBulkShareRequest"; +import { CipherCollectionsRequest } from "../models/request/cipherCollectionsRequest"; +import { CipherCreateRequest } from "../models/request/cipherCreateRequest"; +import { CipherRequest } from "../models/request/cipherRequest"; +import { CipherShareRequest } from "../models/request/cipherShareRequest"; -import { CipherResponse } from '../models/response/cipherResponse'; -import { ErrorResponse } from '../models/response/errorResponse'; +import { CipherResponse } from "../models/response/cipherResponse"; +import { ErrorResponse } from "../models/response/errorResponse"; -import { AttachmentView } from '../models/view/attachmentView'; -import { CipherView } from '../models/view/cipherView'; -import { FieldView } from '../models/view/fieldView'; -import { PasswordHistoryView } from '../models/view/passwordHistoryView'; -import { View } from '../models/view/view'; +import { AttachmentView } from "../models/view/attachmentView"; +import { CipherView } from "../models/view/cipherView"; +import { FieldView } from "../models/view/fieldView"; +import { PasswordHistoryView } from "../models/view/passwordHistoryView"; +import { View } from "../models/view/view"; -import { SortedCiphersCache } from '../models/domain/sortedCiphersCache'; +import { SortedCiphersCache } from "../models/domain/sortedCiphersCache"; -import { ApiService } from '../abstractions/api.service'; -import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { FileUploadService } from '../abstractions/fileUpload.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { SearchService } from '../abstractions/search.service'; -import { SettingsService } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { ApiService } from "../abstractions/api.service"; +import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; +import { CryptoService } from "../abstractions/crypto.service"; +import { FileUploadService } from "../abstractions/fileUpload.service"; +import { I18nService } from "../abstractions/i18n.service"; +import { SearchService } from "../abstractions/search.service"; +import { SettingsService } from "../abstractions/settings.service"; +import { StateService } from "../abstractions/state.service"; -import { ConstantsService } from './constants.service'; - -import { LogService } from '../abstractions/log.service'; -import { sequentialize } from '../misc/sequentialize'; -import { Utils } from '../misc/utils'; - -const Keys = { - ciphersPrefix: 'ciphers_', - localData: 'sitesLocalData', - neverDomains: 'neverDomains', -}; +import { LogService } from "../abstractions/log.service"; +import { sequentialize } from "../misc/sequentialize"; +import { Utils } from "../misc/utils"; const DomainMatchBlacklist = new Map>([ - ['google.com', new Set(['script.google.com'])], + ["google.com", new Set(["script.google.com"])], ]); export class CipherService implements CipherServiceAbstraction { - // tslint:disable-next-line - _decryptedCipherCache: CipherView[]; + private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( + this.sortCiphersByLastUsed + ); - private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(this.sortCiphersByLastUsed); + constructor( + private cryptoService: CryptoService, + private settingsService: SettingsService, + private apiService: ApiService, + private fileUploadService: FileUploadService, + private i18nService: I18nService, + private searchService: () => SearchService, + private logService: LogService, + private stateService: StateService + ) {} - constructor(private cryptoService: CryptoService, private userService: UserService, - private settingsService: SettingsService, private apiService: ApiService, - private fileUploadService: FileUploadService, private storageService: StorageService, - private i18nService: I18nService, private searchService: () => SearchService, - private logService: LogService) { + async getDecryptedCipherCache(): Promise { + const decryptedCiphers = await this.stateService.getDecryptedCiphers(); + return decryptedCiphers; + } + + async setDecryptedCipherCache(value: CipherView[]) { + await this.stateService.setDecryptedCiphers(value); + if (this.searchService != null) { + if (value == null) { + this.searchService().clearIndex(); + } else { + this.searchService().indexCiphers(); + } } + } - get decryptedCipherCache() { - return this._decryptedCipherCache; - } - set decryptedCipherCache(value: CipherView[]) { - this._decryptedCipherCache = value; - if (this.searchService != null) { - if (value == null) { - this.searchService().clearIndex(); - } else { - this.searchService().indexCiphers(); - } + async clearCache(userId?: string): Promise { + await this.clearDecryptedCiphersState(userId); + } + + async encrypt( + model: CipherView, + key?: SymmetricCryptoKey, + originalCipher: Cipher = null + ): Promise { + // Adjust password history + if (model.id != null) { + if (originalCipher == null) { + originalCipher = await this.get(model.id); + } + if (originalCipher != null) { + const existingCipher = await originalCipher.decrypt(); + model.passwordHistory = existingCipher.passwordHistory || []; + if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { + if ( + existingCipher.login.password != null && + existingCipher.login.password !== "" && + existingCipher.login.password !== model.login.password + ) { + const ph = new PasswordHistoryView(); + ph.password = existingCipher.login.password; + ph.lastUsedDate = model.login.passwordRevisionDate = new Date(); + model.passwordHistory.splice(0, 0, ph); + } else { + model.login.passwordRevisionDate = existingCipher.login.passwordRevisionDate; + } } + if (existingCipher.hasFields) { + const existingHiddenFields = existingCipher.fields.filter( + (f) => + f.type === FieldType.Hidden && + f.name != null && + f.name !== "" && + f.value != null && + f.value !== "" + ); + const hiddenFields = + model.fields == null + ? [] + : model.fields.filter( + (f) => f.type === FieldType.Hidden && f.name != null && f.name !== "" + ); + existingHiddenFields.forEach((ef) => { + const matchedField = hiddenFields.find((f) => f.name === ef.name); + if (matchedField == null || matchedField.value !== ef.value) { + const ph = new PasswordHistoryView(); + ph.password = ef.name + ": " + ef.value; + ph.lastUsedDate = new Date(); + model.passwordHistory.splice(0, 0, ph); + } + }); + } + } + if (model.passwordHistory != null && model.passwordHistory.length === 0) { + model.passwordHistory = null; + } else if (model.passwordHistory != null && model.passwordHistory.length > 5) { + // only save last 5 history + model.passwordHistory = model.passwordHistory.slice(0, 5); + } } - clearCache(): void { - this.decryptedCipherCache = null; - this.sortedCiphersCache.clear(); + const cipher = new Cipher(); + cipher.id = model.id; + cipher.folderId = model.folderId; + cipher.favorite = model.favorite; + cipher.organizationId = model.organizationId; + cipher.type = model.type; + cipher.collectionIds = model.collectionIds; + cipher.revisionDate = model.revisionDate; + cipher.reprompt = model.reprompt; + + if (key == null && cipher.organizationId != null) { + key = await this.cryptoService.getOrgKey(cipher.organizationId); + if (key == null) { + throw new Error("Cannot encrypt cipher for organization. No key."); + } + } + await Promise.all([ + this.encryptObjProperty( + model, + cipher, + { + name: null, + notes: null, + }, + key + ), + this.encryptCipherData(cipher, model, key), + this.encryptFields(model.fields, key).then((fields) => { + cipher.fields = fields; + }), + this.encryptPasswordHistories(model.passwordHistory, key).then((ph) => { + cipher.passwordHistory = ph; + }), + this.encryptAttachments(model.attachments, key).then((attachments) => { + cipher.attachments = attachments; + }), + ]); + + return cipher; + } + + async encryptAttachments( + attachmentsModel: AttachmentView[], + key: SymmetricCryptoKey + ): Promise { + if (attachmentsModel == null || attachmentsModel.length === 0) { + return null; } - async encrypt(model: CipherView, key?: SymmetricCryptoKey, originalCipher: Cipher = null): Promise { - // Adjust password history - if (model.id != null) { - if (originalCipher == null) { - originalCipher = await this.get(model.id); - } - if (originalCipher != null) { - const existingCipher = await originalCipher.decrypt(); - model.passwordHistory = existingCipher.passwordHistory || []; - if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { - if (existingCipher.login.password != null && existingCipher.login.password !== '' && - existingCipher.login.password !== model.login.password) { - const ph = new PasswordHistoryView(); - ph.password = existingCipher.login.password; - ph.lastUsedDate = model.login.passwordRevisionDate = new Date(); - model.passwordHistory.splice(0, 0, ph); - } else { - model.login.passwordRevisionDate = existingCipher.login.passwordRevisionDate; - } - } - if (existingCipher.hasFields) { - const existingHiddenFields = existingCipher.fields.filter(f => f.type === FieldType.Hidden && - f.name != null && f.name !== '' && f.value != null && f.value !== ''); - const hiddenFields = model.fields == null ? [] : - model.fields.filter(f => f.type === FieldType.Hidden && f.name != null && f.name !== ''); - existingHiddenFields.forEach(ef => { - const matchedField = hiddenFields.find(f => f.name === ef.name); - if (matchedField == null || matchedField.value !== ef.value) { - const ph = new PasswordHistoryView(); - ph.password = ef.name + ': ' + ef.value; - ph.lastUsedDate = new Date(); - model.passwordHistory.splice(0, 0, ph); - } - }); - } - } - if (model.passwordHistory != null && model.passwordHistory.length === 0) { - model.passwordHistory = null; - } else if (model.passwordHistory != null && model.passwordHistory.length > 5) { - // only save last 5 history - model.passwordHistory = model.passwordHistory.slice(0, 5); - } + const promises: Promise[] = []; + const encAttachments: Attachment[] = []; + attachmentsModel.forEach(async (model) => { + const attachment = new Attachment(); + attachment.id = model.id; + attachment.size = model.size; + attachment.sizeName = model.sizeName; + attachment.url = model.url; + const promise = this.encryptObjProperty( + model, + attachment, + { + fileName: null, + }, + key + ).then(async () => { + if (model.key != null) { + attachment.key = await this.cryptoService.encrypt(model.key.key, key); } + encAttachments.push(attachment); + }); + promises.push(promise); + }); - const cipher = new Cipher(); - cipher.id = model.id; - cipher.folderId = model.folderId; - cipher.favorite = model.favorite; - cipher.organizationId = model.organizationId; - cipher.type = model.type; - cipher.collectionIds = model.collectionIds; - cipher.revisionDate = model.revisionDate; - cipher.reprompt = model.reprompt; + await Promise.all(promises); + return encAttachments; + } - if (key == null && cipher.organizationId != null) { - key = await this.cryptoService.getOrgKey(cipher.organizationId); - if (key == null) { - throw new Error('Cannot encrypt cipher for organization. No key.'); - } - } - await Promise.all([ - this.encryptObjProperty(model, cipher, { - name: null, - notes: null, - }, key), - this.encryptCipherData(cipher, model, key), - this.encryptFields(model.fields, key).then(fields => { - cipher.fields = fields; - }), - this.encryptPasswordHistories(model.passwordHistory, key).then(ph => { - cipher.passwordHistory = ph; - }), - this.encryptAttachments(model.attachments, key).then(attachments => { - cipher.attachments = attachments; - }), - ]); - - return cipher; + async encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise { + if (!fieldsModel || !fieldsModel.length) { + return null; } - async encryptAttachments(attachmentsModel: AttachmentView[], key: SymmetricCryptoKey): Promise { - if (attachmentsModel == null || attachmentsModel.length === 0) { - return null; - } + const self = this; + const encFields: Field[] = []; + await fieldsModel.reduce(async (promise, field) => { + await promise; + const encField = await self.encryptField(field, key); + encFields.push(encField); + }, Promise.resolve()); - const promises: Promise[] = []; - const encAttachments: Attachment[] = []; - attachmentsModel.forEach(async model => { - const attachment = new Attachment(); - attachment.id = model.id; - attachment.size = model.size; - attachment.sizeName = model.sizeName; - attachment.url = model.url; - const promise = this.encryptObjProperty(model, attachment, { - fileName: null, - }, key).then(async () => { - if (model.key != null) { - attachment.key = await this.cryptoService.encrypt(model.key.key, key); - } - encAttachments.push(attachment); + return encFields; + } + + async encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise { + const field = new Field(); + field.type = fieldModel.type; + field.linkedId = fieldModel.linkedId; + // normalize boolean type field values + if (fieldModel.type === FieldType.Boolean && fieldModel.value !== "true") { + fieldModel.value = "false"; + } + + await this.encryptObjProperty( + fieldModel, + field, + { + name: null, + value: null, + }, + key + ); + + return field; + } + + async encryptPasswordHistories( + phModels: PasswordHistoryView[], + key: SymmetricCryptoKey + ): Promise { + if (!phModels || !phModels.length) { + return null; + } + + const self = this; + const encPhs: Password[] = []; + await phModels.reduce(async (promise, ph) => { + await promise; + const encPh = await self.encryptPasswordHistory(ph, key); + encPhs.push(encPh); + }, Promise.resolve()); + + return encPhs; + } + + async encryptPasswordHistory( + phModel: PasswordHistoryView, + key: SymmetricCryptoKey + ): Promise { + const ph = new Password(); + ph.lastUsedDate = phModel.lastUsedDate; + + await this.encryptObjProperty( + phModel, + ph, + { + password: null, + }, + key + ); + + return ph; + } + + async get(id: string): Promise { + const ciphers = await this.stateService.getEncryptedCiphers(); + if (ciphers == null || !ciphers.hasOwnProperty(id)) { + return null; + } + + const localData = await this.stateService.getLocalData(); + return new Cipher(ciphers[id], false, localData ? localData[id] : null); + } + + async getAll(): Promise { + const localData = await this.stateService.getLocalData(); + const ciphers = await this.stateService.getEncryptedCiphers(); + const response: Cipher[] = []; + for (const id in ciphers) { + if (ciphers.hasOwnProperty(id)) { + response.push(new Cipher(ciphers[id], false, localData ? localData[id] : null)); + } + } + return response; + } + + @sequentialize(() => "getAllDecrypted") + async getAllDecrypted(): Promise { + const userId = await this.stateService.getUserId(); + if ((await this.getDecryptedCipherCache()) != null) { + if ( + this.searchService != null && + (this.searchService().indexedEntityId ?? userId) !== userId + ) { + await this.searchService().indexCiphers(userId, await this.getDecryptedCipherCache()); + } + return await this.getDecryptedCipherCache(); + } + + const decCiphers: CipherView[] = []; + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { + throw new Error("No key."); + } + + const promises: any[] = []; + const ciphers = await this.getAll(); + ciphers.forEach(async (cipher) => { + promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); + }); + + await Promise.all(promises); + decCiphers.sort(this.getLocaleSortingFunction()); + await this.stateService.setDecryptedCiphers(decCiphers); + return decCiphers; + } + + async getAllDecryptedForGrouping( + groupingId: string, + folder: boolean = true + ): Promise { + const ciphers = await this.getAllDecrypted(); + + return ciphers.filter((cipher) => { + if (cipher.isDeleted) { + return false; + } + if (folder && cipher.folderId === groupingId) { + return true; + } else if ( + !folder && + cipher.collectionIds != null && + cipher.collectionIds.indexOf(groupingId) > -1 + ) { + return true; + } + + return false; + }); + } + + async getAllDecryptedForUrl( + url: string, + includeOtherTypes?: CipherType[], + defaultMatch: UriMatchType = null + ): Promise { + if (url == null && includeOtherTypes == null) { + return Promise.resolve([]); + } + + const domain = Utils.getDomain(url); + const eqDomainsPromise = + domain == null + ? Promise.resolve([]) + : this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { + let matches: any[] = []; + eqDomains.forEach((eqDomain) => { + if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { + matches = matches.concat(eqDomain); + } }); - promises.push(promise); - }); - await Promise.all(promises); - return encAttachments; - } - - async encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise { - if (!fieldsModel || !fieldsModel.length) { - return null; - } - - const self = this; - const encFields: Field[] = []; - await fieldsModel.reduce((promise, field) => { - return promise.then(() => { - return self.encryptField(field, key); - }).then((encField: Field) => { - encFields.push(encField); - }); - }, Promise.resolve()); - - return encFields; - } - - async encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise { - const field = new Field(); - field.type = fieldModel.type; - field.linkedId = fieldModel.linkedId; - // normalize boolean type field values - if (fieldModel.type === FieldType.Boolean && fieldModel.value !== 'true') { - fieldModel.value = 'false'; - } - - await this.encryptObjProperty(fieldModel, field, { - name: null, - value: null, - }, key); - - return field; - } - - async encryptPasswordHistories(phModels: PasswordHistoryView[], key: SymmetricCryptoKey): Promise { - if (!phModels || !phModels.length) { - return null; - } - - const self = this; - const encPhs: Password[] = []; - await phModels.reduce((promise, ph) => { - return promise.then(() => { - return self.encryptPasswordHistory(ph, key); - }).then((encPh: Password) => { - encPhs.push(encPh); - }); - }, Promise.resolve()); - - return encPhs; - } - - async encryptPasswordHistory(phModel: PasswordHistoryView, key: SymmetricCryptoKey): Promise { - const ph = new Password(); - ph.lastUsedDate = phModel.lastUsedDate; - - await this.encryptObjProperty(phModel, ph, { - password: null, - }, key); - - return ph; - } - - async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const localData = await this.storageService.get(Keys.localData); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); - if (ciphers == null || !ciphers.hasOwnProperty(id)) { - return null; - } - - return new Cipher(ciphers[id], false, localData ? localData[id] : null); - } - - async getAll(): Promise { - const userId = await this.userService.getUserId(); - const localData = await this.storageService.get(Keys.localData); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); - const response: Cipher[] = []; - for (const id in ciphers) { - if (ciphers.hasOwnProperty(id)) { - response.push(new Cipher(ciphers[id], false, localData ? localData[id] : null)); + if (!matches.length) { + matches.push(domain); } - } - return response; + + return matches; + }); + + const result = await Promise.all([eqDomainsPromise, this.getAllDecrypted()]); + const matchingDomains = result[0]; + const ciphers = result[1]; + + if (defaultMatch == null) { + defaultMatch = await this.stateService.getDefaultUriMatch(); + if (defaultMatch == null) { + defaultMatch = UriMatchType.Domain; + } } - @sequentialize(() => 'getAllDecrypted') - async getAllDecrypted(): Promise { - if (this.decryptedCipherCache != null) { - const userId = await this.userService.getUserId(); - if (this.searchService != null && (this.searchService().indexedEntityId ?? userId) !== userId) - { - await this.searchService().indexCiphers(userId, this.decryptedCipherCache); - } - return this.decryptedCipherCache; - } + return ciphers.filter((cipher) => { + if (cipher.deletedDate != null) { + return false; + } + if (includeOtherTypes != null && includeOtherTypes.indexOf(cipher.type) > -1) { + return true; + } - const decCiphers: CipherView[] = []; - const hasKey = await this.cryptoService.hasKey(); - if (!hasKey) { - throw new Error('No key.'); - } + if (url != null && cipher.type === CipherType.Login && cipher.login.uris != null) { + for (let i = 0; i < cipher.login.uris.length; i++) { + const u = cipher.login.uris[i]; + if (u.uri == null) { + continue; + } - const promises: any[] = []; - const ciphers = await this.getAll(); - ciphers.forEach(cipher => { - promises.push(cipher.decrypt().then(c => decCiphers.push(c))); - }); - - await Promise.all(promises); - decCiphers.sort(this.getLocaleSortingFunction()); - this.decryptedCipherCache = decCiphers; - return this.decryptedCipherCache; - } - - async getAllDecryptedForGrouping(groupingId: string, folder: boolean = true): Promise { - const ciphers = await this.getAllDecrypted(); - - return ciphers.filter(cipher => { - if (cipher.isDeleted) { - return false; - } - if (folder && cipher.folderId === groupingId) { + const match = u.match == null ? defaultMatch : u.match; + switch (match) { + case UriMatchType.Domain: + if (domain != null && u.domain != null && matchingDomains.indexOf(u.domain) > -1) { + if (DomainMatchBlacklist.has(u.domain)) { + const domainUrlHost = Utils.getHost(url); + if (!DomainMatchBlacklist.get(u.domain).has(domainUrlHost)) { + return true; + } + } else { + return true; + } + } + break; + case UriMatchType.Host: + const urlHost = Utils.getHost(url); + if (urlHost != null && urlHost === Utils.getHost(u.uri)) { return true; - } else if (!folder && cipher.collectionIds != null && cipher.collectionIds.indexOf(groupingId) > -1) { + } + break; + case UriMatchType.Exact: + if (url === u.uri) { return true; - } - - return false; - }); - } - - async getAllDecryptedForUrl(url: string, includeOtherTypes?: CipherType[], - defaultMatch: UriMatchType = null): Promise { - if (url == null && includeOtherTypes == null) { - return Promise.resolve([]); - } - - const domain = Utils.getDomain(url); - const eqDomainsPromise = domain == null ? Promise.resolve([]) : - this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { - let matches: any[] = []; - eqDomains.forEach(eqDomain => { - if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { - matches = matches.concat(eqDomain); - } - }); - - if (!matches.length) { - matches.push(domain); - } - - return matches; - }); - - const result = await Promise.all([eqDomainsPromise, this.getAllDecrypted()]); - const matchingDomains = result[0]; - const ciphers = result[1]; - - if (defaultMatch == null) { - defaultMatch = await this.storageService.get(ConstantsService.defaultUriMatch); - if (defaultMatch == null) { - defaultMatch = UriMatchType.Domain; - } - } - - return ciphers.filter(cipher => { - if (cipher.deletedDate != null) { - return false; - } - if (includeOtherTypes != null && includeOtherTypes.indexOf(cipher.type) > -1) { + } + break; + case UriMatchType.StartsWith: + if (url.startsWith(u.uri)) { return true; - } - - if (url != null && cipher.type === CipherType.Login && cipher.login.uris != null) { - for (let i = 0; i < cipher.login.uris.length; i++) { - const u = cipher.login.uris[i]; - if (u.uri == null) { - continue; - } - - const match = u.match == null ? defaultMatch : u.match; - switch (match) { - case UriMatchType.Domain: - if (domain != null && u.domain != null && matchingDomains.indexOf(u.domain) > -1) { - if (DomainMatchBlacklist.has(u.domain)) { - const domainUrlHost = Utils.getHost(url); - if (!DomainMatchBlacklist.get(u.domain).has(domainUrlHost)) { - return true; - } - } else { - return true; - } - } - break; - case UriMatchType.Host: - const urlHost = Utils.getHost(url); - if (urlHost != null && urlHost === Utils.getHost(u.uri)) { - return true; - } - break; - case UriMatchType.Exact: - if (url === u.uri) { - return true; - } - break; - case UriMatchType.StartsWith: - if (url.startsWith(u.uri)) { - return true; - } - break; - case UriMatchType.RegularExpression: - try { - const regex = new RegExp(u.uri, 'i'); - if (regex.test(url)) { - return true; - } - } catch (e) { - this.logService.error(e); - } - break; - case UriMatchType.Never: - default: - break; - } + } + break; + case UriMatchType.RegularExpression: + try { + const regex = new RegExp(u.uri, "i"); + if (regex.test(url)) { + return true; } - } - - return false; - }); - } - - async getAllFromApiForOrganization(organizationId: string): Promise { - const ciphers = await this.apiService.getCiphersOrganization(organizationId); - if (ciphers != null && ciphers.data != null && ciphers.data.length) { - const decCiphers: CipherView[] = []; - const promises: any[] = []; - ciphers.data.forEach(r => { - const data = new CipherData(r); - const cipher = new Cipher(data); - promises.push(cipher.decrypt().then(c => decCiphers.push(c))); - }); - await Promise.all(promises); - decCiphers.sort(this.getLocaleSortingFunction()); - return decCiphers; - } else { - return []; - } - } - - async getLastUsedForUrl(url: string, autofillOnPageLoad: boolean = false): Promise { - return this.getCipherForUrl(url, true, false, autofillOnPageLoad); - } - - async getLastLaunchedForUrl(url: string, autofillOnPageLoad: boolean = false): Promise { - return this.getCipherForUrl(url, false, true, autofillOnPageLoad); - } - - async getNextCipherForUrl(url: string): Promise { - return this.getCipherForUrl(url, false, false, false); - } - - updateLastUsedIndexForUrl(url: string) { - this.sortedCiphersCache.updateLastUsedIndex(url); - } - - async updateLastUsedDate(id: string): Promise { - let ciphersLocalData = await this.storageService.get(Keys.localData); - if (!ciphersLocalData) { - ciphersLocalData = {}; - } - - if (ciphersLocalData[id]) { - ciphersLocalData[id].lastUsedDate = new Date().getTime(); - } else { - ciphersLocalData[id] = { - lastUsedDate: new Date().getTime(), - }; - } - - await this.storageService.save(Keys.localData, ciphersLocalData); - - if (this.decryptedCipherCache == null) { - return; - } - - for (let i = 0; i < this.decryptedCipherCache.length; i++) { - const cached = this.decryptedCipherCache[i]; - if (cached.id === id) { - cached.localData = ciphersLocalData[id]; - break; - } - } - } - - async updateLastLaunchedDate(id: string): Promise { - let ciphersLocalData = await this.storageService.get(Keys.localData); - if (!ciphersLocalData) { - ciphersLocalData = {}; - } - - if (ciphersLocalData[id]) { - ciphersLocalData[id].lastLaunched = new Date().getTime(); - } else { - ciphersLocalData[id] = { - lastUsedDate: new Date().getTime(), - }; - } - - await this.storageService.save(Keys.localData, ciphersLocalData); - - if (this.decryptedCipherCache == null) { - return; - } - - for (let i = 0; i < this.decryptedCipherCache.length; i++) { - const cached = this.decryptedCipherCache[i]; - if (cached.id === id) { - cached.localData = ciphersLocalData[id]; - break; - } - } - } - - async saveNeverDomain(domain: string): Promise { - if (domain == null) { - return; - } - - let domains = await this.storageService.get<{ [id: string]: any; }>(Keys.neverDomains); - if (!domains) { - domains = {}; - } - domains[domain] = null; - await this.storageService.save(Keys.neverDomains, domains); - } - - async saveWithServer(cipher: Cipher): Promise { - let response: CipherResponse; - if (cipher.id == null) { - if (cipher.collectionIds != null) { - const request = new CipherCreateRequest(cipher); - response = await this.apiService.postCipherCreate(request); - } else { - const request = new CipherRequest(cipher); - response = await this.apiService.postCipher(request); - } - cipher.id = response.id; - } else { - const request = new CipherRequest(cipher); - response = await this.apiService.putCipher(cipher.id, request); - } - - const userId = await this.userService.getUserId(); - const data = new CipherData(response, userId, cipher.collectionIds); - await this.upsert(data); - } - - async shareWithServer(cipher: CipherView, organizationId: string, collectionIds: string[]): Promise { - const attachmentPromises: Promise[] = []; - if (cipher.attachments != null) { - cipher.attachments.forEach(attachment => { - if (attachment.key == null) { - attachmentPromises.push(this.shareAttachmentWithServer(attachment, cipher.id, organizationId)); - } - }); - } - await Promise.all(attachmentPromises); - - cipher.organizationId = organizationId; - cipher.collectionIds = collectionIds; - const encCipher = await this.encrypt(cipher); - const request = new CipherShareRequest(encCipher); - const response = await this.apiService.putShareCipher(cipher.id, request); - const userId = await this.userService.getUserId(); - const data = new CipherData(response, userId, collectionIds); - await this.upsert(data); - } - - async shareManyWithServer(ciphers: CipherView[], organizationId: string, collectionIds: string[]): Promise { - const promises: Promise[] = []; - const encCiphers: Cipher[] = []; - for (const cipher of ciphers) { - cipher.organizationId = organizationId; - cipher.collectionIds = collectionIds; - promises.push(this.encrypt(cipher).then(c => { - encCiphers.push(c); - })); - } - await Promise.all(promises); - const request = new CipherBulkShareRequest(encCiphers, collectionIds); - await this.apiService.putShareCiphers(request); - const userId = await this.userService.getUserId(); - await this.upsert(encCiphers.map(c => c.toCipherData(userId))); - } - - saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any, admin = false): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsArrayBuffer(unencryptedFile); - reader.onload = async (evt: any) => { - try { - const cData = await this.saveAttachmentRawWithServer(cipher, - unencryptedFile.name, evt.target.result, admin); - resolve(cData); - } catch (e) { - reject(e); - } - }; - reader.onerror = evt => { - reject('Error reading file.'); - }; - }); - } - - async saveAttachmentRawWithServer(cipher: Cipher, filename: string, - data: ArrayBuffer, admin = false): Promise { - const key = await this.cryptoService.getOrgKey(cipher.organizationId); - const encFileName = await this.cryptoService.encrypt(filename, key); - - const dataEncKey = await this.cryptoService.makeEncKey(key); - const encData = await this.cryptoService.encryptToBytes(data, dataEncKey[0]); - - const request: AttachmentRequest = { - key: dataEncKey[1].encryptedString, - fileName: encFileName.encryptedString, - fileSize: encData.buffer.byteLength, - adminRequest: admin, - }; - - let response: CipherResponse; - try { - const uploadDataResponse = await this.apiService.postCipherAttachment(cipher.id, request); - response = admin ? uploadDataResponse.cipherMiniResponse : uploadDataResponse.cipherResponse; - await this.fileUploadService.uploadCipherAttachment(admin, uploadDataResponse, encFileName, encData); - } catch (e) { - if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404 || (e as ErrorResponse).statusCode === 405) { - response = await this.legacyServerAttachmentFileUpload(admin, cipher.id, encFileName, encData, dataEncKey[1]); - } else if (e instanceof ErrorResponse) { - throw new Error((e as ErrorResponse).getSingleMessage()); - } else { - throw e; - } - } - - const userId = await this.userService.getUserId(); - const cData = new CipherData(response, userId, cipher.collectionIds); - if (!admin) { - await this.upsert(cData); - } - return new Cipher(cData); - } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async legacyServerAttachmentFileUpload(admin: boolean, cipherId: string, encFileName: EncString, - encData: EncArrayBuffer, key: EncString) { - const fd = new FormData(); - try { - const blob = new Blob([encData.buffer], { type: 'application/octet-stream' }); - fd.append('key', key.encryptedString); - fd.append('data', blob, encFileName.encryptedString); - } catch (e) { - if (Utils.isNode && !Utils.isBrowser) { - fd.append('key', key.encryptedString); - fd.append('data', Buffer.from(encData.buffer) as any, { - filepath: encFileName.encryptedString, - contentType: 'application/octet-stream', - } as any); - } else { - throw e; - } - } - - let response: CipherResponse; - try { - if (admin) { - response = await this.apiService.postCipherAttachmentAdminLegacy(cipherId, fd); - } else { - response = await this.apiService.postCipherAttachmentLegacy(cipherId, fd); - } - } catch (e) { - throw new Error((e as ErrorResponse).getSingleMessage()); - } - - return response; - } - - async saveCollectionsWithServer(cipher: Cipher): Promise { - const request = new CipherCollectionsRequest(cipher.collectionIds); - await this.apiService.putCipherCollections(cipher.id, request); - const userId = await this.userService.getUserId(); - const data = cipher.toCipherData(userId); - await this.upsert(data); - } - - async upsert(cipher: CipherData | CipherData[]): Promise { - const userId = await this.userService.getUserId(); - let ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); - if (ciphers == null) { - ciphers = {}; - } - - if (cipher instanceof CipherData) { - const c = cipher as CipherData; - ciphers[c.id] = c; - } else { - (cipher as CipherData[]).forEach(c => { - ciphers[c.id] = c; - }); - } - - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; - } - - async replace(ciphers: { [id: string]: CipherData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; - } - - async clear(userId: string): Promise { - await this.storageService.remove(Keys.ciphersPrefix + userId); - this.clearCache(); - } - - async moveManyWithServer(ids: string[], folderId: string): Promise { - await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); - - const userId = await this.userService.getUserId(); - let ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); - if (ciphers == null) { - ciphers = {}; - } - - ids.forEach(id => { - if (ciphers.hasOwnProperty(id)) { - ciphers[id].folderId = folderId; - } - }); - - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; - } - - async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); - if (ciphers == null) { - return; - } - - if (typeof id === 'string') { - if (ciphers[id] == null) { - return; - } - delete ciphers[id]; - } else { - (id as string[]).forEach(i => { - delete ciphers[i]; - }); - } - - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; - } - - async deleteWithServer(id: string): Promise { - await this.apiService.deleteCipher(id); - await this.delete(id); - } - - async deleteManyWithServer(ids: string[]): Promise { - await this.apiService.deleteManyCiphers(new CipherBulkDeleteRequest(ids)); - await this.delete(ids); - } - - async deleteAttachment(id: string, attachmentId: string): Promise { - const userId = await this.userService.getUserId(); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); - - if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) { - return; - } - - for (let i = 0; i < ciphers[id].attachments.length; i++) { - if (ciphers[id].attachments[i].id === attachmentId) { - ciphers[id].attachments.splice(i, 1); - } - } - - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; - } - - async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { - try { - await this.apiService.deleteCipherAttachment(id, attachmentId); - } catch (e) { - return Promise.reject((e as ErrorResponse).getSingleMessage()); - } - await this.deleteAttachment(id, attachmentId); - } - - sortCiphersByLastUsed(a: CipherView, b: CipherView): number { - const aLastUsed = a.localData && a.localData.lastUsedDate ? a.localData.lastUsedDate as number : null; - const bLastUsed = b.localData && b.localData.lastUsedDate ? b.localData.lastUsedDate as number : null; - - const bothNotNull = aLastUsed != null && bLastUsed != null; - if (bothNotNull && aLastUsed < bLastUsed) { - return 1; - } - if (aLastUsed != null && bLastUsed == null) { - return -1; - } - - if (bothNotNull && aLastUsed > bLastUsed) { - return -1; - } - if (bLastUsed != null && aLastUsed == null) { - return 1; - } - - return 0; - } - - sortCiphersByLastUsedThenName(a: CipherView, b: CipherView): number { - const result = this.sortCiphersByLastUsed(a, b); - if (result !== 0) { - return result; - } - - return this.getLocaleSortingFunction()(a, b); - } - - getLocaleSortingFunction(): (a: CipherView, b: CipherView) => number { - return (a, b) => { - let aName = a.name; - let bName = b.name; - - if (aName == null && bName != null) { - return -1; - } - if (aName != null && bName == null) { - return 1; - } - if (aName == null && bName == null) { - return 0; - } - - const result = this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : - aName.localeCompare(bName); - - if (result !== 0 || a.type !== CipherType.Login || b.type !== CipherType.Login) { - return result; - } - - if (a.login.username != null) { - aName += a.login.username; - } - - if (b.login.username != null) { - bName += b.login.username; - } - - return this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : - aName.localeCompare(bName); - }; - } - - async softDelete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); - if (ciphers == null) { - return; - } - - const setDeletedDate = (cipherId: string) => { - if (ciphers[cipherId] == null) { - return; - } - ciphers[cipherId].deletedDate = new Date().toISOString(); - }; - - if (typeof id === 'string') { - setDeletedDate(id); - } else { - (id as string[]).forEach(setDeletedDate); - } - - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; - } - - async softDeleteWithServer(id: string): Promise { - await this.apiService.putDeleteCipher(id); - await this.softDelete(id); - } - - async softDeleteManyWithServer(ids: string[]): Promise { - await this.apiService.putDeleteManyCiphers(new CipherBulkDeleteRequest(ids)); - await this.softDelete(ids); - } - - async restore(cipher: { id: string, revisionDate: string; } | { id: string, revisionDate: string; }[]) { - const userId = await this.userService.getUserId(); - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( - Keys.ciphersPrefix + userId); - if (ciphers == null) { - return; - } - - const clearDeletedDate = (c: { id: string, revisionDate: string; }) => { - if (ciphers[c.id] == null) { - return; - } - ciphers[c.id].deletedDate = null; - ciphers[c.id].revisionDate = c.revisionDate; - }; - - - if (cipher.constructor.name === 'Array') { - (cipher as { id: string, revisionDate: string; }[]).forEach(clearDeletedDate); - } else { - clearDeletedDate(cipher as { id: string, revisionDate: string; }); - } - - await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; - } - - async restoreWithServer(id: string): Promise { - const response = await this.apiService.putRestoreCipher(id); - await this.restore({ id: id, revisionDate: response.revisionDate }); - } - - async restoreManyWithServer(ids: string[]): Promise { - const response = await this.apiService.putRestoreManyCiphers(new CipherBulkRestoreRequest(ids)); - const restores: { id: string, revisionDate: string; }[] = []; - for (const cipher of response.data) { - restores.push({ id: cipher.id, revisionDate: cipher.revisionDate }); - } - await this.restore(restores); - } - - // Helpers - - private async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string, - organizationId: string): Promise { - const attachmentResponse = await this.apiService.nativeFetch( - new Request(attachmentView.url, { cache: 'no-store' })); - if (attachmentResponse.status !== 200) { - throw Error('Failed to download attachment: ' + attachmentResponse.status.toString()); - } - - const buf = await attachmentResponse.arrayBuffer(); - const decBuf = await this.cryptoService.decryptFromBytes(buf, null); - const key = await this.cryptoService.getOrgKey(organizationId); - const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key); - - const dataEncKey = await this.cryptoService.makeEncKey(key); - const encData = await this.cryptoService.encryptToBytes(decBuf, dataEncKey[0]); - - const fd = new FormData(); - try { - const blob = new Blob([encData.buffer], { type: 'application/octet-stream' }); - fd.append('key', dataEncKey[1].encryptedString); - fd.append('data', blob, encFileName.encryptedString); - } catch (e) { - if (Utils.isNode && !Utils.isBrowser) { - fd.append('key', dataEncKey[1].encryptedString); - fd.append('data', Buffer.from(encData.buffer) as any, { - filepath: encFileName.encryptedString, - contentType: 'application/octet-stream', - } as any); - } else { - throw e; - } - } - - try { - await this.apiService.postShareCipherAttachment(cipherId, attachmentView.id, fd, organizationId); - } catch (e) { - throw new Error((e as ErrorResponse).getSingleMessage()); - } - } - - private async encryptObjProperty(model: V, obj: D, - map: any, key: SymmetricCryptoKey): Promise { - const promises = []; - const self = this; - - for (const prop in map) { - if (!map.hasOwnProperty(prop)) { - continue; - } - - // tslint:disable-next-line - (function (theProp, theObj) { - const p = Promise.resolve().then(() => { - const modelProp = (model as any)[(map[theProp] || theProp)]; - if (modelProp && modelProp !== '') { - return self.cryptoService.encrypt(modelProp, key); - } - return null; - }).then((val: EncString) => { - (theObj as any)[theProp] = val; - }); - promises.push(p); - })(prop, obj); - } - - await Promise.all(promises); - } - - private async encryptCipherData(cipher: Cipher, model: CipherView, key: SymmetricCryptoKey) { - switch (cipher.type) { - case CipherType.Login: - cipher.login = new Login(); - cipher.login.passwordRevisionDate = model.login.passwordRevisionDate; - cipher.login.autofillOnPageLoad = model.login.autofillOnPageLoad; - await this.encryptObjProperty(model.login, cipher.login, { - username: null, - password: null, - totp: null, - }, key); - - if (model.login.uris != null) { - cipher.login.uris = []; - for (let i = 0; i < model.login.uris.length; i++) { - const loginUri = new LoginUri(); - loginUri.match = model.login.uris[i].match; - await this.encryptObjProperty(model.login.uris[i], loginUri, { - uri: null, - }, key); - cipher.login.uris.push(loginUri); - } - } - return; - case CipherType.SecureNote: - cipher.secureNote = new SecureNote(); - cipher.secureNote.type = model.secureNote.type; - return; - case CipherType.Card: - cipher.card = new Card(); - await this.encryptObjProperty(model.card, cipher.card, { - cardholderName: null, - brand: null, - number: null, - expMonth: null, - expYear: null, - code: null, - }, key); - return; - case CipherType.Identity: - cipher.identity = new Identity(); - await this.encryptObjProperty(model.identity, cipher.identity, { - title: null, - firstName: null, - middleName: null, - lastName: null, - address1: null, - address2: null, - address3: null, - city: null, - state: null, - postalCode: null, - country: null, - company: null, - email: null, - phone: null, - ssn: null, - username: null, - passportNumber: null, - licenseNumber: null, - }, key); - return; + } catch (e) { + this.logService.error(e); + } + break; + case UriMatchType.Never: default: - throw new Error('Unknown cipher type.'); + break; + } } + } + + return false; + }); + } + + async getAllFromApiForOrganization(organizationId: string): Promise { + const ciphers = await this.apiService.getCiphersOrganization(organizationId); + if (ciphers != null && ciphers.data != null && ciphers.data.length) { + const decCiphers: CipherView[] = []; + const promises: any[] = []; + ciphers.data.forEach((r) => { + const data = new CipherData(r); + const cipher = new Cipher(data); + promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); + }); + await Promise.all(promises); + decCiphers.sort(this.getLocaleSortingFunction()); + return decCiphers; + } else { + return []; + } + } + + async getLastUsedForUrl(url: string, autofillOnPageLoad: boolean = false): Promise { + return this.getCipherForUrl(url, true, false, autofillOnPageLoad); + } + + async getLastLaunchedForUrl( + url: string, + autofillOnPageLoad: boolean = false + ): Promise { + return this.getCipherForUrl(url, false, true, autofillOnPageLoad); + } + + async getNextCipherForUrl(url: string): Promise { + return this.getCipherForUrl(url, false, false, false); + } + + updateLastUsedIndexForUrl(url: string) { + this.sortedCiphersCache.updateLastUsedIndex(url); + } + + async updateLastUsedDate(id: string): Promise { + let ciphersLocalData = await this.stateService.getLocalData(); + if (!ciphersLocalData) { + ciphersLocalData = {}; } - private async getCipherForUrl(url: string, lastUsed: boolean, lastLaunched: boolean, autofillOnPageLoad: boolean): Promise { - const cacheKey = autofillOnPageLoad ? 'autofillOnPageLoad-' + url : url; - - if (!this.sortedCiphersCache.isCached(cacheKey)) { - let ciphers = await this.getAllDecryptedForUrl(url); - if (!ciphers) { - return null; - } - - if (autofillOnPageLoad) { - const autofillOnPageLoadDefault = await this.storageService.get(ConstantsService.autoFillOnPageLoadDefaultKey); - ciphers = ciphers.filter(cipher => cipher.login.autofillOnPageLoad || - (cipher.login.autofillOnPageLoad == null && autofillOnPageLoadDefault !== false)); - if (ciphers.length === 0) { - return null; - } - } - - this.sortedCiphersCache.addCiphers(cacheKey, ciphers); - } - - if (lastLaunched) { - return this.sortedCiphersCache.getLastLaunched(cacheKey); - } else if (lastUsed) { - return this.sortedCiphersCache.getLastUsed(cacheKey); - } else { - return this.sortedCiphersCache.getNext(cacheKey); - } + if (ciphersLocalData[id]) { + ciphersLocalData[id].lastUsedDate = new Date().getTime(); + } else { + ciphersLocalData[id] = { + lastUsedDate: new Date().getTime(), + }; } + + await this.stateService.setLocalData(ciphersLocalData); + + const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + if (!decryptedCipherCache) { + return; + } + + for (let i = 0; i < decryptedCipherCache.length; i++) { + const cached = decryptedCipherCache[i]; + if (cached.id === id) { + cached.localData = ciphersLocalData[id]; + break; + } + } + await this.stateService.setDecryptedCiphers(decryptedCipherCache); + } + + async updateLastLaunchedDate(id: string): Promise { + let ciphersLocalData = await this.stateService.getLocalData(); + if (!ciphersLocalData) { + ciphersLocalData = {}; + } + + if (ciphersLocalData[id]) { + ciphersLocalData[id].lastLaunched = new Date().getTime(); + } else { + ciphersLocalData[id] = { + lastUsedDate: new Date().getTime(), + }; + } + + await this.stateService.setLocalData(ciphersLocalData); + + const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + if (!decryptedCipherCache) { + return; + } + + for (let i = 0; i < decryptedCipherCache.length; i++) { + const cached = decryptedCipherCache[i]; + if (cached.id === id) { + cached.localData = ciphersLocalData[id]; + break; + } + } + await this.stateService.setDecryptedCiphers(decryptedCipherCache); + } + + async saveNeverDomain(domain: string): Promise { + if (domain == null) { + return; + } + + let domains = await this.stateService.getNeverDomains(); + if (!domains) { + domains = {}; + } + domains[domain] = null; + await this.stateService.setNeverDomains(domains); + } + + async saveWithServer(cipher: Cipher): Promise { + let response: CipherResponse; + if (cipher.id == null) { + if (cipher.collectionIds != null) { + const request = new CipherCreateRequest(cipher); + response = await this.apiService.postCipherCreate(request); + } else { + const request = new CipherRequest(cipher); + response = await this.apiService.postCipher(request); + } + cipher.id = response.id; + } else { + const request = new CipherRequest(cipher); + response = await this.apiService.putCipher(cipher.id, request); + } + + const data = new CipherData( + response, + await this.stateService.getUserId(), + cipher.collectionIds + ); + await this.upsert(data); + } + + async shareWithServer( + cipher: CipherView, + organizationId: string, + collectionIds: string[] + ): Promise { + const attachmentPromises: Promise[] = []; + if (cipher.attachments != null) { + cipher.attachments.forEach((attachment) => { + if (attachment.key == null) { + attachmentPromises.push( + this.shareAttachmentWithServer(attachment, cipher.id, organizationId) + ); + } + }); + } + await Promise.all(attachmentPromises); + + cipher.organizationId = organizationId; + cipher.collectionIds = collectionIds; + const encCipher = await this.encrypt(cipher); + const request = new CipherShareRequest(encCipher); + const response = await this.apiService.putShareCipher(cipher.id, request); + const data = new CipherData(response, await this.stateService.getUserId(), collectionIds); + await this.upsert(data); + } + + async shareManyWithServer( + ciphers: CipherView[], + organizationId: string, + collectionIds: string[] + ): Promise { + const promises: Promise[] = []; + const encCiphers: Cipher[] = []; + for (const cipher of ciphers) { + cipher.organizationId = organizationId; + cipher.collectionIds = collectionIds; + promises.push( + this.encrypt(cipher).then((c) => { + encCiphers.push(c); + }) + ); + } + await Promise.all(promises); + const request = new CipherBulkShareRequest(encCiphers, collectionIds); + await this.apiService.putShareCiphers(request); + const userId = await this.stateService.getUserId(); + await this.upsert(encCiphers.map((c) => c.toCipherData(userId))); + } + + saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any, admin = false): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(unencryptedFile); + reader.onload = async (evt: any) => { + try { + const cData = await this.saveAttachmentRawWithServer( + cipher, + unencryptedFile.name, + evt.target.result, + admin + ); + resolve(cData); + } catch (e) { + reject(e); + } + }; + reader.onerror = (_evt) => { + reject("Error reading file."); + }; + }); + } + + async saveAttachmentRawWithServer( + cipher: Cipher, + filename: string, + data: ArrayBuffer, + admin = false + ): Promise { + const key = await this.cryptoService.getOrgKey(cipher.organizationId); + const encFileName = await this.cryptoService.encrypt(filename, key); + + const dataEncKey = await this.cryptoService.makeEncKey(key); + const encData = await this.cryptoService.encryptToBytes(data, dataEncKey[0]); + + const request: AttachmentRequest = { + key: dataEncKey[1].encryptedString, + fileName: encFileName.encryptedString, + fileSize: encData.buffer.byteLength, + adminRequest: admin, + }; + + let response: CipherResponse; + try { + const uploadDataResponse = await this.apiService.postCipherAttachment(cipher.id, request); + response = admin ? uploadDataResponse.cipherMiniResponse : uploadDataResponse.cipherResponse; + await this.fileUploadService.uploadCipherAttachment( + admin, + uploadDataResponse, + encFileName, + encData + ); + } catch (e) { + if ( + (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) || + (e as ErrorResponse).statusCode === 405 + ) { + response = await this.legacyServerAttachmentFileUpload( + admin, + cipher.id, + encFileName, + encData, + dataEncKey[1] + ); + } else if (e instanceof ErrorResponse) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } else { + throw e; + } + } + + const cData = new CipherData( + response, + await this.stateService.getUserId(), + cipher.collectionIds + ); + if (!admin) { + await this.upsert(cData); + } + return new Cipher(cData); + } + + /** + * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. + * This method still exists for backward compatibility with old server versions. + */ + async legacyServerAttachmentFileUpload( + admin: boolean, + cipherId: string, + encFileName: EncString, + encData: EncArrayBuffer, + key: EncString + ) { + const fd = new FormData(); + try { + const blob = new Blob([encData.buffer], { type: "application/octet-stream" }); + fd.append("key", key.encryptedString); + fd.append("data", blob, encFileName.encryptedString); + } catch (e) { + if (Utils.isNode && !Utils.isBrowser) { + fd.append("key", key.encryptedString); + fd.append( + "data", + Buffer.from(encData.buffer) as any, + { + filepath: encFileName.encryptedString, + contentType: "application/octet-stream", + } as any + ); + } else { + throw e; + } + } + + let response: CipherResponse; + try { + if (admin) { + response = await this.apiService.postCipherAttachmentAdminLegacy(cipherId, fd); + } else { + response = await this.apiService.postCipherAttachmentLegacy(cipherId, fd); + } + } catch (e) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } + + return response; + } + + async saveCollectionsWithServer(cipher: Cipher): Promise { + const request = new CipherCollectionsRequest(cipher.collectionIds); + await this.apiService.putCipherCollections(cipher.id, request); + const data = cipher.toCipherData(await this.stateService.getUserId()); + await this.upsert(data); + } + + async upsert(cipher: CipherData | CipherData[]): Promise { + let ciphers = await this.stateService.getEncryptedCiphers(); + if (ciphers == null) { + ciphers = {}; + } + + if (cipher instanceof CipherData) { + const c = cipher as CipherData; + ciphers[c.id] = c; + } else { + (cipher as CipherData[]).forEach((c) => { + ciphers[c.id] = c; + }); + } + + await this.replace(ciphers); + } + + async replace(ciphers: { [id: string]: CipherData }): Promise { + await this.clearDecryptedCiphersState(); + await this.stateService.setEncryptedCiphers(ciphers); + } + + async clear(userId?: string): Promise { + await this.clearEncryptedCiphersState(userId); + await this.clearCache(userId); + } + + async moveManyWithServer(ids: string[], folderId: string): Promise { + await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); + + let ciphers = await this.stateService.getEncryptedCiphers(); + if (ciphers == null) { + ciphers = {}; + } + + ids.forEach((id) => { + if (ciphers.hasOwnProperty(id)) { + ciphers[id].folderId = folderId; + } + }); + + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); + } + + async delete(id: string | string[]): Promise { + const ciphers = await this.stateService.getEncryptedCiphers(); + if (ciphers == null) { + return; + } + + if (typeof id === "string") { + if (ciphers[id] == null) { + return; + } + delete ciphers[id]; + } else { + (id as string[]).forEach((i) => { + delete ciphers[i]; + }); + } + + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); + } + + async deleteWithServer(id: string): Promise { + await this.apiService.deleteCipher(id); + await this.delete(id); + } + + async deleteManyWithServer(ids: string[]): Promise { + await this.apiService.deleteManyCiphers(new CipherBulkDeleteRequest(ids)); + await this.delete(ids); + } + + async deleteAttachment(id: string, attachmentId: string): Promise { + const ciphers = await this.stateService.getEncryptedCiphers(); + + if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) { + return; + } + + for (let i = 0; i < ciphers[id].attachments.length; i++) { + if (ciphers[id].attachments[i].id === attachmentId) { + ciphers[id].attachments.splice(i, 1); + } + } + + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); + } + + async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { + try { + await this.apiService.deleteCipherAttachment(id, attachmentId); + } catch (e) { + return Promise.reject((e as ErrorResponse).getSingleMessage()); + } + await this.deleteAttachment(id, attachmentId); + } + + sortCiphersByLastUsed(a: CipherView, b: CipherView): number { + const aLastUsed = + a.localData && a.localData.lastUsedDate ? (a.localData.lastUsedDate as number) : null; + const bLastUsed = + b.localData && b.localData.lastUsedDate ? (b.localData.lastUsedDate as number) : null; + + const bothNotNull = aLastUsed != null && bLastUsed != null; + if (bothNotNull && aLastUsed < bLastUsed) { + return 1; + } + if (aLastUsed != null && bLastUsed == null) { + return -1; + } + + if (bothNotNull && aLastUsed > bLastUsed) { + return -1; + } + if (bLastUsed != null && aLastUsed == null) { + return 1; + } + + return 0; + } + + sortCiphersByLastUsedThenName(a: CipherView, b: CipherView): number { + const result = this.sortCiphersByLastUsed(a, b); + if (result !== 0) { + return result; + } + + return this.getLocaleSortingFunction()(a, b); + } + + getLocaleSortingFunction(): (a: CipherView, b: CipherView) => number { + return (a, b) => { + let aName = a.name; + let bName = b.name; + + if (aName == null && bName != null) { + return -1; + } + if (aName != null && bName == null) { + return 1; + } + if (aName == null && bName == null) { + return 0; + } + + const result = this.i18nService.collator + ? this.i18nService.collator.compare(aName, bName) + : aName.localeCompare(bName); + + if (result !== 0 || a.type !== CipherType.Login || b.type !== CipherType.Login) { + return result; + } + + if (a.login.username != null) { + aName += a.login.username; + } + + if (b.login.username != null) { + bName += b.login.username; + } + + return this.i18nService.collator + ? this.i18nService.collator.compare(aName, bName) + : aName.localeCompare(bName); + }; + } + + async softDelete(id: string | string[]): Promise { + const ciphers = await this.stateService.getEncryptedCiphers(); + if (ciphers == null) { + return; + } + + const setDeletedDate = (cipherId: string) => { + if (ciphers[cipherId] == null) { + return; + } + ciphers[cipherId].deletedDate = new Date().toISOString(); + }; + + if (typeof id === "string") { + setDeletedDate(id); + } else { + (id as string[]).forEach(setDeletedDate); + } + + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); + } + + async softDeleteWithServer(id: string): Promise { + await this.apiService.putDeleteCipher(id); + await this.softDelete(id); + } + + async softDeleteManyWithServer(ids: string[]): Promise { + await this.apiService.putDeleteManyCiphers(new CipherBulkDeleteRequest(ids)); + await this.softDelete(ids); + } + + async restore( + cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[] + ) { + const ciphers = await this.stateService.getEncryptedCiphers(); + if (ciphers == null) { + return; + } + + const clearDeletedDate = (c: { id: string; revisionDate: string }) => { + if (ciphers[c.id] == null) { + return; + } + ciphers[c.id].deletedDate = null; + ciphers[c.id].revisionDate = c.revisionDate; + }; + + if (cipher.constructor.name === "Array") { + (cipher as { id: string; revisionDate: string }[]).forEach(clearDeletedDate); + } else { + clearDeletedDate(cipher as { id: string; revisionDate: string }); + } + + await this.clearCache(); + await this.stateService.setEncryptedCiphers(ciphers); + } + + async restoreWithServer(id: string): Promise { + const response = await this.apiService.putRestoreCipher(id); + await this.restore({ id: id, revisionDate: response.revisionDate }); + } + + async restoreManyWithServer(ids: string[]): Promise { + const response = await this.apiService.putRestoreManyCiphers(new CipherBulkRestoreRequest(ids)); + const restores: { id: string; revisionDate: string }[] = []; + for (const cipher of response.data) { + restores.push({ id: cipher.id, revisionDate: cipher.revisionDate }); + } + await this.restore(restores); + } + + // Helpers + + private async shareAttachmentWithServer( + attachmentView: AttachmentView, + cipherId: string, + organizationId: string + ): Promise { + const attachmentResponse = await this.apiService.nativeFetch( + new Request(attachmentView.url, { cache: "no-store" }) + ); + if (attachmentResponse.status !== 200) { + throw Error("Failed to download attachment: " + attachmentResponse.status.toString()); + } + + const buf = await attachmentResponse.arrayBuffer(); + const decBuf = await this.cryptoService.decryptFromBytes(buf, null); + const key = await this.cryptoService.getOrgKey(organizationId); + const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key); + + const dataEncKey = await this.cryptoService.makeEncKey(key); + const encData = await this.cryptoService.encryptToBytes(decBuf, dataEncKey[0]); + + const fd = new FormData(); + try { + const blob = new Blob([encData.buffer], { type: "application/octet-stream" }); + fd.append("key", dataEncKey[1].encryptedString); + fd.append("data", blob, encFileName.encryptedString); + } catch (e) { + if (Utils.isNode && !Utils.isBrowser) { + fd.append("key", dataEncKey[1].encryptedString); + fd.append( + "data", + Buffer.from(encData.buffer) as any, + { + filepath: encFileName.encryptedString, + contentType: "application/octet-stream", + } as any + ); + } else { + throw e; + } + } + + try { + await this.apiService.postShareCipherAttachment( + cipherId, + attachmentView.id, + fd, + organizationId + ); + } catch (e) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } + } + + private async encryptObjProperty( + model: V, + obj: D, + map: any, + key: SymmetricCryptoKey + ): Promise { + const promises = []; + const self = this; + + for (const prop in map) { + if (!map.hasOwnProperty(prop)) { + continue; + } + + // tslint:disable-next-line + (function (theProp, theObj) { + const p = Promise.resolve() + .then(() => { + const modelProp = (model as any)[map[theProp] || theProp]; + if (modelProp && modelProp !== "") { + return self.cryptoService.encrypt(modelProp, key); + } + return null; + }) + .then((val: EncString) => { + (theObj as any)[theProp] = val; + }); + promises.push(p); + })(prop, obj); + } + + await Promise.all(promises); + } + + private async encryptCipherData(cipher: Cipher, model: CipherView, key: SymmetricCryptoKey) { + switch (cipher.type) { + case CipherType.Login: + cipher.login = new Login(); + cipher.login.passwordRevisionDate = model.login.passwordRevisionDate; + cipher.login.autofillOnPageLoad = model.login.autofillOnPageLoad; + await this.encryptObjProperty( + model.login, + cipher.login, + { + username: null, + password: null, + totp: null, + }, + key + ); + + if (model.login.uris != null) { + cipher.login.uris = []; + for (let i = 0; i < model.login.uris.length; i++) { + const loginUri = new LoginUri(); + loginUri.match = model.login.uris[i].match; + await this.encryptObjProperty( + model.login.uris[i], + loginUri, + { + uri: null, + }, + key + ); + cipher.login.uris.push(loginUri); + } + } + return; + case CipherType.SecureNote: + cipher.secureNote = new SecureNote(); + cipher.secureNote.type = model.secureNote.type; + return; + case CipherType.Card: + cipher.card = new Card(); + await this.encryptObjProperty( + model.card, + cipher.card, + { + cardholderName: null, + brand: null, + number: null, + expMonth: null, + expYear: null, + code: null, + }, + key + ); + return; + case CipherType.Identity: + cipher.identity = new Identity(); + await this.encryptObjProperty( + model.identity, + cipher.identity, + { + title: null, + firstName: null, + middleName: null, + lastName: null, + address1: null, + address2: null, + address3: null, + city: null, + state: null, + postalCode: null, + country: null, + company: null, + email: null, + phone: null, + ssn: null, + username: null, + passportNumber: null, + licenseNumber: null, + }, + key + ); + return; + default: + throw new Error("Unknown cipher type."); + } + } + + private async getCipherForUrl( + url: string, + lastUsed: boolean, + lastLaunched: boolean, + autofillOnPageLoad: boolean + ): Promise { + const cacheKey = autofillOnPageLoad ? "autofillOnPageLoad-" + url : url; + + if (!this.sortedCiphersCache.isCached(cacheKey)) { + let ciphers = await this.getAllDecryptedForUrl(url); + if (!ciphers) { + return null; + } + + if (autofillOnPageLoad) { + const autofillOnPageLoadDefault = await this.stateService.getAutoFillOnPageLoadDefault(); + ciphers = ciphers.filter( + (cipher) => + cipher.login.autofillOnPageLoad || + (cipher.login.autofillOnPageLoad == null && autofillOnPageLoadDefault !== false) + ); + if (ciphers.length === 0) { + return null; + } + } + + this.sortedCiphersCache.addCiphers(cacheKey, ciphers); + } + + if (lastLaunched) { + return this.sortedCiphersCache.getLastLaunched(cacheKey); + } else if (lastUsed) { + return this.sortedCiphersCache.getLastUsed(cacheKey); + } else { + return this.sortedCiphersCache.getNext(cacheKey); + } + } + + private async clearEncryptedCiphersState(userId?: string) { + await this.stateService.setEncryptedCiphers(null, { userId: userId }); + } + + private async clearDecryptedCiphersState(userId?: string) { + await this.stateService.setDecryptedCiphers(null, { userId: userId }); + this.clearSortedCiphers(); + } + + private clearSortedCiphers() { + this.sortedCiphersCache.clear(); + } } diff --git a/common/src/services/collection.service.ts b/common/src/services/collection.service.ts index 3c594080..7f555e63 100644 --- a/common/src/services/collection.service.ts +++ b/common/src/services/collection.service.ts @@ -1,173 +1,159 @@ -import { CollectionData } from '../models/data/collectionData'; +import { CollectionData } from "../models/data/collectionData"; -import { Collection } from '../models/domain/collection'; -import { TreeNode } from '../models/domain/treeNode'; +import { Collection } from "../models/domain/collection"; +import { TreeNode } from "../models/domain/treeNode"; -import { CollectionView } from '../models/view/collectionView'; +import { CollectionView } from "../models/view/collectionView"; -import { CollectionService as CollectionServiceAbstraction } from '../abstractions/collection.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { CollectionService as CollectionServiceAbstraction } from "../abstractions/collection.service"; +import { CryptoService } from "../abstractions/crypto.service"; +import { I18nService } from "../abstractions/i18n.service"; +import { StateService } from "../abstractions/state.service"; -import { ServiceUtils } from '../misc/serviceUtils'; -import { Utils } from '../misc/utils'; +import { ServiceUtils } from "../misc/serviceUtils"; +import { Utils } from "../misc/utils"; -const Keys = { - collectionsPrefix: 'collections_', -}; -const NestingDelimiter = '/'; +const NestingDelimiter = "/"; export class CollectionService implements CollectionServiceAbstraction { - decryptedCollectionCache: CollectionView[]; + constructor( + private cryptoService: CryptoService, + private i18nService: I18nService, + private stateService: StateService + ) {} - constructor(private cryptoService: CryptoService, private userService: UserService, - private storageService: StorageService, private i18nService: I18nService) { + async clearCache(userId?: string): Promise { + await this.stateService.setDecryptedCollections(null, { userId: userId }); + } + + async encrypt(model: CollectionView): Promise { + if (model.organizationId == null) { + throw new Error("Collection has no organization id."); + } + const key = await this.cryptoService.getOrgKey(model.organizationId); + if (key == null) { + throw new Error("No key for this collection's organization."); + } + const collection = new Collection(); + collection.id = model.id; + collection.organizationId = model.organizationId; + collection.readOnly = model.readOnly; + collection.name = await this.cryptoService.encrypt(model.name, key); + return collection; + } + + async decryptMany(collections: Collection[]): Promise { + if (collections == null) { + return []; + } + const decCollections: CollectionView[] = []; + const promises: Promise[] = []; + collections.forEach((collection) => { + promises.push(collection.decrypt().then((c) => decCollections.push(c))); + }); + await Promise.all(promises); + return decCollections.sort(Utils.getSortFunction(this.i18nService, "name")); + } + + async get(id: string): Promise { + const collections = await this.stateService.getEncryptedCollections(); + if (collections == null || !collections.hasOwnProperty(id)) { + return null; } - clearCache(): void { - this.decryptedCollectionCache = null; + return new Collection(collections[id]); + } + + async getAll(): Promise { + const collections = await this.stateService.getEncryptedCollections(); + const response: Collection[] = []; + for (const id in collections) { + if (collections.hasOwnProperty(id)) { + response.push(new Collection(collections[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + let decryptedCollections = await this.stateService.getDecryptedCollections(); + if (decryptedCollections != null) { + return decryptedCollections; } - async encrypt(model: CollectionView): Promise { - if (model.organizationId == null) { - throw new Error('Collection has no organization id.'); - } - const key = await this.cryptoService.getOrgKey(model.organizationId); - if (key == null) { - throw new Error('No key for this collection\'s organization.'); - } - const collection = new Collection(); - collection.id = model.id; - collection.organizationId = model.organizationId; - collection.readOnly = model.readOnly; - collection.name = await this.cryptoService.encrypt(model.name, key); - return collection; + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { + throw new Error("No key."); } - async decryptMany(collections: Collection[]): Promise { - if (collections == null) { - return []; - } - const decCollections: CollectionView[] = []; - const promises: Promise[] = []; - collections.forEach(collection => { - promises.push(collection.decrypt().then(c => decCollections.push(c))); - }); - await Promise.all(promises); - return decCollections.sort(Utils.getSortFunction(this.i18nService, 'name')); + const collections = await this.getAll(); + decryptedCollections = await this.decryptMany(collections); + await this.stateService.setDecryptedCollections(decryptedCollections); + return decryptedCollections; + } + + async getAllNested(collections: CollectionView[] = null): Promise[]> { + if (collections == null) { + collections = await this.getAllDecrypted(); + } + const nodes: TreeNode[] = []; + collections.forEach((c) => { + const collectionCopy = new CollectionView(); + collectionCopy.id = c.id; + collectionCopy.organizationId = c.organizationId; + const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; + ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter); + }); + return nodes; + } + + async getNested(id: string): Promise> { + const collections = await this.getAllNested(); + return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode; + } + + async upsert(collection: CollectionData | CollectionData[]): Promise { + let collections = await this.stateService.getEncryptedCollections(); + if (collections == null) { + collections = {}; } - async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); - if (collections == null || !collections.hasOwnProperty(id)) { - return null; - } - - return new Collection(collections[id]); + if (collection instanceof CollectionData) { + const c = collection as CollectionData; + collections[c.id] = c; + } else { + (collection as CollectionData[]).forEach((c) => { + collections[c.id] = c; + }); } - async getAll(): Promise { - const userId = await this.userService.getUserId(); - const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); - const response: Collection[] = []; - for (const id in collections) { - if (collections.hasOwnProperty(id)) { - response.push(new Collection(collections[id])); - } - } - return response; + await this.replace(collections); + } + + async replace(collections: { [id: string]: CollectionData }): Promise { + await this.clearCache(); + await this.stateService.setEncryptedCollections(collections); + } + + async clear(userId?: string): Promise { + await this.clearCache(userId); + await this.stateService.setEncryptedCollections(null, { userId: userId }); + } + + async delete(id: string | string[]): Promise { + const collections = await this.stateService.getEncryptedCollections(); + if (collections == null) { + return; } - async getAllDecrypted(): Promise { - if (this.decryptedCollectionCache != null) { - return this.decryptedCollectionCache; - } - - const hasKey = await this.cryptoService.hasKey(); - if (!hasKey) { - throw new Error('No key.'); - } - - const collections = await this.getAll(); - this.decryptedCollectionCache = await this.decryptMany(collections); - return this.decryptedCollectionCache; + if (typeof id === "string") { + delete collections[id]; + } else { + (id as string[]).forEach((i) => { + delete collections[i]; + }); } - async getAllNested(collections: CollectionView[] = null): Promise[]> { - if (collections == null) { - collections = await this.getAllDecrypted(); - } - const nodes: TreeNode[] = []; - collections.forEach(c => { - const collectionCopy = new CollectionView(); - collectionCopy.id = c.id; - collectionCopy.organizationId = c.organizationId; - const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter) : []; - ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter); - }); - return nodes; - } - - async getNested(id: string): Promise> { - const collections = await this.getAllNested(); - return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode; - } - - async upsert(collection: CollectionData | CollectionData[]): Promise { - const userId = await this.userService.getUserId(); - let collections = await this.storageService.get<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); - if (collections == null) { - collections = {}; - } - - if (collection instanceof CollectionData) { - const c = collection as CollectionData; - collections[c.id] = c; - } else { - (collection as CollectionData[]).forEach(c => { - collections[c.id] = c; - }); - } - - await this.storageService.save(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; - } - - async replace(collections: { [id: string]: CollectionData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; - } - - async clear(userId: string): Promise { - await this.storageService.remove(Keys.collectionsPrefix + userId); - this.decryptedCollectionCache = null; - } - - async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); - if (collections == null) { - return; - } - - if (typeof id === 'string') { - const i = id as string; - delete collections[id]; - } else { - (id as string[]).forEach(i => { - delete collections[i]; - }); - } - - await this.storageService.save(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; - } + await this.replace(collections); + } } diff --git a/common/src/services/constants.service.ts b/common/src/services/constants.service.ts deleted file mode 100644 index 9202f862..00000000 --- a/common/src/services/constants.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -export class ConstantsService { - static readonly environmentUrlsKey: string = 'environmentUrls'; - static readonly disableGaKey: string = 'disableGa'; - static readonly disableAddLoginNotificationKey: string = 'disableAddLoginNotification'; - static readonly disableChangedPasswordNotificationKey: string = 'disableChangedPasswordNotification'; - static readonly disableContextMenuItemKey: string = 'disableContextMenuItem'; - static readonly disableFaviconKey: string = 'disableFavicon'; - static readonly disableBadgeCounterKey: string = 'disableBadgeCounter'; - static readonly disableAutoTotpCopyKey: string = 'disableAutoTotpCopy'; - static readonly disableAutoBiometricsPromptKey: string = 'noAutoPromptBiometrics'; - static readonly enableAutoFillOnPageLoadKey: string = 'enableAutoFillOnPageLoad'; - static readonly autoFillOnPageLoadDefaultKey: string = 'autoFillOnPageLoadDefault'; - static readonly vaultTimeoutKey: string = 'lockOption'; - static readonly vaultTimeoutActionKey: string = 'vaultTimeoutAction'; - static readonly lastActiveKey: string = 'lastActive'; - static readonly neverDomainsKey: string = 'neverDomains'; - static readonly installedVersionKey: string = 'installedVersion'; - static readonly localeKey: string = 'locale'; - static readonly themeKey: string = 'theme'; - static readonly collapsedGroupingsKey: string = 'collapsedGroupings'; - static readonly autoConfirmFingerprints: string = 'autoConfirmFingerprints'; - static readonly dontShowCardsCurrentTab: string = 'dontShowCardsCurrentTab'; - static readonly dontShowIdentitiesCurrentTab: string = 'dontShowIdentitiesCurrentTab'; - static readonly defaultUriMatch: string = 'defaultUriMatch'; - static readonly pinProtectedKey: string = 'pinProtectedKey'; - static readonly protectedPin: string = 'protectedPin'; - static readonly clearClipboardKey: string = 'clearClipboardKey'; - static readonly eventCollectionKey: string = 'eventCollection'; - static readonly ssoCodeVerifierKey: string = 'ssoCodeVerifier'; - static readonly ssoStateKey: string = 'ssoState'; - static readonly biometricUnlockKey: string = 'biometric'; - static readonly biometricText: string = 'biometricText'; - static readonly biometricAwaitingAcceptance: string = 'biometricAwaitingAcceptance'; - static readonly biometricFingerprintValidated: string = 'biometricFingerprintValidated'; - - readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; - readonly disableGaKey: string = ConstantsService.disableGaKey; - readonly disableAddLoginNotificationKey: string = ConstantsService.disableAddLoginNotificationKey; - readonly disableContextMenuItemKey: string = ConstantsService.disableContextMenuItemKey; - readonly disableFaviconKey: string = ConstantsService.disableFaviconKey; - readonly disableBadgeCounterKey: string = ConstantsService.disableBadgeCounterKey; - readonly disableAutoTotpCopyKey: string = ConstantsService.disableAutoTotpCopyKey; - readonly disableAutoBiometricsPromptKey: string = ConstantsService.disableAutoBiometricsPromptKey; - readonly enableAutoFillOnPageLoadKey: string = ConstantsService.enableAutoFillOnPageLoadKey; - readonly autoFillOnPageLoadDefaultKey: string = ConstantsService.autoFillOnPageLoadDefaultKey; - readonly vaultTimeoutKey: string = ConstantsService.vaultTimeoutKey; - readonly vaultTimeoutActionKey: string = ConstantsService.vaultTimeoutActionKey; - readonly lastActiveKey: string = ConstantsService.lastActiveKey; - readonly neverDomainsKey: string = ConstantsService.neverDomainsKey; - readonly installedVersionKey: string = ConstantsService.installedVersionKey; - readonly localeKey: string = ConstantsService.localeKey; - readonly themeKey: string = ConstantsService.themeKey; - readonly collapsedGroupingsKey: string = ConstantsService.collapsedGroupingsKey; - readonly autoConfirmFingerprints: string = ConstantsService.autoConfirmFingerprints; - readonly dontShowCardsCurrentTab: string = ConstantsService.dontShowCardsCurrentTab; - readonly dontShowIdentitiesCurrentTab: string = ConstantsService.dontShowIdentitiesCurrentTab; - readonly defaultUriMatch: string = ConstantsService.defaultUriMatch; - readonly pinProtectedKey: string = ConstantsService.pinProtectedKey; - readonly protectedPin: string = ConstantsService.protectedPin; - readonly clearClipboardKey: string = ConstantsService.clearClipboardKey; - readonly eventCollectionKey: string = ConstantsService.eventCollectionKey; - readonly ssoCodeVerifierKey: string = ConstantsService.ssoCodeVerifierKey; - readonly ssoStateKey: string = ConstantsService.ssoStateKey; - readonly biometricUnlockKey: string = ConstantsService.biometricUnlockKey; - readonly biometricText: string = ConstantsService.biometricText; - readonly biometricAwaitingAcceptance: string = ConstantsService.biometricAwaitingAcceptance; - readonly biometricFingerprintValidated: string = ConstantsService.biometricFingerprintValidated; -} diff --git a/common/src/services/crypto.service.ts b/common/src/services/crypto.service.ts index 60ef0ea0..05c14d93 100644 --- a/common/src/services/crypto.service.ts +++ b/common/src/services/crypto.service.ts @@ -1,888 +1,968 @@ -import * as bigInt from 'big-integer'; +import * as bigInt from "big-integer"; -import { EncryptionType } from '../enums/encryptionType'; -import { HashPurpose } from '../enums/hashPurpose'; -import { KdfType } from '../enums/kdfType'; +import { EncryptionType } from "../enums/encryptionType"; +import { HashPurpose } from "../enums/hashPurpose"; +import { KdfType } from "../enums/kdfType"; +import { KeySuffixOptions } from "../enums/keySuffixOptions"; -import { EncArrayBuffer } from '../models/domain/encArrayBuffer'; -import { EncryptedObject } from '../models/domain/encryptedObject'; -import { EncString } from '../models/domain/encString'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; +import { EncArrayBuffer } from "../models/domain/encArrayBuffer"; +import { EncryptedObject } from "../models/domain/encryptedObject"; +import { EncString } from "../models/domain/encString"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; -import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; -import { LogService } from '../abstractions/log.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { - KeySuffixOptions, - StorageService, -} from '../abstractions/storage.service'; +import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service"; +import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; +import { LogService } from "../abstractions/log.service"; +import { PlatformUtilsService } from "../abstractions/platformUtils.service"; +import { StateService } from "../abstractions/state.service"; -import { ConstantsService } from './constants.service'; +import { sequentialize } from "../misc/sequentialize"; +import { Utils } from "../misc/utils"; +import { EEFLongWordList } from "../misc/wordlist"; -import { sequentialize } from '../misc/sequentialize'; -import { Utils } from '../misc/utils'; -import { EEFLongWordList } from '../misc/wordlist'; -import { ProfileProviderOrganizationResponse } from '../models/response/profileProviderOrganizationResponse'; -import { ProfileProviderResponse } from '../models/response/profileProviderResponse'; - -export const Keys = { - key: 'key', // Master Key - encOrgKeys: 'encOrgKeys', - encProviderKeys: 'encProviderKeys', - encPrivateKey: 'encPrivateKey', - encKey: 'encKey', // Generated Symmetric Key - keyHash: 'keyHash', -}; +import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse"; +import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse"; +import { ProfileProviderResponse } from "../models/response/profileProviderResponse"; export class CryptoService implements CryptoServiceAbstraction { - private key: SymmetricCryptoKey; - private encKey: SymmetricCryptoKey; - private legacyEtmKey: SymmetricCryptoKey; - private keyHash: string; - private publicKey: ArrayBuffer; - private privateKey: ArrayBuffer; - private orgKeys: Map; - private providerKeys: Map; + constructor( + private cryptoFunctionService: CryptoFunctionService, + protected platformUtilService: PlatformUtilsService, + protected logService: LogService, + protected stateService: StateService + ) {} - constructor(private storageService: StorageService, protected secureStorageService: StorageService, - private cryptoFunctionService: CryptoFunctionService, protected platformUtilService: PlatformUtilsService, - protected logService: LogService) { + async setKey(key: SymmetricCryptoKey, userId?: string): Promise { + await this.stateService.setCryptoMasterKey(key, { userId: userId }); + await this.storeKey(key, userId); + } + + async setKeyHash(keyHash: string): Promise { + await this.stateService.setKeyHash(keyHash); + } + + async setEncKey(encKey: string): Promise { + if (encKey == null) { + return; } - async setKey(key: SymmetricCryptoKey): Promise { - this.key = key; + await this.stateService.setDecryptedCryptoSymmetricKey(null); + await this.stateService.setEncryptedCryptoSymmetricKey(encKey); + } - await this.storeKey(key); + async setEncPrivateKey(encPrivateKey: string): Promise { + if (encPrivateKey == null) { + return; } - setKeyHash(keyHash: string): Promise<{}> { - this.keyHash = keyHash; - return this.storageService.save(Keys.keyHash, keyHash); + await this.stateService.setDecryptedPrivateKey(null); + await this.stateService.setEncryptedPrivateKey(encPrivateKey); + } + + async setOrgKeys( + orgs: ProfileOrganizationResponse[], + providerOrgs: ProfileProviderOrganizationResponse[] + ): Promise { + const orgKeys: any = {}; + orgs.forEach((org) => { + orgKeys[org.id] = org.key; + }); + + for (const providerOrg of providerOrgs) { + // Convert provider encrypted keys to user encrypted. + const providerKey = await this.getProviderKey(providerOrg.providerId); + const decValue = await this.decryptToBytes(new EncString(providerOrg.key), providerKey); + orgKeys[providerOrg.id] = (await this.rsaEncrypt(decValue)).encryptedString; } - async setEncKey(encKey: string): Promise<{}> { - if (encKey == null) { - return; + await this.stateService.setDecryptedOrganizationKeys(null); + return await this.stateService.setEncryptedOrganizationKeys(orgKeys); + } + + async setProviderKeys(providers: ProfileProviderResponse[]): Promise { + const providerKeys: any = {}; + providers.forEach((provider) => { + providerKeys[provider.id] = provider.key; + }); + + await this.stateService.setDecryptedProviderKeys(null); + return await this.stateService.setEncryptedProviderKeys(providerKeys); + } + + async getKey(keySuffix?: KeySuffixOptions, userId?: string): Promise { + const inMemoryKey = await this.stateService.getCryptoMasterKey({ userId: userId }); + + if (inMemoryKey != null) { + return inMemoryKey; + } + + keySuffix ||= KeySuffixOptions.Auto; + const symmetricKey = await this.getKeyFromStorage(keySuffix, userId); + + if (symmetricKey != null) { + // TODO: Refactor here so get key doesn't also set key + this.setKey(symmetricKey, userId); + } + + return symmetricKey; + } + + async getKeyFromStorage( + keySuffix: KeySuffixOptions, + userId?: string + ): Promise { + const key = await this.retrieveKeyFromStorage(keySuffix, userId); + if (key != null) { + const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer); + + if (!(await this.validateKey(symmetricKey))) { + this.logService.warning("Wrong key, throwing away stored key"); + await this.clearSecretKeyStore(userId); + return null; + } + + return symmetricKey; + } + return null; + } + + async getKeyHash(): Promise { + return await this.stateService.getKeyHash(); + } + + async compareAndUpdateKeyHash(masterPassword: string, key: SymmetricCryptoKey): Promise { + const storedKeyHash = await this.getKeyHash(); + if (masterPassword != null && storedKeyHash != null) { + const localKeyHash = await this.hashPassword( + masterPassword, + key, + HashPurpose.LocalAuthorization + ); + if (localKeyHash != null && storedKeyHash === localKeyHash) { + return true; + } + + // TODO: remove serverKeyHash check in 1-2 releases after everyone's keyHash has been updated + const serverKeyHash = await this.hashPassword( + masterPassword, + key, + HashPurpose.ServerAuthorization + ); + if (serverKeyHash != null && storedKeyHash === serverKeyHash) { + await this.setKeyHash(localKeyHash); + return true; + } + } + + return false; + } + + @sequentialize(() => "getEncKey") + async getEncKey(key: SymmetricCryptoKey = null): Promise { + const inMemoryKey = await this.stateService.getDecryptedCryptoSymmetricKey(); + if (inMemoryKey != null) { + return inMemoryKey; + } + + const encKey = await this.stateService.getEncryptedCryptoSymmetricKey(); + if (encKey == null) { + return null; + } + + if (key == null) { + key = await this.getKey(); + } + if (key == null) { + return null; + } + + let decEncKey: ArrayBuffer; + const encKeyCipher = new EncString(encKey); + if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) { + decEncKey = await this.decryptToBytes(encKeyCipher, key); + } else if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { + const newKey = await this.stretchKey(key); + decEncKey = await this.decryptToBytes(encKeyCipher, newKey); + } else { + throw new Error("Unsupported encKey type."); + } + + if (decEncKey == null) { + return null; + } + const symmetricCryptoKey = new SymmetricCryptoKey(decEncKey); + await this.stateService.setDecryptedCryptoSymmetricKey(symmetricCryptoKey); + return symmetricCryptoKey; + } + + async getPublicKey(): Promise { + const inMemoryPublicKey = await this.stateService.getPublicKey(); + if (inMemoryPublicKey != null) { + return inMemoryPublicKey; + } + + const privateKey = await this.getPrivateKey(); + if (privateKey == null) { + return null; + } + + const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); + await this.stateService.setPublicKey(publicKey); + return publicKey; + } + + async getPrivateKey(): Promise { + const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey(); + if (decryptedPrivateKey != null) { + return decryptedPrivateKey; + } + + const encPrivateKey = await this.stateService.getEncryptedPrivateKey(); + if (encPrivateKey == null) { + return null; + } + + const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), null); + await this.stateService.setDecryptedPrivateKey(privateKey); + return privateKey; + } + + async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise { + if (publicKey == null) { + publicKey = await this.getPublicKey(); + } + if (publicKey === null) { + throw new Error("No public key available."); + } + const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256"); + const userFingerprint = await this.cryptoFunctionService.hkdfExpand( + keyFingerprint, + userId, + 32, + "sha256" + ); + return this.hashPhrase(userFingerprint); + } + + @sequentialize(() => "getOrgKeys") + async getOrgKeys(): Promise> { + const orgKeys: Map = new Map(); + const decryptedOrganizationKeys = await this.stateService.getDecryptedOrganizationKeys(); + if (decryptedOrganizationKeys != null && decryptedOrganizationKeys.size > 0) { + return decryptedOrganizationKeys; + } + + const encOrgKeys = await this.stateService.getEncryptedOrganizationKeys(); + if (encOrgKeys == null) { + return null; + } + + let setKey = false; + + for (const orgId in encOrgKeys) { + if (!encOrgKeys.hasOwnProperty(orgId)) { + continue; + } + + const decValue = await this.rsaDecrypt(encOrgKeys[orgId]); + orgKeys.set(orgId, new SymmetricCryptoKey(decValue)); + setKey = true; + } + + if (setKey) { + await this.stateService.setDecryptedOrganizationKeys(orgKeys); + } + + return orgKeys; + } + + async getOrgKey(orgId: string): Promise { + if (orgId == null) { + return null; + } + + const orgKeys = await this.getOrgKeys(); + if (orgKeys == null || !orgKeys.has(orgId)) { + return null; + } + + return orgKeys.get(orgId); + } + + @sequentialize(() => "getProviderKeys") + async getProviderKeys(): Promise> { + const providerKeys: Map = new Map(); + const decryptedProviderKeys = await this.stateService.getDecryptedProviderKeys(); + if (decryptedProviderKeys != null && decryptedProviderKeys.size > 0) { + return decryptedProviderKeys; + } + + const encProviderKeys = await this.stateService.getEncryptedProviderKeys(); + if (encProviderKeys == null) { + return null; + } + + let setKey = false; + + for (const orgId in encProviderKeys) { + if (!encProviderKeys.hasOwnProperty(orgId)) { + continue; + } + + const decValue = await this.rsaDecrypt(encProviderKeys[orgId]); + providerKeys.set(orgId, new SymmetricCryptoKey(decValue)); + setKey = true; + } + + if (setKey) { + await this.stateService.setDecryptedProviderKeys(providerKeys); + } + + return providerKeys; + } + + async getProviderKey(providerId: string): Promise { + if (providerId == null) { + return null; + } + + const providerKeys = await this.getProviderKeys(); + if (providerKeys == null || !providerKeys.has(providerId)) { + return null; + } + + return providerKeys.get(providerId); + } + + async hasKey(): Promise { + return ( + (await this.hasKeyInMemory()) || + (await this.hasKeyStored(KeySuffixOptions.Auto)) || + (await this.hasKeyStored(KeySuffixOptions.Biometric)) + ); + } + + async hasKeyInMemory(userId?: string): Promise { + return (await this.stateService.getCryptoMasterKey({ userId: userId })) != null; + } + + async hasKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise { + const key = + keySuffix === KeySuffixOptions.Auto + ? await this.stateService.getCryptoMasterKeyAuto({ userId: userId }) + : await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId }); + + return key != null; + } + + async hasEncKey(): Promise { + return (await this.stateService.getEncryptedCryptoSymmetricKey()) != null; + } + + async clearKey(clearSecretStorage: boolean = true, userId?: string): Promise { + await this.stateService.setCryptoMasterKey(null, { userId: userId }); + await this.stateService.setLegacyEtmKey(null, { userId: userId }); + if (clearSecretStorage) { + await this.clearSecretKeyStore(userId); + } + } + + async clearStoredKey(keySuffix: KeySuffixOptions) { + keySuffix === KeySuffixOptions.Auto + ? await this.stateService.setCryptoMasterKeyAuto(null) + : await this.stateService.setCryptoMasterKeyBiometric(null); + } + + async clearKeyHash(userId?: string): Promise { + return await this.stateService.setKeyHash(null, { userId: userId }); + } + + async clearEncKey(memoryOnly?: boolean, userId?: string): Promise { + await this.stateService.setDecryptedCryptoSymmetricKey(null, { userId: userId }); + if (!memoryOnly) { + await this.stateService.setEncryptedCryptoSymmetricKey(null, { userId: userId }); + } + } + + async clearKeyPair(memoryOnly?: boolean, userId?: string): Promise { + const keysToClear: Promise[] = [ + this.stateService.setDecryptedPrivateKey(null, { userId: userId }), + this.stateService.setPublicKey(null, { userId: userId }), + ]; + if (!memoryOnly) { + keysToClear.push(this.stateService.setEncryptedPrivateKey(null, { userId: userId })); + } + return Promise.all(keysToClear); + } + + async clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise { + await this.stateService.setDecryptedOrganizationKeys(null, { userId: userId }); + if (!memoryOnly) { + await this.stateService.setEncryptedOrganizationKeys(null, { userId: userId }); + } + } + + async clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise { + await this.stateService.setDecryptedProviderKeys(null, { userId: userId }); + if (!memoryOnly) { + await this.stateService.setEncryptedProviderKeys(null, { userId: userId }); + } + } + + async clearPinProtectedKey(userId?: string): Promise { + return await this.stateService.setEncryptedPinProtected(null, { userId: userId }); + } + + async clearKeys(userId?: string): Promise { + await this.clearKey(true, userId); + await this.clearKeyHash(userId); + await this.clearOrgKeys(false, userId); + await this.clearProviderKeys(false, userId); + await this.clearEncKey(false, userId); + await this.clearKeyPair(false, userId); + await this.clearPinProtectedKey(userId); + } + + async toggleKey(): Promise { + const key = await this.getKey(); + + await this.setKey(key); + } + + async makeKey( + password: string, + salt: string, + kdf: KdfType, + kdfIterations: number + ): Promise { + let key: ArrayBuffer = null; + if (kdf == null || kdf === KdfType.PBKDF2_SHA256) { + if (kdfIterations == null) { + kdfIterations = 5000; + } else if (kdfIterations < 5000) { + throw new Error("PBKDF2 iteration minimum is 5000."); + } + key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfIterations); + } else { + throw new Error("Unknown Kdf."); + } + return new SymmetricCryptoKey(key); + } + + async makeKeyFromPin( + pin: string, + salt: string, + kdf: KdfType, + kdfIterations: number, + protectedKeyCs: EncString = null + ): Promise { + if (protectedKeyCs == null) { + const pinProtectedKey = await this.stateService.getEncryptedPinProtected(); + if (pinProtectedKey == null) { + throw new Error("No PIN protected key found."); + } + protectedKeyCs = new EncString(pinProtectedKey); + } + const pinKey = await this.makePinKey(pin, salt, kdf, kdfIterations); + const decKey = await this.decryptToBytes(protectedKeyCs, pinKey); + return new SymmetricCryptoKey(decKey); + } + + async makeShareKey(): Promise<[EncString, SymmetricCryptoKey]> { + const shareKey = await this.cryptoFunctionService.randomBytes(64); + const publicKey = await this.getPublicKey(); + const encShareKey = await this.rsaEncrypt(shareKey, publicKey); + return [encShareKey, new SymmetricCryptoKey(shareKey)]; + } + + async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> { + const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); + const publicB64 = Utils.fromBufferToB64(keyPair[0]); + const privateEnc = await this.encrypt(keyPair[1], key); + return [publicB64, privateEnc]; + } + + async makePinKey( + pin: string, + salt: string, + kdf: KdfType, + kdfIterations: number + ): Promise { + const pinKey = await this.makeKey(pin, salt, kdf, kdfIterations); + return await this.stretchKey(pinKey); + } + + async makeSendKey(keyMaterial: ArrayBuffer): Promise { + const sendKey = await this.cryptoFunctionService.hkdf( + keyMaterial, + "bitwarden-send", + "send", + 64, + "sha256" + ); + return new SymmetricCryptoKey(sendKey); + } + + async hashPassword( + password: string, + key: SymmetricCryptoKey, + hashPurpose?: HashPurpose + ): Promise { + if (key == null) { + key = await this.getKey(); + } + if (password == null || key == null) { + throw new Error("Invalid parameters."); + } + + const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1; + const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, "sha256", iterations); + return Utils.fromBufferToB64(hash); + } + + async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, EncString]> { + const theKey = await this.getKeyForEncryption(key); + const encKey = await this.cryptoFunctionService.randomBytes(64); + return this.buildEncKey(theKey, encKey); + } + + async remakeEncKey( + key: SymmetricCryptoKey, + encKey?: SymmetricCryptoKey + ): Promise<[SymmetricCryptoKey, EncString]> { + if (encKey == null) { + encKey = await this.getEncKey(); + } + return this.buildEncKey(key, encKey.key); + } + + async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { + if (plainValue == null) { + return Promise.resolve(null); + } + + let plainBuf: ArrayBuffer; + if (typeof plainValue === "string") { + plainBuf = Utils.fromUtf8ToArray(plainValue).buffer; + } else { + plainBuf = plainValue; + } + + const encObj = await this.aesEncrypt(plainBuf, key); + const iv = Utils.fromBufferToB64(encObj.iv); + const data = Utils.fromBufferToB64(encObj.data); + const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null; + return new EncString(encObj.key.encType, data, iv, mac); + } + + async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise { + const encValue = await this.aesEncrypt(plainValue, key); + let macLen = 0; + if (encValue.mac != null) { + macLen = encValue.mac.byteLength; + } + + const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength); + encBytes.set([encValue.key.encType]); + encBytes.set(new Uint8Array(encValue.iv), 1); + if (encValue.mac != null) { + encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength); + } + + encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen); + return new EncArrayBuffer(encBytes.buffer); + } + + async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise { + if (publicKey == null) { + publicKey = await this.getPublicKey(); + } + if (publicKey == null) { + throw new Error("Public key unavailable."); + } + + const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1"); + return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); + } + + async rsaDecrypt(encValue: string, privateKeyValue?: ArrayBuffer): Promise { + const headerPieces = encValue.split("."); + let encType: EncryptionType = null; + let encPieces: string[]; + + if (headerPieces.length === 1) { + encType = EncryptionType.Rsa2048_OaepSha256_B64; + encPieces = [headerPieces[0]]; + } else if (headerPieces.length === 2) { + try { + encType = parseInt(headerPieces[0], null); + encPieces = headerPieces[1].split("|"); + } catch (e) { + this.logService.error(e); + } + } + + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: + // HmacSha256 types are deprecated + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Error("encType unavailable."); + } + + if (encPieces == null || encPieces.length <= 0) { + throw new Error("encPieces unavailable."); + } + + const data = Utils.fromB64ToArray(encPieces[0]).buffer; + const privateKey = privateKeyValue ?? (await this.getPrivateKey()); + if (privateKey == null) { + throw new Error("No private key."); + } + + let alg: "sha1" | "sha256" = "sha1"; + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + alg = "sha256"; + break; + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Error("encType unavailable."); + } + + return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); + } + + async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise { + const iv = Utils.fromB64ToArray(encString.iv).buffer; + const data = Utils.fromB64ToArray(encString.data).buffer; + const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer : null; + const decipher = await this.aesDecryptToBytes(encString.encryptionType, data, iv, mac, key); + if (decipher == null) { + return null; + } + + return decipher; + } + + async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise { + return await this.aesDecryptToUtf8( + encString.encryptionType, + encString.data, + encString.iv, + encString.mac, + key + ); + } + + async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { + if (encBuf == null) { + throw new Error("no encBuf."); + } + + const encBytes = new Uint8Array(encBuf); + const encType = encBytes[0]; + let ctBytes: Uint8Array = null; + let ivBytes: Uint8Array = null; + let macBytes: Uint8Array = null; + + switch (encType) { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if (encBytes.length <= 49) { + // 1 + 16 + 32 + ctLength + return null; } - await this.storageService.save(Keys.encKey, encKey); - this.encKey = null; - } - - async setEncPrivateKey(encPrivateKey: string): Promise<{}> { - if (encPrivateKey == null) { - return; + ivBytes = encBytes.slice(1, 17); + macBytes = encBytes.slice(17, 49); + ctBytes = encBytes.slice(49); + break; + case EncryptionType.AesCbc256_B64: + if (encBytes.length <= 17) { + // 1 + 16 + ctLength + return null; } - await this.storageService.save(Keys.encPrivateKey, encPrivateKey); - this.privateKey = null; - } - - async setOrgKeys(orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[]): Promise<{}> { - const orgKeys: any = {}; - orgs.forEach(org => { - orgKeys[org.id] = org.key; - }); - - for (const providerOrg of providerOrgs) { - // Convert provider encrypted keys to user encrypted. - const providerKey = await this.getProviderKey(providerOrg.providerId); - const decValue = await this.decryptToBytes(new EncString(providerOrg.key), providerKey); - orgKeys[providerOrg.id] = await (await this.rsaEncrypt(decValue)).encryptedString; - } - - this.orgKeys = null; - return this.storageService.save(Keys.encOrgKeys, orgKeys); - } - - setProviderKeys(providers: ProfileProviderResponse[]): Promise<{}> { - const providerKeys: any = {}; - providers.forEach(provider => { - providerKeys[provider.id] = provider.key; - }); - - this.providerKeys = null; - return this.storageService.save(Keys.encProviderKeys, providerKeys); - } - - async getKey(keySuffix?: KeySuffixOptions): Promise { - if (this.key != null) { - return this.key; - } - - keySuffix ||= 'auto'; - const symmetricKey = await this.getKeyFromStorage(keySuffix); - - if (symmetricKey != null) { - this.setKey(symmetricKey); - } - - return symmetricKey; - } - - async getKeyFromStorage(keySuffix: KeySuffixOptions): Promise { - const key = await this.retrieveKeyFromStorage(keySuffix); - if (key != null) { - - const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer); - - if (!await this.validateKey(symmetricKey)) { - this.logService.warning('Wrong key, throwing away stored key'); - this.secureStorageService.remove(Keys.key, { keySuffix: keySuffix }); - return null; - } - - return symmetricKey; - } + ivBytes = encBytes.slice(1, 17); + ctBytes = encBytes.slice(17); + break; + default: return null; } - async getKeyHash(): Promise { - if (this.keyHash != null) { - return this.keyHash; - } + return await this.aesDecryptToBytes( + encType, + ctBytes.buffer, + ivBytes.buffer, + macBytes != null ? macBytes.buffer : null, + key + ); + } - const keyHash = await this.storageService.get(Keys.keyHash); - if (keyHash != null) { - this.keyHash = keyHash; - } - - return keyHash == null ? null : this.keyHash; + // EFForg/OpenWireless + // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js + async randomNumber(min: number, max: number): Promise { + let rval = 0; + const range = max - min + 1; + const bitsNeeded = Math.ceil(Math.log2(range)); + if (bitsNeeded > 53) { + throw new Error("We cannot generate numbers larger than 53 bits."); } - async compareAndUpdateKeyHash(masterPassword: string, key: SymmetricCryptoKey): Promise { - const storedKeyHash = await this.getKeyHash(); - if (masterPassword != null && storedKeyHash != null) { - const localKeyHash = await this.hashPassword(masterPassword, key, HashPurpose.LocalAuthorization); - if (localKeyHash != null && storedKeyHash === localKeyHash) { - return true; - } + const bytesNeeded = Math.ceil(bitsNeeded / 8); + const mask = Math.pow(2, bitsNeeded) - 1; + // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 - // TODO: remove serverKeyHash check in 1-2 releases after everyone's keyHash has been updated - const serverKeyHash = await this.hashPassword(masterPassword, key, HashPurpose.ServerAuthorization); - if (serverKeyHash != null && storedKeyHash === serverKeyHash) { - await this.setKeyHash(localKeyHash); - return true; - } - } + // Fill a byte array with N random numbers + const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded)); + let p = (bytesNeeded - 1) * 8; + for (let i = 0; i < bytesNeeded; i++) { + rval += byteArray[i] * Math.pow(2, p); + p -= 8; + } + + // Use & to apply the mask and reduce the number of recursive lookups + // tslint:disable-next-line + rval = rval & mask; + + if (rval >= range) { + // Integer out of acceptable range + return this.randomNumber(min, max); + } + + // Return an integer that falls within the range + return min + rval; + } + + async validateKey(key: SymmetricCryptoKey) { + try { + const encPrivateKey = await this.stateService.getEncryptedPrivateKey(); + const encKey = await this.getEncKey(key); + if (encPrivateKey == null || encKey == null) { return false; + } + + const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey); + await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); + } catch (e) { + return false; } - @sequentialize(() => 'getEncKey') - async getEncKey(key: SymmetricCryptoKey = null): Promise { - if (this.encKey != null) { - return this.encKey; - } + return true; + } - const encKey = await this.storageService.get(Keys.encKey); - if (encKey == null) { - return null; - } + // Helpers + protected async storeKey(key: SymmetricCryptoKey, userId?: string) { + if ( + (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) || + (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) + ) { + await this.stateService.setCryptoMasterKeyB64(key.keyB64, { userId: userId }); + } else { + await this.stateService.setCryptoMasterKeyB64(null, { userId: userId }); + } + } - if (key == null) { - key = await this.getKey(); - } - if (key == null) { - return null; - } + protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string) { + let shouldStoreKey = false; + if (keySuffix === KeySuffixOptions.Auto) { + const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId }); + shouldStoreKey = vaultTimeout == null; + } else if (keySuffix === KeySuffixOptions.Biometric) { + const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId }); + shouldStoreKey = biometricUnlock && this.platformUtilService.supportsSecureStorage(); + } + return shouldStoreKey; + } - let decEncKey: ArrayBuffer; - const encKeyCipher = new EncString(encKey); - if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) { - decEncKey = await this.decryptToBytes(encKeyCipher, key); - } else if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { - const newKey = await this.stretchKey(key); - decEncKey = await this.decryptToBytes(encKeyCipher, newKey); - } else { - throw new Error('Unsupported encKey type.'); - } + protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string) { + return keySuffix === KeySuffixOptions.Auto + ? await this.stateService.getCryptoMasterKeyAuto({ userId: userId }) + : await this.stateService.getCryptoMasterKeyBiometric({ userId: userId }); + } - if (decEncKey == null) { - return null; - } - this.encKey = new SymmetricCryptoKey(decEncKey); - return this.encKey; + private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise { + const obj = new EncryptedObject(); + obj.key = await this.getKeyForEncryption(key); + obj.iv = await this.cryptoFunctionService.randomBytes(16); + obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey); + + if (obj.key.macKey != null) { + const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength); + macData.set(new Uint8Array(obj.iv), 0); + macData.set(new Uint8Array(obj.data), obj.iv.byteLength); + obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256"); } - async getPublicKey(): Promise { - if (this.publicKey != null) { - return this.publicKey; - } + return obj; + } - const privateKey = await this.getPrivateKey(); - if (privateKey == null) { - return null; - } + private async aesDecryptToUtf8( + encType: EncryptionType, + data: string, + iv: string, + mac: string, + key: SymmetricCryptoKey + ): Promise { + const keyForEnc = await this.getKeyForEncryption(key); + const theKey = await this.resolveLegacyKey(encType, keyForEnc); - this.publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); - return this.publicKey; + if (theKey.macKey != null && mac == null) { + this.logService.error("mac required."); + return null; } - async getPrivateKey(): Promise { - if (this.privateKey != null) { - return this.privateKey; - } - - const encPrivateKey = await this.storageService.get(Keys.encPrivateKey); - if (encPrivateKey == null) { - return null; - } - - this.privateKey = await this.decryptToBytes(new EncString(encPrivateKey), null); - return this.privateKey; + if (theKey.encType !== encType) { + this.logService.error("encType unavailable."); + return null; } - async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise { - if (publicKey == null) { - publicKey = await this.getPublicKey(); - } - if (publicKey === null) { - throw new Error('No public key available.'); - } - const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, 'sha256'); - const userFingerprint = await this.cryptoFunctionService.hkdfExpand(keyFingerprint, userId, 32, 'sha256'); - return this.hashPhrase(userFingerprint); + const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(data, iv, mac, theKey); + if (fastParams.macKey != null && fastParams.mac != null) { + const computedMac = await this.cryptoFunctionService.hmacFast( + fastParams.macData, + fastParams.macKey, + "sha256" + ); + const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); + if (!macsEqual) { + this.logService.error("mac failed."); + return null; + } } - @sequentialize(() => 'getOrgKeys') - async getOrgKeys(): Promise> { - if (this.orgKeys != null && this.orgKeys.size > 0) { - return this.orgKeys; - } + return this.cryptoFunctionService.aesDecryptFast(fastParams); + } - const encOrgKeys = await this.storageService.get(Keys.encOrgKeys); - if (encOrgKeys == null) { - return null; - } + private async aesDecryptToBytes( + encType: EncryptionType, + data: ArrayBuffer, + iv: ArrayBuffer, + mac: ArrayBuffer, + key: SymmetricCryptoKey + ): Promise { + const keyForEnc = await this.getKeyForEncryption(key); + const theKey = await this.resolveLegacyKey(encType, keyForEnc); - const orgKeys: Map = new Map(); - let setKey = false; - - for (const orgId in encOrgKeys) { - if (!encOrgKeys.hasOwnProperty(orgId)) { - continue; - } - - const decValue = await this.rsaDecrypt(encOrgKeys[orgId]); - orgKeys.set(orgId, new SymmetricCryptoKey(decValue)); - setKey = true; - } - - if (setKey) { - this.orgKeys = orgKeys; - } - - return this.orgKeys; + if (theKey.macKey != null && mac == null) { + return null; } - async getOrgKey(orgId: string): Promise { - if (orgId == null) { - return null; - } - - const orgKeys = await this.getOrgKeys(); - if (orgKeys == null || !orgKeys.has(orgId)) { - return null; - } - - return orgKeys.get(orgId); + if (theKey.encType !== encType) { + return null; } - @sequentialize(() => 'getProviderKeys') - async getProviderKeys(): Promise> { - if (this.providerKeys != null && this.providerKeys.size > 0) { - return this.providerKeys; - } + if (theKey.macKey != null && mac != null) { + const macData = new Uint8Array(iv.byteLength + data.byteLength); + macData.set(new Uint8Array(iv), 0); + macData.set(new Uint8Array(data), iv.byteLength); + const computedMac = await this.cryptoFunctionService.hmac( + macData.buffer, + theKey.macKey, + "sha256" + ); + if (computedMac === null) { + return null; + } - const encProviderKeys = await this.storageService.get(Keys.encProviderKeys); - if (encProviderKeys == null) { - return null; - } - - const providerKeys: Map = new Map(); - let setKey = false; - - for (const orgId in encProviderKeys) { - if (!encProviderKeys.hasOwnProperty(orgId)) { - continue; - } - - const decValue = await this.rsaDecrypt(encProviderKeys[orgId]); - providerKeys.set(orgId, new SymmetricCryptoKey(decValue)); - setKey = true; - } - - if (setKey) { - this.providerKeys = providerKeys; - } - - return this.providerKeys; + const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac); + if (!macsMatch) { + this.logService.error("mac failed."); + return null; + } } - async getProviderKey(providerId: string): Promise { - if (providerId == null) { - return null; - } + return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey); + } - const providerKeys = await this.getProviderKeys(); - if (providerKeys == null || !providerKeys.has(providerId)) { - return null; - } - - return providerKeys.get(providerId); + private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { + if (key != null) { + return key; } - async hasKey(): Promise { - return this.hasKeyInMemory() || await this.hasKeyStored('auto') || await this.hasKeyStored('biometric'); + const encKey = await this.getEncKey(); + if (encKey != null) { + return encKey; } - hasKeyInMemory(): boolean { - return this.key != null; + return await this.getKey(); + } + + private async resolveLegacyKey( + encType: EncryptionType, + key: SymmetricCryptoKey + ): Promise { + if ( + encType === EncryptionType.AesCbc128_HmacSha256_B64 && + key.encType === EncryptionType.AesCbc256_B64 + ) { + // Old encrypt-then-mac scheme, make a new key + let legacyKey = await this.stateService.getLegacyEtmKey(); + if (legacyKey == null) { + legacyKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64); + await this.stateService.setLegacyEtmKey(legacyKey); + } + return legacyKey; } - hasKeyStored(keySuffix: KeySuffixOptions): Promise { - return this.secureStorageService.has(Keys.key, { keySuffix: keySuffix }); + return key; + } + + private async stretchKey(key: SymmetricCryptoKey): Promise { + const newKey = new Uint8Array(64); + const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256"); + const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256"); + newKey.set(new Uint8Array(encKey)); + newKey.set(new Uint8Array(macKey), 32); + return new SymmetricCryptoKey(newKey.buffer); + } + + private async hashPhrase(hash: ArrayBuffer, minimumEntropy: number = 64) { + const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2); + let numWords = Math.ceil(minimumEntropy / entropyPerWord); + + const hashArr = Array.from(new Uint8Array(hash)); + const entropyAvailable = hashArr.length * 4; + if (numWords * entropyPerWord > entropyAvailable) { + throw new Error("Output entropy of hash function is too small"); } - async hasEncKey(): Promise { - const encKey = await this.storageService.get(Keys.encKey); - return encKey != null; + const phrase: string[] = []; + let hashNumber = bigInt.fromArray(hashArr, 256); + while (numWords--) { + const remainder = hashNumber.mod(EEFLongWordList.length); + hashNumber = hashNumber.divide(EEFLongWordList.length); + phrase.push(EEFLongWordList[remainder as any]); } + return phrase; + } - async clearKey(clearSecretStorage: boolean = true): Promise { - this.key = this.legacyEtmKey = null; - if (clearSecretStorage) { - this.clearStoredKey('auto'); - this.clearStoredKey('biometric'); - } + private async buildEncKey( + key: SymmetricCryptoKey, + encKey: ArrayBuffer + ): Promise<[SymmetricCryptoKey, EncString]> { + let encKeyEnc: EncString = null; + if (key.key.byteLength === 32) { + const newKey = await this.stretchKey(key); + encKeyEnc = await this.encrypt(encKey, newKey); + } else if (key.key.byteLength === 64) { + encKeyEnc = await this.encrypt(encKey, key); + } else { + throw new Error("Invalid key size."); } + return [new SymmetricCryptoKey(encKey), encKeyEnc]; + } - async clearStoredKey(keySuffix: KeySuffixOptions) { - await this.secureStorageService.remove(Keys.key, { keySuffix: keySuffix }); - } - - clearKeyHash(): Promise { - this.keyHash = null; - return this.storageService.remove(Keys.keyHash); - } - - clearEncKey(memoryOnly?: boolean): Promise { - this.encKey = null; - if (memoryOnly) { - return Promise.resolve(); - } - return this.storageService.remove(Keys.encKey); - } - - clearKeyPair(memoryOnly?: boolean): Promise { - this.privateKey = null; - this.publicKey = null; - if (memoryOnly) { - return Promise.resolve(); - } - return this.storageService.remove(Keys.encPrivateKey); - } - - clearOrgKeys(memoryOnly?: boolean): Promise { - this.orgKeys = null; - if (memoryOnly) { - return Promise.resolve(); - } - return this.storageService.remove(Keys.encOrgKeys); - } - - clearProviderKeys(memoryOnly?: boolean): Promise { - this.providerKeys = null; - if (memoryOnly) { - return Promise.resolve(); - } - return this.storageService.remove(Keys.encOrgKeys); - } - - clearPinProtectedKey(): Promise { - return this.storageService.remove(ConstantsService.pinProtectedKey); - } - - async clearKeys(): Promise { - await this.clearKey(); - await this.clearKeyHash(); - await this.clearOrgKeys(); - await this.clearProviderKeys(); - await this.clearEncKey(); - await this.clearKeyPair(); - await this.clearPinProtectedKey(); - } - - async toggleKey(): Promise { - const key = await this.getKey(); - - await this.setKey(key); - } - - async makeKey(password: string, salt: string, kdf: KdfType, kdfIterations: number): - Promise { - let key: ArrayBuffer = null; - if (kdf == null || kdf === KdfType.PBKDF2_SHA256) { - if (kdfIterations == null) { - kdfIterations = 5000; - } else if (kdfIterations < 5000) { - throw new Error('PBKDF2 iteration minimum is 5000.'); - } - key = await this.cryptoFunctionService.pbkdf2(password, salt, 'sha256', kdfIterations); - } else { - throw new Error('Unknown Kdf.'); - } - return new SymmetricCryptoKey(key); - } - - async makeKeyFromPin(pin: string, salt: string, kdf: KdfType, kdfIterations: number, - protectedKeyCs: EncString = null): - Promise { - if (protectedKeyCs == null) { - const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); - if (pinProtectedKey == null) { - throw new Error('No PIN protected key found.'); - } - protectedKeyCs = new EncString(pinProtectedKey); - } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfIterations); - const decKey = await this.decryptToBytes(protectedKeyCs, pinKey); - return new SymmetricCryptoKey(decKey); - } - - async makeShareKey(): Promise<[EncString, SymmetricCryptoKey]> { - const shareKey = await this.cryptoFunctionService.randomBytes(64); - const publicKey = await this.getPublicKey(); - const encShareKey = await this.rsaEncrypt(shareKey, publicKey); - return [encShareKey, new SymmetricCryptoKey(shareKey)]; - } - - async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> { - const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); - const publicB64 = Utils.fromBufferToB64(keyPair[0]); - const privateEnc = await this.encrypt(keyPair[1], key); - return [publicB64, privateEnc]; - } - - async makePinKey(pin: string, salt: string, kdf: KdfType, kdfIterations: number): Promise { - const pinKey = await this.makeKey(pin, salt, kdf, kdfIterations); - return await this.stretchKey(pinKey); - } - - async makeSendKey(keyMaterial: ArrayBuffer): Promise { - const sendKey = await this.cryptoFunctionService.hkdf(keyMaterial, 'bitwarden-send', 'send', 64, 'sha256'); - return new SymmetricCryptoKey(sendKey); - } - - async hashPassword(password: string, key: SymmetricCryptoKey, hashPurpose?: HashPurpose): Promise { - if (key == null) { - key = await this.getKey(); - } - if (password == null || key == null) { - throw new Error('Invalid parameters.'); - } - - const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1; - const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, 'sha256', iterations); - return Utils.fromBufferToB64(hash); - } - - async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, EncString]> { - const theKey = await this.getKeyForEncryption(key); - const encKey = await this.cryptoFunctionService.randomBytes(64); - return this.buildEncKey(theKey, encKey); - } - - async remakeEncKey(key: SymmetricCryptoKey, encKey?: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, EncString]> { - if (encKey == null) { - encKey = await this.getEncKey(); - } - return this.buildEncKey(key, encKey.key); - } - - async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { - if (plainValue == null) { - return Promise.resolve(null); - } - - let plainBuf: ArrayBuffer; - if (typeof (plainValue) === 'string') { - plainBuf = Utils.fromUtf8ToArray(plainValue).buffer; - } else { - plainBuf = plainValue; - } - - const encObj = await this.aesEncrypt(plainBuf, key); - const iv = Utils.fromBufferToB64(encObj.iv); - const data = Utils.fromBufferToB64(encObj.data); - const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null; - return new EncString(encObj.key.encType, data, iv, mac); - } - - async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise { - const encValue = await this.aesEncrypt(plainValue, key); - let macLen = 0; - if (encValue.mac != null) { - macLen = encValue.mac.byteLength; - } - - const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength); - encBytes.set([encValue.key.encType]); - encBytes.set(new Uint8Array(encValue.iv), 1); - if (encValue.mac != null) { - encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength); - } - - encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen); - return new EncArrayBuffer(encBytes.buffer); - } - - async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise { - if (publicKey == null) { - publicKey = await this.getPublicKey(); - } - if (publicKey == null) { - throw new Error('Public key unavailable.'); - } - - const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, 'sha1'); - return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); - } - - async rsaDecrypt(encValue: string, privateKeyValue?: ArrayBuffer): Promise { - const headerPieces = encValue.split('.'); - let encType: EncryptionType = null; - let encPieces: string[]; - - if (headerPieces.length === 1) { - encType = EncryptionType.Rsa2048_OaepSha256_B64; - encPieces = [headerPieces[0]]; - } else if (headerPieces.length === 2) { - try { - encType = parseInt(headerPieces[0], null); - encPieces = headerPieces[1].split('|'); - } catch (e) { - this.logService.error(e); - } - } - - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha1_B64: - // HmacSha256 types are deprecated - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error('encType unavailable.'); - } - - if (encPieces == null || encPieces.length <= 0) { - throw new Error('encPieces unavailable.'); - } - - const data = Utils.fromB64ToArray(encPieces[0]).buffer; - const privateKey = privateKeyValue ?? await this.getPrivateKey(); - if (privateKey == null) { - throw new Error('No private key.'); - } - - let alg: 'sha1' | 'sha256' = 'sha1'; - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - alg = 'sha256'; - break; - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error('encType unavailable.'); - } - - return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); - } - - async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise { - const iv = Utils.fromB64ToArray(encString.iv).buffer; - const data = Utils.fromB64ToArray(encString.data).buffer; - const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer : null; - const decipher = await this.aesDecryptToBytes(encString.encryptionType, data, iv, mac, key); - if (decipher == null) { - return null; - } - - return decipher; - } - - async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise { - return await this.aesDecryptToUtf8(encString.encryptionType, encString.data, - encString.iv, encString.mac, key); - } - - async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { - if (encBuf == null) { - throw new Error('no encBuf.'); - } - - const encBytes = new Uint8Array(encBuf); - const encType = encBytes[0]; - let ctBytes: Uint8Array = null; - let ivBytes: Uint8Array = null; - let macBytes: Uint8Array = null; - - switch (encType) { - case EncryptionType.AesCbc128_HmacSha256_B64: - case EncryptionType.AesCbc256_HmacSha256_B64: - if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength - return null; - } - - ivBytes = encBytes.slice(1, 17); - macBytes = encBytes.slice(17, 49); - ctBytes = encBytes.slice(49); - break; - case EncryptionType.AesCbc256_B64: - if (encBytes.length <= 17) { // 1 + 16 + ctLength - return null; - } - - ivBytes = encBytes.slice(1, 17); - ctBytes = encBytes.slice(17); - break; - default: - return null; - } - - return await this.aesDecryptToBytes(encType, ctBytes.buffer, ivBytes.buffer, - macBytes != null ? macBytes.buffer : null, key); - } - - // EFForg/OpenWireless - // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js - async randomNumber(min: number, max: number): Promise { - let rval = 0; - const range = max - min + 1; - const bitsNeeded = Math.ceil(Math.log2(range)); - if (bitsNeeded > 53) { - throw new Error('We cannot generate numbers larger than 53 bits.'); - } - - const bytesNeeded = Math.ceil(bitsNeeded / 8); - const mask = Math.pow(2, bitsNeeded) - 1; - // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 - - // Fill a byte array with N random numbers - const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded)); - - let p = (bytesNeeded - 1) * 8; - for (let i = 0; i < bytesNeeded; i++) { - rval += byteArray[i] * Math.pow(2, p); - p -= 8; - } - - // Use & to apply the mask and reduce the number of recursive lookups - // tslint:disable-next-line - rval = rval & mask; - - if (rval >= range) { - // Integer out of acceptable range - return this.randomNumber(min, max); - } - - // Return an integer that falls within the range - return min + rval; - } - - async validateKey(key: SymmetricCryptoKey) { - try { - const encPrivateKey = await this.storageService.get(Keys.encPrivateKey); - const encKey = await this.getEncKey(key); - if (encPrivateKey == null || encKey == null) { - return false; - } - - const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey); - await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); - } catch (e) { - return false; - } - - return true; - } - - // Helpers - - protected async storeKey(key: SymmetricCryptoKey) { - if (await this.shouldStoreKey('auto') || await this.shouldStoreKey('biometric')) { - this.secureStorageService.save(Keys.key, key.keyB64); - } else { - this.secureStorageService.remove(Keys.key); - } - } - - protected async shouldStoreKey(keySuffix: KeySuffixOptions) { - let shouldStoreKey = false; - if (keySuffix === 'auto') { - const vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); - shouldStoreKey = vaultTimeout == null; - } else if (keySuffix === 'biometric') { - const biometricUnlock = await this.storageService.get(ConstantsService.biometricUnlockKey); - shouldStoreKey = biometricUnlock && this.platformUtilService.supportsSecureStorage(); - } - return shouldStoreKey; - } - - protected retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { - return this.secureStorageService.get(Keys.key, { keySuffix: keySuffix }); - } - - private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise { - const obj = new EncryptedObject(); - obj.key = await this.getKeyForEncryption(key); - obj.iv = await this.cryptoFunctionService.randomBytes(16); - obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey); - - if (obj.key.macKey != null) { - const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength); - macData.set(new Uint8Array(obj.iv), 0); - macData.set(new Uint8Array(obj.data), obj.iv.byteLength); - obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, 'sha256'); - } - - return obj; - } - - private async aesDecryptToUtf8(encType: EncryptionType, data: string, iv: string, mac: string, - key: SymmetricCryptoKey): Promise { - const keyForEnc = await this.getKeyForEncryption(key); - const theKey = this.resolveLegacyKey(encType, keyForEnc); - - if (theKey.macKey != null && mac == null) { - this.logService.error('mac required.'); - return null; - } - - if (theKey.encType !== encType) { - this.logService.error('encType unavailable.'); - return null; - } - - const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(data, iv, mac, theKey); - if (fastParams.macKey != null && fastParams.mac != null) { - const computedMac = await this.cryptoFunctionService.hmacFast(fastParams.macData, - fastParams.macKey, 'sha256'); - const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); - if (!macsEqual) { - this.logService.error('mac failed.'); - return null; - } - } - - return this.cryptoFunctionService.aesDecryptFast(fastParams); - } - - private async aesDecryptToBytes(encType: EncryptionType, data: ArrayBuffer, iv: ArrayBuffer, - mac: ArrayBuffer, key: SymmetricCryptoKey): Promise { - const keyForEnc = await this.getKeyForEncryption(key); - const theKey = this.resolveLegacyKey(encType, keyForEnc); - - if (theKey.macKey != null && mac == null) { - return null; - } - - if (theKey.encType !== encType) { - return null; - } - - if (theKey.macKey != null && mac != null) { - const macData = new Uint8Array(iv.byteLength + data.byteLength); - macData.set(new Uint8Array(iv), 0); - macData.set(new Uint8Array(data), iv.byteLength); - const computedMac = await this.cryptoFunctionService.hmac(macData.buffer, theKey.macKey, 'sha256'); - if (computedMac === null) { - return null; - } - - const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac); - if (!macsMatch) { - this.logService.error('mac failed.'); - return null; - } - } - - return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey); - } - - private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { - if (key != null) { - return key; - } - - const encKey = await this.getEncKey(); - if (encKey != null) { - return encKey; - } - - return await this.getKey(); - } - - private resolveLegacyKey(encType: EncryptionType, key: SymmetricCryptoKey): SymmetricCryptoKey { - if (encType === EncryptionType.AesCbc128_HmacSha256_B64 && - key.encType === EncryptionType.AesCbc256_B64) { - // Old encrypt-then-mac scheme, make a new key - if (this.legacyEtmKey == null) { - this.legacyEtmKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64); - } - return this.legacyEtmKey; - } - - return key; - } - - private async stretchKey(key: SymmetricCryptoKey): Promise { - const newKey = new Uint8Array(64); - const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, 'enc', 32, 'sha256'); - const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, 'mac', 32, 'sha256'); - newKey.set(new Uint8Array(encKey)); - newKey.set(new Uint8Array(macKey), 32); - return new SymmetricCryptoKey(newKey.buffer); - } - - private async hashPhrase(hash: ArrayBuffer, minimumEntropy: number = 64) { - const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2); - let numWords = Math.ceil(minimumEntropy / entropyPerWord); - - const hashArr = Array.from(new Uint8Array(hash)); - const entropyAvailable = hashArr.length * 4; - if (numWords * entropyPerWord > entropyAvailable) { - throw new Error('Output entropy of hash function is too small'); - } - - const phrase: string[] = []; - let hashNumber = bigInt.fromArray(hashArr, 256); - while (numWords--) { - const remainder = hashNumber.mod(EEFLongWordList.length); - hashNumber = hashNumber.divide(EEFLongWordList.length); - phrase.push(EEFLongWordList[remainder as any]); - } - return phrase; - } - - private async buildEncKey(key: SymmetricCryptoKey, encKey: ArrayBuffer) - : Promise<[SymmetricCryptoKey, EncString]> { - let encKeyEnc: EncString = null; - if (key.key.byteLength === 32) { - const newKey = await this.stretchKey(key); - encKeyEnc = await this.encrypt(encKey, newKey); - } else if (key.key.byteLength === 64) { - encKeyEnc = await this.encrypt(encKey, key); - } else { - throw new Error('Invalid key size.'); - } - return [new SymmetricCryptoKey(encKey), encKeyEnc]; - } + private async clearSecretKeyStore(userId?: string): Promise { + await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); + await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId }); + } } diff --git a/common/src/services/environment.service.ts b/common/src/services/environment.service.ts index 8d10995a..d95718e0 100644 --- a/common/src/services/environment.service.ts +++ b/common/src/services/environment.service.ts @@ -1,202 +1,202 @@ -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject } from "rxjs"; -import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { EnvironmentUrls } from "../models/domain/environmentUrls"; -import { ConstantsService } from './constants.service'; - -import { EnvironmentService as EnvironmentServiceAbstraction, Urls } from '../abstractions/environment.service'; -import { StorageService } from '../abstractions/storage.service'; +import { + EnvironmentService as EnvironmentServiceAbstraction, + Urls, +} from "../abstractions/environment.service"; +import { StateService } from "../abstractions/state.service"; export class EnvironmentService implements EnvironmentServiceAbstraction { + private readonly urlsSubject = new Subject(); + urls: Observable = this.urlsSubject; // tslint:disable-line - private readonly urlsSubject = new Subject(); - urls: Observable = this.urlsSubject; // tslint:disable-line + private baseUrl: string; + private webVaultUrl: string; + private apiUrl: string; + private identityUrl: string; + private iconsUrl: string; + private notificationsUrl: string; + private eventsUrl: string; + private keyConnectorUrl: string; - private baseUrl: string; - private webVaultUrl: string; - private apiUrl: string; - private identityUrl: string; - private iconsUrl: string; - private notificationsUrl: string; - private eventsUrl: string; - private keyConnectorUrl: string; + constructor(private stateService: StateService) {} - constructor(private storageService: StorageService) {} + hasBaseUrl() { + return this.baseUrl != null; + } - hasBaseUrl() { - return this.baseUrl != null; + getNotificationsUrl() { + if (this.notificationsUrl != null) { + return this.notificationsUrl; } - getNotificationsUrl() { - if (this.notificationsUrl != null) { - return this.notificationsUrl; - } - - if (this.baseUrl != null) { - return this.baseUrl + '/notifications'; - } - - return 'https://notifications.bitwarden.com'; + if (this.baseUrl != null) { + return this.baseUrl + "/notifications"; } - getWebVaultUrl() { - if (this.webVaultUrl != null) { - return this.webVaultUrl; - } + return "https://notifications.bitwarden.com"; + } - if (this.baseUrl) { - return this.baseUrl; - } - return 'https://vault.bitwarden.com'; + getWebVaultUrl() { + if (this.webVaultUrl != null) { + return this.webVaultUrl; } - getSendUrl() { - return this.getWebVaultUrl() === 'https://vault.bitwarden.com' - ? 'https://send.bitwarden.com/#' - : this.getWebVaultUrl() + '/#/send/'; + if (this.baseUrl) { + return this.baseUrl; + } + return "https://vault.bitwarden.com"; + } + + getSendUrl() { + return this.getWebVaultUrl() === "https://vault.bitwarden.com" + ? "https://send.bitwarden.com/#" + : this.getWebVaultUrl() + "/#/send/"; + } + + getIconsUrl() { + if (this.iconsUrl != null) { + return this.iconsUrl; } - getIconsUrl() { - if (this.iconsUrl != null) { - return this.iconsUrl; - } - - if (this.baseUrl) { - return this.baseUrl + '/icons'; - } - - return 'https://icons.bitwarden.net'; + if (this.baseUrl) { + return this.baseUrl + "/icons"; } - getApiUrl() { - if (this.apiUrl != null) { - return this.apiUrl; - } + return "https://icons.bitwarden.net"; + } - if (this.baseUrl) { - return this.baseUrl + '/api'; - } - - return 'https://api.bitwarden.com'; + getApiUrl() { + if (this.apiUrl != null) { + return this.apiUrl; } - getIdentityUrl() { - if (this.identityUrl != null) { - return this.identityUrl; - } - - if (this.baseUrl) { - return this.baseUrl + '/identity'; - } - - return 'https://identity.bitwarden.com'; + if (this.baseUrl) { + return this.baseUrl + "/api"; } - getEventsUrl() { - if (this.eventsUrl != null) { - return this.eventsUrl; - } + return "https://api.bitwarden.com"; + } - if (this.baseUrl) { - return this.baseUrl + '/events'; - } - - return 'https://events.bitwarden.com'; + getIdentityUrl() { + if (this.identityUrl != null) { + return this.identityUrl; } - getKeyConnectorUrl() { - return this.keyConnectorUrl; + if (this.baseUrl) { + return this.baseUrl + "/identity"; } - async setUrlsFromStorage(): Promise { - const urlsObj: any = await this.storageService.get(ConstantsService.environmentUrlsKey); - const urls = urlsObj || { - base: null, - api: null, - identity: null, - icons: null, - notifications: null, - events: null, - webVault: null, - keyConnector: null, - }; + return "https://identity.bitwarden.com"; + } - const envUrls = new EnvironmentUrls(); - - if (urls.base) { - this.baseUrl = envUrls.base = urls.base; - return; - } - - this.webVaultUrl = urls.webVault; - this.apiUrl = envUrls.api = urls.api; - this.identityUrl = envUrls.identity = urls.identity; - this.iconsUrl = urls.icons; - this.notificationsUrl = urls.notifications; - this.eventsUrl = envUrls.events = urls.events; - this.keyConnectorUrl = urls.keyConnector; + getEventsUrl() { + if (this.eventsUrl != null) { + return this.eventsUrl; } - async setUrls(urls: Urls, saveSettings: boolean = true): Promise { - urls.base = this.formatUrl(urls.base); - urls.webVault = this.formatUrl(urls.webVault); - urls.api = this.formatUrl(urls.api); - urls.identity = this.formatUrl(urls.identity); - urls.icons = this.formatUrl(urls.icons); - urls.notifications = this.formatUrl(urls.notifications); - urls.events = this.formatUrl(urls.events); - urls.keyConnector = this.formatUrl(urls.keyConnector); - - if (saveSettings) { - await this.storageService.save(ConstantsService.environmentUrlsKey, { - base: urls.base, - api: urls.api, - identity: urls.identity, - webVault: urls.webVault, - icons: urls.icons, - notifications: urls.notifications, - events: urls.events, - keyConnector: urls.keyConnector, - }); - } - - this.baseUrl = urls.base; - this.webVaultUrl = urls.webVault; - this.apiUrl = urls.api; - this.identityUrl = urls.identity; - this.iconsUrl = urls.icons; - this.notificationsUrl = urls.notifications; - this.eventsUrl = urls.events; - this.keyConnectorUrl = urls.keyConnector; - - this.urlsSubject.next(urls); - - return urls; + if (this.baseUrl) { + return this.baseUrl + "/events"; } - getUrls() { - return { - base: this.baseUrl, - webVault: this.webVaultUrl, - api: this.apiUrl, - identity: this.identityUrl, - icons: this.iconsUrl, - notifications: this.notificationsUrl, - events: this.eventsUrl, - keyConnector: this.keyConnectorUrl, - }; + return "https://events.bitwarden.com"; + } + + getKeyConnectorUrl() { + return this.keyConnectorUrl; + } + + async setUrlsFromStorage(): Promise { + const urlsObj: any = await this.stateService.getEnvironmentUrls(); + const urls = urlsObj || { + base: null, + api: null, + identity: null, + icons: null, + notifications: null, + events: null, + webVault: null, + keyConnector: null, + }; + + const envUrls = new EnvironmentUrls(); + + if (urls.base) { + this.baseUrl = envUrls.base = urls.base; + return; } - private formatUrl(url: string): string { - if (url == null || url === '') { - return null; - } + this.webVaultUrl = urls.webVault; + this.apiUrl = envUrls.api = urls.api; + this.identityUrl = envUrls.identity = urls.identity; + this.iconsUrl = urls.icons; + this.notificationsUrl = urls.notifications; + this.eventsUrl = envUrls.events = urls.events; + this.keyConnectorUrl = urls.keyConnector; + } - url = url.replace(/\/+$/g, ''); - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = 'https://' + url; - } + async setUrls(urls: Urls, saveSettings: boolean = true): Promise { + urls.base = this.formatUrl(urls.base); + urls.webVault = this.formatUrl(urls.webVault); + urls.api = this.formatUrl(urls.api); + urls.identity = this.formatUrl(urls.identity); + urls.icons = this.formatUrl(urls.icons); + urls.notifications = this.formatUrl(urls.notifications); + urls.events = this.formatUrl(urls.events); + urls.keyConnector = this.formatUrl(urls.keyConnector); - return url.trim(); + if (saveSettings) { + await this.stateService.setEnvironmentUrls({ + base: urls.base, + api: urls.api, + identity: urls.identity, + webVault: urls.webVault, + icons: urls.icons, + notifications: urls.notifications, + events: urls.events, + keyConnector: urls.keyConnector, + }); } + + this.baseUrl = urls.base; + this.webVaultUrl = urls.webVault; + this.apiUrl = urls.api; + this.identityUrl = urls.identity; + this.iconsUrl = urls.icons; + this.notificationsUrl = urls.notifications; + this.eventsUrl = urls.events; + this.keyConnectorUrl = urls.keyConnector; + + this.urlsSubject.next(urls); + + return urls; + } + + getUrls() { + return { + base: this.baseUrl, + webVault: this.webVaultUrl, + api: this.apiUrl, + identity: this.identityUrl, + icons: this.iconsUrl, + notifications: this.notificationsUrl, + events: this.eventsUrl, + keyConnector: this.keyConnectorUrl, + }; + } + + private formatUrl(url: string): string { + if (url == null || url === "") { + return null; + } + + url = url.replace(/\/+$/g, ""); + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; + } + + return url.trim(); + } } diff --git a/common/src/services/event.service.ts b/common/src/services/event.service.ts index 452133fa..ca5bb59f 100644 --- a/common/src/services/event.service.ts +++ b/common/src/services/event.service.ts @@ -1,96 +1,102 @@ -import { EventType } from '../enums/eventType'; +import { EventType } from "../enums/eventType"; -import { EventData } from '../models/data/eventData'; +import { EventData } from "../models/data/eventData"; -import { EventRequest } from '../models/request/eventRequest'; +import { EventRequest } from "../models/request/eventRequest"; -import { ApiService } from '../abstractions/api.service'; -import { CipherService } from '../abstractions/cipher.service'; -import { EventService as EventServiceAbstraction } from '../abstractions/event.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; - -import { LogService } from '../abstractions/log.service'; -import { ConstantsService } from './constants.service'; +import { ApiService } from "../abstractions/api.service"; +import { CipherService } from "../abstractions/cipher.service"; +import { EventService as EventServiceAbstraction } from "../abstractions/event.service"; +import { LogService } from "../abstractions/log.service"; +import { OrganizationService } from "../abstractions/organization.service"; +import { StateService } from "../abstractions/state.service"; export class EventService implements EventServiceAbstraction { - private inited = false; + private inited = false; - constructor(private storageService: StorageService, private apiService: ApiService, - private userService: UserService, private cipherService: CipherService, - private logService: LogService) { } + constructor( + private apiService: ApiService, + private cipherService: CipherService, + private stateService: StateService, + private logService: LogService, + private organizationService: OrganizationService + ) {} - init(checkOnInterval: boolean) { - if (this.inited) { - return; - } - - this.inited = true; - if (checkOnInterval) { - this.uploadEvents(); - setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds - } + init(checkOnInterval: boolean) { + if (this.inited) { + return; } - async collect(eventType: EventType, cipherId: string = null, uploadImmediately = false): Promise { - const authed = await this.userService.isAuthenticated(); - if (!authed) { - return; - } - const organizations = await this.userService.getAllOrganizations(); - if (organizations == null) { - return; - } - const orgIds = new Set(organizations.filter(o => o.useEvents).map(o => o.id)); - if (orgIds.size === 0) { - return; - } - if (cipherId != null) { - const cipher = await this.cipherService.get(cipherId); - if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) { - return; - } - } - let eventCollection = await this.storageService.get(ConstantsService.eventCollectionKey); - if (eventCollection == null) { - eventCollection = []; - } - const event = new EventData(); - event.type = eventType; - event.cipherId = cipherId; - event.date = new Date().toISOString(); - eventCollection.push(event); - await this.storageService.save(ConstantsService.eventCollectionKey, eventCollection); - if (uploadImmediately) { - await this.uploadEvents(); - } + this.inited = true; + if (checkOnInterval) { + this.uploadEvents(); + setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds } + } - async uploadEvents(): Promise { - const authed = await this.userService.isAuthenticated(); - if (!authed) { - return; - } - const eventCollection = await this.storageService.get(ConstantsService.eventCollectionKey); - if (eventCollection == null || eventCollection.length === 0) { - return; - } - const request = eventCollection.map(e => { - const req = new EventRequest(); - req.type = e.type; - req.cipherId = e.cipherId; - req.date = e.date; - return req; - }); - try { - await this.apiService.postEventsCollect(request); - this.clearEvents(); - } catch (e) { - this.logService.error(e); - } + async collect( + eventType: EventType, + cipherId: string = null, + uploadImmediately = false + ): Promise { + const authed = await this.stateService.getIsAuthenticated(); + if (!authed) { + return; } + const organizations = await this.organizationService.getAll(); + if (organizations == null) { + return; + } + const orgIds = new Set(organizations.filter((o) => o.useEvents).map((o) => o.id)); + if (orgIds.size === 0) { + return; + } + if (cipherId != null) { + const cipher = await this.cipherService.get(cipherId); + if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) { + return; + } + } + let eventCollection = await this.stateService.getEventCollection(); + if (eventCollection == null) { + eventCollection = []; + } + const event = new EventData(); + event.type = eventType; + event.cipherId = cipherId; + event.date = new Date().toISOString(); + eventCollection.push(event); + await this.stateService.setEventCollection(eventCollection); + if (uploadImmediately) { + await this.uploadEvents(); + } + } - async clearEvents(): Promise { - await this.storageService.remove(ConstantsService.eventCollectionKey); + async uploadEvents(userId?: string): Promise { + const authed = await this.stateService.getIsAuthenticated({ userId: userId }); + if (!authed) { + return; } + const eventCollection = await this.stateService.getEventCollection({ userId: userId }); + if (eventCollection == null || eventCollection.length === 0) { + return; + } + const request = eventCollection.map((e) => { + const req = new EventRequest(); + req.type = e.type; + req.cipherId = e.cipherId; + req.date = e.date; + return req; + }); + try { + await this.apiService.postEventsCollect(request); + this.clearEvents(userId); + } catch (e) { + this.logService.error(e); + } + } + + async clearEvents(userId?: string): Promise { + await this.stateService.setEventCollection(null, { userId: userId }); + } } diff --git a/common/src/services/folder.service.ts b/common/src/services/folder.service.ts index 0f3ae50e..225c03aa 100644 --- a/common/src/services/folder.service.ts +++ b/common/src/services/folder.service.ts @@ -1,209 +1,199 @@ -import { FolderData } from '../models/data/folderData'; +import { FolderData } from "../models/data/folderData"; -import { Folder } from '../models/domain/folder'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { TreeNode } from '../models/domain/treeNode'; +import { Folder } from "../models/domain/folder"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; +import { TreeNode } from "../models/domain/treeNode"; -import { FolderRequest } from '../models/request/folderRequest'; +import { FolderRequest } from "../models/request/folderRequest"; -import { FolderResponse } from '../models/response/folderResponse'; +import { FolderResponse } from "../models/response/folderResponse"; -import { FolderView } from '../models/view/folderView'; +import { FolderView } from "../models/view/folderView"; -import { ApiService } from '../abstractions/api.service'; -import { CipherService } from '../abstractions/cipher.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; -import { CipherData } from '../models/data/cipherData'; +import { ApiService } from "../abstractions/api.service"; +import { CipherService } from "../abstractions/cipher.service"; +import { CryptoService } from "../abstractions/crypto.service"; +import { FolderService as FolderServiceAbstraction } from "../abstractions/folder.service"; +import { I18nService } from "../abstractions/i18n.service"; +import { StateService } from "../abstractions/state.service"; -import { ServiceUtils } from '../misc/serviceUtils'; -import { Utils } from '../misc/utils'; +import { CipherData } from "../models/data/cipherData"; -const Keys = { - foldersPrefix: 'folders_', - ciphersPrefix: 'ciphers_', -}; -const NestingDelimiter = '/'; +import { ServiceUtils } from "../misc/serviceUtils"; +import { Utils } from "../misc/utils"; + +const NestingDelimiter = "/"; export class FolderService implements FolderServiceAbstraction { - decryptedFolderCache: FolderView[]; + constructor( + private cryptoService: CryptoService, + private apiService: ApiService, + private i18nService: I18nService, + private cipherService: CipherService, + private stateService: StateService + ) {} - constructor(private cryptoService: CryptoService, private userService: UserService, - private apiService: ApiService, private storageService: StorageService, - private i18nService: I18nService, private cipherService: CipherService) { } + async clearCache(userId?: string): Promise { + await this.stateService.setDecryptedFolders(null, { userId: userId }); + } - clearCache(): void { - this.decryptedFolderCache = null; + async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise { + const folder = new Folder(); + folder.id = model.id; + folder.name = await this.cryptoService.encrypt(model.name, key); + return folder; + } + + async get(id: string): Promise { + const folders = await this.stateService.getEncryptedFolders(); + if (folders == null || !folders.hasOwnProperty(id)) { + return null; } - async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise { - const folder = new Folder(); - folder.id = model.id; - folder.name = await this.cryptoService.encrypt(model.name, key); - return folder; + return new Folder(folders[id]); + } + + async getAll(): Promise { + const folders = await this.stateService.getEncryptedFolders(); + const response: Folder[] = []; + for (const id in folders) { + if (folders.hasOwnProperty(id)) { + response.push(new Folder(folders[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + const decryptedFolders = await this.stateService.getDecryptedFolders(); + if (decryptedFolders != null) { + return decryptedFolders; } - async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const folders = await this.storageService.get<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); - if (folders == null || !folders.hasOwnProperty(id)) { - return null; + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { + throw new Error("No key."); + } + + const decFolders: FolderView[] = []; + const promises: Promise[] = []; + const folders = await this.getAll(); + folders.forEach((folder) => { + promises.push(folder.decrypt().then((f) => decFolders.push(f))); + }); + + await Promise.all(promises); + decFolders.sort(Utils.getSortFunction(this.i18nService, "name")); + + const noneFolder = new FolderView(); + noneFolder.name = this.i18nService.t("noneFolder"); + decFolders.push(noneFolder); + + await this.stateService.setDecryptedFolders(decFolders); + return decFolders; + } + + async getAllNested(): Promise[]> { + const folders = await this.getAllDecrypted(); + const nodes: TreeNode[] = []; + folders.forEach((f) => { + const folderCopy = new FolderView(); + folderCopy.id = f.id; + folderCopy.revisionDate = f.revisionDate; + const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; + ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter); + }); + return nodes; + } + + async getNested(id: string): Promise> { + const folders = await this.getAllNested(); + return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode; + } + + async saveWithServer(folder: Folder): Promise { + const request = new FolderRequest(folder); + + let response: FolderResponse; + if (folder.id == null) { + response = await this.apiService.postFolder(request); + folder.id = response.id; + } else { + response = await this.apiService.putFolder(folder.id, request); + } + + const userId = await this.stateService.getUserId(); + const data = new FolderData(response, userId); + await this.upsert(data); + } + + async upsert(folder: FolderData | FolderData[]): Promise { + let folders = await this.stateService.getEncryptedFolders(); + if (folders == null) { + folders = {}; + } + + if (folder instanceof FolderData) { + const f = folder as FolderData; + folders[f.id] = f; + } else { + (folder as FolderData[]).forEach((f) => { + folders[f.id] = f; + }); + } + + await this.stateService.setDecryptedFolders(null); + await this.stateService.setEncryptedFolders(folders); + } + + async replace(folders: { [id: string]: FolderData }): Promise { + await this.stateService.setDecryptedFolders(null); + await this.stateService.setEncryptedFolders(folders); + } + + async clear(userId?: string): Promise { + await this.stateService.setDecryptedFolders(null, { userId: userId }); + await this.stateService.setEncryptedFolders(null, { userId: userId }); + } + + async delete(id: string | string[]): Promise { + const folders = await this.stateService.getEncryptedFolders(); + if (folders == null) { + return; + } + + if (typeof id === "string") { + if (folders[id] == null) { + return; + } + delete folders[id]; + } else { + (id as string[]).forEach((i) => { + delete folders[i]; + }); + } + + await this.stateService.setDecryptedFolders(null); + await this.stateService.setEncryptedFolders(folders); + + // Items in a deleted folder are re-assigned to "No Folder" + const ciphers = await this.stateService.getEncryptedCiphers(); + if (ciphers != null) { + const updates: CipherData[] = []; + for (const cId in ciphers) { + if (ciphers[cId].folderId === id) { + ciphers[cId].folderId = null; + updates.push(ciphers[cId]); } - - return new Folder(folders[id]); + } + if (updates.length > 0) { + this.cipherService.upsert(updates); + } } + } - async getAll(): Promise { - const userId = await this.userService.getUserId(); - const folders = await this.storageService.get<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); - const response: Folder[] = []; - for (const id in folders) { - if (folders.hasOwnProperty(id)) { - response.push(new Folder(folders[id])); - } - } - return response; - } - - async getAllDecrypted(): Promise { - if (this.decryptedFolderCache != null) { - return this.decryptedFolderCache; - } - - const hasKey = await this.cryptoService.hasKey(); - if (!hasKey) { - throw new Error('No key.'); - } - - const decFolders: FolderView[] = []; - const promises: Promise[] = []; - const folders = await this.getAll(); - folders.forEach(folder => { - promises.push(folder.decrypt().then(f => decFolders.push(f))); - }); - - await Promise.all(promises); - decFolders.sort(Utils.getSortFunction(this.i18nService, 'name')); - - const noneFolder = new FolderView(); - noneFolder.name = this.i18nService.t('noneFolder'); - decFolders.push(noneFolder); - - this.decryptedFolderCache = decFolders; - return this.decryptedFolderCache; - } - - async getAllNested(): Promise[]> { - const folders = await this.getAllDecrypted(); - const nodes: TreeNode[] = []; - folders.forEach(f => { - const folderCopy = new FolderView(); - folderCopy.id = f.id; - folderCopy.revisionDate = f.revisionDate; - const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter) : []; - ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter); - }); - return nodes; - } - - async getNested(id: string): Promise> { - const folders = await this.getAllNested(); - return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode; - } - - async saveWithServer(folder: Folder): Promise { - const request = new FolderRequest(folder); - - let response: FolderResponse; - if (folder.id == null) { - response = await this.apiService.postFolder(request); - folder.id = response.id; - } else { - response = await this.apiService.putFolder(folder.id, request); - } - - const userId = await this.userService.getUserId(); - const data = new FolderData(response, userId); - await this.upsert(data); - } - - async upsert(folder: FolderData | FolderData[]): Promise { - const userId = await this.userService.getUserId(); - let folders = await this.storageService.get<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); - if (folders == null) { - folders = {}; - } - - if (folder instanceof FolderData) { - const f = folder as FolderData; - folders[f.id] = f; - } else { - (folder as FolderData[]).forEach(f => { - folders[f.id] = f; - }); - } - - await this.storageService.save(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; - } - - async replace(folders: { [id: string]: FolderData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; - } - - async clear(userId: string): Promise { - await this.storageService.remove(Keys.foldersPrefix + userId); - this.decryptedFolderCache = null; - } - - async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const folders = await this.storageService.get<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); - if (folders == null) { - return; - } - - if (typeof id === 'string') { - if (folders[id] == null) { - return; - } - delete folders[id]; - } else { - (id as string[]).forEach(i => { - delete folders[i]; - }); - } - - await this.storageService.save(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; - - // Items in a deleted folder are re-assigned to "No Folder" - const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>(Keys.ciphersPrefix + userId); - if (ciphers != null) { - const updates: CipherData[] = []; - for (const cId in ciphers) { - if (ciphers[cId].folderId === id) { - ciphers[cId].folderId = null; - updates.push(ciphers[cId]); - } - } - if (updates.length > 0) { - this.cipherService.upsert(updates); - } - } - } - - async deleteWithServer(id: string): Promise { - await this.apiService.deleteFolder(id); - await this.delete(id); - } + async deleteWithServer(id: string): Promise { + await this.apiService.deleteFolder(id); + await this.delete(id); + } } diff --git a/common/src/services/keyConnector.service.ts b/common/src/services/keyConnector.service.ts index d597469f..a258a0e3 100644 --- a/common/src/services/keyConnector.service.ts +++ b/common/src/services/keyConnector.service.ts @@ -1,96 +1,98 @@ -import { ApiService } from '../abstractions/api.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { KeyConnectorService as KeyConnectorServiceAbstraction } from '../abstractions/keyConnector.service'; -import { LogService } from '../abstractions/log.service'; -import { StorageService } from '../abstractions/storage.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; +import { ApiService } from "../abstractions/api.service"; +import { CryptoService } from "../abstractions/crypto.service"; +import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service"; +import { LogService } from "../abstractions/log.service"; +import { OrganizationService } from "../abstractions/organization.service"; +import { StateService } from "../abstractions/state.service"; +import { TokenService } from "../abstractions/token.service"; -import { OrganizationUserType } from '../enums/organizationUserType'; +import { OrganizationUserType } from "../enums/organizationUserType"; -import { Utils } from '../misc/utils'; +import { Utils } from "../misc/utils"; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; - -const Keys = { - usesKeyConnector: 'usesKeyConnector', - convertAccountToKeyConnector: 'convertAccountToKeyConnector', -}; +import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest"; export class KeyConnectorService implements KeyConnectorServiceAbstraction { - private usesKeyConnector?: boolean = null; + constructor( + private stateService: StateService, + private cryptoService: CryptoService, + private apiService: ApiService, + private tokenService: TokenService, + private logService: LogService, + private organizationService: OrganizationService + ) {} - constructor(private storageService: StorageService, private userService: UserService, - private cryptoService: CryptoService, private apiService: ApiService, - private tokenService: TokenService, private logService: LogService) { } + setUsesKeyConnector(usesKeyConnector: boolean) { + return this.stateService.setUsesKeyConnector(usesKeyConnector); + } - setUsesKeyConnector(usesKeyConnector: boolean) { - this.usesKeyConnector = usesKeyConnector; - return this.storageService.save(Keys.usesKeyConnector, usesKeyConnector); + async getUsesKeyConnector(): Promise { + return await this.stateService.getUsesKeyConnector(); + } + + async userNeedsMigration() { + const loggedInUsingSso = this.tokenService.getIsExternal(); + const requiredByOrganization = (await this.getManagingOrganization()) != null; + const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector()); + + return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector; + } + + async migrateUser() { + const organization = await this.getManagingOrganization(); + const key = await this.cryptoService.getKey(); + + try { + const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64); + await this.apiService.postUserKeyToKeyConnector( + organization.keyConnectorUrl, + keyConnectorRequest + ); + } catch (e) { + throw new Error("Unable to reach key connector"); } - async getUsesKeyConnector(): Promise { - return this.usesKeyConnector ??= await this.storageService.get(Keys.usesKeyConnector); + await this.apiService.postConvertToKeyConnector(); + } + + async getAndSetKey(url: string) { + try { + const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url); + const keyArr = Utils.fromB64ToArray(userKeyResponse.key); + const k = new SymmetricCryptoKey(keyArr); + await this.cryptoService.setKey(k); + } catch (e) { + this.logService.error(e); + throw new Error("Unable to reach key connector"); } + } - async userNeedsMigration() { - const loggedInUsingSso = this.tokenService.getIsExternal(); - const requiredByOrganization = await this.getManagingOrganization() != null; - const userIsNotUsingKeyConnector = !await this.getUsesKeyConnector(); + async getManagingOrganization() { + const orgs = await this.organizationService.getAll(); + return orgs.find( + (o) => + o.keyConnectorEnabled && + o.type !== OrganizationUserType.Admin && + o.type !== OrganizationUserType.Owner && + !o.isProviderUser + ); + } - return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector; - } + async setConvertAccountRequired(status: boolean) { + await this.stateService.setConvertAccountToKeyConnector(status); + } - async migrateUser() { - const organization = await this.getManagingOrganization(); - const key = await this.cryptoService.getKey(); + async getConvertAccountRequired(): Promise { + return await this.stateService.getConvertAccountToKeyConnector(); + } - try { - const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64); - await this.apiService.postUserKeyToKeyConnector(organization.keyConnectorUrl, keyConnectorRequest); - } catch (e) { - throw new Error('Unable to reach key connector'); - } + async removeConvertAccountRequired() { + await this.stateService.setConvertAccountToKeyConnector(null); + } - await this.apiService.postConvertToKeyConnector(); - } - - async getAndSetKey(url: string) { - try { - const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url); - const keyArr = Utils.fromB64ToArray(userKeyResponse.key); - const k = new SymmetricCryptoKey(keyArr); - await this.cryptoService.setKey(k); - } catch (e) { - this.logService.error(e); - throw new Error('Unable to reach key connector'); - } - } - - async getManagingOrganization() { - const orgs = await this.userService.getAllOrganizations(); - return orgs.find(o => - o.keyConnectorEnabled && - o.type !== OrganizationUserType.Admin && - o.type !== OrganizationUserType.Owner && - !o.isProviderUser); - } - - async setConvertAccountRequired(status: boolean) { - await this.storageService.save(Keys.convertAccountToKeyConnector, status); - } - - async getConvertAccountRequired(): Promise { - return await this.storageService.get(Keys.convertAccountToKeyConnector); - } - - async removeConvertAccountRequired() { - await this.storageService.remove(Keys.convertAccountToKeyConnector); - } - - async clear() { - await this.removeConvertAccountRequired(); - } + async clear() { + await this.removeConvertAccountRequired(); + } } diff --git a/common/src/services/notifications.service.ts b/common/src/services/notifications.service.ts index b1d5723b..4974b282 100644 --- a/common/src/services/notifications.service.ts +++ b/common/src/services/notifications.service.ts @@ -1,217 +1,231 @@ -import * as signalR from '@microsoft/signalr'; -import * as signalRMsgPack from '@microsoft/signalr-protocol-msgpack'; +import * as signalR from "@microsoft/signalr"; +import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack"; -import { NotificationType } from '../enums/notificationType'; +import { NotificationType } from "../enums/notificationType"; -import { ApiService } from '../abstractions/api.service'; -import { AppIdService } from '../abstractions/appId.service'; -import { EnvironmentService } from '../abstractions/environment.service'; -import { LogService } from '../abstractions/log.service'; -import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; -import { SyncService } from '../abstractions/sync.service'; -import { UserService } from '../abstractions/user.service'; -import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; +import { ApiService } from "../abstractions/api.service"; +import { AppIdService } from "../abstractions/appId.service"; +import { EnvironmentService } from "../abstractions/environment.service"; +import { LogService } from "../abstractions/log.service"; +import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service"; +import { StateService } from "../abstractions/state.service"; +import { SyncService } from "../abstractions/sync.service"; +import { VaultTimeoutService } from "../abstractions/vaultTimeout.service"; import { - NotificationResponse, - SyncCipherNotification, - SyncFolderNotification, - SyncSendNotification, -} from '../models/response/notificationResponse'; + NotificationResponse, + SyncCipherNotification, + SyncFolderNotification, + SyncSendNotification, +} from "../models/response/notificationResponse"; export class NotificationsService implements NotificationsServiceAbstraction { - private signalrConnection: signalR.HubConnection; - private url: string; - private connected = false; - private inited = false; - private inactive = false; - private reconnectTimer: any = null; + private signalrConnection: signalR.HubConnection; + private url: string; + private connected = false; + private inited = false; + private inactive = false; + private reconnectTimer: any = null; - constructor(private userService: UserService, private syncService: SyncService, - private appIdService: AppIdService, private apiService: ApiService, - private vaultTimeoutService: VaultTimeoutService, private environmentService: EnvironmentService, - private logoutCallback: () => Promise, private logService: LogService) { - this.environmentService.urls.subscribe(() => { - if (!this.inited) { - return; - } + constructor( + private syncService: SyncService, + private appIdService: AppIdService, + private apiService: ApiService, + private vaultTimeoutService: VaultTimeoutService, + private environmentService: EnvironmentService, + private logoutCallback: () => Promise, + private logService: LogService, + private stateService: StateService + ) { + this.environmentService.urls.subscribe(() => { + if (!this.inited) { + return; + } - this.init(); - }); + this.init(); + }); + } + + async init(): Promise { + this.inited = false; + this.url = this.environmentService.getNotificationsUrl(); + + // Set notifications server URL to `https://-` to effectively disable communication + // with the notifications server from the client app + if (this.url === "https://-") { + return; } - async init(): Promise { - this.inited = false; - this.url = this.environmentService.getNotificationsUrl(); - - // Set notifications server URL to `https://-` to effectively disable communication - // with the notifications server from the client app - if (this.url === 'https://-') { - return; - } - - if (this.signalrConnection != null) { - this.signalrConnection.off('ReceiveMessage'); - this.signalrConnection.off('Heartbeat'); - await this.signalrConnection.stop(); - this.connected = false; - this.signalrConnection = null; - } - - this.signalrConnection = new signalR.HubConnectionBuilder() - .withUrl(this.url + '/hub', { - accessTokenFactory: () => this.apiService.getActiveBearerToken(), - skipNegotiation: true, - transport: signalR.HttpTransportType.WebSockets, - }) - .withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol) - // .configureLogging(signalR.LogLevel.Trace) - .build(); - - this.signalrConnection.on('ReceiveMessage', - (data: any) => this.processNotification(new NotificationResponse(data))); - this.signalrConnection.on('Heartbeat', - (data: any) => { /*console.log('Heartbeat!');*/ }); - this.signalrConnection.onclose(() => { - this.connected = false; - this.reconnect(true); - }); - this.inited = true; - if (await this.isAuthedAndUnlocked()) { - await this.reconnect(false); - } + if (this.signalrConnection != null) { + this.signalrConnection.off("ReceiveMessage"); + this.signalrConnection.off("Heartbeat"); + await this.signalrConnection.stop(); + this.connected = false; + this.signalrConnection = null; } - async updateConnection(sync = false): Promise { - if (!this.inited) { - return; - } - try { - if (await this.isAuthedAndUnlocked()) { - await this.reconnect(sync); - } else { - await this.signalrConnection.stop(); - } - } catch (e) { - this.logService.error(e.toString()); - } + this.signalrConnection = new signalR.HubConnectionBuilder() + .withUrl(this.url + "/hub", { + accessTokenFactory: () => this.apiService.getActiveBearerToken(), + skipNegotiation: true, + transport: signalR.HttpTransportType.WebSockets, + }) + .withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol) + // .configureLogging(signalR.LogLevel.Trace) + .build(); + + this.signalrConnection.on("ReceiveMessage", (data: any) => + this.processNotification(new NotificationResponse(data)) + ); + this.signalrConnection.on("Heartbeat", (data: any) => { + /*console.log('Heartbeat!');*/ + }); + this.signalrConnection.onclose(() => { + this.connected = false; + this.reconnect(true); + }); + this.inited = true; + if (await this.isAuthedAndUnlocked()) { + await this.reconnect(false); + } + } + + async updateConnection(sync = false): Promise { + if (!this.inited) { + return; + } + try { + if (await this.isAuthedAndUnlocked()) { + await this.reconnect(sync); + } else { + await this.signalrConnection.stop(); + } + } catch (e) { + this.logService.error(e.toString()); + } + } + + async reconnectFromActivity(): Promise { + this.inactive = false; + if (this.inited && !this.connected) { + await this.reconnect(true); + } + } + + async disconnectFromInactivity(): Promise { + this.inactive = true; + if (this.inited && this.connected) { + await this.signalrConnection.stop(); + } + } + + private async processNotification(notification: NotificationResponse) { + const appId = await this.appIdService.getAppId(); + if (notification == null || notification.contextId === appId) { + return; } - async reconnectFromActivity(): Promise { - this.inactive = false; - if (this.inited && !this.connected) { - await this.reconnect(true); - } + const isAuthenticated = await this.stateService.getIsAuthenticated(); + const payloadUserId = notification.payload.userId || notification.payload.UserId; + const myUserId = await this.stateService.getUserId(); + if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) { + return; } - async disconnectFromInactivity(): Promise { - this.inactive = true; - if (this.inited && this.connected) { - await this.signalrConnection.stop(); + switch (notification.type) { + case NotificationType.SyncCipherCreate: + case NotificationType.SyncCipherUpdate: + await this.syncService.syncUpsertCipher( + notification.payload as SyncCipherNotification, + notification.type === NotificationType.SyncCipherUpdate + ); + break; + case NotificationType.SyncCipherDelete: + case NotificationType.SyncLoginDelete: + await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); + break; + case NotificationType.SyncFolderCreate: + case NotificationType.SyncFolderUpdate: + await this.syncService.syncUpsertFolder( + notification.payload as SyncFolderNotification, + notification.type === NotificationType.SyncFolderUpdate + ); + break; + case NotificationType.SyncFolderDelete: + await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification); + break; + case NotificationType.SyncVault: + case NotificationType.SyncCiphers: + case NotificationType.SyncSettings: + if (isAuthenticated) { + await this.syncService.fullSync(false); } + break; + case NotificationType.SyncOrgKeys: + if (isAuthenticated) { + await this.syncService.fullSync(true); + // Stop so a reconnect can be made + await this.signalrConnection.stop(); + } + break; + case NotificationType.LogOut: + if (isAuthenticated) { + this.logoutCallback(); + } + break; + case NotificationType.SyncSendCreate: + case NotificationType.SyncSendUpdate: + await this.syncService.syncUpsertSend( + notification.payload as SyncSendNotification, + notification.type === NotificationType.SyncSendUpdate + ); + break; + case NotificationType.SyncSendDelete: + await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); + default: + break; + } + } + + private async reconnect(sync: boolean) { + if (this.reconnectTimer != null) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + if (this.connected || !this.inited || this.inactive) { + return; + } + const authedAndUnlocked = await this.isAuthedAndUnlocked(); + if (!authedAndUnlocked) { + return; } - private async processNotification(notification: NotificationResponse) { - const appId = await this.appIdService.getAppId(); - if (notification == null || notification.contextId === appId) { - return; - } - - const isAuthenticated = await this.userService.isAuthenticated(); - const payloadUserId = notification.payload.userId || notification.payload.UserId; - const myUserId = await this.userService.getUserId(); - if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) { - return; - } - - switch (notification.type) { - case NotificationType.SyncCipherCreate: - case NotificationType.SyncCipherUpdate: - await this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification, - notification.type === NotificationType.SyncCipherUpdate); - break; - case NotificationType.SyncCipherDelete: - case NotificationType.SyncLoginDelete: - await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); - break; - case NotificationType.SyncFolderCreate: - case NotificationType.SyncFolderUpdate: - await this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification, - notification.type === NotificationType.SyncFolderUpdate); - break; - case NotificationType.SyncFolderDelete: - await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification); - break; - case NotificationType.SyncVault: - case NotificationType.SyncCiphers: - case NotificationType.SyncSettings: - if (isAuthenticated) { - await this.syncService.fullSync(false); - } - break; - case NotificationType.SyncOrgKeys: - if (isAuthenticated) { - await this.syncService.fullSync(true); - // Stop so a reconnect can be made - await this.signalrConnection.stop(); - } - break; - case NotificationType.LogOut: - if (isAuthenticated) { - this.logoutCallback(); - } - break; - case NotificationType.SyncSendCreate: - case NotificationType.SyncSendUpdate: - await this.syncService.syncUpsertSend(notification.payload as SyncSendNotification, - notification.type === NotificationType.SyncSendUpdate); - break; - case NotificationType.SyncSendDelete: - await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); - default: - break; - } + try { + await this.signalrConnection.start(); + this.connected = true; + if (sync) { + await this.syncService.fullSync(false); + } + } catch (e) { + this.logService.error(e); } - private async reconnect(sync: boolean) { - if (this.reconnectTimer != null) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - if (this.connected || !this.inited || this.inactive) { - return; - } - const authedAndUnlocked = await this.isAuthedAndUnlocked(); - if (!authedAndUnlocked) { - return; - } - - try { - await this.signalrConnection.start(); - this.connected = true; - if (sync) { - await this.syncService.fullSync(false); - } - } catch (e) { - this.logService.error(e); - } - - if (!this.connected) { - this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000)); - } + if (!this.connected) { + this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000)); } + } - private async isAuthedAndUnlocked() { - if (await this.userService.isAuthenticated()) { - const locked = await this.vaultTimeoutService.isLocked(); - return !locked; - } - return false; + private async isAuthedAndUnlocked() { + if (await this.stateService.getIsAuthenticated()) { + const locked = await this.vaultTimeoutService.isLocked(); + return !locked; } + return false; + } - private random(min: number, max: number) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; - } + private random(min: number, max: number) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; + } } diff --git a/common/src/services/organization.service.ts b/common/src/services/organization.service.ts new file mode 100644 index 00000000..10a86632 --- /dev/null +++ b/common/src/services/organization.service.ts @@ -0,0 +1,50 @@ +import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service"; +import { StateService } from "../abstractions/state.service"; + +import { OrganizationData } from "../models/data/organizationData"; + +import { Organization } from "../models/domain/organization"; + +export class OrganizationService implements OrganizationServiceAbstraction { + constructor(private stateService: StateService) {} + + async get(id: string): Promise { + const organizations = await this.stateService.getOrganizations(); + if (organizations == null || !organizations.hasOwnProperty(id)) { + return null; + } + + return new Organization(organizations[id]); + } + + async getByIdentifier(identifier: string): Promise { + const organizations = await this.getAll(); + if (organizations == null || organizations.length === 0) { + return null; + } + + return organizations.find((o) => o.identifier === identifier); + } + + async getAll(userId?: string): Promise { + const organizations = await this.stateService.getOrganizations({ userId: userId }); + const response: Organization[] = []; + for (const id in organizations) { + if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) { + response.push(new Organization(organizations[id])); + } + } + return response; + } + + async save(organizations: { [id: string]: OrganizationData }) { + return await this.stateService.setOrganizations(organizations); + } + + async canManageSponsorships(): Promise { + const orgs = await this.getAll(); + return orgs.some( + (o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null + ); + } +} diff --git a/common/src/services/passwordGeneration.service.ts b/common/src/services/passwordGeneration.service.ts index ab4af5b9..4e5d5872 100644 --- a/common/src/services/passwordGeneration.service.ts +++ b/common/src/services/passwordGeneration.service.ts @@ -1,560 +1,575 @@ -import * as zxcvbn from 'zxcvbn'; +import * as zxcvbn from "zxcvbn"; -import { EncString } from '../models/domain/encString'; -import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; -import { PasswordGeneratorPolicyOptions } from '../models/domain/passwordGeneratorPolicyOptions'; -import { Policy } from '../models/domain/policy'; +import { EncString } from "../models/domain/encString"; +import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; +import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions"; +import { Policy } from "../models/domain/policy"; -import { CryptoService } from '../abstractions/crypto.service'; -import { - PasswordGenerationService as PasswordGenerationServiceAbstraction, -} from '../abstractions/passwordGeneration.service'; -import { PolicyService } from '../abstractions/policy.service'; -import { StorageService } from '../abstractions/storage.service'; +import { CryptoService } from "../abstractions/crypto.service"; +import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "../abstractions/passwordGeneration.service"; +import { PolicyService } from "../abstractions/policy.service"; +import { StateService } from "../abstractions/state.service"; -import { EEFLongWordList } from '../misc/wordlist'; +import { EEFLongWordList } from "../misc/wordlist"; -import { PolicyType } from '../enums/policyType'; +import { PolicyType } from "../enums/policyType"; const DefaultOptions = { - length: 14, - ambiguous: false, - number: true, - minNumber: 1, - uppercase: true, - minUppercase: 0, - lowercase: true, - minLowercase: 0, - special: false, - minSpecial: 1, - type: 'password', - numWords: 3, - wordSeparator: '-', - capitalize: false, - includeNumber: false, -}; - -const Keys = { - options: 'passwordGenerationOptions', - history: 'generatedPasswordHistory', + length: 14, + ambiguous: false, + number: true, + minNumber: 1, + uppercase: true, + minUppercase: 0, + lowercase: true, + minLowercase: 0, + special: false, + minSpecial: 1, + type: "password", + numWords: 3, + wordSeparator: "-", + capitalize: false, + includeNumber: false, }; const MaxPasswordsInHistory = 100; export class PasswordGenerationService implements PasswordGenerationServiceAbstraction { - private optionsCache: any; - private history: GeneratedPasswordHistory[]; + constructor( + private cryptoService: CryptoService, + private policyService: PolicyService, + private stateService: StateService + ) {} - constructor(private cryptoService: CryptoService, private storageService: StorageService, - private policyService: PolicyService) { } + async generatePassword(options: any): Promise { + // overload defaults with given options + const o = Object.assign({}, DefaultOptions, options); - async generatePassword(options: any): Promise { - // overload defaults with given options - const o = Object.assign({}, DefaultOptions, options); - - if (o.type === 'passphrase') { - return this.generatePassphrase(options); - } - - // sanitize - this.sanitizePasswordLength(o, true); - - const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; - if (o.length < minLength) { - o.length = minLength; - } - - const positions: string[] = []; - if (o.lowercase && o.minLowercase > 0) { - for (let i = 0; i < o.minLowercase; i++) { - positions.push('l'); - } - } - if (o.uppercase && o.minUppercase > 0) { - for (let i = 0; i < o.minUppercase; i++) { - positions.push('u'); - } - } - if (o.number && o.minNumber > 0) { - for (let i = 0; i < o.minNumber; i++) { - positions.push('n'); - } - } - if (o.special && o.minSpecial > 0) { - for (let i = 0; i < o.minSpecial; i++) { - positions.push('s'); - } - } - while (positions.length < o.length) { - positions.push('a'); - } - - // shuffle - await this.shuffleArray(positions); - - // build out the char sets - let allCharSet = ''; - - let lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz'; - if (o.ambiguous) { - lowercaseCharSet += 'l'; - } - if (o.lowercase) { - allCharSet += lowercaseCharSet; - } - - let uppercaseCharSet = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; - if (o.ambiguous) { - uppercaseCharSet += 'IO'; - } - if (o.uppercase) { - allCharSet += uppercaseCharSet; - } - - let numberCharSet = '23456789'; - if (o.ambiguous) { - numberCharSet += '01'; - } - if (o.number) { - allCharSet += numberCharSet; - } - - const specialCharSet = '!@#$%^&*'; - if (o.special) { - allCharSet += specialCharSet; - } - - let password = ''; - for (let i = 0; i < o.length; i++) { - let positionChars: string; - switch (positions[i]) { - case 'l': - positionChars = lowercaseCharSet; - break; - case 'u': - positionChars = uppercaseCharSet; - break; - case 'n': - positionChars = numberCharSet; - break; - case 's': - positionChars = specialCharSet; - break; - case 'a': - positionChars = allCharSet; - break; - default: - break; - } - - const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1); - password += positionChars.charAt(randomCharIndex); - } - - return password; + if (o.type === "passphrase") { + return this.generatePassphrase(options); } - async generatePassphrase(options: any): Promise { - const o = Object.assign({}, DefaultOptions, options); + // sanitize + this.sanitizePasswordLength(o, true); - if (o.numWords == null || o.numWords <= 2) { - o.numWords = DefaultOptions.numWords; - } - if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) { - o.wordSeparator = ' '; - } - if (o.capitalize == null) { - o.capitalize = false; - } - if (o.includeNumber == null) { - o.includeNumber = false; - } - - const listLength = EEFLongWordList.length - 1; - const wordList = new Array(o.numWords); - for (let i = 0; i < o.numWords; i++) { - const wordIndex = await this.cryptoService.randomNumber(0, listLength); - if (o.capitalize) { - wordList[i] = this.capitalize(EEFLongWordList[wordIndex]); - } else { - wordList[i] = EEFLongWordList[wordIndex]; - } - } - - if (o.includeNumber) { - await this.appendRandomNumberToRandomWord(wordList); - } - return wordList.join(o.wordSeparator); + const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; + if (o.length < minLength) { + o.length = minLength; } - async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> { - if (this.optionsCache == null) { - const options = await this.storageService.get(Keys.options); - if (options == null) { - this.optionsCache = DefaultOptions; - } else { - this.optionsCache = Object.assign({}, DefaultOptions, options); - } - } - const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(this.optionsCache); - this.optionsCache = enforcedOptions[0]; - return [this.optionsCache, enforcedOptions[1]]; + const positions: string[] = []; + if (o.lowercase && o.minLowercase > 0) { + for (let i = 0; i < o.minLowercase; i++) { + positions.push("l"); + } + } + if (o.uppercase && o.minUppercase > 0) { + for (let i = 0; i < o.minUppercase; i++) { + positions.push("u"); + } + } + if (o.number && o.minNumber > 0) { + for (let i = 0; i < o.minNumber; i++) { + positions.push("n"); + } + } + if (o.special && o.minSpecial > 0) { + for (let i = 0; i < o.minSpecial; i++) { + positions.push("s"); + } + } + while (positions.length < o.length) { + positions.push("a"); } - async enforcePasswordGeneratorPoliciesOnOptions(options: any): Promise<[any, PasswordGeneratorPolicyOptions]> { - let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions(); - if (enforcedPolicyOptions != null) { - if (options.length < enforcedPolicyOptions.minLength) { - options.length = enforcedPolicyOptions.minLength; - } + // shuffle + await this.shuffleArray(positions); - if (enforcedPolicyOptions.useUppercase) { - options.uppercase = true; - } + // build out the char sets + let allCharSet = ""; - if (enforcedPolicyOptions.useLowercase) { - options.lowercase = true; - } - - if (enforcedPolicyOptions.useNumbers) { - options.number = true; - } - - if (options.minNumber < enforcedPolicyOptions.numberCount) { - options.minNumber = enforcedPolicyOptions.numberCount; - } - - if (enforcedPolicyOptions.useSpecial) { - options.special = true; - } - - if (options.minSpecial < enforcedPolicyOptions.specialCount) { - options.minSpecial = enforcedPolicyOptions.specialCount; - } - - // Must normalize these fields because the receiving call expects all options to pass the current rules - if (options.minSpecial + options.minNumber > options.length) { - options.minSpecial = options.length - options.minNumber; - } - - if (options.numWords < enforcedPolicyOptions.minNumberWords) { - options.numWords = enforcedPolicyOptions.minNumberWords; - } - - if (enforcedPolicyOptions.capitalize) { - options.capitalize = true; - } - - if (enforcedPolicyOptions.includeNumber) { - options.includeNumber = true; - } - - // Force default type if password/passphrase selected via policy - if (enforcedPolicyOptions.defaultType === 'password' || - enforcedPolicyOptions.defaultType === 'passphrase') { - options.type = enforcedPolicyOptions.defaultType; - } - } else { // UI layer expects an instantiated object to prevent more explicit null checks - enforcedPolicyOptions = new PasswordGeneratorPolicyOptions(); - } - return [options, enforcedPolicyOptions]; + let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz"; + if (o.ambiguous) { + lowercaseCharSet += "l"; + } + if (o.lowercase) { + allCharSet += lowercaseCharSet; } - async getPasswordGeneratorPolicyOptions(): Promise { - const policies: Policy[] = this.policyService == null ? null : - await this.policyService.getAll(PolicyType.PasswordGenerator); - let enforcedOptions: PasswordGeneratorPolicyOptions = null; - - if (policies == null || policies.length === 0) { - return enforcedOptions; - } - - policies.forEach(currentPolicy => { - if (!currentPolicy.enabled || currentPolicy.data == null) { - return; - } - - if (enforcedOptions == null) { - enforcedOptions = new PasswordGeneratorPolicyOptions(); - } - - // Password wins in multi-org collisions - if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== 'password') { - enforcedOptions.defaultType = currentPolicy.data.defaultType; - } - - if (currentPolicy.data.minLength != null - && currentPolicy.data.minLength > enforcedOptions.minLength) { - enforcedOptions.minLength = currentPolicy.data.minLength; - } - - if (currentPolicy.data.useUpper) { - enforcedOptions.useUppercase = true; - } - - if (currentPolicy.data.useLower) { - enforcedOptions.useLowercase = true; - } - - if (currentPolicy.data.useNumbers) { - enforcedOptions.useNumbers = true; - } - - if (currentPolicy.data.minNumbers != null - && currentPolicy.data.minNumbers > enforcedOptions.numberCount) { - enforcedOptions.numberCount = currentPolicy.data.minNumbers; - } - - if (currentPolicy.data.useSpecial) { - enforcedOptions.useSpecial = true; - } - - if (currentPolicy.data.minSpecial != null - && currentPolicy.data.minSpecial > enforcedOptions.specialCount) { - enforcedOptions.specialCount = currentPolicy.data.minSpecial; - } - - if (currentPolicy.data.minNumberWords != null - && currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords) { - enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords; - } - - if (currentPolicy.data.capitalize) { - enforcedOptions.capitalize = true; - } - - if (currentPolicy.data.includeNumber) { - enforcedOptions.includeNumber = true; - } - }); - - return enforcedOptions; + let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ"; + if (o.ambiguous) { + uppercaseCharSet += "IO"; + } + if (o.uppercase) { + allCharSet += uppercaseCharSet; } - async saveOptions(options: any) { - await this.storageService.save(Keys.options, options); - this.optionsCache = options; + let numberCharSet = "23456789"; + if (o.ambiguous) { + numberCharSet += "01"; + } + if (o.number) { + allCharSet += numberCharSet; } - async getHistory(): Promise { - const hasKey = await this.cryptoService.hasKey(); - if (!hasKey) { - return new Array(); - } - - if (!this.history) { - const encrypted = await this.storageService.get(Keys.history); - this.history = await this.decryptHistory(encrypted); - } - - return this.history || new Array(); + const specialCharSet = "!@#$%^&*"; + if (o.special) { + allCharSet += specialCharSet; } - async addHistory(password: string): Promise { - // Cannot add new history if no key is available - const hasKey = await this.cryptoService.hasKey(); - if (!hasKey) { - return; - } + let password = ""; + for (let i = 0; i < o.length; i++) { + let positionChars: string; + switch (positions[i]) { + case "l": + positionChars = lowercaseCharSet; + break; + case "u": + positionChars = uppercaseCharSet; + break; + case "n": + positionChars = numberCharSet; + break; + case "s": + positionChars = specialCharSet; + break; + case "a": + positionChars = allCharSet; + break; + default: + break; + } - const currentHistory = await this.getHistory(); - - // Prevent duplicates - if (this.matchesPrevious(password, currentHistory)) { - return; - } - - currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now())); - - // Remove old items. - if (currentHistory.length > MaxPasswordsInHistory) { - currentHistory.pop(); - } - - const newHistory = await this.encryptHistory(currentHistory); - return await this.storageService.save(Keys.history, newHistory); + const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1); + password += positionChars.charAt(randomCharIndex); } - async clear(): Promise { - this.history = []; - return await this.storageService.remove(Keys.history); + return password; + } + + async generatePassphrase(options: any): Promise { + const o = Object.assign({}, DefaultOptions, options); + + if (o.numWords == null || o.numWords <= 2) { + o.numWords = DefaultOptions.numWords; + } + if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) { + o.wordSeparator = " "; + } + if (o.capitalize == null) { + o.capitalize = false; + } + if (o.includeNumber == null) { + o.includeNumber = false; } - passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult { - if (password == null || password.length === 0) { - return null; - } - let globalUserInputs = ['bitwarden', 'bit', 'warden']; - if (userInputs != null && userInputs.length > 0) { - globalUserInputs = globalUserInputs.concat(userInputs); - } - // Use a hash set to get rid of any duplicate user inputs - const finalUserInputs = Array.from(new Set(globalUserInputs)); - const result = zxcvbn(password, finalUserInputs); - return result; + const listLength = EEFLongWordList.length - 1; + const wordList = new Array(o.numWords); + for (let i = 0; i < o.numWords; i++) { + const wordIndex = await this.cryptoService.randomNumber(0, listLength); + if (o.capitalize) { + wordList[i] = this.capitalize(EEFLongWordList[wordIndex]); + } else { + wordList[i] = EEFLongWordList[wordIndex]; + } } - normalizeOptions(options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) { - options.minLowercase = 0; - options.minUppercase = 0; + if (o.includeNumber) { + await this.appendRandomNumberToRandomWord(wordList); + } + return wordList.join(o.wordSeparator); + } - if (!options.length || options.length < 5) { - options.length = 5; - } else if (options.length > 128) { - options.length = 128; - } + async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> { + let options = await this.stateService.getPasswordGenerationOptions(); + if (options == null) { + options = DefaultOptions; + } else { + options = Object.assign({}, DefaultOptions, options); + } + await this.stateService.setPasswordGenerationOptions(options); + const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(options); + options = enforcedOptions[0]; + return [options, enforcedOptions[1]]; + } - if (options.length < enforcedPolicyOptions.minLength) { - options.length = enforcedPolicyOptions.minLength; - } + async enforcePasswordGeneratorPoliciesOnOptions( + options: any + ): Promise<[any, PasswordGeneratorPolicyOptions]> { + let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions(); + if (enforcedPolicyOptions != null) { + if (options.length < enforcedPolicyOptions.minLength) { + options.length = enforcedPolicyOptions.minLength; + } - if (!options.minNumber) { - options.minNumber = 0; - } else if (options.minNumber > options.length) { - options.minNumber = options.length; - } else if (options.minNumber > 9) { - options.minNumber = 9; - } + if (enforcedPolicyOptions.useUppercase) { + options.uppercase = true; + } - if (options.minNumber < enforcedPolicyOptions.numberCount) { - options.minNumber = enforcedPolicyOptions.numberCount; - } + if (enforcedPolicyOptions.useLowercase) { + options.lowercase = true; + } - if (!options.minSpecial) { - options.minSpecial = 0; - } else if (options.minSpecial > options.length) { - options.minSpecial = options.length; - } else if (options.minSpecial > 9) { - options.minSpecial = 9; - } + if (enforcedPolicyOptions.useNumbers) { + options.number = true; + } - if (options.minSpecial < enforcedPolicyOptions.specialCount) { - options.minSpecial = enforcedPolicyOptions.specialCount; - } + if (options.minNumber < enforcedPolicyOptions.numberCount) { + options.minNumber = enforcedPolicyOptions.numberCount; + } - if (options.minSpecial + options.minNumber > options.length) { - options.minSpecial = options.length - options.minNumber; - } + if (enforcedPolicyOptions.useSpecial) { + options.special = true; + } - if (options.numWords == null || options.length < 3) { - options.numWords = 3; - } else if (options.numWords > 20) { - options.numWords = 20; - } + if (options.minSpecial < enforcedPolicyOptions.specialCount) { + options.minSpecial = enforcedPolicyOptions.specialCount; + } - if (options.numWords < enforcedPolicyOptions.minNumberWords) { - options.numWords = enforcedPolicyOptions.minNumberWords; - } + // Must normalize these fields because the receiving call expects all options to pass the current rules + if (options.minSpecial + options.minNumber > options.length) { + options.minSpecial = options.length - options.minNumber; + } - if (options.wordSeparator != null && options.wordSeparator.length > 1) { - options.wordSeparator = options.wordSeparator[0]; - } + if (options.numWords < enforcedPolicyOptions.minNumberWords) { + options.numWords = enforcedPolicyOptions.minNumberWords; + } - this.sanitizePasswordLength(options, false); + if (enforcedPolicyOptions.capitalize) { + options.capitalize = true; + } + + if (enforcedPolicyOptions.includeNumber) { + options.includeNumber = true; + } + + // Force default type if password/passphrase selected via policy + if ( + enforcedPolicyOptions.defaultType === "password" || + enforcedPolicyOptions.defaultType === "passphrase" + ) { + options.type = enforcedPolicyOptions.defaultType; + } + } else { + // UI layer expects an instantiated object to prevent more explicit null checks + enforcedPolicyOptions = new PasswordGeneratorPolicyOptions(); + } + return [options, enforcedPolicyOptions]; + } + + async getPasswordGeneratorPolicyOptions(): Promise { + const policies: Policy[] = + this.policyService == null + ? null + : await this.policyService.getAll(PolicyType.PasswordGenerator); + let enforcedOptions: PasswordGeneratorPolicyOptions = null; + + if (policies == null || policies.length === 0) { + return enforcedOptions; } - private capitalize(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); + policies.forEach((currentPolicy) => { + if (!currentPolicy.enabled || currentPolicy.data == null) { + return; + } + + if (enforcedOptions == null) { + enforcedOptions = new PasswordGeneratorPolicyOptions(); + } + + // Password wins in multi-org collisions + if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== "password") { + enforcedOptions.defaultType = currentPolicy.data.defaultType; + } + + if ( + currentPolicy.data.minLength != null && + currentPolicy.data.minLength > enforcedOptions.minLength + ) { + enforcedOptions.minLength = currentPolicy.data.minLength; + } + + if (currentPolicy.data.useUpper) { + enforcedOptions.useUppercase = true; + } + + if (currentPolicy.data.useLower) { + enforcedOptions.useLowercase = true; + } + + if (currentPolicy.data.useNumbers) { + enforcedOptions.useNumbers = true; + } + + if ( + currentPolicy.data.minNumbers != null && + currentPolicy.data.minNumbers > enforcedOptions.numberCount + ) { + enforcedOptions.numberCount = currentPolicy.data.minNumbers; + } + + if (currentPolicy.data.useSpecial) { + enforcedOptions.useSpecial = true; + } + + if ( + currentPolicy.data.minSpecial != null && + currentPolicy.data.minSpecial > enforcedOptions.specialCount + ) { + enforcedOptions.specialCount = currentPolicy.data.minSpecial; + } + + if ( + currentPolicy.data.minNumberWords != null && + currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords + ) { + enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords; + } + + if (currentPolicy.data.capitalize) { + enforcedOptions.capitalize = true; + } + + if (currentPolicy.data.includeNumber) { + enforcedOptions.includeNumber = true; + } + }); + + return enforcedOptions; + } + + async saveOptions(options: any) { + await this.stateService.setPasswordGenerationOptions(options); + } + + async getHistory(): Promise { + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { + return new Array(); } - private async appendRandomNumberToRandomWord(wordList: string[]) { - if (wordList == null || wordList.length <= 0) { - return; - } - const index = await this.cryptoService.randomNumber(0, wordList.length - 1); - const num = await this.cryptoService.randomNumber(0, 9); - wordList[index] = wordList[index] + num; + if ((await this.stateService.getDecryptedPasswordGenerationHistory()) != null) { + const encrypted = await this.stateService.getEncryptedPasswordGenerationHistory(); + const decrypted = await this.decryptHistory(encrypted); + await this.stateService.setDecryptedPasswordGenerationHistory(decrypted); } - private async encryptHistory(history: GeneratedPasswordHistory[]): Promise { - if (history == null || history.length === 0) { - return Promise.resolve([]); - } + const passwordGenerationHistory = + await this.stateService.getDecryptedPasswordGenerationHistory(); + return passwordGenerationHistory != null + ? passwordGenerationHistory + : new Array(); + } - const promises = history.map(async item => { - const encrypted = await this.cryptoService.encrypt(item.password); - return new GeneratedPasswordHistory(encrypted.encryptedString, item.date); - }); - - return await Promise.all(promises); + async addHistory(password: string): Promise { + // Cannot add new history if no key is available + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { + return; } - private async decryptHistory(history: GeneratedPasswordHistory[]): Promise { - if (history == null || history.length === 0) { - return Promise.resolve([]); - } + const currentHistory = await this.getHistory(); - const promises = history.map(async item => { - const decrypted = await this.cryptoService.decryptToUtf8(new EncString(item.password)); - return new GeneratedPasswordHistory(decrypted, item.date); - }); - - return await Promise.all(promises); + // Prevent duplicates + if (this.matchesPrevious(password, currentHistory)) { + return; } - private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean { - if (history == null || history.length === 0) { - return false; - } + currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now())); - return history[history.length - 1].password === password; + // Remove old items. + if (currentHistory.length > MaxPasswordsInHistory) { + currentHistory.pop(); } - // ref: https://stackoverflow.com/a/12646864/1090359 - private async shuffleArray(array: string[]) { - for (let i = array.length - 1; i > 0; i--) { - const j = await this.cryptoService.randomNumber(0, i); - [array[i], array[j]] = [array[j], array[i]]; - } + const newHistory = await this.encryptHistory(currentHistory); + return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory); + } + + async clear(userId?: string): Promise { + await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId }); + await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId }); + } + + passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult { + if (password == null || password.length === 0) { + return null; + } + let globalUserInputs = ["bitwarden", "bit", "warden"]; + if (userInputs != null && userInputs.length > 0) { + globalUserInputs = globalUserInputs.concat(userInputs); + } + // Use a hash set to get rid of any duplicate user inputs + const finalUserInputs = Array.from(new Set(globalUserInputs)); + const result = zxcvbn(password, finalUserInputs); + return result; + } + + normalizeOptions(options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) { + options.minLowercase = 0; + options.minUppercase = 0; + + if (!options.length || options.length < 5) { + options.length = 5; + } else if (options.length > 128) { + options.length = 128; } - private sanitizePasswordLength(options: any, forGeneration: boolean) { - let minUppercaseCalc = 0; - let minLowercaseCalc = 0; - let minNumberCalc: number = options.minNumber; - let minSpecialCalc: number = options.minSpecial; - - if (options.uppercase && options.minUppercase <= 0) { - minUppercaseCalc = 1; - } else if (!options.uppercase) { - minUppercaseCalc = 0; - } - - if (options.lowercase && options.minLowercase <= 0) { - minLowercaseCalc = 1; - } else if (!options.lowercase) { - minLowercaseCalc = 0; - } - - if (options.number && options.minNumber <= 0) { - minNumberCalc = 1; - } else if (!options.number) { - minNumberCalc = 0; - } - - if (options.special && options.minSpecial <= 0) { - minSpecialCalc = 1; - } else if (!options.special) { - minSpecialCalc = 0; - } - - // This should never happen but is a final safety net - if (!options.length || options.length < 1) { - options.length = 10; - } - - const minLength: number = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc; - // Normalize and Generation both require this modification - if (options.length < minLength) { - options.length = minLength; - } - - // Apply other changes if the options object passed in is for generation - if (forGeneration) { - options.minUppercase = minUppercaseCalc; - options.minLowercase = minLowercaseCalc; - options.minNumber = minNumberCalc; - options.minSpecial = minSpecialCalc; - } + if (options.length < enforcedPolicyOptions.minLength) { + options.length = enforcedPolicyOptions.minLength; } + + if (!options.minNumber) { + options.minNumber = 0; + } else if (options.minNumber > options.length) { + options.minNumber = options.length; + } else if (options.minNumber > 9) { + options.minNumber = 9; + } + + if (options.minNumber < enforcedPolicyOptions.numberCount) { + options.minNumber = enforcedPolicyOptions.numberCount; + } + + if (!options.minSpecial) { + options.minSpecial = 0; + } else if (options.minSpecial > options.length) { + options.minSpecial = options.length; + } else if (options.minSpecial > 9) { + options.minSpecial = 9; + } + + if (options.minSpecial < enforcedPolicyOptions.specialCount) { + options.minSpecial = enforcedPolicyOptions.specialCount; + } + + if (options.minSpecial + options.minNumber > options.length) { + options.minSpecial = options.length - options.minNumber; + } + + if (options.numWords == null || options.length < 3) { + options.numWords = 3; + } else if (options.numWords > 20) { + options.numWords = 20; + } + + if (options.numWords < enforcedPolicyOptions.minNumberWords) { + options.numWords = enforcedPolicyOptions.minNumberWords; + } + + if (options.wordSeparator != null && options.wordSeparator.length > 1) { + options.wordSeparator = options.wordSeparator[0]; + } + + this.sanitizePasswordLength(options, false); + } + + private capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + private async appendRandomNumberToRandomWord(wordList: string[]) { + if (wordList == null || wordList.length <= 0) { + return; + } + const index = await this.cryptoService.randomNumber(0, wordList.length - 1); + const num = await this.cryptoService.randomNumber(0, 9); + wordList[index] = wordList[index] + num; + } + + private async encryptHistory( + history: GeneratedPasswordHistory[] + ): Promise { + if (history == null || history.length === 0) { + return Promise.resolve([]); + } + + const promises = history.map(async (item) => { + const encrypted = await this.cryptoService.encrypt(item.password); + return new GeneratedPasswordHistory(encrypted.encryptedString, item.date); + }); + + return await Promise.all(promises); + } + + private async decryptHistory( + history: GeneratedPasswordHistory[] + ): Promise { + if (history == null || history.length === 0) { + return Promise.resolve([]); + } + + const promises = history.map(async (item) => { + const decrypted = await this.cryptoService.decryptToUtf8(new EncString(item.password)); + return new GeneratedPasswordHistory(decrypted, item.date); + }); + + return await Promise.all(promises); + } + + private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean { + if (history == null || history.length === 0) { + return false; + } + + return history[history.length - 1].password === password; + } + + // ref: https://stackoverflow.com/a/12646864/1090359 + private async shuffleArray(array: string[]) { + for (let i = array.length - 1; i > 0; i--) { + const j = await this.cryptoService.randomNumber(0, i); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + private sanitizePasswordLength(options: any, forGeneration: boolean) { + let minUppercaseCalc = 0; + let minLowercaseCalc = 0; + let minNumberCalc: number = options.minNumber; + let minSpecialCalc: number = options.minSpecial; + + if (options.uppercase && options.minUppercase <= 0) { + minUppercaseCalc = 1; + } else if (!options.uppercase) { + minUppercaseCalc = 0; + } + + if (options.lowercase && options.minLowercase <= 0) { + minLowercaseCalc = 1; + } else if (!options.lowercase) { + minLowercaseCalc = 0; + } + + if (options.number && options.minNumber <= 0) { + minNumberCalc = 1; + } else if (!options.number) { + minNumberCalc = 0; + } + + if (options.special && options.minSpecial <= 0) { + minSpecialCalc = 1; + } else if (!options.special) { + minSpecialCalc = 0; + } + + // This should never happen but is a final safety net + if (!options.length || options.length < 1) { + options.length = 10; + } + + const minLength: number = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc; + // Normalize and Generation both require this modification + if (options.length < minLength) { + options.length = minLength; + } + + // Apply other changes if the options object passed in is for generation + if (forGeneration) { + options.minUppercase = minUppercaseCalc; + options.minLowercase = minLowercaseCalc; + options.minNumber = minNumberCalc; + options.minSpecial = minSpecialCalc; + } + } } diff --git a/common/src/services/policy.service.ts b/common/src/services/policy.service.ts index 4d23448a..79af726a 100644 --- a/common/src/services/policy.service.ts +++ b/common/src/services/policy.service.ts @@ -1,219 +1,240 @@ -import { PolicyService as PolicyServiceAbstraction } from '../abstractions/policy.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { OrganizationService } from "../abstractions/organization.service"; +import { PolicyService as PolicyServiceAbstraction } from "../abstractions/policy.service"; +import { StateService } from "../abstractions/state.service"; -import { PolicyData } from '../models/data/policyData'; +import { PolicyData } from "../models/data/policyData"; -import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions'; -import { Organization } from '../models/domain/organization'; -import { Policy } from '../models/domain/policy'; -import { ResetPasswordPolicyOptions } from '../models/domain/resetPasswordPolicyOptions'; +import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions"; +import { Organization } from "../models/domain/organization"; +import { Policy } from "../models/domain/policy"; +import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions"; -import { OrganizationUserStatusType } from '../enums/organizationUserStatusType'; -import { OrganizationUserType } from '../enums/organizationUserType'; -import { PolicyType } from '../enums/policyType'; +import { OrganizationUserStatusType } from "../enums/organizationUserStatusType"; +import { OrganizationUserType } from "../enums/organizationUserType"; +import { PolicyType } from "../enums/policyType"; -import { ApiService } from '../abstractions/api.service'; -import { ListResponse } from '../models/response/listResponse'; -import { PolicyResponse } from '../models/response/policyResponse'; - -const Keys = { - policiesPrefix: 'policies_', -}; +import { ApiService } from "../abstractions/api.service"; +import { ListResponse } from "../models/response/listResponse"; +import { PolicyResponse } from "../models/response/policyResponse"; export class PolicyService implements PolicyServiceAbstraction { - policyCache: Policy[]; + policyCache: Policy[]; - constructor(private userService: UserService, private storageService: StorageService, - private apiService: ApiService) { + constructor( + private stateService: StateService, + private organizationService: OrganizationService, + private apiService: ApiService + ) {} + + async clearCache(): Promise { + await this.stateService.setDecryptedPolicies(null); + } + + async getAll(type?: PolicyType, userId?: string): Promise { + let response: Policy[] = []; + const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId }); + if (decryptedPolicies != null) { + response = decryptedPolicies; + } else { + const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId }); + for (const id in diskPolicies) { + if (diskPolicies.hasOwnProperty(id)) { + response.push(new Policy(diskPolicies[id])); + } + } + await this.stateService.setDecryptedPolicies(response, { userId: userId }); + } + if (type != null) { + return response.filter((policy) => policy.type === type); + } else { + return response; + } + } + + async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise { + const org = await this.organizationService.get(organizationId); + if (org?.isProviderUser) { + const orgPolicies = await this.apiService.getPolicies(organizationId); + const policy = orgPolicies.data.find((p) => p.organizationId === organizationId); + + if (policy == null) { + return null; + } + + return new Policy(new PolicyData(policy)); } - clearCache(): void { - this.policyCache = null; + const policies = await this.getAll(policyType); + return policies.find((p) => p.organizationId === organizationId); + } + + async replace(policies: { [id: string]: PolicyData }): Promise { + await this.stateService.setDecryptedPolicies(null); + await this.stateService.setEncryptedPolicies(policies); + } + + async clear(userId?: string): Promise { + await this.stateService.setDecryptedPolicies(null, { userId: userId }); + await this.stateService.setEncryptedPolicies(null, { userId: userId }); + } + + async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise { + let enforcedOptions: MasterPasswordPolicyOptions = null; + + if (policies == null) { + policies = await this.getAll(PolicyType.MasterPassword); + } else { + policies = policies.filter((p) => p.type === PolicyType.MasterPassword); } - async getAll(type?: PolicyType): Promise { - if (this.policyCache == null) { - const userId = await this.userService.getUserId(); - const policies = await this.storageService.get<{ [id: string]: PolicyData; }>( - Keys.policiesPrefix + userId); - const response: Policy[] = []; - for (const id in policies) { - if (policies.hasOwnProperty(id)) { - response.push(new Policy(policies[id])); - } - } - this.policyCache = response; - } - if (type != null) { - return this.policyCache.filter(p => p.type === type); - } else { - return this.policyCache; - } + if (policies == null || policies.length === 0) { + return enforcedOptions; } - async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise { - const org = await this.userService.getOrganization(organizationId); - if (org?.isProviderUser) { - const orgPolicies = await this.apiService.getPolicies(organizationId); - const policy = orgPolicies.data.find(p => p.organizationId === organizationId); + policies.forEach((currentPolicy) => { + if (!currentPolicy.enabled || currentPolicy.data == null) { + return; + } - if (policy == null) { - return null; - } + if (enforcedOptions == null) { + enforcedOptions = new MasterPasswordPolicyOptions(); + } - return new Policy(new PolicyData(policy)); - } + if ( + currentPolicy.data.minComplexity != null && + currentPolicy.data.minComplexity > enforcedOptions.minComplexity + ) { + enforcedOptions.minComplexity = currentPolicy.data.minComplexity; + } - const policies = await this.getAll(policyType); - return policies.find(p => p.organizationId === organizationId); + if ( + currentPolicy.data.minLength != null && + currentPolicy.data.minLength > enforcedOptions.minLength + ) { + enforcedOptions.minLength = currentPolicy.data.minLength; + } + + if (currentPolicy.data.requireUpper) { + enforcedOptions.requireUpper = true; + } + + if (currentPolicy.data.requireLower) { + enforcedOptions.requireLower = true; + } + + if (currentPolicy.data.requireNumbers) { + enforcedOptions.requireNumbers = true; + } + + if (currentPolicy.data.requireSpecial) { + enforcedOptions.requireSpecial = true; + } + }); + + return enforcedOptions; + } + + evaluateMasterPassword( + passwordStrength: number, + newPassword: string, + enforcedPolicyOptions: MasterPasswordPolicyOptions + ): boolean { + if (enforcedPolicyOptions == null) { + return true; } - async replace(policies: { [id: string]: PolicyData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.policiesPrefix + userId, policies); - this.policyCache = null; + if ( + enforcedPolicyOptions.minComplexity > 0 && + enforcedPolicyOptions.minComplexity > passwordStrength + ) { + return false; } - async clear(userId: string): Promise { - await this.storageService.remove(Keys.policiesPrefix + userId); - this.policyCache = null; + if ( + enforcedPolicyOptions.minLength > 0 && + enforcedPolicyOptions.minLength > newPassword.length + ) { + return false; } - async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise { - let enforcedOptions: MasterPasswordPolicyOptions = null; - - if (policies == null) { - policies = await this.getAll(PolicyType.MasterPassword); - } else { - policies = policies.filter(p => p.type === PolicyType.MasterPassword); - } - - if (policies == null || policies.length === 0) { - return enforcedOptions; - } - - policies.forEach(currentPolicy => { - if (!currentPolicy.enabled || currentPolicy.data == null) { - return; - } - - if (enforcedOptions == null) { - enforcedOptions = new MasterPasswordPolicyOptions(); - } - - if (currentPolicy.data.minComplexity != null - && currentPolicy.data.minComplexity > enforcedOptions.minComplexity) { - enforcedOptions.minComplexity = currentPolicy.data.minComplexity; - } - - if (currentPolicy.data.minLength != null - && currentPolicy.data.minLength > enforcedOptions.minLength) { - enforcedOptions.minLength = currentPolicy.data.minLength; - } - - if (currentPolicy.data.requireUpper) { - enforcedOptions.requireUpper = true; - } - - if (currentPolicy.data.requireLower) { - enforcedOptions.requireLower = true; - } - - if (currentPolicy.data.requireNumbers) { - enforcedOptions.requireNumbers = true; - } - - if (currentPolicy.data.requireSpecial) { - enforcedOptions.requireSpecial = true; - } - }); - - return enforcedOptions; + if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) { + return false; } - evaluateMasterPassword(passwordStrength: number, newPassword: string, - enforcedPolicyOptions: MasterPasswordPolicyOptions): boolean { - if (enforcedPolicyOptions == null) { - return true; - } - - if (enforcedPolicyOptions.minComplexity > 0 && enforcedPolicyOptions.minComplexity > passwordStrength) { - return false; - } - - if (enforcedPolicyOptions.minLength > 0 && enforcedPolicyOptions.minLength > newPassword.length) { - return false; - } - - if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) { - return false; - } - - if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) { - return false; - } - - if (enforcedPolicyOptions.requireNumbers && !(/[0-9]/.test(newPassword))) { - return false; - } - - if (enforcedPolicyOptions.requireSpecial && !(/[!@#$%\^&*]/g.test(newPassword))) { - return false; - } - - return true; + if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) { + return false; } - getResetPasswordPolicyOptions(policies: Policy[], orgId: string): [ResetPasswordPolicyOptions, boolean] { - const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions(); - - if (policies == null || orgId == null) { - return [resetPasswordPolicyOptions, false]; - } - - const policy = policies.find(p => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled); - resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false; - - return [resetPasswordPolicyOptions, policy?.enabled ?? false]; + if (enforcedPolicyOptions.requireNumbers && !/[0-9]/.test(newPassword)) { + return false; } - mapPoliciesFromToken(policiesResponse: ListResponse): Policy[] { - if (policiesResponse == null || policiesResponse.data == null) { - return null; - } - - const policiesData = policiesResponse.data.map(p => new PolicyData(p)); - return policiesData.map(p => new Policy(p)); + if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) { + return false; } - async policyAppliesToUser(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) { - const policies = await this.getAll(policyType); - const organizations = await this.userService.getAllOrganizations(); - let filteredPolicies; + return true; + } - if (policyFilter != null) { - filteredPolicies = policies.filter(p => p.enabled && policyFilter(p)); - } - else { - filteredPolicies = policies.filter(p => p.enabled); - } + getResetPasswordPolicyOptions( + policies: Policy[], + orgId: string + ): [ResetPasswordPolicyOptions, boolean] { + const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions(); - const policySet = new Set(filteredPolicies.map(p => p.organizationId)); - - return organizations.some(o => - o.enabled && - o.status >= OrganizationUserStatusType.Accepted && - o.usePolicies && - !this.isExcemptFromPolicies(o, policyType) && - policySet.has(o.id)); + if (policies == null || orgId == null) { + return [resetPasswordPolicyOptions, false]; } - private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) { - if (policyType === PolicyType.MaximumVaultTimeout) { - return organization.type === OrganizationUserType.Owner; - } + const policy = policies.find( + (p) => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled + ); + resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false; - return organization.isExemptFromPolicies; + return [resetPasswordPolicyOptions, policy?.enabled ?? false]; + } + + mapPoliciesFromToken(policiesResponse: ListResponse): Policy[] { + if (policiesResponse == null || policiesResponse.data == null) { + return null; } + + const policiesData = policiesResponse.data.map((p) => new PolicyData(p)); + return policiesData.map((p) => new Policy(p)); + } + + async policyAppliesToUser( + policyType: PolicyType, + policyFilter?: (policy: Policy) => boolean, + userId?: string + ) { + const policies = await this.getAll(policyType, userId); + const organizations = await this.organizationService.getAll(userId); + let filteredPolicies; + + if (policyFilter != null) { + filteredPolicies = policies.filter((p) => p.enabled && policyFilter(p)); + } else { + filteredPolicies = policies.filter((p) => p.enabled); + } + + const policySet = new Set(filteredPolicies.map((p) => p.organizationId)); + + return organizations.some( + (o) => + o.enabled && + o.status >= OrganizationUserStatusType.Accepted && + o.usePolicies && + !this.isExcemptFromPolicies(o, policyType) && + policySet.has(o.id) + ); + } + + private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) { + if (policyType === PolicyType.MaximumVaultTimeout) { + return organization.type === OrganizationUserType.Owner; + } + + return organization.isExemptFromPolicies; + } } diff --git a/common/src/services/provider.service.ts b/common/src/services/provider.service.ts new file mode 100644 index 00000000..349e2082 --- /dev/null +++ b/common/src/services/provider.service.ts @@ -0,0 +1,34 @@ +import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service"; +import { StateService } from "../abstractions/state.service"; + +import { ProviderData } from "../models/data/providerData"; + +import { Provider } from "../models/domain/provider"; + +export class ProviderService implements ProviderServiceAbstraction { + constructor(private stateService: StateService) {} + + async get(id: string): Promise { + const providers = await this.stateService.getProviders(); + if (providers == null || !providers.hasOwnProperty(id)) { + return null; + } + + return new Provider(providers[id]); + } + + async getAll(): Promise { + const providers = await this.stateService.getProviders(); + const response: Provider[] = []; + for (const id in providers) { + if (providers.hasOwnProperty(id)) { + response.push(new Provider(providers[id])); + } + } + return response; + } + + async save(providers: { [id: string]: ProviderData }) { + await this.stateService.setProviders(providers); + } +} diff --git a/common/src/services/send.service.ts b/common/src/services/send.service.ts index a5a9a699..630511d4 100644 --- a/common/src/services/send.service.ts +++ b/common/src/services/send.service.ts @@ -1,285 +1,301 @@ -import { SendData } from '../models/data/sendData'; +import { SendData } from "../models/data/sendData"; -import { SendRequest } from '../models/request/sendRequest'; +import { SendRequest } from "../models/request/sendRequest"; -import { ErrorResponse } from '../models/response/errorResponse'; -import { SendResponse } from '../models/response/sendResponse'; +import { ErrorResponse } from "../models/response/errorResponse"; +import { SendResponse } from "../models/response/sendResponse"; -import { EncArrayBuffer } from '../models/domain/encArrayBuffer'; -import { EncString } from '../models/domain/encString'; -import { Send } from '../models/domain/send'; -import { SendFile } from '../models/domain/sendFile'; -import { SendText } from '../models/domain/sendText'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { EncArrayBuffer } from "../models/domain/encArrayBuffer"; +import { EncString } from "../models/domain/encString"; +import { Send } from "../models/domain/send"; +import { SendFile } from "../models/domain/sendFile"; +import { SendText } from "../models/domain/sendText"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; -import { FileUploadType } from '../enums/fileUploadType'; -import { SendType } from '../enums/sendType'; +import { SendType } from "../enums/sendType"; -import { SendView } from '../models/view/sendView'; +import { SendView } from "../models/view/sendView"; -import { ApiService } from '../abstractions/api.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; -import { FileUploadService } from '../abstractions/fileUpload.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { SendService as SendServiceAbstraction } from '../abstractions/send.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { ApiService } from "../abstractions/api.service"; +import { CryptoService } from "../abstractions/crypto.service"; +import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; +import { FileUploadService } from "../abstractions/fileUpload.service"; +import { I18nService } from "../abstractions/i18n.service"; +import { SendService as SendServiceAbstraction } from "../abstractions/send.service"; +import { StateService } from "../abstractions/state.service"; -import { Utils } from '../misc/utils'; - -const Keys = { - sendsPrefix: 'sends_', -}; +import { Utils } from "../misc/utils"; export class SendService implements SendServiceAbstraction { - decryptedSendCache: SendView[]; + constructor( + private cryptoService: CryptoService, + private apiService: ApiService, + private fileUploadService: FileUploadService, + private i18nService: I18nService, + private cryptoFunctionService: CryptoFunctionService, + private stateService: StateService + ) {} - constructor(private cryptoService: CryptoService, private userService: UserService, - private apiService: ApiService, private fileUploadService: FileUploadService, - private storageService: StorageService, private i18nService: I18nService, - private cryptoFunctionService: CryptoFunctionService) { } + async clearCache(): Promise { + await this.stateService.setDecryptedSends(null); + } - clearCache(): void { - this.decryptedSendCache = null; + async encrypt( + model: SendView, + file: File | ArrayBuffer, + password: string, + key?: SymmetricCryptoKey + ): Promise<[Send, EncArrayBuffer]> { + let fileData: EncArrayBuffer = null; + const send = new Send(); + send.id = model.id; + send.type = model.type; + send.disabled = model.disabled; + send.hideEmail = model.hideEmail; + send.maxAccessCount = model.maxAccessCount; + if (model.key == null) { + model.key = await this.cryptoFunctionService.randomBytes(16); + model.cryptoKey = await this.cryptoService.makeSendKey(model.key); } - - async encrypt(model: SendView, file: File | ArrayBuffer, password: string, - key?: SymmetricCryptoKey): Promise<[Send, EncArrayBuffer]> { - let fileData: EncArrayBuffer = null; - const send = new Send(); - send.id = model.id; - send.type = model.type; - send.disabled = model.disabled; - send.hideEmail = model.hideEmail; - send.maxAccessCount = model.maxAccessCount; - if (model.key == null) { - model.key = await this.cryptoFunctionService.randomBytes(16); - model.cryptoKey = await this.cryptoService.makeSendKey(model.key); - } - if (password != null) { - const passwordHash = await this.cryptoFunctionService.pbkdf2(password, model.key, 'sha256', 100000); - send.password = Utils.fromBufferToB64(passwordHash); - } - send.key = await this.cryptoService.encrypt(model.key, key); - send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey); - send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey); - if (send.type === SendType.Text) { - send.text = new SendText(); - send.text.text = await this.cryptoService.encrypt(model.text.text, model.cryptoKey); - send.text.hidden = model.text.hidden; - } else if (send.type === SendType.File) { - send.file = new SendFile(); - if (file != null) { - if (file instanceof ArrayBuffer) { - const [name, data] = await this.encryptFileData(model.file.fileName, file, model.cryptoKey); - send.file.fileName = name; - fileData = data; - } else { - fileData = await this.parseFile(send, file, model.cryptoKey); - } - } - } - - return [send, fileData]; + if (password != null) { + const passwordHash = await this.cryptoFunctionService.pbkdf2( + password, + model.key, + "sha256", + 100000 + ); + send.password = Utils.fromBufferToB64(passwordHash); } - - async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const sends = await this.storageService.get<{ [id: string]: SendData; }>( - Keys.sendsPrefix + userId); - if (sends == null || !sends.hasOwnProperty(id)) { - return null; - } - - return new Send(sends[id]); - } - - async getAll(): Promise { - const userId = await this.userService.getUserId(); - const sends = await this.storageService.get<{ [id: string]: SendData; }>( - Keys.sendsPrefix + userId); - const response: Send[] = []; - for (const id in sends) { - if (sends.hasOwnProperty(id)) { - response.push(new Send(sends[id])); - } - } - return response; - } - - async getAllDecrypted(): Promise { - if (this.decryptedSendCache != null) { - return this.decryptedSendCache; - } - - const hasKey = await this.cryptoService.hasKey(); - if (!hasKey) { - throw new Error('No key.'); - } - - const decSends: SendView[] = []; - const promises: Promise[] = []; - const sends = await this.getAll(); - sends.forEach(send => { - promises.push(send.decrypt().then(f => decSends.push(f))); - }); - - await Promise.all(promises); - decSends.sort(Utils.getSortFunction(this.i18nService, 'name')); - - this.decryptedSendCache = decSends; - return this.decryptedSendCache; - } - - async saveWithServer(sendData: [Send, EncArrayBuffer]): Promise { - const request = new SendRequest(sendData[0], sendData[1]?.buffer.byteLength); - let response: SendResponse; - if (sendData[0].id == null) { - if (sendData[0].type === SendType.Text) { - response = await this.apiService.postSend(request); - } else { - try { - const uploadDataResponse = await this.apiService.postFileTypeSend(request); - response = uploadDataResponse.sendResponse; - - await this.fileUploadService.uploadSendFile(uploadDataResponse, sendData[0].file.fileName, sendData[1]); - } catch (e) { - if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { - response = await this.legacyServerSendFileUpload(sendData, request); - } else if (e instanceof ErrorResponse) { - throw new Error((e as ErrorResponse).getSingleMessage()); - } else { - throw e; - } - } - } - sendData[0].id = response.id; - sendData[0].accessId = response.accessId; + send.key = await this.cryptoService.encrypt(model.key, key); + send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey); + send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey); + if (send.type === SendType.Text) { + send.text = new SendText(); + send.text.text = await this.cryptoService.encrypt(model.text.text, model.cryptoKey); + send.text.hidden = model.text.hidden; + } else if (send.type === SendType.File) { + send.file = new SendFile(); + if (file != null) { + if (file instanceof ArrayBuffer) { + const [name, data] = await this.encryptFileData( + model.file.fileName, + file, + model.cryptoKey + ); + send.file.fileName = name; + fileData = data; } else { - response = await this.apiService.putSend(sendData[0].id, request); + fileData = await this.parseFile(send, file, model.cryptoKey); } - - const userId = await this.userService.getUserId(); - const data = new SendData(response, userId); - await this.upsert(data); + } } - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async legacyServerSendFileUpload(sendData: [Send, EncArrayBuffer], request: SendRequest): Promise - { - const fd = new FormData(); + return [send, fileData]; + } + + async get(id: string): Promise { + const sends = await this.stateService.getEncryptedSends(); + if (sends == null || !sends.hasOwnProperty(id)) { + return null; + } + + return new Send(sends[id]); + } + + async getAll(): Promise { + const sends = await this.stateService.getEncryptedSends(); + const response: Send[] = []; + for (const id in sends) { + if (sends.hasOwnProperty(id)) { + response.push(new Send(sends[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + let decSends = await this.stateService.getDecryptedSends(); + if (decSends != null) { + return decSends; + } + + decSends = []; + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { + throw new Error("No key."); + } + + const promises: Promise[] = []; + const sends = await this.getAll(); + sends.forEach((send) => { + promises.push(send.decrypt().then((f) => decSends.push(f))); + }); + + await Promise.all(promises); + decSends.sort(Utils.getSortFunction(this.i18nService, "name")); + + await this.stateService.setDecryptedSends(decSends); + return decSends; + } + + async saveWithServer(sendData: [Send, EncArrayBuffer]): Promise { + const request = new SendRequest(sendData[0], sendData[1]?.buffer.byteLength); + let response: SendResponse; + if (sendData[0].id == null) { + if (sendData[0].type === SendType.Text) { + response = await this.apiService.postSend(request); + } else { try { - const blob = new Blob([sendData[1].buffer], { type: 'application/octet-stream' }); - fd.append('model', JSON.stringify(request)); - fd.append('data', blob, sendData[0].file.fileName.encryptedString); + const uploadDataResponse = await this.apiService.postFileTypeSend(request); + response = uploadDataResponse.sendResponse; + + await this.fileUploadService.uploadSendFile( + uploadDataResponse, + sendData[0].file.fileName, + sendData[1] + ); } catch (e) { - if (Utils.isNode && !Utils.isBrowser) { - fd.append('model', JSON.stringify(request)); - fd.append('data', Buffer.from(sendData[1].buffer) as any, { - filepath: sendData[0].file.fileName.encryptedString, - contentType: 'application/octet-stream', - } as any); - } else { - throw e; - } + if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { + response = await this.legacyServerSendFileUpload(sendData, request); + } else if (e instanceof ErrorResponse) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } else { + throw e; + } } - return await this.apiService.postSendFileLegacy(fd); + } + sendData[0].id = response.id; + sendData[0].accessId = response.accessId; + } else { + response = await this.apiService.putSend(sendData[0].id, request); } - async upsert(send: SendData | SendData[]): Promise { - const userId = await this.userService.getUserId(); - let sends = await this.storageService.get<{ [id: string]: SendData; }>( - Keys.sendsPrefix + userId); - if (sends == null) { - sends = {}; + const userId = await this.stateService.getUserId(); + const data = new SendData(response, userId); + await this.upsert(data); + } + + /** + * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. + * This method still exists for backward compatibility with old server versions. + */ + async legacyServerSendFileUpload( + sendData: [Send, EncArrayBuffer], + request: SendRequest + ): Promise { + const fd = new FormData(); + try { + const blob = new Blob([sendData[1].buffer], { type: "application/octet-stream" }); + fd.append("model", JSON.stringify(request)); + fd.append("data", blob, sendData[0].file.fileName.encryptedString); + } catch (e) { + if (Utils.isNode && !Utils.isBrowser) { + fd.append("model", JSON.stringify(request)); + fd.append( + "data", + Buffer.from(sendData[1].buffer) as any, + { + filepath: sendData[0].file.fileName.encryptedString, + contentType: "application/octet-stream", + } as any + ); + } else { + throw e; + } + } + return await this.apiService.postSendFileLegacy(fd); + } + + async upsert(send: SendData | SendData[]): Promise { + let sends = await this.stateService.getEncryptedSends(); + if (sends == null) { + sends = {}; + } + + if (send instanceof SendData) { + const s = send as SendData; + sends[s.id] = s; + } else { + (send as SendData[]).forEach((s) => { + sends[s.id] = s; + }); + } + + await this.replace(sends); + } + + async replace(sends: { [id: string]: SendData }): Promise { + await this.stateService.setDecryptedSends(null); + await this.stateService.setEncryptedSends(sends); + } + + async clear(): Promise { + await this.stateService.setDecryptedSends(null); + await this.stateService.setEncryptedSends(null); + } + + async delete(id: string | string[]): Promise { + const sends = await this.stateService.getEncryptedSends(); + if (sends == null) { + return; + } + + if (typeof id === "string") { + if (sends[id] == null) { + return; + } + delete sends[id]; + } else { + (id as string[]).forEach((i) => { + delete sends[i]; + }); + } + + await this.replace(sends); + } + + async deleteWithServer(id: string): Promise { + await this.apiService.deleteSend(id); + await this.delete(id); + } + + async removePasswordWithServer(id: string): Promise { + const response = await this.apiService.putSendRemovePassword(id); + const userId = await this.stateService.getUserId(); + const data = new SendData(response, userId); + await this.upsert(data); + } + + private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(file); + reader.onload = async (evt) => { + try { + const [name, data] = await this.encryptFileData( + file.name, + evt.target.result as ArrayBuffer, + key + ); + send.file.fileName = name; + resolve(data); + } catch (e) { + reject(e); } + }; + reader.onerror = () => { + reject("Error reading file."); + }; + }); + } - if (send instanceof SendData) { - const s = send as SendData; - sends[s.id] = s; - } else { - (send as SendData[]).forEach(s => { - sends[s.id] = s; - }); - } - - await this.storageService.save(Keys.sendsPrefix + userId, sends); - this.decryptedSendCache = null; - } - - async replace(sends: { [id: string]: SendData; }): Promise { - const userId = await this.userService.getUserId(); - await this.storageService.save(Keys.sendsPrefix + userId, sends); - this.decryptedSendCache = null; - } - - async clear(userId: string): Promise { - await this.storageService.remove(Keys.sendsPrefix + userId); - this.decryptedSendCache = null; - } - - async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const sends = await this.storageService.get<{ [id: string]: SendData; }>( - Keys.sendsPrefix + userId); - if (sends == null) { - return; - } - - if (typeof id === 'string') { - if (sends[id] == null) { - return; - } - delete sends[id]; - } else { - (id as string[]).forEach(i => { - delete sends[i]; - }); - } - - await this.storageService.save(Keys.sendsPrefix + userId, sends); - this.decryptedSendCache = null; - } - - async deleteWithServer(id: string): Promise { - await this.apiService.deleteSend(id); - await this.delete(id); - } - - async removePasswordWithServer(id: string): Promise { - const response = await this.apiService.putSendRemovePassword(id); - const userId = await this.userService.getUserId(); - const data = new SendData(response, userId); - await this.upsert(data); - } - - private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsArrayBuffer(file); - reader.onload = async evt => { - try { - const [name, data] = await this.encryptFileData(file.name, evt.target.result as ArrayBuffer, key); - send.file.fileName = name; - resolve(data); - } catch (e) { - reject(e); - } - }; - reader.onerror = evt => { - reject('Error reading file.'); - }; - }); - } - - private async encryptFileData(fileName: string, data: ArrayBuffer, - key: SymmetricCryptoKey): Promise<[EncString, EncArrayBuffer]> { - const encFileName = await this.cryptoService.encrypt(fileName, key); - const encFileData = await this.cryptoService.encryptToBytes(data, key); - return [encFileName, encFileData]; - } + private async encryptFileData( + fileName: string, + data: ArrayBuffer, + key: SymmetricCryptoKey + ): Promise<[EncString, EncArrayBuffer]> { + const encFileName = await this.cryptoService.encrypt(fileName, key); + const encFileData = await this.cryptoService.encryptToBytes(data, key); + return [encFileName, encFileData]; + } } diff --git a/common/src/services/settings.service.ts b/common/src/services/settings.service.ts index 51d970f3..a8fdf333 100644 --- a/common/src/services/settings.service.ts +++ b/common/src/services/settings.service.ts @@ -1,62 +1,55 @@ -import { SettingsService as SettingsServiceAbstraction } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service"; +import { StateService } from "../abstractions/state.service"; const Keys = { - settingsPrefix: 'settings_', - equivalentDomains: 'equivalentDomains', + settingsPrefix: "settings_", + equivalentDomains: "equivalentDomains", }; export class SettingsService implements SettingsServiceAbstraction { - private settingsCache: any; + constructor(private stateService: StateService) {} - constructor(private userService: UserService, private storageService: StorageService) { + async clearCache(): Promise { + await this.stateService.setSettings(null); + } + + getEquivalentDomains(): Promise { + return this.getSettingsKey(Keys.equivalentDomains); + } + + async setEquivalentDomains(equivalentDomains: string[][]): Promise { + await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains); + } + + async clear(userId?: string): Promise { + await this.stateService.setSettings(null, { userId: userId }); + } + + // Helpers + + private async getSettings(): Promise { + const settings = await this.stateService.getSettings(); + if (settings == null) { + const userId = await this.stateService.getUserId(); + } + return settings; + } + + private async getSettingsKey(key: string): Promise { + const settings = await this.getSettings(); + if (settings != null && settings[key]) { + return settings[key]; + } + return null; + } + + private async setSettingsKey(key: string, value: any): Promise { + let settings = await this.getSettings(); + if (!settings) { + settings = {}; } - clearCache(): void { - this.settingsCache = null; - } - - getEquivalentDomains(): Promise { - return this.getSettingsKey(Keys.equivalentDomains); - } - - async setEquivalentDomains(equivalentDomains: string[][]): Promise { - await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains); - } - - async clear(userId: string): Promise { - await this.storageService.remove(Keys.settingsPrefix + userId); - this.clearCache(); - } - - // Helpers - - private async getSettings(): Promise { - if (this.settingsCache == null) { - const userId = await this.userService.getUserId(); - this.settingsCache = this.storageService.get(Keys.settingsPrefix + userId); - } - return this.settingsCache; - } - - private async getSettingsKey(key: string): Promise { - const settings = await this.getSettings(); - if (settings != null && settings[key]) { - return settings[key]; - } - return null; - } - - private async setSettingsKey(key: string, value: any): Promise { - const userId = await this.userService.getUserId(); - let settings = await this.getSettings(); - if (!settings) { - settings = {}; - } - - settings[key] = value; - await this.storageService.save(Keys.settingsPrefix + userId, settings); - this.settingsCache = settings; - } + settings[key] = value; + await this.stateService.setSettings(settings); + } } diff --git a/common/src/services/state.service.ts b/common/src/services/state.service.ts index 03a09e30..8fc30f8b 100644 --- a/common/src/services/state.service.ts +++ b/common/src/services/state.service.ts @@ -1,27 +1,2360 @@ -import { StateService as StateServiceAbstraction } from '../abstractions/state.service'; +import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; + +import { Account } from "../models/domain/account"; + +import { LogService } from "../abstractions/log.service"; +import { StorageService } from "../abstractions/storage.service"; + +import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; +import { KdfType } from "../enums/kdfType"; +import { StorageLocation } from "../enums/storageLocation"; +import { UriMatchType } from "../enums/uriMatchType"; + +import { CipherView } from "../models/view/cipherView"; +import { CollectionView } from "../models/view/collectionView"; +import { FolderView } from "../models/view/folderView"; +import { SendView } from "../models/view/sendView"; + +import { EncString } from "../models/domain/encString"; +import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; +import { GlobalState } from "../models/domain/globalState"; +import { Policy } from "../models/domain/policy"; +import { State } from "../models/domain/state"; +import { StorageOptions } from "../models/domain/storageOptions"; +import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; + +import { CipherData } from "../models/data/cipherData"; +import { CollectionData } from "../models/data/collectionData"; +import { EventData } from "../models/data/eventData"; +import { FolderData } from "../models/data/folderData"; +import { OrganizationData } from "../models/data/organizationData"; +import { PolicyData } from "../models/data/policyData"; +import { ProviderData } from "../models/data/providerData"; +import { SendData } from "../models/data/sendData"; + +import { BehaviorSubject } from "rxjs"; + +import { StateMigrationService } from "./stateMigration.service"; export class StateService implements StateServiceAbstraction { - private state: any = {}; + accounts = new BehaviorSubject<{ [userId: string]: Account }>({}); + activeAccount = new BehaviorSubject(null); - get(key: string): Promise { - if (this.state.hasOwnProperty(key)) { - return Promise.resolve(this.state[key]); + private state: State = new State(); + + constructor( + private storageService: StorageService, + private secureStorageService: StorageService, + private logService: LogService, + private stateMigrationService: StateMigrationService + ) {} + + async init(): Promise { + if (await this.stateMigrationService.needsMigration()) { + await this.stateMigrationService.migrate(); + } + if (this.state.activeUserId == null) { + await this.loadStateFromDisk(); + } + } + + async loadStateFromDisk() { + if ((await this.getActiveUserIdFromStorage()) != null) { + const diskState = await this.storageService.get( + "state", + await this.defaultOnDiskOptions() + ); + this.state = diskState; + await this.pruneInMemoryAccounts(); + await this.saveStateToStorage(this.state, await this.defaultOnDiskMemoryOptions()); + await this.pushAccounts(); + } + } + + async addAccount(account: Account) { + if (account?.profile?.userId == null) { + return; + } + this.state.accounts[account.profile.userId] = account; + await this.scaffoldNewAccountStorage(account); + await this.setActiveUser(account.profile.userId); + this.activeAccount.next(account.profile.userId); + } + + async setActiveUser(userId: string): Promise { + this.state.activeUserId = userId; + const storedState = await this.storageService.get( + "state", + await this.defaultOnDiskOptions() + ); + storedState.activeUserId = userId; + await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions()); + await this.pushAccounts(); + this.activeAccount.next(this.state.activeUserId); + } + + async clean(options?: StorageOptions): Promise { + // Find and set the next active user if any exists + if (options?.userId == null || options.userId === (await this.getUserId())) { + for (const userId in this.state.accounts) { + if (userId == null) { + continue; } - return Promise.resolve(null); + if (await this.getIsAuthenticated({ userId: userId })) { + await this.setActiveUser(userId); + break; + } + await this.setActiveUser(null); + } } - save(key: string, obj: any): Promise { - this.state[key] = obj; - return Promise.resolve(); + await this.removeAccountFromSessionStorage(options?.userId); + await this.removeAccountFromLocalStorage(options?.userId); + await this.removeAccountFromSecureStorage(options?.userId); + this.removeAccountFromMemory(options?.userId); + await this.pushAccounts(); + } + + async getAccessToken(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.tokens?.accessToken; + } + + async setAccessToken(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.tokens.accessToken = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getAddEditCipherInfo(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.addEditCipherInfo; + } + + async setAddEditCipherInfo(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.addEditCipherInfo = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getAlwaysShowDock(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.alwaysShowDock ?? false + ); + } + + async setAlwaysShowDock(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.alwaysShowDock = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getApiKeyClientId(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.apiKeyClientId; + } + + async setApiKeyClientId(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.profile.apiKeyClientId = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getApiKeyClientSecret(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.keys?.apiKeyClientSecret; + } + + async setApiKeyClientSecret(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.keys.apiKeyClientSecret = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getAutoConfirmFingerPrints(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.autoConfirmFingerPrints ?? true + ); + } + + async setAutoConfirmFingerprints(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.autoConfirmFingerPrints = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getAutoFillOnPageLoadDefault(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.autoFillOnPageLoadDefault ?? false + ); + } + + async setAutoFillOnPageLoadDefault(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.autoFillOnPageLoadDefault = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getBiometricAwaitingAcceptance(options?: StorageOptions): Promise { + return ( + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.biometricAwaitingAcceptance ?? false + ); + } + + async setBiometricAwaitingAcceptance(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.biometricAwaitingAcceptance = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getBiometricFingerprintValidated(options?: StorageOptions): Promise { + return ( + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.biometricFingerprintValidated ?? false + ); + } + + async setBiometricFingerprintValidated(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.biometricFingerprintValidated = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getBiometricLocked(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.settings + ?.biometricLocked ?? false + ); + } + + async setBiometricLocked(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.settings.biometricLocked = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getBiometricText(options?: StorageOptions): Promise { + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.biometricText; + } + + async setBiometricText(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.biometricText = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getBiometricUnlock(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.biometricUnlock ?? false + ); + } + + async setBiometricUnlock(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.biometricUnlock = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getCanAccessPremium(options?: StorageOptions): Promise { + if (!(await this.getIsAuthenticated(options))) { + return false; } - remove(key: string): Promise { - delete this.state[key]; - return Promise.resolve(); + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + if (account.profile.hasPremiumPersonally) { + return true; } - purge(): Promise { - this.state = {}; - return Promise.resolve(); + const organizations = await this.getOrganizations(options); + if (organizations == null) { + return false; } + + for (const id of Object.keys(organizations)) { + const o = organizations[id]; + if (o.enabled && o.usersGetPremium && !o.isProviderUser) { + return true; + } + } + + return false; + } + + async getClearClipboard(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.settings?.clearClipboard; + } + + async setClearClipboard(value: number, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + account.settings.clearClipboard = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getCollapsedGroupings(options?: StorageOptions): Promise> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.collapsedGroupings; + } + + async setCollapsedGroupings(value: Set, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.collapsedGroupings = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getConvertAccountToKeyConnector(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.convertAccountToKeyConnector; + } + + async setConvertAccountToKeyConnector(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.profile.convertAccountToKeyConnector = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getCryptoMasterKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.keys?.cryptoMasterKey; + } + + async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.keys.cryptoMasterKey = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getCryptoMasterKeyAuto(options?: StorageOptions): Promise { + options = this.reconcileOptions( + this.reconcileOptions(options, { keySuffix: "auto" }), + await this.defaultSecureStorageOptions() + ); + if (options?.userId == null) { + return null; + } + return await this.secureStorageService.get(`${options.userId}_masterkey_auto`, options); + } + + async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise { + options = this.reconcileOptions( + this.reconcileOptions(options, { keySuffix: "auto" }), + await this.defaultSecureStorageOptions() + ); + if (options?.userId == null) { + return; + } + await this.secureStorageService.save(`${options.userId}_masterkey_auto`, value, options); + } + + async getCryptoMasterKeyB64(options?: StorageOptions): Promise { + options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + return null; + } + return await this.secureStorageService.get(`${options?.userId}_masterkey`, options); + } + + async setCryptoMasterKeyB64(value: string, options?: StorageOptions): Promise { + options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); + if (options?.userId == null) { + return; + } + await this.secureStorageService.save(`${options.userId}_masterkey`, value, options); + } + + async getCryptoMasterKeyBiometric(options?: StorageOptions): Promise { + options = this.reconcileOptions( + this.reconcileOptions(options, { keySuffix: "biometric" }), + await this.defaultSecureStorageOptions() + ); + if (options?.userId == null) { + return null; + } + return await this.secureStorageService.get(`${options.userId}_masterkey_biometric`, options); + } + + async hasCryptoMasterKeyBiometric(options?: StorageOptions): Promise { + options = this.reconcileOptions( + this.reconcileOptions(options, { keySuffix: "biometric" }), + await this.defaultSecureStorageOptions() + ); + if (options?.userId == null) { + return false; + } + return await this.secureStorageService.has(`${options.userId}_masterkey_biometric`, options); + } + + async setCryptoMasterKeyBiometric(value: string, options?: StorageOptions): Promise { + options = this.reconcileOptions( + this.reconcileOptions(options, { keySuffix: "biometric" }), + await this.defaultSecureStorageOptions() + ); + if (options?.userId == null) { + return; + } + await this.secureStorageService.save(`${options.userId}_masterkey_biometric`, value, options); + } + + async getDecodedToken(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.tokens?.decodedToken; + } + + async setDecodedToken(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.tokens.decodedToken = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedCiphers(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.ciphers?.decrypted; + } + + async setDecryptedCiphers(value: CipherView[], options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.ciphers.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedCollections(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.collections?.decrypted; + } + + async setDecryptedCollections(value: CollectionView[], options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.collections.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.keys?.cryptoSymmetricKey?.decrypted; + } + + async setDecryptedCryptoSymmetricKey( + value: SymmetricCryptoKey, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.keys.cryptoSymmetricKey.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedFolders(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.folders?.decrypted; + } + + async setDecryptedFolders(value: FolderView[], options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.folders.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedOrganizationKeys( + options?: StorageOptions + ): Promise> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.keys?.organizationKeys?.decrypted; + } + + async setDecryptedOrganizationKeys( + value: Map, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.keys.organizationKeys.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedPasswordGenerationHistory( + options?: StorageOptions + ): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.passwordGenerationHistory?.decrypted; + } + + async setDecryptedPasswordGenerationHistory( + value: GeneratedPasswordHistory[], + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.passwordGenerationHistory.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedPinProtected(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.settings?.pinProtected?.decrypted; + } + + async setDecryptedPinProtected(value: EncString, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.settings.pinProtected.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedPolicies(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.policies?.decrypted; + } + + async setDecryptedPolicies(value: Policy[], options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.policies.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedPrivateKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.keys?.privateKey?.decrypted; + } + + async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.keys.privateKey.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedProviderKeys( + options?: StorageOptions + ): Promise> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.keys?.providerKeys?.decrypted; + } + + async setDecryptedProviderKeys( + value: Map, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.keys.providerKeys.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDecryptedSends(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.sends?.decrypted; + } + + async setDecryptedSends(value: SendView[], options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.sends.decrypted = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getDefaultUriMatch(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.settings?.defaultUriMatch; + } + + async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.defaultUriMatch = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDisableAddLoginNotification(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.disableAddLoginNotification ?? false + ); + } + + async setDisableAddLoginNotification(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.disableAddLoginNotification = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDisableAutoBiometricsPrompt(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.disableAutoBiometricsPrompt ?? false + ); + } + + async setDisableAutoBiometricsPrompt(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.disableAutoBiometricsPrompt = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDisableAutoTotpCopy(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.disableAutoTotpCopy ?? false + ); + } + + async setDisableAutoTotpCopy(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.disableAutoTotpCopy = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDisableBadgeCounter(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.disableBadgeCounter ?? false + ); + } + + async setDisableBadgeCounter(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.disableBadgeCounter = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDisableChangedPasswordNotification(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.disableChangedPasswordNotification ?? false + ); + } + + async setDisableChangedPasswordNotification( + value: boolean, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.disableChangedPasswordNotification = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDisableContextMenuItem(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.disableContextMenuItem ?? false + ); + } + + async setDisableContextMenuItem(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.disableContextMenuItem = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDisableFavicon(options?: StorageOptions): Promise { + return ( + ( + await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ) + )?.disableFavicon ?? false + ); + } + + async setDisableFavicon(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + globals.disableFavicon = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getDisableGa(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.disableGa ?? false + ); + } + + async setDisableGa(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.disableGa = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDontShowCardsCurrentTab(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.dontShowCardsCurrentTab ?? false + ); + } + + async setDontShowCardsCurrentTab(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.dontShowCardsCurrentTab = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getDontShowIdentitiesCurrentTab(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.dontShowIdentitiesCurrentTab ?? false + ); + } + + async setDontShowIdentitiesCurrentTab(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.dontShowIdentitiesCurrentTab = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEmail(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.email; + } + + async setEmail(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.profile.email = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEmailVerified(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.profile.emailVerified ?? false + ); + } + + async setEmailVerified(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.profile.emailVerified = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableAlwaysOnTop(options?: StorageOptions): Promise { + const accountPreference = ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.settings?.enableAlwaysOnTop; + const globalPreference = ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.enableAlwaysOnTop; + return accountPreference ?? globalPreference ?? false; + } + + async setEnableAlwaysOnTop(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.enableAlwaysOnTop = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.enableAlwaysOnTop = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableAutoFillOnPageLoad(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.enableAutoFillOnPageLoad ?? false + ); + } + + async setEnableAutoFillOnPageLoad(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.enableAutoFillOnPageLoad = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableBiometric(options?: StorageOptions): Promise { + return ( + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.enableBiometrics ?? false + ); + } + + async setEnableBiometric(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.enableBiometrics = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableBrowserIntegration(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.enableBrowserIntegration ?? false + ); + } + + async setEnableBrowserIntegration(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.enableBrowserIntegration = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableBrowserIntegrationFingerprint(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.enableBrowserIntegrationFingerprint ?? false + ); + } + + async setEnableBrowserIntegrationFingerprint( + value: boolean, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.enableBrowserIntegrationFingerprint = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableCloseToTray(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.enableCloseToTray ?? false + ); + } + + async setEnableCloseToTray(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.enableCloseToTray = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableFullWidth(options?: StorageOptions): Promise { + return ( + ( + await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ) + )?.settings?.enableFullWidth ?? false + ); + } + + async setEnableFullWidth(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + account.settings.enableFullWidth = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getEnableGravitars(options?: StorageOptions): Promise { + return ( + ( + await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ) + )?.settings?.enableGravitars ?? false + ); + } + + async setEnableGravitars(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + account.settings.enableGravitars = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getEnableMinimizeToTray(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.enableMinimizeToTray ?? false + ); + } + + async setEnableMinimizeToTray(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.enableMinimizeToTray = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableStartToTray(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings.enableStartToTray ?? false + ); + } + + async setEnableStartToTray(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.enableStartToTray = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEnableTray(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.enableTray ?? false + ); + } + + async setEnableTray(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.enableTray = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) + )?.data?.ciphers?.encrypted; + } + + async setEncryptedCiphers( + value: { [id: string]: CipherData }, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + account.data.ciphers.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + } + + async getEncryptedCollections( + options?: StorageOptions + ): Promise<{ [id: string]: CollectionData }> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) + )?.data?.collections?.encrypted; + } + + async setEncryptedCollections( + value: { [id: string]: CollectionData }, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + account.data.collections.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + } + + async getEncryptedCryptoSymmetricKey(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.keys.cryptoSymmetricKey.encrypted; + } + + async setEncryptedCryptoSymmetricKey(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.keys.cryptoSymmetricKey.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEncryptedFolders(options?: StorageOptions): Promise<{ [id: string]: FolderData }> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) + )?.data?.folders?.encrypted; + } + + async setEncryptedFolders( + value: { [id: string]: FolderData }, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + account.data.folders.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + } + + async getEncryptedOrganizationKeys(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.keys?.organizationKeys.encrypted; + } + + async setEncryptedOrganizationKeys( + value: Map, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.keys.organizationKeys.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEncryptedPasswordGenerationHistory( + options?: StorageOptions + ): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.data?.passwordGenerationHistory?.encrypted; + } + + async setEncryptedPasswordGenerationHistory( + value: GeneratedPasswordHistory[], + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.data.passwordGenerationHistory.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEncryptedPinProtected(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.settings?.pinProtected?.encrypted; + } + + async setEncryptedPinProtected(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.pinProtected.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEncryptedPolicies(options?: StorageOptions): Promise<{ [id: string]: PolicyData }> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.data?.policies?.encrypted; + } + + async setEncryptedPolicies( + value: { [id: string]: PolicyData }, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.data.policies.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEncryptedPrivateKey(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.keys?.privateKey?.encrypted; + } + + async setEncryptedPrivateKey(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.keys.privateKey.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEncryptedProviderKeys(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.keys?.providerKeys?.encrypted; + } + + async setEncryptedProviderKeys(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.keys.providerKeys.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) + )?.data?.sends.encrypted; + } + + async setEncryptedSends( + value: { [id: string]: SendData }, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + account.data.sends.encrypted = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + } + + async getEntityId(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.profile?.entityId; + } + + async setEntityId(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.profile.entityId = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getEntityType(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.profile?.entityType; + } + + async setEntityType(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.profile.entityType = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getEnvironmentUrls(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.environmentUrls ?? { + server: "bitwarden.com", + } + ); + } + + async setEnvironmentUrls(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.environmentUrls = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEquivalentDomains(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.settings?.equivalentDomains; + } + + async setEquivalentDomains(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.equivalentDomains = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEventCollection(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.data?.eventCollection; + } + + async setEventCollection(value: EventData[], options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.data.eventCollection = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getEverBeenUnlocked(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile + ?.everBeenUnlocked ?? false + ); + } + + async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.profile.everBeenUnlocked = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getForcePasswordReset(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile + ?.forcePasswordReset ?? false + ); + } + + async setForcePasswordReset(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.profile.forcePasswordReset = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getInstalledVersion(options?: StorageOptions): Promise { + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.installedVersion; + } + + async setInstalledVersion(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.installedVersion = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getIsAuthenticated(options?: StorageOptions): Promise { + return (await this.getAccessToken(options)) != null && (await this.getUserId(options)) != null; + } + + async getKdfIterations(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.kdfIterations; + } + + async setKdfIterations(value: number, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.profile.kdfIterations = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getKdfType(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.kdfType; + } + + async setKdfType(value: KdfType, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.profile.kdfType = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getKeyHash(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.keyHash; + } + + async setKeyHash(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.profile.keyHash = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getLastActive(options?: StorageOptions): Promise { + const lastActive = ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.lastActive; + return ( + lastActive ?? + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.lastActive + ); + } + + async setLastActive(value: number, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + if (account != null) { + account.profile.lastActive = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.lastActive = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getLastSync(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) + )?.profile?.lastSync; + } + + async setLastSync(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + account.profile.lastSync = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + } + + async getLegacyEtmKey(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.keys?.legacyEtmKey; + } + + async setLegacyEtmKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.keys.legacyEtmKey = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getLocalData(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.localData; + } + + async setLocalData(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.localData = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getLocale(options?: StorageOptions): Promise { + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.locale; + } + + async setLocale(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + globals.locale = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getLoginRedirect(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.loginRedirect; + } + + async setLoginRedirect(value: any, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + globals.loginRedirect = value; + await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getMainWindowSize(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.mainWindowSize; + } + + async setMainWindowSize(value: number, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + globals.mainWindowSize = value; + await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.minimizeOnCopyToClipboard ?? false + ); + } + + async setMinimizeOnCopyToClipboard(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.minimizeOnCopyToClipboard = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: any }> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.settings?.neverDomains; + } + + async setNeverDomains(value: { [id: string]: any }, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.neverDomains = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getNoAutoPromptBiometrics(options?: StorageOptions): Promise { + return ( + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.noAutoPromptBiometrics ?? false + ); + } + + async setNoAutoPromptBiometrics(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.noAutoPromptBiometrics = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getNoAutoPromptBiometricsText(options?: StorageOptions): Promise { + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.noAutoPromptBiometricsText; + } + + async setNoAutoPromptBiometricsText(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.noAutoPromptBiometricsText = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getOpenAtLogin(options?: StorageOptions): Promise { + return ( + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.openAtLogin ?? false + ); + } + + async setOpenAtLogin(value: boolean, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.openAtLogin = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getOrganizationInvitation(options?: StorageOptions): Promise { + return (await this.getGlobals(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.organizationInvitation; + } + + async setOrganizationInvitation(value: any, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + globals.organizationInvitation = value; + await this.saveGlobals(globals, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getOrganizations(options?: StorageOptions): Promise<{ [id: string]: OrganizationData }> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.data?.organizations; + } + + async setOrganizations( + value: { [id: string]: OrganizationData }, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.data.organizations = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getPasswordGenerationOptions(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.settings?.passwordGenerationOptions; + } + + async setPasswordGenerationOptions(value: any, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.passwordGenerationOptions = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getProtectedPin(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.settings?.protectedPin; + } + + async setProtectedPin(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.protectedPin = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getProviders(options?: StorageOptions): Promise<{ [id: string]: ProviderData }> { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.data?.providers; + } + + async setProviders( + value: { [id: string]: ProviderData }, + options?: StorageOptions + ): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.data.providers = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getPublicKey(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.keys?.publicKey; + } + + async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.keys.publicKey = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getRefreshToken(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.tokens?.refreshToken; + } + + async setRefreshToken(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.tokens.refreshToken = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + async getRememberedEmail(options?: StorageOptions): Promise { + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.rememberedEmail; + } + + async setRememberedEmail(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + globals.rememberedEmail = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getSecurityStamp(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.tokens?.securityStamp; + } + + async setSecurityStamp(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.tokens.securityStamp = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getSettings(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) + )?.settings?.settings; + } + + async setSettings(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + account.settings.settings = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) + ); + } + + async getSsoCodeVerifier(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.profile?.ssoCodeVerifier; + } + + async setSsoCodeVerifier(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.profile.ssoCodeVerifier = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getSsoOrgIdentifier(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.profile?.ssoOrganizationIdentifier; + } + + async setSsoOrganizationIdentifier(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + account.profile.ssoOrganizationIdentifier = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getSsoState(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.profile?.ssoState; + } + + async setSsoState(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.profile.ssoState = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getTheme(options?: StorageOptions): Promise { + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.theme; + } + + async setTheme(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + globals.theme = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getTwoFactorToken(options?: StorageOptions): Promise { + return ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.twoFactorToken; + } + + async setTwoFactorToken(value: string, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + globals.twoFactorToken = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getUserId(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.userId; + } + + async getUsesKeyConnector(options?: StorageOptions): Promise { + return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions))) + ?.profile?.usesKeyConnector; + } + + async setUsesKeyConnector(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, this.defaultInMemoryOptions) + ); + account.profile.usesKeyConnector = value; + await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions)); + } + + async getVaultTimeout(options?: StorageOptions): Promise { + const accountVaultTimeout = ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.settings?.vaultTimeout; + const globalVaultTimeout = ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.vaultTimeout; + return accountVaultTimeout ?? globalVaultTimeout ?? 15; + } + + async setVaultTimeout(value: number, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + account.settings.vaultTimeout = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getVaultTimeoutAction(options?: StorageOptions): Promise { + const accountVaultTimeoutAction = ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.settings?.vaultTimeoutAction; + const globalVaultTimeoutAction = ( + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) + )?.vaultTimeoutAction; + return accountVaultTimeoutAction ?? globalVaultTimeoutAction; + } + + async setVaultTimeoutAction(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + account.settings.vaultTimeoutAction = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) + ); + } + + async getStateVersion(): Promise { + return (await this.getGlobals(await this.defaultOnDiskLocalOptions())).stateVersion ?? 1; + } + + async setStateVersion(value: number): Promise { + const globals = await this.getGlobals(await this.defaultOnDiskOptions()); + globals.stateVersion = value; + await this.saveGlobals(globals, await this.defaultOnDiskOptions()); + } + + async getWindow(): Promise> { + const globals = await this.getGlobals(await this.defaultOnDiskOptions()); + return globals?.window != null && Object.keys(globals.window).length > 0 + ? globals.window + : new Map(); + } + + async setWindow(value: Map, options?: StorageOptions): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + globals.window = value; + return await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + + private async getGlobals(options: StorageOptions): Promise { + let globals: GlobalState; + if (this.useMemory(options.storageLocation)) { + globals = this.getGlobalsFromMemory(); + } + + if (this.useDisk && globals == null) { + globals = await this.getGlobalsFromDisk(options); + } + + return globals ?? new GlobalState(); + } + + private async saveGlobals(globals: GlobalState, options: StorageOptions) { + return this.useMemory(options.storageLocation) + ? this.saveGlobalsToMemory(globals) + : await this.saveGlobalsToDisk(globals, options); + } + + private getGlobalsFromMemory(): GlobalState { + return this.state.globals; + } + + private async getGlobalsFromDisk(options: StorageOptions): Promise { + return (await this.storageService.get("state", options))?.globals; + } + + private saveGlobalsToMemory(globals: GlobalState): void { + this.state.globals = globals; + } + + private async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise { + if (options.useSecureStorage) { + const state = (await this.secureStorageService.get("state", options)) ?? new State(); + state.globals = globals; + await this.secureStorageService.save("state", state, options); + } else { + const state = (await this.storageService.get("state", options)) ?? new State(); + state.globals = globals; + await this.saveStateToStorage(state, options); + } + } + + private async getAccount(options: StorageOptions): Promise { + try { + let account: Account; + if (this.useMemory(options.storageLocation)) { + account = this.getAccountFromMemory(options); + } + + if (this.useDisk(options.storageLocation) && account == null) { + account = await this.getAccountFromDisk(options); + } + + return account != null ? new Account(account) : null; + } catch (e) { + this.logService.error(e); + } + } + + private getAccountFromMemory(options: StorageOptions): Account { + if (this.state.accounts == null) { + return null; + } + return this.state.accounts[this.getUserIdFromMemory(options)]; + } + + private getUserIdFromMemory(options: StorageOptions): string { + return options?.userId != null + ? this.state.accounts[options.userId]?.profile?.userId + : this.state.activeUserId; + } + + private async getAccountFromDisk(options: StorageOptions): Promise { + if (options?.userId == null && this.state.activeUserId == null) { + return null; + } + + const state = options?.useSecureStorage + ? (await this.secureStorageService.get("state", options)) ?? + (await this.storageService.get( + "state", + this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }) + )) + : await this.storageService.get("state", options); + + return state?.accounts[options?.userId ?? this.state.activeUserId]; + } + + private useMemory(storageLocation: StorageLocation) { + return storageLocation === StorageLocation.Memory || storageLocation === StorageLocation.Both; + } + + private useDisk(storageLocation: StorageLocation) { + return storageLocation === StorageLocation.Disk || storageLocation === StorageLocation.Both; + } + + private async saveAccount( + account: Account, + options: StorageOptions = { + storageLocation: StorageLocation.Both, + useSecureStorage: false, + } + ) { + return this.useMemory(options.storageLocation) + ? await this.saveAccountToMemory(account) + : await this.saveAccountToDisk(account, options); + } + + private async saveAccountToDisk(account: Account, options: StorageOptions): Promise { + const storageLocation = options.useSecureStorage + ? this.secureStorageService + : this.storageService; + + const state = (await storageLocation.get("state", options)) ?? new State(); + state.accounts[account.profile.userId] = account; + + await storageLocation.save("state", state, options); + await this.pushAccounts(); + } + + private async saveAccountToMemory(account: Account): Promise { + if (this.getAccountFromMemory({ userId: account.profile.userId }) !== null) { + this.state.accounts[account.profile.userId] = account; + } + await this.pushAccounts(); + } + + private async scaffoldNewAccountStorage(account: Account): Promise { + await this.scaffoldNewAccountLocalStorage(account); + await this.scaffoldNewAccountSessionStorage(account); + await this.scaffoldNewAccountMemoryStorage(account); + } + + private async scaffoldNewAccountLocalStorage(account: Account): Promise { + const storedState = + (await this.storageService.get("state", await this.defaultOnDiskLocalOptions())) ?? + new State(); + const storedAccount = storedState.accounts[account.profile.userId]; + if (storedAccount != null) { + account = { + settings: storedAccount.settings, + profile: account.profile, + tokens: account.tokens, + keys: account.keys, + data: account.data, + }; + } + storedState.accounts[account.profile.userId] = account; + await this.saveStateToStorage(storedState, await this.defaultOnDiskLocalOptions()); + } + + private async scaffoldNewAccountMemoryStorage(account: Account): Promise { + const storedState = + (await this.storageService.get("state", await this.defaultOnDiskMemoryOptions())) ?? + new State(); + const storedAccount = storedState.accounts[account.profile.userId]; + if (storedAccount != null) { + account = { + settings: storedAccount.settings, + profile: account.profile, + tokens: account.tokens, + keys: account.keys, + data: account.data, + }; + } + storedState.accounts[account.profile.userId] = account; + await this.saveStateToStorage(storedState, await this.defaultOnDiskMemoryOptions()); + } + + private async scaffoldNewAccountSessionStorage(account: Account): Promise { + const storedState = + (await this.storageService.get("state", await this.defaultOnDiskOptions())) ?? + new State(); + const storedAccount = storedState.accounts[account.profile.userId]; + if (storedAccount != null) { + account = { + settings: storedAccount.settings, + profile: account.profile, + tokens: account.tokens, + keys: account.keys, + data: account.data, + }; + } + storedState.accounts[account.profile.userId] = account; + await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions()); + } + + private async pushAccounts(): Promise { + await this.pruneInMemoryAccounts(); + if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) { + this.accounts.next(null); + return; + } + + this.accounts.next(this.state.accounts); + } + + private reconcileOptions( + requestedOptions: StorageOptions, + defaultOptions: StorageOptions + ): StorageOptions { + if (requestedOptions == null) { + return defaultOptions; + } + requestedOptions.userId = requestedOptions?.userId ?? defaultOptions.userId; + requestedOptions.storageLocation = + requestedOptions?.storageLocation ?? defaultOptions.storageLocation; + requestedOptions.useSecureStorage = + requestedOptions?.useSecureStorage ?? defaultOptions.useSecureStorage; + requestedOptions.htmlStorageLocation = + requestedOptions?.htmlStorageLocation ?? defaultOptions.htmlStorageLocation; + requestedOptions.keySuffix = requestedOptions?.keySuffix ?? defaultOptions.keySuffix; + return requestedOptions; + } + + private get defaultInMemoryOptions(): StorageOptions { + return { storageLocation: StorageLocation.Memory, userId: this.state.activeUserId }; + } + + private async defaultOnDiskOptions(): Promise { + return { + storageLocation: StorageLocation.Disk, + htmlStorageLocation: HtmlStorageLocation.Session, + userId: await this.getActiveUserIdFromStorage(), + useSecureStorage: false, + }; + } + + private async defaultOnDiskLocalOptions(): Promise { + return { + storageLocation: StorageLocation.Disk, + htmlStorageLocation: HtmlStorageLocation.Local, + userId: await this.getActiveUserIdFromStorage(), + useSecureStorage: false, + }; + } + + private async defaultOnDiskMemoryOptions(): Promise { + return { + storageLocation: StorageLocation.Disk, + htmlStorageLocation: HtmlStorageLocation.Memory, + userId: await this.getUserId(), + useSecureStorage: false, + }; + } + + private async defaultSecureStorageOptions(): Promise { + return { + storageLocation: StorageLocation.Disk, + useSecureStorage: true, + userId: await this.getActiveUserIdFromStorage(), + }; + } + + private async getActiveUserIdFromStorage(): Promise { + const state = await this.storageService.get("state"); + return state?.activeUserId; + } + + private async removeAccountFromLocalStorage( + userId: string = this.state.activeUserId + ): Promise { + const state = await this.storageService.get("state", { + htmlStorageLocation: HtmlStorageLocation.Local, + }); + if (state?.accounts[userId] == null) { + return; + } + + state.accounts[userId] = new Account({ + settings: state.accounts[userId].settings, + }); + + await this.saveStateToStorage(state, await this.defaultOnDiskLocalOptions()); + } + + private async removeAccountFromSessionStorage( + userId: string = this.state.activeUserId + ): Promise { + const state = await this.storageService.get("state", { + htmlStorageLocation: HtmlStorageLocation.Session, + }); + if (state?.accounts[userId] == null) { + return; + } + + state.accounts[userId] = new Account({ + settings: state.accounts[userId].settings, + }); + + await this.saveStateToStorage(state, await this.defaultOnDiskOptions()); + } + + private async removeAccountFromSecureStorage( + userId: string = this.state.activeUserId + ): Promise { + await this.setCryptoMasterKeyAuto(null, { userId: userId }); + await this.setCryptoMasterKeyBiometric(null, { userId: userId }); + await this.setCryptoMasterKeyB64(null, { userId: userId }); + } + + private removeAccountFromMemory(userId: string = this.state.activeUserId): void { + delete this.state.accounts[userId]; + } + + private async saveStateToStorage(state: State, options: StorageOptions): Promise { + await this.storageService.save("state", state, options); + } + + private async pruneInMemoryAccounts() { + // We preserve settings for logged out accounts, but we don't want to consider them when thinking about active account state + for (const userId in this.state.accounts) { + if (!(await this.getIsAuthenticated({ userId: userId }))) { + delete this.state.accounts[userId]; + } + } + } } diff --git a/common/src/services/stateMigration.service.ts b/common/src/services/stateMigration.service.ts new file mode 100644 index 00000000..bab3667f --- /dev/null +++ b/common/src/services/stateMigration.service.ts @@ -0,0 +1,513 @@ +import { StorageService } from "../abstractions/storage.service"; + +import { Account } from "../models/domain/account"; +import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; +import { State } from "../models/domain/state"; +import { StorageOptions } from "../models/domain/storageOptions"; + +import { CipherData } from "../models/data/cipherData"; +import { CollectionData } from "../models/data/collectionData"; +import { EventData } from "../models/data/eventData"; +import { FolderData } from "../models/data/folderData"; +import { OrganizationData } from "../models/data/organizationData"; +import { PolicyData } from "../models/data/policyData"; +import { ProviderData } from "../models/data/providerData"; +import { SendData } from "../models/data/sendData"; + +import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; +import { KdfType } from "../enums/kdfType"; + +// Originally (before January 2022) storage was handled as a flat key/value pair store. +// With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration. +const v1Keys = { + accessToken: "accessToken", + alwaysShowDock: "alwaysShowDock", + autoConfirmFingerprints: "autoConfirmFingerprints", + autoFillOnPageLoadDefault: "autoFillOnPageLoadDefault", + biometricAwaitingAcceptance: "biometricAwaitingAcceptance", + biometricFingerprintValidated: "biometricFingerprintValidated", + biometricText: "biometricText", + biometricUnlock: "biometric", + clearClipboard: "clearClipboardKey", + clientId: "clientId", + clientSecret: "clientSecret", + collapsedGroupings: "collapsedGroupings", + convertAccountToKeyConnector: "convertAccountToKeyConnector", + defaultUriMatch: "defaultUriMatch", + disableAddLoginNotification: "disableAddLoginNotification", + disableAutoBiometricsPrompt: "noAutoPromptBiometrics", + disableAutoTotpCopy: "disableAutoTotpCopy", + disableBadgeCounter: "disableBadgeCounter", + disableChangedPasswordNotification: "disableChangedPasswordNotification", + disableContextMenuItem: "disableContextMenuItem", + disableFavicon: "disableFavicon", + disableGa: "disableGa", + dontShowCardsCurrentTab: "dontShowCardsCurrentTab", + dontShowIdentitiesCurrentTab: "dontShowIdentitiesCurrentTab", + emailVerified: "emailVerified", + enableAlwaysOnTop: "enableAlwaysOnTopKey", + enableAutoFillOnPageLoad: "enableAutoFillOnPageLoad", + enableBiometric: "enabledBiometric", + enableBrowserIntegration: "enableBrowserIntegration", + enableBrowserIntegrationFingerprint: "enableBrowserIntegrationFingerprint", + enableCloseToTray: "enableCloseToTray", + enableFullWidth: "enableFullWidth", + enableGravatars: "enableGravatars", + enableMinimizeToTray: "enableMinimizeToTray", + enableStartToTray: "enableStartToTrayKey", + enableTray: "enableTray", + encKey: "encKey", // Generated Symmetric Key + encOrgKeys: "encOrgKeys", + encPrivate: "encPrivateKey", + encProviderKeys: "encProviderKeys", + entityId: "entityId", + entityType: "entityType", + environmentUrls: "environmentUrls", + equivalentDomains: "equivalentDomains", + eventCollection: "eventCollection", + forcePasswordReset: "forcePasswordReset", + history: "generatedPasswordHistory", + installedVersion: "installedVersion", + kdf: "kdf", + kdfIterations: "kdfIterations", + key: "key", // Master Key + keyHash: "keyHash", + lastActive: "lastActive", + localData: "sitesLocalData", + locale: "locale", + mainWindowSize: "mainWindowSize", + minimizeOnCopyToClipboard: "minimizeOnCopyToClipboardKey", + neverDomains: "neverDomains", + noAutoPromptBiometricsText: "noAutoPromptBiometricsText", + openAtLogin: "openAtLogin", + passwordGenerationOptions: "passwordGenerationOptions", + pinProtected: "pinProtectedKey", + protectedPin: "protectedPin", + refreshToken: "refreshToken", + ssoCodeVerifier: "ssoCodeVerifier", + ssoIdentifier: "ssoOrgIdentifier", + ssoState: "ssoState", + stamp: "securityStamp", + theme: "theme", + userEmail: "userEmail", + userId: "userId", + usesConnector: "usesKeyConnector", + vaultTimeoutAction: "vaultTimeoutAction", + vaultTimeout: "lockOption", + rememberedEmail: "rememberedEmail", +}; + +const v1KeyPrefixes = { + ciphers: "ciphers_", + collections: "collections_", + folders: "folders_", + lastSync: "lastSync_", + policies: "policies_", + twoFactorToken: "twoFactorToken_", + organizations: "organizations_", + providers: "providers_", + sends: "sends_", + settings: "settings_", +}; + +export class StateMigrationService { + readonly latestVersion: number = 2; + + constructor( + private storageService: StorageService, + private secureStorageService: StorageService + ) {} + + async needsMigration(): Promise { + const currentStateVersion = (await this.storageService.get("state"))?.globals + ?.stateVersion; + return currentStateVersion == null || currentStateVersion < this.latestVersion; + } + + async migrate(): Promise { + let currentStateVersion = + (await this.storageService.get("state"))?.globals?.stateVersion ?? 1; + while (currentStateVersion < this.latestVersion) { + switch (currentStateVersion) { + case 1: + await this.migrateStateFrom1To2(); + break; + } + + currentStateVersion += 1; + } + } + + private async migrateStateFrom1To2(): Promise { + const options: StorageOptions = { htmlStorageLocation: HtmlStorageLocation.Local }; + const userId = await this.storageService.get("userId"); + const initialState: State = + userId == null + ? { + globals: { + stateVersion: 2, + }, + accounts: {}, + activeUserId: null, + } + : { + activeUserId: userId, + globals: { + biometricAwaitingAcceptance: await this.storageService.get( + v1Keys.biometricAwaitingAcceptance, + options + ), + biometricFingerprintValidated: await this.storageService.get( + v1Keys.biometricFingerprintValidated, + options + ), + biometricText: await this.storageService.get(v1Keys.biometricText, options), + disableFavicon: await this.storageService.get( + v1Keys.disableFavicon, + options + ), + enableAlwaysOnTop: await this.storageService.get( + v1Keys.enableAlwaysOnTop, + options + ), + enableBiometrics: await this.storageService.get( + v1Keys.enableBiometric, + options + ), + installedVersion: await this.storageService.get( + v1Keys.installedVersion, + options + ), + lastActive: await this.storageService.get(v1Keys.lastActive, options), + locale: await this.storageService.get(v1Keys.locale, options), + loginRedirect: null, + mainWindowSize: null, + noAutoPromptBiometrics: await this.storageService.get( + v1Keys.disableAutoBiometricsPrompt, + options + ), + noAutoPromptBiometricsText: await this.storageService.get( + v1Keys.noAutoPromptBiometricsText, + options + ), + openAtLogin: await this.storageService.get(v1Keys.openAtLogin, options), + organizationInvitation: await this.storageService.get("", options), + rememberedEmail: await this.storageService.get( + v1Keys.rememberedEmail, + options + ), + stateVersion: 2, + theme: await this.storageService.get(v1Keys.theme, options), + twoFactorToken: await this.storageService.get( + v1KeyPrefixes.twoFactorToken + userId, + options + ), + vaultTimeout: await this.storageService.get(v1Keys.vaultTimeout, options), + vaultTimeoutAction: await this.storageService.get( + v1Keys.vaultTimeoutAction, + options + ), + window: null, + }, + accounts: { + [userId]: new Account({ + data: { + addEditCipherInfo: null, + ciphers: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: CipherData }>( + v1KeyPrefixes.ciphers + userId, + options + ), + }, + collapsedGroupings: null, + collections: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: CollectionData }>( + v1KeyPrefixes.collections + userId, + options + ), + }, + eventCollection: await this.storageService.get( + v1Keys.eventCollection, + options + ), + folders: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: FolderData }>( + v1KeyPrefixes.folders + userId, + options + ), + }, + localData: null, + organizations: await this.storageService.get<{ [id: string]: OrganizationData }>( + v1KeyPrefixes.organizations + userId + ), + passwordGenerationHistory: { + decrypted: null, + encrypted: await this.storageService.get( + "TODO", + options + ), // TODO: Whats up here? + }, + policies: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: PolicyData }>( + v1KeyPrefixes.policies + userId, + options + ), + }, + providers: await this.storageService.get<{ [id: string]: ProviderData }>( + v1KeyPrefixes.providers + userId + ), + sends: { + decrypted: null, + encrypted: await this.storageService.get<{ [id: string]: SendData }>( + v1KeyPrefixes.sends, + options + ), + }, + }, + keys: { + apiKeyClientSecret: await this.storageService.get( + v1Keys.clientSecret, + options + ), + cryptoMasterKey: null, + cryptoMasterKeyAuto: null, + cryptoMasterKeyB64: null, + cryptoMasterKeyBiometric: null, + cryptoSymmetricKey: { + encrypted: await this.storageService.get(v1Keys.encKey, options), + decrypted: null, + }, + legacyEtmKey: null, + organizationKeys: { + decrypted: null, + encrypted: await this.storageService.get( + v1Keys.encOrgKeys + userId, + options + ), + }, + privateKey: { + decrypted: null, + encrypted: await this.storageService.get(v1Keys.encPrivate, options), + }, + providerKeys: { + decrypted: null, + encrypted: await this.storageService.get( + v1Keys.encProviderKeys + userId, + options + ), + }, + publicKey: null, + }, + profile: { + apiKeyClientId: await this.storageService.get(v1Keys.clientId, options), + authenticationStatus: null, + convertAccountToKeyConnector: await this.storageService.get( + v1Keys.convertAccountToKeyConnector, + options + ), + email: await this.storageService.get(v1Keys.userEmail, options), + emailVerified: await this.storageService.get( + v1Keys.emailVerified, + options + ), + entityId: null, + entityType: null, + everBeenUnlocked: null, + forcePasswordReset: null, + hasPremiumPersonally: null, + kdfIterations: await this.storageService.get( + v1Keys.kdfIterations, + options + ), + kdfType: await this.storageService.get(v1Keys.kdf, options), + keyHash: await this.storageService.get(v1Keys.keyHash, options), + lastActive: await this.storageService.get(v1Keys.lastActive, options), + lastSync: null, + ssoCodeVerifier: await this.storageService.get( + v1Keys.ssoCodeVerifier, + options + ), + ssoOrganizationIdentifier: await this.storageService.get( + v1Keys.ssoIdentifier, + options + ), + ssoState: null, + userId: userId, + usesKeyConnector: null, + }, + settings: { + alwaysShowDock: await this.storageService.get( + v1Keys.alwaysShowDock, + options + ), + autoConfirmFingerPrints: await this.storageService.get( + v1Keys.autoConfirmFingerprints, + options + ), + autoFillOnPageLoadDefault: await this.storageService.get( + v1Keys.autoFillOnPageLoadDefault, + options + ), + biometricLocked: null, + biometricUnlock: await this.storageService.get( + v1Keys.biometricUnlock, + options + ), + clearClipboard: await this.storageService.get( + v1Keys.clearClipboard, + options + ), + defaultUriMatch: await this.storageService.get( + v1Keys.defaultUriMatch, + options + ), + disableAddLoginNotification: await this.storageService.get( + v1Keys.disableAddLoginNotification, + options + ), + disableAutoBiometricsPrompt: await this.storageService.get( + v1Keys.disableAutoBiometricsPrompt, + options + ), + disableAutoTotpCopy: await this.storageService.get( + v1Keys.disableAutoTotpCopy, + options + ), + disableBadgeCounter: await this.storageService.get( + v1Keys.disableBadgeCounter, + options + ), + disableChangedPasswordNotification: await this.storageService.get( + v1Keys.disableChangedPasswordNotification, + options + ), + disableContextMenuItem: await this.storageService.get( + v1Keys.disableContextMenuItem, + options + ), + disableGa: await this.storageService.get(v1Keys.disableGa, options), + dontShowCardsCurrentTab: await this.storageService.get( + v1Keys.dontShowCardsCurrentTab, + options + ), + dontShowIdentitiesCurrentTab: await this.storageService.get( + v1Keys.dontShowIdentitiesCurrentTab, + options + ), + enableAlwaysOnTop: await this.storageService.get( + v1Keys.enableAlwaysOnTop, + options + ), + enableAutoFillOnPageLoad: await this.storageService.get( + v1Keys.enableAutoFillOnPageLoad, + options + ), + enableBiometric: await this.storageService.get( + v1Keys.enableBiometric, + options + ), + enableBrowserIntegration: await this.storageService.get( + v1Keys.enableBrowserIntegration, + options + ), + enableBrowserIntegrationFingerprint: await this.storageService.get( + v1Keys.enableBrowserIntegrationFingerprint, + options + ), + enableCloseToTray: await this.storageService.get( + v1Keys.enableCloseToTray, + options + ), + enableFullWidth: await this.storageService.get( + v1Keys.enableFullWidth, + options + ), + enableGravitars: await this.storageService.get( + v1Keys.enableGravatars, + options + ), + enableMinimizeToTray: await this.storageService.get( + v1Keys.enableMinimizeToTray, + options + ), + enableStartToTray: await this.storageService.get( + v1Keys.enableStartToTray, + options + ), + enableTray: await this.storageService.get(v1Keys.enableTray, options), + environmentUrls: await this.storageService.get( + v1Keys.environmentUrls, + options + ), + equivalentDomains: await this.storageService.get( + v1Keys.equivalentDomains, + options + ), + minimizeOnCopyToClipboard: await this.storageService.get( + v1Keys.minimizeOnCopyToClipboard, + options + ), + neverDomains: await this.storageService.get(v1Keys.neverDomains, options), + openAtLogin: await this.storageService.get(v1Keys.openAtLogin, options), + passwordGenerationOptions: await this.storageService.get( + v1Keys.passwordGenerationOptions, + options + ), + pinProtected: { + decrypted: null, + encrypted: await this.storageService.get(v1Keys.pinProtected, options), + }, + protectedPin: await this.storageService.get(v1Keys.protectedPin, options), + settings: await this.storageService.get( + v1KeyPrefixes.settings + userId, + options + ), + vaultTimeout: await this.storageService.get(v1Keys.vaultTimeout, options), + vaultTimeoutAction: await this.storageService.get( + v1Keys.vaultTimeoutAction, + options + ), + }, + tokens: { + accessToken: await this.storageService.get(v1Keys.accessToken, options), + decodedToken: null, + refreshToken: await this.storageService.get(v1Keys.refreshToken, options), + securityStamp: null, + }, + }), + }, + }; + + await this.storageService.save("state", initialState, options); + + if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) { + await this.secureStorageService.save( + `${userId}_masterkey_biometric`, + await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }), + { keySuffix: "biometric" } + ); + await this.secureStorageService.remove(v1Keys.key, { keySuffix: "biometric" }); + } + + if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) { + await this.secureStorageService.save( + `${userId}_masterkey_auto`, + await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }), + { keySuffix: "auto" } + ); + await this.secureStorageService.remove(v1Keys.key, { keySuffix: "auto" }); + } + + if (await this.secureStorageService.has(v1Keys.key)) { + await this.secureStorageService.save( + `${userId}_masterkey`, + await this.secureStorageService.get(v1Keys.key) + ); + await this.secureStorageService.remove(v1Keys.key); + } + } +} diff --git a/common/src/services/sync.service.ts b/common/src/services/sync.service.ts index 8395ddcc..e3ef51d3 100644 --- a/common/src/services/sync.service.ts +++ b/common/src/services/sync.service.ts @@ -1,394 +1,401 @@ -import { ApiService } from '../abstractions/api.service'; -import { CipherService } from '../abstractions/cipher.service'; -import { CollectionService } from '../abstractions/collection.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { FolderService } from '../abstractions/folder.service'; -import { KeyConnectorService } from '../abstractions/keyConnector.service'; -import { LogService } from '../abstractions/log.service'; -import { MessagingService } from '../abstractions/messaging.service'; -import { PolicyService } from '../abstractions/policy.service'; -import { SendService } from '../abstractions/send.service'; -import { SettingsService } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; -import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; +import { ApiService } from "../abstractions/api.service"; +import { CipherService } from "../abstractions/cipher.service"; +import { CollectionService } from "../abstractions/collection.service"; +import { CryptoService } from "../abstractions/crypto.service"; +import { FolderService } from "../abstractions/folder.service"; +import { KeyConnectorService } from "../abstractions/keyConnector.service"; +import { LogService } from "../abstractions/log.service"; +import { MessagingService } from "../abstractions/messaging.service"; +import { OrganizationService } from "../abstractions/organization.service"; +import { PolicyService } from "../abstractions/policy.service"; +import { ProviderService } from "../abstractions/provider.service"; +import { SendService } from "../abstractions/send.service"; +import { SettingsService } from "../abstractions/settings.service"; +import { StateService } from "../abstractions/state.service"; +import { SyncService as SyncServiceAbstraction } from "../abstractions/sync.service"; -import { CipherData } from '../models/data/cipherData'; -import { CollectionData } from '../models/data/collectionData'; -import { FolderData } from '../models/data/folderData'; -import { OrganizationData } from '../models/data/organizationData'; -import { PolicyData } from '../models/data/policyData'; -import { ProviderData } from '../models/data/providerData'; -import { SendData } from '../models/data/sendData'; +import { CipherData } from "../models/data/cipherData"; +import { CollectionData } from "../models/data/collectionData"; +import { FolderData } from "../models/data/folderData"; +import { OrganizationData } from "../models/data/organizationData"; +import { PolicyData } from "../models/data/policyData"; +import { ProviderData } from "../models/data/providerData"; +import { SendData } from "../models/data/sendData"; -import { CipherResponse } from '../models/response/cipherResponse'; -import { CollectionDetailsResponse } from '../models/response/collectionResponse'; -import { DomainsResponse } from '../models/response/domainsResponse'; -import { FolderResponse } from '../models/response/folderResponse'; +import { CipherResponse } from "../models/response/cipherResponse"; +import { CollectionDetailsResponse } from "../models/response/collectionResponse"; +import { DomainsResponse } from "../models/response/domainsResponse"; +import { FolderResponse } from "../models/response/folderResponse"; import { - SyncCipherNotification, - SyncFolderNotification, - SyncSendNotification, -} from '../models/response/notificationResponse'; -import { PolicyResponse } from '../models/response/policyResponse'; -import { ProfileResponse } from '../models/response/profileResponse'; -import { SendResponse } from '../models/response/sendResponse'; - -const Keys = { - lastSyncPrefix: 'lastSync_', -}; + SyncCipherNotification, + SyncFolderNotification, + SyncSendNotification, +} from "../models/response/notificationResponse"; +import { PolicyResponse } from "../models/response/policyResponse"; +import { ProfileResponse } from "../models/response/profileResponse"; +import { SendResponse } from "../models/response/sendResponse"; export class SyncService implements SyncServiceAbstraction { - syncInProgress: boolean = false; + syncInProgress: boolean = false; - constructor(private userService: UserService, private apiService: ApiService, - private settingsService: SettingsService, private folderService: FolderService, - private cipherService: CipherService, private cryptoService: CryptoService, - private collectionService: CollectionService, private storageService: StorageService, - private messagingService: MessagingService, private policyService: PolicyService, - private sendService: SendService, private logService: LogService, - private tokenService: TokenService, private keyConnectorService: KeyConnectorService, - private logoutCallback: (expired: boolean) => Promise) { + constructor( + private apiService: ApiService, + private settingsService: SettingsService, + private folderService: FolderService, + private cipherService: CipherService, + private cryptoService: CryptoService, + private collectionService: CollectionService, + private messagingService: MessagingService, + private policyService: PolicyService, + private sendService: SendService, + private logService: LogService, + private keyConnectorService: KeyConnectorService, + private stateService: StateService, + private organizationService: OrganizationService, + private providerService: ProviderService, + private logoutCallback: (expired: boolean) => Promise + ) {} + + async getLastSync(): Promise { + if ((await this.stateService.getUserId()) == null) { + return null; } - async getLastSync(): Promise { - const userId = await this.userService.getUserId(); - if (userId == null) { - return null; - } - - const lastSync = await this.storageService.get(Keys.lastSyncPrefix + userId); - if (lastSync) { - return new Date(lastSync); - } - - return null; + const lastSync = await this.stateService.getLastSync(); + if (lastSync) { + return new Date(lastSync); } - async setLastSync(date: Date): Promise { - const userId = await this.userService.getUserId(); - if (userId == null) { - return; - } + return null; + } - await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); + async setLastSync(date: Date, userId?: string): Promise { + await this.stateService.setLastSync(date.toJSON(), { userId: userId }); + } + + async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { + this.syncStarted(); + const isAuthenticated = await this.stateService.getIsAuthenticated(); + if (!isAuthenticated) { + return this.syncCompleted(false); } - async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { - this.syncStarted(); - const isAuthenticated = await this.userService.isAuthenticated(); - if (!isAuthenticated) { - return this.syncCompleted(false); - } + const now = new Date(); + let needsSync = false; + try { + needsSync = await this.needsSyncing(forceSync); + } catch (e) { + if (allowThrowOnError) { + throw e; + } + } - const now = new Date(); - let needsSync = false; - try { - needsSync = await this.needsSyncing(forceSync); - } catch (e) { - if (allowThrowOnError) { - throw e; - } - } + if (!needsSync) { + await this.setLastSync(now); + return this.syncCompleted(false); + } - if (!needsSync) { - await this.setLastSync(now); - return this.syncCompleted(false); - } + const userId = await this.stateService.getUserId(); + try { + await this.apiService.refreshIdentityToken(); + const response = await this.apiService.getSync(); - const userId = await this.userService.getUserId(); - try { - await this.apiService.refreshIdentityToken(); - const response = await this.apiService.getSync(); + await this.syncProfile(response.profile); + await this.syncFolders(userId, response.folders); + await this.syncCollections(response.collections); + await this.syncCiphers(userId, response.ciphers); + await this.syncSends(userId, response.sends); + await this.syncSettings(response.domains); + await this.syncPolicies(response.policies); - await this.syncProfile(response.profile); - await this.syncFolders(userId, response.folders); - await this.syncCollections(response.collections); - await this.syncCiphers(userId, response.ciphers); - await this.syncSends(userId, response.sends); - await this.syncSettings(userId, response.domains); - await this.syncPolicies(response.policies); + await this.setLastSync(now); + return this.syncCompleted(true); + } catch (e) { + if (allowThrowOnError) { + throw e; + } else { + return this.syncCompleted(false); + } + } + } - await this.setLastSync(now); + async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + try { + const localFolder = await this.folderService.get(notification.id); + if ( + (!isEdit && localFolder == null) || + (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate) + ) { + const remoteFolder = await this.apiService.getFolder(notification.id); + if (remoteFolder != null) { + const userId = await this.stateService.getUserId(); + await this.folderService.upsert(new FolderData(remoteFolder, userId)); + this.messagingService.send("syncedUpsertedFolder", { folderId: notification.id }); return this.syncCompleted(true); - } catch (e) { - if (allowThrowOnError) { - throw e; + } + } + } catch (e) { + this.logService.error(e); + } + } + return this.syncCompleted(false); + } + + async syncDeleteFolder(notification: SyncFolderNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.folderService.delete(notification.id); + this.messagingService.send("syncedDeletedFolder", { folderId: notification.id }); + this.syncCompleted(true); + return true; + } + return this.syncCompleted(false); + } + + async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + try { + let shouldUpdate = true; + const localCipher = await this.cipherService.get(notification.id); + if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) { + shouldUpdate = false; + } + + let checkCollections = false; + if (shouldUpdate) { + if (isEdit) { + shouldUpdate = localCipher != null; + checkCollections = true; + } else { + if (notification.collectionIds == null || notification.organizationId == null) { + shouldUpdate = localCipher == null; } else { - return this.syncCompleted(false); + shouldUpdate = false; + checkCollections = true; } + } } - } - async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { - this.syncStarted(); - if (await this.userService.isAuthenticated()) { - try { - const localFolder = await this.folderService.get(notification.id); - if ((!isEdit && localFolder == null) || - (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)) { - const remoteFolder = await this.apiService.getFolder(notification.id); - if (remoteFolder != null) { - const userId = await this.userService.getUserId(); - await this.folderService.upsert(new FolderData(remoteFolder, userId)); - this.messagingService.send('syncedUpsertedFolder', { folderId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - this.logService.error(e); + if ( + !shouldUpdate && + checkCollections && + notification.organizationId != null && + notification.collectionIds != null && + notification.collectionIds.length > 0 + ) { + const collections = await this.collectionService.getAll(); + if (collections != null) { + for (let i = 0; i < collections.length; i++) { + if (notification.collectionIds.indexOf(collections[i].id) > -1) { + shouldUpdate = true; + break; + } } + } } - return this.syncCompleted(false); - } - async syncDeleteFolder(notification: SyncFolderNotification): Promise { - this.syncStarted(); - if (await this.userService.isAuthenticated()) { - await this.folderService.delete(notification.id); - this.messagingService.send('syncedDeletedFolder', { folderId: notification.id }); - this.syncCompleted(true); - return true; - } - return this.syncCompleted(false); - } - - async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { - this.syncStarted(); - if (await this.userService.isAuthenticated()) { - try { - let shouldUpdate = true; - const localCipher = await this.cipherService.get(notification.id); - if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) { - shouldUpdate = false; - } - - let checkCollections = false; - if (shouldUpdate) { - if (isEdit) { - shouldUpdate = localCipher != null; - checkCollections = true; - } else { - if (notification.collectionIds == null || notification.organizationId == null) { - shouldUpdate = localCipher == null; - } else { - shouldUpdate = false; - checkCollections = true; - } - } - } - - if (!shouldUpdate && checkCollections && notification.organizationId != null && - notification.collectionIds != null && notification.collectionIds.length > 0) { - const collections = await this.collectionService.getAll(); - if (collections != null) { - for (let i = 0; i < collections.length; i++) { - if (notification.collectionIds.indexOf(collections[i].id) > -1) { - shouldUpdate = true; - break; - } - } - } - } - - if (shouldUpdate) { - const remoteCipher = await this.apiService.getCipher(notification.id); - if (remoteCipher != null) { - const userId = await this.userService.getUserId(); - await this.cipherService.upsert(new CipherData(remoteCipher, userId)); - this.messagingService.send('syncedUpsertedCipher', { cipherId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - if (e != null && e.statusCode === 404 && isEdit) { - await this.cipherService.delete(notification.id); - this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id }); - return this.syncCompleted(true); - } - } - } - return this.syncCompleted(false); - } - - async syncDeleteCipher(notification: SyncCipherNotification): Promise { - this.syncStarted(); - if (await this.userService.isAuthenticated()) { - await this.cipherService.delete(notification.id); - this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id }); + if (shouldUpdate) { + const remoteCipher = await this.apiService.getCipher(notification.id); + if (remoteCipher != null) { + const userId = await this.stateService.getUserId(); + await this.cipherService.upsert(new CipherData(remoteCipher, userId)); + this.messagingService.send("syncedUpsertedCipher", { cipherId: notification.id }); return this.syncCompleted(true); + } } - return this.syncCompleted(false); - } - - async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise { - this.syncStarted(); - if (await this.userService.isAuthenticated()) { - try { - const localSend = await this.sendService.get(notification.id); - if ((!isEdit && localSend == null) || - (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)) { - const remoteSend = await this.apiService.getSend(notification.id); - if (remoteSend != null) { - const userId = await this.userService.getUserId(); - await this.sendService.upsert(new SendData(remoteSend, userId)); - this.messagingService.send('syncedUpsertedSend', { sendId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - this.logService.error(e); - } + } catch (e) { + if (e != null && e.statusCode === 404 && isEdit) { + await this.cipherService.delete(notification.id); + this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id }); + return this.syncCompleted(true); } - return this.syncCompleted(false); + } } + return this.syncCompleted(false); + } - async syncDeleteSend(notification: SyncSendNotification): Promise { - this.syncStarted(); - if (await this.userService.isAuthenticated()) { - await this.sendService.delete(notification.id); - this.messagingService.send('syncedDeletedSend', { sendId: notification.id }); - this.syncCompleted(true); - return true; + async syncDeleteCipher(notification: SyncCipherNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.cipherService.delete(notification.id); + this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id }); + return this.syncCompleted(true); + } + return this.syncCompleted(false); + } + + async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + try { + const localSend = await this.sendService.get(notification.id); + if ( + (!isEdit && localSend == null) || + (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate) + ) { + const remoteSend = await this.apiService.getSend(notification.id); + if (remoteSend != null) { + const userId = await this.stateService.getUserId(); + await this.sendService.upsert(new SendData(remoteSend, userId)); + this.messagingService.send("syncedUpsertedSend", { sendId: notification.id }); + return this.syncCompleted(true); + } } - return this.syncCompleted(false); + } catch (e) { + this.logService.error(e); + } + } + return this.syncCompleted(false); + } + + async syncDeleteSend(notification: SyncSendNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.sendService.delete(notification.id); + this.messagingService.send("syncedDeletedSend", { sendId: notification.id }); + this.syncCompleted(true); + return true; + } + return this.syncCompleted(false); + } + + // Helpers + + private syncStarted() { + this.syncInProgress = true; + this.messagingService.send("syncStarted"); + } + + private syncCompleted(successfully: boolean): boolean { + this.syncInProgress = false; + this.messagingService.send("syncCompleted", { successfully: successfully }); + return successfully; + } + + private async needsSyncing(forceSync: boolean) { + if (forceSync) { + return true; } - // Helpers - - private syncStarted() { - this.syncInProgress = true; - this.messagingService.send('syncStarted'); + const lastSync = await this.getLastSync(); + if (lastSync == null || lastSync.getTime() === 0) { + return true; } - private syncCompleted(successfully: boolean): boolean { - this.syncInProgress = false; - this.messagingService.send('syncCompleted', { successfully: successfully }); - return successfully; + const response = await this.apiService.getAccountRevisionDate(); + if (new Date(response) <= lastSync) { + return false; + } + return true; + } + + private async syncProfile(response: ProfileResponse) { + const stamp = await this.stateService.getSecurityStamp(); + if (stamp != null && stamp !== response.securityStamp) { + if (this.logoutCallback != null) { + await this.logoutCallback(true); + } + + throw new Error("Stamp has changed"); } - private async needsSyncing(forceSync: boolean) { - if (forceSync) { - return true; + await this.cryptoService.setEncKey(response.key); + await this.cryptoService.setEncPrivateKey(response.privateKey); + await this.cryptoService.setProviderKeys(response.providers); + await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); + await this.stateService.setSecurityStamp(response.securityStamp); + await this.stateService.setEmailVerified(response.emailVerified); + await this.stateService.setForcePasswordReset(response.forcePasswordReset); + await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); + + const organizations: { [id: string]: OrganizationData } = {}; + response.organizations.forEach((o) => { + organizations[o.id] = new OrganizationData(o); + }); + + const providers: { [id: string]: ProviderData } = {}; + response.providers.forEach((p) => { + providers[p.id] = new ProviderData(p); + }); + + response.providerOrganizations.forEach((o) => { + if (organizations[o.id] == null) { + organizations[o.id] = new OrganizationData(o); + organizations[o.id].isProviderUser = true; + } + }); + + await Promise.all([ + this.organizationService.save(organizations), + this.providerService.save(providers), + ]); + + if (await this.keyConnectorService.userNeedsMigration()) { + this.messagingService.send("convertAccountToKeyConnector"); + } else { + this.keyConnectorService.removeConvertAccountRequired(); + } + } + + private async syncFolders(userId: string, response: FolderResponse[]) { + const folders: { [id: string]: FolderData } = {}; + response.forEach((f) => { + folders[f.id] = new FolderData(f, userId); + }); + return await this.folderService.replace(folders); + } + + private async syncCollections(response: CollectionDetailsResponse[]) { + const collections: { [id: string]: CollectionData } = {}; + response.forEach((c) => { + collections[c.id] = new CollectionData(c); + }); + return await this.collectionService.replace(collections); + } + + private async syncCiphers(userId: string, response: CipherResponse[]) { + const ciphers: { [id: string]: CipherData } = {}; + response.forEach((c) => { + ciphers[c.id] = new CipherData(c, userId); + }); + return await this.cipherService.replace(ciphers); + } + + private async syncSends(userId: string, response: SendResponse[]) { + const sends: { [id: string]: SendData } = {}; + response.forEach((s) => { + sends[s.id] = new SendData(s, userId); + }); + return await this.sendService.replace(sends); + } + + private async syncSettings(response: DomainsResponse) { + let eqDomains: string[][] = []; + if (response != null && response.equivalentDomains != null) { + eqDomains = eqDomains.concat(response.equivalentDomains); + } + + if (response != null && response.globalEquivalentDomains != null) { + response.globalEquivalentDomains.forEach((global) => { + if (global.domains.length > 0) { + eqDomains.push(global.domains); } - - const lastSync = await this.getLastSync(); - if (lastSync == null || lastSync.getTime() === 0) { - return true; - } - - const response = await this.apiService.getAccountRevisionDate(); - if (new Date(response) <= lastSync) { - return false; - } - return true; + }); } - private async syncProfile(response: ProfileResponse) { - const stamp = await this.userService.getSecurityStamp(); - if (stamp != null && stamp !== response.securityStamp) { - if (this.logoutCallback != null) { - await this.logoutCallback(true); - } + return this.settingsService.setEquivalentDomains(eqDomains); + } - throw new Error('Stamp has changed'); - } - - await this.cryptoService.setEncKey(response.key); - await this.cryptoService.setEncPrivateKey(response.privateKey); - await this.cryptoService.setProviderKeys(response.providers); - await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); - await this.userService.setSecurityStamp(response.securityStamp); - await this.userService.setEmailVerified(response.emailVerified); - await this.userService.setForcePasswordReset(response.forcePasswordReset); - await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); - - const organizations: { [id: string]: OrganizationData; } = {}; - response.organizations.forEach(o => { - organizations[o.id] = new OrganizationData(o); - }); - - const providers: { [id: string]: ProviderData; } = {}; - response.providers.forEach(p => { - providers[p.id] = new ProviderData(p); - }); - - response.providerOrganizations.forEach(o => { - if (organizations[o.id] == null) { - organizations[o.id] = new OrganizationData(o); - organizations[o.id].isProviderUser = true; - } - }); - - await Promise.all([ - this.userService.replaceOrganizations(organizations), - this.userService.replaceProviders(providers), - ]); - - if (await this.keyConnectorService.userNeedsMigration()) { - this.messagingService.send('convertAccountToKeyConnector'); - } else { - this.keyConnectorService.removeConvertAccountRequired(); - } - } - - private async syncFolders(userId: string, response: FolderResponse[]) { - const folders: { [id: string]: FolderData; } = {}; - response.forEach(f => { - folders[f.id] = new FolderData(f, userId); - }); - return await this.folderService.replace(folders); - } - - private async syncCollections(response: CollectionDetailsResponse[]) { - const collections: { [id: string]: CollectionData; } = {}; - response.forEach(c => { - collections[c.id] = new CollectionData(c); - }); - return await this.collectionService.replace(collections); - } - - private async syncCiphers(userId: string, response: CipherResponse[]) { - const ciphers: { [id: string]: CipherData; } = {}; - response.forEach(c => { - ciphers[c.id] = new CipherData(c, userId); - }); - return await this.cipherService.replace(ciphers); - } - - private async syncSends(userId: string, response: SendResponse[]) { - const sends: { [id: string]: SendData; } = {}; - response.forEach(s => { - sends[s.id] = new SendData(s, userId); - }); - return await this.sendService.replace(sends); - } - - private async syncSettings(userId: string, response: DomainsResponse) { - let eqDomains: string[][] = []; - if (response != null && response.equivalentDomains != null) { - eqDomains = eqDomains.concat(response.equivalentDomains); - } - - if (response != null && response.globalEquivalentDomains != null) { - response.globalEquivalentDomains.forEach(global => { - if (global.domains.length > 0) { - eqDomains.push(global.domains); - } - }); - } - - return this.settingsService.setEquivalentDomains(eqDomains); - } - - private async syncPolicies(response: PolicyResponse[]) { - const policies: { [id: string]: PolicyData; } = {}; - if (response != null) { - response.forEach(p => { - policies[p.id] = new PolicyData(p); - }); - } - return await this.policyService.replace(policies); + private async syncPolicies(response: PolicyResponse[]) { + const policies: { [id: string]: PolicyData } = {}; + if (response != null) { + response.forEach((p) => { + policies[p.id] = new PolicyData(p); + }); } + return await this.policyService.replace(policies); + } } diff --git a/common/src/services/system.service.ts b/common/src/services/system.service.ts index ada91813..4c84b792 100644 --- a/common/src/services/system.service.ts +++ b/common/src/services/system.service.ts @@ -1,89 +1,91 @@ -import { MessagingService } from '../abstractions/messaging.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { StorageService } from '../abstractions/storage.service'; -import { SystemService as SystemServiceAbstraction } from '../abstractions/system.service'; -import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; +import { MessagingService } from "../abstractions/messaging.service"; +import { PlatformUtilsService } from "../abstractions/platformUtils.service"; +import { StateService } from "../abstractions/state.service"; +import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service"; -import { ConstantsService } from './constants.service'; - -import { Utils } from '../misc/utils'; +import { Utils } from "../misc/utils"; export class SystemService implements SystemServiceAbstraction { - private reloadInterval: any = null; - private clearClipboardTimeout: any = null; - private clearClipboardTimeoutFunction: () => Promise = null; + private reloadInterval: any = null; + private clearClipboardTimeout: any = null; + private clearClipboardTimeoutFunction: () => Promise = null; - constructor(private storageService: StorageService, private vaultTimeoutService: VaultTimeoutService, - private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, - private reloadCallback: () => Promise = null) { - } + constructor( + private messagingService: MessagingService, + private platformUtilsService: PlatformUtilsService, + private reloadCallback: () => Promise = null, + private stateService: StateService + ) {} - startProcessReload(): void { - if (this.vaultTimeoutService.pinProtectedKey != null || - this.vaultTimeoutService.biometricLocked || - this.reloadInterval != null) { - return; - } - this.cancelProcessReload(); - this.reloadInterval = setInterval(async () => { - let doRefresh = false; - const lastActive = await this.storageService.get(ConstantsService.lastActiveKey); - if (lastActive != null) { - const diffSeconds = (new Date()).getTime() - lastActive; - // Don't refresh if they are still active in the window - doRefresh = diffSeconds >= 5000; - } - const biometricLockedFingerprintValidated = - await this.storageService.get(ConstantsService.biometricFingerprintValidated) && this.vaultTimeoutService.biometricLocked; - if (doRefresh && !biometricLockedFingerprintValidated) { - clearInterval(this.reloadInterval); - this.reloadInterval = null; - this.messagingService.send('reloadProcess'); - if (this.reloadCallback != null) { - await this.reloadCallback(); - } - } - }, 10000); + async startProcessReload(): Promise { + if ( + (await this.stateService.getDecryptedPinProtected()) != null || + (await this.stateService.getBiometricLocked()) || + this.reloadInterval != null + ) { + return; } + this.cancelProcessReload(); + this.reloadInterval = setInterval(async () => { + let doRefresh = false; + const lastActive = await this.stateService.getLastActive(); + if (lastActive != null) { + const diffSeconds = new Date().getTime() - lastActive; + // Don't refresh if they are still active in the window + doRefresh = diffSeconds >= 5000; + } + const biometricLockedFingerprintValidated = + (await this.stateService.getBiometricFingerprintValidated()) && + (await this.stateService.getBiometricLocked()); + if (doRefresh && !biometricLockedFingerprintValidated) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + this.messagingService.send("reloadProcess"); + if (this.reloadCallback != null) { + await this.reloadCallback(); + } + } + }, 10000); + } - cancelProcessReload(): void { - if (this.reloadInterval != null) { - clearInterval(this.reloadInterval); - this.reloadInterval = null; - } + cancelProcessReload(): void { + if (this.reloadInterval != null) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; } + } - clearClipboard(clipboardValue: string, timeoutMs: number = null): void { - if (this.clearClipboardTimeout != null) { - clearTimeout(this.clearClipboardTimeout); - this.clearClipboardTimeout = null; - } - if (Utils.isNullOrWhitespace(clipboardValue)) { - return; - } - this.storageService.get(ConstantsService.clearClipboardKey).then(clearSeconds => { - if (clearSeconds == null) { - return; - } - if (timeoutMs == null) { - timeoutMs = clearSeconds * 1000; - } - this.clearClipboardTimeoutFunction = async () => { - const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); - if (clipboardValue === clipboardValueNow) { - this.platformUtilsService.copyToClipboard('', { clearing: true }); - } - }; - this.clearClipboardTimeout = setTimeout(async () => { - await this.clearPendingClipboard(); - }, timeoutMs); - }); + async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise { + if (this.clearClipboardTimeout != null) { + clearTimeout(this.clearClipboardTimeout); + this.clearClipboardTimeout = null; } + if (Utils.isNullOrWhitespace(clipboardValue)) { + return; + } + await this.stateService.getClearClipboard().then((clearSeconds) => { + if (clearSeconds == null) { + return; + } + if (timeoutMs == null) { + timeoutMs = clearSeconds * 1000; + } + this.clearClipboardTimeoutFunction = async () => { + const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); + if (clipboardValue === clipboardValueNow) { + this.platformUtilsService.copyToClipboard("", { clearing: true }); + } + }; + this.clearClipboardTimeout = setTimeout(async () => { + await this.clearPendingClipboard(); + }, timeoutMs); + }); + } - async clearPendingClipboard() { - if (this.clearClipboardTimeoutFunction != null) { - await this.clearClipboardTimeoutFunction(); - this.clearClipboardTimeoutFunction = null; - } + async clearPendingClipboard() { + if (this.clearClipboardTimeoutFunction != null) { + await this.clearClipboardTimeoutFunction(); + this.clearClipboardTimeoutFunction = null; } + } } diff --git a/common/src/services/token.service.ts b/common/src/services/token.service.ts index 58f42e7d..aa4cab32 100644 --- a/common/src/services/token.service.ts +++ b/common/src/services/token.service.ts @@ -1,269 +1,224 @@ -import { ConstantsService } from './constants.service'; +import { StateService } from "../abstractions/state.service"; +import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service"; -import { StorageService } from '../abstractions/storage.service'; -import { TokenService as TokenServiceAbstraction } from '../abstractions/token.service'; - -import { Utils } from '../misc/utils'; - -const Keys = { - accessToken: 'accessToken', - refreshToken: 'refreshToken', - twoFactorTokenPrefix: 'twoFactorToken_', - clientId: 'apikey_clientId', - clientSecret: 'apikey_clientSecret', -}; +import { Utils } from "../misc/utils"; export class TokenService implements TokenServiceAbstraction { - token: string; - decodedToken: any; - refreshToken: string; - clientId: string; - clientSecret: string; + constructor(private stateService: StateService) {} - constructor(private storageService: StorageService) { + async setTokens( + accessToken: string, + refreshToken: string, + clientIdClientSecret: [string, string] + ): Promise { + await this.setToken(accessToken); + await this.setRefreshToken(refreshToken); + if (clientIdClientSecret != null) { + await this.setClientId(clientIdClientSecret[0]); + await this.setClientSecret(clientIdClientSecret[1]); + } + } + + async setClientId(clientId: string): Promise { + if ((await this.skipTokenStorage()) || clientId == null) { + return; + } + return await this.stateService.setApiKeyClientId(clientId); + } + + async getClientId(): Promise { + return await this.stateService.getApiKeyClientId(); + } + + async setClientSecret(clientSecret: string): Promise { + if ((await this.skipTokenStorage()) || clientSecret == null) { + return; + } + return await this.stateService.setApiKeyClientSecret(clientSecret); + } + + async getClientSecret(): Promise { + return await this.stateService.getApiKeyClientSecret(); + } + + async setToken(token: string): Promise { + await this.stateService.setAccessToken(token); + } + + async getToken(): Promise { + return await this.stateService.getAccessToken(); + } + + async setRefreshToken(refreshToken: string): Promise { + if (await this.skipTokenStorage()) { + return; + } + return await this.stateService.setRefreshToken(refreshToken); + } + + async getRefreshToken(): Promise { + return await this.stateService.getRefreshToken(); + } + + async toggleTokens(): Promise { + const token = await this.getToken(); + const refreshToken = await this.getRefreshToken(); + const clientId = await this.getClientId(); + const clientSecret = await this.getClientSecret(); + const timeout = await this.stateService.getVaultTimeout(); + const action = await this.stateService.getVaultTimeoutAction(); + + if ((timeout != null || timeout === 0) && action === "logOut") { + // if we have a vault timeout and the action is log out, reset tokens + await this.clearToken(); } - async setTokens(accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]): Promise { - await this.setToken(accessToken); - await this.setRefreshToken(refreshToken); - if (clientIdClientSecret != null) { - await this.setClientId(clientIdClientSecret[0]); - await this.setClientSecret(clientIdClientSecret[1]); - } + await this.setToken(token); + await this.setRefreshToken(refreshToken); + await this.setClientId(clientId); + await this.setClientSecret(clientSecret); + } + + async setTwoFactorToken(token: string): Promise { + return await this.stateService.setTwoFactorToken(token); + } + + async getTwoFactorToken(): Promise { + return await this.stateService.getTwoFactorToken(); + } + + async clearTwoFactorToken(): Promise { + return await this.stateService.setTwoFactorToken(null); + } + + async clearToken(userId?: string): Promise { + await this.stateService.setAccessToken(null, { userId: userId }); + await this.stateService.setRefreshToken(null, { userId: userId }); + await this.stateService.setApiKeyClientId(null, { userId: userId }); + await this.stateService.setApiKeyClientSecret(null, { userId: userId }); + } + + // jwthelper methods + // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js + + async decodeToken(token?: string): Promise { + const storedToken = await this.stateService.getDecodedToken(); + if (token === null && storedToken != null) { + return storedToken; } - async setClientId(clientId: string): Promise { - this.clientId = clientId; - return this.storeTokenValue(Keys.clientId, clientId); + token = token ?? (await this.stateService.getAccessToken()); + + if (token == null) { + throw new Error("Token not found."); } - async getClientId(): Promise { - if (this.clientId != null) { - return this.clientId; - } - - this.clientId = await this.storageService.get(Keys.clientId); - return this.clientId; + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("JWT must have 3 parts"); } - async setClientSecret(clientSecret: string): Promise { - this.clientSecret = clientSecret; - return this.storeTokenValue(Keys.clientSecret, clientSecret); + const decoded = Utils.fromUrlB64ToUtf8(parts[1]); + if (decoded == null) { + throw new Error("Cannot decode the token"); } - async getClientSecret(): Promise { - if (this.clientSecret != null) { - return this.clientSecret; - } + const decodedToken = JSON.parse(decoded); + return decodedToken; + } - this.clientSecret = await this.storageService.get(Keys.clientSecret); - return this.clientSecret; + async getTokenExpirationDate(): Promise { + const decoded = await this.decodeToken(); + if (typeof decoded.exp === "undefined") { + return null; } - async setToken(token: string): Promise { - this.token = token; - this.decodedToken = null; - return this.storeTokenValue(Keys.accessToken, token); + const d = new Date(0); // The 0 here is the key, which sets the date to the epoch + d.setUTCSeconds(decoded.exp); + return d; + } + + async tokenSecondsRemaining(offsetSeconds: number = 0): Promise { + const d = await this.getTokenExpirationDate(); + if (d == null) { + return 0; } - async getToken(): Promise { - if (this.token != null) { - return this.token; - } + const msRemaining = d.valueOf() - (new Date().valueOf() + offsetSeconds * 1000); + return Math.round(msRemaining / 1000); + } - this.token = await this.storageService.get(Keys.accessToken); - return this.token; + async tokenNeedsRefresh(minutes: number = 5): Promise { + const sRemaining = await this.tokenSecondsRemaining(); + return sRemaining < 60 * minutes; + } + + async getUserId(): Promise { + const decoded = await this.decodeToken(); + if (typeof decoded.sub === "undefined") { + throw new Error("No user id found"); } - async setRefreshToken(refreshToken: string): Promise { - this.refreshToken = refreshToken; - return this.storeTokenValue(Keys.refreshToken, refreshToken); + return decoded.sub as string; + } + + async getEmail(): Promise { + const decoded = await this.decodeToken(); + if (typeof decoded.email === "undefined") { + throw new Error("No email found"); } - async getRefreshToken(): Promise { - if (this.refreshToken != null) { - return this.refreshToken; - } + return decoded.email as string; + } - this.refreshToken = await this.storageService.get(Keys.refreshToken); - return this.refreshToken; + async getEmailVerified(): Promise { + const decoded = await this.decodeToken(); + if (typeof decoded.email_verified === "undefined") { + throw new Error("No email verification found"); } - async toggleTokens(): Promise { - const token = await this.getToken(); - const refreshToken = await this.getRefreshToken(); - const clientId = await this.getClientId(); - const clientSecret = await this.getClientSecret(); - const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); - const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); - if ((timeout != null || timeout === 0) && action === 'logOut') { - // if we have a vault timeout and the action is log out, reset tokens - await this.clearToken(); - this.token = token; - this.refreshToken = refreshToken; - this.clientId = clientId; - this.clientSecret = clientSecret; - return; - } + return decoded.email_verified as boolean; + } - await this.setToken(token); - await this.setRefreshToken(refreshToken); - await this.setClientId(clientId); - await this.setClientSecret(clientSecret); + async getName(): Promise { + const decoded = await this.decodeToken(); + if (typeof decoded.name === "undefined") { + return null; } - setTwoFactorToken(token: string, email: string): Promise { - return this.storageService.save(Keys.twoFactorTokenPrefix + email, token); + return decoded.name as string; + } + + async getPremium(): Promise { + const decoded = await this.decodeToken(); + if (typeof decoded.premium === "undefined") { + return false; } - getTwoFactorToken(email: string): Promise { - return this.storageService.get(Keys.twoFactorTokenPrefix + email); + return decoded.premium as boolean; + } + + async getIssuer(): Promise { + const decoded = await this.decodeToken(); + if (typeof decoded.iss === "undefined") { + throw new Error("No issuer found"); } - clearTwoFactorToken(email: string): Promise { - return this.storageService.remove(Keys.twoFactorTokenPrefix + email); + return decoded.iss as string; + } + + async getIsExternal(): Promise { + const decoded = await this.decodeToken(); + if (!Array.isArray(decoded.amr)) { + throw new Error("No amr found"); } - async clearToken(): Promise { - this.token = null; - this.decodedToken = null; - this.refreshToken = null; - this.clientId = null; - this.clientSecret = null; + return decoded.amr.includes("external"); + } - await this.storageService.remove(Keys.accessToken); - await this.storageService.remove(Keys.refreshToken); - await this.storageService.remove(Keys.clientId); - await this.storageService.remove(Keys.clientSecret); - } - - // jwthelper methods - // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js - - decodeToken(): any { - if (this.decodedToken) { - return this.decodedToken; - } - - if (this.token == null) { - throw new Error('Token not found.'); - } - - const parts = this.token.split('.'); - if (parts.length !== 3) { - throw new Error('JWT must have 3 parts'); - } - - const decoded = Utils.fromUrlB64ToUtf8(parts[1]); - if (decoded == null) { - throw new Error('Cannot decode the token'); - } - - this.decodedToken = JSON.parse(decoded); - return this.decodedToken; - } - - getTokenExpirationDate(): Date { - const decoded = this.decodeToken(); - if (typeof decoded.exp === 'undefined') { - return null; - } - - const d = new Date(0); // The 0 here is the key, which sets the date to the epoch - d.setUTCSeconds(decoded.exp); - return d; - } - - tokenSecondsRemaining(offsetSeconds: number = 0): number { - const d = this.getTokenExpirationDate(); - if (d == null) { - return 0; - } - - const msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000)); - return Math.round(msRemaining / 1000); - } - - tokenNeedsRefresh(minutes: number = 5): boolean { - const sRemaining = this.tokenSecondsRemaining(); - return sRemaining < (60 * minutes); - } - - getUserId(): string { - const decoded = this.decodeToken(); - if (typeof decoded.sub === 'undefined') { - throw new Error('No user id found'); - } - - return decoded.sub as string; - } - - getEmail(): string { - const decoded = this.decodeToken(); - if (typeof decoded.email === 'undefined') { - throw new Error('No email found'); - } - - return decoded.email as string; - } - - getEmailVerified(): boolean { - const decoded = this.decodeToken(); - if (typeof decoded.email_verified === 'undefined') { - throw new Error('No email verification found'); - } - - return decoded.email_verified as boolean; - } - - getName(): string { - const decoded = this.decodeToken(); - if (typeof decoded.name === 'undefined') { - return null; - } - - return decoded.name as string; - } - - getPremium(): boolean { - const decoded = this.decodeToken(); - if (typeof decoded.premium === 'undefined') { - return false; - } - - return decoded.premium as boolean; - } - - getIssuer(): string { - const decoded = this.decodeToken(); - if (typeof decoded.iss === 'undefined') { - throw new Error('No issuer found'); - } - - return decoded.iss as string; - } - - getIsExternal(): boolean { - const decoded = this.decodeToken(); - if (!Array.isArray(decoded.amr)) { - throw new Error('No amr found'); - } - - return decoded.amr.includes('external'); - } - - private async storeTokenValue(key: string, value: string) { - if (await this.skipTokenStorage()) { - // if we have a vault timeout and the action is log out, don't store token - return; - } - - return this.storageService.save(key, value); - } - - private async skipTokenStorage(): Promise { - const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); - const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); - return timeout != null && action === 'logOut'; - } + private async skipTokenStorage(): Promise { + const timeout = await this.stateService.getVaultTimeout(); + const action = await this.stateService.getVaultTimeoutAction(); + return timeout != null && action === "logOut"; + } } diff --git a/common/src/services/totp.service.ts b/common/src/services/totp.service.ts index 50c8c4eb..e3d653c0 100644 --- a/common/src/services/totp.service.ts +++ b/common/src/services/totp.service.ts @@ -1,170 +1,178 @@ -import { ConstantsService } from './constants.service'; +import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; +import { LogService } from "../abstractions/log.service"; +import { StateService } from "../abstractions/state.service"; +import { TotpService as TotpServiceAbstraction } from "../abstractions/totp.service"; -import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; -import { LogService } from '../abstractions/log.service'; -import { StorageService } from '../abstractions/storage.service'; -import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.service'; +import { Utils } from "../misc/utils"; -import { Utils } from '../misc/utils'; - -const B32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; -const SteamChars = '23456789BCDFGHJKMNPQRTVWXY'; +const B32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +const SteamChars = "23456789BCDFGHJKMNPQRTVWXY"; export class TotpService implements TotpServiceAbstraction { - constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService, - private logService: LogService) { } + constructor( + private cryptoFunctionService: CryptoFunctionService, + private logService: LogService, + private stateService: StateService + ) {} - async getCode(key: string): Promise { - if (key == null) { - return null; + async getCode(key: string): Promise { + if (key == null) { + return null; + } + let period = 30; + let alg: "sha1" | "sha256" | "sha512" = "sha1"; + let digits = 6; + let keyB32 = key; + const isOtpAuth = key.toLowerCase().indexOf("otpauth://") === 0; + const isSteamAuth = !isOtpAuth && key.toLowerCase().indexOf("steam://") === 0; + if (isOtpAuth) { + const params = Utils.getQueryParams(key); + if (params.has("digits") && params.get("digits") != null) { + try { + const digitParams = parseInt(params.get("digits").trim(), null); + if (digitParams > 10) { + digits = 10; + } else if (digitParams > 0) { + digits = digitParams; + } + } catch { + this.logService.error("Invalid digits param."); } - let period = 30; - let alg: 'sha1' | 'sha256' | 'sha512' = 'sha1'; - let digits = 6; - let keyB32 = key; - const isOtpAuth = key.toLowerCase().indexOf('otpauth://') === 0; - const isSteamAuth = !isOtpAuth && key.toLowerCase().indexOf('steam://') === 0; - if (isOtpAuth) { - const params = Utils.getQueryParams(key); - if (params.has('digits') && params.get('digits') != null) { - try { - const digitParams = parseInt(params.get('digits').trim(), null); - if (digitParams > 10) { - digits = 10; - } else if (digitParams > 0) { - digits = digitParams; - } - } catch { - this.logService.error('Invalid digits param.'); - } - } - if (params.has('period') && params.get('period') != null) { - try { - const periodParam = parseInt(params.get('period').trim(), null); - if (periodParam > 0) { - period = periodParam; - } - } catch { - this.logService.error('Invalid period param.'); - } - } - if (params.has('secret') && params.get('secret') != null) { - keyB32 = params.get('secret'); - } - if (params.has('algorithm') && params.get('algorithm') != null) { - const algParam = params.get('algorithm').toLowerCase(); - if (algParam === 'sha1' || algParam === 'sha256' || algParam === 'sha512') { - alg = algParam; - } - } - } else if (isSteamAuth) { - keyB32 = key.substr('steam://'.length); - digits = 5; + } + if (params.has("period") && params.get("period") != null) { + try { + const periodParam = parseInt(params.get("period").trim(), null); + if (periodParam > 0) { + period = periodParam; + } + } catch { + this.logService.error("Invalid period param."); } - - const epoch = Math.round(new Date().getTime() / 1000.0); - const timeHex = this.leftPad(this.decToHex(Math.floor(epoch / period)), 16, '0'); - const timeBytes = Utils.fromHexToArray(timeHex); - const keyBytes = this.b32ToBytes(keyB32); - - if (!keyBytes.length || !timeBytes.length) { - return null; + } + if (params.has("secret") && params.get("secret") != null) { + keyB32 = params.get("secret"); + } + if (params.has("algorithm") && params.get("algorithm") != null) { + const algParam = params.get("algorithm").toLowerCase(); + if (algParam === "sha1" || algParam === "sha256" || algParam === "sha512") { + alg = algParam; } - - const hash = await this.sign(keyBytes, timeBytes, alg); - if (hash.length === 0) { - return null; - } - - /* tslint:disable */ - const offset = (hash[hash.length - 1] & 0xf); - const binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | - ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); - /* tslint:enable */ - - let otp = ''; - if (isSteamAuth) { - // tslint:disable-next-line - let fullCode = binary & 0x7fffffff; - for (let i = 0; i < digits; i++) { - otp += SteamChars[fullCode % SteamChars.length]; - fullCode = Math.trunc(fullCode / SteamChars.length); - } - } else { - otp = (binary % Math.pow(10, digits)).toString(); - otp = this.leftPad(otp, digits, '0'); - } - - return otp; + } + } else if (isSteamAuth) { + keyB32 = key.substr("steam://".length); + digits = 5; } - getTimeInterval(key: string): number { - let period = 30; - if (key != null && key.toLowerCase().indexOf('otpauth://') === 0) { - const params = Utils.getQueryParams(key); - if (params.has('period') && params.get('period') != null) { - try { - period = parseInt(params.get('period').trim(), null); - } catch { - this.logService.error('Invalid period param.'); - } - } + const epoch = Math.round(new Date().getTime() / 1000.0); + const timeHex = this.leftPad(this.decToHex(Math.floor(epoch / period)), 16, "0"); + const timeBytes = Utils.fromHexToArray(timeHex); + const keyBytes = this.b32ToBytes(keyB32); + + if (!keyBytes.length || !timeBytes.length) { + return null; + } + + const hash = await this.sign(keyBytes, timeBytes, alg); + if (hash.length === 0) { + return null; + } + + /* tslint:disable */ + const offset = hash[hash.length - 1] & 0xf; + const binary = + ((hash[offset] & 0x7f) << 24) | + ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | + (hash[offset + 3] & 0xff); + /* tslint:enable */ + + let otp = ""; + if (isSteamAuth) { + // tslint:disable-next-line + let fullCode = binary & 0x7fffffff; + for (let i = 0; i < digits; i++) { + otp += SteamChars[fullCode % SteamChars.length]; + fullCode = Math.trunc(fullCode / SteamChars.length); + } + } else { + otp = (binary % Math.pow(10, digits)).toString(); + otp = this.leftPad(otp, digits, "0"); + } + + return otp; + } + + getTimeInterval(key: string): number { + let period = 30; + if (key != null && key.toLowerCase().indexOf("otpauth://") === 0) { + const params = Utils.getQueryParams(key); + if (params.has("period") && params.get("period") != null) { + try { + period = parseInt(params.get("period").trim(), null); + } catch { + this.logService.error("Invalid period param."); } - return period; + } } + return period; + } - async isAutoCopyEnabled(): Promise { - return !(await this.storageService.get(ConstantsService.disableAutoTotpCopyKey)); + async isAutoCopyEnabled(): Promise { + return !(await this.stateService.getDisableAutoTotpCopy()); + } + + // Helpers + + private leftPad(s: string, l: number, p: string): string { + if (l + 1 >= s.length) { + s = Array(l + 1 - s.length).join(p) + s; } + return s; + } - // Helpers + private decToHex(d: number): string { + return (d < 15.5 ? "0" : "") + Math.round(d).toString(16); + } - private leftPad(s: string, l: number, p: string): string { - if (l + 1 >= s.length) { - s = Array(l + 1 - s.length).join(p) + s; - } - return s; + private b32ToHex(s: string): string { + s = s.toUpperCase(); + let cleanedInput = ""; + + for (let i = 0; i < s.length; i++) { + if (B32Chars.indexOf(s[i]) < 0) { + continue; + } + + cleanedInput += s[i]; } + s = cleanedInput; - private decToHex(d: number): string { - return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); + let bits = ""; + let hex = ""; + for (let i = 0; i < s.length; i++) { + const byteIndex = B32Chars.indexOf(s.charAt(i)); + if (byteIndex < 0) { + continue; + } + bits += this.leftPad(byteIndex.toString(2), 5, "0"); } - - private b32ToHex(s: string): string { - s = s.toUpperCase(); - let cleanedInput = ''; - - for (let i = 0; i < s.length; i++) { - if (B32Chars.indexOf(s[i]) < 0) { - continue; - } - - cleanedInput += s[i]; - } - s = cleanedInput; - - let bits = ''; - let hex = ''; - for (let i = 0; i < s.length; i++) { - const byteIndex = B32Chars.indexOf(s.charAt(i)); - if (byteIndex < 0) { - continue; - } - bits += this.leftPad(byteIndex.toString(2), 5, '0'); - } - for (let i = 0; i + 4 <= bits.length; i += 4) { - const chunk = bits.substr(i, 4); - hex = hex + parseInt(chunk, 2).toString(16); - } - return hex; + for (let i = 0; i + 4 <= bits.length; i += 4) { + const chunk = bits.substr(i, 4); + hex = hex + parseInt(chunk, 2).toString(16); } + return hex; + } - private b32ToBytes(s: string): Uint8Array { - return Utils.fromHexToArray(this.b32ToHex(s)); - } + private b32ToBytes(s: string): Uint8Array { + return Utils.fromHexToArray(this.b32ToHex(s)); + } - private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array, alg: 'sha1' | 'sha256' | 'sha512') { - const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg); - return new Uint8Array(signature); - } + private async sign( + keyBytes: Uint8Array, + timeBytes: Uint8Array, + alg: "sha1" | "sha256" | "sha512" + ) { + const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg); + return new Uint8Array(signature); + } } diff --git a/common/src/services/user.service.ts b/common/src/services/user.service.ts deleted file mode 100644 index a572691a..00000000 --- a/common/src/services/user.service.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { StorageService } from '../abstractions/storage.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService as UserServiceAbstraction } from '../abstractions/user.service'; - -import { OrganizationData } from '../models/data/organizationData'; -import { Organization } from '../models/domain/organization'; - -import { KdfType } from '../enums/kdfType'; - -import { ProviderData } from '../models/data/providerData'; -import { Provider } from '../models/domain/provider'; - -const Keys = { - userId: 'userId', - userEmail: 'userEmail', - stamp: 'securityStamp', - kdf: 'kdf', - kdfIterations: 'kdfIterations', - organizationsPrefix: 'organizations_', - providersPrefix: 'providers_', - emailVerified: 'emailVerified', - forcePasswordReset: 'forcePasswordReset', -}; - -export class UserService implements UserServiceAbstraction { - private userId: string; - private email: string; - private stamp: string; - private kdf: KdfType; - private kdfIterations: number; - private emailVerified: boolean; - private forcePasswordReset: boolean; - - constructor(private tokenService: TokenService, private storageService: StorageService) { } - - async setInformation(userId: string, email: string, kdf: KdfType, kdfIterations: number): Promise { - this.email = email; - this.userId = userId; - this.kdf = kdf; - this.kdfIterations = kdfIterations; - - await this.storageService.save(Keys.userEmail, email); - await this.storageService.save(Keys.userId, userId); - await this.storageService.save(Keys.kdf, kdf); - await this.storageService.save(Keys.kdfIterations, kdfIterations); - } - - setSecurityStamp(stamp: string): Promise { - this.stamp = stamp; - return this.storageService.save(Keys.stamp, stamp); - } - - setEmailVerified(emailVerified: boolean) { - this.emailVerified = emailVerified; - return this.storageService.save(Keys.emailVerified, emailVerified); - } - - setForcePasswordReset(forcePasswordReset: boolean) { - this.forcePasswordReset = forcePasswordReset; - return this.storageService.save(Keys.forcePasswordReset, forcePasswordReset); - } - - async getUserId(): Promise { - if (this.userId == null) { - this.userId = await this.storageService.get(Keys.userId); - } - return this.userId; - } - - async getEmail(): Promise { - if (this.email == null) { - this.email = await this.storageService.get(Keys.userEmail); - } - return this.email; - } - - async getSecurityStamp(): Promise { - if (this.stamp == null) { - this.stamp = await this.storageService.get(Keys.stamp); - } - return this.stamp; - } - - async getKdf(): Promise { - if (this.kdf == null) { - this.kdf = await this.storageService.get(Keys.kdf); - } - return this.kdf; - } - - async getKdfIterations(): Promise { - if (this.kdfIterations == null) { - this.kdfIterations = await this.storageService.get(Keys.kdfIterations); - } - return this.kdfIterations; - } - - async getEmailVerified(): Promise { - if (this.emailVerified == null) { - this.emailVerified = await this.storageService.get(Keys.emailVerified); - } - return this.emailVerified; - } - - async getForcePasswordReset(): Promise { - if (this.forcePasswordReset == null) { - this.forcePasswordReset = await this.storageService.get(Keys.forcePasswordReset); - } - return this.forcePasswordReset; - } - - async clear(): Promise { - const userId = await this.getUserId(); - - await this.storageService.remove(Keys.userId); - await this.storageService.remove(Keys.userEmail); - await this.storageService.remove(Keys.stamp); - await this.storageService.remove(Keys.kdf); - await this.storageService.remove(Keys.kdfIterations); - await this.storageService.remove(Keys.forcePasswordReset); - await this.clearOrganizations(userId); - await this.clearProviders(userId); - - this.userId = this.email = this.stamp = null; - this.kdf = null; - this.kdfIterations = null; - } - - async isAuthenticated(): Promise { - const token = await this.tokenService.getToken(); - if (token == null) { - return false; - } - - const userId = await this.getUserId(); - return userId != null; - } - - async canAccessPremium(): Promise { - const authed = await this.isAuthenticated(); - if (!authed) { - return false; - } - - const tokenPremium = this.tokenService.getPremium(); - if (tokenPremium) { - return true; - } - - const orgs = await this.getAllOrganizations(); - for (let i = 0; i < orgs.length; i++) { - if (orgs[i].usersGetPremium && orgs[i].enabled) { - return true; - } - } - return false; - } - - async canManageSponsorships(): Promise { - const orgs = await this.getAllOrganizations(); - return orgs.some(o => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null); - } - - async getOrganization(id: string): Promise { - const userId = await this.getUserId(); - const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( - Keys.organizationsPrefix + userId); - if (organizations == null || !organizations.hasOwnProperty(id)) { - return null; - } - - return new Organization(organizations[id]); - } - - async getOrganizationByIdentifier(identifier: string): Promise { - const organizations = await this.getAllOrganizations(); - if (organizations == null || organizations.length === 0) { - return null; - } - - return organizations.find(o => o.identifier === identifier); - } - - async getAllOrganizations(): Promise { - const userId = await this.getUserId(); - const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( - Keys.organizationsPrefix + userId); - const response: Organization[] = []; - for (const id in organizations) { - if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) { - response.push(new Organization(organizations[id])); - } - } - return response; - } - - async replaceOrganizations(organizations: { [id: string]: OrganizationData; }): Promise { - const userId = await this.getUserId(); - await this.storageService.save(Keys.organizationsPrefix + userId, organizations); - } - - async clearOrganizations(userId: string): Promise { - await this.storageService.remove(Keys.organizationsPrefix + userId); - } - - async getProvider(id: string): Promise { - const userId = await this.getUserId(); - const providers = await this.storageService.get<{ [id: string]: ProviderData; }>( - Keys.providersPrefix + userId); - if (providers == null || !providers.hasOwnProperty(id)) { - return null; - } - - return new Provider(providers[id]); - } - - async getAllProviders(): Promise { - const userId = await this.getUserId(); - const providers = await this.storageService.get<{ [id: string]: ProviderData; }>( - Keys.providersPrefix + userId); - const response: Provider[] = []; - for (const id in providers) { - if (providers.hasOwnProperty(id)) { - response.push(new Provider(providers[id])); - } - } - return response; - } - - async replaceProviders(providers: { [id: string]: ProviderData; }): Promise { - const userId = await this.getUserId(); - await this.storageService.save(Keys.providersPrefix + userId, providers); - } - - async clearProviders(userId: string): Promise { - await this.storageService.remove(Keys.providersPrefix + userId); - } -} diff --git a/common/src/services/vaultTimeout.service.ts b/common/src/services/vaultTimeout.service.ts index 54d288b1..544e6bc2 100644 --- a/common/src/services/vaultTimeout.service.ts +++ b/common/src/services/vaultTimeout.service.ts @@ -1,180 +1,203 @@ -import { ConstantsService } from './constants.service'; +import { CipherService } from "../abstractions/cipher.service"; +import { CollectionService } from "../abstractions/collection.service"; +import { CryptoService } from "../abstractions/crypto.service"; +import { FolderService } from "../abstractions/folder.service"; +import { KeyConnectorService } from "../abstractions/keyConnector.service"; +import { MessagingService } from "../abstractions/messaging.service"; +import { PlatformUtilsService } from "../abstractions/platformUtils.service"; +import { PolicyService } from "../abstractions/policy.service"; +import { SearchService } from "../abstractions/search.service"; +import { StateService } from "../abstractions/state.service"; +import { TokenService } from "../abstractions/token.service"; +import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vaultTimeout.service"; +import { KeySuffixOptions } from "../enums/keySuffixOptions"; -import { CipherService } from '../abstractions/cipher.service'; -import { CollectionService } from '../abstractions/collection.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { FolderService } from '../abstractions/folder.service'; -import { KeyConnectorService } from '../abstractions/keyConnector.service'; -import { MessagingService } from '../abstractions/messaging.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { PolicyService } from '../abstractions/policy.service'; -import { SearchService } from '../abstractions/search.service'; -import { StorageService } from '../abstractions/storage.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; -import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from '../abstractions/vaultTimeout.service'; - -import { PolicyType } from '../enums/policyType'; -import { EncString } from '../models/domain/encString'; +import { PolicyType } from "../enums/policyType"; export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { - pinProtectedKey: EncString = null; - biometricLocked: boolean = true; - everBeenUnlocked: boolean = false; + private inited = false; - private inited = false; + constructor( + private cipherService: CipherService, + private folderService: FolderService, + private collectionService: CollectionService, + private cryptoService: CryptoService, + protected platformUtilsService: PlatformUtilsService, + private messagingService: MessagingService, + private searchService: SearchService, + private tokenService: TokenService, + private policyService: PolicyService, + private keyConnectorService: KeyConnectorService, + private stateService: StateService, + private lockedCallback: () => Promise = null, + private loggedOutCallback: (userId?: string) => Promise = null + ) {} - constructor(private cipherService: CipherService, private folderService: FolderService, - private collectionService: CollectionService, private cryptoService: CryptoService, - protected platformUtilsService: PlatformUtilsService, private storageService: StorageService, - private messagingService: MessagingService, private searchService: SearchService, - private userService: UserService, private tokenService: TokenService, private policyService: PolicyService, - private keyConnectorService: KeyConnectorService, - private lockedCallback: () => Promise = null, private loggedOutCallback: () => Promise = null) { + init(checkOnInterval: boolean) { + if (this.inited) { + return; } - init(checkOnInterval: boolean) { - if (this.inited) { - return; - } + this.inited = true; + if (checkOnInterval) { + this.startCheck(); + } + } - this.inited = true; - if (checkOnInterval) { - this.startCheck(); - } + startCheck() { + this.checkVaultTimeout(); + setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + } + + // Keys aren't stored for a device that is locked or logged out. + async isLocked(userId?: string): Promise { + const neverLock = + (await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) && + !(await this.stateService.getEverBeenUnlocked({ userId: userId })); + if (neverLock) { + // TODO: This also _sets_ the key so when we check memory in the next line it finds a key. + // We should refactor here. + await this.cryptoService.getKey(KeySuffixOptions.Auto, userId); } - startCheck() { - this.checkVaultTimeout(); - setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + return !(await this.cryptoService.hasKeyInMemory(userId)); + } + + async checkVaultTimeout(): Promise { + if (await this.platformUtilsService.isViewOpen()) { + return; } - // Keys aren't stored for a device that is locked or logged out. - async isLocked(): Promise { - // Handle never lock startup situation - if (await this.cryptoService.hasKeyStored('auto') && !this.everBeenUnlocked) { - await this.cryptoService.getKey('auto'); - } + for (const userId in this.stateService.accounts.getValue()) { + if (userId != null && (await this.shouldLock(userId))) { + await this.executeTimeoutAction(userId); + } + } + } - return !this.cryptoService.hasKeyInMemory(); + async lock(allowSoftLock = false, userId?: string): Promise { + const authed = await this.stateService.getIsAuthenticated({ userId: userId }); + if (!authed) { + return; } - async checkVaultTimeout(): Promise { - if (await this.platformUtilsService.isViewOpen()) { - // Do not lock - return; - } + if (await this.keyConnectorService.getUsesKeyConnector()) { + const pinSet = await this.isPinLockSet(); + const pinLock = + (pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) || pinSet[1]; - // "is logged out check" - similar to isLocked, below - const authed = await this.userService.isAuthenticated(); - if (!authed) { - return; - } - - if (await this.isLocked()) { - return; - } - - const vaultTimeout = await this.getVaultTimeout(); - if (vaultTimeout == null || vaultTimeout < 0) { - return; - } - - const lastActive = await this.storageService.get(ConstantsService.lastActiveKey); - if (lastActive == null) { - return; - } - - const vaultTimeoutSeconds = vaultTimeout * 60; - const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; - if (diffSeconds >= vaultTimeoutSeconds) { - // Pivot based on the saved vault timeout action - const timeoutAction = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); - timeoutAction === 'logOut' ? await this.logOut() : await this.lock(true); - } + if (!pinLock && !(await this.isBiometricLockSet())) { + await this.logOut(); + } } - async lock(allowSoftLock = false): Promise { - const authed = await this.userService.isAuthenticated(); - if (!authed) { - return; - } - - if (await this.keyConnectorService.getUsesKeyConnector()) { - const pinSet = await this.isPinLockSet(); - const pinLock = (pinSet[0] && this.pinProtectedKey != null) || pinSet[1]; - - if (!pinLock && !await this.isBiometricLockSet()) { - await this.logOut(); - } - } - - this.biometricLocked = true; - this.everBeenUnlocked = true; - await this.cryptoService.clearKey(false); - await this.cryptoService.clearOrgKeys(true); - await this.cryptoService.clearKeyPair(true); - await this.cryptoService.clearEncKey(true); - - this.folderService.clearCache(); - this.cipherService.clearCache(); - this.collectionService.clearCache(); - this.searchService.clearIndex(); - this.messagingService.send('locked'); - if (this.lockedCallback != null) { - await this.lockedCallback(); - } + if (userId == null || userId === (await this.stateService.getUserId())) { + this.searchService.clearIndex(); } - async logOut(): Promise { - if (this.loggedOutCallback != null) { - await this.loggedOutCallback(); - } + await this.stateService.setEverBeenUnlocked(true, { userId: userId }); + await this.stateService.setBiometricLocked(true, { userId: userId }); + + await this.cryptoService.clearKey(false, userId); + await this.cryptoService.clearOrgKeys(true, userId); + await this.cryptoService.clearKeyPair(true, userId); + await this.cryptoService.clearEncKey(true, userId); + + await this.folderService.clearCache(userId); + await this.cipherService.clearCache(userId); + await this.collectionService.clearCache(userId); + + this.messagingService.send("locked", { userId: userId }); + + if (this.lockedCallback != null) { + await this.lockedCallback(); + } + } + + async logOut(userId?: string): Promise { + if (this.loggedOutCallback != null) { + await this.loggedOutCallback(userId); + } + } + + async setVaultTimeoutOptions(timeout: number, action: string): Promise { + await this.stateService.setVaultTimeout(timeout); + await this.stateService.setVaultTimeoutAction(action); + await this.cryptoService.toggleKey(); + await this.tokenService.toggleTokens(); + } + + async isPinLockSet(): Promise<[boolean, boolean]> { + const protectedPin = await this.stateService.getProtectedPin(); + const pinProtectedKey = await this.stateService.getEncryptedPinProtected(); + return [protectedPin != null, pinProtectedKey != null]; + } + + async isBiometricLockSet(): Promise { + return await this.stateService.getBiometricUnlock(); + } + + async getVaultTimeout(userId?: string): Promise { + const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId }); + + if ( + await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId) + ) { + const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId); + // Remove negative values, and ensure it's smaller than maximum allowed value according to policy + let timeout = Math.min(vaultTimeout, policy[0].data.minutes); + + if (vaultTimeout == null || timeout < 0) { + timeout = policy[0].data.minutes; + } + + // We really shouldn't need to set the value here, but multiple services relies on this value being correct. + if (vaultTimeout !== timeout) { + await this.stateService.setVaultTimeout(timeout, { userId: userId }); + } + + return timeout; } - async setVaultTimeoutOptions(timeout: number, action: string): Promise { - await this.storageService.save(ConstantsService.vaultTimeoutKey, timeout); - await this.storageService.save(ConstantsService.vaultTimeoutActionKey, action); - await this.cryptoService.toggleKey(); - await this.tokenService.toggleTokens(); + return vaultTimeout; + } + + async clear(userId?: string): Promise { + await this.stateService.setEverBeenUnlocked(false, { userId: userId }); + await this.stateService.setDecryptedPinProtected(null, { userId: userId }); + await this.stateService.setProtectedPin(null, { userId: userId }); + } + + private async isLoggedOut(userId?: string): Promise { + return !(await this.stateService.getIsAuthenticated({ userId: userId })); + } + + private async shouldLock(userId: string): Promise { + if (await this.isLoggedOut(userId)) { + return false; } - async isPinLockSet(): Promise<[boolean, boolean]> { - const protectedPin = await this.storageService.get(ConstantsService.protectedPin); - const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); - return [protectedPin != null, pinProtectedKey != null]; + if (await this.isLocked(userId)) { + return false; } - async isBiometricLockSet(): Promise { - return await this.storageService.get(ConstantsService.biometricUnlockKey); + const vaultTimeout = await this.getVaultTimeout(userId); + if (vaultTimeout == null || vaultTimeout < 0) { + return false; } - async getVaultTimeout(): Promise { - const vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); - - if (await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout)) { - const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout); - // Remove negative values, and ensure it's smaller than maximum allowed value according to policy - let timeout = Math.min(vaultTimeout, policy[0].data.minutes); - - if (vaultTimeout == null || timeout < 0) { - timeout = policy[0].data.minutes; - } - - // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - if (vaultTimeout !== timeout) { - await this.storageService.save(ConstantsService.vaultTimeoutKey, timeout); - } - - return timeout; - } - - return vaultTimeout; + const lastActive = await this.stateService.getLastActive({ userId: userId }); + if (lastActive == null) { + return false; } - clear(): Promise { - this.everBeenUnlocked = false; - this.pinProtectedKey = null; - return this.storageService.remove(ConstantsService.protectedPin); - } + const vaultTimeoutSeconds = vaultTimeout * 60; + const diffSeconds = (new Date().getTime() - lastActive) / 1000; + return diffSeconds >= vaultTimeoutSeconds; + } + + private async executeTimeoutAction(userId: string): Promise { + const timeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId }); + timeoutAction === "logOut" ? await this.logOut() : await this.lock(true, userId); + } } diff --git a/electron/package-lock.json b/electron/package-lock.json index 1201fd87..1c0ff8cf 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -11,17 +11,17 @@ "dependencies": { "@bitwarden/jslib-common": "file:../common", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", - "electron": "14.2.0", + "electron": "16.0.2", "electron-log": "4.4.1", "electron-store": "8.0.1", - "electron-updater": "4.3.9", + "electron-updater": "4.6.1", "forcefocus": "^1.1.0", "keytar": "7.7.0" }, "devDependencies": { - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "../common": { @@ -36,19 +36,19 @@ "lunr": "^2.3.9", "node-forge": "^0.10.0", "papaparse": "^5.3.0", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldjs": "^2.3.1", "zxcvbn": "^4.4.2" }, "devDependencies": { "@types/lunr": "^2.3.3", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-forge": "^0.9.7", "@types/papaparse": "^5.2.5", "@types/tldjs": "^2.3.0", "@types/zxcvbn": "^4.4.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "node_modules/@bitwarden/jslib-common": { @@ -56,9 +56,9 @@ "link": true }, "node_modules/@electron/get": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.0.tgz", - "integrity": "sha512-+SjZhRuRo+STTO1Fdhzqnv9D2ZhjxXP6egsJ9kiO8dtP68cDx7dFCwWi64dlMQV7sWcfW1OYCW4wviEBzmRsfQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.1.tgz", + "integrity": "sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA==", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", @@ -72,7 +72,7 @@ "node": ">=8.6" }, "optionalDependencies": { - "global-agent": "^2.0.2", + "global-agent": "^3.0.0", "global-tunnel-ng": "^2.7.1" } }, @@ -105,19 +105,20 @@ } }, "node_modules/@types/node": { - "version": "14.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", - "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==" + "version": "16.11.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", + "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", + "dev": true }, "node_modules/@types/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==" + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==" }, "node_modules/ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -289,9 +290,9 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/builder-util-runtime": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.5.tgz", - "integrity": "sha512-fgUFHKtMNjdvH6PDRFntdIGUPgwZ69sXsAqEulCtoiqgWes5agrMq/Ud274zjJRTbckYh2PHh8/1CpFc6dpsbQ==", + "version": "8.9.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.9.1.tgz", + "integrity": "sha512-c8a8J3wK6BIVLW7ls+7TRK9igspTbzWmUqxFbgK0m40Ggm6efUbxtWVCGIjc+dtchyr5qAMAUL6iEGRdS/6vwg==", "dependencies": { "debug": "^4.3.2", "sax": "^1.2.4" @@ -381,9 +382,9 @@ } }, "node_modules/conf": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/conf/-/conf-10.0.3.tgz", - "integrity": "sha512-4gtQ/Q36qVxBzMe6B7gWOAfni1VdhuHkIzxydHkclnwGmgN+eW4bb6jj73vigCfr7d3WlmqawvhZrpCUCTPYxQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.1.1.tgz", + "integrity": "sha512-z2civwq/k8TMYtcn3SVP0Peso4otIWnHtcTuHhQ0zDZDdP4NTxqEc8owfkz4zBsdMYdn/LFcE+ZhbCeqkhtq3Q==", "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", @@ -432,17 +433,6 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, - "node_modules/core-js": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.1.tgz", - "integrity": "sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA==", - "hasInstallScript": true, - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -463,9 +453,9 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dependencies": { "ms": "2.1.2" }, @@ -556,13 +546,12 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, "node_modules/electron": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-14.2.0.tgz", - "integrity": "sha512-6CmAv1P0xcwK3FQOSA27fHI36/wctSFVgj46VODn56srXXQWeolkK1VzeAFNE613iAuuH9jJdHvE3gz+c7XkNA==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.2.tgz", + "integrity": "sha512-kT746yVMztrP4BbT3nrFNcUcfgFu2yelUw6TWBVTy0pju+fBISaqcvoiMrq+8U0vRpoXSu2MJYygOf4T0Det7g==", "hasInstallScript": true, - "license": "MIT", "dependencies": { - "@electron/get": "^1.0.1", + "@electron/get": "^1.13.0", "@types/node": "^14.6.2", "extract-zip": "^1.0.3" }, @@ -591,15 +580,15 @@ } }, "node_modules/electron-updater": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.3.9.tgz", - "integrity": "sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.6.1.tgz", + "integrity": "sha512-YsU1mHqXLrXXmBMsxhxy24PrbaB8rnpZDPmFa2gOkTYk/Ch13+R0fjsRSpPYvqtskVVY0ux8fu+HnUkVkqc7og==", "dependencies": { - "@types/semver": "^7.3.5", - "builder-util-runtime": "8.7.5", + "@types/semver": "^7.3.6", + "builder-util-runtime": "8.9.1", "fs-extra": "^10.0.0", "js-yaml": "^4.1.0", - "lazy-val": "^1.0.4", + "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "^7.3.5" @@ -651,6 +640,11 @@ "node": ">= 10.0.0" } }, + "node_modules/electron/node_modules/@types/node": { + "version": "14.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.0.tgz", + "integrity": "sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -847,13 +841,12 @@ } }, "node_modules/global-agent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.2.0.tgz", - "integrity": "sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "optional": true, "dependencies": { "boolean": "^3.0.1", - "core-js": "^3.6.5", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", @@ -1621,9 +1614,9 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "node_modules/signal-exit": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.4.tgz", - "integrity": "sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "node_modules/simple-concat": { "version": "1.0.1", @@ -1817,9 +1810,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "node_modules/typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -1867,11 +1860,11 @@ "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" }, "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "dependencies": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "node_modules/wrappy": { @@ -1901,7 +1894,7 @@ "@microsoft/signalr": "5.0.10", "@microsoft/signalr-protocol-msgpack": "5.0.10", "@types/lunr": "^2.3.3", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-forge": "^0.9.7", "@types/papaparse": "^5.2.5", "@types/tldjs": "^2.3.0", @@ -1912,21 +1905,21 @@ "node-forge": "^0.10.0", "papaparse": "^5.3.0", "rimraf": "^3.0.2", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldjs": "^2.3.1", - "typescript": "4.1.5", + "typescript": "4.3.5", "zxcvbn": "^4.4.2" } }, "@electron/get": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.0.tgz", - "integrity": "sha512-+SjZhRuRo+STTO1Fdhzqnv9D2ZhjxXP6egsJ9kiO8dtP68cDx7dFCwWi64dlMQV7sWcfW1OYCW4wviEBzmRsfQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.1.tgz", + "integrity": "sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA==", "requires": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", - "global-agent": "^2.0.2", + "global-agent": "^3.0.0", "global-tunnel-ng": "^2.7.1", "got": "^9.6.0", "progress": "^2.0.3", @@ -1956,19 +1949,20 @@ } }, "@types/node": { - "version": "14.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", - "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==" + "version": "16.11.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", + "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", + "dev": true }, "@types/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==" + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==" }, "ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -2090,9 +2084,9 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "builder-util-runtime": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.5.tgz", - "integrity": "sha512-fgUFHKtMNjdvH6PDRFntdIGUPgwZ69sXsAqEulCtoiqgWes5agrMq/Ud274zjJRTbckYh2PHh8/1CpFc6dpsbQ==", + "version": "8.9.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.9.1.tgz", + "integrity": "sha512-c8a8J3wK6BIVLW7ls+7TRK9igspTbzWmUqxFbgK0m40Ggm6efUbxtWVCGIjc+dtchyr5qAMAUL6iEGRdS/6vwg==", "requires": { "debug": "^4.3.2", "sax": "^1.2.4" @@ -2163,9 +2157,9 @@ } }, "conf": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/conf/-/conf-10.0.3.tgz", - "integrity": "sha512-4gtQ/Q36qVxBzMe6B7gWOAfni1VdhuHkIzxydHkclnwGmgN+eW4bb6jj73vigCfr7d3WlmqawvhZrpCUCTPYxQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.1.1.tgz", + "integrity": "sha512-z2civwq/k8TMYtcn3SVP0Peso4otIWnHtcTuHhQ0zDZDdP4NTxqEc8owfkz4zBsdMYdn/LFcE+ZhbCeqkhtq3Q==", "requires": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", @@ -2204,12 +2198,6 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, - "core-js": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.1.tgz", - "integrity": "sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA==", - "optional": true - }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2224,9 +2212,9 @@ } }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -2288,13 +2276,20 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, "electron": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-14.2.0.tgz", - "integrity": "sha512-6CmAv1P0xcwK3FQOSA27fHI36/wctSFVgj46VODn56srXXQWeolkK1VzeAFNE613iAuuH9jJdHvE3gz+c7XkNA==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.2.tgz", + "integrity": "sha512-kT746yVMztrP4BbT3nrFNcUcfgFu2yelUw6TWBVTy0pju+fBISaqcvoiMrq+8U0vRpoXSu2MJYygOf4T0Det7g==", "requires": { - "@electron/get": "^1.0.1", + "@electron/get": "^1.13.0", "@types/node": "^14.6.2", "extract-zip": "^1.0.3" + }, + "dependencies": { + "@types/node": { + "version": "14.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.0.tgz", + "integrity": "sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ==" + } } }, "electron-log": { @@ -2312,15 +2307,15 @@ } }, "electron-updater": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.3.9.tgz", - "integrity": "sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.6.1.tgz", + "integrity": "sha512-YsU1mHqXLrXXmBMsxhxy24PrbaB8rnpZDPmFa2gOkTYk/Ch13+R0fjsRSpPYvqtskVVY0ux8fu+HnUkVkqc7og==", "requires": { - "@types/semver": "^7.3.5", - "builder-util-runtime": "8.7.5", + "@types/semver": "^7.3.6", + "builder-util-runtime": "8.9.1", "fs-extra": "^10.0.0", "js-yaml": "^4.1.0", - "lazy-val": "^1.0.4", + "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "^7.3.5" @@ -2521,13 +2516,12 @@ } }, "global-agent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.2.0.tgz", - "integrity": "sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "optional": true, "requires": { "boolean": "^3.0.1", - "core-js": "^3.6.5", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", @@ -3131,9 +3125,9 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "signal-exit": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.4.tgz", - "integrity": "sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "simple-concat": { "version": "1.0.1", @@ -3275,9 +3269,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true }, "universalify": { @@ -3312,11 +3306,11 @@ "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" }, "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "requires": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "wrappy": { diff --git a/electron/package.json b/electron/package.json index ffd2c2f7..977261fe 100644 --- a/electron/package.json +++ b/electron/package.json @@ -20,17 +20,17 @@ "lint:fix": "tslint 'src/**/*.ts' 'spec/**/*.ts' --fix" }, "devDependencies": { - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" }, "dependencies": { "@bitwarden/jslib-common": "file:../common", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", - "electron": "14.2.0", + "electron": "16.0.2", "electron-log": "4.4.1", "electron-store": "8.0.1", - "electron-updater": "4.3.9", + "electron-updater": "4.6.1", "forcefocus": "^1.1.0", "keytar": "7.7.0" } diff --git a/electron/src/biometric.darwin.main.ts b/electron/src/biometric.darwin.main.ts index 26d3bd74..15588ac0 100644 --- a/electron/src/biometric.darwin.main.ts +++ b/electron/src/biometric.darwin.main.ts @@ -1,37 +1,34 @@ -import { ipcMain, systemPreferences } from 'electron'; +import { ipcMain, systemPreferences } from "electron"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; - -import { BiometricMain } from 'jslib-common/abstractions/biometric.main'; -import { ElectronConstants } from './electronConstants'; +import { BiometricMain } from "jslib-common/abstractions/biometric.main"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { StateService } from "jslib-common/abstractions/state.service"; export default class BiometricDarwinMain implements BiometricMain { - isError: boolean = false; + isError: boolean = false; - constructor(private storageService: StorageService, private i18nservice: I18nService) {} + constructor(private i18nservice: I18nService, private stateService: StateService) {} - async init() { - this.storageService.save(ElectronConstants.enableBiometric, await this.supportsBiometric()); - this.storageService.save(ConstantsService.biometricText, 'unlockWithTouchId'); - this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptTouchId'); + async init() { + await this.stateService.setEnableBiometric(await this.supportsBiometric()); + await this.stateService.setBiometricText("unlockWithTouchId"); + await this.stateService.setNoAutoPromptBiometricsText("noAutoPromptTouchId"); - ipcMain.on('biometric', async (event: any, message: any) => { - event.returnValue = await this.authenticateBiometric(); - }); - } - - supportsBiometric(): Promise { - return Promise.resolve(systemPreferences.canPromptTouchID()); - } - - async authenticateBiometric(): Promise { - try { - await systemPreferences.promptTouchID(this.i18nservice.t('touchIdConsentMessage')); - return true; - } catch { - return false; - } + ipcMain.on("biometric", async (event: any, message: any) => { + event.returnValue = await this.authenticateBiometric(); + }); + } + + supportsBiometric(): Promise { + return Promise.resolve(systemPreferences.canPromptTouchID()); + } + + async authenticateBiometric(): Promise { + try { + await systemPreferences.promptTouchID(this.i18nservice.t("touchIdConsentMessage")); + return true; + } catch { + return false; } + } } diff --git a/electron/src/biometric.windows.main.ts b/electron/src/biometric.windows.main.ts index d33875b7..bb6288b8 100644 --- a/electron/src/biometric.windows.main.ts +++ b/electron/src/biometric.windows.main.ts @@ -1,135 +1,144 @@ -import { ipcMain } from 'electron'; -import forceFocus from 'forcefocus'; +import { ipcMain } from "electron"; +import forceFocus from "forcefocus"; -import { ElectronConstants } from './electronConstants'; -import { WindowMain } from './window.main'; +import { WindowMain } from "./window.main"; -import { BiometricMain } from 'jslib-common/abstractions/biometric.main'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; +import { BiometricMain } from "jslib-common/abstractions/biometric.main"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { StateService } from "jslib-common/abstractions/state.service"; export default class BiometricWindowsMain implements BiometricMain { - isError: boolean = false; + isError: boolean = false; - private windowsSecurityCredentialsUiModule: any; + private windowsSecurityCredentialsUiModule: any; - constructor(private storageService: StorageService, private i18nservice: I18nService, private windowMain: WindowMain, - private logService: LogService) { } + constructor( + private i18nservice: I18nService, + private windowMain: WindowMain, + private stateService: StateService, + private logService: LogService + ) {} - async init() { - this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule(); - let supportsBiometric = false; + async init() { + this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule(); + let supportsBiometric = false; + try { + supportsBiometric = await this.supportsBiometric(); + } catch { + // store error state so we can let the user know on the settings page + this.isError = true; + } + await this.stateService.setEnableBiometric(supportsBiometric); + await this.stateService.setBiometricText("unlockWithWindowsHello"); + await this.stateService.setNoAutoPromptBiometricsText("noAutoPromptWindowsHello"); + + ipcMain.on("biometric", async (event: any, message: any) => { + event.returnValue = await this.authenticateBiometric(); + }); + } + + async supportsBiometric(): Promise { + const availability = await this.checkAvailabilityAsync(); + + return this.getAllowedAvailabilities().includes(availability); + } + + async authenticateBiometric(): Promise { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module == null) { + return false; + } + + const verification = await this.requestVerificationAsync( + this.i18nservice.t("windowsHelloConsentMessage") + ); + + return verification === module.UserConsentVerificationResult.verified; + } + + getWindowsSecurityCredentialsUiModule(): any { + try { + if (this.windowsSecurityCredentialsUiModule == null && this.getWindowsMajorVersion() >= 10) { + this.windowsSecurityCredentialsUiModule = require("@nodert-win10-rs4/windows.security.credentials.ui"); + } + return this.windowsSecurityCredentialsUiModule; + } catch { + this.isError = true; + } + return null; + } + + async checkAvailabilityAsync(): Promise { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return new Promise((resolve, reject) => { try { - supportsBiometric = await this.supportsBiometric(); - } catch { - // store error state so we can let the user know on the settings page - this.isError = true; - } - this.storageService.save(ElectronConstants.enableBiometric, supportsBiometric); - this.storageService.save(ConstantsService.biometricText, 'unlockWithWindowsHello'); - this.storageService.save(ElectronConstants.noAutoPromptBiometricsText, 'noAutoPromptWindowsHello'); - - ipcMain.on('biometric', async (event: any, message: any) => { - event.returnValue = await this.authenticateBiometric(); - }); - } - - async supportsBiometric(): Promise { - const availability = await this.checkAvailabilityAsync(); - - return this.getAllowedAvailabilities().includes(availability); - } - - async authenticateBiometric(): Promise { - const module = this.getWindowsSecurityCredentialsUiModule(); - if (module == null) { - return false; - } - - const verification = await this.requestVerificationAsync(this.i18nservice.t('windowsHelloConsentMessage')); - - return verification === module.UserConsentVerificationResult.verified; - } - - getWindowsSecurityCredentialsUiModule(): any { - try { - if (this.windowsSecurityCredentialsUiModule == null && this.getWindowsMajorVersion() >= 10) { - this.windowsSecurityCredentialsUiModule = require('@nodert-win10-rs4/windows.security.credentials.ui'); + module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => { + if (error) { + return resolve(null); } - return this.windowsSecurityCredentialsUiModule; + return resolve(result); + }); } catch { - this.isError = true; + this.isError = true; + return resolve(null); } - return null; + }); } + return Promise.resolve(null); + } - async checkAvailabilityAsync(): Promise { - const module = this.getWindowsSecurityCredentialsUiModule(); - if (module != null) { - return new Promise((resolve, reject) => { - try { - module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => { - if (error) { - return resolve(null); - } - return resolve(result); - }); - } catch { - this.isError = true; - return resolve(null); - } - }); - } - return Promise.resolve(null); - } - - async requestVerificationAsync(message: string): Promise { - const module = this.getWindowsSecurityCredentialsUiModule(); - if (module != null) { - return new Promise((resolve, reject) => { - try { - module.UserConsentVerifier.requestVerificationAsync(message, (error: Error, result: any) => { - if (error) { - return resolve(null); - } - return resolve(result); - }); - - forceFocus.focusWindow(this.windowMain.win); - } catch (error) { - this.isError = true; - return reject(error); - } - }); - } - return Promise.resolve(null); - } - - getAllowedAvailabilities(): any[] { + async requestVerificationAsync(message: string): Promise { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return new Promise((resolve, reject) => { try { - const module = this.getWindowsSecurityCredentialsUiModule(); - if (module != null) { - return [ - module.UserConsentVerifierAvailability.available, - module.UserConsentVerifierAvailability.deviceBusy, - ]; + module.UserConsentVerifier.requestVerificationAsync( + message, + (error: Error, result: any) => { + if (error) { + return resolve(null); + } + return resolve(result); } - } catch { /*Ignore error*/ } - return []; - } + ); - getWindowsMajorVersion(): number { - if (process.platform !== 'win32') { - return -1; + forceFocus.focusWindow(this.windowMain.win); + } catch (error) { + this.isError = true; + return reject(error); } - try { - const version = require('os').release(); - return Number.parseInt(version.split('.')[0], 10); - } catch { - this.logService.error('Unable to resolve windows major version number'); - } - return -1; + }); } + return Promise.resolve(null); + } + + getAllowedAvailabilities(): any[] { + try { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return [ + module.UserConsentVerifierAvailability.available, + module.UserConsentVerifierAvailability.deviceBusy, + ]; + } + } catch { + /*Ignore error*/ + } + return []; + } + + getWindowsMajorVersion(): number { + if (process.platform !== "win32") { + return -1; + } + try { + const version = require("os").release(); + return Number.parseInt(version.split(".")[0], 10); + } catch { + this.logService.error("Unable to resolve windows major version number"); + } + return -1; + } } diff --git a/electron/src/electronConstants.ts b/electron/src/electronConstants.ts deleted file mode 100644 index 68be1956..00000000 --- a/electron/src/electronConstants.ts +++ /dev/null @@ -1,14 +0,0 @@ -export class ElectronConstants { - static readonly enableMinimizeToTrayKey: string = 'enableMinimizeToTray'; - static readonly enableCloseToTrayKey: string = 'enableCloseToTray'; - static readonly enableTrayKey: string = 'enableTray'; - static readonly enableStartToTrayKey: string = 'enableStartToTrayKey'; - static readonly enableAlwaysOnTopKey: string = 'enableAlwaysOnTopKey'; - static readonly minimizeOnCopyToClipboardKey: string = 'minimizeOnCopyToClipboardKey'; - static readonly enableBiometric: string = 'enabledBiometric'; - static readonly enableBrowserIntegration: string = 'enableBrowserIntegration'; - static readonly enableBrowserIntegrationFingerprint: string = 'enableBrowserIntegrationFingerprint'; - static readonly alwaysShowDock: string = 'alwaysShowDock'; - static readonly openAtLogin: string = 'openAtLogin'; - static readonly noAutoPromptBiometricsText: string = 'noAutoPromptBiometricsText'; -} diff --git a/electron/src/services/electronCrypto.service.ts b/electron/src/services/electronCrypto.service.ts index dc602e0f..a98ad0d4 100644 --- a/electron/src/services/electronCrypto.service.ts +++ b/electron/src/services/electronCrypto.service.ts @@ -1,66 +1,74 @@ -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { KeySuffixOptions, StorageService } from 'jslib-common/abstractions/storage.service'; -import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; -import { CryptoService, Keys } from 'jslib-common/services/crypto.service'; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; + +import { CryptoService } from "jslib-common/services/crypto.service"; + +import { KeySuffixOptions } from "jslib-common/enums/keySuffixOptions"; +import { StorageLocation } from "jslib-common/enums/storageLocation"; +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; export class ElectronCryptoService extends CryptoService { + constructor( + cryptoFunctionService: CryptoFunctionService, + platformUtilService: PlatformUtilsService, + logService: LogService, + stateService: StateService + ) { + super(cryptoFunctionService, platformUtilService, logService, stateService); + } - constructor(storageService: StorageService, secureStorageService: StorageService, - cryptoFunctionService: CryptoFunctionService, platformUtilService: PlatformUtilsService, - logService: LogService) { - super(storageService, secureStorageService, cryptoFunctionService, platformUtilService, logService); + async hasKeyStored(keySuffix: KeySuffixOptions): Promise { + await this.upgradeSecurelyStoredKey(); + return super.hasKeyStored(keySuffix); + } + + protected async storeKey(key: SymmetricCryptoKey, userId?: string) { + if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) { + await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId }); + } else { + this.clearStoredKey(KeySuffixOptions.Auto); } - async hasKeyStored(keySuffix: KeySuffixOptions): Promise { - await this.upgradeSecurelyStoredKey(); - return super.hasKeyStored(keySuffix); + if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) { + await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId }); + } else { + this.clearStoredKey(KeySuffixOptions.Biometric); + } + } + + protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { + await this.upgradeSecurelyStoredKey(); + return super.retrieveKeyFromStorage(keySuffix); + } + + /** + * @deprecated 4 Jun 2021 This is temporary upgrade method to move from a single shared stored key to + * multiple, unique stored keys for each use, e.g. never logout vs. biometric authentication. + */ + private async upgradeSecurelyStoredKey() { + // attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway. + const key = await this.stateService.getCryptoMasterKeyB64(); + + if (key == null) { + return; } - protected async storeKey(key: SymmetricCryptoKey) { - if (await this.shouldStoreKey('auto')) { - await this.secureStorageService.save(Keys.key, key.keyB64, { keySuffix: 'auto' }); - } else { - this.clearStoredKey('auto'); - } - - if (await this.shouldStoreKey('biometric')) { - await this.secureStorageService.save(Keys.key, key.keyB64, { keySuffix: 'biometric' }); - } else { - this.clearStoredKey('biometric'); - } + try { + if (await this.shouldStoreKey(KeySuffixOptions.Auto)) { + await this.stateService.setCryptoMasterKeyAuto(key); + } + if (await this.shouldStoreKey(KeySuffixOptions.Biometric)) { + await this.stateService.setCryptoMasterKeyBiometric(key); + } + } catch (e) { + this.logService.error( + `Encountered error while upgrading obsolete Bitwarden secure storage item:` + ); + this.logService.error(e); } - protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { - await this.upgradeSecurelyStoredKey(); - return super.retrieveKeyFromStorage(keySuffix); - } - - /** - * @deprecated 4 Jun 2021 This is temporary upgrade method to move from a single shared stored key to - * multiple, unique stored keys for each use, e.g. never logout vs. biometric authentication. - */ - private async upgradeSecurelyStoredKey() { - // attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway. - const key = await this.secureStorageService.get(Keys.key); - - if (key == null) { - return; - } - - try { - if (await this.shouldStoreKey('auto')) { - await this.secureStorageService.save(Keys.key, key, { keySuffix: 'auto' }); - } - if (await this.shouldStoreKey('biometric')) { - await this.secureStorageService.save(Keys.key, key, { keySuffix: 'biometric' }); - } - } catch (e) { - this.logService.error(`Encountered error while upgrading obsolete Bitwarden secure storage item:`); - this.logService.error(e); - } - - await this.secureStorageService.remove(Keys.key); - } + await this.stateService.setCryptoMasterKeyB64(null); + } } diff --git a/electron/src/services/electronPlatformUtils.service.ts b/electron/src/services/electronPlatformUtils.service.ts index 7ffe3e3f..7ec81327 100644 --- a/electron/src/services/electronPlatformUtils.service.ts +++ b/electron/src/services/electronPlatformUtils.service.ts @@ -1,214 +1,218 @@ -import { - clipboard, - ipcRenderer, - shell, -} from 'electron'; +import { clipboard, ipcRenderer, shell } from "electron"; -import { - isDev, - isMacAppStore, -} from '../utils'; +import { isDev, isMacAppStore } from "../utils"; -import { DeviceType } from 'jslib-common/enums/deviceType'; -import { ThemeType } from 'jslib-common/enums/themeType'; +import { DeviceType } from "jslib-common/enums/deviceType"; +import { ThemeType } from "jslib-common/enums/themeType"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { MessagingService } from 'jslib-common/abstractions/messaging.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; - -import { ConstantsService } from 'jslib-common/services/constants.service'; - -import { ElectronConstants } from '../electronConstants'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; export class ElectronPlatformUtilsService implements PlatformUtilsService { - identityClientId: string; + identityClientId: string; - private deviceCache: DeviceType = null; + private deviceCache: DeviceType = null; - constructor(protected i18nService: I18nService, private messagingService: MessagingService, - private isDesktopApp: boolean, private storageService: StorageService) { - this.identityClientId = isDesktopApp ? 'desktop' : 'connector'; + constructor( + protected i18nService: I18nService, + private messagingService: MessagingService, + private isDesktopApp: boolean, + private stateService: StateService + ) { + this.identityClientId = isDesktopApp ? "desktop" : "connector"; + } + + getDevice(): DeviceType { + if (!this.deviceCache) { + switch (process.platform) { + case "win32": + this.deviceCache = DeviceType.WindowsDesktop; + break; + case "darwin": + this.deviceCache = DeviceType.MacOsDesktop; + break; + case "linux": + default: + this.deviceCache = DeviceType.LinuxDesktop; + break; + } } - getDevice(): DeviceType { - if (!this.deviceCache) { - switch (process.platform) { - case 'win32': - this.deviceCache = DeviceType.WindowsDesktop; - break; - case 'darwin': - this.deviceCache = DeviceType.MacOsDesktop; - break; - case 'linux': - default: - this.deviceCache = DeviceType.LinuxDesktop; - break; - } - } + return this.deviceCache; + } - return this.deviceCache; + getDeviceString(): string { + const device = DeviceType[this.getDevice()].toLowerCase(); + return device.replace("desktop", ""); + } + + isFirefox(): boolean { + return false; + } + + isChrome(): boolean { + return true; + } + + isEdge(): boolean { + return false; + } + + isOpera(): boolean { + return false; + } + + isVivaldi(): boolean { + return false; + } + + isSafari(): boolean { + return false; + } + + isIE(): boolean { + return false; + } + + isMacAppStore(): boolean { + return isMacAppStore(); + } + + isViewOpen(): Promise { + return Promise.resolve(false); + } + + launchUri(uri: string, options?: any): void { + shell.openExternal(uri); + } + + saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { + const blob = new Blob([blobData], blobOptions); + const a = win.document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = fileName; + win.document.body.appendChild(a); + a.click(); + win.document.body.removeChild(a); + } + + getApplicationVersion(): Promise { + return ipcRenderer.invoke("appVersion"); + } + + // Temporarily restricted to only Windows until https://github.com/electron/electron/pull/28349 + // has been merged and an updated electron build is available. + supportsWebAuthn(win: Window): boolean { + return process.platform === "win32"; + } + + supportsDuo(): boolean { + return true; + } + + showToast( + type: "error" | "success" | "warning" | "info", + title: string, + text: string | string[], + options?: any + ): void { + this.messagingService.send("showToast", { + text: text, + title: title, + type: type, + options: options, + }); + } + + async showDialog( + text: string, + title?: string, + confirmText?: string, + cancelText?: string, + type?: string + ): Promise { + const buttons = [confirmText == null ? this.i18nService.t("ok") : confirmText]; + if (cancelText != null) { + buttons.push(cancelText); } - getDeviceString(): string { - const device = DeviceType[this.getDevice()].toLowerCase(); - return device.replace('desktop', ''); + const result = await ipcRenderer.invoke("showMessageBox", { + type: type, + title: title, + message: title, + detail: text, + buttons: buttons, + cancelId: buttons.length === 2 ? 1 : null, + defaultId: 0, + noLink: true, + }); + + return Promise.resolve(result.response === 0); + } + + isDev(): boolean { + return isDev(); + } + + isSelfHost(): boolean { + return false; + } + + copyToClipboard(text: string, options?: any): void { + const type = options ? options.type : null; + const clearing = options ? !!options.clearing : false; + const clearMs: number = options && options.clearMs ? options.clearMs : null; + clipboard.writeText(text, type); + if (!clearing) { + this.messagingService.send("copiedToClipboard", { + clipboardValue: text, + clearMs: clearMs, + type: type, + clearing: clearing, + }); } + } - isFirefox(): boolean { - return false; + readFromClipboard(options?: any): Promise { + const type = options ? options.type : null; + return Promise.resolve(clipboard.readText(type)); + } + + async supportsBiometric(): Promise { + return await this.stateService.getEnableBiometric(); + } + + authenticateBiometric(): Promise { + return new Promise((resolve) => { + const val = ipcRenderer.sendSync("biometric", { + action: "authenticate", + }); + resolve(val); + }); + } + + getDefaultSystemTheme() { + return ipcRenderer.invoke("systemTheme"); + } + + onDefaultSystemThemeChange(callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown) { + ipcRenderer.on("systemThemeUpdated", (event, theme: ThemeType.Light | ThemeType.Dark) => + callback(theme) + ); + } + + async getEffectiveTheme() { + const theme = await this.stateService.getTheme(); + if (theme == null || theme === ThemeType.System) { + return this.getDefaultSystemTheme(); + } else { + return theme; } + } - isChrome(): boolean { - return true; - } - - isEdge(): boolean { - return false; - } - - isOpera(): boolean { - return false; - } - - isVivaldi(): boolean { - return false; - } - - isSafari(): boolean { - return false; - } - - isIE(): boolean { - return false; - } - - isMacAppStore(): boolean { - return isMacAppStore(); - } - - isViewOpen(): Promise { - return Promise.resolve(false); - } - - launchUri(uri: string, options?: any): void { - shell.openExternal(uri); - } - - saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { - const blob = new Blob([blobData], blobOptions); - const a = win.document.createElement('a'); - a.href = URL.createObjectURL(blob); - a.download = fileName; - win.document.body.appendChild(a); - a.click(); - win.document.body.removeChild(a); - } - - getApplicationVersion(): Promise { - return ipcRenderer.invoke('appVersion'); - } - - // Temporarily restricted to only Windows until https://github.com/electron/electron/pull/28349 - // has been merged and an updated electron build is available. - supportsWebAuthn(win: Window): boolean { - return process.platform === 'win32'; - } - - supportsDuo(): boolean { - return true; - } - - showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], - options?: any): void { - this.messagingService.send('showToast', { - text: text, - title: title, - type: type, - options: options, - }); - } - - async showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): - Promise { - const buttons = [confirmText == null ? this.i18nService.t('ok') : confirmText]; - if (cancelText != null) { - buttons.push(cancelText); - } - - const result = await ipcRenderer.invoke('showMessageBox', { - type: type, - title: title, - message: title, - detail: text, - buttons: buttons, - cancelId: buttons.length === 2 ? 1 : null, - defaultId: 0, - noLink: true, - }); - - return Promise.resolve(result.response === 0); - } - - isDev(): boolean { - return isDev(); - } - - isSelfHost(): boolean { - return false; - } - - copyToClipboard(text: string, options?: any): void { - const type = options ? options.type : null; - const clearing = options ? !!options.clearing : false; - const clearMs: number = options && options.clearMs ? options.clearMs : null; - clipboard.writeText(text, type); - if (!clearing) { - this.messagingService.send('copiedToClipboard', { - clipboardValue: text, - clearMs: clearMs, - type: type, - clearing: clearing, - }); - } - } - - readFromClipboard(options?: any): Promise { - const type = options ? options.type : null; - return Promise.resolve(clipboard.readText(type)); - } - - supportsBiometric(): Promise { - return this.storageService.get(ElectronConstants.enableBiometric); - } - - authenticateBiometric(): Promise { - return new Promise(resolve => { - const val = ipcRenderer.sendSync('biometric', { - action: 'authenticate', - }); - resolve(val); - }); - } - - getDefaultSystemTheme() { - return ipcRenderer.invoke('systemTheme'); - } - - onDefaultSystemThemeChange(callback: ((theme: ThemeType.Light | ThemeType.Dark) => unknown)) { - ipcRenderer.on('systemThemeUpdated', (event, theme: ThemeType.Light | ThemeType.Dark) => callback(theme)); - } - - async getEffectiveTheme() { - const theme = await this.storageService.get(ConstantsService.themeKey); - if (theme == null || theme === ThemeType.System) { - return this.getDefaultSystemTheme(); - } else { - return theme; - } - } - - supportsSecureStorage(): boolean { - return true; - } + supportsSecureStorage(): boolean { + return true; + } } diff --git a/electron/src/services/electronRendererSecureStorage.service.ts b/electron/src/services/electronRendererSecureStorage.service.ts index fb718058..5d293350 100644 --- a/electron/src/services/electronRendererSecureStorage.service.ts +++ b/electron/src/services/electronRendererSecureStorage.service.ts @@ -1,42 +1,44 @@ -import { ipcRenderer } from 'electron'; +import { ipcRenderer } from "electron"; -import { StorageService, StorageServiceOptions } from 'jslib-common/abstractions/storage.service'; +import { StorageService } from "jslib-common/abstractions/storage.service"; + +import { StorageOptions } from "jslib-common/models/domain/storageOptions"; export class ElectronRendererSecureStorageService implements StorageService { - async get(key: string, options?: StorageServiceOptions): Promise { - const val = ipcRenderer.sendSync('keytar', { - action: 'getPassword', - key: key, - keySuffix: options?.keySuffix ?? '', - }); - return Promise.resolve(val != null ? JSON.parse(val) as T : null); - } + async get(key: string, options?: StorageOptions): Promise { + const val = ipcRenderer.sendSync("keytar", { + action: "getPassword", + key: key, + keySuffix: options?.keySuffix ?? "", + }); + return Promise.resolve(val != null ? (JSON.parse(val) as T) : null); + } - async has(key: string, options?: StorageServiceOptions): Promise { - const val = ipcRenderer.sendSync('keytar', { - action: 'hasPassword', - key: key, - keySuffix: options?.keySuffix ?? '', - }); - return Promise.resolve(!!val); - } + async has(key: string, options?: StorageOptions): Promise { + const val = ipcRenderer.sendSync("keytar", { + action: "hasPassword", + key: key, + keySuffix: options?.keySuffix ?? "", + }); + return Promise.resolve(!!val); + } - async save(key: string, obj: any, options?: StorageServiceOptions): Promise { - ipcRenderer.sendSync('keytar', { - action: 'setPassword', - key: key, - keySuffix: options?.keySuffix ?? '', - value: JSON.stringify(obj), - }); - return Promise.resolve(); - } + async save(key: string, obj: any, options?: StorageOptions): Promise { + ipcRenderer.sendSync("keytar", { + action: "setPassword", + key: key, + keySuffix: options?.keySuffix ?? "", + value: JSON.stringify(obj), + }); + return Promise.resolve(); + } - async remove(key: string, options?: StorageServiceOptions): Promise { - ipcRenderer.sendSync('keytar', { - action: 'deletePassword', - key: key, - keySuffix: options?.keySuffix ?? '', - }); - return Promise.resolve(); - } + async remove(key: string, options?: StorageOptions): Promise { + ipcRenderer.sendSync("keytar", { + action: "deletePassword", + key: key, + keySuffix: options?.keySuffix ?? "", + }); + return Promise.resolve(); + } } diff --git a/electron/src/tray.main.ts b/electron/src/tray.main.ts index 30df8410..c5c3f661 100644 --- a/electron/src/tray.main.ts +++ b/electron/src/tray.main.ts @@ -1,187 +1,185 @@ -import { - app, - BrowserWindow, - Menu, - MenuItem, - MenuItemConstructorOptions, - nativeImage, - Tray, -} from 'electron'; -import * as path from 'path'; +import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron"; +import * as path from "path"; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { ElectronConstants } from './electronConstants'; -import { WindowMain } from './window.main'; +import { WindowMain } from "./window.main"; export class TrayMain { - contextMenu: Menu; + contextMenu: Menu; - private appName: string; - private tray: Tray; - private icon: string | Electron.NativeImage; - private pressedIcon: Electron.NativeImage; + private appName: string; + private tray: Tray; + private icon: string | Electron.NativeImage; + private pressedIcon: Electron.NativeImage; - constructor(private windowMain: WindowMain, private i18nService: I18nService, - private storageService: StorageService) { - if (process.platform === 'win32') { - this.icon = path.join(__dirname, '/images/icon.ico'); - } else if (process.platform === 'darwin') { - const nImage = nativeImage.createFromPath(path.join(__dirname, '/images/icon-template.png')); - nImage.setTemplateImage(true); - this.icon = nImage; - this.pressedIcon = nativeImage.createFromPath(path.join(__dirname, '/images/icon-highlight.png')); - } else { - this.icon = path.join(__dirname, '/images/icon.png'); - } + constructor( + private windowMain: WindowMain, + private i18nService: I18nService, + private stateService: StateService + ) { + if (process.platform === "win32") { + this.icon = path.join(__dirname, "/images/icon.ico"); + } else if (process.platform === "darwin") { + const nImage = nativeImage.createFromPath(path.join(__dirname, "/images/icon-template.png")); + nImage.setTemplateImage(true); + this.icon = nImage; + this.pressedIcon = nativeImage.createFromPath( + path.join(__dirname, "/images/icon-highlight.png") + ); + } else { + this.icon = path.join(__dirname, "/images/icon.png"); + } + } + + async init(appName: string, additionalMenuItems: MenuItemConstructorOptions[] = null) { + this.appName = appName; + + const menuItemOptions: MenuItemConstructorOptions[] = [ + { + label: this.i18nService.t("showHide"), + click: () => this.toggleWindow(), + }, + { type: "separator" }, + { + label: this.i18nService.t("exit"), + click: () => this.closeWindow(), + }, + ]; + + if (additionalMenuItems != null) { + menuItemOptions.splice(1, 0, ...additionalMenuItems); } - async init(appName: string, additionalMenuItems: MenuItemConstructorOptions[] = null) { - this.appName = appName; + this.contextMenu = Menu.buildFromTemplate(menuItemOptions); + if (await this.stateService.getEnableTray()) { + this.showTray(); + } + } - const menuItemOptions: MenuItemConstructorOptions[] = [{ - label: this.i18nService.t('showHide'), - click: () => this.toggleWindow(), - }, - { type: 'separator' }, - { - label: this.i18nService.t('exit'), - click: () => this.closeWindow(), - }]; + setupWindowListeners(win: BrowserWindow) { + win.on("minimize", async (e: Event) => { + if (await this.stateService.getEnableMinimizeToTray()) { + e.preventDefault(); + this.hideToTray(); + } + }); - if (additionalMenuItems != null) { - menuItemOptions.splice(1, 0, ...additionalMenuItems); + win.on("close", async (e: Event) => { + if (await this.stateService.getEnableCloseToTray()) { + if (!this.windowMain.isQuitting) { + e.preventDefault(); + this.hideToTray(); } + } + }); - this.contextMenu = Menu.buildFromTemplate(menuItemOptions); - if (await this.storageService.get(ElectronConstants.enableTrayKey)) { - this.showTray(); - } + win.on("show", async (e: Event) => { + const enableTray = await this.stateService.getEnableTray(); + if (!enableTray) { + setTimeout(() => this.removeTray(false), 100); + } + }); + } + + removeTray(showWindow = true) { + // Due to https://github.com/electron/electron/issues/17622 + // we cannot destroy the tray icon on linux. + if (this.tray != null && process.platform !== "linux") { + this.tray.destroy(); + this.tray = null; } - setupWindowListeners(win: BrowserWindow) { - win.on('minimize', async (e: Event) => { - if (await this.storageService.get(ElectronConstants.enableMinimizeToTrayKey)) { - e.preventDefault(); - this.hideToTray(); - } - }); - - win.on('close', async (e: Event) => { - if (await this.storageService.get(ElectronConstants.enableCloseToTrayKey)) { - if (!this.windowMain.isQuitting) { - e.preventDefault(); - this.hideToTray(); - } - } - }); - - win.on('show', async (e: Event) => { - const enableTray = await this.storageService.get(ElectronConstants.enableTrayKey); - if (!enableTray) { - setTimeout(() => this.removeTray(false), 100); - } + if (showWindow && this.windowMain.win != null && !this.windowMain.win.isVisible()) { + this.windowMain.win.show(); + } + } + + async hideToTray() { + this.showTray(); + if (this.windowMain.win != null) { + this.windowMain.win.hide(); + } + if (this.isDarwin() && !(await this.stateService.getAlwaysShowDock())) { + this.hideDock(); + } + } + + restoreFromTray() { + if (this.windowMain.win == null || !this.windowMain.win.isVisible()) { + this.toggleWindow(); + } + } + + showTray() { + if (this.tray != null) { + return; + } + + this.tray = new Tray(this.icon); + this.tray.setToolTip(this.appName); + this.tray.on("click", () => this.toggleWindow()); + this.tray.on("right-click", () => this.tray.popUpContextMenu(this.contextMenu)); + + if (this.pressedIcon != null) { + this.tray.setPressedImage(this.pressedIcon); + } + if (this.contextMenu != null && !this.isDarwin()) { + this.tray.setContextMenu(this.contextMenu); + } + } + + updateContextMenu() { + if (this.contextMenu != null && this.isLinux()) { + this.tray.setContextMenu(this.contextMenu); + } + } + + private hideDock() { + app.dock.hide(); + } + + private showDock() { + app.dock.show(); + } + + private isDarwin() { + return process.platform === "darwin"; + } + + private isLinux() { + return process.platform === "linux"; + } + + private async toggleWindow() { + if (this.windowMain.win == null) { + if (this.isDarwin()) { + // On MacOS, closing the window via the red button destroys the BrowserWindow instance. + this.windowMain.createWindow().then(() => { + this.windowMain.win.show(); + this.showDock(); }); + } + return; } - - removeTray(showWindow = true) { - // Due to https://github.com/electron/electron/issues/17622 - // we cannot destroy the tray icon on linux. - if (this.tray != null && process.platform !== 'linux') { - this.tray.destroy(); - this.tray = null; - } - - if (showWindow && this.windowMain.win != null && !this.windowMain.win.isVisible()) { - this.windowMain.win.show(); - } + if (this.windowMain.win.isVisible()) { + this.windowMain.win.hide(); + if (this.isDarwin() && !(await this.stateService.getAlwaysShowDock())) { + this.hideDock(); + } + } else { + this.windowMain.win.show(); + if (this.isDarwin()) { + this.showDock(); + } } + } - async hideToTray() { - this.showTray(); - if (this.windowMain.win != null) { - this.windowMain.win.hide(); - } - if (this.isDarwin() && !await this.storageService.get(ElectronConstants.alwaysShowDock)) { - this.hideDock(); - } - } - - restoreFromTray() { - if (this.windowMain.win == null || !this.windowMain.win.isVisible()) { - this.toggleWindow(); - } - } - - showTray() { - if (this.tray != null) { - return; - } - - this.tray = new Tray(this.icon); - this.tray.setToolTip(this.appName); - this.tray.on('click', () => this.toggleWindow()); - this.tray.on('right-click', () => this.tray.popUpContextMenu(this.contextMenu)); - - if (this.pressedIcon != null) { - this.tray.setPressedImage(this.pressedIcon); - } - if (this.contextMenu != null && !this.isDarwin()) { - this.tray.setContextMenu(this.contextMenu); - } - } - - updateContextMenu() { - if (this.contextMenu != null && this.isLinux()) { - this.tray.setContextMenu(this.contextMenu); - } - } - - private hideDock() { - app.dock.hide(); - } - - private showDock() { - app.dock.show(); - } - - private isDarwin() { - return process.platform === 'darwin'; - } - - private isLinux() { - return process.platform === 'linux'; - } - - private async toggleWindow() { - if (this.windowMain.win == null) { - if (this.isDarwin()) { - // On MacOS, closing the window via the red button destroys the BrowserWindow instance. - this.windowMain.createWindow().then(() => { - this.windowMain.win.show(); - this.showDock(); - }); - } - return; - } - if (this.windowMain.win.isVisible()) { - this.windowMain.win.hide(); - if (this.isDarwin() && !await this.storageService.get(ElectronConstants.alwaysShowDock)) { - this.hideDock(); - } - } else { - this.windowMain.win.show(); - if (this.isDarwin()) { - this.showDock(); - } - } - } - - private closeWindow() { - this.windowMain.isQuitting = true; - if (this.windowMain.win != null) { - this.windowMain.win.close(); - } + private closeWindow() { + this.windowMain.isQuitting = true; + if (this.windowMain.win != null) { + this.windowMain.win.close(); } + } } diff --git a/electron/src/utils.ts b/electron/src/utils.ts index ce4de605..a29a27a2 100644 --- a/electron/src/utils.ts +++ b/electron/src/utils.ts @@ -1,64 +1,76 @@ -import { ipcRenderer } from 'electron'; +import { ipcRenderer } from "electron"; -export type RendererMenuItem = {label?: string, type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'), click?: () => any}; +export type RendererMenuItem = { + label?: string; + type?: "normal" | "separator" | "submenu" | "checkbox" | "radio"; + click?: () => any; +}; export function invokeMenu(menu: RendererMenuItem[]) { - const menuWithoutClick = menu.map(m => { - return { label: m.label, type: m.type }; - }); - ipcRenderer.invoke('openContextMenu', { menu: menuWithoutClick }).then((i: number) => { - if (i !== -1) { - menu[i].click(); - } - }); + const menuWithoutClick = menu.map((m) => { + return { label: m.label, type: m.type }; + }); + ipcRenderer.invoke("openContextMenu", { menu: menuWithoutClick }).then((i: number) => { + if (i !== -1) { + menu[i].click(); + } + }); } export function isDev() { - // ref: https://github.com/sindresorhus/electron-is-dev - if ('ELECTRON_IS_DEV' in process.env) { - return parseInt(process.env.ELECTRON_IS_DEV, 10) === 1; - } - return (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath)); + // ref: https://github.com/sindresorhus/electron-is-dev + if ("ELECTRON_IS_DEV" in process.env) { + return parseInt(process.env.ELECTRON_IS_DEV, 10) === 1; + } + return process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath); } export function isAppImage() { - return process.platform === 'linux' && 'APPIMAGE' in process.env; + return process.platform === "linux" && "APPIMAGE" in process.env; +} + +export function isMac() { + return process.platform === "darwin"; } export function isMacAppStore() { - return process.platform === 'darwin' && process.mas && process.mas === true; + return isMac() && process.mas && process.mas === true; } export function isWindowsStore() { - const isWindows = process.platform === 'win32'; - let windowsStore = process.windowsStore; - if (isWindows && !windowsStore && - process.resourcesPath.indexOf('8bitSolutionsLLC.bitwardendesktop_') > -1) { - windowsStore = true; - } - return isWindows && windowsStore === true; + const isWindows = process.platform === "win32"; + let windowsStore = process.windowsStore; + if ( + isWindows && + !windowsStore && + process.resourcesPath.indexOf("8bitSolutionsLLC.bitwardendesktop_") > -1 + ) { + windowsStore = true; + } + return isWindows && windowsStore === true; } export function isSnapStore() { - return process.platform === 'linux' && process.env.SNAP_USER_DATA != null; + return process.platform === "linux" && process.env.SNAP_USER_DATA != null; } export function isWindowsPortable() { - return process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null; + return process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR != null; } /** * Sanitize user agent so external resources used by the app can't built data on our users. */ export function cleanUserAgent(userAgent: string): string { - const userAgentItem = (startString: string, endString: string) => { - const startIndex = userAgent.indexOf(startString); - return userAgent.substring(startIndex, userAgent.indexOf(endString, startIndex) + 1); - }; - const systemInformation = '(Windows NT 10.0; Win64; x64)'; + const userAgentItem = (startString: string, endString: string) => { + const startIndex = userAgent.indexOf(startString); + return userAgent.substring(startIndex, userAgent.indexOf(endString, startIndex) + 1); + }; + const systemInformation = "(Windows NT 10.0; Win64; x64)"; - // Set system information, remove bitwarden, and electron information - return userAgent.replace(userAgentItem('(', ')'), systemInformation) - .replace(userAgentItem('Bitwarden', ' '), '') - .replace(userAgentItem('Electron', ' '), ''); + // Set system information, remove bitwarden, and electron information + return userAgent + .replace(userAgentItem("(", ")"), systemInformation) + .replace(userAgentItem("Bitwarden", " "), "") + .replace(userAgentItem("Electron", " "), ""); } diff --git a/electron/src/window.main.ts b/electron/src/window.main.ts index f51dc114..97f9648b 100644 --- a/electron/src/window.main.ts +++ b/electron/src/window.main.ts @@ -1,290 +1,296 @@ -import { - app, - BrowserWindow, - screen, -} from 'electron'; -import * as path from 'path'; -import * as url from 'url'; +import { app, BrowserWindow, screen } from "electron"; +import * as path from "path"; +import * as url from "url"; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { LogService } from "jslib-common/abstractions/log.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { ElectronConstants } from './electronConstants'; -import { - cleanUserAgent, - isDev, - isMacAppStore, - isSnapStore, -} from './utils'; +import { cleanUserAgent, isDev, isMacAppStore, isSnapStore } from "./utils"; +const mainWindowSizeKey = "mainWindowSize"; const WindowEventHandlingDelay = 100; -const Keys = { - mainWindowSize: 'mainWindowSize', -}; - export class WindowMain { - win: BrowserWindow; - isQuitting: boolean = false; + win: BrowserWindow; + isQuitting: boolean = false; - private windowStateChangeTimer: NodeJS.Timer; - private windowStates: { [key: string]: any; } = {}; - private enableAlwaysOnTop: boolean = false; + private windowStateChangeTimer: NodeJS.Timer; + private windowStates: { [key: string]: any } = {}; + private enableAlwaysOnTop: boolean = false; - constructor(private storageService: StorageService, private logService: LogService, - private hideTitleBar = false, private defaultWidth = 950, private defaultHeight = 600, - private argvCallback: (argv: string[]) => void = null, - private createWindowCallback: (win: BrowserWindow) => void) { } + constructor( + private stateService: StateService, + private logService: LogService, + private hideTitleBar = false, + private defaultWidth = 950, + private defaultHeight = 600, + private argvCallback: (argv: string[]) => void = null, + private createWindowCallback: (win: BrowserWindow) => void + ) {} - init(): Promise { - return new Promise((resolve, reject) => { - try { - if (!isMacAppStore() && !isSnapStore()) { - const gotTheLock = app.requestSingleInstanceLock(); - if (!gotTheLock) { - app.quit(); - return; - } else { - app.on('second-instance', (event, argv, workingDirectory) => { - // Someone tried to run a second instance, we should focus our window. - if (this.win != null) { - if (this.win.isMinimized() || !this.win.isVisible()) { - this.win.show(); - } - this.win.focus(); - } - if (process.platform === 'win32' || process.platform === 'linux') { - if (this.argvCallback != null) { - this.argvCallback(argv); - } - } - }); - } - } - - // This method will be called when Electron is shutting - // down the application. - app.on('before-quit', () => { - this.isQuitting = true; - }); - - // This method will be called when Electron has finished - // initialization and is ready to create browser windows. - // Some APIs can only be used after this event occurs. - app.on('ready', async () => { - await this.createWindow(); - resolve(); - if (this.argvCallback != null) { - this.argvCallback(process.argv); - } - }); - - // Quit when all windows are closed. - app.on('window-all-closed', () => { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin' || this.isQuitting || isMacAppStore()) { - app.quit(); - } - }); - - app.on('activate', async () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (this.win === null) { - await this.createWindow(); - } else { - // Show the window when clicking on Dock icon - this.win.show(); - } - }); - - } catch (e) { - // Catch Error - // throw e; - reject(e); - } - }); - } - - async createWindow(): Promise { - this.windowStates[Keys.mainWindowSize] = await this.getWindowState(Keys.mainWindowSize, this.defaultWidth, - this.defaultHeight); - this.enableAlwaysOnTop = await this.storageService.get(ElectronConstants.enableAlwaysOnTopKey); - - // Create the browser window. - this.win = new BrowserWindow({ - width: this.windowStates[Keys.mainWindowSize].width, - height: this.windowStates[Keys.mainWindowSize].height, - minWidth: 680, - minHeight: 500, - x: this.windowStates[Keys.mainWindowSize].x, - y: this.windowStates[Keys.mainWindowSize].y, - title: app.name, - icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined, - titleBarStyle: this.hideTitleBar && process.platform === 'darwin' ? 'hiddenInset' : undefined, - show: false, - backgroundColor: '#fff', - alwaysOnTop: this.enableAlwaysOnTop, - webPreferences: { - nodeIntegration: true, - backgroundThrottling: false, - contextIsolation: false, - }, - }); - - if (this.windowStates[Keys.mainWindowSize].isMaximized) { - this.win.maximize(); - } - - // Show it later since it might need to be maximized. - this.win.show(); - - // and load the index.html of the app. - this.win.loadURL(url.format( - { - protocol: 'file:', - pathname: path.join(__dirname, '/index.html'), - slashes: true, - }), - { - userAgent: cleanUserAgent(this.win.webContents.userAgent), - } - ); - - // Open the DevTools. - if (isDev()) { - this.win.webContents.openDevTools(); - } - - // Emitted when the window is closed. - this.win.on('closed', async () => { - await this.updateWindowState(Keys.mainWindowSize, this.win); - - // Dereference the window object, usually you would store window - // in an array if your app supports multi windows, this is the time - // when you should delete the corresponding element. - this.win = null; - }); - - this.win.on('close', async () => { - await this.updateWindowState(Keys.mainWindowSize, this.win); - }); - - this.win.on('maximize', async () => { - await this.updateWindowState(Keys.mainWindowSize, this.win); - }); - - this.win.on('unmaximize', async () => { - await this.updateWindowState(Keys.mainWindowSize, this.win); - }); - - this.win.on('resize', () => { - this.windowStateChangeHandler(Keys.mainWindowSize, this.win); - }); - - this.win.on('move', () => { - this.windowStateChangeHandler(Keys.mainWindowSize, this.win); - }); - this.win.on('focus', () => { - this.win.webContents.send('messagingService', { - command: 'windowIsFocused', - windowIsFocused: true, - }); - }); - - if (this.createWindowCallback) { - this.createWindowCallback(this.win); - } - } - - async toggleAlwaysOnTop() { - this.enableAlwaysOnTop = !this.win.isAlwaysOnTop(); - this.win.setAlwaysOnTop(this.enableAlwaysOnTop); - await this.storageService.save(ElectronConstants.enableAlwaysOnTopKey, this.enableAlwaysOnTop); - } - - private windowStateChangeHandler(configKey: string, win: BrowserWindow) { - global.clearTimeout(this.windowStateChangeTimer); - this.windowStateChangeTimer = global.setTimeout(async () => { - await this.updateWindowState(configKey, win); - }, WindowEventHandlingDelay); - } - - private async updateWindowState(configKey: string, win: BrowserWindow) { - if (win == null) { + init(): Promise { + return new Promise((resolve, reject) => { + try { + if (!isMacAppStore() && !isSnapStore()) { + const gotTheLock = app.requestSingleInstanceLock(); + if (!gotTheLock) { + app.quit(); return; - } - - try { - const bounds = win.getBounds(); - - if (this.windowStates[configKey] == null) { - this.windowStates[configKey] = await this.storageService.get(configKey); - if (this.windowStates[configKey] == null) { - this.windowStates[configKey] = {}; + } else { + app.on("second-instance", (event, argv, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (this.win != null) { + if (this.win.isMinimized() || !this.win.isVisible()) { + this.win.show(); } - } - - this.windowStates[configKey].isMaximized = win.isMaximized(); - this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds; - - if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) { - this.windowStates[configKey].x = bounds.x; - this.windowStates[configKey].y = bounds.y; - this.windowStates[configKey].width = bounds.width; - this.windowStates[configKey].height = bounds.height; - } - - await this.storageService.save(configKey, this.windowStates[configKey]); - } catch (e) { - this.logService.error(e); - } - } - - private async getWindowState(configKey: string, defaultWidth: number, defaultHeight: number) { - let state = await this.storageService.get(configKey); - - const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized); - let displayBounds: Electron.Rectangle = null; - if (!isValid) { - state = { - width: defaultWidth, - height: defaultHeight, - }; - - displayBounds = screen.getPrimaryDisplay().bounds; - } else if (this.stateHasBounds(state) && state.displayBounds) { - // Check if the display where the window was last open is still available - displayBounds = screen.getDisplayMatching(state.displayBounds).bounds; - - if (displayBounds.width !== state.displayBounds.width || - displayBounds.height !== state.displayBounds.height || - displayBounds.x !== state.displayBounds.x || - displayBounds.y !== state.displayBounds.y) { - state.x = undefined; - state.y = undefined; - displayBounds = screen.getPrimaryDisplay().bounds; - } + this.win.focus(); + } + if (process.platform === "win32" || process.platform === "linux") { + if (this.argvCallback != null) { + this.argvCallback(argv); + } + } + }); + } } - if (displayBounds != null) { - if (state.width > displayBounds.width && state.height > displayBounds.height) { - state.isMaximized = true; - } + // This method will be called when Electron is shutting + // down the application. + app.on("before-quit", () => { + this.isQuitting = true; + }); - if (state.width > displayBounds.width) { - state.width = displayBounds.width - 10; - } - if (state.height > displayBounds.height) { - state.height = displayBounds.height - 10; - } + // This method will be called when Electron has finished + // initialization and is ready to create browser windows. + // Some APIs can only be used after this event occurs. + app.on("ready", async () => { + await this.createWindow(); + resolve(); + if (this.argvCallback != null) { + this.argvCallback(process.argv); + } + }); + + // Quit when all windows are closed. + app.on("window-all-closed", () => { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== "darwin" || this.isQuitting || isMacAppStore()) { + app.quit(); + } + }); + + app.on("activate", async () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (this.win === null) { + await this.createWindow(); + } else { + // Show the window when clicking on Dock icon + this.win.show(); + } + }); + } catch (e) { + // Catch Error + // throw e; + reject(e); + } + }); + } + + async createWindow(): Promise { + this.windowStates[mainWindowSizeKey] = await this.getWindowState( + mainWindowSizeKey, + this.defaultWidth, + this.defaultHeight + ); + this.enableAlwaysOnTop = await this.stateService.getEnableAlwaysOnTop(); + + // Create the browser window. + this.win = new BrowserWindow({ + width: this.windowStates[mainWindowSizeKey].width, + height: this.windowStates[mainWindowSizeKey].height, + minWidth: 680, + minHeight: 500, + x: this.windowStates[mainWindowSizeKey].x, + y: this.windowStates[mainWindowSizeKey].y, + title: app.name, + icon: process.platform === "linux" ? path.join(__dirname, "/images/icon.png") : undefined, + titleBarStyle: this.hideTitleBar && process.platform === "darwin" ? "hiddenInset" : undefined, + show: false, + backgroundColor: "#fff", + alwaysOnTop: this.enableAlwaysOnTop, + webPreferences: { + nodeIntegration: true, + backgroundThrottling: false, + contextIsolation: false, + }, + }); + + if (this.windowStates[mainWindowSizeKey].isMaximized) { + this.win.maximize(); + } + + // Show it later since it might need to be maximized. + this.win.show(); + + // and load the index.html of the app. + this.win.loadURL( + url.format({ + protocol: "file:", + pathname: path.join(__dirname, "/index.html"), + slashes: true, + }), + { + userAgent: cleanUserAgent(this.win.webContents.userAgent), + } + ); + + // Open the DevTools. + if (isDev()) { + this.win.webContents.openDevTools(); + } + + // Emitted when the window is closed. + this.win.on("closed", async () => { + await this.updateWindowState(mainWindowSizeKey, this.win); + + // Dereference the window object, usually you would store window + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + this.win = null; + }); + + this.win.on("close", async () => { + await this.updateWindowState(mainWindowSizeKey, this.win); + }); + + this.win.on("maximize", async () => { + await this.updateWindowState(mainWindowSizeKey, this.win); + }); + + this.win.on("unmaximize", async () => { + await this.updateWindowState(mainWindowSizeKey, this.win); + }); + + this.win.on("resize", () => { + this.windowStateChangeHandler(mainWindowSizeKey, this.win); + }); + + this.win.on("move", () => { + this.windowStateChangeHandler(mainWindowSizeKey, this.win); + }); + this.win.on("focus", () => { + this.win.webContents.send("messagingService", { + command: "windowIsFocused", + windowIsFocused: true, + }); + }); + + if (this.createWindowCallback) { + this.createWindowCallback(this.win); + } + } + + async toggleAlwaysOnTop() { + this.enableAlwaysOnTop = !this.win.isAlwaysOnTop(); + this.win.setAlwaysOnTop(this.enableAlwaysOnTop); + await this.stateService.setEnableAlwaysOnTop(this.enableAlwaysOnTop); + } + + private windowStateChangeHandler(configKey: string, win: BrowserWindow) { + global.clearTimeout(this.windowStateChangeTimer); + this.windowStateChangeTimer = global.setTimeout(async () => { + await this.updateWindowState(configKey, win); + }, WindowEventHandlingDelay); + } + + private async updateWindowState(configKey: string, win: BrowserWindow) { + if (win == null) { + return; + } + + try { + const bounds = win.getBounds(); + + if (this.windowStates[configKey] == null) { + this.windowStates[configKey] = (await this.stateService.getWindow()).get(configKey); + if (this.windowStates[configKey] == null) { + this.windowStates[configKey] = {}; } + } - return state; + this.windowStates[configKey].isMaximized = win.isMaximized(); + this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds; + + if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) { + this.windowStates[configKey].x = bounds.x; + this.windowStates[configKey].y = bounds.y; + this.windowStates[configKey].width = bounds.width; + this.windowStates[configKey].height = bounds.height; + } + + const cachedWindow = (await this.stateService.getWindow()) ?? new Map(); + cachedWindow.set(configKey, this.windowStates[configKey]); + await this.stateService.setWindow(cachedWindow); + } catch (e) { + this.logService.error(e); + } + } + + private async getWindowState(configKey: string, defaultWidth: number, defaultHeight: number) { + const windowState = (await this.stateService.getWindow()) ?? new Map(); + let state = windowState.has(configKey) ? windowState.get(configKey) : null; + + const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized); + let displayBounds: Electron.Rectangle = null; + if (!isValid) { + state = { + width: defaultWidth, + height: defaultHeight, + }; + + displayBounds = screen.getPrimaryDisplay().bounds; + } else if (this.stateHasBounds(state) && state.displayBounds) { + // Check if the display where the window was last open is still available + displayBounds = screen.getDisplayMatching(state.displayBounds).bounds; + + if ( + displayBounds.width !== state.displayBounds.width || + displayBounds.height !== state.displayBounds.height || + displayBounds.x !== state.displayBounds.x || + displayBounds.y !== state.displayBounds.y + ) { + state.x = undefined; + state.y = undefined; + displayBounds = screen.getPrimaryDisplay().bounds; + } } - private stateHasBounds(state: any): boolean { - return state != null && Number.isInteger(state.x) && Number.isInteger(state.y) && - Number.isInteger(state.width) && state.width > 0 && Number.isInteger(state.height) && state.height > 0; + if (displayBounds != null) { + if (state.width > displayBounds.width && state.height > displayBounds.height) { + state.isMaximized = true; + } + + if (state.width > displayBounds.width) { + state.width = displayBounds.width - 10; + } + if (state.height > displayBounds.height) { + state.height = displayBounds.height - 10; + } } + + return state; + } + + private stateHasBounds(state: any): boolean { + return ( + state != null && + Number.isInteger(state.x) && + Number.isInteger(state.y) && + Number.isInteger(state.width) && + state.width > 0 && + Number.isInteger(state.height) && + state.height > 0 + ); + } } diff --git a/node/package-lock.json b/node/package-lock.json index ef230d0f..ab1b64a3 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -21,13 +21,14 @@ "devDependencies": { "@types/inquirer": "^7.3.1", "@types/lowdb": "^1.0.10", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-fetch": "^2.5.10", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "../common": { + "name": "@bitwarden/jslib-common", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -38,19 +39,19 @@ "lunr": "^2.3.9", "node-forge": "^0.10.0", "papaparse": "^5.3.0", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldjs": "^2.3.1", "zxcvbn": "^4.4.2" }, "devDependencies": { "@types/lunr": "^2.3.3", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-forge": "^0.9.7", "@types/papaparse": "^5.2.5", "@types/tldjs": "^2.3.0", "@types/zxcvbn": "^4.4.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "node_modules/@bitwarden/jslib-common": { @@ -59,7 +60,6 @@ }, "node_modules/@types/inquirer": { "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz", "integrity": "sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ==", "dev": true, "dependencies": { @@ -69,13 +69,11 @@ }, "node_modules/@types/lodash": { "version": "4.14.175", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz", "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==", "dev": true }, "node_modules/@types/lowdb": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/lowdb/-/lowdb-1.0.11.tgz", "integrity": "sha512-h99VMxvTuz+VsXUVCCJo4dsps4vbkXwvU71TpmxDoiBU24bJ0VBygIHgmMm+UPoQIFihmV6euRik4z8J7XDJWg==", "dev": true, "dependencies": { @@ -83,14 +81,13 @@ } }, "node_modules/@types/node": { - "version": "14.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", - "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==", + "version": "16.11.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", + "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", "dev": true }, "node_modules/@types/node-fetch": { "version": "2.5.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", "dev": true, "dependencies": { @@ -100,7 +97,6 @@ }, "node_modules/@types/node-fetch/node_modules/form-data": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dev": true, "dependencies": { @@ -114,7 +110,6 @@ }, "node_modules/@types/through": { "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", "integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==", "dev": true, "dependencies": { @@ -123,7 +118,6 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dependencies": { "debug": "4" @@ -134,7 +128,6 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dependencies": { "type-fest": "^0.21.3" @@ -148,7 +141,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { "node": ">=8" @@ -156,7 +148,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" @@ -170,18 +161,15 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { @@ -191,7 +179,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { "ansi-styles": "^4.1.0", @@ -206,12 +193,10 @@ }, "node_modules/chardet": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "node_modules/cli-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dependencies": { "restore-cursor": "^3.1.0" @@ -222,7 +207,6 @@ }, "node_modules/cli-width": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "engines": { "node": ">= 10" @@ -230,7 +214,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" @@ -241,12 +224,10 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { "delayed-stream": "~1.0.0" @@ -264,13 +245,11 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "node_modules/debug": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dependencies": { "ms": "2.1.2" @@ -286,7 +265,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "engines": { "node": ">=0.4.0" @@ -294,12 +272,10 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "engines": { "node": ">=0.8.0" @@ -307,7 +283,6 @@ }, "node_modules/external-editor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dependencies": { "chardet": "^0.7.0", @@ -320,7 +295,6 @@ }, "node_modules/figures": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dependencies": { "escape-string-regexp": "^1.0.5" @@ -346,13 +320,11 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, "node_modules/glob": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "dependencies": { @@ -372,12 +344,10 @@ }, "node_modules/graceful-fs": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { "node": ">=8" @@ -385,7 +355,6 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "dependencies": { "agent-base": "6", @@ -397,7 +366,6 @@ }, "node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -408,7 +376,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "dependencies": { @@ -418,13 +385,11 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, "node_modules/inquirer": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.0.0.tgz", "integrity": "sha512-ON8pEJPPCdyjxj+cxsYRe6XfCJepTxANdNnTebsTuQgXpRyZRRT9t4dJwjRubgmvn20CLSEnozRUayXyM9VTXA==", "dependencies": { "ansi-escapes": "^4.2.1", @@ -447,7 +412,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "engines": { "node": ">=8" @@ -455,17 +419,14 @@ }, "node_modules/is-promise": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lowdb": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", "dependencies": { "graceful-fs": "^4.1.3", @@ -479,19 +440,17 @@ } }, "node_modules/mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "version": "1.50.0", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.33", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", "dependencies": { - "mime-db": "1.49.0" + "mime-db": "1.50.0" }, "engines": { "node": ">= 0.6" @@ -499,7 +458,6 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "engines": { "node": ">=6" @@ -507,7 +465,6 @@ }, "node_modules/minimatch": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "dependencies": { @@ -519,17 +476,14 @@ }, "node_modules/ms": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stream": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "node_modules/node-fetch": { "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", "dependencies": { "whatwg-url": "^5.0.0" @@ -540,7 +494,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "dependencies": { @@ -549,7 +502,6 @@ }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dependencies": { "mimic-fn": "^2.1.0" @@ -563,7 +515,6 @@ }, "node_modules/os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "engines": { "node": ">=0.10.0" @@ -571,7 +522,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "engines": { @@ -580,7 +530,6 @@ }, "node_modules/pify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "engines": { "node": ">=4" @@ -588,7 +537,6 @@ }, "node_modules/restore-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dependencies": { "onetime": "^5.1.0", @@ -600,7 +548,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "dependencies": { @@ -615,7 +562,6 @@ }, "node_modules/run-async": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "engines": { "node": ">=0.12.0" @@ -623,7 +569,6 @@ }, "node_modules/rxjs": { "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dependencies": { "tslib": "^1.9.0" @@ -634,17 +579,14 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/signal-exit": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.4.tgz", - "integrity": "sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q==" + "version": "3.0.5", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" }, "node_modules/steno": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", "dependencies": { "graceful-fs": "^4.1.3" @@ -652,7 +594,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { "emoji-regex": "^8.0.0", @@ -665,7 +606,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { "ansi-regex": "^5.0.1" @@ -676,7 +616,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { "has-flag": "^4.0.0" @@ -687,12 +626,10 @@ }, "node_modules/through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "node_modules/tmp": { "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dependencies": { "os-tmpdir": "~1.0.2" @@ -703,17 +640,14 @@ }, "node_modules/tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "node_modules/tslib": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "engines": { "node": ">=10" @@ -723,9 +657,9 @@ } }, "node_modules/typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -737,12 +671,10 @@ }, "node_modules/webidl-conversions": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "node_modules/whatwg-url": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "dependencies": { "tr46": "~0.0.3", @@ -751,7 +683,6 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true } @@ -763,7 +694,7 @@ "@microsoft/signalr": "5.0.10", "@microsoft/signalr-protocol-msgpack": "5.0.10", "@types/lunr": "^2.3.3", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-forge": "^0.9.7", "@types/papaparse": "^5.2.5", "@types/tldjs": "^2.3.0", @@ -774,15 +705,14 @@ "node-forge": "^0.10.0", "papaparse": "^5.3.0", "rimraf": "^3.0.2", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldjs": "^2.3.1", - "typescript": "4.1.5", + "typescript": "4.3.5", "zxcvbn": "^4.4.2" } }, "@types/inquirer": { "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz", "integrity": "sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ==", "dev": true, "requires": { @@ -792,13 +722,11 @@ }, "@types/lodash": { "version": "4.14.175", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz", "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==", "dev": true }, "@types/lowdb": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/lowdb/-/lowdb-1.0.11.tgz", "integrity": "sha512-h99VMxvTuz+VsXUVCCJo4dsps4vbkXwvU71TpmxDoiBU24bJ0VBygIHgmMm+UPoQIFihmV6euRik4z8J7XDJWg==", "dev": true, "requires": { @@ -806,14 +734,13 @@ } }, "@types/node": { - "version": "14.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", - "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==", + "version": "16.11.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", + "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", "dev": true }, "@types/node-fetch": { "version": "2.5.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", "dev": true, "requires": { @@ -823,7 +750,6 @@ "dependencies": { "form-data": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dev": true, "requires": { @@ -836,7 +762,6 @@ }, "@types/through": { "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", "integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==", "dev": true, "requires": { @@ -845,7 +770,6 @@ }, "agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "requires": { "debug": "4" @@ -853,7 +777,6 @@ }, "ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "requires": { "type-fest": "^0.21.3" @@ -861,12 +784,10 @@ }, "ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { "color-convert": "^2.0.1" @@ -874,18 +795,15 @@ }, "asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, "brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { @@ -895,7 +813,6 @@ }, "chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "requires": { "ansi-styles": "^4.1.0", @@ -904,12 +821,10 @@ }, "chardet": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "cli-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "requires": { "restore-cursor": "^3.1.0" @@ -917,12 +832,10 @@ }, "cli-width": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" }, "color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { "color-name": "~1.1.4" @@ -930,29 +843,26 @@ }, "color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } }, "commander": { - "version": "7.2.0" + "version": "7.2.0", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" }, "concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "debug": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" @@ -960,22 +870,18 @@ }, "delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "external-editor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "requires": { "chardet": "^0.7.0", @@ -985,7 +891,6 @@ }, "figures": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "requires": { "escape-string-regexp": "^1.0.5" @@ -993,6 +898,7 @@ }, "form-data": { "version": "4.0.0", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -1001,13 +907,11 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, "glob": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { @@ -1021,17 +925,14 @@ }, "graceful-fs": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "https-proxy-agent": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "requires": { "agent-base": "6", @@ -1040,7 +941,6 @@ }, "iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -1048,7 +948,6 @@ }, "inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { @@ -1058,13 +957,11 @@ }, "inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, "inquirer": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.0.0.tgz", "integrity": "sha512-ON8pEJPPCdyjxj+cxsYRe6XfCJepTxANdNnTebsTuQgXpRyZRRT9t4dJwjRubgmvn20CLSEnozRUayXyM9VTXA==", "requires": { "ansi-escapes": "^4.2.1", @@ -1084,22 +981,18 @@ }, "is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-promise": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, "lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lowdb": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", "requires": { "graceful-fs": "^4.1.3", @@ -1110,26 +1003,22 @@ } }, "mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" + "version": "1.50.0", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" }, "mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.33", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", "requires": { - "mime-db": "1.49.0" + "mime-db": "1.50.0" } }, "mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "minimatch": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { @@ -1138,17 +1027,14 @@ }, "ms": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mute-stream": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "node-fetch": { "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", "requires": { "whatwg-url": "^5.0.0" @@ -1156,7 +1042,6 @@ }, "once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { @@ -1165,7 +1050,6 @@ }, "onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "requires": { "mimic-fn": "^2.1.0" @@ -1173,23 +1057,19 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, "pify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" }, "restore-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "requires": { "onetime": "^5.1.0", @@ -1198,7 +1078,6 @@ }, "rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { @@ -1207,12 +1086,10 @@ }, "run-async": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" }, "rxjs": { "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "requires": { "tslib": "^1.9.0" @@ -1220,17 +1097,14 @@ }, "safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "signal-exit": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.4.tgz", - "integrity": "sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q==" + "version": "3.0.5", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" }, "steno": { "version": "0.4.4", - "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", "requires": { "graceful-fs": "^4.1.3" @@ -1238,7 +1112,6 @@ }, "string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "requires": { "emoji-regex": "^8.0.0", @@ -1248,7 +1121,6 @@ }, "strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "requires": { "ansi-regex": "^5.0.1" @@ -1256,7 +1128,6 @@ }, "supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { "has-flag": "^4.0.0" @@ -1264,12 +1135,10 @@ }, "through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "tmp": { "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "requires": { "os-tmpdir": "~1.0.2" @@ -1277,33 +1146,28 @@ }, "tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "tslib": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" }, "typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true }, "webidl-conversions": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "whatwg-url": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "requires": { "tr46": "~0.0.3", @@ -1312,7 +1176,6 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true } diff --git a/node/package.json b/node/package.json index 607cfce1..af6c0e3a 100644 --- a/node/package.json +++ b/node/package.json @@ -22,10 +22,10 @@ "devDependencies": { "@types/inquirer": "^7.3.1", "@types/lowdb": "^1.0.10", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-fetch": "^2.5.10", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" }, "dependencies": { "@bitwarden/jslib-common": "file:../common", diff --git a/node/src/cli/baseProgram.ts b/node/src/cli/baseProgram.ts index 7772ba80..e8c15f9a 100644 --- a/node/src/cli/baseProgram.ts +++ b/node/src/cli/baseProgram.ts @@ -1,108 +1,113 @@ -import * as chalk from 'chalk'; +import * as chalk from "chalk"; -import { Response } from './models/response'; -import { ListResponse } from './models/response/listResponse'; -import { MessageResponse } from './models/response/messageResponse'; -import { StringResponse } from './models/response/stringResponse'; +import { StateService } from "jslib-common/abstractions/state.service"; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { Response } from "./models/response"; +import { ListResponse } from "./models/response/listResponse"; +import { MessageResponse } from "./models/response/messageResponse"; +import { StringResponse } from "./models/response/stringResponse"; export abstract class BaseProgram { - constructor( - private userService: UserService, - private writeLn: (s: string, finalLine: boolean, error: boolean) => void) { } + constructor( + private stateService: StateService, + private writeLn: (s: string, finalLine: boolean, error: boolean) => void + ) {} - protected processResponse(response: Response, exitImmediately = false, dataProcessor: () => string = null) { - if (!response.success) { - if (process.env.BW_QUIET !== 'true') { - if (process.env.BW_RESPONSE === 'true') { - this.writeLn(this.getJson(response), true, false); - } else { - this.writeLn(chalk.redBright(response.message), true, true); - } - } - const exitCode = process.env.BW_CLEANEXIT ? 0 : 1; - if (exitImmediately) { - process.exit(exitCode); - } else { - process.exitCode = exitCode; - } - return; - } - - if (process.env.BW_RESPONSE === 'true') { - this.writeLn(this.getJson(response), true, false); - } else if (response.data != null) { - let out: string = dataProcessor != null ? dataProcessor() : null; - if (out == null) { - if (response.data.object === 'string') { - const data = (response.data as StringResponse).data; - if (data != null) { - out = data; - } - } else if (response.data.object === 'list') { - out = this.getJson((response.data as ListResponse).data); - } else if (response.data.object === 'message') { - out = this.getMessage(response); - } else { - out = this.getJson(response.data); - } - } - - if (out != null && process.env.BW_QUIET !== 'true') { - this.writeLn(out, true, false); - } - } - if (exitImmediately) { - process.exit(0); + protected processResponse( + response: Response, + exitImmediately = false, + dataProcessor: () => string = null + ) { + if (!response.success) { + if (process.env.BW_QUIET !== "true") { + if (process.env.BW_RESPONSE === "true") { + this.writeLn(this.getJson(response), true, false); } else { - process.exitCode = 0; + this.writeLn(chalk.redBright(response.message), true, true); } + } + const exitCode = process.env.BW_CLEANEXIT ? 0 : 1; + if (exitImmediately) { + process.exit(exitCode); + } else { + process.exitCode = exitCode; + } + return; } - protected getJson(obj: any): string { - if (process.env.BW_PRETTY === 'true') { - return JSON.stringify(obj, null, ' '); + if (process.env.BW_RESPONSE === "true") { + this.writeLn(this.getJson(response), true, false); + } else if (response.data != null) { + let out: string = dataProcessor != null ? dataProcessor() : null; + if (out == null) { + if (response.data.object === "string") { + const data = (response.data as StringResponse).data; + if (data != null) { + out = data; + } + } else if (response.data.object === "list") { + out = this.getJson((response.data as ListResponse).data); + } else if (response.data.object === "message") { + out = this.getMessage(response); } else { - return JSON.stringify(obj); + out = this.getJson(response.data); } + } + + if (out != null && process.env.BW_QUIET !== "true") { + this.writeLn(out, true, false); + } + } + if (exitImmediately) { + process.exit(0); + } else { + process.exitCode = 0; + } + } + + protected getJson(obj: any): string { + if (process.env.BW_PRETTY === "true") { + return JSON.stringify(obj, null, " "); + } else { + return JSON.stringify(obj); + } + } + + protected getMessage(response: Response): string { + const message = response.data as MessageResponse; + if (process.env.BW_RAW === "true") { + return message.raw; } - protected getMessage(response: Response): string { - const message = (response.data as MessageResponse); - if (process.env.BW_RAW === 'true') { - return message.raw; - } - - let out: string = ''; - if (message.title != null) { - if (message.noColor) { - out = message.title; - } else { - out = chalk.greenBright(message.title); - } - } - if (message.message != null) { - if (message.title != null) { - out += '\n'; - } - out += message.message; - } - return out.trim() === '' ? null : out; + let out: string = ""; + if (message.title != null) { + if (message.noColor) { + out = message.title; + } else { + out = chalk.greenBright(message.title); + } } - - protected async exitIfAuthed() { - const authed = await this.userService.isAuthenticated(); - if (authed) { - const email = await this.userService.getEmail(); - this.processResponse(Response.error('You are already logged in as ' + email + '.'), true); - } + if (message.message != null) { + if (message.title != null) { + out += "\n"; + } + out += message.message; } + return out.trim() === "" ? null : out; + } - protected async exitIfNotAuthed() { - const authed = await this.userService.isAuthenticated(); - if (!authed) { - this.processResponse(Response.error('You are not logged in.'), true); - } + protected async exitIfAuthed() { + const authed = await this.stateService.getIsAuthenticated(); + if (authed) { + const email = await this.stateService.getEmail(); + this.processResponse(Response.error("You are already logged in as " + email + "."), true); } + } + + protected async exitIfNotAuthed() { + const authed = await this.stateService.getIsAuthenticated(); + if (!authed) { + this.processResponse(Response.error("You are not logged in."), true); + } + } } diff --git a/node/src/cli/commands/login.command.ts b/node/src/cli/commands/login.command.ts index 7cdb607e..c1e7caeb 100644 --- a/node/src/cli/commands/login.command.ts +++ b/node/src/cli/commands/login.command.ts @@ -1,572 +1,685 @@ -import * as program from 'commander'; -import * as http from 'http'; -import * as inquirer from 'inquirer'; +import * as program from "commander"; +import * as http from "http"; +import * as inquirer from "inquirer"; -import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType'; +import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; -import { AuthResult } from 'jslib-common/models/domain/authResult'; -import { TwoFactorEmailRequest } from 'jslib-common/models/request/twoFactorEmailRequest'; -import { ErrorResponse } from 'jslib-common/models/response/errorResponse'; +import { AuthResult } from "jslib-common/models/domain/authResult"; +import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest"; +import { ErrorResponse } from "jslib-common/models/response/errorResponse"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { AuthService } from 'jslib-common/abstractions/auth.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; -import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; -import { PolicyService } from 'jslib-common/abstractions/policy.service'; -import { SyncService } from 'jslib-common/abstractions/sync.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { SyncService } from "jslib-common/abstractions/sync.service"; -import { Response } from '../models/response'; +import { Response } from "../models/response"; -import { KeyConnectorUserKeyRequest } from 'jslib-common/models/request/keyConnectorUserKeyRequest'; -import { UpdateTempPasswordRequest } from 'jslib-common/models/request/updateTempPasswordRequest'; +import { KeyConnectorUserKeyRequest } from "jslib-common/models/request/keyConnectorUserKeyRequest"; +import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest"; -import { MessageResponse } from '../models/response/messageResponse'; +import { MessageResponse } from "../models/response/messageResponse"; -import { NodeUtils } from 'jslib-common/misc/nodeUtils'; -import { Utils } from 'jslib-common/misc/utils'; +import { NodeUtils } from "jslib-common/misc/nodeUtils"; +import { Utils } from "jslib-common/misc/utils"; // tslint:disable-next-line -const open = require('open'); +const open = require("open"); export class LoginCommand { - protected validatedParams: () => Promise; - protected success: () => Promise; - protected logout: () => Promise; - protected canInteract: boolean; - protected clientId: string; - protected clientSecret: string; - protected email: string; + protected validatedParams: () => Promise; + protected success: () => Promise; + protected logout: () => Promise; + protected canInteract: boolean; + protected clientId: string; + protected clientSecret: string; + protected email: string; - private ssoRedirectUri: string = null; + private ssoRedirectUri: string = null; - constructor(protected authService: AuthService, protected apiService: ApiService, - protected i18nService: I18nService, protected environmentService: EnvironmentService, - protected passwordGenerationService: PasswordGenerationService, - protected cryptoFunctionService: CryptoFunctionService, protected platformUtilsService: PlatformUtilsService, - protected userService: UserService, protected cryptoService: CryptoService, - protected policyService: PolicyService, clientId: string, private syncService: SyncService, - protected keyConnectorService: KeyConnectorService) { - this.clientId = clientId; + constructor( + protected authService: AuthService, + protected apiService: ApiService, + protected i18nService: I18nService, + protected environmentService: EnvironmentService, + protected passwordGenerationService: PasswordGenerationService, + protected cryptoFunctionService: CryptoFunctionService, + protected platformUtilsService: PlatformUtilsService, + protected stateService: StateService, + protected cryptoService: CryptoService, + protected policyService: PolicyService, + clientId: string, + private syncService: SyncService, + protected keyConnectorService: KeyConnectorService + ) { + this.clientId = clientId; + } + + async run(email: string, password: string, options: program.OptionValues) { + this.canInteract = process.env.BW_NOINTERACTION !== "true"; + + let ssoCodeVerifier: string = null; + let ssoCode: string = null; + let orgIdentifier: string = null; + + let clientId: string = null; + let clientSecret: string = null; + + if (options.apikey != null) { + const apiIdentifiers = await this.apiIdentifiers(); + clientId = apiIdentifiers.clientId; + clientSecret = apiIdentifiers.clientSecret; + } else if (options.sso != null && this.canInteract) { + const passwordOptions: any = { + type: "password", + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + const state = await this.passwordGenerationService.generatePassword(passwordOptions); + ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256"); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + try { + const ssoParams = await this.openSsoPrompt(codeChallenge, state); + ssoCode = ssoParams.ssoCode; + orgIdentifier = ssoParams.orgIdentifier; + } catch { + return Response.badRequest("Something went wrong. Try again."); + } + } else { + if ((email == null || email === "") && this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "input", + name: "email", + message: "Email address:", + }); + email = answer.email; + } + if (email == null || email.trim() === "") { + return Response.badRequest("Email address is required."); + } + if (email.indexOf("@") === -1) { + return Response.badRequest("Email address is invalid."); + } + this.email = email; + + if (password == null || password === "") { + if (options.passwordfile) { + password = await NodeUtils.readFirstLine(options.passwordfile); + } else if (options.passwordenv && process.env[options.passwordenv]) { + password = process.env[options.passwordenv]; + } else if (this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "password", + name: "password", + message: "Master password:", + }); + password = answer.password; + } + } + + if (password == null || password === "") { + return Response.badRequest("Master password is required."); + } } - async run(email: string, password: string, options: program.OptionValues) { - this.canInteract = process.env.BW_NOINTERACTION !== 'true'; + let twoFactorToken: string = options.code; + let twoFactorMethod: TwoFactorProviderType = null; + try { + if (options.method != null) { + twoFactorMethod = parseInt(options.method, null); + } + } catch (e) { + return Response.error("Invalid two-step login method."); + } - let ssoCodeVerifier: string = null; - let ssoCode: string = null; - let orgIdentifier: string = null; + try { + if (this.validatedParams != null) { + await this.validatedParams(); + } - let clientId: string = null; - let clientSecret: string = null; + let response: AuthResult = null; + if (twoFactorToken != null && twoFactorMethod != null) { + if (clientId != null && clientSecret != null) { + response = await this.authService.logInApiKeyComplete( + clientId, + clientSecret, + twoFactorMethod, + twoFactorToken, + false + ); + } else if (ssoCode != null && ssoCodeVerifier != null) { + response = await this.authService.logInSsoComplete( + ssoCode, + ssoCodeVerifier, + this.ssoRedirectUri, + twoFactorMethod, + twoFactorToken, + false + ); + } else { + response = await this.authService.logInComplete( + email, + password, + twoFactorMethod, + twoFactorToken, + false, + this.clientSecret + ); + } + } else { + if (clientId != null && clientSecret != null) { + response = await this.authService.logInApiKey(clientId, clientSecret); + } else if (ssoCode != null && ssoCodeVerifier != null) { + response = await this.authService.logInSso( + ssoCode, + ssoCodeVerifier, + this.ssoRedirectUri, + orgIdentifier + ); + } else { + response = await this.authService.logIn(email, password); + } + if (response.captchaSiteKey) { + const badCaptcha = Response.badRequest( + "Your authentication request appears to be coming from a bot\n" + + "Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" + + "(https://bitwarden.com/help/article/cli-auth-challenges)" + ); - if (options.apikey != null) { - const apiIdentifiers = await this.apiIdentifiers(); - clientId = apiIdentifiers.clientId; - clientSecret = apiIdentifiers.clientSecret; - } else if (options.sso != null && this.canInteract) { - const passwordOptions: any = { - type: 'password', - length: 64, - uppercase: true, - lowercase: true, - numbers: true, - special: false, - }; - const state = await this.passwordGenerationService.generatePassword(passwordOptions); - ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256'); - const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + try { + const captchaClientSecret = await this.apiClientSecret(true); + if (Utils.isNullOrWhitespace(captchaClientSecret)) { + return badCaptcha; + } + + const secondResponse = await this.authService.logInComplete( + email, + password, + twoFactorMethod, + twoFactorToken, + false, + captchaClientSecret + ); + response = secondResponse; + } catch (e) { + if ( + (e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") && + (e as ErrorResponse).message.includes("Captcha is invalid") + ) { + return badCaptcha; + } else { + throw e; + } + } + } + if (response.twoFactor) { + let selectedProvider: any = null; + const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null); + if (twoFactorProviders.length === 0) { + return Response.badRequest("No providers available for this client."); + } + + if (twoFactorMethod != null) { try { - const ssoParams = await this.openSsoPrompt(codeChallenge, state); - ssoCode = ssoParams.ssoCode; - orgIdentifier = ssoParams.orgIdentifier; - } catch { - return Response.badRequest('Something went wrong. Try again.'); + selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0]; + } catch (e) { + return Response.error("Invalid two-step login method."); } - } else { - if ((email == null || email === '') && this.canInteract) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'email', - message: 'Email address:', - }); - email = answer.email; + } + + if (selectedProvider == null) { + if (twoFactorProviders.length === 1) { + selectedProvider = twoFactorProviders[0]; + } else if (this.canInteract) { + const twoFactorOptions = twoFactorProviders.map((p) => p.name); + twoFactorOptions.push(new inquirer.Separator()); + twoFactorOptions.push("Cancel"); + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "list", + name: "method", + message: "Two-step login method:", + choices: twoFactorOptions, + }); + const i = twoFactorOptions.indexOf(answer.method); + if (i === twoFactorOptions.length - 1) { + return Response.error("Login failed."); + } + selectedProvider = twoFactorProviders[i]; } - if (email == null || email.trim() === '') { - return Response.badRequest('Email address is required.'); + if (selectedProvider == null) { + return Response.error("Login failed. No provider selected."); } - if (email.indexOf('@') === -1) { - return Response.badRequest('Email address is invalid.'); - } - this.email = email; + } - if (password == null || password === '') { - if (options.passwordfile) { - password = await NodeUtils.readFirstLine(options.passwordfile); - } else if (options.passwordenv && process.env[options.passwordenv]) { - password = process.env[options.passwordenv]; - } else if (this.canInteract) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'password', - name: 'password', - message: 'Master password:', - }); - password = answer.password; - } - } + if ( + twoFactorToken == null && + response.twoFactorProviders.size > 1 && + selectedProvider.type === TwoFactorProviderType.Email + ) { + const emailReq = new TwoFactorEmailRequest(); + emailReq.email = this.authService.email; + emailReq.masterPasswordHash = this.authService.masterPasswordHash; + await this.apiService.postTwoFactorEmail(emailReq); + } - if (password == null || password === '') { - return Response.badRequest('Master password is required.'); - } - } - - let twoFactorToken: string = options.code; - let twoFactorMethod: TwoFactorProviderType = null; - try { - if (options.method != null) { - twoFactorMethod = parseInt(options.method, null); - } - } catch (e) { - return Response.error('Invalid two-step login method.'); - } - - try { - if (this.validatedParams != null) { - await this.validatedParams(); - } - - let response: AuthResult = null; - if (twoFactorToken != null && twoFactorMethod != null) { - if (clientId != null && clientSecret != null) { - response = await this.authService.logInApiKeyComplete(clientId, clientSecret, twoFactorMethod, - twoFactorToken, false); - } else if (ssoCode != null && ssoCodeVerifier != null) { - response = await this.authService.logInSsoComplete(ssoCode, ssoCodeVerifier, this.ssoRedirectUri, - twoFactorMethod, twoFactorToken, false); - } else { - response = await this.authService.logInComplete(email, password, twoFactorMethod, - twoFactorToken, false, this.clientSecret); - } - } else { - if (clientId != null && clientSecret != null) { - response = await this.authService.logInApiKey(clientId, clientSecret); - } else if (ssoCode != null && ssoCodeVerifier != null) { - response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri, - orgIdentifier); - } else { - response = await this.authService.logIn(email, password); - } - if (response.captchaSiteKey) { - const badCaptcha = Response.badRequest('Your authentication request appears to be coming from a bot\n' + - 'Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n' + - '(https://bitwarden.com/help/article/cli-auth-challenges)'); - - try { - const captchaClientSecret = await this.apiClientSecret(true); - if (Utils.isNullOrWhitespace(captchaClientSecret)) { - return badCaptcha; - } - - const secondResponse = await this.authService.logInComplete(email, password, twoFactorMethod, - twoFactorToken, false, captchaClientSecret); - response = secondResponse; - } catch (e) { - if ((e instanceof ErrorResponse || e.constructor.name === 'ErrorResponse') && - (e as ErrorResponse).message.includes('Captcha is invalid')) { - return badCaptcha; - } else { - throw e; - } - } - } - if (response.twoFactor) { - let selectedProvider: any = null; - const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null); - if (twoFactorProviders.length === 0) { - return Response.badRequest('No providers available for this client.'); - } - - if (twoFactorMethod != null) { - try { - selectedProvider = twoFactorProviders.filter(p => p.type === twoFactorMethod)[0]; - } catch (e) { - return Response.error('Invalid two-step login method.'); - } - } - - if (selectedProvider == null) { - if (twoFactorProviders.length === 1) { - selectedProvider = twoFactorProviders[0]; - } else if (this.canInteract) { - const twoFactorOptions = twoFactorProviders.map(p => p.name); - twoFactorOptions.push(new inquirer.Separator()); - twoFactorOptions.push('Cancel'); - const answer: inquirer.Answers = - await inquirer.createPromptModule({ output: process.stderr })({ - type: 'list', - name: 'method', - message: 'Two-step login method:', - choices: twoFactorOptions, - }); - const i = twoFactorOptions.indexOf(answer.method); - if (i === (twoFactorOptions.length - 1)) { - return Response.error('Login failed.'); - } - selectedProvider = twoFactorProviders[i]; - } - if (selectedProvider == null) { - return Response.error('Login failed. No provider selected.'); - } - } - - if (twoFactorToken == null && response.twoFactorProviders.size > 1 && - selectedProvider.type === TwoFactorProviderType.Email) { - const emailReq = new TwoFactorEmailRequest(); - emailReq.email = this.authService.email; - emailReq.masterPasswordHash = this.authService.masterPasswordHash; - await this.apiService.postTwoFactorEmail(emailReq); - } - - if (twoFactorToken == null) { - if (this.canInteract) { - const answer: inquirer.Answers = - await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'token', - message: 'Two-step login code:', - }); - twoFactorToken = answer.token; - } - if (twoFactorToken == null || twoFactorToken === '') { - return Response.badRequest('Code is required.'); - } - } - - response = await this.authService.logInTwoFactor(selectedProvider.type, - twoFactorToken, false); - } - } - - if (response.twoFactor) { - return Response.error('Login failed.'); - } - - if (response.resetMasterPassword) { - return Response.error('In order to log in with SSO from the CLI, you must first log in' + - ' through the web vault to set your master password.'); - } - - // Full sync required for the reset password and key connector checks - await this.syncService.fullSync(true); - - // Handle converting to Key Connector if required - if (await this.keyConnectorService.userNeedsMigration()) { - return await this.migrateToKeyConnector(); - } - - // Handle Updating Temp Password if NOT using an API Key for authentication - if (response.forcePasswordReset && (clientId == null && clientSecret == null)) { - return await this.updateTempPassword(); - } - - return await this.handleSuccessResponse(); - } catch (e) { - return Response.error(e); - } - } - - private async handleSuccessResponse(): Promise { - if (this.success != null) { - const res = await this.success(); - return Response.success(res); - } else { - const res = new MessageResponse('You are logged in!', null); - return Response.success(res); - } - } - - private async updateTempPassword(error?: string): Promise { - // If no interaction available, alert user to use web vault - if (!this.canInteract) { - await this.logout(); - this.authService.logOut(() => { /* Do nothing */ }); - return Response.error(new MessageResponse('An organization administrator recently changed your master password. In order to access the vault, you must update your master password now via the web vault. You have been logged out.', null)); - } - - if (this.email == null || this.email === 'undefined') { - this.email = await this.userService.getEmail(); - } - - // Get New Master Password - const baseMessage = 'An organization administrator recently changed your master password. In order to access the vault, you must update your master password now.\n' + 'Master password: '; - const firstMessage = error != null ? error + baseMessage : baseMessage; - const mp: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'password', - name: 'password', - message: firstMessage, - }); - const masterPassword = mp.password; - - // Master Password Validation - if (masterPassword == null || masterPassword === '') { - return this.updateTempPassword('Master password is required.\n'); - } - - if (masterPassword.length < 8) { - return this.updateTempPassword('Master password must be at least 8 characters long.\n'); - } - - // Strength & Policy Validation - const strengthResult = this.passwordGenerationService.passwordStrength(masterPassword, - this.getPasswordStrengthUserInput()); - - // Get New Master Password Re-type - const reTypeMessage = 'Re-type New Master password (Strength: ' + strengthResult.score + ')'; - const retype: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'password', - name: 'password', - message: reTypeMessage, - }); - const masterPasswordRetype = retype.password; - - // Re-type Validation - if (masterPassword !== masterPasswordRetype) { - return this.updateTempPassword('Master password confirmation does not match.\n'); - } - - // Get Hint (optional) - const hint: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'input', - message: 'Master Password Hint (optional):', - }); - const masterPasswordHint = hint.input; - - // Retrieve details for key generation - const enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); - - if (enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - strengthResult.score, - masterPassword, - enforcedPolicyOptions)) { - return this.updateTempPassword('Your new master password does not meet the policy requirements.\n'); - } - - try { - // Create new key and hash new password - const newKey = await this.cryptoService.makeKey(masterPassword, this.email.trim().toLowerCase(), - kdf, kdfIterations); - const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey); - - // Grab user's current enc key - const userEncKey = await this.cryptoService.getEncKey(); - - // Create new encKey for the User - const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey); - - // Create request - const request = new UpdateTempPasswordRequest(); - request.key = newEncKey[1].encryptedString; - request.newMasterPasswordHash = newPasswordHash; - request.masterPasswordHint = masterPasswordHint; - - // Update user's password - await this.apiService.putUpdateTempPassword(request); - return this.handleSuccessResponse(); - } catch (e) { - await this.logout(); - this.authService.logOut(() => { /* Do nothing */ }); - return Response.error(e); - } - } - - private getPasswordStrengthUserInput() { - let userInput: string[] = []; - const atPosition = this.email.indexOf('@'); - if (atPosition > -1) { - userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/)); - } - return userInput; - } - - private async migrateToKeyConnector() { - // If no interaction available, alert user to use web vault - if (!this.canInteract) { - await this.logout(); - this.authService.logOut(() => { /* Do nothing */ }); - return Response.error(new MessageResponse('An organization you are a member of is using Key Connector. ' + - 'In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.', null)); - } - - const organization = await this.keyConnectorService.getManagingOrganization(); - - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'list', - name: 'convert', - message: organization.name + ' is using a self-hosted key server. A master password is no longer required to log in for members of this organization. ', - choices: [ - { - name: 'Remove master password and log in', - value: 'remove', - }, - { - name: 'Leave organization and log in', - value: 'leave', - }, - { - name: 'Exit', - value: 'exit', - }, - ], - }); - - if (answer.convert === 'remove') { - await this.keyConnectorService.migrateUser(); - - // Update environment URL - required for api key login - const urls = this.environmentService.getUrls(); - urls.keyConnector = organization.keyConnectorUrl; - await this.environmentService.setUrls(urls, true); - - return await this.handleSuccessResponse(); - } else if (answer.convert === 'leave') { - await this.apiService.postLeaveOrganization(organization.id); - await this.syncService.fullSync(true); - return await this.handleSuccessResponse(); - } else { - await this.logout(); - this.authService.logOut(() => { /* Do nothing */ }); - return Response.error('You have been logged out.'); - } - } - - private async apiClientId(): Promise { - let clientId: string = null; - - const storedClientId: string = process.env.BW_CLIENTID; - if (storedClientId == null) { + if (twoFactorToken == null) { if (this.canInteract) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'clientId', - message: 'client_id:', - }); - clientId = answer.clientId; - } else { - clientId = null; + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "input", + name: "token", + message: "Two-step login code:", + }); + twoFactorToken = answer.token; } - } else { - clientId = storedClientId; + if (twoFactorToken == null || twoFactorToken === "") { + return Response.badRequest("Code is required."); + } + } + + response = await this.authService.logInTwoFactor( + selectedProvider.type, + twoFactorToken, + false + ); } + } - return clientId; + if (response.twoFactor) { + return Response.error("Login failed."); + } + + if (response.resetMasterPassword) { + return Response.error( + "In order to log in with SSO from the CLI, you must first log in" + + " through the web vault to set your master password." + ); + } + + // Full sync required for the reset password and key connector checks + await this.syncService.fullSync(true); + + // Handle converting to Key Connector if required + if (await this.keyConnectorService.userNeedsMigration()) { + return await this.migrateToKeyConnector(); + } + + // Handle Updating Temp Password if NOT using an API Key for authentication + if (response.forcePasswordReset && clientId == null && clientSecret == null) { + return await this.updateTempPassword(); + } + + return await this.handleSuccessResponse(); + } catch (e) { + return Response.error(e); + } + } + + private async handleSuccessResponse(): Promise { + if (this.success != null) { + const res = await this.success(); + return Response.success(res); + } else { + const res = new MessageResponse("You are logged in!", null); + return Response.success(res); + } + } + + private async updateTempPassword(error?: string): Promise { + // If no interaction available, alert user to use web vault + if (!this.canInteract) { + await this.logout(); + this.authService.logOut(() => { + /* Do nothing */ + }); + return Response.error( + new MessageResponse( + "An organization administrator recently changed your master password. In order to access the vault, you must update your master password now via the web vault. You have been logged out.", + null + ) + ); } - private async apiClientSecret(isAdditionalAuthentication: boolean = false): Promise { - const additionalAuthenticationMessage = 'Additional authentication required.\nAPI key '; - let clientSecret: string = null; - - const storedClientSecret: string = this.clientSecret || process.env.BW_CLIENTSECRET; - if (this.canInteract && storedClientSecret == null) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'clientSecret', - message: (isAdditionalAuthentication ? additionalAuthenticationMessage : '') + 'client_secret:', - - }); - clientSecret = answer.clientSecret; - } else { - clientSecret = storedClientSecret; - } - - return clientSecret; + if (this.email == null || this.email === "undefined") { + this.email = await this.stateService.getEmail(); } - private async apiIdentifiers(): Promise<{ clientId: string, clientSecret: string; }> { - return { - clientId: await this.apiClientId(), - clientSecret: await this.apiClientSecret(), - }; + // Get New Master Password + const baseMessage = + "An organization administrator recently changed your master password. In order to access the vault, you must update your master password now.\n" + + "Master password: "; + const firstMessage = error != null ? error + baseMessage : baseMessage; + const mp: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: "password", + name: "password", + message: firstMessage, + }); + const masterPassword = mp.password; + + // Master Password Validation + if (masterPassword == null || masterPassword === "") { + return this.updateTempPassword("Master password is required.\n"); } - private async openSsoPrompt(codeChallenge: string, state: string): Promise<{ ssoCode: string, orgIdentifier: string }> { - return new Promise((resolve, reject) => { - const callbackServer = http.createServer((req, res) => { - const urlString = 'http://localhost' + req.url; - const url = new URL(urlString); - const code = url.searchParams.get('code'); - const receivedState = url.searchParams.get('state'); - const orgIdentifier = this.getOrgIdentifierFromState(receivedState); - res.setHeader('Content-Type', 'text/html'); - if (code != null && receivedState != null && this.checkState(receivedState, state)) { - res.writeHead(200); - res.end('Success | Bitwarden CLI' + - '

Successfully authenticated with the Bitwarden CLI

' + - '

You may now close this tab and return to the terminal.

' + - ''); - callbackServer.close(() => resolve({ - ssoCode: code, - orgIdentifier: orgIdentifier, - })); - } else { - res.writeHead(400); - res.end('Failed | Bitwarden CLI' + - '

Something went wrong logging into the Bitwarden CLI

' + - '

You may now close this tab and return to the terminal.

' + - ''); - callbackServer.close(() => reject()); - } - }); - let foundPort = false; - const webUrl = this.environmentService.getWebVaultUrl(); - for (let port = 8065; port <= 8070; port++) { - try { - this.ssoRedirectUri = 'http://localhost:' + port; - callbackServer.listen(port, () => { - this.platformUtilsService.launchUri(webUrl + '/#/sso?clientId=' + this.clientId + - '&redirectUri=' + encodeURIComponent(this.ssoRedirectUri) + - '&state=' + state + '&codeChallenge=' + codeChallenge); - }); - foundPort = true; - break; - } catch { - // Ignore error since we run the same command up to 5 times. - } - } - if (!foundPort) { - reject(); - } + if (masterPassword.length < 8) { + return this.updateTempPassword("Master password must be at least 8 characters long.\n"); + } + + // Strength & Policy Validation + const strengthResult = this.passwordGenerationService.passwordStrength( + masterPassword, + this.getPasswordStrengthUserInput() + ); + + // Get New Master Password Re-type + const reTypeMessage = "Re-type New Master password (Strength: " + strengthResult.score + ")"; + const retype: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: "password", + name: "password", + message: reTypeMessage, + }); + const masterPasswordRetype = retype.password; + + // Re-type Validation + if (masterPassword !== masterPasswordRetype) { + return this.updateTempPassword("Master password confirmation does not match.\n"); + } + + // Get Hint (optional) + const hint: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: "input", + name: "input", + message: "Master Password Hint (optional):", + }); + const masterPasswordHint = hint.input; + + // Retrieve details for key generation + const enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); + const kdf = await this.stateService.getKdfType(); + const kdfIterations = await this.stateService.getKdfIterations(); + + if ( + enforcedPolicyOptions != null && + !this.policyService.evaluateMasterPassword( + strengthResult.score, + masterPassword, + enforcedPolicyOptions + ) + ) { + return this.updateTempPassword( + "Your new master password does not meet the policy requirements.\n" + ); + } + + try { + // Create new key and hash new password + const newKey = await this.cryptoService.makeKey( + masterPassword, + this.email.trim().toLowerCase(), + kdf, + kdfIterations + ); + const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey); + + // Grab user's current enc key + const userEncKey = await this.cryptoService.getEncKey(); + + // Create new encKey for the User + const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey); + + // Create request + const request = new UpdateTempPasswordRequest(); + request.key = newEncKey[1].encryptedString; + request.newMasterPasswordHash = newPasswordHash; + request.masterPasswordHint = masterPasswordHint; + + // Update user's password + await this.apiService.putUpdateTempPassword(request); + return this.handleSuccessResponse(); + } catch (e) { + await this.logout(); + this.authService.logOut(() => { + /* Do nothing */ + }); + return Response.error(e); + } + } + + private getPasswordStrengthUserInput() { + let userInput: string[] = []; + const atPosition = this.email.indexOf("@"); + if (atPosition > -1) { + userInput = userInput.concat( + this.email + .substr(0, atPosition) + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/) + ); + } + return userInput; + } + + private async migrateToKeyConnector() { + // If no interaction available, alert user to use web vault + if (!this.canInteract) { + await this.logout(); + this.authService.logOut(() => { + /* Do nothing */ + }); + return Response.error( + new MessageResponse( + "An organization you are a member of is using Key Connector. " + + "In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.", + null + ) + ); + } + + const organization = await this.keyConnectorService.getManagingOrganization(); + + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: "list", + name: "convert", + message: + organization.name + + " is using a self-hosted key server. A master password is no longer required to log in for members of this organization. ", + choices: [ + { + name: "Remove master password and log in", + value: "remove", + }, + { + name: "Leave organization and log in", + value: "leave", + }, + { + name: "Exit", + value: "exit", + }, + ], + }); + + if (answer.convert === "remove") { + await this.keyConnectorService.migrateUser(); + + // Update environment URL - required for api key login + const urls = this.environmentService.getUrls(); + urls.keyConnector = organization.keyConnectorUrl; + await this.environmentService.setUrls(urls, true); + + return await this.handleSuccessResponse(); + } else if (answer.convert === "leave") { + await this.apiService.postLeaveOrganization(organization.id); + await this.syncService.fullSync(true); + return await this.handleSuccessResponse(); + } else { + await this.logout(); + this.authService.logOut(() => { + /* Do nothing */ + }); + return Response.error("You have been logged out."); + } + } + + private async apiClientId(): Promise { + let clientId: string = null; + + const storedClientId: string = process.env.BW_CLIENTID; + if (storedClientId == null) { + if (this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "input", + name: "clientId", + message: "client_id:", }); + clientId = answer.clientId; + } else { + clientId = null; + } + } else { + clientId = storedClientId; } - private getOrgIdentifierFromState(state: string): string { - if (state === null || state === undefined) { - return null; - } + return clientId; + } - const stateSplit = state.split('_identifier='); - return stateSplit.length > 1 ? stateSplit[1] : null; + private async apiClientSecret(isAdditionalAuthentication: boolean = false): Promise { + const additionalAuthenticationMessage = "Additional authentication required.\nAPI key "; + let clientSecret: string = null; + + const storedClientSecret: string = this.clientSecret || process.env.BW_CLIENTSECRET; + if (this.canInteract && storedClientSecret == null) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ + output: process.stderr, + })({ + type: "input", + name: "clientSecret", + message: + (isAdditionalAuthentication ? additionalAuthenticationMessage : "") + "client_secret:", + }); + clientSecret = answer.clientSecret; + } else { + clientSecret = storedClientSecret; } - private checkState(state: string, checkState: string): boolean { - if (state === null || state === undefined) { - return false; - } - if (checkState === null || checkState === undefined) { - return false; - } + return clientSecret; + } - const stateSplit = state.split('_identifier='); - const checkStateSplit = checkState.split('_identifier='); - return stateSplit[0] === checkStateSplit[0]; + private async apiIdentifiers(): Promise<{ clientId: string; clientSecret: string }> { + return { + clientId: await this.apiClientId(), + clientSecret: await this.apiClientSecret(), + }; + } + + private async openSsoPrompt( + codeChallenge: string, + state: string + ): Promise<{ ssoCode: string; orgIdentifier: string }> { + return new Promise((resolve, reject) => { + const callbackServer = http.createServer((req, res) => { + const urlString = "http://localhost" + req.url; + const url = new URL(urlString); + const code = url.searchParams.get("code"); + const receivedState = url.searchParams.get("state"); + const orgIdentifier = this.getOrgIdentifierFromState(receivedState); + res.setHeader("Content-Type", "text/html"); + if (code != null && receivedState != null && this.checkState(receivedState, state)) { + res.writeHead(200); + res.end( + "Success | Bitwarden CLI" + + "

Successfully authenticated with the Bitwarden CLI

" + + "

You may now close this tab and return to the terminal.

" + + "" + ); + callbackServer.close(() => + resolve({ + ssoCode: code, + orgIdentifier: orgIdentifier, + }) + ); + } else { + res.writeHead(400); + res.end( + "Failed | Bitwarden CLI" + + "

Something went wrong logging into the Bitwarden CLI

" + + "

You may now close this tab and return to the terminal.

" + + "" + ); + callbackServer.close(() => reject()); + } + }); + let foundPort = false; + const webUrl = this.environmentService.getWebVaultUrl(); + for (let port = 8065; port <= 8070; port++) { + try { + this.ssoRedirectUri = "http://localhost:" + port; + callbackServer.listen(port, () => { + this.platformUtilsService.launchUri( + webUrl + + "/#/sso?clientId=" + + this.clientId + + "&redirectUri=" + + encodeURIComponent(this.ssoRedirectUri) + + "&state=" + + state + + "&codeChallenge=" + + codeChallenge + ); + }); + foundPort = true; + break; + } catch { + // Ignore error since we run the same command up to 5 times. + } + } + if (!foundPort) { + reject(); + } + }); + } + + private getOrgIdentifierFromState(state: string): string { + if (state === null || state === undefined) { + return null; } + + const stateSplit = state.split("_identifier="); + return stateSplit.length > 1 ? stateSplit[1] : null; + } + + private checkState(state: string, checkState: string): boolean { + if (state === null || state === undefined) { + return false; + } + if (checkState === null || checkState === undefined) { + return false; + } + + const stateSplit = state.split("_identifier="); + const checkStateSplit = checkState.split("_identifier="); + return stateSplit[0] === checkStateSplit[0]; + } } diff --git a/package-lock.json b/package-lock.json index ada0fea4..22940353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,11 @@ "devDependencies": { "@fluffy-spoon/substitute": "^1.202.0", "@types/jasmine": "^3.7.6", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", + "commander": "7.2.0", "concurrently": "^6.1.0", + "form-data": "4.0.0", + "husky": "^7.0.4", "jasmine": "^3.7.0", "jasmine-core": "^3.7.1", "jasmine-ts-console-reporter": "^3.1.1", @@ -32,19 +35,22 @@ "karma-jasmine-html-reporter": "^1.5.4", "karma-safari-launcher": "^1.0.0", "karma-webpack": "^4.0.2", + "lint-staged": "^12.1.2", "nodemon": "^2.0.7", + "prettier": "2.5.1", "rimraf": "^3.0.2", + "rxjs": "^7.4.0", "ts-loader": "^8.1.0", "tslint": "^6.1.3", "ttypescript": "^1.5.12", "typemoq": "^2.1.0", - "typescript": "4.1.5", + "typescript": "4.3.5", "typescript-transform-paths": "^2.2.3", "webpack": "^4.46.0" }, "engines": { - "node": "~14", - "npm": "~7" + "node": "~16", + "npm": "~8" } }, "angular": { @@ -52,24 +58,25 @@ "version": "0.0.0", "license": "GPL-3.0", "dependencies": { - "@angular/animations": "^11.2.11", - "@angular/cdk": "^11.2.10", - "@angular/common": "^11.2.11", - "@angular/compiler": "^11.2.11", - "@angular/core": "^11.2.11", - "@angular/forms": "^11.2.11", - "@angular/platform-browser": "^11.2.11", - "@angular/platform-browser-dynamic": "^11.2.11", - "@angular/router": "^11.2.11", + "@angular/animations": "^12.2.13", + "@angular/cdk": "^12.2.13", + "@angular/common": "^12.2.13", + "@angular/compiler": "^12.2.13", + "@angular/core": "^12.2.13", + "@angular/forms": "^12.2.13", + "@angular/platform-browser": "^12.2.13", + "@angular/platform-browser-dynamic": "^12.2.13", + "@angular/router": "^12.2.13", "@bitwarden/jslib-common": "file:../common", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", + "tldjs": "^2.3.1", "zone.js": "0.11.4" }, "devDependencies": { "@types/duo_web_sdk": "^2.7.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "common": { @@ -84,18 +91,18 @@ "lunr": "^2.3.9", "node-forge": "^0.10.0", "papaparse": "^5.3.0", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldts": "5.7.52", "zxcvbn": "^4.4.2" }, "devDependencies": { "@types/lunr": "^2.3.3", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-forge": "^0.9.7", "@types/papaparse": "^5.2.5", "@types/zxcvbn": "^4.4.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "electron": { @@ -105,17 +112,17 @@ "dependencies": { "@bitwarden/jslib-common": "file:../common", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", - "electron": "14.2.0", + "electron": "16.0.2", "electron-log": "4.4.1", "electron-store": "8.0.1", - "electron-updater": "4.3.9", + "electron-updater": "4.6.1", "forcefocus": "^1.1.0", "keytar": "7.7.0" }, "devDependencies": { - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "node": { @@ -135,21 +142,24 @@ "devDependencies": { "@types/inquirer": "^7.3.1", "@types/lowdb": "^1.0.10", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-fetch": "^2.5.10", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "node_modules/@angular/animations": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-11.2.14.tgz", - "integrity": "sha512-Heq/nNrCmb3jbkusu+BQszOecfFI/31Oxxj+CDQkqqYpBcswk6bOJLoEE472o+vmgxaXbgeflU9qbIiCQhpMFA==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-12.2.14.tgz", + "integrity": "sha512-1BR5u32auVePvXNNP96DB2008V+Ku0OGqeZQl2h4XA9xzES/Zk5WllIJZXqRmWMRBVARfXsfb0RdMty9gcaVjA==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/core": "11.2.14" + "@angular/core": "12.2.14" } }, "node_modules/@angular/animations/node_modules/tslib": { @@ -158,18 +168,19 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@angular/cdk": { - "version": "11.2.13", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-11.2.13.tgz", - "integrity": "sha512-FkE4iCwoLbQxLDUOjV1I7M/6hmpyb7erAjEdWgch7nGRNxF1hqX5Bqf1lvLFKPNCbx5NRI5K7YVAdIUQUR8vug==", + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.13.tgz", + "integrity": "sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "optionalDependencies": { "parse5": "^5.0.0" }, "peerDependencies": { - "@angular/common": "^11.0.0 || ^12.0.0-0", - "@angular/core": "^11.0.0 || ^12.0.0-0" + "@angular/common": "^12.0.0 || ^13.0.0-0", + "@angular/core": "^12.0.0 || ^13.0.0-0", + "rxjs": "^6.5.3 || ^7.0.0" } }, "node_modules/@angular/cdk/node_modules/parse5": { @@ -184,15 +195,18 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@angular/common": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-11.2.14.tgz", - "integrity": "sha512-ZSLV/3j7eCTyLf/8g4yBFLWySjiLz3vLJAGWscYoUpnJWMnug1VRu6zoF/COxCbtORgE+Wz6K0uhfS6MziBGVw==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-12.2.14.tgz", + "integrity": "sha512-ffYUYdwZETmFJw0AcWY30WsaWBhJxj/zSmFXWjgEGEGZH56zwbbNwfMZOYZ1jz4haAVxGu+TdXsOl2yMGzN7jQ==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/core": "11.2.14", - "rxjs": "^6.5.3" + "@angular/core": "12.2.14", + "rxjs": "^6.5.3 || ^7.0.0" } }, "node_modules/@angular/common/node_modules/tslib": { @@ -201,11 +215,14 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@angular/compiler": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-11.2.14.tgz", - "integrity": "sha512-XBOK3HgA+/y6Cz7kOX4zcJYmgJ264XnfcbXUMU2cD7Ac+mbNhLPKohWrEiSWalfcjnpf5gRfufQrQP7lpAGu0A==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-12.2.14.tgz", + "integrity": "sha512-dwmZi+n66IUzRFlGWu9mjXq170ZEsaDvlNLZzaPgs6vZTa4Kt7PWvIF/Y7TMvnVv/uqNG6kOhfmOkf6rfz1Gjg==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" } }, "node_modules/@angular/compiler/node_modules/tslib": { @@ -214,15 +231,18 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@angular/core": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-11.2.14.tgz", - "integrity": "sha512-vpR4XqBGitk1Faph37CSpemwIYTmJ3pdIVNoHKP6jLonpWu+0azkchf0f7oD8/2ivj2F81opcIw0tcsy/D/5Vg==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-12.2.14.tgz", + "integrity": "sha512-dlVk7yqUHL2R/eCmM8LsWuxhEBfzg0y1zHt0UqCuFwlCoiw+IG4HFy4OlZEUw9NUEZJSv0aDv3sWqxLkvK5vvg==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "rxjs": "^6.5.3", - "zone.js": "^0.10.2 || ^0.11.3" + "rxjs": "^6.5.3 || ^7.0.0", + "zone.js": "~0.11.4" } }, "node_modules/@angular/core/node_modules/tslib": { @@ -231,17 +251,20 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@angular/forms": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-11.2.14.tgz", - "integrity": "sha512-4LWqY6KEIk1AZQFnk+4PJSOCamlD4tumuVN06gO4D0dZo9Cx+GcvW6pM6N0CPubRvPs3sScCnu20WT11HNWC1w==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-12.2.14.tgz", + "integrity": "sha512-/9/gSJUBXVRVdRnzgJnALAQZYJATuGDMkFC9ms9DEMG4PMAhe9x4if1lJjN6noz5RAom3qNuVBNWaYAPUxlcBQ==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/common": "11.2.14", - "@angular/core": "11.2.14", - "@angular/platform-browser": "11.2.14", - "rxjs": "^6.5.3" + "@angular/common": "12.2.14", + "@angular/core": "12.2.14", + "@angular/platform-browser": "12.2.14", + "rxjs": "^6.5.3 || ^7.0.0" } }, "node_modules/@angular/forms/node_modules/tslib": { @@ -250,16 +273,19 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@angular/platform-browser": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-11.2.14.tgz", - "integrity": "sha512-fb7b7ss/gRoP8wLAN17W62leMgjynuyjEPU2eUoAAazsG9f2cgM+z3rK29GYncDVyYQxZUZYnjSqvL6GSXx86A==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.2.14.tgz", + "integrity": "sha512-fWcE2rnJ3ZCISa1oPfsIDV7FBZBoLFEdDuMXAiDYqDPKvF/E5U5nHrS+K4SlLAi094bMobtTOReNWl/Ienniyw==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/animations": "11.2.14", - "@angular/common": "11.2.14", - "@angular/core": "11.2.14" + "@angular/animations": "12.2.14", + "@angular/common": "12.2.14", + "@angular/core": "12.2.14" }, "peerDependenciesMeta": { "@angular/animations": { @@ -268,17 +294,20 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.14.tgz", - "integrity": "sha512-TWTPdFs6iBBcp+/YMsgCRQwdHpWGq8KjeJDJ2tfatGgBD3Gqt2YaHOMST1zPW6RkrmupytTejuVqXzeaKWFxuw==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.2.14.tgz", + "integrity": "sha512-0NPF7mS91Tct8rBmOLZPmnLSuS4kbLHXo6eTgrg80OC0vlzBiQwGDVW4X3KncCoX9CpevaGJCdSMc+uPNsFOUQ==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/common": "11.2.14", - "@angular/compiler": "11.2.14", - "@angular/core": "11.2.14", - "@angular/platform-browser": "11.2.14" + "@angular/common": "12.2.14", + "@angular/compiler": "12.2.14", + "@angular/core": "12.2.14", + "@angular/platform-browser": "12.2.14" } }, "node_modules/@angular/platform-browser-dynamic/node_modules/tslib": { @@ -292,17 +321,20 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@angular/router": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-11.2.14.tgz", - "integrity": "sha512-3aYBmj+zrEL9yf/ntIQxHIYaWShZOBKP3U07X2mX+TPMpGlvHDnR7L6bWhQVZwewzMMz7YVR16ldg50IFuAlfA==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-12.2.14.tgz", + "integrity": "sha512-yP5grSnqBvc4vNhtYdcxDgDYIebUKs5f0xyFkUJM5030UnQ0CV45tBsSxHMkQbPZucIfOuxpRy8xy5+4GizuwQ==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0" }, "peerDependencies": { - "@angular/common": "11.2.14", - "@angular/core": "11.2.14", - "@angular/platform-browser": "11.2.14", - "rxjs": "^6.5.3" + "@angular/common": "12.2.14", + "@angular/core": "12.2.14", + "@angular/platform-browser": "12.2.14", + "rxjs": "^6.5.3 || ^7.0.0" } }, "node_modules/@angular/router/node_modules/tslib": { @@ -311,12 +343,12 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", + "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.14.5" + "@babel/highlight": "^7.16.0" }, "engines": { "node": ">=6.9.0" @@ -332,12 +364,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.14.5", + "@babel/helper-validator-identifier": "^7.15.7", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -434,9 +466,9 @@ } }, "node_modules/@cspotcode/source-map-support": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", - "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, "peer": true, "dependencies": { @@ -447,9 +479,9 @@ } }, "node_modules/@electron/get": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.0.tgz", - "integrity": "sha512-+SjZhRuRo+STTO1Fdhzqnv9D2ZhjxXP6egsJ9kiO8dtP68cDx7dFCwWi64dlMQV7sWcfW1OYCW4wviEBzmRsfQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.1.tgz", + "integrity": "sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA==", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", @@ -463,7 +495,7 @@ "node": ">=8.6" }, "optionalDependencies": { - "global-agent": "^2.0.2", + "global-agent": "^3.0.0", "global-tunnel-ng": "^2.7.1" } }, @@ -583,9 +615,9 @@ "peer": true }, "node_modules/@types/component-emitter": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", - "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==", "dev": true }, "node_modules/@types/cookie": { @@ -616,16 +648,34 @@ "rxjs": "^6.4.0" } }, + "node_modules/@types/inquirer/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@types/inquirer/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/@types/jasmine": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.9.1.tgz", - "integrity": "sha512-PVpjh8S8lqKFKurWSKdFATlfBHGPzgy0PoDdzQ+rr78jTQ0aacyh9YndzZcAUPxhk4kRujItFFGQdUJ7flHumw==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.2.tgz", + "integrity": "sha512-qs4xjVm4V/XjM6owGm/x6TNmhGl5iKX8dkTdsgdgl9oFnqgzxLepnS7rN9Tdo7kDmnFD/VEqKrW57cGD2odbEg==", "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.175", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz", - "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==", + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", "dev": true }, "node_modules/@types/lowdb": { @@ -644,9 +694,10 @@ "dev": true }, "node_modules/@types/node": { - "version": "14.17.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.20.tgz", - "integrity": "sha512-gI5Sl30tmhXsqkNvopFydP7ASc4c2cLfGNQrVKN3X90ADFWFsPEsotm/8JHSUJQKTHbwowAHtcJPeyVhtKv0TQ==" + "version": "16.11.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.13.tgz", + "integrity": "sha512-eUXZzHLHoZqj1frtUetNkUetYoJ6X55UmrVnFD4DMhVeAmwLjniZhtBmsRiemQh4uq4G3vUra/Ws/hs9vEvL3Q==", + "dev": true }, "node_modules/@types/node-fetch": { "version": "2.5.12", @@ -658,6 +709,20 @@ "form-data": "^3.0.0" } }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/node-forge": { "version": "0.9.10", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.9.10.tgz", @@ -668,18 +733,18 @@ } }, "node_modules/@types/papaparse": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.2.6.tgz", - "integrity": "sha512-xGKSd0UTn58N1h0+zf8mW863Rv8BvXcGibEgKFtBIXZlcDXAmX/T4RdDO2mwmrmOypUDt5vRgo2v32a78JdqUA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.1.tgz", + "integrity": "sha512-1lbngk9wty2kCyQB42LjqSa12SEop3t9wcEC7/xYr3ujTSTmv7HWKjKYXly0GkMfQ42PRb2lFPFEibDOiMXS0g==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==" + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==" }, "node_modules/@types/through": { "version": "0.0.30", @@ -920,9 +985,9 @@ } }, "node_modules/acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -973,6 +1038,19 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1015,9 +1093,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -1052,10 +1130,39 @@ "string-width": "^4.1.0" } }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true, "engines": { "node": ">=6" @@ -1228,6 +1335,15 @@ "node": ">=0.10.0" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", @@ -1340,9 +1456,9 @@ } }, "node_modules/base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz", + "integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==", "dev": true, "engines": { "node": ">= 0.6.0" @@ -1432,21 +1548,21 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==", "dev": true, "dependencies": { - "bytes": "3.1.0", + "bytes": "3.1.1", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "1.7.2", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "qs": "6.9.6", + "raw-body": "2.4.2", + "type-is": "~1.6.18" }, "engines": { "node": ">= 0.8" @@ -1495,6 +1611,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/boxen/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/boxen/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -1684,9 +1829,9 @@ "dev": true }, "node_modules/builder-util-runtime": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.5.tgz", - "integrity": "sha512-fgUFHKtMNjdvH6PDRFntdIGUPgwZ69sXsAqEulCtoiqgWes5agrMq/Ud274zjJRTbckYh2PHh8/1CpFc6dpsbQ==", + "version": "8.9.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.9.1.tgz", + "integrity": "sha512-c8a8J3wK6BIVLW7ls+7TRK9igspTbzWmUqxFbgK0m40Ggm6efUbxtWVCGIjc+dtchyr5qAMAUL6iEGRdS/6vwg==", "dependencies": { "debug": "^4.3.2", "sax": "^1.2.4" @@ -1711,9 +1856,9 @@ "dev": true }, "node_modules/bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", "dev": true, "engines": { "node": ">= 0.8" @@ -1814,9 +1959,9 @@ } }, "node_modules/camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", "dev": true, "engines": { "node": ">=10" @@ -1929,6 +2074,15 @@ "node": ">=0.10.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -1952,6 +2106,22 @@ "node": ">=8" } }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -1971,6 +2141,35 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -2030,6 +2229,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, "node_modules/colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -2091,9 +2296,9 @@ } }, "node_modules/concurrently": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.2.2.tgz", - "integrity": "sha512-7a45BjVakAl3pprLOeqaOoZfIWZRmdC68NkjyzPbKu0/pE6CRmqS3l8RG7Q2cX9LnRHkB0oPM6af8PS7NEQvYQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.4.0.tgz", + "integrity": "sha512-HZ3D0RTQMH3oS4gvtYj1P+NBc6PzE2McEra6yEFcQKrUQ9HvtTGU4Dbne083F034p+LRb7kWU0tPRNvSGs1UCQ==", "dev": true, "dependencies": { "chalk": "^4.1.0", @@ -2112,10 +2317,28 @@ "node": ">=10.0.0" } }, + "node_modules/concurrently/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/concurrently/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/conf": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/conf/-/conf-10.0.3.tgz", - "integrity": "sha512-4gtQ/Q36qVxBzMe6B7gWOAfni1VdhuHkIzxydHkclnwGmgN+eW4bb6jj73vigCfr7d3WlmqawvhZrpCUCTPYxQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.1.1.tgz", + "integrity": "sha512-z2civwq/k8TMYtcn3SVP0Peso4otIWnHtcTuHhQ0zDZDdP4NTxqEc8owfkz4zBsdMYdn/LFcE+ZhbCeqkhtq3Q==", "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", @@ -2136,9 +2359,9 @@ } }, "node_modules/conf/node_modules/ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -2332,17 +2555,6 @@ "node": ">=0.10.0" } }, - "node_modules/core-js": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.1.tgz", - "integrity": "sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA==", - "hasInstallScript": true, - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2411,6 +2623,35 @@ "dev": true, "peer": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -2493,9 +2734,9 @@ } }, "node_modules/date-fns": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz", - "integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.27.0.tgz", + "integrity": "sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==", "dev": true, "engines": { "node": ">=0.11" @@ -2537,9 +2778,9 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dependencies": { "ms": "2.1.2" }, @@ -2786,12 +3027,12 @@ "dev": true }, "node_modules/electron": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-14.2.0.tgz", - "integrity": "sha512-6CmAv1P0xcwK3FQOSA27fHI36/wctSFVgj46VODn56srXXQWeolkK1VzeAFNE613iAuuH9jJdHvE3gz+c7XkNA==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.2.tgz", + "integrity": "sha512-kT746yVMztrP4BbT3nrFNcUcfgFu2yelUw6TWBVTy0pju+fBISaqcvoiMrq+8U0vRpoXSu2MJYygOf4T0Det7g==", "hasInstallScript": true, "dependencies": { - "@electron/get": "^1.0.1", + "@electron/get": "^1.13.0", "@types/node": "^14.6.2", "extract-zip": "^1.0.3" }, @@ -2831,15 +3072,15 @@ } }, "node_modules/electron-updater": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.3.9.tgz", - "integrity": "sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.6.1.tgz", + "integrity": "sha512-YsU1mHqXLrXXmBMsxhxy24PrbaB8rnpZDPmFa2gOkTYk/Ch13+R0fjsRSpPYvqtskVVY0ux8fu+HnUkVkqc7og==", "dependencies": { - "@types/semver": "^7.3.5", - "builder-util-runtime": "8.7.5", + "@types/semver": "^7.3.6", + "builder-util-runtime": "8.9.1", "fs-extra": "^10.0.0", "js-yaml": "^4.1.0", - "lazy-val": "^1.0.4", + "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "^7.3.5" @@ -2923,6 +3164,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/electron/node_modules/@types/node": { + "version": "14.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.0.tgz", + "integrity": "sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ==" + }, "node_modules/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", @@ -2945,9 +3191,10 @@ "dev": true }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/emojis-list": { "version": "3.0.0", @@ -2976,42 +3223,45 @@ } }, "node_modules/engine.io": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.1.tgz", - "integrity": "sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.0.tgz", + "integrity": "sha512-ErhZOVu2xweCjEfYcTdkCnEYUiZgkAcBBAhW4jbIvNG8SLU3orAqoJCiytZjYF7eTpVmmCrLDjLIEaPlUAs1uw==", "dev": true, "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~4.0.0", - "ws": "~7.4.2" + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" }, "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz", - "integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.2.tgz", + "integrity": "sha512-wuiO7qO/OEkPJSFueuATIXtrxF7/6GTbAO9QLv7nnbjwZ5tYhLm9zxvLwxstRs0dcT0KUlWTjtIOs1T86jt12g==", "dev": true, "dependencies": { - "base64-arraybuffer": "0.1.4" + "base64-arraybuffer": "~1.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "dev": true, "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", @@ -3040,6 +3290,18 @@ "node": ">=6.9.0" } }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", @@ -3198,9 +3460,9 @@ } }, "node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { "node": ">=4.0" @@ -3259,6 +3521,29 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -3331,6 +3616,17 @@ "node": ">=4" } }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -3607,9 +3903,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz", + "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==", "dev": true, "funding": [ { @@ -3649,10 +3945,9 @@ } }, "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3808,14 +4103,15 @@ } }, "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dependencies": { - "pump": "^3.0.0" - }, + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/get-value": { @@ -3865,13 +4161,12 @@ } }, "node_modules/global-agent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.2.0.tgz", - "integrity": "sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "optional": true, "dependencies": { "boolean": "^3.0.1", - "core-js": "^3.6.5", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", @@ -3981,6 +4276,17 @@ "node": ">=8.6" } }, + "node_modules/got/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", @@ -4170,27 +4476,21 @@ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "node_modules/http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/http-errors/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -4237,6 +4537,30 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", + "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4297,6 +4621,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -4350,6 +4683,48 @@ "node": ">=8.0.0" } }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -4405,9 +4780,9 @@ } }, "node_modules/is-core-module": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz", - "integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -4497,11 +4872,15 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-glob": { @@ -4593,6 +4972,18 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -4659,22 +5050,22 @@ } }, "node_modules/jasmine": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.9.0.tgz", - "integrity": "sha512-JgtzteG7xnqZZ51fg7N2/wiQmXon09szkALcRMTgCMX4u/m17gVJFjObnvw5FXkZOWuweHPaPRVB6DI2uN0wVA==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.10.0.tgz", + "integrity": "sha512-2Y42VsC+3CQCTzTwJezOvji4qLORmKIE0kwowWC+934Krn6ZXNQYljiwK5st9V3PVx96BSiDYXSB60VVah3IlQ==", "dev": true, "dependencies": { "glob": "^7.1.6", - "jasmine-core": "~3.9.0" + "jasmine-core": "~3.10.0" }, "bin": { "jasmine": "bin/jasmine.js" } }, "node_modules/jasmine-core": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.9.0.tgz", - "integrity": "sha512-Tv3kVbPCGVrjsnHBZ38NsPU3sDOtNa0XmbG2baiyJqdb5/SPpDO6GVwJYtUryl6KB4q1Ssckwg612ES9Z0dreQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.10.1.tgz", + "integrity": "sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==", "dev": true }, "node_modules/jasmine-ts-console-reporter": { @@ -4754,6 +5145,20 @@ } } }, + "node_modules/jsdom/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", @@ -4803,9 +5208,9 @@ } }, "node_modules/karma": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.4.tgz", - "integrity": "sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.9.tgz", + "integrity": "sha512-E/MqdLM9uVIhfuyVnrhlGBu4miafBdXEAEqCmwdEMh3n17C7UWC/8Kvm3AYKr91gc7scutekZ0xv6rxRaUCtnw==", "dev": true, "dependencies": { "body-parser": "^1.19.0", @@ -4826,10 +5231,10 @@ "qjobs": "^1.2.0", "range-parser": "^1.2.1", "rimraf": "^3.0.2", - "socket.io": "^3.1.0", + "socket.io": "^4.2.0", "source-map": "^0.6.1", "tmp": "^0.2.1", - "ua-parser-js": "^0.7.28", + "ua-parser-js": "^0.7.30", "yargs": "^16.1.1" }, "bin": { @@ -4873,9 +5278,9 @@ } }, "node_modules/karma-firefox-launcher": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.1.tgz", - "integrity": "sha512-VzDMgPseXak9DtfyE1O5bB2BwsMy1zzO1kUxVW1rP0yhC4tDNJ0p3JoFdzvrK4QqVzdqUMa9Rx9YzkdFp8hz3Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz", + "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==", "dev": true, "dependencies": { "is-wsl": "^2.2.0", @@ -4970,18 +5375,6 @@ "node": ">=0.10.0" } }, - "node_modules/karma/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, "node_modules/keytar": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", @@ -5065,6 +5458,153 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", + "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lint-staged": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.1.2.tgz", + "integrity": "sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.16", + "commander": "^8.3.0", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "execa": "^5.1.1", + "lilconfig": "2.0.4", + "listr2": "^3.13.3", + "micromatch": "^4.0.4", + "normalize-path": "^3.0.0", + "object-inspect": "^1.11.0", + "string-argv": "^0.3.1", + "supports-color": "^9.0.2", + "yaml": "^1.10.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/lint-staged/node_modules/supports-color": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.1.tgz", + "integrity": "sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/listr2": { + "version": "3.13.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.13.5.tgz", + "integrity": "sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.4.0", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -5115,6 +5655,84 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/log4js": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz", @@ -5277,6 +5895,12 @@ "node": ">=4.3.0 <5.0.0 || >=5.10" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -5310,9 +5934,9 @@ "dev": true }, "node_modules/mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "bin": { "mime": "cli.js" @@ -5322,19 +5946,19 @@ } }, "node_modules/mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dependencies": { - "mime-db": "1.49.0" + "mime-db": "1.51.0" }, "engines": { "node": ">= 0.6" @@ -5631,9 +6255,9 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" }, "node_modules/node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -5706,21 +6330,21 @@ "dev": true }, "node_modules/nodemon": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.13.tgz", - "integrity": "sha512-UMXMpsZsv1UXUttCn6gv8eQPhn6DR4BW+txnL3IN5IHqrCwcrT/yWHfL35UsClGXknTH79r5xbu+6J1zNHuSyA==", + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz", + "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==", "dev": true, "hasInstallScript": true, "dependencies": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", + "chokidar": "^3.5.2", + "debug": "^3.2.7", "ignore-by-default": "^1.0.1", "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", + "pstree.remy": "^1.1.8", "semver": "^5.7.1", "supports-color": "^5.5.0", "touch": "^3.1.0", - "undefsafe": "^2.0.3", + "undefsafe": "^2.0.5", "update-notifier": "^5.1.0" }, "bin": { @@ -5814,6 +6438,18 @@ "node": ">=4" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -5873,6 +6509,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.1.tgz", + "integrity": "sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -6012,6 +6657,21 @@ "node": ">=6" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -6133,6 +6793,15 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -6267,6 +6936,18 @@ "node": ">=4" } }, + "node_modules/prettier": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -6398,12 +7079,15 @@ } }, "node_modules/qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==", "dev": true, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/querystring": { @@ -6459,13 +7143,13 @@ } }, "node_modules/raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", + "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", "dev": true, "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.2", + "bytes": "3.1.1", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, @@ -6748,14 +7432,11 @@ } }, "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "~2.1.0" } }, "node_modules/safe-buffer": { @@ -6906,9 +7587,9 @@ "dev": true }, "node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, "node_modules/sha.js": { @@ -6936,10 +7617,31 @@ "node": ">=8" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "node_modules/simple-concat": { "version": "1.0.1", @@ -6992,6 +7694,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", + "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -7115,29 +7845,26 @@ "dev": true }, "node_modules/socket.io": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz", - "integrity": "sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.0.tgz", + "integrity": "sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ==", "dev": true, "dependencies": { - "@types/cookie": "^0.4.0", - "@types/cors": "^2.8.8", - "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "~2.0.0", - "debug": "~4.3.1", - "engine.io": "~4.1.0", - "socket.io-adapter": "~2.1.0", - "socket.io-parser": "~4.0.3" + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" }, "engines": { "node": ">=10.0.0" } }, "node_modules/socket.io-adapter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz", - "integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", "dev": true }, "node_modules/socket.io-parser": { @@ -7183,9 +7910,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "dependencies": { "buffer-from": "^1.0.0", @@ -7371,17 +8098,57 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.0.1.tgz", + "integrity": "sha512-5ohWO/M4//8lErlUUtrFy3b11GtNOuMOU0ysKCDXFcfXuuvUXu95akgj/i8ofmaGdN0hCqyl6uu9i8dS/mQp5g==", + "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^9.2.2", + "is-fullwidth-code-point": "^4.0.0", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-ansi": { @@ -7395,6 +8162,15 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -7633,14 +8409,15 @@ "integrity": "sha512-xLa9sSdmSIec2kok76Fv68vvf58QZJFGEOan/pLsk9rrPfkfMZq7Vr03X0hPr8VCc09tcbfU6izXzRDl8oItYQ==" }, "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, "dependencies": { - "os-tmpdir": "~1.0.2" + "rimraf": "^3.0.0" }, "engines": { - "node": ">=0.6.0" + "node": ">=8.17.0" } }, "node_modules/to-arraybuffer": { @@ -7785,9 +8562,9 @@ } }, "node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "engines": { "node": ">=0.6" @@ -7876,9 +8653,9 @@ } }, "node_modules/ts-loader/node_modules/loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -7923,13 +8700,13 @@ "dev": true }, "node_modules/ts-node": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", - "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, "peer": true, "dependencies": { - "@cspotcode/source-map-support": "0.6.1", + "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -7949,9 +8726,6 @@ "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" }, - "engines": { - "node": ">=12.0.0" - }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", @@ -7978,9 +8752,9 @@ } }, "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, "node_modules/tslint": { "version": "6.1.3", @@ -8081,6 +8855,12 @@ "node": ">=4" } }, + "node_modules/tslint/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/tslint/node_modules/tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", @@ -8100,9 +8880,9 @@ "dev": true }, "node_modules/ttypescript": { - "version": "1.5.12", - "resolved": "https://registry.npmjs.org/ttypescript/-/ttypescript-1.5.12.tgz", - "integrity": "sha512-1ojRyJvpnmgN9kIHmUnQPlEV1gq+VVsxVYjk/NfvMlHSmYxjK5hEvOOU2MQASrbekTUiUM7pR/nXeCc8bzvMOQ==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/ttypescript/-/ttypescript-1.5.13.tgz", + "integrity": "sha512-KT/RBfGGlVJFqEI8cVvI3nMsmYcFvPSZh8bU0qX+pAwbi7/ABmYkzn7l/K8skw0xmYjVCoyaV6WLsBQxdadybQ==", "dev": true, "dependencies": { "resolve": ">=1.9.0" @@ -8202,9 +8982,9 @@ } }, "node_modules/typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -8227,9 +9007,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.28", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz", - "integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==", + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", "dev": true, "funding": [ { @@ -8246,27 +9026,9 @@ } }, "node_modules/undefsafe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", - "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", - "dev": true, - "dependencies": { - "debug": "^2.2.0" - } - }, - "node_modules/undefsafe/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/undefsafe/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, "node_modules/union-value": { @@ -9033,6 +9795,15 @@ "node": ">= 6" } }, + "node_modules/webpack-log/node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", @@ -9294,50 +10065,37 @@ "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" }, "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "dependencies": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/wide-align/node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "engines": { - "node": ">=4" - } + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/wide-align/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/wide-align/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=4" - } - }, - "node_modules/wide-align/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/widest-line": { @@ -9352,6 +10110,35 @@ "node": ">=8" } }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/widest-line/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -9387,6 +10174,35 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -9405,9 +10221,9 @@ } }, "node_modules/ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", "dev": true, "engines": { "node": ">=8.3.0" @@ -9467,6 +10283,15 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -9494,6 +10319,35 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yargs/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9530,37 +10384,19 @@ "tslib": "^2.0.0" } }, - "node_modules/zone.js/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - }, "node_modules/zxcvbn": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", "integrity": "sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=" - }, - "node/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } } }, "dependencies": { "@angular/animations": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-11.2.14.tgz", - "integrity": "sha512-Heq/nNrCmb3jbkusu+BQszOecfFI/31Oxxj+CDQkqqYpBcswk6bOJLoEE472o+vmgxaXbgeflU9qbIiCQhpMFA==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-12.2.14.tgz", + "integrity": "sha512-1BR5u32auVePvXNNP96DB2008V+Ku0OGqeZQl2h4XA9xzES/Zk5WllIJZXqRmWMRBVARfXsfb0RdMty9gcaVjA==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "tslib": { @@ -9571,12 +10407,12 @@ } }, "@angular/cdk": { - "version": "11.2.13", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-11.2.13.tgz", - "integrity": "sha512-FkE4iCwoLbQxLDUOjV1I7M/6hmpyb7erAjEdWgch7nGRNxF1hqX5Bqf1lvLFKPNCbx5NRI5K7YVAdIUQUR8vug==", + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.13.tgz", + "integrity": "sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg==", "requires": { "parse5": "^5.0.0", - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "parse5": { @@ -9593,11 +10429,11 @@ } }, "@angular/common": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-11.2.14.tgz", - "integrity": "sha512-ZSLV/3j7eCTyLf/8g4yBFLWySjiLz3vLJAGWscYoUpnJWMnug1VRu6zoF/COxCbtORgE+Wz6K0uhfS6MziBGVw==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-12.2.14.tgz", + "integrity": "sha512-ffYUYdwZETmFJw0AcWY30WsaWBhJxj/zSmFXWjgEGEGZH56zwbbNwfMZOYZ1jz4haAVxGu+TdXsOl2yMGzN7jQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "tslib": { @@ -9608,11 +10444,11 @@ } }, "@angular/compiler": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-11.2.14.tgz", - "integrity": "sha512-XBOK3HgA+/y6Cz7kOX4zcJYmgJ264XnfcbXUMU2cD7Ac+mbNhLPKohWrEiSWalfcjnpf5gRfufQrQP7lpAGu0A==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-12.2.14.tgz", + "integrity": "sha512-dwmZi+n66IUzRFlGWu9mjXq170ZEsaDvlNLZzaPgs6vZTa4Kt7PWvIF/Y7TMvnVv/uqNG6kOhfmOkf6rfz1Gjg==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "tslib": { @@ -9623,11 +10459,11 @@ } }, "@angular/core": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-11.2.14.tgz", - "integrity": "sha512-vpR4XqBGitk1Faph37CSpemwIYTmJ3pdIVNoHKP6jLonpWu+0azkchf0f7oD8/2ivj2F81opcIw0tcsy/D/5Vg==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-12.2.14.tgz", + "integrity": "sha512-dlVk7yqUHL2R/eCmM8LsWuxhEBfzg0y1zHt0UqCuFwlCoiw+IG4HFy4OlZEUw9NUEZJSv0aDv3sWqxLkvK5vvg==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "tslib": { @@ -9638,11 +10474,11 @@ } }, "@angular/forms": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-11.2.14.tgz", - "integrity": "sha512-4LWqY6KEIk1AZQFnk+4PJSOCamlD4tumuVN06gO4D0dZo9Cx+GcvW6pM6N0CPubRvPs3sScCnu20WT11HNWC1w==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-12.2.14.tgz", + "integrity": "sha512-/9/gSJUBXVRVdRnzgJnALAQZYJATuGDMkFC9ms9DEMG4PMAhe9x4if1lJjN6noz5RAom3qNuVBNWaYAPUxlcBQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "tslib": { @@ -9653,11 +10489,11 @@ } }, "@angular/platform-browser": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-11.2.14.tgz", - "integrity": "sha512-fb7b7ss/gRoP8wLAN17W62leMgjynuyjEPU2eUoAAazsG9f2cgM+z3rK29GYncDVyYQxZUZYnjSqvL6GSXx86A==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.2.14.tgz", + "integrity": "sha512-fWcE2rnJ3ZCISa1oPfsIDV7FBZBoLFEdDuMXAiDYqDPKvF/E5U5nHrS+K4SlLAi094bMobtTOReNWl/Ienniyw==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "tslib": { @@ -9668,11 +10504,11 @@ } }, "@angular/platform-browser-dynamic": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.14.tgz", - "integrity": "sha512-TWTPdFs6iBBcp+/YMsgCRQwdHpWGq8KjeJDJ2tfatGgBD3Gqt2YaHOMST1zPW6RkrmupytTejuVqXzeaKWFxuw==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.2.14.tgz", + "integrity": "sha512-0NPF7mS91Tct8rBmOLZPmnLSuS4kbLHXo6eTgrg80OC0vlzBiQwGDVW4X3KncCoX9CpevaGJCdSMc+uPNsFOUQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "tslib": { @@ -9683,11 +10519,11 @@ } }, "@angular/router": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-11.2.14.tgz", - "integrity": "sha512-3aYBmj+zrEL9yf/ntIQxHIYaWShZOBKP3U07X2mX+TPMpGlvHDnR7L6bWhQVZwewzMMz7YVR16ldg50IFuAlfA==", + "version": "12.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-12.2.14.tgz", + "integrity": "sha512-yP5grSnqBvc4vNhtYdcxDgDYIebUKs5f0xyFkUJM5030UnQ0CV45tBsSxHMkQbPZucIfOuxpRy8xy5+4GizuwQ==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" }, "dependencies": { "tslib": { @@ -9698,12 +10534,12 @@ } }, "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", + "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", "dev": true, "requires": { - "@babel/highlight": "^7.14.5" + "@babel/highlight": "^7.16.0" } }, "@babel/helper-validator-identifier": { @@ -9713,12 +10549,12 @@ "dev": true }, "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.5", + "@babel/helper-validator-identifier": "^7.15.7", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -9778,21 +10614,22 @@ "@bitwarden/jslib-angular": { "version": "file:angular", "requires": { - "@angular/animations": "^11.2.11", - "@angular/cdk": "^11.2.10", - "@angular/common": "^11.2.11", - "@angular/compiler": "^11.2.11", - "@angular/core": "^11.2.11", - "@angular/forms": "^11.2.11", - "@angular/platform-browser": "^11.2.11", - "@angular/platform-browser-dynamic": "^11.2.11", - "@angular/router": "^11.2.11", + "@angular/animations": "^12.2.13", + "@angular/cdk": "^12.2.13", + "@angular/common": "^12.2.13", + "@angular/compiler": "^12.2.13", + "@angular/core": "^12.2.13", + "@angular/forms": "^12.2.13", + "@angular/platform-browser": "^12.2.13", + "@angular/platform-browser-dynamic": "^12.2.13", + "@angular/router": "^12.2.13", "@bitwarden/jslib-common": "file:../common", "@types/duo_web_sdk": "^2.7.1", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "rimraf": "^3.0.2", - "rxjs": "6.6.7", - "typescript": "4.1.5", + "rxjs": "^7.4.0", + "tldjs": "^2.3.1", + "typescript": "4.3.5", "zone.js": "0.11.4" } }, @@ -9802,7 +10639,7 @@ "@microsoft/signalr": "5.0.10", "@microsoft/signalr-protocol-msgpack": "5.0.10", "@types/lunr": "^2.3.3", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-forge": "^0.9.7", "@types/papaparse": "^5.2.5", "@types/zxcvbn": "^4.4.1", @@ -9812,9 +10649,9 @@ "node-forge": "^0.10.0", "papaparse": "^5.3.0", "rimraf": "^3.0.2", - "rxjs": "6.6.7", + "rxjs": "^7.4.0", "tldts": "5.7.52", - "typescript": "4.1.5", + "typescript": "4.3.5", "zxcvbn": "^4.4.2" } }, @@ -9823,15 +10660,15 @@ "requires": { "@bitwarden/jslib-common": "file:../common", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", - "@types/node": "^14.17.1", - "electron": "14.2.0", + "@types/node": "^16.11.12", + "electron": "16.0.2", "electron-log": "4.4.1", "electron-store": "8.0.1", - "electron-updater": "4.3.9", + "electron-updater": "4.6.1", "forcefocus": "^1.1.0", "keytar": "7.7.0", "rimraf": "^3.0.2", - "typescript": "4.1.5" + "typescript": "4.3.5" } }, "@bitwarden/jslib-node": { @@ -9840,7 +10677,7 @@ "@bitwarden/jslib-common": "file:../common", "@types/inquirer": "^7.3.1", "@types/lowdb": "^1.0.10", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", "@types/node-fetch": "^2.5.10", "chalk": "^4.1.1", "commander": "7.2.0", @@ -9850,19 +10687,7 @@ "lowdb": "1.0.0", "node-fetch": "^2.6.1", "rimraf": "^3.0.2", - "typescript": "4.1.5" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } + "typescript": "4.3.5" } }, "@cspotcode/source-map-consumer": { @@ -9873,9 +10698,9 @@ "peer": true }, "@cspotcode/source-map-support": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", - "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, "peer": true, "requires": { @@ -9883,14 +10708,14 @@ } }, "@electron/get": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.0.tgz", - "integrity": "sha512-+SjZhRuRo+STTO1Fdhzqnv9D2ZhjxXP6egsJ9kiO8dtP68cDx7dFCwWi64dlMQV7sWcfW1OYCW4wviEBzmRsfQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.1.tgz", + "integrity": "sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA==", "requires": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", - "global-agent": "^2.0.2", + "global-agent": "^3.0.0", "global-tunnel-ng": "^2.7.1", "got": "^9.6.0", "progress": "^2.0.3", @@ -9998,9 +10823,9 @@ "peer": true }, "@types/component-emitter": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", - "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==", "dev": true }, "@types/cookie": { @@ -10029,18 +10854,35 @@ "requires": { "@types/through": "*", "rxjs": "^6.4.0" + }, + "dependencies": { + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "@types/jasmine": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.9.1.tgz", - "integrity": "sha512-PVpjh8S8lqKFKurWSKdFATlfBHGPzgy0PoDdzQ+rr78jTQ0aacyh9YndzZcAUPxhk4kRujItFFGQdUJ7flHumw==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.2.tgz", + "integrity": "sha512-qs4xjVm4V/XjM6owGm/x6TNmhGl5iKX8dkTdsgdgl9oFnqgzxLepnS7rN9Tdo7kDmnFD/VEqKrW57cGD2odbEg==", "dev": true }, "@types/lodash": { - "version": "4.14.175", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz", - "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==", + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", "dev": true }, "@types/lowdb": { @@ -10059,9 +10901,10 @@ "dev": true }, "@types/node": { - "version": "14.17.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.20.tgz", - "integrity": "sha512-gI5Sl30tmhXsqkNvopFydP7ASc4c2cLfGNQrVKN3X90ADFWFsPEsotm/8JHSUJQKTHbwowAHtcJPeyVhtKv0TQ==" + "version": "16.11.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.13.tgz", + "integrity": "sha512-eUXZzHLHoZqj1frtUetNkUetYoJ6X55UmrVnFD4DMhVeAmwLjniZhtBmsRiemQh4uq4G3vUra/Ws/hs9vEvL3Q==", + "dev": true }, "@types/node-fetch": { "version": "2.5.12", @@ -10071,6 +10914,19 @@ "requires": { "@types/node": "*", "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } } }, "@types/node-forge": { @@ -10083,18 +10939,18 @@ } }, "@types/papaparse": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.2.6.tgz", - "integrity": "sha512-xGKSd0UTn58N1h0+zf8mW863Rv8BvXcGibEgKFtBIXZlcDXAmX/T4RdDO2mwmrmOypUDt5vRgo2v32a78JdqUA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.1.tgz", + "integrity": "sha512-1lbngk9wty2kCyQB42LjqSa12SEop3t9wcEC7/xYr3ujTSTmv7HWKjKYXly0GkMfQ42PRb2lFPFEibDOiMXS0g==", "dev": true, "requires": { "@types/node": "*" } }, "@types/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==" + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==" }, "@types/through": { "version": "0.0.30", @@ -10329,9 +11185,9 @@ } }, "acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", "dev": true }, "acorn-globals": { @@ -10366,6 +11222,16 @@ "debug": "4" } }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -10394,9 +11260,9 @@ }, "dependencies": { "ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -10425,12 +11291,37 @@ "dev": true, "requires": { "string-width": "^4.1.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } } }, "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, "ansi-escapes": { @@ -10571,6 +11462,12 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", @@ -10661,9 +11558,9 @@ } }, "base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz", + "integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==", "dev": true }, "base64-js": { @@ -10724,21 +11621,21 @@ "dev": true }, "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==", "dev": true, "requires": { - "bytes": "3.1.0", + "bytes": "3.1.1", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "1.7.2", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "qs": "6.9.6", + "raw-body": "2.4.2", + "type-is": "~1.6.18" }, "dependencies": { "debug": { @@ -10780,6 +11677,29 @@ "wrap-ansi": "^7.0.0" }, "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -10944,9 +11864,9 @@ "dev": true }, "builder-util-runtime": { - "version": "8.7.5", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.5.tgz", - "integrity": "sha512-fgUFHKtMNjdvH6PDRFntdIGUPgwZ69sXsAqEulCtoiqgWes5agrMq/Ud274zjJRTbckYh2PHh8/1CpFc6dpsbQ==", + "version": "8.9.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.9.1.tgz", + "integrity": "sha512-c8a8J3wK6BIVLW7ls+7TRK9igspTbzWmUqxFbgK0m40Ggm6efUbxtWVCGIjc+dtchyr5qAMAUL6iEGRdS/6vwg==", "requires": { "debug": "^4.3.2", "sax": "^1.2.4" @@ -10965,9 +11885,9 @@ "dev": true }, "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==", "dev": true }, "cacache": { @@ -11051,9 +11971,9 @@ } }, "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", "dev": true }, "chalk": { @@ -11141,6 +12061,12 @@ "static-extend": "^0.1.1" } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -11155,6 +12081,16 @@ "restore-cursor": "^3.1.0" } }, + "cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + } + }, "cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -11169,6 +12105,31 @@ "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } } }, "clone-deep": { @@ -11218,6 +12179,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -11267,9 +12234,9 @@ } }, "concurrently": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.2.2.tgz", - "integrity": "sha512-7a45BjVakAl3pprLOeqaOoZfIWZRmdC68NkjyzPbKu0/pE6CRmqS3l8RG7Q2cX9LnRHkB0oPM6af8PS7NEQvYQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.4.0.tgz", + "integrity": "sha512-HZ3D0RTQMH3oS4gvtYj1P+NBc6PzE2McEra6yEFcQKrUQ9HvtTGU4Dbne083F034p+LRb7kWU0tPRNvSGs1UCQ==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -11280,12 +12247,29 @@ "supports-color": "^8.1.0", "tree-kill": "^1.2.2", "yargs": "^16.2.0" + }, + "dependencies": { + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "conf": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/conf/-/conf-10.0.3.tgz", - "integrity": "sha512-4gtQ/Q36qVxBzMe6B7gWOAfni1VdhuHkIzxydHkclnwGmgN+eW4bb6jj73vigCfr7d3WlmqawvhZrpCUCTPYxQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.1.1.tgz", + "integrity": "sha512-z2civwq/k8TMYtcn3SVP0Peso4otIWnHtcTuHhQ0zDZDdP4NTxqEc8owfkz4zBsdMYdn/LFcE+ZhbCeqkhtq3Q==", "requires": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", @@ -11300,9 +12284,9 @@ }, "dependencies": { "ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -11467,12 +12451,6 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, - "core-js": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.1.tgz", - "integrity": "sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA==", - "optional": true - }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -11540,6 +12518,28 @@ "dev": true, "peer": true }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -11612,9 +12612,9 @@ } }, "date-fns": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz", - "integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.27.0.tgz", + "integrity": "sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==", "dev": true }, "date-format": { @@ -11639,9 +12639,9 @@ } }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -11840,13 +12840,20 @@ "dev": true }, "electron": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-14.2.0.tgz", - "integrity": "sha512-6CmAv1P0xcwK3FQOSA27fHI36/wctSFVgj46VODn56srXXQWeolkK1VzeAFNE613iAuuH9jJdHvE3gz+c7XkNA==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.2.tgz", + "integrity": "sha512-kT746yVMztrP4BbT3nrFNcUcfgFu2yelUw6TWBVTy0pju+fBISaqcvoiMrq+8U0vRpoXSu2MJYygOf4T0Det7g==", "requires": { - "@electron/get": "^1.0.1", + "@electron/get": "^1.13.0", "@types/node": "^14.6.2", "extract-zip": "^1.0.3" + }, + "dependencies": { + "@types/node": { + "version": "14.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.0.tgz", + "integrity": "sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ==" + } } }, "electron-log": { @@ -11871,15 +12878,15 @@ } }, "electron-updater": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.3.9.tgz", - "integrity": "sha512-LCNfedSwZfS4Hza+pDyPR05LqHtGorCStaBgVpRnfKxOlZcvpYEX0AbMeH5XUtbtGRoH2V8osbbf2qKPNb7AsA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.6.1.tgz", + "integrity": "sha512-YsU1mHqXLrXXmBMsxhxy24PrbaB8rnpZDPmFa2gOkTYk/Ch13+R0fjsRSpPYvqtskVVY0ux8fu+HnUkVkqc7og==", "requires": { - "@types/semver": "^7.3.5", - "builder-util-runtime": "8.7.5", + "@types/semver": "^7.3.6", + "builder-util-runtime": "8.9.1", "fs-extra": "^10.0.0", "js-yaml": "^4.1.0", - "lazy-val": "^1.0.4", + "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "^7.3.5" @@ -11969,9 +12976,10 @@ } }, "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "emojis-list": { "version": "3.0.0", @@ -11994,36 +13002,39 @@ } }, "engine.io": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.1.tgz", - "integrity": "sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.0.tgz", + "integrity": "sha512-ErhZOVu2xweCjEfYcTdkCnEYUiZgkAcBBAhW4jbIvNG8SLU3orAqoJCiytZjYF7eTpVmmCrLDjLIEaPlUAs1uw==", "dev": true, "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~4.0.0", - "ws": "~7.4.2" + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" }, "dependencies": { "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "dev": true, "requires": {} } } }, "engine.io-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz", - "integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.2.tgz", + "integrity": "sha512-wuiO7qO/OEkPJSFueuATIXtrxF7/6GTbAO9QLv7nnbjwZ5tYhLm9zxvLwxstRs0dcT0KUlWTjtIOs1T86jt12g==", "dev": true, "requires": { - "base64-arraybuffer": "0.1.4" + "base64-arraybuffer": "~1.0.1" } }, "enhanced-resolve": { @@ -12037,6 +13048,15 @@ "tapable": "^1.0.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", @@ -12156,9 +13176,9 @@ } }, "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, "esutils": { @@ -12202,6 +13222,23 @@ "safe-buffer": "^5.1.1" } }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -12262,6 +13299,16 @@ "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + } } }, "extglob": { @@ -12505,9 +13552,9 @@ } }, "follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz", + "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==", "dev": true }, "for-in": { @@ -12526,10 +13573,9 @@ } }, "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -12656,12 +13702,10 @@ "dev": true }, "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true }, "get-value": { "version": "2.0.6", @@ -12698,13 +13742,12 @@ } }, "global-agent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.2.0.tgz", - "integrity": "sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "optional": true, "requires": { "boolean": "^3.0.1", - "core-js": "^3.6.5", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", @@ -12784,6 +13827,16 @@ "p-cancelable": "^1.0.0", "to-readable-stream": "^1.0.0", "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + } } }, "graceful-fs": { @@ -12934,24 +13987,16 @@ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "requires": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "toidentifier": "1.0.1" } }, "http-proxy": { @@ -12991,6 +14036,18 @@ "debug": "4" } }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "husky": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", + "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -13028,6 +14085,12 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -13073,6 +14136,41 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } } }, "is-accessor-descriptor": { @@ -13120,9 +14218,9 @@ } }, "is-core-module": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz", - "integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "dev": true, "requires": { "has": "^1.0.3" @@ -13186,9 +14284,10 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true }, "is-glob": { "version": "4.0.3", @@ -13252,6 +14351,12 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -13303,19 +14408,19 @@ "dev": true }, "jasmine": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.9.0.tgz", - "integrity": "sha512-JgtzteG7xnqZZ51fg7N2/wiQmXon09szkALcRMTgCMX4u/m17gVJFjObnvw5FXkZOWuweHPaPRVB6DI2uN0wVA==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.10.0.tgz", + "integrity": "sha512-2Y42VsC+3CQCTzTwJezOvji4qLORmKIE0kwowWC+934Krn6ZXNQYljiwK5st9V3PVx96BSiDYXSB60VVah3IlQ==", "dev": true, "requires": { "glob": "^7.1.6", - "jasmine-core": "~3.9.0" + "jasmine-core": "~3.10.0" } }, "jasmine-core": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.9.0.tgz", - "integrity": "sha512-Tv3kVbPCGVrjsnHBZ38NsPU3sDOtNa0XmbG2baiyJqdb5/SPpDO6GVwJYtUryl6KB4q1Ssckwg612ES9Z0dreQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.10.1.tgz", + "integrity": "sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==", "dev": true }, "jasmine-ts-console-reporter": { @@ -13379,6 +14484,19 @@ "whatwg-url": "^8.5.0", "ws": "^7.4.6", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } } }, "json-buffer": { @@ -13427,9 +14545,9 @@ } }, "karma": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.4.tgz", - "integrity": "sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.9.tgz", + "integrity": "sha512-E/MqdLM9uVIhfuyVnrhlGBu4miafBdXEAEqCmwdEMh3n17C7UWC/8Kvm3AYKr91gc7scutekZ0xv6rxRaUCtnw==", "dev": true, "requires": { "body-parser": "^1.19.0", @@ -13450,10 +14568,10 @@ "qjobs": "^1.2.0", "range-parser": "^1.2.1", "rimraf": "^3.0.2", - "socket.io": "^3.1.0", + "socket.io": "^4.2.0", "source-map": "^0.6.1", "tmp": "^0.2.1", - "ua-parser-js": "^0.7.28", + "ua-parser-js": "^0.7.30", "yargs": "^16.1.1" }, "dependencies": { @@ -13462,15 +14580,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } } } }, @@ -13502,9 +14611,9 @@ } }, "karma-firefox-launcher": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.1.tgz", - "integrity": "sha512-VzDMgPseXak9DtfyE1O5bB2BwsMy1zzO1kUxVW1rP0yhC4tDNJ0p3JoFdzvrK4QqVzdqUMa9Rx9YzkdFp8hz3Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz", + "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==", "dev": true, "requires": { "is-wsl": "^2.2.0", @@ -13636,6 +14745,110 @@ "type-check": "~0.3.2" } }, + "lilconfig": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", + "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", + "dev": true + }, + "lint-staged": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.1.2.tgz", + "integrity": "sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==", + "dev": true, + "requires": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.16", + "commander": "^8.3.0", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "execa": "^5.1.1", + "lilconfig": "2.0.4", + "listr2": "^3.13.3", + "micromatch": "^4.0.4", + "normalize-path": "^3.0.0", + "object-inspect": "^1.11.0", + "string-argv": "^0.3.1", + "supports-color": "^9.0.2", + "yaml": "^1.10.2" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "supports-color": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.1.tgz", + "integrity": "sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==", + "dev": true + } + } + }, + "listr2": { + "version": "3.13.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.13.5.tgz", + "integrity": "sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==", + "dev": true, + "requires": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.4.0", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } + } + }, "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -13677,6 +14890,65 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "log4js": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz", @@ -13804,6 +15076,12 @@ "readable-stream": "^2.0.1" } }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -13833,22 +15111,22 @@ } }, "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true }, "mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" }, "mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "requires": { - "mime-db": "1.49.0" + "mime-db": "1.51.0" } }, "mimic-fn": { @@ -14100,9 +15378,9 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" }, "node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "requires": { "whatwg-url": "^5.0.0" }, @@ -14173,20 +15451,20 @@ } }, "nodemon": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.13.tgz", - "integrity": "sha512-UMXMpsZsv1UXUttCn6gv8eQPhn6DR4BW+txnL3IN5IHqrCwcrT/yWHfL35UsClGXknTH79r5xbu+6J1zNHuSyA==", + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz", + "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==", "dev": true, "requires": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", + "chokidar": "^3.5.2", + "debug": "^3.2.7", "ignore-by-default": "^1.0.1", "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", + "pstree.remy": "^1.1.8", "semver": "^5.7.1", "supports-color": "^5.5.0", "touch": "^3.1.0", - "undefsafe": "^2.0.3", + "undefsafe": "^2.0.5", "update-notifier": "^5.1.0" }, "dependencies": { @@ -14251,6 +15529,15 @@ "pify": "^3.0.0" } }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -14300,6 +15587,12 @@ } } }, + "object-inspect": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.1.tgz", + "integrity": "sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA==", + "dev": true + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -14403,6 +15696,15 @@ "p-limit": "^2.0.0" } }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -14505,6 +15807,12 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -14602,6 +15910,12 @@ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" }, + "prettier": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "dev": true + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -14722,9 +16036,9 @@ "dev": true }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==", "dev": true }, "querystring": { @@ -14770,13 +16084,13 @@ "dev": true }, "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", + "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", "dev": true, "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", + "bytes": "3.1.1", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } @@ -15007,11 +16321,11 @@ } }, "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", "requires": { - "tslib": "^1.9.0" + "tslib": "~2.1.0" } }, "safe-buffer": { @@ -15136,9 +16450,9 @@ "dev": true }, "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, "sha.js": { @@ -15160,10 +16474,25 @@ "kind-of": "^6.0.2" } }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "simple-concat": { "version": "1.0.1", @@ -15195,6 +16524,24 @@ } } }, + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz", + "integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==", + "dev": true + } + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -15300,26 +16647,23 @@ } }, "socket.io": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz", - "integrity": "sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.0.tgz", + "integrity": "sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ==", "dev": true, "requires": { - "@types/cookie": "^0.4.0", - "@types/cors": "^2.8.8", - "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "~2.0.0", - "debug": "~4.3.1", - "engine.io": "~4.1.0", - "socket.io-adapter": "~2.1.0", - "socket.io-parser": "~4.0.3" + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" } }, "socket.io-adapter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz", - "integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", "dev": true }, "socket.io-parser": { @@ -15359,9 +16703,9 @@ } }, "source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -15529,14 +16873,38 @@ "safe-buffer": "~5.1.0" } }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.0.1.tgz", + "integrity": "sha512-5ohWO/M4//8lErlUUtrFy3b11GtNOuMOU0ysKCDXFcfXuuvUXu95akgj/i8ofmaGdN0hCqyl6uu9i8dS/mQp5g==", + "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^9.2.2", + "is-fullwidth-code-point": "^4.0.0", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } } }, "strip-ansi": { @@ -15547,6 +16915,12 @@ "ansi-regex": "^5.0.1" } }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -15729,11 +17103,12 @@ "integrity": "sha512-xLa9sSdmSIec2kok76Fv68vvf58QZJFGEOan/pLsk9rrPfkfMZq7Vr03X0hPr8VCc09tcbfU6izXzRDl8oItYQ==" }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, "requires": { - "os-tmpdir": "~1.0.2" + "rimraf": "^3.0.0" } }, "to-arraybuffer": { @@ -15849,9 +17224,9 @@ } }, "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, "touch": { @@ -15912,9 +17287,9 @@ } }, "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -15949,13 +17324,13 @@ } }, "ts-node": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", - "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, "peer": true, "requires": { - "@cspotcode/source-map-support": "0.6.1", + "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -15979,9 +17354,9 @@ } }, "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, "tslint": { "version": "6.1.3", @@ -16060,6 +17435,12 @@ "has-flag": "^3.0.0" } }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", @@ -16078,9 +17459,9 @@ "dev": true }, "ttypescript": { - "version": "1.5.12", - "resolved": "https://registry.npmjs.org/ttypescript/-/ttypescript-1.5.12.tgz", - "integrity": "sha512-1ojRyJvpnmgN9kIHmUnQPlEV1gq+VVsxVYjk/NfvMlHSmYxjK5hEvOOU2MQASrbekTUiUM7pR/nXeCc8bzvMOQ==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/ttypescript/-/ttypescript-1.5.13.tgz", + "integrity": "sha512-KT/RBfGGlVJFqEI8cVvI3nMsmYcFvPSZh8bU0qX+pAwbi7/ABmYkzn7l/K8skw0xmYjVCoyaV6WLsBQxdadybQ==", "dev": true, "requires": { "resolve": ">=1.9.0" @@ -16150,9 +17531,9 @@ } }, "typescript": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", - "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true }, "typescript-transform-paths": { @@ -16165,36 +17546,16 @@ } }, "ua-parser-js": { - "version": "0.7.28", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz", - "integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==", + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", "dev": true }, "undefsafe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", - "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", - "dev": true, - "requires": { - "debug": "^2.2.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true }, "union-value": { "version": "1.0.1", @@ -16979,6 +18340,14 @@ "requires": { "ansi-colors": "^3.0.0", "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + } } }, "webpack-sources": { @@ -17040,38 +18409,31 @@ "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" }, "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "requires": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2 || 2 || 3 || 4" }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } } } @@ -17083,6 +18445,31 @@ "dev": true, "requires": { "string-width": "^4.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } } }, "word-wrap": { @@ -17109,6 +18496,31 @@ "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + } } }, "wrappy": { @@ -17129,9 +18541,9 @@ } }, "ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", "dev": true, "requires": {} }, @@ -17171,6 +18583,12 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -17186,6 +18604,29 @@ "yargs-parser": "^20.2.2" }, "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -17222,13 +18663,6 @@ "integrity": "sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw==", "requires": { "tslib": "^2.0.0" - }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - } } }, "zxcvbn": { diff --git a/package.json b/package.json index 0ff173fa..9dc0cd84 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,21 @@ "build:watch": "npm run clean && ttsc -watch", "lint": "tslint '*/src/**/*.ts' 'spec/**/*.ts'", "lint:fix": "tslint '*/src/**/*.ts' 'spec/**/*.ts' --fix", + "prettier": "prettier --write .", "test": "karma start ./spec/support/karma.conf.js --single-run", "test:watch": "karma start ./spec/support/karma.conf.js", "test:node": "npm run build && jasmine", - "test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"" + "test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"", + "prepare": "husky install" }, "devDependencies": { "@fluffy-spoon/substitute": "^1.202.0", "@types/jasmine": "^3.7.6", - "@types/node": "^14.17.1", + "@types/node": "^16.11.12", + "commander": "7.2.0", "concurrently": "^6.1.0", + "form-data": "4.0.0", + "husky": "^7.0.4", "jasmine": "^3.7.0", "jasmine-core": "^3.7.1", "jasmine-ts-console-reporter": "^3.1.1", @@ -41,13 +46,16 @@ "karma-jasmine-html-reporter": "^1.5.4", "karma-safari-launcher": "^1.0.0", "karma-webpack": "^4.0.2", + "lint-staged": "^12.1.2", "nodemon": "^2.0.7", + "prettier": "2.5.1", "rimraf": "^3.0.2", + "rxjs": "^7.4.0", "ts-loader": "^8.1.0", "tslint": "^6.1.3", "ttypescript": "^1.5.12", "typemoq": "^2.1.0", - "typescript": "4.1.5", + "typescript": "4.3.5", "typescript-transform-paths": "^2.2.3", "webpack": "^4.46.0" }, @@ -58,7 +66,10 @@ "@bitwarden/jslib-node": "file:node" }, "engines": { - "node": "~14", - "npm": "~7" + "node": "~16", + "npm": "~8" + }, + "lint-staged": { + "*.{js,ts,css,scss,md}": "prettier --write" } } diff --git a/spec/common/importers/fsecureFskImporter.spec.ts b/spec/common/importers/fsecureFskImporter.spec.ts new file mode 100644 index 00000000..63d082df --- /dev/null +++ b/spec/common/importers/fsecureFskImporter.spec.ts @@ -0,0 +1,77 @@ +import { FSecureFskImporter as Importer } from "jslib-common/importers/fsecureFskImporter"; + +const TestDataWithStyleSetToWebsite: string = JSON.stringify({ + data: { + "8d58b5cf252dd06fbd98f5289e918ab1": { + color: "#00baff", + reatedDate: 1609302913, + creditCvv: "", + creditExpiry: "", + creditNumber: "", + favorite: 0, + modifiedDate: 1609302913, + notes: "note", + password: "word", + passwordList: [], + passwordModifiedDate: 1609302913, + rev: 1, + service: "My first pass", + style: "website", + type: 1, + url: "https://bitwarden.com", + username: "pass", + }, + }, +}); + +const TestDataWithStyleSetToGlobe: string = JSON.stringify({ + data: { + "8d58b5cf252dd06fbd98f5289e918ab1": { + color: "#00baff", + reatedDate: 1609302913, + creditCvv: "", + creditExpiry: "", + creditNumber: "", + favorite: 0, + modifiedDate: 1609302913, + notes: "note", + password: "word", + passwordList: [], + passwordModifiedDate: 1609302913, + rev: 1, + service: "My first pass", + style: "globe", + type: 1, + url: "https://bitwarden.com", + username: "pass", + }, + }, +}); + +describe("FSecure FSK Importer", () => { + it("should parse data with style set to website", async () => { + const importer = new Importer(); + const result = await importer.parse(TestDataWithStyleSetToWebsite); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.login.username).toEqual("pass"); + expect(cipher.login.password).toEqual("word"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://bitwarden.com"); + }); + + it("should parse data with style set to globe", async () => { + const importer = new Importer(); + const result = await importer.parse(TestDataWithStyleSetToGlobe); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.login.username).toEqual("pass"); + expect(cipher.login.password).toEqual("word"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://bitwarden.com"); + }); +}); diff --git a/spec/common/services/cipher.service.spec.ts b/spec/common/services/cipher.service.spec.ts index 83dce24c..e1bf461c 100644 --- a/spec/common/services/cipher.service.spec.ts +++ b/spec/common/services/cipher.service.spec.ts @@ -1,64 +1,71 @@ -import { Arg, Substitute, SubstituteOf } from '@fluffy-spoon/substitute'; +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { CryptoService } from 'jslib-common/abstractions/crypto.service'; -import { FileUploadService } from 'jslib-common/abstractions/fileUpload.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; -import { LogService } from 'jslib-common/abstractions/log.service'; -import { SearchService } from 'jslib-common/abstractions/search.service'; -import { SettingsService } from 'jslib-common/abstractions/settings.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; -import { Utils } from 'jslib-common/misc/utils'; -import { Cipher } from 'jslib-common/models/domain/cipher'; -import { EncArrayBuffer } from 'jslib-common/models/domain/encArrayBuffer'; -import { EncString } from 'jslib-common/models/domain/encString'; -import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; +import { ApiService } from "jslib-common/abstractions/api.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { FileUploadService } from "jslib-common/abstractions/fileUpload.service"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { SearchService } from "jslib-common/abstractions/search.service"; +import { SettingsService } from "jslib-common/abstractions/settings.service"; +import { StateService } from "jslib-common/abstractions/state.service"; -import { CipherService } from 'jslib-common/services/cipher.service'; +import { Utils } from "jslib-common/misc/utils"; +import { Cipher } from "jslib-common/models/domain/cipher"; +import { EncArrayBuffer } from "jslib-common/models/domain/encArrayBuffer"; +import { EncString } from "jslib-common/models/domain/encString"; +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; -const ENCRYPTED_TEXT = 'This data has been encrypted'; +import { CipherService } from "jslib-common/services/cipher.service"; + +const ENCRYPTED_TEXT = "This data has been encrypted"; const ENCRYPTED_BYTES = new EncArrayBuffer(Utils.fromUtf8ToArray(ENCRYPTED_TEXT).buffer); -describe('Cipher Service', () => { - let cryptoService: SubstituteOf; - let userService: SubstituteOf; - let settingsService: SubstituteOf; - let apiService: SubstituteOf; - let fileUploadService: SubstituteOf; - let storageService: SubstituteOf; - let i18nService: SubstituteOf; - let searchService: SubstituteOf; - let logService: SubstituteOf; +describe("Cipher Service", () => { + let cryptoService: SubstituteOf; + let stateService: SubstituteOf; + let settingsService: SubstituteOf; + let apiService: SubstituteOf; + let fileUploadService: SubstituteOf; + let i18nService: SubstituteOf; + let searchService: SubstituteOf; + let logService: SubstituteOf; - let cipherService: CipherService; + let cipherService: CipherService; - beforeEach(() => { - cryptoService = Substitute.for(); - userService = Substitute.for(); - settingsService = Substitute.for(); - apiService = Substitute.for(); - fileUploadService = Substitute.for(); - storageService = Substitute.for(); - i18nService = Substitute.for(); - searchService = Substitute.for(); - logService = Substitute.for(); + beforeEach(() => { + cryptoService = Substitute.for(); + stateService = Substitute.for(); + settingsService = Substitute.for(); + apiService = Substitute.for(); + fileUploadService = Substitute.for(); + i18nService = Substitute.for(); + searchService = Substitute.for(); + logService = Substitute.for(); - cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES); - cryptoService.encrypt(Arg.any(), Arg.any()).resolves(new EncString(ENCRYPTED_TEXT)); + cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES); + cryptoService.encrypt(Arg.any(), Arg.any()).resolves(new EncString(ENCRYPTED_TEXT)); - cipherService = new CipherService(cryptoService, userService, settingsService, apiService, fileUploadService, - storageService, i18nService, () => searchService, logService); - }); + cipherService = new CipherService( + cryptoService, + settingsService, + apiService, + fileUploadService, + i18nService, + () => searchService, + logService, + stateService + ); + }); - it('attachments upload encrypted file contents', async () => { - const key = new SymmetricCryptoKey(new Uint8Array(32).buffer); - const fileName = 'filename'; - const fileData = new Uint8Array(10).buffer; - cryptoService.getOrgKey(Arg.any()).resolves(new SymmetricCryptoKey(new Uint8Array(32).buffer)); + it("attachments upload encrypted file contents", async () => { + const fileName = "filename"; + const fileData = new Uint8Array(10).buffer; + cryptoService.getOrgKey(Arg.any()).resolves(new SymmetricCryptoKey(new Uint8Array(32).buffer)); - await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData); + await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData); - fileUploadService.received(1).uploadCipherAttachment(Arg.any(), Arg.any(), new EncString(ENCRYPTED_TEXT), ENCRYPTED_BYTES); - }); + fileUploadService + .received(1) + .uploadCipherAttachment(Arg.any(), Arg.any(), new EncString(ENCRYPTED_TEXT), ENCRYPTED_BYTES); + }); }); diff --git a/tslint.json b/tslint.json index ff350764..4122e547 100644 --- a/tslint.json +++ b/tslint.json @@ -40,7 +40,6 @@ "object-literal-shorthand": [ true, "never" ], "ordered-imports": true, "prefer-for-of": false, - "quotemark": [ true, "single" ], "whitespace": [ true, "check-branch", @@ -52,14 +51,7 @@ "check-type" ], "max-classes-per-file": false, - "arrow-parens": [ - true, - "ban-single-arg-parens" - ], - "semicolon": [ - true, - "always" - ], + "arrow-parens": [true], "trailing-comma": [ true, {