mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-10 05:13:17 +00:00
Compare commits
1 Commits
ac/pm-1180
...
mimartin12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e38c2e88 |
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -6,3 +6,10 @@
|
|||||||
|
|
||||||
# Default file owners.
|
# Default file owners.
|
||||||
* @bitwarden/team-admin-console-dev
|
* @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
|
||||||
47
.github/PULL_REQUEST_TEMPLATE.md
vendored
47
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,34 +1,33 @@
|
|||||||
## 🎟️ Tracking
|
## Type of change
|
||||||
|
|
||||||
<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature development
|
||||||
|
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||||
|
- [ ] Build/deploy pipeline (DevOps)
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
## 📔 Objective
|
## Objective
|
||||||
|
|
||||||
<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->
|
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
|
||||||
|
|
||||||
## 📸 Screenshots
|
## Code changes
|
||||||
|
|
||||||
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
|
<!--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-->
|
||||||
|
|
||||||
## ⏰ Reminders before review
|
- **file.ext:** Description of what was changed and why
|
||||||
|
|
||||||
- Contributor guidelines followed
|
## Screenshots
|
||||||
- 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
|
|
||||||
|
|
||||||
## 🦮 Reviewer guidelines
|
<!--Required for any UI changes. Delete if not applicable-->
|
||||||
|
|
||||||
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
|
## Testing requirements
|
||||||
|
|
||||||
- 👍 (`:+1:`) or similar for great changes
|
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
|
||||||
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
|
|
||||||
- ❓ (`:question:`) for questions
|
## Before you submit
|
||||||
- 🤔 (`: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
|
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
|
||||||
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
|
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
||||||
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
|
- [ ] This change requires a **documentation update** (notify the documentation team)
|
||||||
- ⛏ (`:pick:`) for minor or nitpick changes
|
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
||||||
|
|||||||
25
.github/renovate.json
vendored
25
.github/renovate.json
vendored
@@ -1,12 +1,31 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["github>bitwarden/renovate-config"],
|
"extends": [
|
||||||
|
"config:base",
|
||||||
|
"github>bitwarden/renovate-config:pin-actions",
|
||||||
|
":combinePatchMinorReleases",
|
||||||
|
":dependencyDashboard",
|
||||||
|
":maintainLockFilesWeekly",
|
||||||
|
":pinAllExceptPeerDependencies",
|
||||||
|
":prConcurrentLimit10",
|
||||||
|
":rebaseStalePrs",
|
||||||
|
":separateMajorReleases",
|
||||||
|
"group:monorepos",
|
||||||
|
"schedule:weekends"
|
||||||
|
],
|
||||||
"enabledManagers": ["github-actions", "npm"],
|
"enabledManagers": ["github-actions", "npm"],
|
||||||
|
"commitMessagePrefix": "[deps]:",
|
||||||
|
"commitMessageTopic": "{{depName}}",
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"groupName": "gh minor",
|
"groupName": "npm minor",
|
||||||
"matchManagers": ["github-actions"],
|
"matchManagers": ["npm"],
|
||||||
"matchUpdateTypes": ["minor", "patch"]
|
"matchUpdateTypes": ["minor", "patch"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchFileNames": ["package.json"],
|
||||||
|
"description": "Admin Console owns general dependencies",
|
||||||
|
"reviewers": ["team:team-admin-console-dev"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
.github/secrets/devid-app-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/devid-app-cert.p12.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/devid-installer-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/devid-installer-cert.p12.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/macdev-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/macdev-cert.p12.gpg
vendored
Normal file
Binary file not shown.
140
.github/workflows/build.yml
vendored
140
.github/workflows/build.yml
vendored
@@ -3,9 +3,6 @@ name: Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request: {}
|
pull_request: {}
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "main"
|
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -14,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Set up CLOC
|
- name: Set up CLOC
|
||||||
run: |
|
run: |
|
||||||
@@ -32,7 +29,7 @@ jobs:
|
|||||||
package_version: ${{ steps.retrieve-version.outputs.package_version }}
|
package_version: ${{ steps.retrieve-version.outputs.package_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Get Package Version
|
- name: Get Package Version
|
||||||
id: retrieve-version
|
id: retrieve-version
|
||||||
@@ -51,7 +48,7 @@ jobs:
|
|||||||
_PKG_FETCH_VERSION: 3.4
|
_PKG_FETCH_VERSION: 3.4
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
@@ -122,14 +119,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload Linux Zip to GitHub
|
- name: Upload Linux Zip to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||||
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Linux checksum to GitHub
|
- name: Upload Linux checksum to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
|
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
|
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
@@ -138,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
macos-cli:
|
macos-cli:
|
||||||
name: Build Mac CLI
|
name: Build Mac CLI
|
||||||
runs-on: macos-13
|
runs-on: macos-12
|
||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||||
@@ -146,7 +143,7 @@ jobs:
|
|||||||
_PKG_FETCH_VERSION: 3.4
|
_PKG_FETCH_VERSION: 3.4
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
@@ -210,14 +207,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload Mac Zip to GitHub
|
- name: Upload Mac Zip to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||||
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Mac checksum to GitHub
|
- name: Upload Mac checksum to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
|
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
|
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
@@ -233,7 +230,7 @@ jobs:
|
|||||||
_WIN_PKG_VERSION: 3.4
|
_WIN_PKG_VERSION: 3.4
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Setup Windows builder
|
- name: Setup Windows builder
|
||||||
run: |
|
run: |
|
||||||
@@ -354,14 +351,14 @@ jobs:
|
|||||||
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
|
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
|
||||||
|
|
||||||
- name: Upload Windows Zip to GitHub
|
- name: Upload Windows Zip to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||||
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Windows checksum to GitHub
|
- name: Upload Windows checksum to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||||
@@ -378,7 +375,7 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
@@ -414,28 +411,28 @@ jobs:
|
|||||||
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
|
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
|
||||||
|
|
||||||
- name: Upload Portable Executable to GitHub
|
- name: Upload Portable Executable to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||||
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Installer Executable to GitHub
|
- name: Upload Installer Executable to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Installer Executable Blockmap to GitHub
|
- name: Upload Installer Executable Blockmap to GitHub
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
||||||
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload latest auto-update artifact
|
- name: Upload latest auto-update artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: latest.yml
|
name: latest.yml
|
||||||
path: ./dist/latest.yml
|
path: ./dist/latest.yml
|
||||||
@@ -452,7 +449,7 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
@@ -482,14 +479,14 @@ jobs:
|
|||||||
run: npm run dist:lin
|
run: npm run dist:lin
|
||||||
|
|
||||||
- name: Upload AppImage
|
- name: Upload AppImage
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload latest auto-update artifact
|
- name: Upload latest auto-update artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: latest-linux.yml
|
name: latest-linux.yml
|
||||||
path: ./dist/latest-linux.yml
|
path: ./dist/latest-linux.yml
|
||||||
@@ -498,7 +495,7 @@ jobs:
|
|||||||
|
|
||||||
macos-gui:
|
macos-gui:
|
||||||
name: Build MacOS GUI
|
name: Build MacOS GUI
|
||||||
runs-on: macos-13
|
runs-on: macos-12
|
||||||
needs: setup
|
needs: setup
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max_old_space_size=4096
|
NODE_OPTIONS: --max_old_space_size=4096
|
||||||
@@ -506,7 +503,7 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
@@ -526,43 +523,44 @@ jobs:
|
|||||||
npm --version
|
npm --version
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Login to Azure
|
- name: Decrypt secrets
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
env:
|
||||||
with:
|
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
shell: bash
|
||||||
|
|
||||||
- name: Get certificates
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/certificates
|
mkdir -p $HOME/secrets
|
||||||
|
|
||||||
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert |
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12
|
--output "$HOME/secrets/devid-app-cert.p12" \
|
||||||
|
"$GITHUB_WORKSPACE/.github/secrets/devid-app-cert.p12.gpg"
|
||||||
|
|
||||||
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert |
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
jq -r .value | base64 -d > $HOME/certificates/devid-installer-cert.p12
|
--output "$HOME/secrets/devid-installer-cert.p12" \
|
||||||
|
"$GITHUB_WORKSPACE/.github/secrets/devid-installer-cert.p12.gpg"
|
||||||
|
|
||||||
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
|
--output "$HOME/secrets/macdev-cert.p12" \
|
||||||
|
"$GITHUB_WORKSPACE/.github/secrets/macdev-cert.p12.gpg"
|
||||||
|
|
||||||
- name: Set up keychain
|
- name: Set up keychain
|
||||||
env:
|
env:
|
||||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }}
|
||||||
|
MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }}
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||||
security default-keychain -s build.keychain
|
security default-keychain -s build.keychain
|
||||||
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||||
security set-keychain-settings -lut 1200 build.keychain
|
security set-keychain-settings -lut 1200 build.keychain
|
||||||
|
security import "$HOME/secrets/devid-app-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
|
||||||
security import "$HOME/certificates/devid-app-cert.p12" -k build.keychain -P "" \
|
|
||||||
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
||||||
|
security import "$HOME/secrets/devid-installer-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
|
||||||
security import "$HOME/certificates/devid-installer-cert.p12" -k build.keychain -P "" \
|
|
||||||
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
||||||
|
security import "$HOME/secrets/macdev-cert.p12" -k build.keychain -P $MACDEV_CERT_PASSWORD \
|
||||||
security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \
|
|
||||||
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
||||||
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
||||||
|
|
||||||
- name: Load package version
|
- name: Load package version
|
||||||
@@ -577,44 +575,36 @@ jobs:
|
|||||||
- name: Install Node dependencies
|
- name: Install Node dependencies
|
||||||
run: npm install
|
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
|
- name: Build application
|
||||||
run: npm run dist:mac
|
run: npm run dist:mac
|
||||||
env:
|
env:
|
||||||
APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
|
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||||
APP_STORE_CONNECT_AUTH_KEY: UFD296548T
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_UFD296548T.p8
|
|
||||||
CSC_FOR_PULL_REQUEST: true
|
CSC_FOR_PULL_REQUEST: true
|
||||||
|
|
||||||
- name: Upload .zip artifact
|
- name: Upload .zip artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .dmg artifact
|
- name: Upload .dmg artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload .dmg Blockmap artifact
|
- name: Upload .dmg Blockmap artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
||||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload latest auto-update artifact
|
- name: Upload latest auto-update artifact
|
||||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: latest-mac.yml
|
name: latest-mac.yml
|
||||||
path: ./dist/latest-mac.yml
|
path: ./dist/latest-mac.yml
|
||||||
@@ -635,8 +625,34 @@ jobs:
|
|||||||
- macos-gui
|
- macos-gui
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: github.ref == 'refs/heads/main' && contains(needs.*.result, 'failure')
|
if: ${{ (github.ref == 'refs/heads/main') || (github.ref == 'refs/heads/rc') }}
|
||||||
run: exit 1
|
env:
|
||||||
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
|
SETUP_STATUS: ${{ needs.setup.result }}
|
||||||
|
LINUX_CLI_STATUS: ${{ needs.linux-cli.result }}
|
||||||
|
MACOS_CLI_STATUS: ${{ needs.macos-cli.result }}
|
||||||
|
WINDOWS_CLI_STATUS: ${{ needs.windows-cli.result }}
|
||||||
|
WINDOWS_GUI_STATUS: ${{ needs.windows-gui.result }}
|
||||||
|
LINUX_GUI_STATUS: ${{ needs.linux-gui.result }}
|
||||||
|
MACOS_GUI_STATUS: ${{ needs.macos-gui.result }}
|
||||||
|
run: |
|
||||||
|
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$SETUP_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$LINUX_CLI_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$MACOS_CLI_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$WINDOWS_CLI_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$WINDOWS_GUI_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$LINUX_GUI_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
elif [ "$MACOS_GUI_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Login to Azure - CI subscription
|
- name: Login to Azure - CI subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@@ -653,7 +669,7 @@ jobs:
|
|||||||
secrets: "devops-alerts-slack-webhook-url"
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
- name: Notify Slack on failure
|
- name: Notify Slack on failure
|
||||||
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
|
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
|
|||||||
35
.github/workflows/release.yml
vendored
35
.github/workflows/release.yml
vendored
@@ -13,6 +13,7 @@ on:
|
|||||||
- Initial Release
|
- Initial Release
|
||||||
- Redeploy
|
- Redeploy
|
||||||
- Dry Run
|
- Dry Run
|
||||||
|
- NOOP
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
@@ -22,14 +23,14 @@ jobs:
|
|||||||
release-version: ${{ steps.version.outputs.version }}
|
release-version: ${{ steps.version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Branch check
|
- name: Branch check
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
|
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
echo "[!] Can only release from the 'main' branch"
|
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -47,6 +48,16 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
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
|
- name: Download all artifacts
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: bitwarden/gh-actions/download-artifacts@main
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
@@ -55,7 +66,7 @@ jobs:
|
|||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ github.ref_name }}
|
branch: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Dry Run - Download all artifacts
|
- name: Download all artifacts
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
uses: bitwarden/gh-actions/download-artifacts@main
|
uses: bitwarden/gh-actions/download-artifacts@main
|
||||||
with:
|
with:
|
||||||
@@ -91,3 +102,19 @@ jobs:
|
|||||||
body: "<insert release notes here>"
|
body: "<insert release notes here>"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
draft: true
|
draft: true
|
||||||
|
|
||||||
|
- name: Update deployment status to Success
|
||||||
|
if: ${{ success() }}
|
||||||
|
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||||
|
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@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||||
|
with:
|
||||||
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
state: 'failure'
|
||||||
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|||||||
78
.github/workflows/scan.yml
vendored
78
.github/workflows/scan.yml
vendored
@@ -1,78 +0,0 @@
|
|||||||
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
|
|
||||||
72
.github/workflows/test.yml
vendored
72
.github/workflows/test.yml
vendored
@@ -1,46 +1,28 @@
|
|||||||
name: Testing
|
---
|
||||||
|
name: Run tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
pull_request:
|
- "rc"
|
||||||
|
- "hotfix-rc-*"
|
||||||
|
pull_request: {}
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-test-secrets:
|
test:
|
||||||
name: Check for test secrets
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
outputs:
|
|
||||||
available: ${{ steps.check-test-secrets.outputs.available }}
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
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
|
name: Run tests
|
||||||
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: check-test-secrets
|
|
||||||
permissions:
|
|
||||||
checks: write
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Get Node version
|
- name: Get Node Version
|
||||||
id: retrieve-node-version
|
id: retrieve-node-version
|
||||||
run: |
|
run: |
|
||||||
NODE_NVMRC=$(cat .nvmrc)
|
NODE_NVMRC=$(cat .nvmrc)
|
||||||
@@ -54,6 +36,11 @@ jobs:
|
|||||||
cache-dependency-path: '**/package-lock.json'
|
cache-dependency-path: '**/package-lock.json'
|
||||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||||
|
|
||||||
|
- name: Print environment
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
- name: Install Node dependencies
|
- name: Install Node dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
@@ -64,25 +51,4 @@ jobs:
|
|||||||
run: npm run test:types --coverage
|
run: npm run test:types --coverage
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: npm run test --coverage
|
run: npm run test
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
|
|||||||
157
.github/workflows/version-bump.yml
vendored
157
.github/workflows/version-bump.yml
vendored
@@ -1,42 +1,24 @@
|
|||||||
---
|
---
|
||||||
name: Version Bump
|
name: Version Bump
|
||||||
|
run-name: Version Bump - v${{ inputs.version_number }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version_number_override:
|
version_number:
|
||||||
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
description: "New version (example: '2024.1.0')"
|
||||||
required: false
|
required: true
|
||||||
type: string
|
type: string
|
||||||
enable_slack_notification:
|
cut_rc_branch:
|
||||||
description: "Enable Slack notifications for upcoming release?"
|
description: "Cut RC branch?"
|
||||||
default: false
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
name: Bump Version
|
name: "Bump Version to v${{ inputs.version_number }}"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
|
||||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Validate version input
|
|
||||||
if: ${{ inputs.version_number_override != '' }}
|
|
||||||
uses: bitwarden/gh-actions/version-check@main
|
|
||||||
with:
|
|
||||||
version: ${{ inputs.version_number_override }}
|
|
||||||
|
|
||||||
- name: Slack Notification Check
|
|
||||||
run: |
|
|
||||||
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
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
with:
|
with:
|
||||||
@@ -48,7 +30,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-gpg-private-key,
|
secrets: "github-gpg-private-key,
|
||||||
github-gpg-private-key-passphrase"
|
github-gpg-private-key-passphrase,
|
||||||
|
github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
||||||
@@ -58,30 +56,19 @@ jobs:
|
|||||||
git_user_signingkey: true
|
git_user_signingkey: true
|
||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
- name: Setup git
|
|
||||||
run: |
|
|
||||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
|
||||||
git config --local user.name "bitwarden-devops-bot"
|
|
||||||
|
|
||||||
- name: Create Version Branch
|
- name: Create Version Branch
|
||||||
id: create-branch
|
id: create-branch
|
||||||
run: |
|
run: |
|
||||||
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||||
git switch -c $NAME
|
git switch -c $NAME
|
||||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Get current version
|
- name: Verify input version
|
||||||
id: current-version
|
env:
|
||||||
|
NEW_VERSION: ${{ inputs.version_number }}
|
||||||
run: |
|
run: |
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Verify input version
|
|
||||||
if: ${{ inputs.version_number_override != '' }}
|
|
||||||
env:
|
|
||||||
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
|
|
||||||
NEW_VERSION: ${{ inputs.version_number_override }}
|
|
||||||
run: |
|
|
||||||
# Error if version has not changed.
|
# Error if version has not changed.
|
||||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||||
echo "Version has not changed."
|
echo "Version has not changed."
|
||||||
@@ -97,37 +84,16 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Calculate next release version
|
- name: Bump Version - Package
|
||||||
if: ${{ inputs.version_number_override == '' }}
|
|
||||||
id: calculate-next-version
|
|
||||||
uses: bitwarden/gh-actions/version-next@main
|
|
||||||
with:
|
|
||||||
version: ${{ steps.current-version.outputs.version }}
|
|
||||||
|
|
||||||
- name: Bump Version - Package - Version Override
|
|
||||||
if: ${{ inputs.version_number_override != '' }}
|
|
||||||
id: bump-version-override
|
|
||||||
uses: bitwarden/gh-actions/version-bump@main
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
|
version: ${{ inputs.version_number }}
|
||||||
file_path: "./package.json"
|
file_path: "./package.json"
|
||||||
version: ${{ inputs.version_number_override }}
|
|
||||||
|
|
||||||
- name: Bump Version - Package - Automatic Calculation
|
- name: Setup git
|
||||||
if: ${{ inputs.version_number_override == '' }}
|
|
||||||
id: bump-version-automatic
|
|
||||||
uses: bitwarden/gh-actions/version-bump@main
|
|
||||||
with:
|
|
||||||
file_path: "./package.json"
|
|
||||||
version: ${{ steps.calculate-next-version.outputs.version }}
|
|
||||||
|
|
||||||
- name: Set final version output
|
|
||||||
id: set-final-version-output
|
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ steps.bump-version-override.outcome }}" == "success" ]]; then
|
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||||
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
git config --local user.name "bitwarden-devops-bot"
|
||||||
elif [[ "${{ steps.bump-version-automatic.outcome }}" == "success" ]]; then
|
|
||||||
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check if version changed
|
- name: Check if version changed
|
||||||
id: version-changed
|
id: version-changed
|
||||||
@@ -141,7 +107,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit files
|
- name: Commit files
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
@@ -149,21 +115,13 @@ jobs:
|
|||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
run: git push -u origin $PR_BRANCH
|
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
|
- name: Create Version PR
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
id: create-pr
|
id: create-pr
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
TITLE: "Bump version to ${{ inputs.version_number }}"
|
||||||
run: |
|
run: |
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
PR_URL=$(gh pr create --title "$TITLE" \
|
||||||
--base "main" \
|
--base "main" \
|
||||||
@@ -179,27 +137,48 @@ jobs:
|
|||||||
- [X] Other
|
- [X] Other
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
Automated version bump to ${{ inputs.version_number }}")
|
||||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Approve PR
|
- name: Approve PR
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr review $PR_NUMBER --approve
|
run: gh pr review $PR_NUMBER --approve
|
||||||
|
|
||||||
- name: Merge PR
|
- name: Merge PR
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||||
|
|
||||||
- name: Report upcoming release version to Slack
|
cut_rc:
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && inputs.enable_slack_notification == true }}
|
name: Cut RC branch
|
||||||
uses: bitwarden/gh-actions/report-upcoming-release-version@main
|
needs: bump_version
|
||||||
|
if: ${{ inputs.cut_rc_branch == true }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
ref: main
|
||||||
project: ${{ github.repository }}
|
|
||||||
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
- name: Verify version has been updated
|
||||||
|
env:
|
||||||
|
NEW_VERSION: ${{ inputs.version_number }}
|
||||||
|
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
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,7 +26,6 @@ npm-debug.log
|
|||||||
# Build directories
|
# Build directories
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
build-cli
|
|
||||||
.angular/cache
|
.angular/cache
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
|
|||||||
@@ -11,14 +11,6 @@ module.exports = {
|
|||||||
// ...angularPreset,
|
// ...angularPreset,
|
||||||
preset: "jest-preset-angular",
|
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",
|
testEnvironment: "jsdom",
|
||||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
|
|
||||||
|
|||||||
45
jslib/angular/src/services/auth-guard.service.ts
Normal file
45
jslib/angular/src/services/auth-guard.service.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +1,188 @@
|
|||||||
import { LOCALE_ID, NgModule } from "@angular/core";
|
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.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 { 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 { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.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 { 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 { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.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 { 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 { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
|
||||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||||
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.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 { 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 { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||||
import { Account } from "@/jslib/common/src/models/domain/account";
|
import { Account } from "@/jslib/common/src/models/domain/account";
|
||||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||||
import { ApiService } from "@/jslib/common/src/services/api.service";
|
import { ApiService } from "@/jslib/common/src/services/api.service";
|
||||||
import { AppIdService } from "@/jslib/common/src/services/appId.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 { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
|
||||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.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 { StateService } from "@/jslib/common/src/services/state.service";
|
||||||
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.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 { 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 {
|
import { AuthGuardService } from "./auth-guard.service";
|
||||||
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 { BroadcasterService } from "./broadcaster.service";
|
||||||
|
import { LockGuardService } from "./lock-guard.service";
|
||||||
import { ModalService } from "./modal.service";
|
import { ModalService } from "./modal.service";
|
||||||
|
import { PasswordRepromptService } from "./passwordReprompt.service";
|
||||||
|
import { UnauthGuardService } from "./unauth-guard.service";
|
||||||
import { ValidationService } from "./validation.service";
|
import { ValidationService } from "./validation.service";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
providers: [
|
providers: [
|
||||||
safeProvider({ provide: WINDOW, useValue: window }),
|
{ provide: "WINDOW", useValue: window },
|
||||||
safeProvider({
|
{
|
||||||
provide: LOCALE_ID as SafeInjectionToken<string>,
|
provide: LOCALE_ID,
|
||||||
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
|
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
|
||||||
deps: [I18nServiceAbstraction],
|
deps: [I18nServiceAbstraction],
|
||||||
}),
|
},
|
||||||
safeProvider(ValidationService),
|
ValidationService,
|
||||||
safeProvider(ModalService),
|
AuthGuardService,
|
||||||
safeProvider({
|
UnauthGuardService,
|
||||||
|
LockGuardService,
|
||||||
|
ModalService,
|
||||||
|
{
|
||||||
provide: AppIdServiceAbstraction,
|
provide: AppIdServiceAbstraction,
|
||||||
useClass: AppIdService,
|
useClass: AppIdService,
|
||||||
deps: [StorageServiceAbstraction],
|
deps: [StorageServiceAbstraction],
|
||||||
}),
|
},
|
||||||
safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }),
|
{
|
||||||
safeProvider({
|
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],
|
||||||
|
},
|
||||||
|
{
|
||||||
provide: EnvironmentServiceAbstraction,
|
provide: EnvironmentServiceAbstraction,
|
||||||
useClass: EnvironmentService,
|
useClass: EnvironmentService,
|
||||||
deps: [StateServiceAbstraction],
|
deps: [StateServiceAbstraction],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
provide: TokenServiceAbstraction,
|
provide: TotpServiceAbstraction,
|
||||||
useClass: TokenService,
|
useClass: TotpService,
|
||||||
deps: [StateServiceAbstraction],
|
deps: [CryptoFunctionServiceAbstraction, LogService, StateServiceAbstraction],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] },
|
||||||
|
{
|
||||||
provide: CryptoServiceAbstraction,
|
provide: CryptoServiceAbstraction,
|
||||||
useClass: CryptoService,
|
useClass: CryptoService,
|
||||||
deps: [
|
deps: [
|
||||||
@@ -73,8 +191,18 @@ import { ValidationService } from "./validation.service";
|
|||||||
LogService,
|
LogService,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
|
provide: PasswordGenerationServiceAbstraction,
|
||||||
|
useClass: PasswordGenerationService,
|
||||||
|
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: UsernameGenerationServiceAbstraction,
|
||||||
|
useClass: UsernameGenerationService,
|
||||||
|
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
|
||||||
|
},
|
||||||
|
{
|
||||||
provide: ApiServiceAbstraction,
|
provide: ApiServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
tokenService: TokenServiceAbstraction,
|
tokenService: TokenServiceAbstraction,
|
||||||
@@ -97,13 +225,116 @@ import { ValidationService } from "./validation.service";
|
|||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
provide: BroadcasterServiceAbstraction,
|
provide: FileUploadServiceAbstraction,
|
||||||
useClass: BroadcasterService,
|
useClass: FileUploadService,
|
||||||
useAngularDecorators: true,
|
deps: [LogService, ApiServiceAbstraction],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: StorageServiceAbstraction,
|
storageService: StorageServiceAbstraction,
|
||||||
@@ -120,12 +351,12 @@ import { ValidationService } from "./validation.service";
|
|||||||
),
|
),
|
||||||
deps: [
|
deps: [
|
||||||
StorageServiceAbstraction,
|
StorageServiceAbstraction,
|
||||||
SECURE_STORAGE,
|
"SECURE_STORAGE",
|
||||||
LogService,
|
LogService,
|
||||||
StateMigrationServiceAbstraction,
|
StateMigrationServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
provide: StateMigrationServiceAbstraction,
|
provide: StateMigrationServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: StorageServiceAbstraction,
|
storageService: StorageServiceAbstraction,
|
||||||
@@ -136,8 +367,108 @@ import { ValidationService } from "./validation.service";
|
|||||||
secureStorageService,
|
secureStorageService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
),
|
),
|
||||||
deps: [StorageServiceAbstraction, SECURE_STORAGE],
|
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
||||||
}),
|
},
|
||||||
] satisfies SafeProvider[],
|
{
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
|||||||
29
jslib/angular/src/services/lock-guard.service.ts
Normal file
29
jslib/angular/src/services/lock-guard.service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,7 +143,7 @@ export class ModalService {
|
|||||||
dialogEl.style.zIndex = `${this.modalCount}050`;
|
dialogEl.style.zIndex = `${this.modalCount}050`;
|
||||||
|
|
||||||
const modals = Array.from(
|
const modals = Array.from(
|
||||||
el.querySelectorAll('.modal-backdrop, .modal *[data-bs-dismiss="modal"]'),
|
el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]'),
|
||||||
);
|
);
|
||||||
for (const closeElement of modals) {
|
for (const closeElement of modals) {
|
||||||
closeElement.addEventListener("click", () => {
|
closeElement.addEventListener("click", () => {
|
||||||
|
|||||||
45
jslib/angular/src/services/passwordReprompt.service.ts
Normal file
45
jslib/angular/src/services/passwordReprompt.service.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
29
jslib/angular/src/services/unauth-guard.service.ts
Normal file
29
jslib/angular/src/services/unauth-guard.service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
jslib/common/spec/misc/logInStrategies/apiLogIn.strategy.spec.ts
Normal file
114
jslib/common/spec/misc/logInStrategies/apiLogIn.strategy.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
288
jslib/common/spec/misc/logInStrategies/logIn.strategy.spec.ts
Normal file
288
jslib/common/spec/misc/logInStrategies/logIn.strategy.spec.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
127
jslib/common/spec/misc/logInStrategies/ssoLogIn.strategy.spec.ts
Normal file
127
jslib/common/spec/misc/logInStrategies/ssoLogIn.strategy.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
69
jslib/common/spec/services/cipher.service.spec.ts
Normal file
69
jslib/common/spec/services/cipher.service.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
6
jslib/common/src/abstractions/audit.service.ts
Normal file
6
jslib/common/src/abstractions/audit.service.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
|
||||||
|
|
||||||
|
export abstract class AuditService {
|
||||||
|
passwordLeaked: (password: string) => Promise<number>;
|
||||||
|
breachedAccounts: (username: string) => Promise<BreachAccountResponse[]>;
|
||||||
|
}
|
||||||
25
jslib/common/src/abstractions/auth.service.ts
Normal file
25
jslib/common/src/abstractions/auth.service.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
79
jslib/common/src/abstractions/cipher.service.ts
Normal file
79
jslib/common/src/abstractions/cipher.service.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
19
jslib/common/src/abstractions/collection.service.ts
Normal file
19
jslib/common/src/abstractions/collection.service.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
7
jslib/common/src/abstractions/event.service.ts
Normal file
7
jslib/common/src/abstractions/event.service.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
18
jslib/common/src/abstractions/fileUpload.service.ts
Normal file
18
jslib/common/src/abstractions/fileUpload.service.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
21
jslib/common/src/abstractions/folder.service.ts
Normal file
21
jslib/common/src/abstractions/folder.service.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
19
jslib/common/src/abstractions/keyConnector.service.ts
Normal file
19
jslib/common/src/abstractions/keyConnector.service.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
6
jslib/common/src/abstractions/notifications.service.ts
Normal file
6
jslib/common/src/abstractions/notifications.service.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export abstract class NotificationsService {
|
||||||
|
init: () => Promise<void>;
|
||||||
|
updateConnection: (sync?: boolean) => Promise<void>;
|
||||||
|
reconnectFromActivity: () => Promise<void>;
|
||||||
|
disconnectFromInactivity: () => Promise<void>;
|
||||||
|
}
|
||||||
11
jslib/common/src/abstractions/organization.service.ts
Normal file
11
jslib/common/src/abstractions/organization.service.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
20
jslib/common/src/abstractions/passwordGeneration.service.ts
Normal file
20
jslib/common/src/abstractions/passwordGeneration.service.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export abstract class PasswordRepromptService {
|
||||||
|
protectedFields: () => string[];
|
||||||
|
showPasswordPrompt: () => Promise<boolean>;
|
||||||
|
enabled: () => Promise<boolean>;
|
||||||
|
}
|
||||||
32
jslib/common/src/abstractions/policy.service.ts
Normal file
32
jslib/common/src/abstractions/policy.service.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
8
jslib/common/src/abstractions/provider.service.ts
Normal file
8
jslib/common/src/abstractions/provider.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
16
jslib/common/src/abstractions/search.service.ts
Normal file
16
jslib/common/src/abstractions/search.service.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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[];
|
||||||
|
}
|
||||||
25
jslib/common/src/abstractions/send.service.ts
Normal file
25
jslib/common/src/abstractions/send.service.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
6
jslib/common/src/abstractions/settings.service.ts
Normal file
6
jslib/common/src/abstractions/settings.service.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export abstract class SettingsService {
|
||||||
|
clearCache: () => Promise<void>;
|
||||||
|
getEquivalentDomains: () => Promise<any>;
|
||||||
|
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
|
||||||
|
clear: (userId?: string) => Promise<void>;
|
||||||
|
}
|
||||||
@@ -210,6 +210,9 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
getEncryptedSends: (options?: StorageOptions) => Promise<{ [id: string]: SendData }>;
|
getEncryptedSends: (options?: StorageOptions) => Promise<{ [id: string]: SendData }>;
|
||||||
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
|
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
|
||||||
getEntityId: (options?: StorageOptions) => Promise<string>;
|
getEntityId: (options?: StorageOptions) => Promise<string>;
|
||||||
|
setEntityId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getEntityType: (options?: StorageOptions) => Promise<any>;
|
||||||
|
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
|
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
|
||||||
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
|
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
|
||||||
getEquivalentDomains: (options?: StorageOptions) => Promise<any>;
|
getEquivalentDomains: (options?: StorageOptions) => Promise<any>;
|
||||||
|
|||||||
19
jslib/common/src/abstractions/sync.service.ts
Normal file
19
jslib/common/src/abstractions/sync.service.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
6
jslib/common/src/abstractions/system.service.ts
Normal file
6
jslib/common/src/abstractions/system.service.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export abstract class SystemService {
|
||||||
|
startProcessReload: () => Promise<void>;
|
||||||
|
cancelProcessReload: () => void;
|
||||||
|
clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise<void>;
|
||||||
|
clearPendingClipboard: () => Promise<any>;
|
||||||
|
}
|
||||||
5
jslib/common/src/abstractions/totp.service.ts
Normal file
5
jslib/common/src/abstractions/totp.service.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export abstract class TotpService {
|
||||||
|
getCode: (key: string) => Promise<string>;
|
||||||
|
getTimeInterval: (key: string) => number;
|
||||||
|
isAutoCopyEnabled: () => Promise<boolean>;
|
||||||
|
}
|
||||||
23
jslib/common/src/abstractions/twoFactor.service.ts
Normal file
23
jslib/common/src/abstractions/twoFactor.service.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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 }>;
|
||||||
|
}
|
||||||
12
jslib/common/src/abstractions/userVerification.service.ts
Normal file
12
jslib/common/src/abstractions/userVerification.service.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
11
jslib/common/src/abstractions/vaultTimeout.service.ts
Normal file
11
jslib/common/src/abstractions/vaultTimeout.service.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
5
jslib/common/src/enums/authenticationType.ts
Normal file
5
jslib/common/src/enums/authenticationType.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum AuthenticationType {
|
||||||
|
Password = 0,
|
||||||
|
Sso = 1,
|
||||||
|
Api = 2,
|
||||||
|
}
|
||||||
70
jslib/common/src/misc/logInStrategies/apiLogin.strategy.ts
Normal file
70
jslib/common/src/misc/logInStrategies/apiLogin.strategy.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
170
jslib/common/src/misc/logInStrategies/logIn.strategy.ts
Normal file
170
jslib/common/src/misc/logInStrategies/logIn.strategy.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
70
jslib/common/src/misc/logInStrategies/ssoLogin.strategy.ts
Normal file
70
jslib/common/src/misc/logInStrategies/ssoLogin.strategy.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { I18nService } from "../abstractions/i18n.service";
|
|||||||
|
|
||||||
import * as tldjs from "tldjs";
|
import * as tldjs from "tldjs";
|
||||||
|
|
||||||
|
|
||||||
const nodeURL = typeof window === "undefined" ? require("url") : null;
|
const nodeURL = typeof window === "undefined" ? require("url") : null;
|
||||||
|
|
||||||
export class Utils {
|
export class Utils {
|
||||||
|
|||||||
17
jslib/common/src/models/domain/authResult.ts
Normal file
17
jslib/common/src/models/domain/authResult.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
jslib/common/src/models/domain/logInCredentials.ts
Normal file
34
jslib/common/src/models/domain/logInCredentials.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
44
jslib/common/src/services/audit.service.ts
Normal file
44
jslib/common/src/services/audit.service.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
jslib/common/src/services/auth.service.ts
Normal file
198
jslib/common/src/services/auth.service.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
jslib/common/src/services/azureFileUpload.service.ts
Normal file
214
jslib/common/src/services/azureFileUpload.service.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
jslib/common/src/services/bitwardenFileUpload.service.ts
Normal file
34
jslib/common/src/services/bitwardenFileUpload.service.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1282
jslib/common/src/services/cipher.service.ts
Normal file
1282
jslib/common/src/services/cipher.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
157
jslib/common/src/services/collection.service.ts
Normal file
157
jslib/common/src/services/collection.service.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
jslib/common/src/services/event.service.ts
Normal file
99
jslib/common/src/services/event.service.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
111
jslib/common/src/services/fileUpload.service.ts
Normal file
111
jslib/common/src/services/fileUpload.service.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
194
jslib/common/src/services/folder.service.ts
Normal file
194
jslib/common/src/services/folder.service.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
134
jslib/common/src/services/keyConnector.service.ts
Normal file
134
jslib/common/src/services/keyConnector.service.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
231
jslib/common/src/services/notifications.service.ts
Normal file
231
jslib/common/src/services/notifications.service.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
jslib/common/src/services/organization.service.ts
Normal file
55
jslib/common/src/services/organization.service.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
572
jslib/common/src/services/passwordGeneration.service.ts
Normal file
572
jslib/common/src/services/passwordGeneration.service.ts
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
247
jslib/common/src/services/policy.service.ts
Normal file
247
jslib/common/src/services/policy.service.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
jslib/common/src/services/provider.service.ts
Normal file
34
jslib/common/src/services/provider.service.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service";
|
||||||
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
import { ProviderData } from "../models/data/providerData";
|
||||||
|
import { Provider } from "../models/domain/provider";
|
||||||
|
|
||||||
|
export class ProviderService implements ProviderServiceAbstraction {
|
||||||
|
constructor(private stateService: StateService) {}
|
||||||
|
|
||||||
|
async get(id: string): Promise<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
284
jslib/common/src/services/search.service.ts
Normal file
284
jslib/common/src/services/search.service.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
297
jslib/common/src/services/send.service.ts
Normal file
297
jslib/common/src/services/send.service.ts
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
56
jslib/common/src/services/settings.service.ts
Normal file
56
jslib/common/src/services/settings.service.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1355,6 +1355,34 @@ export class StateService<
|
|||||||
)?.profile?.entityId;
|
)?.profile?.entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setEntityId(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||||
|
);
|
||||||
|
account.profile.entityId = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntityType(options?: StorageOptions): Promise<any> {
|
||||||
|
return (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
)?.profile?.entityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setEntityType(value: string, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||||
|
);
|
||||||
|
account.profile.entityType = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
||||||
if (this.state.activeUserId == null) {
|
if (this.state.activeUserId == null) {
|
||||||
return await this.getGlobalEnvironmentUrls(options);
|
return await this.getGlobalEnvironmentUrls(options);
|
||||||
@@ -2172,11 +2200,11 @@ export class StateService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const account = options?.useSecureStorage
|
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>(
|
(await this.storageService.get<TAccount>(
|
||||||
options.userId,
|
options.userId,
|
||||||
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }),
|
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }),
|
||||||
)))
|
))
|
||||||
: await this.storageService.get<TAccount>(options.userId, options);
|
: await this.storageService.get<TAccount>(options.userId, options);
|
||||||
|
|
||||||
if (this.useAccountCache) {
|
if (this.useAccountCache) {
|
||||||
|
|||||||
400
jslib/common/src/services/sync.service.ts
Normal file
400
jslib/common/src/services/sync.service.ts
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
jslib/common/src/services/system.service.ts
Normal file
90
jslib/common/src/services/system.service.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
jslib/common/src/services/totp.service.ts
Normal file
174
jslib/common/src/services/totp.service.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
186
jslib/common/src/services/twoFactor.service.ts
Normal file
186
jslib/common/src/services/twoFactor.service.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
jslib/common/src/services/userVerification.service.ts
Normal file
88
jslib/common/src/services/userVerification.service.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
jslib/common/src/services/usernameGeneration.service.ts
Normal file
131
jslib/common/src/services/usernameGeneration.service.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
225
jslib/common/src/services/vaultTimeout.service.ts
Normal file
225
jslib/common/src/services/vaultTimeout.service.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
jslib/electron/src/services/electronCrypto.service.ts
Normal file
71
jslib/electron/src/services/electronCrypto.service.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ export class ElectronLogService extends BaseLogService {
|
|||||||
super(isDev(), filter);
|
super(isDev(), filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init () {
|
||||||
if (log.transports == null) {
|
if (log.transports == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class WindowMain {
|
|||||||
win: BrowserWindow;
|
win: BrowserWindow;
|
||||||
isQuitting = false;
|
isQuitting = false;
|
||||||
|
|
||||||
private windowStateChangeTimer: NodeJS.Timeout;
|
private windowStateChangeTimer: NodeJS.Timer;
|
||||||
private windowStates: { [key: string]: any } = {};
|
private windowStates: { [key: string]: any } = {};
|
||||||
private enableAlwaysOnTop = false;
|
private enableAlwaysOnTop = false;
|
||||||
|
|
||||||
|
|||||||
624
jslib/node/src/cli/commands/login.command.ts
Normal file
624
jslib/node/src/cli/commands/login.command.ts
Normal file
@@ -0,0 +1,624 @@
|
|||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
||||||
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
|
|
||||||
import { Response } from "@/jslib/node/src/cli/models/response";
|
import { Response } from "../models/response";
|
||||||
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
|
import { MessageResponse } from "../models/response/messageResponse";
|
||||||
|
|
||||||
import { AuthService } from "../abstractions/auth.service";
|
|
||||||
|
|
||||||
export class LogoutCommand {
|
export class LogoutCommand {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private i18nService: I18nService,
|
||||||
private logoutCallback: () => Promise<void>,
|
private logoutCallback: () => Promise<void>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import * as child_process from "child_process";
|
||||||
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { ClientType } from "@/jslib/common/src/enums/clientType";
|
import { ClientType } from "@/jslib/common/src/enums/clientType";
|
||||||
import { DeviceType } from "@/jslib/common/src/enums/deviceType";
|
import { DeviceType } from "@/jslib/common/src/enums/deviceType";
|
||||||
import { ThemeType } from "@/jslib/common/src/enums/themeType";
|
import { ThemeType } from "@/jslib/common/src/enums/themeType";
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const open = require("open");
|
||||||
|
|
||||||
export class CliPlatformUtilsService implements PlatformUtilsService {
|
export class CliPlatformUtilsService implements PlatformUtilsService {
|
||||||
clientType: ClientType;
|
clientType: ClientType;
|
||||||
|
|
||||||
@@ -75,8 +80,12 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
launchUri(_uri: string, _options?: any): void {
|
launchUri(uri: string, options?: any): void {
|
||||||
throw new Error("Not implemented.");
|
if (process.platform === "linux") {
|
||||||
|
child_process.spawnSync("xdg-open", [uri]);
|
||||||
|
} else {
|
||||||
|
open(uri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
|
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
|
||||||
|
|||||||
2596
package-lock.json
generated
2596
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "@bitwarden/directory-connector",
|
"name": "@bitwarden/directory-connector",
|
||||||
"productName": "Bitwarden Directory Connector",
|
"productName": "Bitwarden Directory Connector",
|
||||||
"description": "Sync your user directory to your Bitwarden organization.",
|
"description": "Sync your user directory to your Bitwarden organization.",
|
||||||
"version": "2024.9.0",
|
"version": "2024.2.2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bitwarden",
|
"bitwarden",
|
||||||
"password",
|
"password",
|
||||||
@@ -70,76 +70,75 @@
|
|||||||
"test:types": "npx tsc --noEmit"
|
"test:types": "npx tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "16.2.12",
|
|
||||||
"@angular-eslint/eslint-plugin-template": "17.2.0",
|
"@angular-eslint/eslint-plugin-template": "17.2.0",
|
||||||
"@angular-eslint/template-parser": "17.2.0",
|
"@angular-eslint/template-parser": "17.2.0",
|
||||||
"@angular/compiler-cli": "16.2.12",
|
"@angular/compiler-cli": "16.2.12",
|
||||||
"@electron/notarize": "2.2.1",
|
|
||||||
"@electron/rebuild": "3.6.0",
|
|
||||||
"@fluffy-spoon/substitute": "1.208.0",
|
"@fluffy-spoon/substitute": "1.208.0",
|
||||||
"@microsoft/microsoft-graph-types": "2.40.0",
|
"@microsoft/microsoft-graph-types": "2.40.0",
|
||||||
"@ngtools/webpack": "16.2.12",
|
"@ngtools/webpack": "16.2.12",
|
||||||
"@types/inquirer": "8.2.10",
|
"@types/inquirer": "8.2.6",
|
||||||
"@types/jest": "29.5.11",
|
"@types/jest": "29.5.11",
|
||||||
|
"@types/ldapjs": "2.2.5",
|
||||||
"@types/lowdb": "1.0.15",
|
"@types/lowdb": "1.0.15",
|
||||||
"@types/node": "18.19.50",
|
"@types/lunr": "2.3.7",
|
||||||
"@types/node-fetch": "2.6.11",
|
"@types/node": "18.17.12",
|
||||||
|
"@types/node-fetch": "2.6.10",
|
||||||
"@types/node-forge": "1.3.11",
|
"@types/node-forge": "1.3.11",
|
||||||
"@types/proper-lockfile": "4.1.4",
|
"@types/proper-lockfile": "4.1.4",
|
||||||
"@types/tldjs": "2.3.4",
|
"@types/tldjs": "2.3.4",
|
||||||
|
"@types/zxcvbn": "4.4.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||||
"@typescript-eslint/parser": "5.62.0",
|
"@typescript-eslint/parser": "5.62.0",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"concurrently": "9.0.1",
|
"concurrently": "8.2.2",
|
||||||
"copy-webpack-plugin": "12.0.2",
|
"copy-webpack-plugin": "12.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "6.11.0",
|
"css-loader": "6.9.0",
|
||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.1",
|
||||||
"electron": "28.3.3",
|
"electron": "28.2.0",
|
||||||
"electron-builder": "24.9.1",
|
"electron-builder": "24.9.1",
|
||||||
"electron-log": "5.2.0",
|
"electron-log": "5.0.1",
|
||||||
|
"@electron/notarize": "2.2.1",
|
||||||
|
"@electron/rebuild": "3.6.0",
|
||||||
"electron-reload": "2.0.0-alpha.1",
|
"electron-reload": "2.0.0-alpha.1",
|
||||||
"electron-store": "8.2.0",
|
"electron-store": "8.1.0",
|
||||||
"electron-updater": "6.3.0",
|
"electron-updater": "6.1.7",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-import-resolver-typescript": "3.6.3",
|
"eslint-import-resolver-typescript": "3.6.1",
|
||||||
"eslint-plugin-import": "2.30.0",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-rxjs": "5.0.3",
|
"eslint-plugin-rxjs": "5.0.3",
|
||||||
"eslint-plugin-rxjs-angular": "2.0.1",
|
"eslint-plugin-rxjs-angular": "2.0.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"html-loader": "5.1.0",
|
"html-loader": "5.0.0",
|
||||||
"html-webpack-plugin": "5.6.0",
|
"html-webpack-plugin": "5.6.0",
|
||||||
"husky": "9.0.10",
|
"husky": "9.0.10",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-junit": "16.0.0",
|
"jest-preset-angular": "13.1.1",
|
||||||
"jest-mock-extended": "3.0.7",
|
"lint-staged": "15.2.0",
|
||||||
"jest-preset-angular": "13.1.6",
|
"mini-css-extract-plugin": "2.7.7",
|
||||||
"lint-staged": "15.2.10",
|
|
||||||
"mini-css-extract-plugin": "2.9.1",
|
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"node-loader": "2.0.0",
|
"node-loader": "2.0.0",
|
||||||
"pkg": "5.8.1",
|
"pkg": "5.8.1",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.2.2",
|
||||||
"rimraf": "5.0.10",
|
"rimraf": "5.0.5",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"sass": "1.78.0",
|
"sass": "1.69.7",
|
||||||
"sass-loader": "16.0.1",
|
"sass-loader": "14.0.0",
|
||||||
"ts-jest": "29.2.5",
|
"ts-jest": "29.1.1",
|
||||||
"ts-loader": "9.5.1",
|
"ts-loader": "9.5.1",
|
||||||
"tsconfig-paths-webpack-plugin": "4.1.0",
|
"tsconfig-paths-webpack-plugin": "4.1.0",
|
||||||
"type-fest": "3.13.1",
|
|
||||||
"typescript": "4.9.5",
|
"typescript": "4.9.5",
|
||||||
"typescript-transform-paths": "3.5.1",
|
"typescript-transform-paths": "3.4.6",
|
||||||
"webpack": "5.94.0",
|
"webpack": "5.89.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-merge": "6.0.1",
|
"webpack-merge": "5.10.0",
|
||||||
"webpack-node-externals": "3.0.0",
|
"webpack-node-externals": "3.0.0",
|
||||||
"zone.js": "0.13.1"
|
"zone.js": "0.13.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "16.2.12",
|
"@angular/animations": "16.2.12",
|
||||||
"@angular/cdk": "16.2.14",
|
"@angular/cdk": "16.2.12",
|
||||||
"@angular/common": "16.2.12",
|
"@angular/common": "16.2.12",
|
||||||
"@angular/compiler": "16.2.12",
|
"@angular/compiler": "16.2.12",
|
||||||
"@angular/core": "16.2.12",
|
"@angular/core": "16.2.12",
|
||||||
@@ -148,11 +147,13 @@
|
|||||||
"@angular/platform-browser-dynamic": "16.2.12",
|
"@angular/platform-browser-dynamic": "16.2.12",
|
||||||
"@angular/router": "16.2.12",
|
"@angular/router": "16.2.12",
|
||||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||||
|
"@microsoft/signalr": "7.0.10",
|
||||||
|
"@microsoft/signalr-protocol-msgpack": "7.0.10",
|
||||||
"big-integer": "1.6.52",
|
"big-integer": "1.6.52",
|
||||||
"bootstrap": "5.0.0",
|
"bootstrap": "4.6.2",
|
||||||
"browser-hrtime": "1.1.8",
|
"browser-hrtime": "1.1.8",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"commander": "12.1.0",
|
"commander": "12.0.0",
|
||||||
"core-js": "3.35.0",
|
"core-js": "3.35.0",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"google-auth-library": "7.14.1",
|
"google-auth-library": "7.14.1",
|
||||||
@@ -160,18 +161,21 @@
|
|||||||
"https-proxy-agent": "7.0.4",
|
"https-proxy-agent": "7.0.4",
|
||||||
"inquirer": "8.2.6",
|
"inquirer": "8.2.6",
|
||||||
"keytar": "7.9.0",
|
"keytar": "7.9.0",
|
||||||
"ldapts": "7.2.0",
|
"ldapjs": "2.3.3",
|
||||||
"lowdb": "1.0.0",
|
"lowdb": "1.0.0",
|
||||||
|
"lunr": "2.3.9",
|
||||||
"ngx-toastr": "16.2.0",
|
"ngx-toastr": "16.2.0",
|
||||||
"node-fetch": "2.7.0",
|
"node-fetch": "2.7.0",
|
||||||
|
"open": "8.4.2",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"tldjs": "2.3.1",
|
"tldjs": "2.3.1",
|
||||||
"zone.js": "0.13.1"
|
"zone.js": "0.13.1",
|
||||||
|
"zxcvbn": "4.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~18",
|
"node": "~18",
|
||||||
"npm": "~10"
|
"npm": "~10.4.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"./!(jslib)**": "prettier --ignore-unknown --write",
|
"./!(jslib)**": "prettier --ignore-unknown --write",
|
||||||
|
|||||||
@@ -7,23 +7,9 @@ exports.default = async function notarizing(context) {
|
|||||||
if (electronPlatformName !== "darwin") {
|
if (electronPlatformName !== "darwin") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appName = context.packager.appInfo.productFilename;
|
|
||||||
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 appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
|
||||||
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
|
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
return await notarize({
|
return await notarize({
|
||||||
tool: "notarytool",
|
tool: "notarytool",
|
||||||
appBundleId: "com.bitwarden.directory-connector",
|
appBundleId: "com.bitwarden.directory-connector",
|
||||||
@@ -32,5 +18,4 @@ exports.default = async function notarizing(context) {
|
|||||||
appleId: appleId,
|
appleId: appleId,
|
||||||
appleIdPassword: appleIdPassword,
|
appleIdPassword: appleIdPassword,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export abstract class AuthService {
|
|
||||||
logIn: (credentials: { clientId: string; clientSecret: string }) => Promise<void>;
|
|
||||||
logOut: (callback: () => void) => void;
|
|
||||||
}
|
|
||||||
@@ -2,17 +2,18 @@
|
|||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8 col-lg-6">
|
<div class="col-md-8 col-lg-6">
|
||||||
<p class="text-center fw-bold">{{ "welcome" | i18n }}</p>
|
<p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
|
||||||
<p class="text-center">{{ "logInDesc" | i18n }}</p>
|
<p class="text-center">{{ "logInDesc" | i18n }}</p>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
|
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="client_id" class="form-label">{{ "clientId" | i18n }}</label>
|
<label for="client_id">{{ "clientId" | i18n }}</label>
|
||||||
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
|
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="client_secret" class="form-label">{{ "clientSecret" | i18n }}</label>
|
<div class="row-main">
|
||||||
|
<label for="client_secret">{{ "clientSecret" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="{{ showSecret ? 'text' : 'password' }}"
|
type="{{ showSecret ? 'text' : 'password' }}"
|
||||||
@@ -21,16 +22,23 @@
|
|||||||
[(ngModel)]="clientSecret"
|
[(ngModel)]="clientSecret"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
|
<div class="input-group-append">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary"
|
class="ml-1 btn btn-link"
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||||
(click)="toggleSecret()"
|
(click)="toggleSecret()"
|
||||||
>
|
>
|
||||||
<i class="bwi" [ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"></i>
|
<i
|
||||||
|
class="bwi bwi-lg"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||||
|
></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
|
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
|
||||||
@@ -39,7 +47,7 @@
|
|||||||
{{ "logIn" | i18n }}
|
{{ "logIn" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-link ms-auto" (click)="settings()">
|
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
|
||||||
{{ "settings" | i18n }}
|
{{ "settings" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { Router } from "@angular/router";
|
|||||||
import { takeUntil } from "rxjs";
|
import { takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ModalService } from "@/jslib/angular/src/services/modal.service";
|
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 { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
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 { StateService } from "../../abstractions/state.service";
|
||||||
|
|
||||||
import { EnvironmentComponent } from "./environment.component";
|
import { EnvironmentComponent } from "./environment.component";
|
||||||
@@ -80,10 +81,9 @@ export class ApiKeyComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.formPromise = this.authService.logIn({
|
this.formPromise = this.authService.logIn(
|
||||||
clientId: this.clientId,
|
new ApiLogInCredentials(this.clientId, this.clientSecret),
|
||||||
clientSecret: this.clientSecret,
|
);
|
||||||
});
|
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
const organizationId = await this.stateService.getEntityId();
|
const organizationId = await this.stateService.getEntityId();
|
||||||
await this.stateService.setOrganizationId(organizationId);
|
await this.stateService.setOrganizationId(organizationId);
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
<form class="modal-content" (ngSubmit)="submit()">
|
<form class="modal-content" (ngSubmit)="submit()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
|
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="close" data-dismiss="modal" title="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
|
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
|
||||||
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
|
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="baseUrl" class="form-label">{{ "baseUrl" | i18n }}</label>
|
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="baseUrl"
|
id="baseUrl"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -17,12 +19,14 @@
|
|||||||
[(ngModel)]="baseUrl"
|
[(ngModel)]="baseUrl"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
<div class="form-text">{{ "ex" | i18n }} https://bitwarden.company.com</div>
|
<small class="text-muted form-text"
|
||||||
|
>{{ "ex" | i18n }} https://bitwarden.company.com</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<h4>{{ "customEnvironment" | i18n }}</h4>
|
<h4>{{ "customEnvironment" | i18n }}</h4>
|
||||||
<p>{{ "customEnvironmentFooter" | i18n }}</p>
|
<p>{{ "customEnvironmentFooter" | i18n }}</p>
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="webVaultUrl" class="form-label">{{ "webVaultUrl" | i18n }}</label>
|
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="webVaultUrl"
|
id="webVaultUrl"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -31,12 +35,12 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="apiUrl" class="form-label">{{ "apiUrl" | i18n }}</label>
|
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
|
||||||
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
|
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="identityUrl" class="form-label">{{ "identityUrl" | i18n }}</label>
|
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="identityUrl"
|
id="identityUrl"
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { DomSanitizer } from "@angular/platform-browser";
|
|||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
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 { BroadcasterService } from "@/jslib/common/src/abstractions/broadcaster.service";
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
@@ -17,7 +18,6 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
|
|||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
||||||
|
|
||||||
import { AuthService } from "../abstractions/auth.service";
|
|
||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { SyncService } from "../services/sync.service";
|
import { SyncService } from "../services/sync.service";
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
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");
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -3,17 +3,20 @@ import { APP_INITIALIZER, NgModule } from "@angular/core";
|
|||||||
import { JslibServicesModule } from "@/jslib/angular/src/services/jslib-services.module";
|
import { JslibServicesModule } from "@/jslib/angular/src/services/jslib-services.module";
|
||||||
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.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 { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.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 { LogService as LogServiceAbstraction } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||||
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
||||||
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.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 { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||||
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||||
@@ -25,23 +28,21 @@ import { ElectronRendererStorageService } from "@/jslib/electron/src/services/el
|
|||||||
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
|
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
|
||||||
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.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 { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { AuthService } from "../../services/auth.service";
|
import { AuthService } from "../../services/auth.service";
|
||||||
import { I18nService } from "../../services/i18n.service";
|
import { I18nService } from "../../services/i18n.service";
|
||||||
|
import { NoopTwoFactorService } from "../../services/noop/noopTwoFactor.service";
|
||||||
import { StateService } from "../../services/state.service";
|
import { StateService } from "../../services/state.service";
|
||||||
import { StateMigrationService } from "../../services/stateMigration.service";
|
import { StateMigrationService } from "../../services/stateMigration.service";
|
||||||
import { SyncService } from "../../services/sync.service";
|
import { SyncService } from "../../services/sync.service";
|
||||||
|
|
||||||
import { AuthGuardService } from "./auth-guard.service";
|
import { AuthGuardService } from "./auth-guard.service";
|
||||||
import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from "./injection-tokens";
|
|
||||||
import { LaunchGuardService } from "./launch-guard.service";
|
import { LaunchGuardService } from "./launch-guard.service";
|
||||||
import { SafeProvider, safeProvider } from "./safe-provider";
|
|
||||||
|
|
||||||
export function initFactory(
|
export function initFactory(
|
||||||
environmentService: EnvironmentServiceAbstraction,
|
environmentService: EnvironmentServiceAbstraction,
|
||||||
i18nService: I18nServiceAbstraction,
|
i18nService: I18nService,
|
||||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||||
stateService: StateServiceAbstraction,
|
stateService: StateServiceAbstraction,
|
||||||
cryptoService: CryptoServiceAbstraction,
|
cryptoService: CryptoServiceAbstraction,
|
||||||
@@ -49,7 +50,7 @@ export function initFactory(
|
|||||||
return async () => {
|
return async () => {
|
||||||
await stateService.init();
|
await stateService.init();
|
||||||
await environmentService.setUrlsFromStorage();
|
await environmentService.setUrlsFromStorage();
|
||||||
await (i18nService as I18nService).init();
|
await i18nService.init();
|
||||||
const htmlEl = window.document.documentElement;
|
const htmlEl = window.document.documentElement;
|
||||||
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
|
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
|
||||||
htmlEl.classList.add("locale_" + i18nService.translationLocale);
|
htmlEl.classList.add("locale_" + i18nService.translationLocale);
|
||||||
@@ -77,8 +78,8 @@ export function initFactory(
|
|||||||
imports: [JslibServicesModule],
|
imports: [JslibServicesModule],
|
||||||
declarations: [],
|
declarations: [],
|
||||||
providers: [
|
providers: [
|
||||||
safeProvider({
|
{
|
||||||
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
|
provide: APP_INITIALIZER,
|
||||||
useFactory: initFactory,
|
useFactory: initFactory,
|
||||||
deps: [
|
deps: [
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentServiceAbstraction,
|
||||||
@@ -88,29 +89,21 @@ export function initFactory(
|
|||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
],
|
],
|
||||||
multi: true,
|
multi: true,
|
||||||
}),
|
},
|
||||||
safeProvider({ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }),
|
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
|
||||||
safeProvider({
|
{
|
||||||
provide: I18nServiceAbstraction,
|
provide: I18nServiceAbstraction,
|
||||||
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
|
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
|
||||||
deps: [WINDOW],
|
deps: ["WINDOW"],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
provide: MessagingServiceAbstraction,
|
provide: MessagingServiceAbstraction,
|
||||||
useClass: ElectronRendererMessagingService,
|
useClass: ElectronRendererMessagingService,
|
||||||
deps: [BroadcasterServiceAbstraction],
|
deps: [BroadcasterServiceAbstraction],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
|
||||||
provide: StorageServiceAbstraction,
|
{ provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService },
|
||||||
useClass: ElectronRendererStorageService,
|
{
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
provide: SECURE_STORAGE,
|
|
||||||
useClass: ElectronRendererSecureStorageService,
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
provide: PlatformUtilsServiceAbstraction,
|
provide: PlatformUtilsServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
i18nService: I18nServiceAbstraction,
|
i18nService: I18nServiceAbstraction,
|
||||||
@@ -118,13 +111,9 @@ export function initFactory(
|
|||||||
stateService: StateServiceAbstraction,
|
stateService: StateServiceAbstraction,
|
||||||
) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService),
|
) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService),
|
||||||
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
|
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{ provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] },
|
||||||
provide: CryptoFunctionServiceAbstraction,
|
{
|
||||||
useClass: NodeCryptoFunctionService,
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
provide: ApiServiceAbstraction,
|
provide: ApiServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
tokenService: TokenServiceAbstraction,
|
tokenService: TokenServiceAbstraction,
|
||||||
@@ -152,19 +141,26 @@ export function initFactory(
|
|||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
provide: AuthServiceAbstraction,
|
provide: AuthServiceAbstraction,
|
||||||
useClass: AuthService,
|
useClass: AuthService,
|
||||||
deps: [
|
deps: [
|
||||||
|
CryptoServiceAbstraction,
|
||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
|
TokenServiceAbstraction,
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
|
LogServiceAbstraction,
|
||||||
|
KeyConnectorServiceAbstraction,
|
||||||
|
EnvironmentServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
|
TwoFactorServiceAbstraction,
|
||||||
|
I18nServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
provide: SyncService,
|
provide: SyncService,
|
||||||
useClass: SyncService,
|
useClass: SyncService,
|
||||||
deps: [
|
deps: [
|
||||||
@@ -176,10 +172,10 @@ export function initFactory(
|
|||||||
EnvironmentServiceAbstraction,
|
EnvironmentServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
safeProvider(AuthGuardService),
|
AuthGuardService,
|
||||||
safeProvider(LaunchGuardService),
|
LaunchGuardService,
|
||||||
safeProvider({
|
{
|
||||||
provide: StateMigrationServiceAbstraction,
|
provide: StateMigrationServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: StorageServiceAbstraction,
|
storageService: StorageServiceAbstraction,
|
||||||
@@ -190,9 +186,9 @@ export function initFactory(
|
|||||||
secureStorageService,
|
secureStorageService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
),
|
),
|
||||||
deps: [StorageServiceAbstraction, SECURE_STORAGE],
|
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
||||||
}),
|
},
|
||||||
safeProvider({
|
{
|
||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: StorageServiceAbstraction,
|
storageService: StorageServiceAbstraction,
|
||||||
@@ -210,11 +206,15 @@ export function initFactory(
|
|||||||
),
|
),
|
||||||
deps: [
|
deps: [
|
||||||
StorageServiceAbstraction,
|
StorageServiceAbstraction,
|
||||||
SECURE_STORAGE,
|
"SECURE_STORAGE",
|
||||||
LogServiceAbstraction,
|
LogServiceAbstraction,
|
||||||
StateMigrationServiceAbstraction,
|
StateMigrationServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
},
|
||||||
] satisfies SafeProvider[],
|
{
|
||||||
|
provide: TwoFactorServiceAbstraction,
|
||||||
|
useClass: NoopTwoFactorService,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ServicesModule {}
|
export class ServicesModule {}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user