1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

Compare commits

...

69 Commits

Author SHA1 Message Date
Thomas Rittson
01e894b5bc uninstall ldapjs 2024-09-30 11:14:02 +10:00
Thomas Rittson
aa5a9f3e2f Initial implementation of ldapts 2024-09-30 11:13:16 +10:00
Thomas Rittson
f4181b13f7 Install ldapts 2024-09-30 09:54:04 +10:00
renovate[bot]
4bb96f049c [deps]: Update electron-updater to v6.3.0 [SECURITY] (#624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 12:03:07 -05:00
renovate[bot]
b991fea958 [deps]: Update jest-preset-angular to v13.1.6 (#597)
* [deps]: Update jest-preset-angular to v13.1.6

* Install angular-devkit/build-angular

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2024-09-23 10:51:49 +10:00
renovate[bot]
111b8bd646 [deps]: Update electron-store to v8.2.0 (#606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 10:21:45 +10:00
renovate[bot]
42af888615 [deps]: Update electron to v28.3.3 (#603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 10:11:54 +10:00
renovate[bot]
a28fad020b [deps]: Update @types/node-fetch to v2.6.11 (#593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 10:00:58 +10:00
renovate[bot]
f190433348 [deps]: Update @angular/cdk to v16.2.14 (#589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 09:51:05 +10:00
renovate[bot]
1e211becc3 [deps]: Update @types/inquirer to v8.2.10 (#591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 09:40:09 +10:00
Thomas Rittson
4652c6489f [AC-3043] Refactor AuthService to only use organization api key login (#622)
* Remove jslib authService and unused loginStrategies

* Delete KeyConnectorService

* Move OrganizationLoginStrategy into base LoginStrategy

* Remove unused code and services from loginStrategy

* Delete OrganizationService

* Move loginStrategy into authService
2024-09-23 08:46:38 +10:00
Thomas Rittson
9dc497dd13 [AC-3047] Refactor LoginCommand to only use organization api key login (#621)
* Add tests

* Remove unused code from LoginCommand and refactor

* Remove unused services

* Remove unused npm deps

* Install missing type-fest dep
2024-09-19 07:50:40 +10:00
Opeyemi
3d9465917d [BRE-246] - Use GH App for Auto PR 2024-09-17 18:08:01 +01:00
renovate[bot]
6b1b6bf1c4 [deps]: Update bootstrap to v5 [SECURITY] (#585)
* [deps]: Update bootstrap to v5 [SECURITY]

* Use the color-contrast method instead of the deprecated color-yiq method

* Update settings component for bootstrap 5

* Update ApiKey component for bootstrap 5

* Update environment component for bootstrap 5

* Run prettier

* Revert back to data-dismiss

* Update modal close button attribute to use data-bs-dismiss instead of data-dismiss

* Run npm prettier

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Rui Tome <rtome@bitwarden.com>
2024-09-17 10:53:56 +01:00
renovate[bot]
f52af53dad [deps]: Update sass-loader to v16 (#561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 10:46:23 +10:00
renovate[bot]
6e6039d298 [deps]: Update sass to v1.78.0 (#612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 10:38:21 +10:00
renovate[bot]
332d07eca6 [deps]: Update html-loader to v5.1.0 (#609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2024-09-17 09:50:56 +10:00
renovate[bot]
c3ed541efd [deps]: Update eslint-import-resolver-typescript to v3.6.3 (#595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 09:45:16 +10:00
renovate[bot]
4ae4cba877 [deps]: Update @types/node to v18.19.50 (#599)
* [deps]: Update @types/node to v18.19.50

* Adjust timer type

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Addison Beck <github@addisonbeck.com>
2024-09-16 11:17:57 -04:00
renovate[bot]
3ecca16f50 [deps]: Update css-loader to v6.11.0 (#602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 09:14:18 -04:00
renovate[bot]
1f30ef165f [deps]: Update electron-log to v5.2.0 (#605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 08:38:08 -04:00
renovate[bot]
dfd8fce231 [deps]: Update mini-css-extract-plugin to v2.9.1 (#611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 08:29:10 -04:00
renovate[bot]
0b7c0ec9c2 [deps]: Update ts-jest to v29.2.5 (#614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 08:25:25 -04:00
renovate[bot]
6d569e9319 [deps]: Update eslint-plugin-import to v2.30.0 (#608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 08:12:12 -04:00
renovate[bot]
3ed4e76f95 [deps]: Update concurrently to v9 (#617)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 07:58:30 -04:00
renovate[bot]
abf7e0400c [deps]: Update typescript-transform-paths to v3.5.1 (#615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 14:21:51 +10:00
renovate[bot]
5600f20760 [deps]: Update commander to v12.1.0 (#600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 13:50:19 +10:00
Vince Grassia
bbc65d77e3 Revert "[BRE-246] - Use GH-App for version bump workflow (#586)" (#587)
This reverts commit 5d0cde9cfa.
2024-09-11 09:58:17 -04:00
Opeyemi
5d0cde9cfa [BRE-246] - Use GH-App for version bump workflow (#586)
* Use GH-App for version bump workflow

* clean secret
2024-09-11 11:45:50 +01:00
renovate[bot]
eff7c848f8 [AC-2224] [deps]: Update open to v10 (#456)
* [deps]: Update open to v10

* Remove package

* Remove references

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Addison Beck <hello@addisonbeck.com>
Co-authored-by: Addison Beck <github@addisonbeck.com>
2024-09-10 08:33:02 -04:00
renovate[bot]
46fb407c0c [deps]: Update sonarsource/sonarcloud-github-action action to v3 (#572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-09 16:07:21 -04:00
renovate[bot]
e2fe5ef9ad [deps]: Update dotenv to v16.4.5 (#581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-09 15:33:53 -04:00
Thomas Rittson
dd10538d0f Add SafeProvider (#582) 2024-09-06 08:11:57 +10:00
Bitwarden DevOps
6d355812e0 Bumped version to 2024.9.0 (#583) 2024-09-05 14:28:13 -05:00
Alex Urbina
c973d5fea0 BRE-141 Refactor Release workflow to split deploy/publish steps (#567)
* BRE-141 REFACTOR: Release workflow to split deploy/publish steps

* Update .github/workflows/release.yml

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

* BRE-141 DELETE: publish.yml workflow

---------

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2024-09-05 14:49:28 +10:00
renovate[bot]
01a3f68480 [deps]: Update lint-staged to v15.2.10 (#579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-05 13:47:41 +10:00
renovate[bot]
d31f14cfe7 [deps]: Update rimraf to v5.0.10 (#578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-05 13:36:48 +10:00
renovate[bot]
401daa0187 [deps]: Update prettier to v3.3.3 (#577)
* [deps]: Update prettier to v3.3.3

* Run prettier

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2024-09-05 11:13:53 +10:00
Thomas Rittson
1951b9507d Update PR template from template repo (#574) 2024-09-05 08:13:42 +10:00
Thomas Rittson
eae9cac931 [AC-3020] Remove unused jslib code - services (#575)
* Delete NotificationsService

* Remove SyncService

* Delete VaultTimeoutService

* Remove ProviderService

* Remove UserVerificationService

* Remove SendService

* Remove EventService

* Remove PasswordRepromptService

* Remove UsernameGenerationService

* Remove TotpService

* Remove CollectionService

* Remove FolderService

* Remove AuditService

* Remove CipherService and SearchService together

* Remove FileUploadService

* Remove SettingsService

* Remove SystemService

* Remove ElectronCryptoService

* Remove unused deps
2024-09-05 08:11:18 +10:00
Thomas Rittson
21cecc3c0a Remove npm minor grouping from renovate config (#576) 2024-09-05 06:50:41 +10:00
Thomas Rittson
21638f3fdc Specify npm version ~10 (#573)
This was previously set to ~10.4, but there is no node version that
satisifed the constraint of node ~18 and npm ~10.4.

This change follows the approach in the main `clients` repository.
2024-09-03 06:39:13 +10:00
renovate[bot]
f47806ddd2 [deps]: Update webpack-merge to v6 (#564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 12:03:41 +10:00
renovate[bot]
c304650a6a [deps]: Update webpack to v5.94.0 [SECURITY] (#568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-02 11:08:05 +10:00
Matt Bishop
d01522bfc4 Configure Codecov coverage and results (#569)
* Configure Codecov coverage and results

* Actually produce reports
2024-08-30 16:48:13 -04:00
Vince Grassia
14314a3553 Switch to API key (#566) 2024-08-26 11:49:15 -04:00
Matt Bishop
ffac82e865 Don't scan on nonexistent RCs (#549) 2024-08-14 09:08:41 -04:00
Addison Beck
decada8745 Filter out deleted AD users unless otherwise instructed (#548) 2024-08-12 11:04:07 -04:00
Matt Bishop
3a639bb8f2 Use latest Renovate config (#547) 2024-08-07 09:53:40 -04:00
Dillon Beresford
a2b5dac108 Include action to support Checkmarx and Sonar (#546)
* Include action to support Checkmarx and Sonar

* Update .github/workflows/scan.yml

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

* Update .github/workflows/scan.yml

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

* Bump versions for outdated github actions

---------

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2024-08-05 12:12:46 -04:00
Rui Tomé
6e76d8fcbd [PM-4747] List members under each group when doing test sync (#507)
* Add user list under each group when doing test sync

* run prettier and lint, replace '@' with &#64;
2024-07-23 10:46:20 +01:00
Addison Beck
63b06f6950 Throw an error if the gsuite member query fails (#522) 2024-07-19 10:05:48 -04:00
Vince Grassia
f730aeba23 Remove logic for 'rc' branch (#521) 2024-07-19 10:49:36 +02:00
Bitwarden DevOps
52a8a35f41 Bumped version to 2024.7.0 (#495) 2024-07-01 17:14:31 +00:00
renovate[bot]
601a83ebfa [deps]: Update actions/checkout action to v4.1.7 (#491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-24 13:16:20 -04:00
Bitwarden DevOps
0cdb12229e Bumped version to 2024.6.0 (#488) 2024-06-03 18:09:06 +00:00
Opeyemi
423a48ab2e Upgrade to macos-13 runner (#486) 2024-06-01 01:03:31 +01:00
renovate[bot]
9a2bf331bc [deps]: Update act10ns/slack action to v2.1.0 (#484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-29 11:39:28 -04:00
Alex Urbina
0d211f351c BRE-87 ADD: Slack notification enable feature and check in version-bump workflow (#485) 2024-05-27 15:34:03 -06:00
Alex Urbina
d1d4f53866 BRE-40 ADD: step to report upcoming release version to Slack (#482) 2024-05-21 10:59:58 -06:00
renovate[bot]
dede587b78 [deps]: Update actions/checkout action to v4.1.6 (#483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 11:06:15 -04:00
renovate[bot]
d76a6e993d [deps]: Update actions/checkout action to v4.1.5 (#480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-13 09:24:46 -04:00
Bitwarden DevOps
32d514ebcc Bumped version to 2024.5.0 (#479) 2024-05-06 22:35:47 +00:00
renovate[bot]
1f08c7e5cc [deps]: Update actions/upload-artifact action to v4.3.3 (#478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-06 10:19:12 -04:00
renovate[bot]
dd80dce657 [deps]: Update actions/checkout action to v4.1.4 (#477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-29 09:59:22 -04:00
Bitwarden DevOps
ff6dff329a Bumped version to 2024.4.0 (#476) 2024-04-23 11:30:19 +00:00
renovate[bot]
8c6f1aab90 [deps]: Update actions/upload-artifact action to v4.3.2 (#475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 14:19:02 -04:00
Vince Grassia
9a3aae16a7 Add push trigger (#474) 2024-03-27 17:07:25 +00:00
Bitwarden DevOps
1480445d35 Bumped version to 2024.3.2 (#473) 2024-03-27 16:25:01 +00:00
112 changed files with 2615 additions and 10645 deletions

7
.github/CODEOWNERS vendored
View File

@@ -6,10 +6,3 @@
# Default file owners.
* @bitwarden/team-admin-console-dev
# DevOps for Actions and other workflow changes.
.github/workflows @bitwarden/dept-devops
.github/secrets @bitwarden/dept-devops
# Multiple Owners
**/package.json

View File

@@ -1,33 +1,34 @@
## Type of change
## 🎟️ Tracking
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->
## Objective
## 📔 Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->
## Code changes
## 📸 Screenshots
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories-->
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
- **file.ext:** Description of what was changed and why
## ⏰ Reminders before review
## Screenshots
- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team
<!--Required for any UI changes. Delete if not applicable-->
## 🦮 Reviewer guidelines
## Testing requirements
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes

25
.github/renovate.json vendored
View File

@@ -1,31 +1,12 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
"github>bitwarden/renovate-config:pin-actions",
":combinePatchMinorReleases",
":dependencyDashboard",
":maintainLockFilesWeekly",
":pinAllExceptPeerDependencies",
":prConcurrentLimit10",
":rebaseStalePrs",
":separateMajorReleases",
"group:monorepos",
"schedule:weekends"
],
"extends": ["github>bitwarden/renovate-config"],
"enabledManagers": ["github-actions", "npm"],
"commitMessagePrefix": "[deps]:",
"commitMessageTopic": "{{depName}}",
"packageRules": [
{
"groupName": "npm minor",
"matchManagers": ["npm"],
"groupName": "gh minor",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"matchFileNames": ["package.json"],
"description": "Admin Console owns general dependencies",
"reviewers": ["team:team-admin-console-dev"]
}
]
}

View File

@@ -3,6 +3,9 @@ name: Build
on:
pull_request: {}
push:
branches:
- "main"
workflow_dispatch: {}
jobs:
@@ -11,7 +14,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Set up CLOC
run: |
@@ -29,7 +32,7 @@ jobs:
package_version: ${{ steps.retrieve-version.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Get Package Version
id: retrieve-version
@@ -48,7 +51,7 @@ jobs:
_PKG_FETCH_VERSION: 3.4
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Set up Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -119,14 +122,14 @@ jobs:
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Linux checksum to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -135,7 +138,7 @@ jobs:
macos-cli:
name: Build Mac CLI
runs-on: macos-12
runs-on: macos-13
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
@@ -143,7 +146,7 @@ jobs:
_PKG_FETCH_VERSION: 3.4
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Set up Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -207,14 +210,14 @@ jobs:
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Mac checksum to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -230,7 +233,7 @@ jobs:
_WIN_PKG_VERSION: 3.4
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup Windows builder
run: |
@@ -351,14 +354,14 @@ jobs:
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Windows checksum to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -375,7 +378,7 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Set up Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -411,28 +414,28 @@ jobs:
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: latest.yml
path: ./dist/latest.yml
@@ -449,7 +452,7 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Set up Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -479,14 +482,14 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
@@ -495,7 +498,7 @@ jobs:
macos-gui:
name: Build MacOS GUI
runs-on: macos-12
runs-on: macos-13
needs: setup
env:
NODE_OPTIONS: --max_old_space_size=4096
@@ -503,7 +506,7 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Set up Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -573,37 +576,45 @@ jobs:
- name: Install Node dependencies
run: npm install
- name: Set up private auth key
run: |
mkdir ~/private_keys
cat << EOF > ~/private_keys/AuthKey_UFD296548T.p8
${{ secrets.APP_STORE_CONNECT_AUTH_KEY }}
EOF
- name: Build application
run: npm run dist:mac
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: UFD296548T
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_UFD296548T.p8
CSC_FOR_PULL_REQUEST: true
- name: Upload .zip artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml
@@ -624,7 +635,7 @@ jobs:
- macos-gui
steps:
- name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc') && contains(needs.*.result, 'failure')
if: github.ref == 'refs/heads/main' && contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - CI subscription
@@ -642,7 +653,7 @@ jobs:
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}

View File

@@ -1,53 +0,0 @@
---
name: Cleanup RC Branch
on:
push:
tags:
- v**
jobs:
delete-rc:
name: Delete RC Branch
runs-on: ubuntu-22.04
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve bot secrets
id: retrieve-bot-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout main
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
- name: Check if a RC branch exists
id: branch-check
run: |
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
elif [[ "${rc_branch_check}" -gt 0 ]]; then
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=rc" >> $GITHUB_OUTPUT
fi
- name: Delete RC branch
env:
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
run: |
if ! [[ -z "$BRANCH_NAME" ]]; then
git push --quiet origin --delete $BRANCH_NAME
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
fi

View File

@@ -22,14 +22,14 @@ jobs:
release-version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "[!] Can only release from the 'main' branch"
echo "==================================="
exit 1
fi
@@ -47,16 +47,6 @@ jobs:
runs-on: ubuntu-22.04
needs: setup
steps:
- name: Create GitHub deployment
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
id: deployment
with:
token: '${{ secrets.GITHUB_TOKEN }}'
initial-status: 'in_progress'
environment: 'production'
description: 'Deployment ${{ needs.setup.outputs.release-version }} from branch ${{ github.ref_name }}'
task: release
- name: Download all artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
@@ -65,7 +55,7 @@ jobs:
workflow_conclusion: success
branch: ${{ github.ref_name }}
- name: Download all artifacts
- name: Dry Run - Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
@@ -101,19 +91,3 @@ jobs:
body: "<insert release notes here>"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
- name: Update deployment status to Success
if: ${{ success() }}
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'success'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
- name: Update deployment status to Failure
if: ${{ failure() }}
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'failure'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}

78
.github/workflows/scan.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: Scan
on:
workflow_dispatch:
push:
branches:
- "main"
pull_request_target:
types: [opened, synchronize]
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
sast:
name: SAST scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
pull-requests: write
security-events: write
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@6c56658230f79c227a55120e9b24845d574d5225 # 2.0.31
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
additional_params: |
--report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.tests=.
-Dsonar.sources=.
-Dsonar.test.inclusions=**/*.spec.ts
-Dsonar.exclusions=**/*.spec.ts

View File

@@ -1,28 +1,46 @@
---
name: Run tests
name: Testing
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc-*"
pull_request: {}
defaults:
run:
shell: bash
pull_request:
jobs:
test:
name: Run tests
check-test-secrets:
name: Check for test secrets
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
outputs:
available: ${{ steps.check-test-secrets.outputs.available }}
permissions:
contents: read
- name: Get Node Version
steps:
- name: Check
id: check-test-secrets
run: |
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
echo "available=true" >> $GITHUB_OUTPUT;
else
echo "available=false" >> $GITHUB_OUTPUT;
fi
testing:
name: Run tests
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
runs-on: ubuntu-22.04
needs: check-test-secrets
permissions:
checks: write
contents: read
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Get Node version
id: retrieve-node-version
run: |
NODE_NVMRC=$(cat .nvmrc)
@@ -36,11 +54,6 @@ jobs:
cache-dependency-path: '**/package-lock.json'
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
- name: Print environment
run: |
node --version
npm --version
- name: Install Node dependencies
run: npm ci
@@ -51,4 +64,25 @@ jobs:
run: npm run test:types --coverage
- name: Run tests
run: npm run test
run: npm run test --coverage
- name: Report test results
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
with:
name: Test Results
path: "junit.xml"
reporter: jest-junit
fail-on-error: true
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Upload results to codecov.io
uses: codecov/test-results-action@1b5b448b98e58ba90d1a1a1d9fcb72ca2263be46 # v1.0.0
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -8,9 +8,9 @@ on:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
enable_slack_notification:
description: "Enable Slack notifications for upcoming release?"
default: false
type: boolean
jobs:
@@ -26,21 +26,17 @@ jobs:
with:
version: ${{ inputs.version_number_override }}
- name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
- name: Check if RC branch exists
if: ${{ inputs.cut_rc_branch == true }}
- name: Slack Notification Check
run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
echo "Slack notifications enabled."
else
echo "Slack notifications disabled."
fi
- name: Checkout Branch
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
@@ -52,8 +48,7 @@ jobs:
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
@@ -154,11 +149,19 @@ jobs:
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
run: git push -u origin $PR_BRANCH
- name: Generate GH App token
uses: actions/create-github-app-token@3378cda945da322a8db4b193e19d46352ebe2de5 # v1.10.4
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
owner: ${{ github.repository_owner }}
- name: Create Version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: create-pr
env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: |
@@ -189,37 +192,14 @@ jobs:
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
steps:
- name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Report upcoming release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
ref: main
- name: Verify version has been updated
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
- name: Cut RC branch
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc
version: ${{ steps.set-final-version-output.outputs.version }}
project: ${{ github.repository }}
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@ npm-debug.log
# Build directories
dist
build
build-cli
.angular/cache
# Testing

View File

@@ -11,6 +11,14 @@ module.exports = {
// ...angularPreset,
preset: "jest-preset-angular",
reporters: ["default", "jest-junit"],
collectCoverage: true,
// Ensure we collect coverage from files without tests
collectCoverageFrom: ["src/**/*.ts"],
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],

View File

@@ -1,45 +0,0 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
@Injectable()
export class AuthGuardService {
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.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;
}
}

View File

@@ -1,188 +1,70 @@
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
import { LOCALE_ID, NgModule } from "@angular/core";
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
import { AuditService as AuditServiceAbstraction } from "@/jslib/common/src/abstractions/audit.service";
import { AuthService as AuthServiceAbstraction } from "@/jslib/common/src/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
import { CipherService as CipherServiceAbstraction } from "@/jslib/common/src/abstractions/cipher.service";
import { CollectionService as CollectionServiceAbstraction } from "@/jslib/common/src/abstractions/collection.service";
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
import { EventService as EventServiceAbstraction } from "@/jslib/common/src/abstractions/event.service";
import { FileUploadService as FileUploadServiceAbstraction } from "@/jslib/common/src/abstractions/fileUpload.service";
import { FolderService as FolderServiceAbstraction } from "@/jslib/common/src/abstractions/folder.service";
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@/jslib/common/src/abstractions/notifications.service";
import { OrganizationService as OrganizationServiceAbstraction } from "@/jslib/common/src/abstractions/organization.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@/jslib/common/src/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
import { PolicyService as PolicyServiceAbstraction } from "@/jslib/common/src/abstractions/policy.service";
import { ProviderService as ProviderServiceAbstraction } from "@/jslib/common/src/abstractions/provider.service";
import { SearchService as SearchServiceAbstraction } from "@/jslib/common/src/abstractions/search.service";
import { SendService as SendServiceAbstraction } from "@/jslib/common/src/abstractions/send.service";
import { SettingsService as SettingsServiceAbstraction } from "@/jslib/common/src/abstractions/settings.service";
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "@/jslib/common/src/abstractions/sync.service";
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
import { TotpService as TotpServiceAbstraction } from "@/jslib/common/src/abstractions/totp.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@/jslib/common/src/abstractions/userVerification.service";
import { UsernameGenerationService as UsernameGenerationServiceAbstraction } from "@/jslib/common/src/abstractions/usernameGeneration.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@/jslib/common/src/abstractions/vaultTimeout.service";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { Account } from "@/jslib/common/src/models/domain/account";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { ApiService } from "@/jslib/common/src/services/api.service";
import { AppIdService } from "@/jslib/common/src/services/appId.service";
import { AuditService } from "@/jslib/common/src/services/audit.service";
import { AuthService } from "@/jslib/common/src/services/auth.service";
import { CipherService } from "@/jslib/common/src/services/cipher.service";
import { CollectionService } from "@/jslib/common/src/services/collection.service";
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
import { EventService } from "@/jslib/common/src/services/event.service";
import { FileUploadService } from "@/jslib/common/src/services/fileUpload.service";
import { FolderService } from "@/jslib/common/src/services/folder.service";
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
import { NotificationsService } from "@/jslib/common/src/services/notifications.service";
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
import { PolicyService } from "@/jslib/common/src/services/policy.service";
import { ProviderService } from "@/jslib/common/src/services/provider.service";
import { SearchService } from "@/jslib/common/src/services/search.service";
import { SendService } from "@/jslib/common/src/services/send.service";
import { SettingsService } from "@/jslib/common/src/services/settings.service";
import { StateService } from "@/jslib/common/src/services/state.service";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
import { SyncService } from "@/jslib/common/src/services/sync.service";
import { TokenService } from "@/jslib/common/src/services/token.service";
import { TotpService } from "@/jslib/common/src/services/totp.service";
import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service";
import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service";
import { UsernameGenerationService } from "@/jslib/common/src/services/usernameGeneration.service";
import { VaultTimeoutService } from "@/jslib/common/src/services/vaultTimeout.service";
import { AuthGuardService } from "./auth-guard.service";
import {
SafeInjectionToken,
SECURE_STORAGE,
WINDOW,
} from "../../../../src/app/services/injection-tokens";
import { SafeProvider, safeProvider } from "../../../../src/app/services/safe-provider";
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({
declarations: [],
providers: [
{ provide: "WINDOW", useValue: window },
{
provide: LOCALE_ID,
safeProvider({ provide: WINDOW, useValue: window }),
safeProvider({
provide: LOCALE_ID as SafeInjectionToken<string>,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
deps: [I18nServiceAbstraction],
},
ValidationService,
AuthGuardService,
UnauthGuardService,
LockGuardService,
ModalService,
{
}),
safeProvider(ValidationService),
safeProvider(ModalService),
safeProvider({
provide: AppIdServiceAbstraction,
useClass: AppIdService,
deps: [StorageServiceAbstraction],
},
{
provide: AuditServiceAbstraction,
useClass: AuditService,
deps: [CryptoFunctionServiceAbstraction, ApiServiceAbstraction],
},
{
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
LogService,
KeyConnectorServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
],
},
{
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],
},
{
}),
safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }),
safeProvider({
provide: EnvironmentServiceAbstraction,
useClass: EnvironmentService,
deps: [StateServiceAbstraction],
},
{
provide: TotpServiceAbstraction,
useClass: TotpService,
deps: [CryptoFunctionServiceAbstraction, LogService, StateServiceAbstraction],
},
{ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] },
{
}),
safeProvider({
provide: TokenServiceAbstraction,
useClass: TokenService,
deps: [StateServiceAbstraction],
}),
safeProvider({
provide: CryptoServiceAbstraction,
useClass: CryptoService,
deps: [
@@ -191,18 +73,8 @@ import { ValidationService } from "./validation.service";
LogService,
StateServiceAbstraction,
],
},
{
provide: PasswordGenerationServiceAbstraction,
useClass: PasswordGenerationService,
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
},
{
provide: UsernameGenerationServiceAbstraction,
useClass: UsernameGenerationService,
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
},
{
}),
safeProvider({
provide: ApiServiceAbstraction,
useFactory: (
tokenService: TokenServiceAbstraction,
@@ -225,116 +97,13 @@ import { ValidationService } from "./validation.service";
MessagingServiceAbstraction,
AppIdServiceAbstraction,
],
},
{
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 (userId?: string) =>
messagingService.send("logout", { expired: false, userId: userId }),
),
deps: [
CipherServiceAbstraction,
FolderServiceAbstraction,
CollectionServiceAbstraction,
CryptoServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
SearchServiceAbstraction,
TokenServiceAbstraction,
PolicyServiceAbstraction,
KeyConnectorServiceAbstraction,
StateServiceAbstraction,
],
},
{
}),
safeProvider({
provide: BroadcasterServiceAbstraction,
useClass: BroadcasterService,
useAngularDecorators: true,
}),
safeProvider({
provide: StateServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
@@ -351,12 +120,12 @@ import { ValidationService } from "./validation.service";
),
deps: [
StorageServiceAbstraction,
"SECURE_STORAGE",
SECURE_STORAGE,
LogService,
StateMigrationServiceAbstraction,
],
},
{
}),
safeProvider({
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
@@ -367,108 +136,8 @@ import { ValidationService } from "./validation.service";
secureStorageService,
new StateFactory(GlobalState, Account),
),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
},
{
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: 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,
CryptoFunctionServiceAbstraction,
],
},
{
provide: UserVerificationServiceAbstraction,
useClass: UserVerificationService,
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, ApiServiceAbstraction],
},
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
{
provide: OrganizationServiceAbstraction,
useClass: OrganizationService,
deps: [StateServiceAbstraction],
},
{
provide: ProviderServiceAbstraction,
useClass: ProviderService,
deps: [StateServiceAbstraction],
},
{
provide: TwoFactorServiceAbstraction,
useClass: TwoFactorService,
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
},
],
deps: [StorageServiceAbstraction, SECURE_STORAGE],
}),
] satisfies SafeProvider[],
})
export class JslibServicesModule {}

View File

@@ -1,29 +0,0 @@
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
@Injectable()
export class LockGuardService {
protected homepage = "vault";
protected loginpage = "login";
constructor(
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private stateService: StateService,
) {}
async canActivate() {
if (await this.vaultTimeoutService.isLocked()) {
return true;
}
const redirectUrl = (await this.stateService.getIsAuthenticated())
? [this.homepage]
: [this.loginpage];
this.router.navigate(redirectUrl);
return false;
}
}

View File

@@ -143,7 +143,7 @@ export class ModalService {
dialogEl.style.zIndex = `${this.modalCount}050`;
const modals = Array.from(
el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]'),
el.querySelectorAll('.modal-backdrop, .modal *[data-bs-dismiss="modal"]'),
);
for (const closeElement of modals) {
closeElement.addEventListener("click", () => {

View File

@@ -1,45 +0,0 @@
import { Injectable } from "@angular/core";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
import { ModalService } from "./modal.service";
/**
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
* See UserVerificationService for any other situation where you need to verify the user's identity.
*/
@Injectable()
export class PasswordRepromptService implements PasswordRepromptServiceAbstraction {
protected component = PasswordRepromptComponent;
constructor(
private modalService: ModalService,
private keyConnectorService: KeyConnectorService,
) {}
protectedFields() {
return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"];
}
async showPasswordPrompt() {
if (!(await this.enabled())) {
return true;
}
const ref = this.modalService.open(this.component, { allowMultipleModals: true });
if (ref == null) {
return false;
}
const result = await ref.onClosedPromise();
return result === true;
}
async enabled() {
return !(await this.keyConnectorService.getUsesKeyConnector());
}
}

View File

@@ -1,29 +0,0 @@
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { VaultTimeoutService } from "@/jslib/common/src/abstractions/vaultTimeout.service";
@Injectable()
export class UnauthGuardService {
protected homepage = "vault";
constructor(
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private stateService: StateService,
) {}
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;
}
}

View File

@@ -1,114 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { ApiLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/apiLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
describe("ApiLogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let environmentService: SubstituteOf<EnvironmentService>;
let keyConnectorService: SubstituteOf<KeyConnectorService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let apiLogInStrategy: ApiLogInStrategy;
let credentials: ApiLogInCredentials;
const deviceId = Utils.newGuid();
const keyConnectorUrl = "KEY_CONNECTOR_URL";
const apiClientId = "API_CLIENT_ID";
const apiClientSecret = "API_CLIENT_SECRET";
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
environmentService = Substitute.for<EnvironmentService>();
stateService = Substitute.for<StateService>();
keyConnectorService = Substitute.for<KeyConnectorService>();
twoFactorService = Substitute.for<TwoFactorService>();
appIdService.getAppId().resolves(deviceId);
tokenService.getTwoFactorToken().resolves(null);
apiLogInStrategy = new ApiLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
environmentService,
keyConnectorService,
);
credentials = new ApiLogInCredentials(apiClientId, apiClientSecret);
});
it("sends api key credentials to the server", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await apiLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const apiTokenRequest = actual as any;
return (
apiTokenRequest.clientId === apiClientId &&
apiTokenRequest.clientSecret === apiClientSecret &&
apiTokenRequest.device.identifier === deviceId &&
apiTokenRequest.twoFactor.provider == null &&
apiTokenRequest.twoFactor.token == null &&
apiTokenRequest.captchaResponse == null
);
}),
);
});
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await apiLogInStrategy.logIn(credentials);
stateService.received(1).setApiKeyClientId(apiClientId);
stateService.received(1).setApiKeyClientSecret(apiClientSecret);
stateService.received(1).addAccount(Arg.any());
});
it("gets and sets the Key Connector key from environmentUrl", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
await apiLogInStrategy.logIn(credentials);
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
});
});

View File

@@ -1,288 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { Account, AccountProfile, AccountTokens } from "@/jslib/common/src/models/domain/account";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import { EncString } from "@/jslib/common/src/models/domain/encString";
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { PasswordTokenRequest } from "@/jslib/common/src/models/request/identityToken/passwordTokenRequest";
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
import { IdentityCaptchaResponse } from "@/jslib/common/src/models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { IdentityTwoFactorResponse } from "@/jslib/common/src/models/response/identityTwoFactorResponse";
const email = "hello@world.com";
const masterPassword = "password";
const deviceId = Utils.newGuid();
const accessToken = "ACCESS_TOKEN";
const refreshToken = "REFRESH_TOKEN";
const encKey = "ENC_KEY";
const privateKey = "PRIVATE_KEY";
const captchaSiteKey = "CAPTCHA_SITE_KEY";
const kdf = 0;
const kdfIterations = 10000;
const userId = Utils.newGuid();
const masterPasswordHash = "MASTER_PASSWORD_HASH";
const decodedToken = {
sub: userId,
email: email,
premium: false,
};
const twoFactorProviderType = TwoFactorProviderType.Authenticator;
const twoFactorToken = "TWO_FACTOR_TOKEN";
const twoFactorRemember = true;
export function identityTokenResponseFactory() {
return new IdentityTokenResponse({
ForcePasswordReset: false,
Kdf: kdf,
KdfIterations: kdfIterations,
Key: encKey,
PrivateKey: privateKey,
ResetMasterPassword: false,
access_token: accessToken,
expires_in: 3600,
refresh_token: refreshToken,
scope: "api offline_access",
token_type: "Bearer",
});
}
describe("LogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let authService: SubstituteOf<AuthService>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
stateService = Substitute.for<StateService>();
twoFactorService = Substitute.for<TwoFactorService>();
authService = Substitute.for<AuthService>();
appIdService.getAppId().resolves(deviceId);
// The base class is abstract so we test it via PasswordLogInStrategy
passwordLogInStrategy = new PasswordLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
authService,
);
credentials = new PasswordLogInCredentials(email, masterPassword);
});
describe("base class", () => {
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
tokenService.decodeToken(accessToken).resolves(decodedToken);
await passwordLogInStrategy.logIn(credentials);
stateService.received(1).addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: userId,
email: email,
hasPremiumPersonally: false,
kdfIterations: kdfIterations,
kdfType: kdf,
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: accessToken,
refreshToken: refreshToken,
},
},
}),
);
cryptoService.received(1).setEncKey(encKey);
cryptoService.received(1).setEncPrivateKey(privateKey);
stateService.received(1).setBiometricLocked(false);
messagingService.received(1).send("loggedIn");
});
it("builds AuthResult", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.forcePasswordReset = true;
tokenResponse.resetMasterPassword = true;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
const expected = new AuthResult();
expected.forcePasswordReset = true;
expected.resetMasterPassword = true;
expected.twoFactorProviders = null;
expected.captchaSiteKey = "";
expect(result).toEqual(expected);
});
it("rejects login if CAPTCHA is required", async () => {
// Sample CAPTCHA response
const tokenResponse = new IdentityCaptchaResponse({
error: "invalid_grant",
error_description: "Captcha required.",
HCaptcha_SiteKey: captchaSiteKey,
});
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
stateService.didNotReceive().addAccount(Arg.any());
messagingService.didNotReceive().send(Arg.any());
const expected = new AuthResult();
expected.captchaSiteKey = captchaSiteKey;
expect(result).toEqual(expected);
});
it("makes a new public and private key for an old account", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.privateKey = null;
cryptoService.makeKeyPair(Arg.any()).resolves(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postAccountKeys(Arg.any());
});
});
describe("Two-factor authentication", () => {
it("rejects login if 2FA is required", async () => {
// Sample response where TOTP 2FA required
const tokenResponse = new IdentityTwoFactorResponse({
TwoFactorProviders: ["0"],
TwoFactorProviders2: { 0: null },
error: "invalid_grant",
error_description: "Two factor required.",
});
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
stateService.didNotReceive().addAccount(Arg.any());
messagingService.didNotReceive().send(Arg.any());
const expected = new AuthResult();
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
expected.twoFactorProviders.set(0, null);
expect(result).toEqual(expected);
});
it("sends stored 2FA token to server", async () => {
tokenService.getTwoFactorToken().resolves(twoFactorToken);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === false
);
}),
);
});
it("sends 2FA token provided by user to server (single step)", async () => {
// This occurs if the user enters the 2FA code as an argument in the CLI
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
credentials.twoFactor = new TokenRequestTwoFactor(
twoFactorProviderType,
twoFactorToken,
twoFactorRemember,
);
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === twoFactorRemember
);
}),
);
});
it("sends 2FA token provided by user to server (two-step)", async () => {
// Simulate a partially completed login
passwordLogInStrategy.tokenRequest = new PasswordTokenRequest(
email,
masterPasswordHash,
null,
null,
);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await passwordLogInStrategy.logInTwoFactor(
new TokenRequestTwoFactor(twoFactorProviderType, twoFactorToken, twoFactorRemember),
null,
);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === twoFactorRemember
);
}),
);
});
});
});

View File

@@ -1,110 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { HashPurpose } from "@/jslib/common/src/enums/hashPurpose";
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
const email = "hello@world.com";
const masterPassword = "password";
const hashedPassword = "HASHED_PASSWORD";
const localHashedPassword = "LOCAL_HASHED_PASSWORD";
const preloginKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg==",
),
);
const deviceId = Utils.newGuid();
describe("PasswordLogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let authService: SubstituteOf<AuthService>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
stateService = Substitute.for<StateService>();
twoFactorService = Substitute.for<TwoFactorService>();
authService = Substitute.for<AuthService>();
appIdService.getAppId().resolves(deviceId);
tokenService.getTwoFactorToken().resolves(null);
authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey);
cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword);
cryptoService
.hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization)
.resolves(localHashedPassword);
passwordLogInStrategy = new PasswordLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
authService,
);
credentials = new PasswordLogInCredentials(email, masterPassword);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
});
it("sends master password credentials to the server", async () => {
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any; // Need to access private fields
return (
passwordTokenRequest.email === email &&
passwordTokenRequest.masterPasswordHash === hashedPassword &&
passwordTokenRequest.device.identifier === deviceId &&
passwordTokenRequest.twoFactor.provider == null &&
passwordTokenRequest.twoFactor.token == null &&
passwordTokenRequest.captchaResponse == null
);
}),
);
});
it("sets the local environment after a successful login", async () => {
await passwordLogInStrategy.logIn(credentials);
cryptoService.received(1).setKey(preloginKey);
cryptoService.received(1).setKeyHash(localHashedPassword);
});
});

View File

@@ -1,127 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { SsoLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/ssoLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { SsoLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
describe("SsoLogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let keyConnectorService: SubstituteOf<KeyConnectorService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let ssoLogInStrategy: SsoLogInStrategy;
let credentials: SsoLogInCredentials;
const deviceId = Utils.newGuid();
const encKey = "ENC_KEY";
const privateKey = "PRIVATE_KEY";
const keyConnectorUrl = "KEY_CONNECTOR_URL";
const ssoCode = "SSO_CODE";
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
const ssoRedirectUrl = "SSO_REDIRECT_URL";
const ssoOrgId = "SSO_ORG_ID";
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
stateService = Substitute.for<StateService>();
keyConnectorService = Substitute.for<KeyConnectorService>();
twoFactorService = Substitute.for<TwoFactorService>();
tokenService.getTwoFactorToken().resolves(null);
appIdService.getAppId().resolves(deviceId);
ssoLogInStrategy = new SsoLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
keyConnectorService,
);
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
});
it("sends SSO information to server", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await ssoLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const ssoTokenRequest = actual as any;
return (
ssoTokenRequest.code === ssoCode &&
ssoTokenRequest.codeVerifier === ssoCodeVerifier &&
ssoTokenRequest.redirectUri === ssoRedirectUrl &&
ssoTokenRequest.device.identifier === deviceId &&
ssoTokenRequest.twoFactor.provider == null &&
ssoTokenRequest.twoFactor.token == null
);
}),
);
});
it("does not set keys for new SSO user flow", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.key = null;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
cryptoService.didNotReceive().setEncPrivateKey(privateKey);
cryptoService.didNotReceive().setEncKey(encKey);
});
it("gets and sets KeyConnector key for enrolled user", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
});
it("converts new SSO user to Key Connector on first login", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
tokenResponse.key = null;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
keyConnectorService.received(1).convertNewSsoUserToKeyConnector(tokenResponse, ssoOrgId);
});
});

View File

@@ -1,69 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { FileUploadService } from "@/jslib/common/src/abstractions/fileUpload.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { SearchService } from "@/jslib/common/src/abstractions/search.service";
import { SettingsService } from "@/jslib/common/src/abstractions/settings.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import { Cipher } from "@/jslib/common/src/models/domain/cipher";
import { EncArrayBuffer } from "@/jslib/common/src/models/domain/encArrayBuffer";
import { EncString } from "@/jslib/common/src/models/domain/encString";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { CipherService } from "@/jslib/common/src/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<CryptoService>;
let stateService: SubstituteOf<StateService>;
let settingsService: SubstituteOf<SettingsService>;
let apiService: SubstituteOf<ApiService>;
let fileUploadService: SubstituteOf<FileUploadService>;
let i18nService: SubstituteOf<I18nService>;
let searchService: SubstituteOf<SearchService>;
let logService: SubstituteOf<LogService>;
let cipherService: CipherService;
beforeEach(() => {
cryptoService = Substitute.for<CryptoService>();
stateService = Substitute.for<StateService>();
settingsService = Substitute.for<SettingsService>();
apiService = Substitute.for<ApiService>();
fileUploadService = Substitute.for<FileUploadService>();
i18nService = Substitute.for<I18nService>();
searchService = Substitute.for<SearchService>();
logService = Substitute.for<LogService>();
cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES);
cryptoService.encrypt(Arg.any(), Arg.any()).resolves(new EncString(ENCRYPTED_TEXT));
cipherService = new CipherService(
cryptoService,
settingsService,
apiService,
fileUploadService,
i18nService,
() => searchService,
logService,
stateService,
);
});
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);
fileUploadService
.received(1)
.uploadCipherAttachment(Arg.any(), Arg.any(), new EncString(ENCRYPTED_TEXT), ENCRYPTED_BYTES);
});
});

View File

@@ -1,6 +0,0 @@
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
export abstract class AuditService {
passwordLeaked: (password: string) => Promise<number>;
breachedAccounts: (username: string) => Promise<BreachAccountResponse[]>;
}

View File

@@ -1,25 +0,0 @@
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
} from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
export abstract class AuthService {
masterPasswordHash: string;
email: string;
logIn: (
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
) => Promise<AuthResult>;
logInTwoFactor: (
twoFactor: TokenRequestTwoFactor,
captchaResponse: string,
) => Promise<AuthResult>;
logOut: (callback: () => void) => void;
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
authingWithApiKey: () => boolean;
authingWithSso: () => boolean;
authingWithPassword: () => boolean;
}

View File

@@ -1,79 +0,0 @@
import { CipherType } from "../enums/cipherType";
import { UriMatchType } from "../enums/uriMatchType";
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 { CipherView } from "../models/view/cipherView";
import { FieldView } from "../models/view/fieldView";
export abstract class CipherService {
clearCache: (userId?: string) => Promise<void>;
encrypt: (
model: CipherView,
key?: SymmetricCryptoKey,
originalCipher?: Cipher,
) => Promise<Cipher>;
encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise<Field[]>;
encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise<Field>;
get: (id: string) => Promise<Cipher>;
getAll: () => Promise<Cipher[]>;
getAllDecrypted: () => Promise<CipherView[]>;
getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise<CipherView[]>;
getAllDecryptedForUrl: (
url: string,
includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchType,
) => Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
getNextCipherForUrl: (url: string) => Promise<CipherView>;
updateLastUsedIndexForUrl: (url: string) => void;
updateLastUsedDate: (id: string) => Promise<void>;
updateLastLaunchedDate: (id: string) => Promise<void>;
saveNeverDomain: (domain: string) => Promise<void>;
saveWithServer: (cipher: Cipher) => Promise<any>;
shareWithServer: (
cipher: CipherView,
organizationId: string,
collectionIds: string[],
) => Promise<any>;
shareManyWithServer: (
ciphers: CipherView[],
organizationId: string,
collectionIds: string[],
) => Promise<any>;
saveAttachmentWithServer: (
cipher: Cipher,
unencryptedFile: any,
admin?: boolean,
) => Promise<Cipher>;
saveAttachmentRawWithServer: (
cipher: Cipher,
filename: string,
data: ArrayBuffer,
admin?: boolean,
) => Promise<Cipher>;
saveCollectionsWithServer: (cipher: Cipher) => Promise<any>;
upsert: (cipher: CipherData | CipherData[]) => Promise<any>;
replace: (ciphers: { [id: string]: CipherData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
moveManyWithServer: (ids: string[], folderId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
deleteWithServer: (id: string) => Promise<any>;
deleteManyWithServer: (ids: string[]) => Promise<any>;
deleteAttachment: (id: string, attachmentId: string) => Promise<void>;
deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise<void>;
sortCiphersByLastUsed: (a: any, b: any) => number;
sortCiphersByLastUsedThenName: (a: any, b: any) => number;
getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number;
softDelete: (id: string | string[]) => Promise<any>;
softDeleteWithServer: (id: string) => Promise<any>;
softDeleteManyWithServer: (ids: string[]) => Promise<any>;
restore: (
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) => Promise<any>;
restoreWithServer: (id: string) => Promise<any>;
restoreManyWithServer: (ids: string[]) => Promise<any>;
}

View File

@@ -1,19 +0,0 @@
import { CollectionData } from "../models/data/collectionData";
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/treeNode";
import { CollectionView } from "../models/view/collectionView";
export abstract class CollectionService {
clearCache: (userId?: string) => Promise<void>;
encrypt: (model: CollectionView) => Promise<Collection>;
decryptMany: (collections: Collection[]) => Promise<CollectionView[]>;
get: (id: string) => Promise<Collection>;
getAll: () => Promise<Collection[]>;
getAllDecrypted: () => Promise<CollectionView[]>;
getAllNested: (collections?: CollectionView[]) => Promise<TreeNode<CollectionView>[]>;
getNested: (id: string) => Promise<TreeNode<CollectionView>>;
upsert: (collection: CollectionData | CollectionData[]) => Promise<any>;
replace: (collections: { [id: string]: CollectionData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@@ -1,7 +0,0 @@
import { EventType } from "../enums/eventType";
export abstract class EventService {
collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise<any>;
uploadEvents: (userId?: string) => Promise<any>;
clearEvents: (userId?: string) => Promise<any>;
}

View File

@@ -1,18 +0,0 @@
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { EncString } from "../models/domain/encString";
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
import { SendFileUploadDataResponse } from "../models/response/sendFileUploadDataResponse";
export abstract class FileUploadService {
uploadSendFile: (
uploadData: SendFileUploadDataResponse,
fileName: EncString,
encryptedFileData: EncArrayBuffer,
) => Promise<any>;
uploadCipherAttachment: (
admin: boolean,
uploadData: AttachmentUploadDataResponse,
fileName: EncString,
encryptedFileData: EncArrayBuffer,
) => Promise<any>;
}

View File

@@ -1,21 +0,0 @@
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 { FolderView } from "../models/view/folderView";
export abstract class FolderService {
clearCache: (userId?: string) => Promise<void>;
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
get: (id: string) => Promise<Folder>;
getAll: () => Promise<Folder[]>;
getAllDecrypted: () => Promise<FolderView[]>;
getAllNested: () => Promise<TreeNode<FolderView>[]>;
getNested: (id: string) => Promise<TreeNode<FolderView>>;
saveWithServer: (folder: Folder) => Promise<any>;
upsert: (folder: FolderData | FolderData[]) => Promise<any>;
replace: (folders: { [id: string]: FolderData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
deleteWithServer: (id: string) => Promise<any>;
}

View File

@@ -1,19 +0,0 @@
import { Organization } from "../models/domain/organization";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
export abstract class KeyConnectorService {
getAndSetKey: (url?: string) => Promise<void>;
getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>;
userNeedsMigration: () => Promise<boolean>;
convertNewSsoUserToKeyConnector: (
tokenResponse: IdentityTokenResponse,
orgId: string,
) => Promise<void>;
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
setConvertAccountRequired: (status: boolean) => Promise<void>;
getConvertAccountRequired: () => Promise<boolean>;
removeConvertAccountRequired: () => Promise<void>;
clear: () => Promise<void>;
}

View File

@@ -1,6 +0,0 @@
export abstract class NotificationsService {
init: () => Promise<void>;
updateConnection: (sync?: boolean) => Promise<void>;
reconnectFromActivity: () => Promise<void>;
disconnectFromInactivity: () => Promise<void>;
}

View File

@@ -1,11 +0,0 @@
import { OrganizationData } from "../models/data/organizationData";
import { Organization } from "../models/domain/organization";
export abstract class OrganizationService {
get: (id: string) => Promise<Organization>;
getByIdentifier: (identifier: string) => Promise<Organization>;
getAll: (userId?: string) => Promise<Organization[]>;
save: (orgs: { [id: string]: OrganizationData }) => Promise<any>;
canManageSponsorships: () => Promise<boolean>;
hasOrganizations: (userId?: string) => Promise<boolean>;
}

View File

@@ -1,20 +0,0 @@
import * as zxcvbn from "zxcvbn";
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
export abstract class PasswordGenerationService {
generatePassword: (options: any) => Promise<string>;
generatePassphrase: (options: any) => Promise<string>;
getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>;
enforcePasswordGeneratorPoliciesOnOptions: (
options: any,
) => Promise<[any, PasswordGeneratorPolicyOptions]>;
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
saveOptions: (options: any) => Promise<any>;
getHistory: () => Promise<GeneratedPasswordHistory[]>;
addHistory: (password: string) => Promise<any>;
clear: (userId?: string) => Promise<any>;
passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult;
normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void;
}

View File

@@ -1,5 +0,0 @@
export abstract class PasswordRepromptService {
protectedFields: () => string[];
showPasswordPrompt: () => Promise<boolean>;
enabled: () => Promise<boolean>;
}

View File

@@ -1,32 +0,0 @@
import { PolicyType } from "../enums/policyType";
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 { ListResponse } from "../models/response/listResponse";
import { PolicyResponse } from "../models/response/policyResponse";
export abstract class PolicyService {
clearCache: () => void;
getAll: (type?: PolicyType, userId?: string) => Promise<Policy[]>;
getPolicyForOrganization: (policyType: PolicyType, organizationId: string) => Promise<Policy>;
replace: (policies: { [id: string]: PolicyData }) => Promise<any>;
clear: (userId?: string) => Promise<any>;
getMasterPasswordPoliciesForInvitedUsers: (orgId: string) => Promise<MasterPasswordPolicyOptions>;
getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise<MasterPasswordPolicyOptions>;
evaluateMasterPassword: (
passwordStrength: number,
newPassword: string,
enforcedPolicyOptions?: MasterPasswordPolicyOptions,
) => boolean;
getResetPasswordPolicyOptions: (
policies: Policy[],
orgId: string,
) => [ResetPasswordPolicyOptions, boolean];
mapPoliciesFromToken: (policiesResponse: ListResponse<PolicyResponse>) => Policy[];
policyAppliesToUser: (
policyType: PolicyType,
policyFilter?: (policy: Policy) => boolean,
userId?: string,
) => Promise<boolean>;
}

View File

@@ -1,8 +0,0 @@
import { ProviderData } from "../models/data/providerData";
import { Provider } from "../models/domain/provider";
export abstract class ProviderService {
get: (id: string) => Promise<Provider>;
getAll: () => Promise<Provider[]>;
save: (providers: { [id: string]: ProviderData }) => Promise<any>;
}

View File

@@ -1,16 +0,0 @@
import { CipherView } from "../models/view/cipherView";
import { SendView } from "../models/view/sendView";
export abstract class SearchService {
indexedEntityId?: string = null;
clearIndex: () => void;
isSearchable: (query: string) => boolean;
indexCiphers: (indexedEntityGuid?: string, ciphersToIndex?: CipherView[]) => Promise<void>;
searchCiphers: (
query: string,
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
ciphers?: CipherView[],
) => Promise<CipherView[]>;
searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[];
searchSends: (sends: SendView[], query: string) => SendView[];
}

View File

@@ -1,25 +0,0 @@
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 { SendView } from "../models/view/sendView";
export abstract class SendService {
clearCache: () => Promise<void>;
encrypt: (
model: SendView,
file: File | ArrayBuffer,
password: string,
key?: SymmetricCryptoKey,
) => Promise<[Send, EncArrayBuffer]>;
get: (id: string) => Promise<Send>;
getAll: () => Promise<Send[]>;
getAllDecrypted: () => Promise<SendView[]>;
saveWithServer: (sendData: [Send, EncArrayBuffer]) => Promise<any>;
upsert: (send: SendData | SendData[]) => Promise<any>;
replace: (sends: { [id: string]: SendData }) => Promise<any>;
clear: (userId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;
deleteWithServer: (id: string) => Promise<any>;
removePasswordWithServer: (id: string) => Promise<any>;
}

View File

@@ -1,6 +0,0 @@
export abstract class SettingsService {
clearCache: () => Promise<void>;
getEquivalentDomains: () => Promise<any>;
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -1,19 +0,0 @@
import {
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
} from "../models/response/notificationResponse";
export abstract class SyncService {
syncInProgress: boolean;
getLastSync: () => Promise<Date>;
setLastSync: (date: Date, userId?: string) => Promise<any>;
fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>;
syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise<boolean>;
syncDeleteFolder: (notification: SyncFolderNotification) => Promise<boolean>;
syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise<boolean>;
syncDeleteCipher: (notification: SyncFolderNotification) => Promise<boolean>;
syncUpsertSend: (notification: SyncSendNotification, isEdit: boolean) => Promise<boolean>;
syncDeleteSend: (notification: SyncSendNotification) => Promise<boolean>;
}

View File

@@ -1,6 +0,0 @@
export abstract class SystemService {
startProcessReload: () => Promise<void>;
cancelProcessReload: () => void;
clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise<void>;
clearPendingClipboard: () => Promise<any>;
}

View File

@@ -1,5 +0,0 @@
export abstract class TotpService {
getCode: (key: string) => Promise<string>;
getTimeInterval: (key: string) => number;
isAutoCopyEnabled: () => Promise<boolean>;
}

View File

@@ -1,23 +0,0 @@
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
export interface TwoFactorProviderDetails {
type: TwoFactorProviderType;
name: string;
description: string;
priority: number;
sort: number;
premium: boolean;
}
export abstract class TwoFactorService {
init: () => void;
getSupportedProviders: (win: Window) => TwoFactorProviderDetails[];
getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
setSelectedProvider: (type: TwoFactorProviderType) => void;
clearSelectedProvider: () => void;
setProviders: (response: IdentityTwoFactorResponse) => void;
clearProviders: () => void;
getProviders: () => Map<TwoFactorProviderType, { [key: string]: string }>;
}

View File

@@ -1,12 +0,0 @@
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
import { Verification } from "../types/verification";
export abstract class UserVerificationService {
buildRequest: <T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean,
) => Promise<T>;
verifyUser: (verification: Verification) => Promise<boolean>;
requestOTP: () => Promise<void>;
}

View File

@@ -1,8 +0,0 @@
export abstract class UsernameGenerationService {
generateUsername: (options: any) => Promise<string>;
generateWord: (options: any) => Promise<string>;
generateSubaddress: (options: any) => Promise<string>;
generateCatchall: (options: any) => Promise<string>;
getOptions: () => Promise<any>;
saveOptions: (options: any) => Promise<void>;
}

View File

@@ -1,11 +0,0 @@
export abstract class VaultTimeoutService {
isLocked: (userId?: string) => Promise<boolean>;
checkVaultTimeout: () => Promise<void>;
lock: (allowSoftLock?: boolean, userId?: string) => Promise<void>;
logOut: (userId?: string) => Promise<void>;
setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise<void>;
getVaultTimeout: () => Promise<number>;
isPinLockSet: () => Promise<[boolean, boolean]>;
isBiometricLockSet: () => Promise<boolean>;
clear: (userId?: string) => Promise<any>;
}

View File

@@ -1,5 +0,0 @@
export enum AuthenticationType {
Password = 0,
Sso = 1,
Api = 2,
}

View File

@@ -1,70 +0,0 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { EnvironmentService } from "../../abstractions/environment.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 { TwoFactorService } from "../../abstractions/twoFactor.service";
import { ApiLogInCredentials } from "../../models/domain/logInCredentials";
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
import { LogInStrategy } from "./logIn.strategy";
export class ApiLogInStrategy extends LogInStrategy {
tokenRequest: ApiTokenRequest;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private environmentService: EnvironmentService,
private keyConnectorService: KeyConnectorService,
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
);
}
async onSuccessfulLogin(tokenResponse: IdentityTokenResponse) {
if (tokenResponse.apiUseKeyConnector) {
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
}
}
async logIn(credentials: ApiLogInCredentials) {
this.tokenRequest = new ApiTokenRequest(
credentials.clientId,
credentials.clientSecret,
await this.buildTwoFactor(),
await this.buildDeviceRequest(),
);
return this.startLogIn();
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
await super.saveAccountInformation(tokenResponse);
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);
await this.stateService.setApiKeyClientSecret(this.tokenRequest.clientSecret);
}
}

View File

@@ -1,170 +0,0 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { CryptoService } from "../../abstractions/crypto.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 { TwoFactorService } from "../../abstractions/twoFactor.service";
import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
import { Account, AccountProfile, AccountTokens } from "../../models/domain/account";
import { AuthResult } from "../../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
} from "../../models/domain/logInCredentials";
import { DeviceRequest } from "../../models/request/deviceRequest";
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
import { SsoTokenRequest } from "../../models/request/identityToken/ssoTokenRequest";
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
import { KeysRequest } from "../../models/request/keysRequest";
import { IdentityCaptchaResponse } from "../../models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
import { IdentityTwoFactorResponse } from "../../models/response/identityTwoFactorResponse";
export abstract class LogInStrategy {
protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
protected captchaBypassToken: string = null;
constructor(
protected cryptoService: CryptoService,
protected apiService: ApiService,
protected tokenService: TokenService,
protected appIdService: AppIdService,
protected platformUtilsService: PlatformUtilsService,
protected messagingService: MessagingService,
protected logService: LogService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
) {}
abstract logIn(
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
): Promise<AuthResult>;
async logInTwoFactor(
twoFactor: TokenRequestTwoFactor,
captchaResponse: string = null,
): Promise<AuthResult> {
this.tokenRequest.setTwoFactor(twoFactor);
return this.startLogIn();
}
protected async startLogIn(): Promise<AuthResult> {
this.twoFactorService.clearSelectedProvider();
const response = await this.apiService.postIdentityToken(this.tokenRequest);
if (response instanceof IdentityTwoFactorResponse) {
return this.processTwoFactorResponse(response);
} else if (response instanceof IdentityCaptchaResponse) {
return this.processCaptchaResponse(response);
} else if (response instanceof IdentityTokenResponse) {
return this.processTokenResponse(response);
}
throw new Error("Invalid response object.");
}
protected onSuccessfulLogin(response: IdentityTokenResponse): Promise<void> {
// Implemented in subclass if required
return null;
}
protected async buildDeviceRequest() {
const appId = await this.appIdService.getAppId();
return new DeviceRequest(appId, this.platformUtilsService);
}
protected async buildTwoFactor(userProvidedTwoFactor?: TokenRequestTwoFactor) {
if (userProvidedTwoFactor != null) {
return userProvidedTwoFactor;
}
const storedTwoFactorToken = await this.tokenService.getTwoFactorToken();
if (storedTwoFactorToken != null) {
return new TokenRequestTwoFactor(TwoFactorProviderType.Remember, storedTwoFactorToken, false);
}
return new TokenRequestTwoFactor();
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
await this.stateService.addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: accountInformation.sub,
email: accountInformation.email,
hasPremiumPersonally: accountInformation.premium,
kdfIterations: tokenResponse.kdfIterations,
kdfType: tokenResponse.kdf,
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
},
},
}),
);
}
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
const result = new AuthResult();
result.resetMasterPassword = response.resetMasterPassword;
result.forcePasswordReset = response.forcePasswordReset;
await this.saveAccountInformation(response);
if (response.twoFactorToken != null) {
await this.tokenService.setTwoFactorToken(response);
}
const newSsoUser = response.key == null;
if (!newSsoUser) {
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
);
}
await this.onSuccessfulLogin(response);
await this.stateService.setBiometricLocked(false);
this.messagingService.send("loggedIn");
return result;
}
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
const result = new AuthResult();
result.twoFactorProviders = response.twoFactorProviders2;
this.twoFactorService.setProviders(response);
this.captchaBypassToken = response.captchaToken ?? null;
return result;
}
private async processCaptchaResponse(response: IdentityCaptchaResponse): Promise<AuthResult> {
const result = new AuthResult();
result.captchaSiteKey = response.siteKey;
return result;
}
private async createKeyPairForOldAccount() {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
return privateKey.encryptedString;
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,95 +0,0 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { AuthService } from "../../abstractions/auth.service";
import { CryptoService } from "../../abstractions/crypto.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 { TwoFactorService } from "../../abstractions/twoFactor.service";
import { HashPurpose } from "../../enums/hashPurpose";
import { AuthResult } from "../../models/domain/authResult";
import { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
import { LogInStrategy } from "./logIn.strategy";
export class PasswordLogInStrategy extends LogInStrategy {
get email() {
return this.tokenRequest.email;
}
get masterPasswordHash() {
return this.tokenRequest.masterPasswordHash;
}
tokenRequest: PasswordTokenRequest;
private localHashedPassword: string;
private key: SymmetricCryptoKey;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private authService: AuthService,
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
);
}
async onSuccessfulLogin() {
await this.cryptoService.setKey(this.key);
await this.cryptoService.setKeyHash(this.localHashedPassword);
}
async logInTwoFactor(
twoFactor: TokenRequestTwoFactor,
captchaResponse: string,
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
}
async logIn(credentials: PasswordLogInCredentials) {
const { email, masterPassword, captchaToken, twoFactor } = credentials;
this.key = await this.authService.makePreloginKey(masterPassword, email);
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
this.localHashedPassword = await this.cryptoService.hashPassword(
masterPassword,
this.key,
HashPurpose.LocalAuthorization,
);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key);
this.tokenRequest = new PasswordTokenRequest(
email,
hashedPassword,
captchaToken,
await this.buildTwoFactor(twoFactor),
await this.buildDeviceRequest(),
);
return this.startLogIn();
}
}

View File

@@ -1,70 +0,0 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { CryptoService } from "../../abstractions/crypto.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 { TwoFactorService } from "../../abstractions/twoFactor.service";
import { SsoLogInCredentials } from "../../models/domain/logInCredentials";
import { SsoTokenRequest } from "../../models/request/identityToken/ssoTokenRequest";
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
import { LogInStrategy } from "./logIn.strategy";
export class SsoLogInStrategy extends LogInStrategy {
tokenRequest: SsoTokenRequest;
orgId: string;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private keyConnectorService: KeyConnectorService,
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
);
}
async onSuccessfulLogin(tokenResponse: IdentityTokenResponse) {
const newSsoUser = tokenResponse.key == null;
if (tokenResponse.keyConnectorUrl != null) {
if (!newSsoUser) {
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
} else {
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
}
}
}
async logIn(credentials: SsoLogInCredentials) {
this.orgId = credentials.orgId;
this.tokenRequest = new SsoTokenRequest(
credentials.code,
credentials.codeVerifier,
credentials.redirectUrl,
await this.buildTwoFactor(credentials.twoFactor),
await this.buildDeviceRequest(),
);
return this.startLogIn();
}
}

View File

@@ -3,7 +3,6 @@ import { I18nService } from "../abstractions/i18n.service";
import * as tldjs from "tldjs";
const nodeURL = typeof window === "undefined" ? require("url") : null;
export class Utils {

View File

@@ -1,17 +0,0 @@
import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
import { Utils } from "../../misc/utils";
export class AuthResult {
captchaSiteKey = "";
resetMasterPassword = false;
forcePasswordReset = false;
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
get requiresCaptcha() {
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
}
get requiresTwoFactor() {
return this.twoFactorProviders != null;
}
}

View File

@@ -1,34 +0,0 @@
import { AuthenticationType } from "../../enums/authenticationType";
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor";
export class PasswordLogInCredentials {
readonly type = AuthenticationType.Password;
constructor(
public email: string,
public masterPassword: string,
public captchaToken?: string,
public twoFactor?: TokenRequestTwoFactor,
) {}
}
export class SsoLogInCredentials {
readonly type = AuthenticationType.Sso;
constructor(
public code: string,
public codeVerifier: string,
public redirectUrl: string,
public orgId: string,
public twoFactor?: TokenRequestTwoFactor,
) {}
}
export class ApiLogInCredentials {
readonly type = AuthenticationType.Api;
constructor(
public clientId: string,
public clientSecret: string,
) {}
}

View File

@@ -1,44 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "../abstractions/audit.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { throttle } from "../misc/throttle";
import { Utils } from "../misc/utils";
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
import { ErrorResponse } from "../models/response/errorResponse";
const PwnedPasswordsApi = "https://api.pwnedpasswords.com/range/";
export class AuditService implements AuditServiceAbstraction {
constructor(
private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
) {}
@throttle(100, () => "passwordLeaked")
async passwordLeaked(password: string): Promise<number> {
const hashBytes = await this.cryptoFunctionService.hash(password, "sha1");
const hash = Utils.fromBufferToHex(hashBytes).toUpperCase();
const hashStart = hash.substr(0, 5);
const hashEnding = hash.substr(5);
const response = await this.apiService.nativeFetch(new Request(PwnedPasswordsApi + hashStart));
const leakedHashes = await response.text();
const match = leakedHashes.split(/\r?\n/).find((v) => {
return v.split(":")[0] === hashEnding;
});
return match != null ? parseInt(match.split(":")[1], 10) : 0;
}
async breachedAccounts(username: string): Promise<BreachAccountResponse[]> {
try {
return await this.apiService.getHibpBreach(username);
} catch (e) {
const error = e as ErrorResponse;
if (error.statusCode === 404) {
return [];
}
throw new Error();
}
}
}

View File

@@ -1,198 +0,0 @@
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 { 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 { TwoFactorService } from "../abstractions/twoFactor.service";
import { AuthenticationType } from "../enums/authenticationType";
import { KdfType } from "../enums/kdfType";
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
} from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
import { PreloginRequest } from "../models/request/preloginRequest";
import { ErrorResponse } from "../models/response/errorResponse";
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
export class AuthService implements AuthServiceAbstraction {
get email(): string {
return this.logInStrategy instanceof PasswordLogInStrategy ? this.logInStrategy.email : null;
}
get masterPasswordHash(): string {
return this.logInStrategy instanceof PasswordLogInStrategy
? this.logInStrategy.masterPasswordHash
: null;
}
private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
private sessionTimeout: any;
constructor(
protected cryptoService: CryptoService,
protected apiService: ApiService,
protected tokenService: TokenService,
protected appIdService: AppIdService,
protected platformUtilsService: PlatformUtilsService,
protected messagingService: MessagingService,
protected logService: LogService,
protected keyConnectorService: KeyConnectorService,
protected environmentService: EnvironmentService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
protected i18nService: I18nService,
) {}
async logIn(
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
): Promise<AuthResult> {
this.clearState();
let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
if (credentials.type === AuthenticationType.Password) {
strategy = new PasswordLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this,
);
} else if (credentials.type === AuthenticationType.Sso) {
strategy = new SsoLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService,
);
} else if (credentials.type === AuthenticationType.Api) {
strategy = new ApiLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.environmentService,
this.keyConnectorService,
);
}
const result = await strategy.logIn(credentials as any);
if (result?.requiresTwoFactor) {
this.saveState(strategy);
}
return result;
}
async logInTwoFactor(
twoFactor: TokenRequestTwoFactor,
captchaResponse: string,
): Promise<AuthResult> {
if (this.logInStrategy == null) {
throw new Error(this.i18nService.t("sessionTimeout"));
}
try {
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse);
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again
if (!result.requiresTwoFactor && !result.requiresCaptcha) {
this.clearState();
}
return result;
} catch (e) {
// API exceptions are okay, but if there are any unhandled client-side errors then clear state to be safe
if (!(e instanceof ErrorResponse)) {
this.clearState();
}
throw e;
}
}
logOut(callback: () => void) {
callback();
this.messagingService.send("loggedOut");
}
authingWithApiKey(): boolean {
return this.logInStrategy instanceof ApiLogInStrategy;
}
authingWithSso(): boolean {
return this.logInStrategy instanceof SsoLogInStrategy;
}
authingWithPassword(): boolean {
return this.logInStrategy instanceof PasswordLogInStrategy;
}
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
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);
}
private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
this.logInStrategy = strategy;
this.startSessionTimeout();
}
private clearState() {
this.logInStrategy = null;
this.clearSessionTimeout();
}
private startSessionTimeout() {
this.clearSessionTimeout();
this.sessionTimeout = setTimeout(() => this.clearState(), sessionTimeoutLength);
}
private clearSessionTimeout() {
if (this.sessionTimeout != null) {
clearTimeout(this.sessionTimeout);
}
}
}

View File

@@ -1,214 +0,0 @@
import { LogService } from "../abstractions/log.service";
import { Utils } from "../misc/utils";
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
const MAX_SINGLE_BLOB_UPLOAD_SIZE = 256 * 1024 * 1024; // 256 MiB
const MAX_BLOCKS_PER_BLOB = 50000;
export class AzureFileUploadService {
constructor(private logService: LogService) {}
async upload(url: string, data: EncArrayBuffer, renewalCallback: () => Promise<string>) {
if (data.buffer.byteLength <= MAX_SINGLE_BLOB_UPLOAD_SIZE) {
return await this.azureUploadBlob(url, data);
} else {
return await this.azureUploadBlocks(url, data, renewalCallback);
}
}
private async azureUploadBlob(url: string, data: EncArrayBuffer) {
const urlObject = Utils.getUrl(url);
const headers = new Headers({
"x-ms-date": new Date().toUTCString(),
"x-ms-version": urlObject.searchParams.get("sv"),
"Content-Length": data.buffer.byteLength.toString(),
"x-ms-blob-type": "BlockBlob",
});
const request = new Request(url, {
body: data.buffer,
cache: "no-store",
method: "PUT",
headers: headers,
});
const blobResponse = await fetch(request);
if (blobResponse.status !== 201) {
throw new Error(`Failed to create Azure blob: ${blobResponse.status}`);
}
}
private async azureUploadBlocks(
url: string,
data: EncArrayBuffer,
renewalCallback: () => Promise<string>,
) {
const baseUrl = Utils.getUrl(url);
const blockSize = this.getMaxBlockSize(baseUrl.searchParams.get("sv"));
let blockIndex = 0;
const numBlocks = Math.ceil(data.buffer.byteLength / blockSize);
const blocksStaged: string[] = [];
if (numBlocks > MAX_BLOCKS_PER_BLOB) {
throw new Error(
`Cannot upload file, exceeds maximum size of ${blockSize * MAX_BLOCKS_PER_BLOB}`,
);
}
// eslint-disable-next-line
try {
while (blockIndex < numBlocks) {
url = await this.renewUrlIfNecessary(url, renewalCallback);
const blockUrl = Utils.getUrl(url);
const blockId = this.encodedBlockId(blockIndex);
blockUrl.searchParams.append("comp", "block");
blockUrl.searchParams.append("blockid", blockId);
const start = blockIndex * blockSize;
const blockData = data.buffer.slice(start, start + blockSize);
const blockHeaders = new Headers({
"x-ms-date": new Date().toUTCString(),
"x-ms-version": blockUrl.searchParams.get("sv"),
"Content-Length": blockData.byteLength.toString(),
});
const blockRequest = new Request(blockUrl.toString(), {
body: blockData,
cache: "no-store",
method: "PUT",
headers: blockHeaders,
});
const blockResponse = await fetch(blockRequest);
if (blockResponse.status !== 201) {
const message = `Unsuccessful block PUT. Received status ${blockResponse.status}`;
this.logService.error(message + "\n" + (await blockResponse.json()));
throw new Error(message);
}
blocksStaged.push(blockId);
blockIndex++;
}
url = await this.renewUrlIfNecessary(url, renewalCallback);
const blockListUrl = Utils.getUrl(url);
const blockListXml = this.blockListXml(blocksStaged);
blockListUrl.searchParams.append("comp", "blocklist");
const headers = new Headers({
"x-ms-date": new Date().toUTCString(),
"x-ms-version": blockListUrl.searchParams.get("sv"),
"Content-Length": blockListXml.length.toString(),
});
const request = new Request(blockListUrl.toString(), {
body: blockListXml,
cache: "no-store",
method: "PUT",
headers: headers,
});
const response = await fetch(request);
if (response.status !== 201) {
const message = `Unsuccessful block list PUT. Received status ${response.status}`;
this.logService.error(message + "\n" + (await response.json()));
throw new Error(message);
}
} catch (e) {
throw e;
}
}
private async renewUrlIfNecessary(
url: string,
renewalCallback: () => Promise<string>,
): Promise<string> {
const urlObject = Utils.getUrl(url);
const expiry = new Date(urlObject.searchParams.get("se") ?? "");
if (isNaN(expiry.getTime())) {
expiry.setTime(Date.now() + 3600000);
}
if (expiry.getTime() < Date.now() + 1000) {
return await renewalCallback();
}
return url;
}
private encodedBlockId(blockIndex: number) {
// Encoded blockId max size is 64, so pre-encoding max size is 48
const utfBlockId = (
"000000000000000000000000000000000000000000000000" + blockIndex.toString()
).slice(-48);
return Utils.fromUtf8ToB64(utfBlockId);
}
private blockListXml(blockIdList: string[]) {
let xml = '<?xml version="1.0" encoding="utf-8"?><BlockList>';
blockIdList.forEach((blockId) => {
xml += `<Latest>${blockId}</Latest>`;
});
xml += "</BlockList>";
return xml;
}
private getMaxBlockSize(version: string) {
if (Version.compare(version, "2019-12-12") >= 0) {
return 4000 * 1024 * 1024; // 4000 MiB
} else if (Version.compare(version, "2016-05-31") >= 0) {
return 100 * 1024 * 1024; // 100 MiB
} else {
return 4 * 1024 * 1024; // 4 MiB
}
}
}
class Version {
/**
* Compares two Azure Versions against each other
* @param a Version to compare
* @param b Version to compare
* @returns a number less than zero if b is newer than a, 0 if equal,
* and greater than zero if a is newer than b
*/
static compare(a: Required<Version> | string, b: Required<Version> | string) {
if (typeof a === "string") {
a = new Version(a);
}
if (typeof b === "string") {
b = new Version(b);
}
return a.year !== b.year
? a.year - b.year
: a.month !== b.month
? a.month - b.month
: a.day !== b.day
? a.day - b.day
: 0;
}
year = 0;
month = 0;
day = 0;
constructor(version: string) {
try {
const parts = version.split("-").map((v) => Number.parseInt(v, 10));
this.year = parts[0];
this.month = parts[1];
this.day = parts[2];
} catch {
// Ignore error
}
}
/**
* Compares two Azure Versions against each other
* @param compareTo Version to compare against
* @returns a number less than zero if compareTo is newer, 0 if equal,
* and greater than zero if this is greater than compareTo
*/
compare(compareTo: Required<Version> | string) {
return Version.compare(this, compareTo);
}
}

View File

@@ -1,34 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { Utils } from "../misc/utils";
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
export class BitwardenFileUploadService {
constructor(private apiService: ApiService) {}
async upload(
encryptedFileName: string,
encryptedFileData: EncArrayBuffer,
apiCall: (fd: FormData) => Promise<any>,
) {
const fd = new FormData();
try {
const blob = new Blob([encryptedFileData.buffer], { type: "application/octet-stream" });
fd.append("data", blob, encryptedFileName);
} catch (e) {
if (Utils.isNode && !Utils.isBrowser) {
fd.append(
"data",
Buffer.from(encryptedFileData.buffer) as any,
{
filepath: encryptedFileName,
contentType: "application/octet-stream",
} as any,
);
} else {
throw e;
}
}
await apiCall(fd);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,157 +0,0 @@
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 { CollectionData } from "../models/data/collectionData";
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/treeNode";
import { CollectionView } from "../models/view/collectionView";
const NestingDelimiter = "/";
export class CollectionService implements CollectionServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private stateService: StateService,
) {}
async clearCache(userId?: string): Promise<void> {
await this.stateService.setDecryptedCollections(null, { userId: userId });
}
async encrypt(model: CollectionView): Promise<Collection> {
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<CollectionView[]> {
if (collections == null) {
return [];
}
const decCollections: CollectionView[] = [];
const promises: Promise<any>[] = [];
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<Collection> {
const collections = await this.stateService.getEncryptedCollections();
// eslint-disable-next-line
if (collections == null || !collections.hasOwnProperty(id)) {
return null;
}
return new Collection(collections[id]);
}
async getAll(): Promise<Collection[]> {
const collections = await this.stateService.getEncryptedCollections();
const response: Collection[] = [];
for (const id in collections) {
// eslint-disable-next-line
if (collections.hasOwnProperty(id)) {
response.push(new Collection(collections[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<CollectionView[]> {
let decryptedCollections = await this.stateService.getDecryptedCollections();
if (decryptedCollections != null) {
return decryptedCollections;
}
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error("No key.");
}
const collections = await this.getAll();
decryptedCollections = await this.decryptMany(collections);
await this.stateService.setDecryptedCollections(decryptedCollections);
return decryptedCollections;
}
async getAllNested(collections: CollectionView[] = null): Promise<TreeNode<CollectionView>[]> {
if (collections == null) {
collections = await this.getAllDecrypted();
}
const nodes: TreeNode<CollectionView>[] = [];
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<TreeNode<CollectionView>> {
const collections = await this.getAllNested();
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionView>;
}
async upsert(collection: CollectionData | CollectionData[]): Promise<any> {
let collections = await this.stateService.getEncryptedCollections();
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.replace(collections);
}
async replace(collections: { [id: string]: CollectionData }): Promise<any> {
await this.clearCache();
await this.stateService.setEncryptedCollections(collections);
}
async clear(userId?: string): Promise<any> {
await this.clearCache(userId);
await this.stateService.setEncryptedCollections(null, { userId: userId });
}
async delete(id: string | string[]): Promise<any> {
const collections = await this.stateService.getEncryptedCollections();
if (collections == null) {
return;
}
if (typeof id === "string") {
delete collections[id];
} else {
(id as string[]).forEach((i) => {
delete collections[i];
});
}
await this.replace(collections);
}
}

View File

@@ -1,99 +0,0 @@
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";
import { EventType } from "../enums/eventType";
import { EventData } from "../models/data/eventData";
import { EventRequest } from "../models/request/eventRequest";
export class EventService implements EventServiceAbstraction {
private inited = false;
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
}
}
async collect(
eventType: EventType,
cipherId: string = null,
uploadImmediately = false,
): Promise<any> {
const authed = await this.stateService.getIsAuthenticated();
if (!authed) {
return;
}
const organizations = await this.organizationService.getAll();
if (organizations == null) {
return;
}
const orgIds = new Set<string>(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 uploadEvents(userId?: string): Promise<any> {
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<any> {
await this.stateService.setEventCollection(null, { userId: userId });
}
}

View File

@@ -1,111 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { FileUploadService as FileUploadServiceAbstraction } from "../abstractions/fileUpload.service";
import { LogService } from "../abstractions/log.service";
import { FileUploadType } from "../enums/fileUploadType";
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { EncString } from "../models/domain/encString";
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
import { SendFileUploadDataResponse } from "../models/response/sendFileUploadDataResponse";
import { AzureFileUploadService } from "./azureFileUpload.service";
import { BitwardenFileUploadService } from "./bitwardenFileUpload.service";
export class FileUploadService implements FileUploadServiceAbstraction {
private azureFileUploadService: AzureFileUploadService;
private bitwardenFileUploadService: BitwardenFileUploadService;
constructor(
private logService: LogService,
private apiService: ApiService,
) {
this.azureFileUploadService = new AzureFileUploadService(logService);
this.bitwardenFileUploadService = new BitwardenFileUploadService(apiService);
}
async uploadSendFile(
uploadData: SendFileUploadDataResponse,
fileName: EncString,
encryptedFileData: EncArrayBuffer,
) {
try {
switch (uploadData.fileUploadType) {
case FileUploadType.Direct:
await this.bitwardenFileUploadService.upload(
fileName.encryptedString,
encryptedFileData,
(fd) =>
this.apiService.postSendFile(
uploadData.sendResponse.id,
uploadData.sendResponse.file.id,
fd,
),
);
break;
case FileUploadType.Azure: {
const renewalCallback = async () => {
const renewalResponse = await this.apiService.renewSendFileUploadUrl(
uploadData.sendResponse.id,
uploadData.sendResponse.file.id,
);
return renewalResponse.url;
};
await this.azureFileUploadService.upload(
uploadData.url,
encryptedFileData,
renewalCallback,
);
break;
}
default:
throw new Error("Unknown file upload type");
}
} catch (e) {
await this.apiService.deleteSend(uploadData.sendResponse.id);
throw e;
}
}
async uploadCipherAttachment(
admin: boolean,
uploadData: AttachmentUploadDataResponse,
encryptedFileName: EncString,
encryptedFileData: EncArrayBuffer,
) {
const response = admin ? uploadData.cipherMiniResponse : uploadData.cipherResponse;
try {
switch (uploadData.fileUploadType) {
case FileUploadType.Direct:
await this.bitwardenFileUploadService.upload(
encryptedFileName.encryptedString,
encryptedFileData,
(fd) => this.apiService.postAttachmentFile(response.id, uploadData.attachmentId, fd),
);
break;
case FileUploadType.Azure: {
const renewalCallback = async () => {
const renewalResponse = await this.apiService.renewAttachmentUploadUrl(
response.id,
uploadData.attachmentId,
);
return renewalResponse.url;
};
await this.azureFileUploadService.upload(
uploadData.url,
encryptedFileData,
renewalCallback,
);
break;
}
default:
throw new Error("Unknown file upload type.");
}
} catch (e) {
if (admin) {
await this.apiService.deleteCipherAttachmentAdmin(response.id, uploadData.attachmentId);
} else {
await this.apiService.deleteCipherAttachment(response.id, uploadData.attachmentId);
}
throw e;
}
}
}

View File

@@ -1,194 +0,0 @@
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";
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 { FolderRequest } from "../models/request/folderRequest";
import { FolderResponse } from "../models/response/folderResponse";
import { FolderView } from "../models/view/folderView";
const NestingDelimiter = "/";
export class FolderService implements FolderServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private apiService: ApiService,
private i18nService: I18nService,
private cipherService: CipherService,
private stateService: StateService,
) {}
async clearCache(userId?: string): Promise<void> {
await this.stateService.setDecryptedFolders(null, { userId: userId });
}
async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise<Folder> {
const folder = new Folder();
folder.id = model.id;
folder.name = await this.cryptoService.encrypt(model.name, key);
return folder;
}
async get(id: string): Promise<Folder> {
const folders = await this.stateService.getEncryptedFolders();
// eslint-disable-next-line
if (folders == null || !folders.hasOwnProperty(id)) {
return null;
}
return new Folder(folders[id]);
}
async getAll(): Promise<Folder[]> {
const folders = await this.stateService.getEncryptedFolders();
const response: Folder[] = [];
for (const id in folders) {
// eslint-disable-next-line
if (folders.hasOwnProperty(id)) {
response.push(new Folder(folders[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<FolderView[]> {
const decryptedFolders = await this.stateService.getDecryptedFolders();
if (decryptedFolders != null) {
return decryptedFolders;
}
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error("No key.");
}
const decFolders: FolderView[] = [];
const promises: Promise<any>[] = [];
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<TreeNode<FolderView>[]> {
const folders = await this.getAllDecrypted();
const nodes: TreeNode<FolderView>[] = [];
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<TreeNode<FolderView>> {
const folders = await this.getAllNested();
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
}
async saveWithServer(folder: Folder): Promise<any> {
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<any> {
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<any> {
await this.stateService.setDecryptedFolders(null);
await this.stateService.setEncryptedFolders(folders);
}
async clear(userId?: string): Promise<any> {
await this.stateService.setDecryptedFolders(null, { userId: userId });
await this.stateService.setEncryptedFolders(null, { userId: userId });
}
async delete(id: string | string[]): Promise<any> {
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]);
}
}
if (updates.length > 0) {
this.cipherService.upsert(updates);
}
}
}
async deleteWithServer(id: string): Promise<any> {
await this.apiService.deleteFolder(id);
await this.delete(id);
}
}

View File

@@ -1,134 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.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 { Utils } from "../misc/utils";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
import { KeysRequest } from "../models/request/keysRequest";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
constructor(
private stateService: StateService,
private cryptoService: CryptoService,
private apiService: ApiService,
private tokenService: TokenService,
private logService: LogService,
private organizationService: OrganizationService,
private cryptoFunctionService: CryptoFunctionService,
) {}
setUsesKeyConnector(usesKeyConnector: boolean) {
return this.stateService.setUsesKeyConnector(usesKeyConnector);
}
async getUsesKeyConnector(): Promise<boolean> {
return await this.stateService.getUsesKeyConnector();
}
async userNeedsMigration() {
const loggedInUsingSso = await 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();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
try {
await this.apiService.postUserKeyToKeyConnector(
organization.keyConnectorUrl,
keyConnectorRequest,
);
} catch (e) {
throw new Error("Unable to reach key connector");
}
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.organizationService.getAll();
return orgs.find(
(o) =>
o.keyConnectorEnabled &&
o.type !== OrganizationUserType.Admin &&
o.type !== OrganizationUserType.Owner &&
!o.isProviderUser,
);
}
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
const { kdf, kdfIterations, keyConnectorUrl } = tokenResponse;
const password = await this.cryptoFunctionService.randomBytes(64);
const k = await this.cryptoService.makeKey(
Utils.fromBufferToB64(password),
await this.tokenService.getEmail(),
kdf,
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(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,
kdf,
kdfIterations,
orgId,
keys,
);
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
}
async setConvertAccountRequired(status: boolean) {
await this.stateService.setConvertAccountToKeyConnector(status);
}
async getConvertAccountRequired(): Promise<boolean> {
return await this.stateService.getConvertAccountToKeyConnector();
}
async removeConvertAccountRequired() {
await this.stateService.setConvertAccountToKeyConnector(null);
}
async clear() {
await this.removeConvertAccountRequired();
}
}

View File

@@ -1,231 +0,0 @@
import * as signalR from "@microsoft/signalr";
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
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 { NotificationType } from "../enums/notificationType";
import {
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;
constructor(
private syncService: SyncService,
private appIdService: AppIdService,
private apiService: ApiService,
private vaultTimeoutService: VaultTimeoutService,
private environmentService: EnvironmentService,
private logoutCallback: () => Promise<void>,
private logService: LogService,
private stateService: StateService,
) {
this.environmentService.urls.subscribe(() => {
if (!this.inited) {
return;
}
this.init();
});
}
async init(): Promise<void> {
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)),
);
// eslint-disable-next-line
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<void> {
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<void> {
this.inactive = false;
if (this.inited && !this.connected) {
await this.reconnect(true);
}
}
async disconnectFromInactivity(): Promise<void> {
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;
}
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;
}
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);
break;
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;
}
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));
}
}
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;
}
}

View File

@@ -1,55 +0,0 @@
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<Organization> {
const organizations = await this.stateService.getOrganizations();
// eslint-disable-next-line
if (organizations == null || !organizations.hasOwnProperty(id)) {
return null;
}
return new Organization(organizations[id]);
}
async getByIdentifier(identifier: string): Promise<Organization> {
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<Organization[]> {
const organizations = await this.stateService.getOrganizations({ userId: userId });
const response: Organization[] = [];
for (const id in organizations) {
// eslint-disable-next-line
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<boolean> {
const orgs = await this.getAll();
return orgs.some(
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null,
);
}
async hasOrganizations(userId?: string): Promise<boolean> {
const organizations = await this.getAll(userId);
return organizations.length > 0;
}
}

View File

@@ -1,572 +0,0 @@
import * as zxcvbn from "zxcvbn";
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 { PolicyType } from "../enums/policyType";
import { EEFLongWordList } from "../misc/wordlist";
import { EncString } from "../models/domain/encString";
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
import { Policy } from "../models/domain/policy";
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 MaxPasswordsInHistory = 100;
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private policyService: PolicyService,
private stateService: StateService,
) {}
async generatePassword(options: any): Promise<string> {
// 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;
}
async generatePassphrase(options: any): Promise<string> {
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;
}
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);
}
async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> {
let options = await this.stateService.getPasswordGenerationOptions();
if (options == null) {
options = Object.assign({}, 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]];
}
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 (enforcedPolicyOptions.useUppercase) {
options.uppercase = true;
}
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];
}
async getPasswordGeneratorPolicyOptions(): Promise<PasswordGeneratorPolicyOptions> {
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;
}
async saveOptions(options: any) {
await this.stateService.setPasswordGenerationOptions(options);
}
async getHistory(): Promise<GeneratedPasswordHistory[]> {
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
return new Array<GeneratedPasswordHistory>();
}
if ((await this.stateService.getDecryptedPasswordGenerationHistory()) == null) {
const encrypted = await this.stateService.getEncryptedPasswordGenerationHistory();
const decrypted = await this.decryptHistory(encrypted);
await this.stateService.setDecryptedPasswordGenerationHistory(decrypted);
}
const passwordGenerationHistory =
await this.stateService.getDecryptedPasswordGenerationHistory();
return passwordGenerationHistory != null
? passwordGenerationHistory
: new Array<GeneratedPasswordHistory>();
}
async addHistory(password: string): Promise<any> {
// Cannot add new history if no key is available
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
return;
}
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.stateService.setEncryptedPasswordGenerationHistory(newHistory);
}
async clear(userId?: string): Promise<any> {
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;
}
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<GeneratedPasswordHistory[]> {
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<GeneratedPasswordHistory[]> {
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;
}
}
}

View File

@@ -1,247 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { OrganizationService } from "../abstractions/organization.service";
import { PolicyService as PolicyServiceAbstraction } from "../abstractions/policy.service";
import { StateService } from "../abstractions/state.service";
import { OrganizationUserStatusType } from "../enums/organizationUserStatusType";
import { OrganizationUserType } from "../enums/organizationUserType";
import { PolicyType } from "../enums/policyType";
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 { ListResponse } from "../models/response/listResponse";
import { PolicyResponse } from "../models/response/policyResponse";
export class PolicyService implements PolicyServiceAbstraction {
policyCache: Policy[];
constructor(
private stateService: StateService,
private organizationService: OrganizationService,
private apiService: ApiService,
) {}
async clearCache(): Promise<void> {
await this.stateService.setDecryptedPolicies(null);
}
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
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) {
// eslint-disable-next-line
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<Policy> {
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));
}
const policies = await this.getAll(policyType);
return policies.find((p) => p.organizationId === organizationId);
}
async replace(policies: { [id: string]: PolicyData }): Promise<any> {
await this.stateService.setDecryptedPolicies(null);
await this.stateService.setEncryptedPolicies(policies);
}
async clear(userId?: string): Promise<any> {
await this.stateService.setDecryptedPolicies(null, { userId: userId });
await this.stateService.setEncryptedPolicies(null, { userId: userId });
}
async getMasterPasswordPoliciesForInvitedUsers(
orgId: string,
): Promise<MasterPasswordPolicyOptions> {
const userId = await this.stateService.getUserId();
const response = await this.apiService.getPoliciesByInvitedUser(orgId, userId);
const policies = await this.mapPoliciesFromToken(response);
return this.getMasterPasswordPolicyOptions(policies);
}
async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise<MasterPasswordPolicyOptions> {
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;
}
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;
}
// eslint-disable-next-line
if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) {
return false;
}
return true;
}
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];
}
mapPoliciesFromToken(policiesResponse: ListResponse<PolicyResponse>): 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;
}
}

View File

@@ -1,34 +0,0 @@
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<Provider> {
const providers = await this.stateService.getProviders();
// eslint-disable-next-line
if (providers == null || !providers.hasOwnProperty(id)) {
return null;
}
return new Provider(providers[id]);
}
async getAll(): Promise<Provider[]> {
const providers = await this.stateService.getProviders();
const response: Provider[] = [];
for (const id in providers) {
// eslint-disable-next-line
if (providers.hasOwnProperty(id)) {
response.push(new Provider(providers[id]));
}
}
return response;
}
async save(providers: { [id: string]: ProviderData }) {
await this.stateService.setProviders(providers);
}
}

View File

@@ -1,284 +0,0 @@
import * as lunr from "lunr";
import { CipherService } from "../abstractions/cipher.service";
import { I18nService } from "../abstractions/i18n.service";
import { LogService } from "../abstractions/log.service";
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
import { CipherType } from "../enums/cipherType";
import { FieldType } from "../enums/fieldType";
import { UriMatchType } from "../enums/uriMatchType";
import { CipherView } from "../models/view/cipherView";
import { SendView } from "../models/view/sendView";
export class SearchService implements SearchServiceAbstraction {
indexedEntityId?: string = null;
private indexing = false;
private index: lunr.Index = null;
private searchableMinLength = 2;
constructor(
private cipherService: CipherService,
private logService: LogService,
private i18nService: I18nService,
) {
if (["zh-CN", "zh-TW"].indexOf(i18nService.locale) !== -1) {
this.searchableMinLength = 1;
}
}
clearIndex(): void {
this.indexedEntityId = null;
this.index = null;
}
isSearchable(query: string): boolean {
const notSearchable =
query == null ||
(this.index == null && query.length < this.searchableMinLength) ||
(this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
return !notSearchable;
}
async indexCiphers(indexedEntityId?: string, ciphers?: CipherView[]): Promise<void> {
if (this.indexing) {
return;
}
this.logService.time("search indexing");
this.indexing = true;
this.indexedEntityId = indexedEntityId;
this.index = null;
const builder = new lunr.Builder();
builder.ref("id");
builder.field("shortid", { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) });
builder.field("name", { boost: 10 });
builder.field("subtitle", {
boost: 5,
extractor: (c: CipherView) => {
if (c.subTitle != null && c.type === CipherType.Card) {
return c.subTitle.replace(/\*/g, "");
}
return c.subTitle;
},
});
builder.field("notes");
builder.field("login.username", {
extractor: (c: CipherView) =>
c.type === CipherType.Login && c.login != null ? c.login.username : null,
});
builder.field("login.uris", { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) });
builder.field("fields", { extractor: (c: CipherView) => this.fieldExtractor(c, false) });
builder.field("fields_joined", { extractor: (c: CipherView) => this.fieldExtractor(c, true) });
builder.field("attachments", {
extractor: (c: CipherView) => this.attachmentExtractor(c, false),
});
builder.field("attachments_joined", {
extractor: (c: CipherView) => this.attachmentExtractor(c, true),
});
builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId });
ciphers = ciphers || (await this.cipherService.getAllDecrypted());
ciphers.forEach((c) => builder.add(c));
this.index = builder.build();
this.indexing = false;
this.logService.timeEnd("search indexing");
}
async searchCiphers(
query: string,
filter: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[] = null,
ciphers: CipherView[] = null,
): Promise<CipherView[]> {
const results: CipherView[] = [];
if (query != null) {
query = query.trim().toLowerCase();
}
if (query === "") {
query = null;
}
if (ciphers == null) {
ciphers = await this.cipherService.getAllDecrypted();
}
if (filter != null && Array.isArray(filter) && filter.length > 0) {
ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c)));
} else if (filter != null) {
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
}
if (!this.isSearchable(query)) {
return ciphers;
}
if (this.indexing) {
await new Promise((r) => setTimeout(r, 250));
if (this.indexing) {
await new Promise((r) => setTimeout(r, 500));
}
}
const index = this.getIndexForSearch();
if (index == null) {
// Fall back to basic search if index is not available
return this.searchCiphersBasic(ciphers, query);
}
const ciphersMap = new Map<string, CipherView>();
ciphers.forEach((c) => ciphersMap.set(c.id, c));
let searchResults: lunr.Index.Result[] = null;
const isQueryString = query != null && query.length > 1 && query.indexOf(">") === 0;
if (isQueryString) {
try {
searchResults = index.search(query.substr(1).trim());
} catch (e) {
this.logService.error(e);
}
} else {
const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING;
searchResults = index.query((q) => {
lunr.tokenizer(query).forEach((token) => {
const t = token.toString();
q.term(t, { fields: ["name"], wildcard: soWild });
q.term(t, { fields: ["subtitle"], wildcard: soWild });
q.term(t, { fields: ["login.uris"], wildcard: soWild });
q.term(t, {});
});
});
}
if (searchResults != null) {
searchResults.forEach((r) => {
if (ciphersMap.has(r.ref)) {
results.push(ciphersMap.get(r.ref));
}
});
}
return results;
}
searchCiphersBasic(ciphers: CipherView[], query: string, deleted = false) {
query = query.trim().toLowerCase();
return ciphers.filter((c) => {
if (deleted !== c.isDeleted) {
return false;
}
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (query.length >= 8 && c.id.startsWith(query)) {
return true;
}
if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) {
return true;
}
if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) {
return true;
}
return false;
});
}
searchSends(sends: SendView[], query: string) {
query = query.trim().toLocaleLowerCase();
return sends.filter((s) => {
if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (
query.length >= 8 &&
(s.id.startsWith(query) ||
s.accessId.toLocaleLowerCase().startsWith(query) ||
(s.file?.id != null && s.file.id.startsWith(query)))
) {
return true;
}
if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) {
return true;
}
if (s.text?.text != null && s.text.text.toLowerCase().indexOf(query) > -1) {
return true;
}
if (s.file?.fileName != null && s.file.fileName.toLowerCase().indexOf(query) > -1) {
return true;
}
});
}
getIndexForSearch(): lunr.Index {
return this.index;
}
private fieldExtractor(c: CipherView, joined: boolean) {
if (!c.hasFields) {
return null;
}
let fields: string[] = [];
c.fields.forEach((f) => {
if (f.name != null) {
fields.push(f.name);
}
if (f.type === FieldType.Text && f.value != null) {
fields.push(f.value);
}
});
fields = fields.filter((f) => f.trim() !== "");
if (fields.length === 0) {
return null;
}
return joined ? fields.join(" ") : fields;
}
private attachmentExtractor(c: CipherView, joined: boolean) {
if (!c.hasAttachments) {
return null;
}
let attachments: string[] = [];
c.attachments.forEach((a) => {
if (a != null && a.fileName != null) {
if (joined && a.fileName.indexOf(".") > -1) {
attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf(".")));
} else {
attachments.push(a.fileName);
}
}
});
attachments = attachments.filter((f) => f.trim() !== "");
if (attachments.length === 0) {
return null;
}
return joined ? attachments.join(" ") : attachments;
}
private uriExtractor(c: CipherView) {
if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) {
return null;
}
const uris: string[] = [];
c.login.uris.forEach((u) => {
if (u.uri == null || u.uri === "") {
return;
}
if (u.hostname != null) {
uris.push(u.hostname);
return;
}
let uri = u.uri;
if (u.match !== UriMatchType.RegularExpression) {
const protocolIndex = uri.indexOf("://");
if (protocolIndex > -1) {
uri = uri.substr(protocolIndex + 3);
}
const queryIndex = uri.search(/\?|&|#/);
if (queryIndex > -1) {
uri = uri.substring(0, queryIndex);
}
}
uris.push(uri);
});
return uris.length > 0 ? uris : null;
}
}

View File

@@ -1,297 +0,0 @@
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 { SEND_KDF_ITERATIONS } from "../enums/kdfType";
import { SendType } from "../enums/sendType";
import { Utils } from "../misc/utils";
import { SendData } from "../models/data/sendData";
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 { SendRequest } from "../models/request/sendRequest";
import { ErrorResponse } from "../models/response/errorResponse";
import { SendResponse } from "../models/response/sendResponse";
import { SendView } from "../models/view/sendView";
export class SendService implements SendServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private apiService: ApiService,
private fileUploadService: FileUploadService,
private i18nService: I18nService,
private cryptoFunctionService: CryptoFunctionService,
private stateService: StateService,
) {}
async clearCache(): Promise<void> {
await this.stateService.setDecryptedSends(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);
}
if (password != null) {
const passwordHash = await this.cryptoFunctionService.pbkdf2(
password,
model.key,
"sha256",
SEND_KDF_ITERATIONS,
);
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];
}
async get(id: string): Promise<Send> {
const sends = await this.stateService.getEncryptedSends();
// eslint-disable-next-line
if (sends == null || !sends.hasOwnProperty(id)) {
return null;
}
return new Send(sends[id]);
}
async getAll(): Promise<Send[]> {
const sends = await this.stateService.getEncryptedSends();
const response: Send[] = [];
for (const id in sends) {
// eslint-disable-next-line
if (sends.hasOwnProperty(id)) {
response.push(new Send(sends[id]));
}
}
return response;
}
async getAllDecrypted(): Promise<SendView[]> {
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<any>[] = [];
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<any> {
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;
} else {
response = await this.apiService.putSend(sendData[0].id, request);
}
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<SendResponse> {
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<any> {
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<any> {
await this.stateService.setDecryptedSends(null);
await this.stateService.setEncryptedSends(sends);
}
async clear(): Promise<any> {
await this.stateService.setDecryptedSends(null);
await this.stateService.setEncryptedSends(null);
}
async delete(id: string | string[]): Promise<any> {
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<any> {
await this.apiService.deleteSend(id);
await this.delete(id);
}
async removePasswordWithServer(id: string): Promise<any> {
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<EncArrayBuffer> {
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.");
};
});
}
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];
}
}

View File

@@ -1,56 +0,0 @@
import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service";
import { StateService } from "../abstractions/state.service";
const Keys = {
settingsPrefix: "settings_",
equivalentDomains: "equivalentDomains",
};
export class SettingsService implements SettingsServiceAbstraction {
constructor(private stateService: StateService) {}
async clearCache(): Promise<void> {
await this.stateService.setSettings(null);
}
getEquivalentDomains(): Promise<any> {
return this.getSettingsKey(Keys.equivalentDomains);
}
async setEquivalentDomains(equivalentDomains: string[][]): Promise<void> {
await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains);
}
async clear(userId?: string): Promise<void> {
await this.stateService.setSettings(null, { userId: userId });
}
// Helpers
private async getSettings(): Promise<any> {
const settings = await this.stateService.getSettings();
if (settings == null) {
// eslint-disable-next-line
const userId = await this.stateService.getUserId();
}
return settings;
}
private async getSettingsKey(key: string): Promise<any> {
const settings = await this.getSettings();
if (settings != null && settings[key]) {
return settings[key];
}
return null;
}
private async setSettingsKey(key: string, value: any): Promise<void> {
let settings = await this.getSettings();
if (!settings) {
settings = {};
}
settings[key] = value;
await this.stateService.setSettings(settings);
}
}

View File

@@ -2172,11 +2172,11 @@ export class StateService<
}
const account = options?.useSecureStorage
? (await this.secureStorageService.get<TAccount>(options.userId, options)) ??
? ((await this.secureStorageService.get<TAccount>(options.userId, options)) ??
(await this.storageService.get<TAccount>(
options.userId,
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }),
))
)))
: await this.storageService.get<TAccount>(options.userId, options);
if (this.useAccountCache) {

View File

@@ -1,400 +0,0 @@
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 { sequentialize } from "../misc/sequentialize";
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 {
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 = false;
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<void>,
) {}
async getLastSync(): Promise<Date> {
if ((await this.stateService.getUserId()) == null) {
return null;
}
const lastSync = await this.stateService.getLastSync();
if (lastSync) {
return new Date(lastSync);
}
return null;
}
async setLastSync(date: Date, userId?: string): Promise<any> {
await this.stateService.setLastSync(date.toJSON(), { userId: userId });
}
@sequentialize(() => "fullSync")
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
this.syncStarted();
const isAuthenticated = await this.stateService.getIsAuthenticated();
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;
}
}
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();
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.setLastSync(now);
return this.syncCompleted(true);
} catch (e) {
if (allowThrowOnError) {
throw e;
} else {
return this.syncCompleted(false);
}
}
}
async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise<boolean> {
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) {
this.logService.error(e);
}
}
return this.syncCompleted(false);
}
async syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean> {
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<boolean> {
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 {
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.stateService.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<boolean> {
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<boolean> {
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);
}
}
} catch (e) {
this.logService.error(e);
}
}
return this.syncCompleted(false);
}
async syncDeleteSend(notification: SyncSendNotification): Promise<boolean> {
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;
}
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.stateService.getSecurityStamp();
if (stamp != null && stamp !== response.securityStamp) {
if (this.logoutCallback != null) {
await this.logoutCallback(true);
}
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.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 this.organizationService.save(organizations);
await this.providerService.save(providers);
if (await this.keyConnectorService.userNeedsMigration()) {
await this.keyConnectorService.setConvertAccountRequired(true);
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);
}
});
}
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);
}
}

View File

@@ -1,90 +0,0 @@
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 { Utils } from "../misc/utils";
export class SystemService implements SystemServiceAbstraction {
private reloadInterval: any = null;
private clearClipboardTimeout: any = null;
private clearClipboardTimeoutFunction: () => Promise<any> = null;
constructor(
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private reloadCallback: () => Promise<void> = null,
private stateService: StateService,
) {}
async startProcessReload(): Promise<void> {
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;
}
}
async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise<void> {
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;
}
}
}

View File

@@ -1,174 +0,0 @@
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 { Utils } from "../misc/utils";
const B32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const SteamChars = "23456789BCDFGHJKMNPQRTVWXY";
export class TotpService implements TotpServiceAbstraction {
constructor(
private cryptoFunctionService: CryptoFunctionService,
private logService: LogService,
private stateService: StateService,
) {}
async getCode(key: string): Promise<string> {
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.");
}
}
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;
}
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;
}
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);
let otp = "";
if (isSteamAuth) {
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;
}
async isAutoCopyEnabled(): Promise<boolean> {
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;
}
private decToHex(d: number): string {
return (d < 15.5 ? "0" : "") + Math.round(d).toString(16);
}
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;
}
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);
}
}

View File

@@ -1,186 +0,0 @@
import { I18nService } from "../abstractions/i18n.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import {
TwoFactorProviderDetails,
TwoFactorService as TwoFactorServiceAbstraction,
} from "../abstractions/twoFactor.service";
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactorProviderDetails>> =
{
[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 TwoFactorService implements TwoFactorServiceAbstraction {
private twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
private selectedTwoFactorProviderType: TwoFactorProviderType = null;
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
) {}
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");
}
getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
const providers: any[] = [];
if (this.twoFactorProvidersData == null) {
return providers;
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) &&
this.platformUtilsService.supportsDuo()
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) &&
this.platformUtilsService.supportsDuo()
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) &&
this.platformUtilsService.supportsWebAuthn(win)
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
}
return providers;
}
getDefaultProvider(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;
}
setSelectedProvider(type: TwoFactorProviderType) {
this.selectedTwoFactorProviderType = type;
}
clearSelectedProvider() {
this.selectedTwoFactorProviderType = null;
}
setProviders(response: IdentityTwoFactorResponse) {
this.twoFactorProvidersData = response.twoFactorProviders2;
}
clearProviders() {
this.twoFactorProvidersData = null;
}
getProviders() {
return this.twoFactorProvidersData;
}
}

View File

@@ -1,88 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../abstractions/userVerification.service";
import { VerificationType } from "../enums/verificationType";
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
import { Verification } from "../types/verification";
/**
* Used for general-purpose user verification throughout the app.
* Use it to verify the input collected by UserVerificationComponent.
*/
export class UserVerificationService implements UserVerificationServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private apiService: ApiService,
) {}
/**
* Create a new request model to be used for server-side verification
* @param verification User-supplied verification data (Master Password or OTP)
* @param requestClass The request model to create
* @param alreadyHashed Whether the master password is already hashed
*/
async buildRequest<T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean,
) {
this.validateInput(verification);
const request =
requestClass != null ? new requestClass() : (new SecretVerificationRequest() as T);
if (verification.type === VerificationType.OTP) {
request.otp = verification.secret;
} else {
request.masterPasswordHash = alreadyHashed
? verification.secret
: await this.cryptoService.hashPassword(verification.secret, null);
}
return request;
}
/**
* Used to verify the Master Password client-side, or send the OTP to the server for verification (with no other data)
* Generally used for client-side verification only.
* @param verification User-supplied verification data (Master Password or OTP)
*/
async verifyUser(verification: Verification): Promise<boolean> {
this.validateInput(verification);
if (verification.type === VerificationType.OTP) {
const request = new VerifyOTPRequest(verification.secret);
try {
await this.apiService.postAccountVerifyOTP(request);
} catch (e) {
throw new Error(this.i18nService.t("invalidVerificationCode"));
}
} else {
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
verification.secret,
null,
);
if (!passwordValid) {
throw new Error(this.i18nService.t("invalidMasterPassword"));
}
}
return true;
}
async requestOTP() {
await this.apiService.postAccountRequestOTP();
}
private validateInput(verification: Verification) {
if (verification?.secret == null || verification.secret === "") {
if (verification.type === VerificationType.OTP) {
throw new Error(this.i18nService.t("verificationCodeRequired"));
} else {
throw new Error(this.i18nService.t("masterPassRequired"));
}
}
}
}

View File

@@ -1,131 +0,0 @@
import { CryptoService } from "../abstractions/crypto.service";
import { StateService } from "../abstractions/state.service";
import { UsernameGenerationService as BaseUsernameGenerationService } from "../abstractions/usernameGeneration.service";
import { EEFLongWordList } from "../misc/wordlist";
const DefaultOptions = {
type: "word",
wordCapitalize: true,
wordIncludeNumber: true,
subaddressType: "random",
catchallType: "random",
};
export class UsernameGenerationService implements BaseUsernameGenerationService {
constructor(
private cryptoService: CryptoService,
private stateService: StateService,
) {}
generateUsername(options: any): Promise<string> {
if (options.type === "catchall") {
return this.generateCatchall(options);
} else if (options.type === "subaddress") {
return this.generateSubaddress(options);
} else if (options.type === "forwarded") {
return this.generateSubaddress(options);
} else {
return this.generateWord(options);
}
}
async generateWord(options: any): Promise<string> {
const o = Object.assign({}, DefaultOptions, options);
if (o.wordCapitalize == null) {
o.wordCapitalize = true;
}
if (o.wordIncludeNumber == null) {
o.wordIncludeNumber = true;
}
const wordIndex = await this.cryptoService.randomNumber(0, EEFLongWordList.length - 1);
let word = EEFLongWordList[wordIndex];
if (o.wordCapitalize) {
word = word.charAt(0).toUpperCase() + word.slice(1);
}
if (o.wordIncludeNumber) {
const num = await this.cryptoService.randomNumber(1, 9999);
word = word + this.zeroPad(num.toString(), 4);
}
return word;
}
async generateSubaddress(options: any): Promise<string> {
const o = Object.assign({}, DefaultOptions, options);
const subaddressEmail = o.subaddressEmail;
if (subaddressEmail == null || subaddressEmail.length < 3) {
return o.subaddressEmail;
}
const atIndex = subaddressEmail.indexOf("@");
if (atIndex < 1 || atIndex >= subaddressEmail.length - 1) {
return subaddressEmail;
}
if (o.subaddressType == null) {
o.subaddressType = "random";
}
const emailBeginning = subaddressEmail.substr(0, atIndex);
const emailEnding = subaddressEmail.substr(atIndex + 1, subaddressEmail.length);
let subaddressString = "";
if (o.subaddressType === "random") {
subaddressString = await this.randomString(8);
} else if (o.subaddressType === "website-name") {
subaddressString = o.website;
}
return emailBeginning + "+" + subaddressString + "@" + emailEnding;
}
async generateCatchall(options: any): Promise<string> {
const o = Object.assign({}, DefaultOptions, options);
if (o.catchallDomain == null || o.catchallDomain === "") {
return null;
}
if (o.catchallType == null) {
o.catchallType = "random";
}
let startString = "";
if (o.catchallType === "random") {
startString = await this.randomString(8);
} else if (o.catchallType === "website-name") {
startString = o.website;
}
return startString + "@" + o.catchallDomain;
}
async getOptions(): Promise<any> {
let options = await this.stateService.getUsernameGenerationOptions();
if (options == null) {
options = Object.assign({}, DefaultOptions);
} else {
options = Object.assign({}, DefaultOptions, options);
}
await this.stateService.setUsernameGenerationOptions(options);
return options;
}
async saveOptions(options: any) {
await this.stateService.setUsernameGenerationOptions(options);
}
private async randomString(length: number) {
let str = "";
const charSet = "abcdefghijklmnopqrstuvwxyz1234567890";
for (let i = 0; i < length; i++) {
const randomCharIndex = await this.cryptoService.randomNumber(0, charSet.length - 1);
str += charSet.charAt(randomCharIndex);
}
return str;
}
// ref: https://stackoverflow.com/a/10073788
private zeroPad(number: string, width: number) {
return number.length >= width
? number
: new Array(width - number.length + 1).join("0") + number;
}
}

View File

@@ -1,225 +0,0 @@
import { firstValueFrom } from "rxjs";
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 { PolicyType } from "../enums/policyType";
export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
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: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (userId?: string) => Promise<void> = null,
) {}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
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<boolean> {
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);
}
return !(await this.cryptoService.hasKeyInMemory(userId));
}
async checkVaultTimeout(): Promise<void> {
if (await this.platformUtilsService.isViewOpen()) {
return;
}
const accounts = await firstValueFrom(this.stateService.accounts$);
for (const userId in accounts) {
if (userId != null && (await this.shouldLock(userId))) {
await this.executeTimeoutAction(userId);
}
}
}
async lock(allowSoftLock = false, userId?: string): Promise<void> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
if (await this.keyConnectorService.getUsesKeyConnector()) {
const pinSet = await this.isPinLockSet();
const pinLock =
(pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) || pinSet[1];
if (!pinLock && !(await this.isBiometricLockSet())) {
await this.logOut(userId);
}
}
if (userId == null || userId === (await this.stateService.getUserId())) {
this.searchService.clearIndex();
}
await this.stateService.setEverBeenUnlocked(true, { userId: userId });
await this.stateService.setBiometricLocked(true, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { 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(userId);
}
}
async logOut(userId?: string): Promise<void> {
if (this.loggedOutCallback != null) {
await this.loggedOutCallback(userId);
}
}
async setVaultTimeoutOptions(timeout: number, action: string): Promise<void> {
await this.stateService.setVaultTimeout(timeout);
// We swap these tokens from being on disk for lock actions, and in memory for logout actions
// Get them here to set them to their new location after changing the timeout action and clearing if needed
const token = await this.tokenService.getToken();
const refreshToken = await this.tokenService.getRefreshToken();
const clientId = await this.tokenService.getClientId();
const clientSecret = await this.tokenService.getClientSecret();
const currentAction = await this.stateService.getVaultTimeoutAction();
if ((timeout != null || timeout === 0) && action === "logOut" && action !== currentAction) {
// if we have a vault timeout and the action is log out, reset tokens
await this.tokenService.clearToken();
}
await this.stateService.setVaultTimeoutAction(action);
await this.tokenService.setToken(token);
await this.tokenService.setRefreshToken(refreshToken);
await this.tokenService.setClientId(clientId);
await this.tokenService.setClientSecret(clientSecret);
await this.cryptoService.toggleKey();
}
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<boolean> {
return await this.stateService.getBiometricUnlock();
}
async getVaultTimeout(userId?: string): Promise<number> {
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;
}
return vaultTimeout;
}
async clear(userId?: string): Promise<void> {
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<boolean> {
return !(await this.stateService.getIsAuthenticated({ userId: userId }));
}
private async shouldLock(userId: string): Promise<boolean> {
if (await this.isLoggedOut(userId)) {
return false;
}
if (await this.isLocked(userId)) {
return false;
}
const vaultTimeout = await this.getVaultTimeout(userId);
if (vaultTimeout == null || vaultTimeout < 0) {
return false;
}
const lastActive = await this.stateService.getLastActive({ userId: userId });
if (lastActive == null) {
return false;
}
const vaultTimeoutSeconds = vaultTimeout * 60;
const diffSeconds = (new Date().getTime() - lastActive) / 1000;
return diffSeconds >= vaultTimeoutSeconds;
}
private async executeTimeoutAction(userId: string): Promise<void> {
const timeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId });
timeoutAction === "logOut" ? await this.logOut(userId) : await this.lock(true, userId);
}
}

View File

@@ -1,71 +0,0 @@
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { KeySuffixOptions } from "@/jslib/common/src/enums/keySuffixOptions";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
export class ElectronCryptoService extends CryptoService {
constructor(
cryptoFunctionService: CryptoFunctionService,
platformUtilService: PlatformUtilsService,
logService: LogService,
stateService: StateService,
) {
super(cryptoFunctionService, platformUtilService, logService, stateService);
}
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
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);
}
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, userId?: string) {
await this.upgradeSecurelyStoredKey();
return super.retrieveKeyFromStorage(keySuffix, userId);
}
/**
* @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;
}
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);
}
await this.stateService.setCryptoMasterKeyB64(null);
}
}

View File

@@ -15,7 +15,7 @@ export class ElectronLogService extends BaseLogService {
super(isDev(), filter);
}
init () {
init() {
if (log.transports == null) {
return;
}

View File

@@ -14,7 +14,7 @@ export class WindowMain {
win: BrowserWindow;
isQuitting = false;
private windowStateChangeTimer: NodeJS.Timer;
private windowStateChangeTimer: NodeJS.Timeout;
private windowStates: { [key: string]: any } = {};
private enableAlwaysOnTop = false;

View File

@@ -1,624 +0,0 @@
import * as http from "http";
import * as program from "commander";
import * as inquirer from "inquirer";
import Separator from "inquirer/lib/objects/separator";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { PasswordGenerationService } from "@/jslib/common/src/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { PolicyService } from "@/jslib/common/src/abstractions/policy.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
import { NodeUtils } from "@/jslib/common/src/misc/nodeUtils";
import { Utils } from "@/jslib/common/src/misc/utils";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
} from "@/jslib/common/src/models/domain/logInCredentials";
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
import { TwoFactorEmailRequest } from "@/jslib/common/src/models/request/twoFactorEmailRequest";
import { UpdateTempPasswordRequest } from "@/jslib/common/src/models/request/updateTempPasswordRequest";
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/messageResponse";
export class LoginCommand {
protected validatedParams: () => Promise<any>;
protected success: () => Promise<MessageResponse>;
protected logout: () => Promise<void>;
protected canInteract: boolean;
protected clientId: string;
protected clientSecret: string;
protected email: string;
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 stateService: StateService,
protected cryptoService: CryptoService,
protected policyService: PolicyService,
protected twoFactorService: TwoFactorService,
clientId: string,
) {
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;
let selectedProvider: any = 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.");
}
}
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.");
}
const twoFactor =
twoFactorToken == null
? null
: new TokenRequestTwoFactor(twoFactorMethod, twoFactorToken, false);
try {
if (this.validatedParams != null) {
await this.validatedParams();
}
let response: AuthResult = null;
if (clientId != null && clientSecret != null) {
response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
} else if (ssoCode != null && ssoCodeVerifier != null) {
response = await this.authService.logIn(
new SsoLogInCredentials(
ssoCode,
ssoCodeVerifier,
this.ssoRedirectUri,
orgIdentifier,
twoFactor,
),
);
} else {
response = await this.authService.logIn(
new PasswordLogInCredentials(email, password, null, twoFactor),
);
}
if (response.captchaSiteKey) {
const credentials = new PasswordLogInCredentials(email, password);
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
// Error Response
if (handledResponse instanceof Response) {
return handledResponse;
} else {
response = handledResponse;
}
}
if (response.requiresTwoFactor) {
const twoFactorProviders = this.twoFactorService.getSupportedProviders(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: (string | Separator)[] = 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(
new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken),
null,
);
}
if (response.captchaSiteKey) {
const twoFactorRequest = new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken);
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
// Error Response
if (handledResponse instanceof Response) {
return handledResponse;
} else {
response = handledResponse;
}
}
if (response.requiresTwoFactor) {
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.",
);
}
// 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<Response> {
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<Response> {
// 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.stateService.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.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 async handleCaptchaRequired(
twoFactorRequest: TokenRequestTwoFactor,
credentials: PasswordLogInCredentials = null,
): Promise<AuthResult | Response> {
const badCaptcha = Response.badRequest(
"Your authentication request has been flagged and will require user interaction to proceed.\n" +
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
"(https://bitwarden.com/help/cli-auth-challenges)",
);
try {
const captchaClientSecret = await this.apiClientSecret(true);
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
return badCaptcha;
}
let authResultResponse: AuthResult = null;
if (credentials != null) {
credentials.captchaToken = captchaClientSecret;
credentials.twoFactor = twoFactorRequest;
authResultResponse = await this.authService.logIn(credentials);
} else {
authResultResponse = await this.authService.logInTwoFactor(
twoFactorRequest,
captchaClientSecret,
);
}
return authResultResponse;
} catch (e) {
if (
e instanceof ErrorResponse ||
(e.constructor.name === ErrorResponse.name &&
(e as ErrorResponse).message.includes("Captcha is invalid"))
) {
return badCaptcha;
} else {
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 apiClientId(): Promise<string> {
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;
}
return clientId;
}
private async apiClientSecret(isAdditionalAuthentication = false): Promise<string> {
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;
}
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(
"<html><head><title>Success | Bitwarden CLI</title></head><body>" +
"<h1>Successfully authenticated with the Bitwarden CLI</h1>" +
"<p>You may now close this tab and return to the terminal.</p>" +
"</body></html>",
);
callbackServer.close(() =>
resolve({
ssoCode: code,
orgIdentifier: orgIdentifier,
}),
);
} else {
res.writeHead(400);
res.end(
"<html><head><title>Failed | Bitwarden CLI</title></head><body>" +
"<h1>Something went wrong logging into the Bitwarden CLI</h1>" +
"<p>You may now close this tab and return to the terminal.</p>" +
"</body></html>",
);
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];
}
}

View File

@@ -1,13 +1,8 @@
import * as child_process from "child_process";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { ClientType } from "@/jslib/common/src/enums/clientType";
import { DeviceType } from "@/jslib/common/src/enums/deviceType";
import { ThemeType } from "@/jslib/common/src/enums/themeType";
// eslint-disable-next-line
const open = require("open");
export class CliPlatformUtilsService implements PlatformUtilsService {
clientType: ClientType;
@@ -80,12 +75,8 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
return Promise.resolve(false);
}
launchUri(uri: string, options?: any): void {
if (process.platform === "linux") {
child_process.spawnSync("xdg-open", [uri]);
} else {
open(uri);
}
launchUri(_uri: string, _options?: any): void {
throw new Error("Not implemented.");
}
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {

2606
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2024.3.1",
"version": "2024.9.0",
"keywords": [
"bitwarden",
"password",
@@ -70,75 +70,76 @@
"test:types": "npx tsc --noEmit"
},
"devDependencies": {
"@angular-devkit/build-angular": "16.2.12",
"@angular-eslint/eslint-plugin-template": "17.2.0",
"@angular-eslint/template-parser": "17.2.0",
"@angular/compiler-cli": "16.2.12",
"@electron/notarize": "2.2.1",
"@electron/rebuild": "3.6.0",
"@fluffy-spoon/substitute": "1.208.0",
"@microsoft/microsoft-graph-types": "2.40.0",
"@ngtools/webpack": "16.2.12",
"@types/inquirer": "8.2.6",
"@types/inquirer": "8.2.10",
"@types/jest": "29.5.11",
"@types/ldapjs": "2.2.5",
"@types/lowdb": "1.0.15",
"@types/lunr": "2.3.7",
"@types/node": "18.17.12",
"@types/node-fetch": "2.6.10",
"@types/node": "18.19.50",
"@types/node-fetch": "2.6.11",
"@types/node-forge": "1.3.11",
"@types/proper-lockfile": "4.1.4",
"@types/tldjs": "2.3.4",
"@types/zxcvbn": "4.4.4",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"clean-webpack-plugin": "4.0.0",
"concurrently": "8.2.2",
"concurrently": "9.0.1",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "6.9.0",
"dotenv": "16.4.1",
"electron": "28.2.0",
"css-loader": "6.11.0",
"dotenv": "16.4.5",
"electron": "28.3.3",
"electron-builder": "24.9.1",
"electron-log": "5.0.1",
"@electron/notarize": "2.2.1",
"@electron/rebuild": "3.6.0",
"electron-log": "5.2.0",
"electron-reload": "2.0.0-alpha.1",
"electron-store": "8.1.0",
"electron-updater": "6.1.7",
"electron-store": "8.2.0",
"electron-updater": "6.3.0",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-import-resolver-typescript": "3.6.3",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-rxjs": "5.0.3",
"eslint-plugin-rxjs-angular": "2.0.1",
"form-data": "4.0.0",
"html-loader": "5.0.0",
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.0",
"husky": "9.0.10",
"jest": "29.7.0",
"jest-preset-angular": "13.1.1",
"lint-staged": "15.2.0",
"mini-css-extract-plugin": "2.7.7",
"jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jest-preset-angular": "13.1.6",
"lint-staged": "15.2.10",
"mini-css-extract-plugin": "2.9.1",
"node-forge": "1.3.1",
"node-loader": "2.0.0",
"pkg": "5.8.1",
"prettier": "3.2.2",
"rimraf": "5.0.5",
"prettier": "3.3.3",
"rimraf": "5.0.10",
"rxjs": "7.8.1",
"sass": "1.69.7",
"sass-loader": "14.0.0",
"ts-jest": "29.1.1",
"sass": "1.78.0",
"sass-loader": "16.0.1",
"ts-jest": "29.2.5",
"ts-loader": "9.5.1",
"tsconfig-paths-webpack-plugin": "4.1.0",
"type-fest": "3.13.1",
"typescript": "4.9.5",
"typescript-transform-paths": "3.4.6",
"webpack": "5.89.0",
"typescript-transform-paths": "3.5.1",
"webpack": "5.94.0",
"webpack-cli": "5.1.4",
"webpack-merge": "5.10.0",
"webpack-merge": "6.0.1",
"webpack-node-externals": "3.0.0",
"zone.js": "0.13.1"
},
"dependencies": {
"@angular/animations": "16.2.12",
"@angular/cdk": "16.2.12",
"@angular/cdk": "16.2.14",
"@angular/common": "16.2.12",
"@angular/compiler": "16.2.12",
"@angular/core": "16.2.12",
@@ -147,13 +148,11 @@
"@angular/platform-browser-dynamic": "16.2.12",
"@angular/router": "16.2.12",
"@microsoft/microsoft-graph-client": "3.0.7",
"@microsoft/signalr": "7.0.10",
"@microsoft/signalr-protocol-msgpack": "7.0.10",
"big-integer": "1.6.52",
"bootstrap": "4.6.2",
"bootstrap": "5.0.0",
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "12.0.0",
"commander": "12.1.0",
"core-js": "3.35.0",
"form-data": "4.0.0",
"google-auth-library": "7.14.1",
@@ -161,21 +160,18 @@
"https-proxy-agent": "7.0.4",
"inquirer": "8.2.6",
"keytar": "7.9.0",
"ldapjs": "2.3.3",
"ldapts": "7.2.0",
"lowdb": "1.0.0",
"lunr": "2.3.9",
"ngx-toastr": "16.2.0",
"node-fetch": "2.7.0",
"open": "8.4.2",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
"tldjs": "2.3.1",
"zone.js": "0.13.1",
"zxcvbn": "4.4.2"
"zone.js": "0.13.1"
},
"engines": {
"node": "~18",
"npm": "~10.4.0"
"npm": "~10"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",

View File

@@ -7,15 +7,30 @@ exports.default = async function notarizing(context) {
if (electronPlatformName !== "darwin") {
return;
}
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
const appName = context.packager.appInfo.productFilename;
return await notarize({
tool: "notarytool",
appBundleId: "com.bitwarden.directory-connector",
appPath: `${appOutDir}/${appName}.app`,
teamId: "LTZ2PFU5D6",
appleId: appleId,
appleIdPassword: appleIdPassword,
});
if (process.env.APP_STORE_CONNECT_TEAM_ISSUER) {
const appleApiIssuer = process.env.APP_STORE_CONNECT_TEAM_ISSUER;
const appleApiKey = process.env.APP_STORE_CONNECT_AUTH_KEY_PATH;
const appleApiKeyId = process.env.APP_STORE_CONNECT_AUTH_KEY;
return await notarize({
tool: "notarytool",
appBundleId: "com.bitwarden.directory-connector",
appPath: `${appOutDir}/${appName}.app`,
appleApiIssuer: appleApiIssuer,
appleApiKey: appleApiKey,
appleApiKeyId: appleApiKeyId,
});
} else {
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
return await notarize({
tool: "notarytool",
appBundleId: "com.bitwarden.directory-connector",
appPath: `${appOutDir}/${appName}.app`,
teamId: "LTZ2PFU5D6",
appleId: appleId,
appleIdPassword: appleIdPassword,
});
}
};

View File

@@ -0,0 +1,4 @@
export abstract class AuthService {
logIn: (credentials: { clientId: string; clientSecret: string }) => Promise<void>;
logOut: (callback: () => void) => void;
}

View File

@@ -2,41 +2,33 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
<p class="text-center fw-bold">{{ "welcome" | i18n }}</p>
<p class="text-center">{{ "logInDesc" | i18n }}</p>
<div class="card">
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
<div class="card-body">
<div class="form-group">
<label for="client_id">{{ "clientId" | i18n }}</label>
<div class="mb-3">
<label for="client_id" class="form-label">{{ "clientId" | i18n }}</label>
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
</div>
<div class="form-group">
<div class="row-main">
<label for="client_secret">{{ "clientSecret" | i18n }}</label>
<div class="input-group">
<input
type="{{ showSecret ? 'text' : 'password' }}"
id="client_secret"
name="ClientSecret"
[(ngModel)]="clientSecret"
class="form-control"
/>
<div class="input-group-append">
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleSecret()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
<div class="mb-3">
<label for="client_secret" class="form-label">{{ "clientSecret" | i18n }}</label>
<div class="input-group">
<input
type="{{ showSecret ? 'text' : 'password' }}"
id="client_secret"
name="ClientSecret"
[(ngModel)]="clientSecret"
class="form-control"
/>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleSecret()"
>
<i class="bwi" [ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"></i>
</button>
</div>
</div>
<div class="d-flex">
@@ -47,7 +39,7 @@
{{ "logIn" | i18n }}
</button>
</div>
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
<button type="button" class="btn btn-link ms-auto" (click)="settings()">
{{ "settings" | i18n }}
</button>
</div>

View File

@@ -3,13 +3,12 @@ import { Router } from "@angular/router";
import { takeUntil } from "rxjs";
import { ModalService } from "@/jslib/angular/src/services/modal.service";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { AuthService } from "../../abstractions/auth.service";
import { StateService } from "../../abstractions/state.service";
import { EnvironmentComponent } from "./environment.component";
@@ -81,9 +80,10 @@ export class ApiKeyComponent {
}
try {
this.formPromise = this.authService.logIn(
new ApiLogInCredentials(this.clientId, this.clientSecret),
);
this.formPromise = this.authService.logIn({
clientId: this.clientId,
clientSecret: this.clientSecret,
});
await this.formPromise;
const organizationId = await this.stateService.getEntityId();
await this.stateService.setOrganizationId(organizationId);

View File

@@ -3,15 +3,13 @@
<form class="modal-content" (ngSubmit)="submit()">
<div class="modal-header">
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
<button type="button" class="close" data-dismiss="modal" title="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
<div class="form-group">
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
<div class="mb-3">
<label for="baseUrl" class="form-label">{{ "baseUrl" | i18n }}</label>
<input
id="baseUrl"
type="text"
@@ -19,14 +17,12 @@
[(ngModel)]="baseUrl"
class="form-control"
/>
<small class="text-muted form-text"
>{{ "ex" | i18n }} https://bitwarden.company.com</small
>
<div class="form-text">{{ "ex" | i18n }} https://bitwarden.company.com</div>
</div>
<h4>{{ "customEnvironment" | i18n }}</h4>
<p>{{ "customEnvironmentFooter" | i18n }}</p>
<div class="form-group">
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
<div class="mb-3">
<label for="webVaultUrl" class="form-label">{{ "webVaultUrl" | i18n }}</label>
<input
id="webVaultUrl"
type="text"
@@ -35,12 +31,12 @@
class="form-control"
/>
</div>
<div class="form-group">
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
<div class="mb-3">
<label for="apiUrl" class="form-label">{{ "apiUrl" | i18n }}</label>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
</div>
<div class="form-group">
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
<div class="mb-3">
<label for="identityUrl" class="form-label">{{ "identityUrl" | i18n }}</label>
<input
id="identityUrl"
type="text"

View File

@@ -10,7 +10,6 @@ import { DomSanitizer } from "@angular/platform-browser";
import { Router } from "@angular/router";
import { IndividualConfig, ToastrService } from "ngx-toastr";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { BroadcasterService } from "@/jslib/common/src/abstractions/broadcaster.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
@@ -18,6 +17,7 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { AuthService } from "../abstractions/auth.service";
import { StateService } from "../abstractions/state.service";
import { SyncService } from "../services/sync.service";

View File

@@ -0,0 +1,17 @@
import { InjectionToken } from "@angular/core";
import { StorageService } from "../../../jslib/common/src/abstractions/storage.service";
declare const tag: unique symbol;
/**
* A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter.
* @remarks The default angular implementation does not use the generic type to define the structure of the object,
* so the structural type system will not complain about a mismatch in the type parameter.
* This is solved by assigning T to an arbitrary private property.
*/
export class SafeInjectionToken<T> extends InjectionToken<T> {
private readonly [tag]: T;
}
export const SECURE_STORAGE = new SafeInjectionToken<StorageService>("SECURE_STORAGE");
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");

View File

@@ -0,0 +1,144 @@
import { Provider } from "@angular/core";
import { Constructor, Opaque } from "type-fest";
import { SafeInjectionToken } from "./injection-tokens";
// ******
// NOTE: this is a copy/paste of safe-provider.ts from the clients repository.
// The clients repository remains the primary version of this code.
// Make any changes there and copy it back to this repository.
// ******
/**
* The return type of the {@link safeProvider} helper function.
* Used to distinguish a type safe provider definition from a non-type safe provider definition.
*/
export type SafeProvider = Opaque<Provider>;
// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0
type AbstractConstructor<T> = abstract new (...args: any) => T;
type MapParametersToDeps<T> = {
[K in keyof T]: AbstractConstructor<T[K]> | SafeInjectionToken<T[K]>;
};
type SafeInjectionTokenType<T> = T extends SafeInjectionToken<infer J> ? J : never;
/**
* Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken
*/
type ProviderInstanceType<T> =
T extends SafeInjectionToken<any>
? InstanceType<SafeInjectionTokenType<T>>
: T extends Constructor<any> | AbstractConstructor<any>
? InstanceType<T>
: never;
/**
* Represents a dependency provided with the useClass option.
*/
type SafeClassProvider<
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
I extends Constructor<ProviderInstanceType<A>>,
D extends MapParametersToDeps<ConstructorParameters<I>>,
> = {
provide: A;
useClass: I;
deps: D;
};
/**
* Represents a dependency provided with the useValue option.
*/
type SafeValueProvider<A extends SafeInjectionToken<any>, V extends SafeInjectionTokenType<A>> = {
provide: A;
useValue: V;
};
/**
* Represents a dependency provided with the useFactory option.
*/
type SafeFactoryProvider<
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
I extends (...args: any) => ProviderInstanceType<A>,
D extends MapParametersToDeps<Parameters<I>>,
> = {
provide: A;
useFactory: I;
deps: D;
multi?: boolean;
};
/**
* Represents a dependency provided with the useExisting option.
*/
type SafeExistingProvider<
A extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
I extends Constructor<ProviderInstanceType<A>> | AbstractConstructor<ProviderInstanceType<A>>,
> = {
provide: A;
useExisting: I;
};
/**
* Represents a dependency where there is no abstract token, the token is the implementation
*/
type SafeConcreteProvider<
I extends Constructor<any>,
D extends MapParametersToDeps<ConstructorParameters<I>>,
> = {
provide: I;
deps: D;
};
/**
* If useAngularDecorators: true is specified, do not require a deps array.
* This is a manual override for where @Injectable decorators are used
*/
type UseAngularDecorators<T extends { deps: any }> = Omit<T, "deps"> & {
useAngularDecorators: true;
};
/**
* Represents a type with a deps array that may optionally be overridden with useAngularDecorators
*/
type AllowAngularDecorators<T extends { deps: any }> = T | UseAngularDecorators<T>;
/**
* A factory function that creates a provider for the ngModule providers array.
* This (almost) guarantees type safety for your provider definition. It does nothing at runtime.
* Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator,
* however this cannot be enforced by the type system and will not cause an error if the decorator is not used.
* @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] })
* @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.)
* @returns The exact same object without modification (pass-through).
*/
export const safeProvider = <
// types for useClass
AClass extends AbstractConstructor<any> | SafeInjectionToken<any>,
IClass extends Constructor<ProviderInstanceType<AClass>>,
DClass extends MapParametersToDeps<ConstructorParameters<IClass>>,
// types for useValue
AValue extends SafeInjectionToken<any>,
VValue extends SafeInjectionTokenType<AValue>,
// types for useFactory
AFactory extends AbstractConstructor<any> | SafeInjectionToken<any>,
IFactory extends (...args: any) => ProviderInstanceType<AFactory>,
DFactory extends MapParametersToDeps<Parameters<IFactory>>,
// types for useExisting
AExisting extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
IExisting extends
| Constructor<ProviderInstanceType<AExisting>>
| AbstractConstructor<ProviderInstanceType<AExisting>>,
// types for no token
IConcrete extends Constructor<any>,
DConcrete extends MapParametersToDeps<ConstructorParameters<IConcrete>>,
>(
provider:
| AllowAngularDecorators<SafeClassProvider<AClass, IClass, DClass>>
| SafeValueProvider<AValue, VValue>
| AllowAngularDecorators<SafeFactoryProvider<AFactory, IFactory, DFactory>>
| SafeExistingProvider<AExisting, IExisting>
| AllowAngularDecorators<SafeConcreteProvider<IConcrete, DConcrete>>
| Constructor<unknown>,
): SafeProvider => provider as SafeProvider;

View File

@@ -3,20 +3,17 @@ import { APP_INITIALIZER, NgModule } from "@angular/core";
import { JslibServicesModule } from "@/jslib/angular/src/services/jslib-services.module";
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
import { AuthService as AuthServiceAbstraction } from "@/jslib/common/src/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService as LogServiceAbstraction } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { ContainerService } from "@/jslib/common/src/services/container.service";
@@ -28,21 +25,23 @@ import { ElectronRendererStorageService } from "@/jslib/electron/src/services/el
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";
import { AuthService as AuthServiceAbstraction } from "../../abstractions/auth.service";
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
import { Account } from "../../models/account";
import { AuthService } from "../../services/auth.service";
import { I18nService } from "../../services/i18n.service";
import { NoopTwoFactorService } from "../../services/noop/noopTwoFactor.service";
import { StateService } from "../../services/state.service";
import { StateMigrationService } from "../../services/stateMigration.service";
import { SyncService } from "../../services/sync.service";
import { AuthGuardService } from "./auth-guard.service";
import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from "./injection-tokens";
import { LaunchGuardService } from "./launch-guard.service";
import { SafeProvider, safeProvider } from "./safe-provider";
export function initFactory(
environmentService: EnvironmentServiceAbstraction,
i18nService: I18nService,
i18nService: I18nServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
stateService: StateServiceAbstraction,
cryptoService: CryptoServiceAbstraction,
@@ -50,7 +49,7 @@ export function initFactory(
return async () => {
await stateService.init();
await environmentService.setUrlsFromStorage();
await i18nService.init();
await (i18nService as I18nService).init();
const htmlEl = window.document.documentElement;
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
htmlEl.classList.add("locale_" + i18nService.translationLocale);
@@ -78,8 +77,8 @@ export function initFactory(
imports: [JslibServicesModule],
declarations: [],
providers: [
{
provide: APP_INITIALIZER,
safeProvider({
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
useFactory: initFactory,
deps: [
EnvironmentServiceAbstraction,
@@ -89,21 +88,29 @@ export function initFactory(
CryptoServiceAbstraction,
],
multi: true,
},
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
{
}),
safeProvider({ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }),
safeProvider({
provide: I18nServiceAbstraction,
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
deps: ["WINDOW"],
},
{
deps: [WINDOW],
}),
safeProvider({
provide: MessagingServiceAbstraction,
useClass: ElectronRendererMessagingService,
deps: [BroadcasterServiceAbstraction],
},
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
{ provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService },
{
}),
safeProvider({
provide: StorageServiceAbstraction,
useClass: ElectronRendererStorageService,
deps: [],
}),
safeProvider({
provide: SECURE_STORAGE,
useClass: ElectronRendererSecureStorageService,
deps: [],
}),
safeProvider({
provide: PlatformUtilsServiceAbstraction,
useFactory: (
i18nService: I18nServiceAbstraction,
@@ -111,9 +118,13 @@ export function initFactory(
stateService: StateServiceAbstraction,
) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService),
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
},
{ provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] },
{
}),
safeProvider({
provide: CryptoFunctionServiceAbstraction,
useClass: NodeCryptoFunctionService,
deps: [],
}),
safeProvider({
provide: ApiServiceAbstraction,
useFactory: (
tokenService: TokenServiceAbstraction,
@@ -141,26 +152,19 @@ export function initFactory(
MessagingServiceAbstraction,
AppIdServiceAbstraction,
],
},
{
}),
safeProvider({
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
LogServiceAbstraction,
KeyConnectorServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
],
},
{
}),
safeProvider({
provide: SyncService,
useClass: SyncService,
deps: [
@@ -172,10 +176,10 @@ export function initFactory(
EnvironmentServiceAbstraction,
StateServiceAbstraction,
],
},
AuthGuardService,
LaunchGuardService,
{
}),
safeProvider(AuthGuardService),
safeProvider(LaunchGuardService),
safeProvider({
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
@@ -186,9 +190,9 @@ export function initFactory(
secureStorageService,
new StateFactory(GlobalState, Account),
),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
},
{
deps: [StorageServiceAbstraction, SECURE_STORAGE],
}),
safeProvider({
provide: StateServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
@@ -206,15 +210,11 @@ export function initFactory(
),
deps: [
StorageServiceAbstraction,
"SECURE_STORAGE",
SECURE_STORAGE,
LogServiceAbstraction,
StateMigrationServiceAbstraction,
],
},
{
provide: TwoFactorServiceAbstraction,
useClass: NoopTwoFactorService,
},
],
}),
] satisfies SafeProvider[],
})
export class ServicesModule {}

View File

@@ -105,6 +105,11 @@
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
<i class="bwi bwi-li bwi-sitemap"></i>
{{ g.displayName }}
<ul class="small" *ngIf="g.users && g.users.length">
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
{{ u.displayName }}
</li>
</ul>
</li>
</ul>
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>

View File

@@ -1,19 +1,19 @@
<div class="row">
<div class="row g-3">
<div class="col-sm">
<div class="card mb-3">
<h3 class="card-header">{{ "directory" | i18n }}</h3>
<div class="card-body">
<div class="form-group">
<label for="directory">{{ "type" | i18n }}</label>
<select class="form-control" id="directory" name="Directory" [(ngModel)]="directory">
<div class="mb-3">
<label for="directory" class="form-label">{{ "type" | i18n }}</label>
<select class="form-select" id="directory" name="Directory" [(ngModel)]="directory">
<option *ngFor="let o of directoryOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div [hidden]="directory != directoryType.Ldap">
<div class="form-group">
<label for="hostname">{{ "serverHostname" | i18n }}</label>
<div class="mb-3">
<label for="hostname" class="form-label">{{ "serverHostname" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -21,15 +21,15 @@
name="Hostname"
[(ngModel)]="ldap.hostname"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} ad.company.com</small>
<div class="form-text">{{ "ex" | i18n }} ad.company.com</div>
</div>
<div class="form-group">
<label for="port">{{ "port" | i18n }}</label>
<div class="mb-3">
<label for="port" class="form-label">{{ "port" | i18n }}</label>
<input type="text" class="form-control" id="port" name="Port" [(ngModel)]="ldap.port" />
<small class="text-muted form-text">{{ "ex" | i18n }} 389</small>
<div class="form-text">{{ "ex" | i18n }} 389</div>
</div>
<div class="form-group">
<label for="rootPath">{{ "rootPath" | i18n }}</label>
<div class="mb-3">
<label for="rootPath" class="form-label">{{ "rootPath" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -37,9 +37,9 @@
name="RootPath"
[(ngModel)]="ldap.rootPath"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} dc=company,dc=com</small>
<div class="form-text">{{ "ex" | i18n }} dc=company,dc=com</div>
</div>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -51,7 +51,7 @@
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
</div>
</div>
<div class="form-group" *ngIf="!ldap.ad">
<div class="mb-3" *ngIf="!ldap.ad">
<div class="form-check">
<input
class="form-check-input"
@@ -65,7 +65,7 @@
}}</label>
</div>
</div>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -79,38 +79,38 @@
}}</label>
</div>
</div>
<div class="ml-4" *ngIf="ldap.ssl">
<div class="form-group">
<div class="form-radio">
<div class="ms-4" *ngIf="ldap.ssl">
<div class="mb-3">
<div class="form-check">
<input
class="form-radio-input"
class="form-check-input"
type="radio"
[value]="false"
id="ssl"
[(ngModel)]="ldap.startTls"
name="SSL"
/>
<label class="form-radio-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
</div>
<div class="form-radio">
<div class="form-check">
<input
class="form-radio-input"
class="form-check-input"
type="radio"
[value]="true"
id="startTls"
[(ngModel)]="ldap.startTls"
name="StartTLS"
/>
<label class="form-radio-label" for="startTls">{{ "ldapTls" | i18n }}</label>
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
</div>
</div>
<div class="ml-4" *ngIf="ldap.startTls">
<div class="ms-4" *ngIf="ldap.startTls">
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
<div class="form-group">
<label for="tlsCaPath">{{ "ldapTlsCa" | i18n }}</label>
<div class="mb-3">
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
<input
type="file"
class="form-control-file mb-2"
class="form-control mb-2"
id="tlsCaPath_file"
(change)="setSslPath('tlsCaPath')"
/>
@@ -123,13 +123,13 @@
/>
</div>
</div>
<div class="ml-4" *ngIf="!ldap.startTls">
<div class="ms-4" *ngIf="!ldap.startTls">
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
<div class="form-group">
<label for="sslCertPath">{{ "ldapSslCert" | i18n }}</label>
<div class="mb-3">
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
<input
type="file"
class="form-control-file mb-2"
class="form-control mb-2"
id="sslCertPath_file"
(change)="setSslPath('sslCertPath')"
/>
@@ -141,11 +141,11 @@
[(ngModel)]="ldap.sslCertPath"
/>
</div>
<div class="form-group">
<label for="sslKeyPath">{{ "ldapSslKey" | i18n }}</label>
<div class="mb-3">
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
<input
type="file"
class="form-control-file mb-2"
class="form-control mb-2"
id="sslKeyPath_file"
(change)="setSslPath('sslKeyPath')"
/>
@@ -157,11 +157,11 @@
[(ngModel)]="ldap.sslKeyPath"
/>
</div>
<div class="form-group">
<label for="sslCaPath">{{ "ldapSslCa" | i18n }}</label>
<div class="mb-3">
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
<input
type="file"
class="form-control-file mb-2"
class="form-control mb-2"
id="sslCaPath_file"
(change)="setSslPath('sslCaPath')"
/>
@@ -174,7 +174,7 @@
/>
</div>
</div>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -189,7 +189,7 @@
</div>
</div>
</div>
<div class="form-group" [hidden]="true">
<div class="mb-3" [hidden]="true">
<div class="form-check">
<input
class="form-check-input"
@@ -202,8 +202,8 @@
</div>
</div>
<div [hidden]="ldap.currentUser">
<div class="form-group">
<label for="username">{{ "username" | i18n }}</label>
<div class="mb-3">
<label for="username" class="form-label">{{ "username" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -211,15 +211,13 @@
name="Username"
[(ngModel)]="ldap.username"
/>
<small class="text-muted form-text" *ngIf="ldap.ad"
>{{ "ex" | i18n }} company\admin</small
>
<small class="text-muted form-text" *ngIf="!ldap.ad"
>{{ "ex" | i18n }} cn=admin,dc=company,dc=com</small
>
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} company\admin</div>
<div class="form-text" *ngIf="!ldap.ad">
{{ "ex" | i18n }} cn=admin,dc=company,dc=com
</div>
</div>
<div class="form-group">
<label for="password">{{ "password" | i18n }}</label>
<div class="mb-3">
<label for="password" class="form-label">{{ "password" | i18n }}</label>
<div class="input-group">
<input
type="{{ showLdapPassword ? 'text' : 'password' }}"
@@ -228,29 +226,29 @@
name="Password"
[(ngModel)]="ldap.password"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleLdapPassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showLdapPassword ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleLdapPassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showLdapPassword ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.AzureActiveDirectory">
<div class="form-group">
<label for="identityAuthority">{{ "identityAuthority" | i18n }}</label>
<div class="mb-3">
<label for="identityAuthority" class="form-label">{{
"identityAuthority" | i18n
}}</label>
<select
class="form-control"
class="form-select"
id="identityAuthority"
name="IdentityAuthority"
[(ngModel)]="azure.identityAuthority"
@@ -259,8 +257,8 @@
<option value="login.microsoftonline.us">Azure AD Government</option>
</select>
</div>
<div class="form-group">
<label for="tenant">{{ "tenant" | i18n }}</label>
<div class="mb-3">
<label for="tenant" class="form-label">{{ "tenant" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -268,10 +266,10 @@
name="Tenant"
[(ngModel)]="azure.tenant"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</small>
<div class="form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</div>
</div>
<div class="form-group">
<label for="applicationId">{{ "applicationId" | i18n }}</label>
<div class="mb-3">
<label for="applicationId" class="form-label">{{ "applicationId" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -280,8 +278,8 @@
[(ngModel)]="azure.applicationId"
/>
</div>
<div class="form-group">
<label for="secretKey">{{ "secretKey" | i18n }}</label>
<div class="mb-3">
<label for="secretKey" class="form-label">{{ "secretKey" | i18n }}</label>
<div class="input-group">
<input
type="{{ showAzureKey ? 'text' : 'password' }}"
@@ -290,26 +288,24 @@
name="SecretKey"
[(ngModel)]="azure.key"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleAzureKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showAzureKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleAzureKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showAzureKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.Okta">
<div class="form-group">
<label for="orgUrl">{{ "organizationUrl" | i18n }}</label>
<div class="mb-3">
<label for="orgUrl" class="form-label">{{ "organizationUrl" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -317,12 +313,10 @@
name="OrgUrl"
[(ngModel)]="okta.orgUrl"
/>
<small class="text-muted form-text"
>{{ "ex" | i18n }} https://mycompany.okta.com/</small
>
<div class="form-text">{{ "ex" | i18n }} https://mycompany.okta.com/</div>
</div>
<div class="form-group">
<label for="oktaToken">{{ "token" | i18n }}</label>
<div class="mb-3">
<label for="oktaToken" class="form-label">{{ "token" | i18n }}</label>
<div class="input-group">
<input
type="{{ showOktaKey ? 'text' : 'password' }}"
@@ -331,26 +325,24 @@
name="OktaToken"
[(ngModel)]="okta.token"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOktaKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOktaKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOktaKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOktaKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.OneLogin">
<div class="form-group">
<label for="oneLoginClientId">{{ "clientId" | i18n }}</label>
<div class="mb-3">
<label for="oneLoginClientId" class="form-label">{{ "clientId" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -359,8 +351,8 @@
[(ngModel)]="oneLogin.clientId"
/>
</div>
<div class="form-group">
<label for="oneLoginClientSecret">{{ "clientSecret" | i18n }}</label>
<div class="mb-3">
<label for="oneLoginClientSecret" class="form-label">{{ "clientSecret" | i18n }}</label>
<div class="input-group">
<input
type="{{ showOneLoginSecret ? 'text' : 'password' }}"
@@ -369,26 +361,24 @@
name="OneLoginClientSecret"
[(ngModel)]="oneLogin.clientSecret"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOneLoginSecret()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOneLoginSecret ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOneLoginSecret()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOneLoginSecret ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
<div class="form-group">
<label for="oneLoginRegion">{{ "region" | i18n }}</label>
<div class="mb-3">
<label for="oneLoginRegion" class="form-label">{{ "region" | i18n }}</label>
<select
class="form-control"
class="form-select"
id="oneLoginRegion"
name="OneLoginRegion"
[(ngModel)]="oneLogin.region"
@@ -399,8 +389,8 @@
</div>
</div>
<div [hidden]="directory != directoryType.GSuite">
<div class="form-group">
<label for="domain">{{ "domain" | i18n }}</label>
<div class="mb-3">
<label for="domain" class="form-label">{{ "domain" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -408,10 +398,10 @@
name="Domain"
[(ngModel)]="gsuite.domain"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} company.com</small>
<div class="form-text">{{ "ex" | i18n }} company.com</div>
</div>
<div class="form-group">
<label for="adminUser">{{ "adminUser" | i18n }}</label>
<div class="mb-3">
<label for="adminUser" class="form-label">{{ "adminUser" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -419,10 +409,10 @@
name="AdminUser"
[(ngModel)]="gsuite.adminUser"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} admin@company.com</small>
<div class="form-text">{{ "ex" | i18n }} admin&#64;company.com</div>
</div>
<div class="form-group">
<label for="customerId">{{ "customerId" | i18n }}</label>
<div class="mb-3">
<label for="customerId" class="form-label">{{ "customerId" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -430,21 +420,21 @@
name="CustomerId"
[(ngModel)]="gsuite.customer"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} 39204722352</small>
<div class="form-text">{{ "ex" | i18n }} 39204722352</div>
</div>
<div class="form-group">
<label for="keyFile">{{ "jsonKeyFile" | i18n }}</label>
<div class="mb-3">
<label for="keyFile" class="form-label">{{ "jsonKeyFile" | i18n }}</label>
<input
type="file"
class="form-control-file"
class="form-control"
id="keyFile"
accept=".json"
(change)="parseKeyFile()"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} My Project-jksd3jd223.json</small>
<div class="form-text">{{ "ex" | i18n }} My Project-jksd3jd223.json</div>
</div>
<div class="form-group" [hidden]="!gsuite.clientEmail">
<label for="clientEmail">{{ "clientEmail" | i18n }}</label>
<div class="mb-3" [hidden]="!gsuite.clientEmail">
<label for="clientEmail" class="form-label">{{ "clientEmail" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -453,8 +443,8 @@
[(ngModel)]="gsuite.clientEmail"
/>
</div>
<div class="form-group" [hidden]="!gsuite.privateKey">
<label for="privateKey">{{ "privateKey" | i18n }}</label>
<div class="mb-3" [hidden]="!gsuite.privateKey">
<label for="privateKey" class="form-label">{{ "privateKey" | i18n }}</label>
<textarea
class="form-control text-monospace"
id="privateKey"
@@ -471,8 +461,8 @@
<div class="card">
<h3 class="card-header">{{ "sync" | i18n }}</h3>
<div class="card-body">
<div class="form-group">
<label for="interval">{{ "interval" | i18n }}</label>
<div class="mb-3">
<label for="interval" class="form-label">{{ "interval" | i18n }}</label>
<input
type="number"
min="5"
@@ -481,9 +471,9 @@
name="Interval"
[(ngModel)]="sync.interval"
/>
<small class="text-muted form-text">{{ "intervalMin" | i18n }}</small>
<div class="form-text">{{ "intervalMin" | i18n }}</div>
</div>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -497,7 +487,7 @@
}}</label>
</div>
</div>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -511,7 +501,7 @@
}}</label>
</div>
</div>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -525,8 +515,8 @@
</div>
<div [hidden]="directory != directoryType.Ldap">
<div [hidden]="ldap.ad">
<div class="form-group">
<label for="memberAttribute">{{ "memberAttribute" | i18n }}</label>
<div class="mb-3">
<label for="memberAttribute" class="form-label">{{ "memberAttribute" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -534,10 +524,12 @@
name="MemberAttribute"
[(ngModel)]="sync.memberAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} uniqueMember</small>
<div class="form-text">{{ "ex" | i18n }} uniqueMember</div>
</div>
<div class="form-group">
<label for="creationDateAttribute">{{ "creationDateAttribute" | i18n }}</label>
<div class="mb-3">
<label for="creationDateAttribute" class="form-label">{{
"creationDateAttribute" | i18n
}}</label>
<input
type="text"
class="form-control"
@@ -545,10 +537,12 @@
name="CreationDateAttribute"
[(ngModel)]="sync.creationDateAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} whenCreated</small>
<div class="form-text">{{ "ex" | i18n }} whenCreated</div>
</div>
<div class="form-group">
<label for="revisionDateAttribute">{{ "revisionDateAttribute" | i18n }}</label>
<div class="mb-3">
<label for="revisionDateAttribute" class="form-label">{{
"revisionDateAttribute" | i18n
}}</label>
<input
type="text"
class="form-control"
@@ -556,12 +550,12 @@
name="RevisionDateAttribute"
[(ngModel)]="sync.revisionDateAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} whenChanged</small>
<div class="form-text">{{ "ex" | i18n }} whenChanged</div>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin">
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -576,8 +570,10 @@
</div>
</div>
<div [hidden]="!sync.useEmailPrefixSuffix">
<div class="form-group" [hidden]="ldap.ad || directory != directoryType.Ldap">
<label for="emailPrefixAttribute">{{ "emailPrefixAttribute" | i18n }}</label>
<div class="mb-3" [hidden]="ldap.ad || directory != directoryType.Ldap">
<label for="emailPrefixAttribute" class="form-label">{{
"emailPrefixAttribute" | i18n
}}</label>
<input
type="text"
class="form-control"
@@ -585,10 +581,10 @@
name="EmailPrefixAttribute"
[(ngModel)]="sync.emailPrefixAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} accountName</small>
<div class="form-text">{{ "ex" | i18n }} accountName</div>
</div>
<div class="form-group">
<label for="emailSuffix">{{ "emailSuffix" | i18n }}</label>
<div class="mb-3">
<label for="emailSuffix" class="form-label">{{ "emailSuffix" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -596,12 +592,12 @@
name="EmailSuffix"
[(ngModel)]="sync.emailSuffix"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} @company.com</small>
<div class="form-text">{{ "ex" | i18n }} &#64;company.com</div>
</div>
</div>
</div>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -614,31 +610,29 @@
</div>
</div>
<div [hidden]="!sync.users">
<div class="form-group">
<label for="userFilter">{{ "userFilter" | i18n }}</label>
<div class="mb-3">
<label for="userFilter" class="form-label">{{ "userFilter" | i18n }}</label>
<textarea
class="form-control"
id="userFilter"
name="UserFilter"
[(ngModel)]="sync.userFilter"
></textarea>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap"
>{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))</small
>
<small
class="text-muted form-text"
*ngIf="directory === directoryType.AzureActiveDirectory"
>{{ "ex" | i18n }} exclude:joe@company.com</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
>{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
>{{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering</small
>
<div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))
</div>
<div class="form-text" *ngIf="directory === directoryType.AzureActiveDirectory">
{{ "ex" | i18n }} exclude:joe&#64;company.com
</div>
<div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgName=Engineering
</div>
</div>
<div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="userPath">{{ "userPath" | i18n }}</label>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
<label for="userPath" class="form-label">{{ "userPath" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -646,11 +640,11 @@
name="UserPath"
[(ngModel)]="sync.userPath"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} CN=Users</small>
<div class="form-text">{{ "ex" | i18n }} CN=Users</div>
</div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="form-group">
<label for="userObjectClass">{{ "userObjectClass" | i18n }}</label>
<div class="mb-3">
<label for="userObjectClass" class="form-label">{{ "userObjectClass" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -658,10 +652,12 @@
name="UserObjectClass"
[(ngModel)]="sync.userObjectClass"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} inetOrgPerson</small>
<div class="form-text">{{ "ex" | i18n }} inetOrgPerson</div>
</div>
<div class="form-group">
<label for="userEmailAttribute">{{ "userEmailAttribute" | i18n }}</label>
<div class="mb-3">
<label for="userEmailAttribute" class="form-label">{{
"userEmailAttribute" | i18n
}}</label>
<input
type="text"
class="form-control"
@@ -669,12 +665,12 @@
name="UserEmailAttribute"
[(ngModel)]="sync.userEmailAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} mail</small>
<div class="form-text">{{ "ex" | i18n }} mail</div>
</div>
</div>
</div>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
@@ -689,8 +685,8 @@
</div>
</div>
<div [hidden]="!sync.groups">
<div class="form-group">
<label for="groupFilter">{{
<div class="mb-3">
<label for="groupFilter" class="form-label">{{
(directory !== directoryType.OneLogin ? "groupFilter" : "groupFilterOneLogin") | i18n
}}</label>
<textarea
@@ -699,23 +695,21 @@
name="GroupFilter"
[(ngModel)]="sync.groupFilter"
></textarea>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap"
>{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))</small
>
<small
class="text-muted form-text"
*ngIf="directory === directoryType.AzureActiveDirectory"
>{{ "ex" | i18n }} include:Sales,IT</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
>{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
>{{ "ex" | i18n }} include:Sales,IT</small
>
<div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
</div>
<div class="form-text" *ngIf="directory === directoryType.AzureActiveDirectory">
{{ "ex" | i18n }} include:Sales,IT
</div>
<div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} include:Sales,IT
</div>
</div>
<div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="groupPath">{{ "groupPath" | i18n }}</label>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
<label for="groupPath" class="form-label">{{ "groupPath" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -723,12 +717,14 @@
name="GroupPath"
[(ngModel)]="sync.groupPath"
/>
<small class="text-muted form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</small>
<small class="text-muted form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</small>
<div class="form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</div>
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</div>
</div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="form-group">
<label for="groupObjectClass">{{ "groupObjectClass" | i18n }}</label>
<div class="mb-3">
<label for="groupObjectClass" class="form-label">{{
"groupObjectClass" | i18n
}}</label>
<input
type="text"
class="form-control"
@@ -736,10 +732,12 @@
name="GroupObjectClass"
[(ngModel)]="sync.groupObjectClass"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} groupOfUniqueNames</small>
<div class="form-text">{{ "ex" | i18n }} groupOfUniqueNames</div>
</div>
<div class="form-group">
<label for="groupNameAttribute">{{ "groupNameAttribute" | i18n }}</label>
<div class="mb-3">
<label for="groupNameAttribute" class="form-label">{{
"groupNameAttribute" | i18n
}}</label>
<input
type="text"
class="form-control"
@@ -747,7 +745,7 @@
name="GroupNameAttribute"
[(ngModel)]="sync.groupNameAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} name</small>
<div class="form-text">{{ "ex" | i18n }} name</div>
</div>
</div>
</div>

View File

@@ -2,28 +2,15 @@ import * as fs from "fs";
import * as path from "path";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
import { ClientType } from "@/jslib/common/src/enums/clientType";
import { LogLevelType } from "@/jslib/common/src/enums/logLevelType";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { AppIdService } from "@/jslib/common/src/services/appId.service";
import { CipherService } from "@/jslib/common/src/services/cipher.service";
import { CollectionService } from "@/jslib/common/src/services/collection.service";
import { ContainerService } from "@/jslib/common/src/services/container.service";
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
import { FileUploadService } from "@/jslib/common/src/services/fileUpload.service";
import { FolderService } from "@/jslib/common/src/services/folder.service";
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service";
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
import { PolicyService } from "@/jslib/common/src/services/policy.service";
import { ProviderService } from "@/jslib/common/src/services/provider.service";
import { SearchService } from "@/jslib/common/src/services/search.service";
import { SendService } from "@/jslib/common/src/services/send.service";
import { SettingsService } from "@/jslib/common/src/services/settings.service";
import { TokenService } from "@/jslib/common/src/services/token.service";
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
@@ -36,7 +23,6 @@ import { AuthService } from "./services/auth.service";
import { I18nService } from "./services/i18n.service";
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NoopTwoFactorService } from "./services/noop/noopTwoFactor.service";
import { StateService } from "./services/state.service";
import { StateMigrationService } from "./services/stateMigration.service";
import { SyncService } from "./services/sync.service";
@@ -44,10 +30,11 @@ import { SyncService } from "./services/sync.service";
// eslint-disable-next-line
const packageJson = require("../package.json");
export const searchService: SearchService = null;
export class Main {
dataFilePath: string;
logService: ConsoleLogService;
program: Program;
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: StorageServiceAbstraction;
@@ -61,23 +48,9 @@ export class Main {
containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
collectionService: CollectionService;
cipherService: CipherService;
fileUploadService: FileUploadService;
folderService: FolderService;
searchService: SearchService;
sendService: SendService;
settingsService: SettingsService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
policyService: PolicyService;
keyConnectorService: KeyConnectorService;
program: Program;
stateService: StateService;
stateMigrationService: StateMigrationService;
organizationService: OrganizationService;
providerService: ProviderService;
twoFactorService: TwoFactorServiceAbstraction;
constructor() {
const applicationName = "Bitwarden Directory Connector";
@@ -165,33 +138,12 @@ export class Main {
);
this.containerService = new ContainerService(this.cryptoService);
this.organizationService = new OrganizationService(this.stateService);
this.keyConnectorService = new KeyConnectorService(
this.stateService,
this.cryptoService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.cryptoFunctionService,
);
this.twoFactorService = new NoopTwoFactorService();
this.authService = new AuthService(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.keyConnectorService,
this.environmentService,
this.stateService,
this.twoFactorService,
this.i18nService,
);
this.syncService = new SyncService(
@@ -204,60 +156,6 @@ export class Main {
this.stateService,
);
this.policyService = new PolicyService(
this.stateService,
this.organizationService,
this.apiService,
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService,
);
this.settingsService = new SettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
this.cipherService = new CipherService(
this.cryptoService,
this.settingsService,
this.apiService,
this.fileUploadService,
this.i18nService,
() => searchService,
this.logService,
this.stateService,
);
this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService);
this.folderService = new FolderService(
this.cryptoService,
this.apiService,
this.i18nService,
this.cipherService,
this.stateService,
);
this.collectionService = new CollectionService(
this.cryptoService,
this.i18nService,
this.stateService,
);
this.sendService = new SendService(
this.cryptoService,
this.apiService,
this.fileUploadService,
this.i18nService,
this.cryptoFunctionService,
this.stateService,
);
this.providerService = new ProviderService(this.stateService);
this.program = new Program(this);
}

View File

@@ -0,0 +1,60 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "../abstractions/auth.service";
import { LoginCommand } from "./login.command";
const clientId = "test_client_id";
const clientSecret = "test_client_secret";
// Mock responses from the inquirer prompt
// This combines both prompt results into a single object which is returned both times
jest.mock("inquirer", () => ({
createPromptModule: () => () => ({
clientId,
clientSecret,
}),
}));
describe("LoginCommand", () => {
let authService: MockProxy<AuthService>;
let loginCommand: LoginCommand;
beforeEach(() => {
// reset env variables
delete process.env.BW_CLIENTID;
delete process.env.BW_CLIENTSECRET;
authService = mock();
loginCommand = new LoginCommand(authService);
});
it("uses client id and secret stored in environment variables", async () => {
process.env.BW_CLIENTID = clientId;
process.env.BW_CLIENTSECRET = clientSecret;
const result = await loginCommand.run();
expect(authService.logIn).toHaveBeenCalledWith({ clientId, clientSecret });
expect(result).toMatchObject({
data: {
title: "You are logged in!",
},
success: true,
});
});
it("uses client id and secret prompted from the user", async () => {
const result = await loginCommand.run();
expect(authService.logIn).toHaveBeenCalledWith({ clientId, clientSecret });
expect(result).toMatchObject({
data: {
title: "You are logged in!",
},
success: true,
});
});
});

Some files were not shown because too many files have changed in this diff Show More