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

Compare commits

...

76 Commits

Author SHA1 Message Date
Vince Grassia
b2bc451370 Fix checksum generation (#369)
(cherry picked from commit a5f779c231)
2023-11-01 13:31:46 -04:00
Vince Grassia
159fe6ea68 Update release workflow to match clients (#368)
(cherry picked from commit dd7df6504b)
2023-11-01 13:00:21 -04:00
Vince Grassia
3b96fbaef0 Update workflows to current standard (#367)
(cherry picked from commit f064de83e8)
2023-11-01 12:14:24 -04:00
github-actions[bot]
d3ef4026b6 Bumped version to 2023.10.0 (#366)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
(cherry picked from commit e27bdbc561)
2023-10-31 11:44:49 -04:00
renovate[bot]
33da38a3f3 Update bitwarden/gh-actions digest to f112580 (#359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-05 14:57:06 -04:00
Rui Tomé
26e4930a9e [AC-1614] Updated webpack config (#352)
* [AC-1614] Updated webpack config

* [AC-1614] Ran prettier

* [AC-1614] Fix build
2023-09-26 10:07:39 +01:00
renovate[bot]
4dba787df2 Update bitwarden/gh-actions digest to 62d1bf7 (#357)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-25 08:36:14 -04:00
renovate[bot]
2e2b5d988c Update actions/checkout action to v4.1.0 (#358)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-25 08:35:59 -04:00
renovate[bot]
d0c126cf3e Update actions/upload-artifact action to v3.1.3 (#354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 10:44:39 -04:00
renovate[bot]
8700332d13 Update bitwarden/gh-actions digest to 8fccdae (#334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 10:28:23 -04:00
Opeyemi
0e4f9c0de5 Update all workflow (#355) 2023-09-15 15:36:20 +01:00
Matt Bishop
c76033b8ad Mention Node 18 (#351) 2023-08-30 14:41:28 -04:00
renovate[bot]
50cfc50ed7 Update npm minor (#347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 12:13:30 -04:00
renovate[bot]
739ab3d2dd Update dependency concurrently to v8 (#348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 16:52:45 +01:00
Thomas Rittson
1ac402ca06 [AC-1291] Add CODEOWNERS file (#349) 2023-07-18 08:21:15 +10:00
renovate[bot]
a3c8629f6d Update npm minor (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-21 09:01:52 -04:00
renovate[bot]
161c5f82be Update dependency electron-builder to v24 (#346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-20 15:47:52 -04:00
renovate[bot]
0476c97f5f Update dotnet monorepo to v7 (#344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-20 12:48:57 -04:00
renovate[bot]
41967d6dc1 Update npm minor (#338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 09:16:09 -04:00
Matt Bishop
40908e4b88 Remove .NET SDK usage (#339) 2023-06-13 13:53:09 -04:00
renovate[bot]
9e7a11a27c Update actions/checkout action to v3.5.3 (#337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-12 19:23:35 -04:00
renovate[bot]
a491e2691d Lock file maintenance (#323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-12 16:38:31 -04:00
renovate[bot]
2c65003cf9 Update npm minor (#321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-12 12:29:41 -04:00
Matt Bishop
0d78b33ae1 Remove more lower-level packages (#336)
* Remove more lower-level packages

* Restore keytar for src
2023-06-12 11:53:10 -04:00
Matt Bishop
a3939a31a9 Move JS library dependencies up to root (#335) 2023-06-12 07:43:09 -04:00
renovate[bot]
3540a2741e Update dependency @types/ldapjs to v2 (#332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-06 06:18:11 -04:00
Matt Bishop
0ddf81f644 Angular 15 (#326)
* Install Angular CLI

* Core setup and cleanup

* TypeScript and webpack updates

* Angular 13

* Add JS lib to Angular workspace

* Do not use JS library with workspace

* Angular 14

* Angular 15

* Code fixes

* Couple package bumps

* Restore angularCompilerOptions

* Remove property reference to users inside group that didn't exist
2023-06-02 11:42:01 -04:00
renovate[bot]
83d527a83e Update actions/setup-dotnet action to v3 (#324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-31 15:09:34 -04:00
renovate[bot]
63b7b9124f Update bitwarden/gh-actions digest to 72594be (#319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-31 11:27:26 -04:00
renovate[bot]
5d85df2105 Update dependency node-forge to v1.3.0 [SECURITY] (#302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-31 11:25:49 -04:00
renovate[bot]
dbc3c8795d Update dependency webpack to v5.76.0 [SECURITY] (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-31 09:43:16 -04:00
renovate[bot]
f09f7a0e51 Update gh minor (#320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-31 08:41:39 -04:00
renovate[bot]
e3afe9fb69 Update dependency husky to v8 (#322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-31 08:40:28 -04:00
renovate[bot]
1a6c51b3aa Update dependency electron to v18 [SECURITY] (#299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-30 15:49:23 -04:00
renovate[bot]
107d7afb26 Update dependency node-fetch to 2.6.7 [SECURITY] (#313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-30 14:27:32 -04:00
renovate[bot]
0959e58dc9 Update Node.js to v18 (#310)
* Update Node.js to v18

* Update builds

* Add NVM

* Update pkg-fetch

* Update NPM too

* Update pkg too

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
2023-05-25 10:44:31 -04:00
Opeyemi
911f9c0dfc update enforce label action tag (#315) 2023-05-22 20:25:35 +01:00
renovate[bot]
7cf518edb5 Pin dependencies (#304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-22 13:47:46 -04:00
Opeyemi
4234b3b1cf update all actions pin (#312) 2023-05-22 16:25:24 +01:00
Matt Bishop
0f5cdd53df Renovate configuration update (#311) 2023-05-22 08:46:44 -04:00
Matt Bishop
50bd7ca2b3 Renovate configuration (#298) 2023-05-05 10:11:08 -04:00
mimartin12
8531a64568 Update key vault name and cred (#297) 2023-04-14 13:18:29 -06:00
mimartin12
c1cec89995 [DEVOPS-1167] - Update DC MacOS GUI artifact name (#296) 2023-04-13 10:16:53 -06:00
Michał Chęciński
3d214dbedc Use new CI Azure Key Vault (#295) 2023-04-11 17:19:41 +02:00
github-actions[bot]
a528480e07 Bumped version to 2022.11.0 (#290)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-11-29 18:41:09 +00:00
Opeyemi
af5c863f1f update az login cred (#289) 2022-11-29 17:34:43 +00:00
Vincent Salucci
365bda7e21 [EC-682] Adjust group query assignment (#287) 2022-11-08 21:31:39 -06:00
Thomas Rittson
f1b533f7b6 Handle falsy values in keytarSecureStorageService (#286) 2022-10-31 10:04:13 +00:00
Tomáš Drtina
5bf9b128d4 GSuite: Use filter query when fetching groups. (#279)
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
2022-10-17 10:52:51 -05:00
sneakernuts
0a8c4d30bb added import GPG step and changed to service account for commit (#284)
* added import GPG step and changed to service account for commit

* added steps for kv and updated import gpg key step
2022-10-13 19:56:08 +00:00
Michał Chęciński
1b3f277c1f Update deprecated Azure Key Vault in workflows (#282) 2022-09-05 11:38:57 +02:00
Todd Martin
8039f93434 [ENG-71] Add deployments to release workflow (#281)
* Added deployments to release workflow.

* Removed in_progress update

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

* Added in_progress initial-status

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

* Added explicit expression syntax

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

* Added explicit expression syntex on failure()

Co-authored-by: Todd Martin <>
Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2022-08-04 13:47:29 -04:00
Vince Grassia
d05e8ab7af Update 'Dry Run' path in Release workflow (#278) 2022-07-19 15:01:07 -04:00
github-actions[bot]
fb3d082b88 Bumped version to 2022.6.0 (#273)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-29 09:13:05 -07:00
Vincent Salucci
8541a4252b [EC-250] AU group next link (#272) 2022-06-10 11:25:15 -05:00
Jordan Cooks
e0d36a7407 Update example after LDAP library syntax change (#271)
The in-app example is no longer valid after an upstream LDAP library change; updating the example with the correct syntax
2022-05-18 11:57:24 -04:00
Oscar Hinton
73b031b884 Add all dependencies to the root package.json (#267) 2022-05-05 15:20:57 +02:00
github-actions[bot]
167c5e0108 Bump version to 2.10.2 (#270)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-05-03 13:03:51 -05:00
Robyn MacCallum
f67f113fe1 [EC-176] Fix CLI errors caused by server URLs (#269)
* Only get global environment urls

* remove unnecessary await
2022-05-03 07:20:10 -04:00
Thomas Rittson
073126949b [EC-182] Refactor StateService secrets handling (#268) 2022-05-03 21:06:04 +10:00
Oscar Hinton
45d0192f82 Copy jslib into Directory Connector [TI-6] (#262) 2022-04-28 16:41:07 +02:00
github-actions[bot]
2d02d54b56 Bumped version to 2.10.1 (#265)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-04-25 08:44:58 -07:00
Thomas Rittson
8f4da6d490 Update jslib (#261) 2022-04-19 10:30:01 -05:00
Joseph Flinn
7753749b62 Bumping pinned commit of the download-artifact action to bypass the broken GitHub api (#260) 2022-04-18 14:28:59 -07:00
Oscar Hinton
c5cc8eab0a Move storage listener to DC (#258) 2022-04-05 19:30:37 +02:00
Thomas Rittson
8981b97632 EC-134 Fix api token refresh (#257)
* Fix api token refresh

* Update jslib
2022-04-01 14:48:35 +10:00
Thomas Rittson
c75d26b618 Update to use new JslibModule (#254) 2022-03-21 23:08:12 +01:00
Joseph Flinn
13a13dd18f Adding a manual trigger to the build pipeline in the directory-connector project (#256) 2022-03-21 15:05:12 -07:00
Vince Grassia
954b23d91f Fix Node caching error (#255) 2022-03-21 14:57:44 -07:00
Micaiah Martin
b9d35c3dc7 Updated actions & applied linting (#253) 2022-03-17 12:27:57 -04:00
github-actions[bot]
536f48b3c7 Bump version to 2.10.0 (#252)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-03-16 10:51:01 -06:00
Vince Grassia
8cd768c7c2 Add Node caching (#251) 2022-03-16 11:18:37 -04:00
Chad Scharf
b233d2e87d Update SECURITY.md (#250)
* Update SECURITY.md

Add link to our HackerOne program for submitting potential security issues.

* Revise language on SECURITY.md
2022-03-15 20:53:33 +01:00
Thomas Rittson
1f6d8c1458 Update jslib (#248) 2022-03-15 15:15:27 +10:00
Joseph Flinn
ae05183aa3 Update hotfix release branch name to hotfix-rc (#247) 2022-03-09 12:46:27 -08:00
Robyn MacCallum
8374103a15 Move delta tokens out of secure storage (#246) 2022-03-09 07:51:27 -05:00
729 changed files with 88331 additions and 10273 deletions

View File

@@ -1,9 +1,10 @@
dist
build
build-cli
jslib
webpack.cli.js
webpack.main.js
webpack.renderer.js
**/node_modules
**/jest.config.js

12
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,12 @@
# Please sort into logical groups with comment headers. Sort groups in order of specificity.
# For example, default owners should always be the first group.
# Sort lines alphabetically within these groups to avoid accidentally adding duplicates.
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default file owners.
* @bitwarden/team-admin-console-dev
# DevOps for Actions and other workflow changes.
.github/workflows @bitwarden/dept-devops
.github/secrets @bitwarden/dept-devops

41
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":combinePatchMinorReleases",
":dependencyDashboard",
":maintainLockFilesWeekly",
":pinAllExceptPeerDependencies",
":prConcurrentLimit10",
":rebaseStalePrs",
"schedule:weekends",
":separateMajorReleases"
],
"enabledManagers": ["github-actions", "npm"],
"packageRules": [
{
"groupName": "gh minor",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "npm minor",
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"packageNames": ["typescript"],
"matchUpdateTypes": ["major", "minor"],
"enabled": false
},
{
"packageNames": ["typescript"],
"matchUpdateTypes": "patch"
},
{
"groupName": "jest",
"packageNames": ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
"matchUpdateTypes": "major"
}
]
}

View File

@@ -7,15 +7,16 @@ on:
- 'l10n_master'
paths-ignore:
- '.github/workflows/**'
workflow_dispatch: {}
jobs:
cloc:
name: CLOC
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up CLOC
run: |
@@ -28,36 +29,38 @@ jobs:
setup:
name: Setup
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Get Package Version
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version src/package.json)
echo "::set-output name=package_version::$PKG_VERSION"
echo "package_version=$PKG_VERSION" >> $GITHUB_OUTPUT
linux-cli:
name: Build Linux CLI
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 16.13.0
_PKG_FETCH_VERSION: 3.2
_PKG_FETCH_NODE_VERSION: 18.5.0
_PKG_FETCH_VERSION: 3.4
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '18'
- name: Update NPM
run: |
@@ -91,15 +94,16 @@ jobs:
run: npm run dist:cli:lin
- name: Zip
run: |
zip -j ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip ./dist-cli/linux/bwdc ./keytar/linux/build/Release/keytar.node
run: zip -j dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip dist-cli/linux/bwdc keytar/linux/build/Release/keytar.node
- name: Create checksums
run: |
sha256sum ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip | cut -d " " -f 1 > ./dist-cli/bwdc-linux-sha256-$_PACKAGE_VERSION.txt
shasum -a 256 dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip | \
cut -d " " -f 1 > dist-cli/bwdc-linux-sha256-$_PACKAGE_VERSION.txt
- name: Version Test
run: |
sudo apt-get update
sudo apt install libsecret-1-0 dbus-x11 gnome-keyring
eval $(dbus-launch --sh-syntax)
@@ -120,14 +124,14 @@ jobs:
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Linux checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -136,20 +140,22 @@ jobs:
macos-cli:
name: Build Mac CLI
runs-on: macos-11
runs-on: macos-12
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 16.13.0
_PKG_FETCH_VERSION: 3.2
_PKG_FETCH_NODE_VERSION: 18.5.0
_PKG_FETCH_VERSION: 3.4
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '18'
- name: Update NPM
run: |
@@ -183,12 +189,12 @@ jobs:
run: npm run dist:cli:mac
- name: Zip
run: |
zip -j ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip ./dist-cli/macos/bwdc ./keytar/macos/build/Release/keytar.node
run: zip -j dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip dist-cli/macos/bwdc keytar/macos/build/Release/keytar.node
- name: Create checksums
run: |
sha256sum ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip | cut -d " " -f 1 > ./dist-cli/bwdc-macos-sha256-$_PACKAGE_VERSION.txt
shasum -a 256 dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip | \
cut -d " " -f 1 > dist-cli/bwdc-macos-sha256-$_PACKAGE_VERSION.txt
- name: Version Test
run: |
@@ -206,31 +212,30 @@ jobs:
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Mac checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
windows-cli:
name: Build Windows CLI
runs-on: windows-2019
runs-on: windows-2022
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_WIN_PKG_FETCH_VERSION: 16.13.0
_WIN_PKG_VERSION: 3.2
_WIN_PKG_FETCH_VERSION: 18.5.0
_WIN_PKG_VERSION: 3.4
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Windows builder
run: |
@@ -238,9 +243,11 @@ jobs:
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '18'
- name: Update NPM
run: |
@@ -331,15 +338,16 @@ jobs:
- name: Zip
shell: cmd
run: |
7z a ./dist-cli/bwdc-windows-%_PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
- name: Version Test
shell: pwsh
run: |
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
echo "version: $env:_PACKAGE_VERSION"
Expand-Archive -Path "dist-cli\bwdc-windows-${{ env._PACKAGE_VERSION }}.zip" -DestinationPath "test\windows"
$testVersion = Invoke-Expression '& .\test\windows\bwdc.exe -v'
echo "version: ${env:_PACKAGE_VERSION}"
echo "testVersion: $testVersion"
if($testVersion -ne $env:_PACKAGE_VERSION) {
if ($testVersion -ne ${env:_PACKAGE_VERSION}) {
Throw "Version test failed."
}
@@ -349,14 +357,14 @@ jobs:
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Windows checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -365,48 +373,38 @@ jobs:
windows-gui:
name: Build Windows GUI
runs-on: windows-2019
runs-on: windows-2022
needs: setup
env:
NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up .NET
uses: actions/setup-dotnet@a71d1eb2c86af85faa8c772c03fb365e377e45ea
with:
dotnet-version: "3.1.x"
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '18'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
shell: pwsh
- name: Print environment
run: |
node --version
npm --version
dotnet --version
- name: Install AST
uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
uses: bitwarden/gh-actions/install-ast@master
- name: Install Node dependencies
run: npm install
# - name: Run linter
# run: npm run lint
- name: Build & Sign
run: npm run dist:win
env:
@@ -418,28 +416,28 @@ jobs:
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: latest.yml
path: ./dist/latest.yml
@@ -448,33 +446,33 @@ jobs:
linux-gui:
name: Build Linux GUI
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: setup
env:
NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '18'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install rpm
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: NPM Install
run: npm install
@@ -485,14 +483,14 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
@@ -501,24 +499,27 @@ jobs:
macos-gui:
name: Build MacOS GUI
runs-on: macos-11
runs-on: macos-12
needs: setup
env:
NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '18'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Print environment
run: |
node --version
@@ -527,9 +528,6 @@ jobs:
echo "GitHub event: $GITHUB_EVENT"
shell: bash
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Decrypt secrets
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
@@ -580,44 +578,35 @@ jobs:
- name: Install Node dependencies
run: npm install
# - name: Run linter
# run: npm run lint
- name: Build application
run: npm run dist:mac
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Rename Zip Artifact
run: |
cd dist
mv "Bitwarden Directory Connector-${{ env._PACKAGE_VERSION }}-mac.zip" \
"Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip"
- name: Upload .zip artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml
@@ -626,7 +615,7 @@ jobs:
check-failures:
name: Check for failures
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs:
- cloc
- setup
@@ -667,22 +656,22 @@ jobs:
exit 1
fi
- name: Login to Azure - Prod Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
- name: Login to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
if: failure()
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
uses: bitwarden/gh-actions/get-keyvault-secrets@master
if: failure()
with:
keyvault: "bitwarden-prod-kv"
keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}

View File

@@ -7,10 +7,10 @@ on:
jobs:
enforce-label:
name: EnforceLabel
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Enforce Label
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
with:
BANNED_LABELS: "hold"
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"

View File

@@ -17,61 +17,67 @@ on:
jobs:
setup:
name: Setup
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
release-version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix' branches"
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "==================================="
exit 1
fi
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Check Release Version
id: version
uses: bitwarden/gh-actions/release-version-check@master
with:
release-type: ${{ github.event.inputs.release_type }}
project-type: ts
file: src/package.json
- name: Retrieve Directory Connector release version
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version src/package.json)
echo "::set-output name=package_version::$PKG_VERSION"
- name: Check to make sure Mobile release version has been bumped
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
latest_ver=$(hub release -L 1 -f '%T')
latest_ver=${latest_ver:1}
echo "Latest version: $latest_ver"
ver=${{ steps.retrieve-version.outputs.package_version }}
echo "Version: $ver"
if [ "$latest_ver" = "$ver" ]; then
echo "Version has not been bumped!"
exit 1
fi
shell: bash
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
release:
name: Release
runs-on: ubuntu-22.04
needs: setup
steps:
- name: Create GitHub deployment
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
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
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@master
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ steps.branch.outputs.branch-name }}
branch: ${{ github.ref_name }}
- name: Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@master
with:
workflow: build.yml
workflow_conclusion: success
branch: master
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
env:
PKG_VERSION: ${{ steps.retrieve-version.outputs.package_version }}
PKG_VERSION: ${{ needs.setup.outputs.release-version }}
with:
artifacts: "./bwdc-windows-${{ env.PKG_VERSION }}.zip,
./bwdc-macos-${{ env.PKG_VERSION }}.zip,
@@ -95,3 +101,19 @@ jobs:
body: "<insert release notes here>"
token: ${{ secrets.GITHUB_TOKEN }}
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 }}

View File

@@ -11,10 +11,30 @@ on:
jobs:
bump_version:
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Login to Azure - Prod Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@master
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Create Version Branch
run: |
@@ -22,20 +42,20 @@ jobs:
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
ref: version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Package
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
uses: bitwarden/gh-actions/version-bump@master
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/package.json"
- name: Commit files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
@@ -60,6 +80,6 @@ jobs:
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ github.event.inputs.version_number }}"

View File

@@ -8,4 +8,4 @@ on:
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@f1125802b1ccae8c601d7c4f61ce39ea254b10c8

46
.gitignore vendored
View File

@@ -1,17 +1,39 @@
.vs
.idea
# General
.DS_Store
Thumbs.db
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Node
node_modules
npm-debug.log
vwd.webinfo
dist/
dist-cli/
css/
# Build directories
dist
build
.angular/cache
# Testing
coverage
junit.xml
# Misc
*.crx
*.pem
build-cli/
build/
yarn-error.log
.DS_Store
*.nupkg
*.zip
*.provisionprofile
*.env

4
.gitmodules vendored
View File

@@ -1,4 +0,0 @@
[submodule "jslib"]
path = jslib
url = https://github.com/bitwarden/jslib.git
branch = master

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v18

View File

@@ -3,8 +3,6 @@ build
build-cli
dist
jslib
# External libraries / auto synced locales
src/locales

View File

@@ -48,7 +48,7 @@ We provide detailed documentation and examples for using the Directory Connector
**Requirements**
- [Node.js](https://nodejs.org) v16.13.1 (LTS)
- [Node.js](https://nodejs.org) v18 (LTS)
- Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules).
**Run the app**

View File

@@ -1,39 +1,11 @@
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
# In-scope
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
code is available at https://github.com/bitwarden.
# Exclusions
The following bug classes are out-of scope:
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under Bitwarden's control
- Vulnerabilities in outdated versions of Bitwarden
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
While researching, we'd like to ask you to refrain from:
@@ -42,4 +14,8 @@ While researching, we'd like to ask you to refrain from:
- Social engineering (including phishing) of Bitwarden staff or contractors
- Any physical attempts against Bitwarden property or data centers
# We want to help you!
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
Thank you for helping keep Bitwarden and our users safe!

35
angular.json Normal file
View File

@@ -0,0 +1,35 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "apps",
"cli": {
"analytics": false
},
"projects": {
"app": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": ".",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.json",
"assets": [],
"styles": [],
"scripts": []
}
}
}
}
}
}

67
electron-builder.json Normal file
View File

@@ -0,0 +1,67 @@
{
"extraMetadata": {
"name": "bitwarden-directory-connector"
},
"productName": "Bitwarden Directory Connector",
"appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
"app": "build"
},
"afterSign": "scripts/notarize.js",
"mac": {
"artifactName": "Bitwarden-Connector-${version}-mac.${ext}",
"category": "public.app-category.productivity",
"gatekeeperAssess": false,
"hardenedRuntime": true,
"entitlements": "resources/entitlements.mac.plist",
"entitlementsInherit": "resources/entitlements.mac.plist",
"target": ["dmg", "zip"]
},
"win": {
"target": ["portable", "nsis"],
"sign": "scripts/sign.js"
},
"linux": {
"category": "Utility",
"synopsis": "Sync your user directory to your Bitwarden organization.",
"target": ["AppImage"]
},
"dmg": {
"artifactName": "Bitwarden-Connector-${version}.${ext}",
"icon": "dmg.icns",
"contents": [
{
"x": 150,
"y": 185,
"type": "file"
},
{
"x": 390,
"y": 180,
"type": "link",
"path": "/Applications"
}
],
"window": {
"width": 540,
"height": 380
}
},
"nsis": {
"oneClick": false,
"perMachine": true,
"allowToChangeInstallationDirectory": true,
"artifactName": "Bitwarden-Connector-Installer-${version}.${ext}",
"uninstallDisplayName": "${productName}",
"deleteAppDataOnUninstall": true
},
"portable": {
"artifactName": "Bitwarden-Connector-Portable-${version}.${ext}"
},
"appImage": {
"artifactName": "Bitwarden-Connector-${version}-${arch}.${ext}"
}
}

1
jslib

Submodule jslib deleted from 1cbc119ad8

9
jslib/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.vs
.idea
node_modules
npm-debug.log
vwd.webinfo
*.crx
*.pem
dist
coverage

3
jslib/.prettierrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"printWidth": 100
}

View File

@@ -0,0 +1,17 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { compilerOptions } = require("./tsconfig");
module.exports = {
name: "angular",
displayName: "angular tests",
preset: "jest-preset-angular",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),
};

13
jslib/angular/package-lock.json generated Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "@bitwarden/jslib-angular",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/jslib-angular",
"version": "0.0.0",
"license": "GPL-3.0"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "@bitwarden/jslib-angular",
"version": "0.0.0",
"description": "Common code used across Bitwarden JavaScript projects.",
"keywords": [
"bitwarden"
],
"author": "Bitwarden Inc.",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/jslib"
},
"license": "GPL-3.0",
"scripts": {
"clean": "rimraf dist/**/*",
"build": "npm run clean && tsc",
"build:watch": "npm run clean && tsc -watch"
},
"dependencies": {}
}

View File

@@ -0,0 +1,28 @@
import { webcrypto } from "crypto";
import "jest-preset-angular/setup-jest";
Object.defineProperty(window, "CSS", { value: null });
Object.defineProperty(window, "getComputedStyle", {
value: () => {
return {
display: "none",
appearance: ["-webkit-appearance"],
};
},
});
Object.defineProperty(document, "doctype", {
value: "<!DOCTYPE html>",
});
Object.defineProperty(document.body.style, "transform", {
value: () => {
return {
enumerable: true,
configurable: true,
};
},
});
Object.defineProperty(window, "crypto", {
value: webcrypto,
});

View File

@@ -0,0 +1,35 @@
<div
#callout
class="callout callout-{{ calloutStyle }}"
[ngClass]="{ clickable: clickable }"
[attr.role]="useAlertRole ? 'alert' : null"
>
<h3 class="callout-heading" *ngIf="title">
<i class="bwi {{ icon }}" *ngIf="icon" aria-hidden="true"></i>
{{ title }}
</h3>
<div class="enforced-policy-options" *ngIf="enforcedPolicyOptions">
{{ enforcedPolicyMessage }}
<ul>
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{ "policyInEffectMinComplexity" | i18n : getPasswordScoreAlertDisplay() }}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{ "policyInEffectMinLength" | i18n : enforcedPolicyOptions?.minLength.toString() }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{ "policyInEffectUppercase" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{ "policyInEffectLowercase" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{ "policyInEffectNumbers" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{ "policyInEffectSpecial" | i18n : "!@#$%^&*" }}
</li>
</ul>
</div>
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,78 @@
import { Component, Input, OnInit } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions";
@Component({
selector: "app-callout",
templateUrl: "callout.component.html",
})
export class CalloutComponent implements OnInit {
@Input() type = "info";
@Input() icon: string;
@Input() title: string;
@Input() clickable: boolean;
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
@Input() enforcedPolicyMessage: string;
@Input() useAlertRole = false;
calloutStyle: string;
constructor(private i18nService: I18nService) {}
ngOnInit() {
this.calloutStyle = this.type;
if (this.enforcedPolicyMessage === undefined) {
this.enforcedPolicyMessage = this.i18nService.t("masterPasswordPolicyInEffect");
}
if (this.type === "warning" || this.type === "danger") {
if (this.type === "danger") {
this.calloutStyle = "danger";
}
if (this.title === undefined) {
this.title = this.i18nService.t("warning");
}
if (this.icon === undefined) {
this.icon = "bwi-exclamation-triangle";
}
} else if (this.type === "error") {
this.calloutStyle = "danger";
if (this.title === undefined) {
this.title = this.i18nService.t("error");
}
if (this.icon === undefined) {
this.icon = "bwi-error";
}
} else if (this.type === "tip") {
this.calloutStyle = "success";
if (this.title === undefined) {
this.title = this.i18nService.t("tip");
}
if (this.icon === undefined) {
this.icon = "bwi-lightbulb";
}
}
}
getPasswordScoreAlertDisplay() {
if (this.enforcedPolicyOptions == null) {
return "";
}
let str: string;
switch (this.enforcedPolicyOptions.minComplexity) {
case 4:
str = this.i18nService.t("strong");
break;
case 3:
str = this.i18nService.t("good");
break;
default:
str = this.i18nService.t("weak");
break;
}
return str + " (" + this.enforcedPolicyOptions.minComplexity + ")";
}
}

View File

@@ -0,0 +1,63 @@
import { Directive, EventEmitter, Output } from "@angular/core";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Directive()
export class EnvironmentComponent {
@Output() onSaved = new EventEmitter();
iconsUrl: string;
identityUrl: string;
apiUrl: string;
webVaultUrl: string;
notificationsUrl: string;
baseUrl: string;
showCustom = false;
constructor(
protected platformUtilsService: PlatformUtilsService,
protected environmentService: EnvironmentService,
protected i18nService: I18nService
) {
const urls = this.environmentService.getUrls();
this.baseUrl = urls.base || "";
this.webVaultUrl = urls.webVault || "";
this.apiUrl = urls.api || "";
this.identityUrl = urls.identity || "";
this.iconsUrl = urls.icons || "";
this.notificationsUrl = urls.notifications || "";
}
async submit() {
const resUrls = await this.environmentService.setUrls({
base: this.baseUrl,
api: this.apiUrl,
identity: this.identityUrl,
webVault: this.webVaultUrl,
icons: this.iconsUrl,
notifications: this.notificationsUrl,
});
// re-set urls since service can change them, ex: prefixing https://
this.baseUrl = resUrls.base;
this.apiUrl = resUrls.api;
this.identityUrl = resUrls.identity;
this.webVaultUrl = resUrls.webVault;
this.iconsUrl = resUrls.icons;
this.notificationsUrl = resUrls.notifications;
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
this.saved();
}
toggleCustom() {
this.showCustom = !this.showCustom;
}
protected saved() {
this.onSaved.emit();
}
}

View File

@@ -0,0 +1,11 @@
<div class="icon" aria-hidden="true">
<img
[src]="image"
appFallbackSrc="{{ fallbackImage }}"
*ngIf="imageEnabled && image"
alt=""
decoding="async"
loading="lazy"
/>
<i class="bwi bwi-fw bwi-lg {{ icon }}" *ngIf="!imageEnabled || !image"></i>
</div>

View File

@@ -0,0 +1,112 @@
import { Component, Input, OnChanges } from "@angular/core";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { CipherType } from "jslib-common/enums/cipherType";
import { Utils } from "jslib-common/misc/utils";
import { CipherView } from "jslib-common/models/view/cipherView";
/**
* Provides a mapping from supported card brands to
* the filenames of icon that should be present in images/cards folder of clients.
*/
const cardIcons: Record<string, string> = {
Visa: "card-visa",
Mastercard: "card-mastercard",
Amex: "card-amex",
Discover: "card-discover",
"Diners Club": "card-diners-club",
JCB: "card-jcb",
Maestro: "card-maestro",
UnionPay: "card-union-pay",
};
@Component({
selector: "app-vault-icon",
templateUrl: "icon.component.html",
})
export class IconComponent implements OnChanges {
@Input() cipher: CipherView;
icon: string;
image: string;
fallbackImage: string;
imageEnabled: boolean;
private iconsUrl: string;
constructor(environmentService: EnvironmentService, private stateService: StateService) {
this.iconsUrl = environmentService.getIconsUrl();
}
async ngOnChanges() {
// Components may be re-used when using cdk-virtual-scroll. Which puts the component in a weird state,
// to avoid this we reset all state variables.
this.image = null;
this.fallbackImage = null;
this.imageEnabled = !(await this.stateService.getDisableFavicon());
this.load();
}
protected load() {
switch (this.cipher.type) {
case CipherType.Login:
this.icon = "bwi-globe";
this.setLoginIcon();
break;
case CipherType.SecureNote:
this.icon = "bwi-sticky-note";
break;
case CipherType.Card:
this.icon = "bwi-credit-card";
this.setCardIcon();
break;
case CipherType.Identity:
this.icon = "bwi-id-card";
break;
default:
break;
}
}
private setLoginIcon() {
if (this.cipher.login.uri) {
let hostnameUri = this.cipher.login.uri;
let isWebsite = false;
if (hostnameUri.indexOf("androidapp://") === 0) {
this.icon = "bwi-android";
this.image = null;
} else if (hostnameUri.indexOf("iosapp://") === 0) {
this.icon = "bwi-apple";
this.image = null;
} else if (
this.imageEnabled &&
hostnameUri.indexOf("://") === -1 &&
hostnameUri.indexOf(".") > -1
) {
hostnameUri = "http://" + hostnameUri;
isWebsite = true;
} else if (this.imageEnabled) {
isWebsite = hostnameUri.indexOf("http") === 0 && hostnameUri.indexOf(".") > -1;
}
if (this.imageEnabled && isWebsite) {
try {
this.image = this.iconsUrl + "/" + Utils.getHostname(hostnameUri) + "/icon.png";
this.fallbackImage = "images/bwi-globe.png";
} catch (e) {
// Ignore error since the fallback icon will be shown if image is null.
}
}
} else {
this.image = null;
}
}
private setCardIcon() {
const brand = this.cipher.card.brand;
if (this.imageEnabled && brand in cardIcons) {
this.icon = "credit-card-icon " + cardIcons[brand];
}
}
}

View File

@@ -0,0 +1,79 @@
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from "@angular/cdk/a11y";
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ComponentRef,
ElementRef,
OnDestroy,
Type,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ModalService } from "../../services/modal.service";
import { ModalRef } from "./modal.ref";
@Component({
selector: "app-modal",
template: "<ng-template #modalContent></ng-template>",
})
export class DynamicModalComponent implements AfterViewInit, OnDestroy {
componentRef: ComponentRef<any>;
@ViewChild("modalContent", { read: ViewContainerRef, static: true })
modalContentRef: ViewContainerRef;
childComponentType: Type<any>;
setComponentParameters: (component: any) => void;
private focusTrap: ConfigurableFocusTrap;
constructor(
private modalService: ModalService,
private cd: ChangeDetectorRef,
private el: ElementRef<HTMLElement>,
private focusTrapFactory: ConfigurableFocusTrapFactory,
public modalRef: ModalRef
) {}
ngAfterViewInit() {
this.loadChildComponent(this.childComponentType);
if (this.setComponentParameters != null) {
this.setComponentParameters(this.componentRef.instance);
}
this.cd.detectChanges();
this.modalRef.created(this.el.nativeElement);
this.focusTrap = this.focusTrapFactory.create(
this.el.nativeElement.querySelector(".modal-dialog")
);
if (this.el.nativeElement.querySelector("[appAutoFocus]") == null) {
this.focusTrap.focusFirstTabbableElementWhenReady();
}
}
loadChildComponent(componentType: Type<any>) {
const componentFactory = this.modalService.resolveComponentFactory(componentType);
this.modalContentRef.clear();
this.componentRef = this.modalContentRef.createComponent(componentFactory);
}
ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
}
this.focusTrap.destroy();
}
close() {
this.modalRef.close();
}
getFocus() {
const autoFocusEl = this.el.nativeElement.querySelector("[appAutoFocus]") as HTMLElement;
autoFocusEl?.focus();
}
}

View File

@@ -0,0 +1,18 @@
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
export class ModalInjector implements Injector {
constructor(private _parentInjector: Injector, private _additionalTokens: WeakMap<any, any>) {}
get<T>(
token: ProviderToken<T>,
notFoundValue: undefined,
options: InjectOptions & { optional?: false }
): T;
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | InjectFlags): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
get(token: any, notFoundValue?: any): any;
get(token: any, notFoundValue?: any, flags?: any): any {
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);
}
}

View File

@@ -0,0 +1,50 @@
import { Observable, Subject } from "rxjs";
import { first } from "rxjs/operators";
export class ModalRef {
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
onClose: Observable<any>; // Initiated close.
onClosed: Observable<any>; // Modal was closed (Remove element from DOM)
onShow: Observable<void>; // Start showing modal
onShown: Observable<void>; // Modal is fully visible
private readonly _onCreated = new Subject<HTMLElement>();
private readonly _onClose = new Subject<any>();
private readonly _onClosed = new Subject<any>();
private readonly _onShow = new Subject<void>();
private readonly _onShown = new Subject<void>();
private lastResult: any;
constructor() {
this.onCreated = this._onCreated.asObservable();
this.onClose = this._onClose.asObservable();
this.onClosed = this._onClosed.asObservable();
this.onShow = this._onShow.asObservable();
this.onShown = this._onShow.asObservable();
}
show() {
this._onShow.next();
}
shown() {
this._onShown.next();
}
close(result?: any) {
this.lastResult = result;
this._onClose.next(result);
}
closed() {
this._onClosed.next(this.lastResult);
}
created(el: HTMLElement) {
this._onCreated.next(el);
}
onClosedPromise(): Promise<any> {
return this.onClosed.pipe(first()).toPromise();
}
}

View File

@@ -0,0 +1,41 @@
import { Directive } from "@angular/core";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ModalRef } from "./modal/modal.ref";
/**
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
* See UserVerificationComponent for any other situation where you need to verify the user's identity.
*/
@Directive()
export class PasswordRepromptComponent {
showPassword = false;
masterPassword = "";
constructor(
private modalRef: ModalRef,
private cryptoService: CryptoService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
) {}
togglePassword() {
this.showPassword = !this.showPassword;
}
async submit() {
if (!(await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, null))) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidMasterPassword")
);
return;
}
this.modalRef.close(true);
}
}

View File

@@ -0,0 +1,95 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { CommonModule } from "@angular/common";
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
import {
DefaultNoComponentGlobalConfig,
GlobalConfig,
Toast as BaseToast,
ToastPackage,
ToastrService,
TOAST_CONFIG,
} from "ngx-toastr";
@Component({
selector: "[toast-component2]",
template: `
<button
*ngIf="options.closeButton"
(click)="remove()"
type="button"
class="toast-close-button"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
<div class="icon">
<i></i>
</div>
<div>
<div *ngIf="title" [class]="options.titleClass" [attr.aria-label]="title">
{{ title }} <ng-container *ngIf="duplicatesCount">[{{ duplicatesCount + 1 }}]</ng-container>
</div>
<div
*ngIf="message && options.enableHtml"
role="alertdialog"
aria-live="polite"
[class]="options.messageClass"
[innerHTML]="message"
></div>
<div
*ngIf="message && !options.enableHtml"
role="alertdialog"
aria-live="polite"
[class]="options.messageClass"
[attr.aria-label]="message"
>
{{ message }}
</div>
</div>
<div *ngIf="options.progressBar">
<div class="toast-progress" [style.width]="width + '%'"></div>
</div>
`,
animations: [
trigger("flyInOut", [
state("inactive", style({ opacity: 0 })),
state("active", style({ opacity: 1 })),
state("removed", style({ opacity: 0 })),
transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")),
transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")),
]),
],
preserveWhitespaces: false,
})
export class BitwardenToast extends BaseToast {
constructor(protected toastrService: ToastrService, public toastPackage: ToastPackage) {
super(toastrService, toastPackage);
}
}
export const BitwardenToastGlobalConfig: GlobalConfig = {
...DefaultNoComponentGlobalConfig,
toastComponent: BitwardenToast,
};
@NgModule({
imports: [CommonModule],
declarations: [BitwardenToast],
exports: [BitwardenToast],
})
export class BitwardenToastModule {
static forRoot(config: Partial<GlobalConfig> = {}): ModuleWithProviders<BitwardenToastModule> {
return {
ngModule: BitwardenToastModule,
providers: [
{
provide: TOAST_CONFIG,
useValue: {
default: BitwardenToastGlobalConfig,
config: config,
},
},
],
};
}
}

View File

@@ -0,0 +1,46 @@
<ng-container *ngIf="!usesKeyConnector">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
<small class="form-text text-muted">{{ "confirmIdentity" | i18n }}</small>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<div class="form-group">
<label class="d-block">{{ "sendVerificationCode" | i18n }}</label>
<button
type="button"
class="btn btn-outline-secondary"
(click)="requestOTP()"
[disabled]="disableRequestOTP"
>
{{ "sendCode" | i18n }}
</button>
<span class="ml-2 text-success" role="alert" @sent *ngIf="sentCode">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "codeSent" | i18n }}
</span>
</div>
<div class="form-group">
<label for="verificationCode">{{ "verificationCode" | i18n }}</label>
<input
id="verificationCode"
type="input"
name="verificationCode"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
<small class="form-text text-muted">{{ "confirmIdentity" | i18n }}</small>
</div>
</ng-container>

View File

@@ -0,0 +1,96 @@
import { animate, style, transition, trigger } from "@angular/animations";
import { Component, OnInit } from "@angular/core";
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from "@angular/forms";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { VerificationType } from "jslib-common/enums/verificationType";
import { Verification } from "jslib-common/types/verification";
/**
* Used for general-purpose user verification throughout the app.
* Collects the user's master password, or if they are using Key Connector, prompts for an OTP via email.
* This is exposed to the parent component via the ControlValueAccessor interface (e.g. bind it to a FormControl).
* Use UserVerificationService to verify the user's input.
*/
@Component({
selector: "app-user-verification",
templateUrl: "user-verification.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: UserVerificationComponent,
},
],
animations: [
trigger("sent", [
transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]),
]),
],
})
export class UserVerificationComponent implements ControlValueAccessor, OnInit {
usesKeyConnector = false;
disableRequestOTP = false;
sentCode = false;
secret = new FormControl("");
private onChange: (value: Verification) => void;
constructor(
private keyConnectorService: KeyConnectorService,
private userVerificationService: UserVerificationService
) {}
async ngOnInit() {
this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
this.processChanges(this.secret.value);
this.secret.valueChanges.subscribe((secret: string) => this.processChanges(secret));
}
async requestOTP() {
if (this.usesKeyConnector) {
this.disableRequestOTP = true;
try {
await this.userVerificationService.requestOTP();
this.sentCode = true;
} finally {
this.disableRequestOTP = false;
}
}
}
writeValue(obj: any): void {
this.secret.setValue(obj);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
// Not implemented
}
setDisabledState?(isDisabled: boolean): void {
this.disableRequestOTP = isDisabled;
if (isDisabled) {
this.secret.disable();
} else {
this.secret.enable();
}
}
private processChanges(secret: string) {
if (this.onChange == null) {
return;
}
this.onChange({
type: this.usesKeyConnector ? VerificationType.OTP : VerificationType.MasterPassword,
secret: secret,
});
}
}

View File

@@ -0,0 +1,23 @@
import { Directive, ElementRef, Input, Renderer2 } from "@angular/core";
@Directive({
selector: "[appA11yTitle]",
})
export class A11yTitleDirective {
@Input() set appA11yTitle(title: string) {
this.title = title;
}
private title: string;
constructor(private el: ElementRef, private renderer: Renderer2) {}
ngOnInit() {
if (!this.el.nativeElement.hasAttribute("title")) {
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
}
if (!this.el.nativeElement.hasAttribute("aria-label")) {
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
}
}
}

View File

@@ -0,0 +1,49 @@
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
import { LogService } from "jslib-common/abstractions/log.service";
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import { ValidationService } from "../services/validation.service";
/**
* Provides error handling, in particular for any error returned by the server in an api call.
* Attach it to a <form> element and provide the name of the class property that will hold the api call promise.
* e.g. <form [appApiAction]="this.formPromise">
* Any errors/rejections that occur will be intercepted and displayed as error toasts.
*/
@Directive({
selector: "[appApiAction]",
})
export class ApiActionDirective implements OnChanges {
@Input() appApiAction: Promise<any>;
constructor(
private el: ElementRef,
private validationService: ValidationService,
private logService: LogService
) {}
ngOnChanges(changes: any) {
if (this.appApiAction == null || this.appApiAction.then == null) {
return;
}
this.el.nativeElement.loading = true;
this.appApiAction.then(
(response: any) => {
this.el.nativeElement.loading = false;
},
(e: any) => {
this.el.nativeElement.loading = false;
if ((e as ErrorResponse).captchaRequired) {
this.logService.error("Captcha required error response: " + e.getSingleMessage());
return;
}
this.logService?.error(`Received API exception: ${e}`);
this.validationService.showError(e);
}
);
}
}

View File

@@ -0,0 +1,27 @@
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
import { take } from "rxjs/operators";
import { Utils } from "jslib-common/misc/utils";
@Directive({
selector: "[appAutofocus]",
})
export class AutofocusDirective {
@Input() set appAutofocus(condition: boolean | string) {
this.autofocus = condition === "" || condition === true;
}
private autofocus: boolean;
constructor(private el: ElementRef, private ngZone: NgZone) {}
ngOnInit() {
if (!Utils.isMobileBrowser && this.autofocus) {
if (this.ngZone.isStable) {
this.el.nativeElement.focus();
} else {
this.ngZone.onStable.pipe(take(1)).subscribe(() => this.el.nativeElement.focus());
}
}
}
}

View File

@@ -0,0 +1,12 @@
import { Directive, ElementRef, HostListener } from "@angular/core";
@Directive({
selector: "[appBlurClick]",
})
export class BlurClickDirective {
constructor(private el: ElementRef) {}
@HostListener("click") onClick() {
this.el.nativeElement.blur();
}
}

View File

@@ -0,0 +1,59 @@
import { Directive, ElementRef, HostListener, OnInit } from "@angular/core";
@Directive({
selector: "[appBoxRow]",
})
export class BoxRowDirective implements OnInit {
el: HTMLElement = null;
formEls: Element[];
constructor(elRef: ElementRef) {
this.el = elRef.nativeElement;
}
ngOnInit(): void {
this.formEls = Array.from(
this.el.querySelectorAll('input:not([type="hidden"]), select, textarea')
);
this.formEls.forEach((formEl) => {
formEl.addEventListener(
"focus",
() => {
this.el.classList.add("active");
},
false
);
formEl.addEventListener(
"blur",
() => {
this.el.classList.remove("active");
},
false
);
});
}
@HostListener("click", ["$event"]) onClick(event: Event) {
const target = event.target as HTMLElement;
if (
target !== this.el &&
!target.classList.contains("progress") &&
!target.classList.contains("progress-bar")
) {
return;
}
if (this.formEls.length > 0) {
const formEl = this.formEls[0] as HTMLElement;
if (formEl.tagName.toLowerCase() === "input") {
const inputEl = formEl as HTMLInputElement;
if (inputEl.type != null && inputEl.type.toLowerCase() === "checkbox") {
inputEl.click();
return;
}
}
formEl.focus();
}
}
}

View File

@@ -0,0 +1,14 @@
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
@Directive({
selector: "[appFallbackSrc]",
})
export class FallbackSrcDirective {
@Input("appFallbackSrc") appFallbackSrc: string;
constructor(private el: ElementRef) {}
@HostListener("error") onError() {
this.el.nativeElement.src = this.appFallbackSrc;
}
}

View File

@@ -0,0 +1,10 @@
import { Directive, HostListener } from "@angular/core";
@Directive({
selector: "[appStopClick]",
})
export class StopClickDirective {
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
$event.preventDefault();
}
}

View File

@@ -0,0 +1,10 @@
import { Directive, HostListener } from "@angular/core";
@Directive({
selector: "[appStopProp]",
})
export class StopPropDirective {
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
$event.stopPropagation();
}
}

View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@Pipe({
name: "i18n",
})
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
return this.i18nService.t(id, p1, p2, p3);
}
}

View File

@@ -0,0 +1,41 @@
import { Pipe, PipeTransform } from "@angular/core";
import { CipherView } from "jslib-common/models/view/cipherView";
@Pipe({
name: "searchCiphers",
})
export class SearchCiphersPipe implements PipeTransform {
transform(ciphers: CipherView[], searchText: string, deleted = false): CipherView[] {
if (ciphers == null || ciphers.length === 0) {
return [];
}
if (searchText == null || searchText.length < 2) {
return ciphers.filter((c) => {
return deleted !== c.isDeleted;
});
}
searchText = searchText.trim().toLowerCase();
return ciphers.filter((c) => {
if (deleted !== c.isDeleted) {
return false;
}
if (c.name != null && c.name.toLowerCase().indexOf(searchText) > -1) {
return true;
}
if (searchText.length >= 8 && c.id.startsWith(searchText)) {
return true;
}
if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(searchText) > -1) {
return true;
}
if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(searchText) > -1) {
return true;
}
return false;
});
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,250 @@
$icomoon-font-family: "bwi-font" !default;
$icomoon-font-path: "~@bitwarden/jslib-angular/src/scss/bwicons/fonts/" !default;
// New font sheet? Update the font-face information below
@font-face {
font-family: "#{$icomoon-font-family}";
src: url($icomoon-font-path + "bwi-font.svg") format("svg"),
url($icomoon-font-path + "bwi-font.ttf") format("truetype"),
url($icomoon-font-path + "bwi-font.woff") format("woff"),
url($icomoon-font-path + "bwi-font.woff2") format("woff2");
font-weight: normal;
font-style: normal;
font-display: block;
}
// Base Class
.bwi {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: "#{$icomoon-font-family}" !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
display: inline-block;
/* Better Font Rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// Fixed Width Icons
.bwi-fw {
width: calc(18em / 14);
text-align: center;
}
// Sizing Changes
.bwi-sm {
font-size: 0.875em;
}
.bwi-lg {
font-size: calc(4em / 3);
line-height: calc(3em / 4);
vertical-align: -15%;
}
.bwi-2x {
font-size: 2em;
}
.bwi-3x {
font-size: 3em;
}
.bwi-4x {
font-size: 4em;
}
// Spin Animations
.bwi-spin {
animation: bwi-spin 2s infinite linear;
}
@keyframes bwi-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
// List Icons
.bwi-ul {
padding-left: 0;
margin-left: calc(30em / 14);
list-style-type: none;
> li {
position: relative;
}
}
.bwi-li {
position: absolute;
left: calc(-30em / 14);
width: calc(30em / 14);
top: calc(2em / 14);
text-align: center;
&.bwi-lg {
left: calc(-30em / 14) + calc(4em / 14);
}
}
// Rotation
.bwi-rotate-270 {
transform: rotate(270deg);
}
// For new icons - add their glyph name and value to the map below
$icons: (
"save-changes": "\e988",
"browser": "\e985",
"mobile": "\e986",
"cli": "\e987",
"providers": "\e983",
"vault": "\e984",
"folder-closed-f": "\e982",
"rocket": "\e9ee",
"ellipsis-h": "\e9ef",
"ellipsis-v": "\e9f0",
"safari": "\e974",
"opera": "\e975",
"firefox": "\e976",
"edge": "\e977",
"chrome": "\e978",
"star-f": "\e979",
"arrow-circle-up": "\e97a",
"arrow-circle-right": "\e97b",
"arrow-circle-left": "\e97c",
"arrow-circle-down": "\e97d",
"undo": "\e97e",
"bolt": "\e97f",
"puzzle": "\e980",
"rss": "\e973",
"dbl-angle-left": "\e970",
"dbl-angle-right": "\e971",
"hamburger": "\e972",
"bw-folder-open-f": "\e93e",
"desktop": "\e96a",
"angle-left": "\e96b",
"user": "\e900",
"user-f": "\e901",
"key": "\e902",
"share-square": "\e903",
"hashtag": "\e904",
"clone": "\e905",
"list-alt": "\e906",
"id-card": "\e907",
"credit-card": "\e908",
"globe": "\e909",
"sticky-note": "\e90a",
"folder": "\e90b",
"lock": "\e90c",
"lock-f": "\e90d",
"generate": "\e90e",
"generate-f": "\e90f",
"cog": "\e910",
"cog-f": "\e911",
"check-circle": "\e912",
"eye": "\e913",
"pencil-square": "\e914",
"bookmark": "\e915",
"files": "\e916",
"trash": "\e917",
"plus": "\e918",
"star": "\e919",
"list": "\e91a",
"angle-right": "\e91b",
"external-link": "\e91c",
"refresh": "\e91d",
"search": "\e91f",
"filter": "\e920",
"plus-circle": "\e921",
"user-circle": "\e922",
"question-circle": "\e923",
"cogs": "\e924",
"minus-circle": "\e925",
"send": "\e926",
"send-f": "\e927",
"download": "\e928",
"pencil": "\e929",
"sign-out": "\e92a",
"share": "\e92b",
"clock": "\e92c",
"angle-down": "\e92d",
"caret-down": "\e92e",
"square": "\e92f",
"collection": "\e930",
"bank": "\e931",
"shield": "\e932",
"stop": "\e933",
"plus-square": "\e934",
"save": "\e935",
"sign-in": "\e936",
"spinner": "\e937",
"dollar": "\e939",
"check": "\e93a",
"check-square": "\e93b",
"minus-square": "\e93c",
"close": "\e93d",
"share-arrow": "\e96c",
"paperclip": "\e93f",
"bitcoin": "\e940",
"cut": "\e941",
"frown": "\e942",
"folder-open": "\e943",
"bug": "\e946",
"chain-broken": "\e947",
"dashboard": "\e948",
"envelope": "\e949",
"exclamation-circle": "\e94a",
"exclamation-triangle": "\e94b",
"caret-right": "\e94c",
"file-pdf": "\e94e",
"file-text": "\e94f",
"info-circle": "\e952",
"lightbulb": "\e953",
"link": "\e954",
"linux": "\e956",
"long-arrow-right": "\e957",
"money": "\e958",
"play": "\e959",
"reddit": "\e95a",
"refresh-tab": "\e95b",
"sitemap": "\e95c",
"sliders": "\e95d",
"tag": "\e95e",
"thumb-tack": "\e95f",
"thumbs-up": "\e960",
"unlock": "\e962",
"users": "\e963",
"wrench": "\e965",
"ban": "\e967",
"camera": "\e968",
"chevron-up": "\e969",
"eye-slash": "\e96d",
"file": "\e96e",
"paste": "\e96f",
"github": "\e950",
"facebook": "\e94d",
"paypal": "\e938",
"google": "\e951",
"linkedin": "\e955",
"discourse": "\e91e",
"twitter": "\e961",
"youtube": "\e966",
"windows": "\e964",
"apple": "\e945",
"android": "\e944",
"error": "\e981",
"numbered-list": "\e989",
);
@each $name, $glyph in $icons {
.bwi-#{$name}:before {
content: $glyph;
}
}

View File

@@ -0,0 +1,44 @@
$card-icons-base: "~@bitwarden/jslib-angular/src/images/cards/";
$card-icons: (
"visa": $card-icons-base + "visa-light.png",
"amex": $card-icons-base + "amex-light.png",
"diners-club": $card-icons-base + "diners_club-light.png",
"discover": $card-icons-base + "discover-light.png",
"jcb": $card-icons-base + "jcb-light.png",
"maestro": $card-icons-base + "maestro-light.png",
"mastercard": $card-icons-base + "mastercard-light.png",
"union-pay": $card-icons-base + "union_pay-light.png",
);
$card-icons-dark: (
"visa": $card-icons-base + "visa-dark.png",
"amex": $card-icons-base + "amex-dark.png",
"diners-club": $card-icons-base + "diners_club-dark.png",
"discover": $card-icons-base + "discover-dark.png",
"jcb": $card-icons-base + "jcb-dark.png",
"maestro": $card-icons-base + "maestro-dark.png",
"mastercard": $card-icons-base + "mastercard-dark.png",
"union-pay": $card-icons-base + "union_pay-dark.png",
);
.credit-card-icon {
display: block; // Resolves the parent container being slighly to big
height: 19px;
width: 24px;
background-size: contain;
background-repeat: no-repeat;
}
@each $name, $url in $card-icons {
.card-#{$name} {
background-image: url("#{$url}");
}
}
@each $theme in $dark-icon-themes {
@each $name, $url in $card-icons-dark {
.#{$theme} .card-#{$name} {
background-image: url("#{$url}");
}
}
}

View File

@@ -0,0 +1,89 @@
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 300;
font-display: auto;
src: url(webfonts/Open_Sans-italic-300.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400;
font-display: auto;
src: url(webfonts/Open_Sans-italic-400.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 600;
font-display: auto;
src: url(webfonts/Open_Sans-italic-600.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 700;
font-display: auto;
src: url(webfonts/Open_Sans-italic-700.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 800;
font-display: auto;
src: url(webfonts/Open_Sans-italic-800.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 300;
font-display: auto;
src: url(webfonts/Open_Sans-normal-300.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400;
font-display: auto;
src: url(webfonts/Open_Sans-normal-400.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 600;
font-display: auto;
src: url(webfonts/Open_Sans-normal-600.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 700;
font-display: auto;
src: url(webfonts/Open_Sans-normal-700.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 800;
font-display: auto;
src: url(webfonts/Open_Sans-normal-800.woff) format("woff");
unicode-range: U+0-10FFFF;
}

View File

@@ -0,0 +1,45 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private messagingService: MessagingService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService
) {}
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
const isAuthed = await this.stateService.getIsAuthenticated();
if (!isAuthed) {
this.messagingService.send("authBlocked");
return false;
}
const locked = await this.vaultTimeoutService.isLocked();
if (locked) {
if (routerState != null) {
this.messagingService.send("lockedUrl", { url: routerState.url });
}
this.router.navigate(["lock"], { queryParams: { promptBiometric: true } });
return false;
}
if (
!routerState.url.includes("remove-password") &&
(await this.keyConnectorService.getConvertAccountRequired())
) {
this.router.navigate(["/remove-password"]);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,6 @@
import { Injectable } from "@angular/core";
import { BroadcasterService as BaseBroadcasterService } from "jslib-common/services/broadcaster.service";
@Injectable()
export class BroadcasterService extends BaseBroadcasterService {}

View File

@@ -0,0 +1,492 @@
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
import { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service";
import { AuditService as AuditServiceAbstraction } from "jslib-common/abstractions/audit.service";
import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "jslib-common/abstractions/broadcaster.service";
import { CipherService as CipherServiceAbstraction } from "jslib-common/abstractions/cipher.service";
import { CollectionService as CollectionServiceAbstraction } from "jslib-common/abstractions/collection.service";
import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "jslib-common/abstractions/environment.service";
import { EventService as EventServiceAbstraction } from "jslib-common/abstractions/event.service";
import { ExportService as ExportServiceAbstraction } from "jslib-common/abstractions/export.service";
import { FileUploadService as FileUploadServiceAbstraction } from "jslib-common/abstractions/fileUpload.service";
import { FolderService as FolderServiceAbstraction } from "jslib-common/abstractions/folder.service";
import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "jslib-common/abstractions/messaging.service";
import { NotificationsService as NotificationsServiceAbstraction } from "jslib-common/abstractions/notifications.service";
import { OrganizationService as OrganizationServiceAbstraction } from "jslib-common/abstractions/organization.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "jslib-common/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService as PolicyServiceAbstraction } from "jslib-common/abstractions/policy.service";
import { ProviderService as ProviderServiceAbstraction } from "jslib-common/abstractions/provider.service";
import { SearchService as SearchServiceAbstraction } from "jslib-common/abstractions/search.service";
import { SendService as SendServiceAbstraction } from "jslib-common/abstractions/send.service";
import { SettingsService as SettingsServiceAbstraction } from "jslib-common/abstractions/settings.service";
import { StateService as StateServiceAbstraction } from "jslib-common/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "jslib-common/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "jslib-common/abstractions/sync.service";
import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service";
import { TotpService as TotpServiceAbstraction } from "jslib-common/abstractions/totp.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "jslib-common/abstractions/userVerification.service";
import { UsernameGenerationService as UsernameGenerationServiceAbstraction } from "jslib-common/abstractions/usernameGeneration.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service";
import { StateFactory } from "jslib-common/factories/stateFactory";
import { Account } from "jslib-common/models/domain/account";
import { GlobalState } from "jslib-common/models/domain/globalState";
import { ApiService } from "jslib-common/services/api.service";
import { AppIdService } from "jslib-common/services/appId.service";
import { AuditService } from "jslib-common/services/audit.service";
import { AuthService } from "jslib-common/services/auth.service";
import { CipherService } from "jslib-common/services/cipher.service";
import { CollectionService } from "jslib-common/services/collection.service";
import { ConsoleLogService } from "jslib-common/services/consoleLog.service";
import { CryptoService } from "jslib-common/services/crypto.service";
import { EnvironmentService } from "jslib-common/services/environment.service";
import { EventService } from "jslib-common/services/event.service";
import { ExportService } from "jslib-common/services/export.service";
import { FileUploadService } from "jslib-common/services/fileUpload.service";
import { FolderService } from "jslib-common/services/folder.service";
import { KeyConnectorService } from "jslib-common/services/keyConnector.service";
import { NotificationsService } from "jslib-common/services/notifications.service";
import { OrganizationService } from "jslib-common/services/organization.service";
import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service";
import { PolicyService } from "jslib-common/services/policy.service";
import { ProviderService } from "jslib-common/services/provider.service";
import { SearchService } from "jslib-common/services/search.service";
import { SendService } from "jslib-common/services/send.service";
import { SettingsService } from "jslib-common/services/settings.service";
import { StateService } from "jslib-common/services/state.service";
import { StateMigrationService } from "jslib-common/services/stateMigration.service";
import { SyncService } from "jslib-common/services/sync.service";
import { TokenService } from "jslib-common/services/token.service";
import { TotpService } from "jslib-common/services/totp.service";
import { TwoFactorService } from "jslib-common/services/twoFactor.service";
import { UserVerificationService } from "jslib-common/services/userVerification.service";
import { UsernameGenerationService } from "jslib-common/services/usernameGeneration.service";
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service";
import { AuthGuardService } from "./auth-guard.service";
import { BroadcasterService } from "./broadcaster.service";
import { LockGuardService } from "./lock-guard.service";
import { ModalService } from "./modal.service";
import { PasswordRepromptService } from "./passwordReprompt.service";
import { UnauthGuardService } from "./unauth-guard.service";
import { ValidationService } from "./validation.service";
@NgModule({
declarations: [],
providers: [
{ provide: "WINDOW", useValue: window },
{
provide: LOCALE_ID,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
deps: [I18nServiceAbstraction],
},
ValidationService,
AuthGuardService,
UnauthGuardService,
LockGuardService,
ModalService,
{
provide: AppIdServiceAbstraction,
useClass: AppIdService,
deps: [StorageServiceAbstraction],
},
{
provide: AuditServiceAbstraction,
useClass: AuditService,
deps: [CryptoFunctionServiceAbstraction, ApiServiceAbstraction],
},
{
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
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,
useClass: EnvironmentService,
deps: [StateServiceAbstraction],
},
{
provide: TotpServiceAbstraction,
useClass: TotpService,
deps: [CryptoFunctionServiceAbstraction, LogService, StateServiceAbstraction],
},
{ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] },
{
provide: CryptoServiceAbstraction,
useClass: CryptoService,
deps: [
CryptoFunctionServiceAbstraction,
PlatformUtilsServiceAbstraction,
LogService,
StateServiceAbstraction,
],
},
{
provide: PasswordGenerationServiceAbstraction,
useClass: PasswordGenerationService,
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
},
{
provide: UsernameGenerationServiceAbstraction,
useClass: UsernameGenerationService,
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
},
{
provide: ApiServiceAbstraction,
useFactory: (
tokenService: TokenServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
appIdService: AppIdServiceAbstraction
) =>
new ApiService(
tokenService,
platformUtilsService,
environmentService,
appIdService,
async (expired: boolean) => messagingService.send("logout", { expired: expired })
),
deps: [
TokenServiceAbstraction,
PlatformUtilsServiceAbstraction,
EnvironmentServiceAbstraction,
MessagingServiceAbstraction,
AppIdServiceAbstraction,
],
},
{
provide: FileUploadServiceAbstraction,
useClass: FileUploadService,
deps: [LogService, ApiServiceAbstraction],
},
{
provide: SyncServiceAbstraction,
useFactory: (
apiService: ApiServiceAbstraction,
settingsService: SettingsServiceAbstraction,
folderService: FolderServiceAbstraction,
cipherService: CipherServiceAbstraction,
cryptoService: CryptoServiceAbstraction,
collectionService: CollectionServiceAbstraction,
messagingService: MessagingServiceAbstraction,
policyService: PolicyServiceAbstraction,
sendService: SendServiceAbstraction,
logService: LogService,
keyConnectorService: KeyConnectorServiceAbstraction,
stateService: StateServiceAbstraction,
organizationService: OrganizationServiceAbstraction,
providerService: ProviderServiceAbstraction
) =>
new SyncService(
apiService,
settingsService,
folderService,
cipherService,
cryptoService,
collectionService,
messagingService,
policyService,
sendService,
logService,
keyConnectorService,
stateService,
organizationService,
providerService,
async (expired: boolean) => messagingService.send("logout", { expired: expired })
),
deps: [
ApiServiceAbstraction,
SettingsServiceAbstraction,
FolderServiceAbstraction,
CipherServiceAbstraction,
CryptoServiceAbstraction,
CollectionServiceAbstraction,
MessagingServiceAbstraction,
PolicyServiceAbstraction,
SendServiceAbstraction,
LogService,
KeyConnectorServiceAbstraction,
StateServiceAbstraction,
OrganizationServiceAbstraction,
ProviderServiceAbstraction,
],
},
{ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService },
{
provide: SettingsServiceAbstraction,
useClass: SettingsService,
deps: [StateServiceAbstraction],
},
{
provide: VaultTimeoutServiceAbstraction,
useFactory: (
cipherService: CipherServiceAbstraction,
folderService: FolderServiceAbstraction,
collectionService: CollectionServiceAbstraction,
cryptoService: CryptoServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
messagingService: MessagingServiceAbstraction,
searchService: SearchServiceAbstraction,
tokenService: TokenServiceAbstraction,
policyService: PolicyServiceAbstraction,
keyConnectorService: KeyConnectorServiceAbstraction,
stateService: StateServiceAbstraction
) =>
new VaultTimeoutService(
cipherService,
folderService,
collectionService,
cryptoService,
platformUtilsService,
messagingService,
searchService,
tokenService,
policyService,
keyConnectorService,
stateService,
null,
async (userId?: string) =>
messagingService.send("logout", { expired: false, userId: userId })
),
deps: [
CipherServiceAbstraction,
FolderServiceAbstraction,
CollectionServiceAbstraction,
CryptoServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
SearchServiceAbstraction,
TokenServiceAbstraction,
PolicyServiceAbstraction,
KeyConnectorServiceAbstraction,
StateServiceAbstraction,
],
},
{
provide: StateServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction,
logService: LogService,
stateMigrationService: StateMigrationServiceAbstraction
) =>
new StateService(
storageService,
secureStorageService,
logService,
stateMigrationService,
new StateFactory(GlobalState, Account)
),
deps: [
StorageServiceAbstraction,
"SECURE_STORAGE",
LogService,
StateMigrationServiceAbstraction,
],
},
{
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction
) =>
new StateMigrationService(
storageService,
secureStorageService,
new StateFactory(GlobalState, Account)
),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
},
{
provide: ExportServiceAbstraction,
useClass: ExportService,
deps: [
FolderServiceAbstraction,
CipherServiceAbstraction,
ApiServiceAbstraction,
CryptoServiceAbstraction,
],
},
{
provide: SearchServiceAbstraction,
useClass: SearchService,
deps: [CipherServiceAbstraction, LogService, I18nServiceAbstraction],
},
{
provide: NotificationsServiceAbstraction,
useFactory: (
syncService: SyncServiceAbstraction,
appIdService: AppIdServiceAbstraction,
apiService: ApiServiceAbstraction,
vaultTimeoutService: VaultTimeoutServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
logService: LogService,
stateService: StateServiceAbstraction
) =>
new NotificationsService(
syncService,
appIdService,
apiService,
vaultTimeoutService,
environmentService,
async () => messagingService.send("logout", { expired: true }),
logService,
stateService
),
deps: [
SyncServiceAbstraction,
AppIdServiceAbstraction,
ApiServiceAbstraction,
VaultTimeoutServiceAbstraction,
EnvironmentServiceAbstraction,
MessagingServiceAbstraction,
LogService,
StateServiceAbstraction,
],
},
{
provide: CryptoFunctionServiceAbstraction,
useClass: WebCryptoFunctionService,
deps: ["WINDOW"],
},
{
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 {}

View File

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

View File

@@ -0,0 +1,180 @@
import {
ApplicationRef,
ComponentFactory,
ComponentFactoryResolver,
ComponentRef,
EmbeddedViewRef,
Injectable,
Injector,
Type,
ViewContainerRef,
} from "@angular/core";
import { first } from "rxjs/operators";
import { DynamicModalComponent } from "../components/modal/dynamic-modal.component";
import { ModalInjector } from "../components/modal/modal-injector";
import { ModalRef } from "../components/modal/modal.ref";
export class ModalConfig<D = any> {
data?: D;
allowMultipleModals = false;
}
@Injectable()
export class ModalService {
protected modalList: ComponentRef<DynamicModalComponent>[] = [];
// Lazy loaded modules are not available in componentFactoryResolver,
// therefore modules needs to manually initialize their resolvers.
private factoryResolvers: Map<Type<any>, ComponentFactoryResolver> = new Map();
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private applicationRef: ApplicationRef,
private injector: Injector
) {
document.addEventListener("keyup", (event) => {
if (event.key === "Escape" && this.modalCount > 0) {
this.topModal.instance.close();
}
});
}
get modalCount() {
return this.modalList.length;
}
private get topModal() {
return this.modalList[this.modalCount - 1];
}
async openViewRef<T>(
componentType: Type<T>,
viewContainerRef: ViewContainerRef,
setComponentParameters: (component: T) => void = null
): Promise<[ModalRef, T]> {
const [modalRef, modalComponentRef] = this.openInternal(componentType, null, false);
modalComponentRef.instance.setComponentParameters = setComponentParameters;
viewContainerRef.insert(modalComponentRef.hostView);
await modalRef.onCreated.pipe(first()).toPromise();
return [modalRef, modalComponentRef.instance.componentRef.instance];
}
open(componentType: Type<any>, config?: ModalConfig) {
if (!(config?.allowMultipleModals ?? false) && this.modalCount > 0) {
return;
}
// eslint-disable-next-line
const [modalRef, _] = this.openInternal(componentType, config, true);
return modalRef;
}
registerComponentFactoryResolver<T>(
componentType: Type<T>,
componentFactoryResolver: ComponentFactoryResolver
): void {
this.factoryResolvers.set(componentType, componentFactoryResolver);
}
resolveComponentFactory<T>(componentType: Type<T>): ComponentFactory<T> {
if (this.factoryResolvers.has(componentType)) {
return this.factoryResolvers.get(componentType).resolveComponentFactory(componentType);
}
return this.componentFactoryResolver.resolveComponentFactory(componentType);
}
protected openInternal(
componentType: Type<any>,
config?: ModalConfig,
attachToDom?: boolean
): [ModalRef, ComponentRef<DynamicModalComponent>] {
const [modalRef, componentRef] = this.createModalComponent(config);
componentRef.instance.childComponentType = componentType;
if (attachToDom) {
this.applicationRef.attachView(componentRef.hostView);
const domElem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
document.body.appendChild(domElem);
}
modalRef.onClosed.pipe(first()).subscribe(() => {
if (attachToDom) {
this.applicationRef.detachView(componentRef.hostView);
}
componentRef.destroy();
this.modalList.pop();
if (this.modalCount > 0) {
this.topModal.instance.getFocus();
}
});
this.setupHandlers(modalRef);
this.modalList.push(componentRef);
return [modalRef, componentRef];
}
protected setupHandlers(modalRef: ModalRef) {
let backdrop: HTMLElement = null;
// Add backdrop, setup [data-dismiss] handler.
modalRef.onCreated.pipe(first()).subscribe((el) => {
document.body.classList.add("modal-open");
const modalEl: HTMLElement = el.querySelector(".modal");
const dialogEl = modalEl.querySelector(".modal-dialog") as HTMLElement;
backdrop = document.createElement("div");
backdrop.className = "modal-backdrop fade";
backdrop.style.zIndex = `${this.modalCount}040`;
modalEl.prepend(backdrop);
dialogEl.addEventListener("click", (e: Event) => {
e.stopPropagation();
});
dialogEl.style.zIndex = `${this.modalCount}050`;
const modals = Array.from(
el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]')
);
for (const closeElement of modals) {
closeElement.addEventListener("click", () => {
modalRef.close();
});
}
});
// onClose is used in Web to hook into bootstrap. On other projects we pipe it directly to closed.
modalRef.onClose.pipe(first()).subscribe(() => {
modalRef.closed();
if (this.modalCount === 0) {
document.body.classList.remove("modal-open");
}
});
}
protected createModalComponent(
config: ModalConfig
): [ModalRef, ComponentRef<DynamicModalComponent>] {
const modalRef = new ModalRef();
const map = new WeakMap();
map.set(ModalConfig, config);
map.set(ModalRef, modalRef);
const componentFactory =
this.componentFactoryResolver.resolveComponentFactory(DynamicModalComponent);
const componentRef = componentFactory.create(new ModalInjector(this.injector, map));
return [modalRef, componentRef];
}
}

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
import { Injectable } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
@Injectable()
export class ValidationService {
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService
) {}
showError(data: any): string[] {
const defaultErrorMessage = this.i18nService.t("unexpectedError");
let errors: string[] = [];
if (data != null && typeof data === "string") {
errors.push(data);
} else if (data == null || typeof data !== "object") {
errors.push(defaultErrorMessage);
} else if (data.validationErrors != null) {
errors = errors.concat((data as ErrorResponse).getAllMessages());
} else {
errors.push(data.message ? data.message : defaultErrorMessage);
}
if (errors.length === 1) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errors[0]);
} else if (errors.length > 1) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errors, {
timeout: 5000 * errors.length,
});
}
return errors;
}
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../shared/tsconfig",
"compilerOptions": {
"paths": {
"jslib-common/*": ["../common/src/*"],
"jslib-angular/*": ["./src/"]
}
},
"include": ["src", "spec"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "./tsconfig.json"
}

View File

@@ -0,0 +1,18 @@
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { compilerOptions } = require("./tsconfig");
module.exports = {
name: "common",
displayName: "common jslib tests",
preset: "ts-jest",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),
};

13
jslib/common/package-lock.json generated Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "@bitwarden/jslib-common",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/jslib-common",
"version": "0.0.0",
"license": "GPL-3.0"
}
}
}

21
jslib/common/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "@bitwarden/jslib-common",
"version": "0.0.0",
"description": "Common code used across Bitwarden JavaScript projects.",
"keywords": [
"bitwarden"
],
"author": "Bitwarden Inc.",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/jslib"
},
"license": "GPL-3.0",
"scripts": {
"clean": "rimraf dist/**/*",
"build": "npm run clean && tsc",
"build:watch": "npm run clean && tsc -watch"
},
"dependencies": {}
}

View File

@@ -0,0 +1,83 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { AttachmentData } from "jslib-common/models/data/attachmentData";
import { Attachment } from "jslib-common/models/domain/attachment";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { ContainerService } from "jslib-common/services/container.service";
import { makeStaticByteArray, mockEnc } from "../utils";
describe("Attachment", () => {
let data: AttachmentData;
beforeEach(() => {
data = {
id: "id",
url: "url",
fileName: "fileName",
key: "key",
size: "1100",
sizeName: "1.1 KB",
};
});
it("Convert from empty", () => {
const data = new AttachmentData();
const attachment = new Attachment(data);
expect(attachment).toEqual({
id: null,
url: null,
size: undefined,
sizeName: null,
key: null,
fileName: null,
});
});
it("Convert", () => {
const attachment = new Attachment(data);
expect(attachment).toEqual({
size: "1100",
id: "id",
url: "url",
sizeName: "1.1 KB",
fileName: { encryptedString: "fileName", encryptionType: 0 },
key: { encryptedString: "key", encryptionType: 0 },
});
});
it("toAttachmentData", () => {
const attachment = new Attachment(data);
expect(attachment.toAttachmentData()).toEqual(data);
});
it("Decrypt", async () => {
const attachment = new Attachment();
attachment.id = "id";
attachment.url = "url";
attachment.size = "1100";
attachment.sizeName = "1.1 KB";
attachment.key = mockEnc("key");
attachment.fileName = mockEnc("fileName");
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(32));
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
const view = await attachment.decrypt(null);
expect(view).toEqual({
id: "id",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "fileName",
key: expect.any(SymmetricCryptoKey),
});
});
});

View File

@@ -0,0 +1,73 @@
import { CardData } from "jslib-common/models/data/cardData";
import { Card } from "jslib-common/models/domain/card";
import { mockEnc } from "../utils";
describe("Card", () => {
let data: CardData;
beforeEach(() => {
data = {
cardholderName: "encHolder",
brand: "encBrand",
number: "encNumber",
expMonth: "encMonth",
expYear: "encYear",
code: "encCode",
};
});
it("Convert from empty", () => {
const data = new CardData();
const card = new Card(data);
expect(card).toEqual({
cardholderName: null,
brand: null,
number: null,
expMonth: null,
expYear: null,
code: null,
});
});
it("Convert", () => {
const card = new Card(data);
expect(card).toEqual({
cardholderName: { encryptedString: "encHolder", encryptionType: 0 },
brand: { encryptedString: "encBrand", encryptionType: 0 },
number: { encryptedString: "encNumber", encryptionType: 0 },
expMonth: { encryptedString: "encMonth", encryptionType: 0 },
expYear: { encryptedString: "encYear", encryptionType: 0 },
code: { encryptedString: "encCode", encryptionType: 0 },
});
});
it("toCardData", () => {
const card = new Card(data);
expect(card.toCardData()).toEqual(data);
});
it("Decrypt", async () => {
const card = new Card();
card.cardholderName = mockEnc("cardHolder");
card.brand = mockEnc("brand");
card.number = mockEnc("number");
card.expMonth = mockEnc("expMonth");
card.expYear = mockEnc("expYear");
card.code = mockEnc("code");
const view = await card.decrypt(null);
expect(view).toEqual({
_brand: "brand",
_number: "number",
_subTitle: null,
cardholderName: "cardHolder",
code: "code",
expMonth: "expMonth",
expYear: "expYear",
});
});
});

View File

@@ -0,0 +1,599 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from "jslib-common/enums/cipherType";
import { FieldType } from "jslib-common/enums/fieldType";
import { SecureNoteType } from "jslib-common/enums/secureNoteType";
import { UriMatchType } from "jslib-common/enums/uriMatchType";
import { CipherData } from "jslib-common/models/data/cipherData";
import { Card } from "jslib-common/models/domain/card";
import { Cipher } from "jslib-common/models/domain/cipher";
import { Identity } from "jslib-common/models/domain/identity";
import { Login } from "jslib-common/models/domain/login";
import { SecureNote } from "jslib-common/models/domain/secureNote";
import { CardView } from "jslib-common/models/view/cardView";
import { IdentityView } from "jslib-common/models/view/identityView";
import { LoginView } from "jslib-common/models/view/loginView";
import { mockEnc } from "../utils";
describe("Cipher DTO", () => {
it("Convert from empty CipherData", () => {
const data = new CipherData();
const cipher = new Cipher(data);
expect(cipher).toEqual({
id: null,
userId: null,
organizationId: null,
folderId: null,
name: null,
notes: null,
type: undefined,
favorite: undefined,
organizationUseTotp: undefined,
edit: undefined,
viewPassword: true,
revisionDate: null,
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: undefined,
attachments: null,
fields: null,
passwordHistory: null,
});
});
describe("LoginCipher", () => {
let cipherData: CipherData;
beforeEach(() => {
cipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
userId: "userId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Login,
name: "EncryptedString",
notes: "EncryptedString",
deletedDate: null,
reprompt: CipherRepromptType.None,
login: {
uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }],
username: "EncryptedString",
password: "EncryptedString",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "EncryptedString",
autofillOnPageLoad: false,
},
passwordHistory: [
{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" },
],
attachments: [
{
id: "a1",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
{
id: "a2",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
],
fields: [
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Text,
linkedId: null,
},
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Hidden,
linkedId: null,
},
],
};
});
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
id: "id",
userId: "userId",
organizationId: "orgId",
folderId: "folderId",
name: { encryptedString: "EncryptedString", encryptionType: 0 },
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 1,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: 0,
login: {
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
autofillOnPageLoad: false,
username: { encryptedString: "EncryptedString", encryptionType: 0 },
password: { encryptedString: "EncryptedString", encryptionType: 0 },
totp: { encryptedString: "EncryptedString", encryptionType: 0 },
uris: [{ match: 0, uri: { encryptedString: "EncryptedString", encryptionType: 0 } }],
},
attachments: [
{
fileName: { encryptedString: "file", encryptionType: 0 },
id: "a1",
key: { encryptedString: "EncKey", encryptionType: 0 },
size: "1100",
sizeName: "1.1 KB",
url: "url",
},
{
fileName: { encryptedString: "file", encryptionType: 0 },
id: "a2",
key: { encryptedString: "EncKey", encryptionType: 0 },
size: "1100",
sizeName: "1.1 KB",
url: "url",
},
],
fields: [
{
linkedId: null,
name: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 0,
value: { encryptedString: "EncryptedString", encryptionType: 0 },
},
{
linkedId: null,
name: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 1,
value: { encryptedString: "EncryptedString", encryptionType: 0 },
},
],
passwordHistory: [
{
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
password: { encryptedString: "EncryptedString", encryptionType: 0 },
},
],
});
});
it("toCipherData", () => {
const cipher = new Cipher(cipherData);
expect(cipher.toCipherData("userId")).toEqual(cipherData);
});
it("Decrypt", async () => {
const cipher = new Cipher();
cipher.id = "id";
cipher.organizationId = "orgId";
cipher.folderId = "folderId";
cipher.edit = true;
cipher.viewPassword = true;
cipher.organizationUseTotp = true;
cipher.favorite = false;
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
cipher.type = CipherType.Login;
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None;
const loginView = new LoginView();
loginView.username = "username";
loginView.password = "password";
const login = Substitute.for<Login>();
login.decrypt(Arg.any(), Arg.any()).resolves(loginView);
cipher.login = login;
const cipherView = await cipher.decrypt();
expect(cipherView).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: "EncryptedString",
notes: "EncryptedString",
type: 1,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
login: loginView,
attachments: null,
fields: null,
passwordHistory: null,
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
deletedDate: null,
reprompt: 0,
localData: undefined,
});
});
});
describe("SecureNoteCipher", () => {
let cipherData: CipherData;
beforeEach(() => {
cipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
userId: "userId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.SecureNote,
name: "EncryptedString",
notes: "EncryptedString",
deletedDate: null,
reprompt: CipherRepromptType.None,
secureNote: {
type: SecureNoteType.Generic,
},
};
});
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
id: "id",
userId: "userId",
organizationId: "orgId",
folderId: "folderId",
name: { encryptedString: "EncryptedString", encryptionType: 0 },
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 2,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: 0,
secureNote: { type: SecureNoteType.Generic },
attachments: null,
fields: null,
passwordHistory: null,
});
});
it("toCipherData", () => {
const cipher = new Cipher(cipherData);
expect(cipher.toCipherData("userId")).toEqual(cipherData);
});
it("Decrypt", async () => {
const cipher = new Cipher();
cipher.id = "id";
cipher.organizationId = "orgId";
cipher.folderId = "folderId";
cipher.edit = true;
cipher.viewPassword = true;
cipher.organizationUseTotp = true;
cipher.favorite = false;
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
cipher.type = CipherType.SecureNote;
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None;
cipher.secureNote = new SecureNote();
cipher.secureNote.type = SecureNoteType.Generic;
const cipherView = await cipher.decrypt();
expect(cipherView).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: "EncryptedString",
notes: "EncryptedString",
type: 2,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
secureNote: { type: 0 },
attachments: null,
fields: null,
passwordHistory: null,
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
deletedDate: null,
reprompt: 0,
localData: undefined,
});
});
});
describe("CardCipher", () => {
let cipherData: CipherData;
beforeEach(() => {
cipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
userId: "userId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Card,
name: "EncryptedString",
notes: "EncryptedString",
deletedDate: null,
reprompt: CipherRepromptType.None,
card: {
cardholderName: "EncryptedString",
brand: "EncryptedString",
number: "EncryptedString",
expMonth: "EncryptedString",
expYear: "EncryptedString",
code: "EncryptedString",
},
};
});
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
id: "id",
userId: "userId",
organizationId: "orgId",
folderId: "folderId",
name: { encryptedString: "EncryptedString", encryptionType: 0 },
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 3,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: 0,
card: {
cardholderName: { encryptedString: "EncryptedString", encryptionType: 0 },
brand: { encryptedString: "EncryptedString", encryptionType: 0 },
number: { encryptedString: "EncryptedString", encryptionType: 0 },
expMonth: { encryptedString: "EncryptedString", encryptionType: 0 },
expYear: { encryptedString: "EncryptedString", encryptionType: 0 },
code: { encryptedString: "EncryptedString", encryptionType: 0 },
},
attachments: null,
fields: null,
passwordHistory: null,
});
});
it("toCipherData", () => {
const cipher = new Cipher(cipherData);
expect(cipher.toCipherData("userId")).toEqual(cipherData);
});
it("Decrypt", async () => {
const cipher = new Cipher();
cipher.id = "id";
cipher.organizationId = "orgId";
cipher.folderId = "folderId";
cipher.edit = true;
cipher.viewPassword = true;
cipher.organizationUseTotp = true;
cipher.favorite = false;
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
cipher.type = CipherType.Card;
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None;
const cardView = new CardView();
cardView.cardholderName = "cardholderName";
cardView.number = "4111111111111111";
const card = Substitute.for<Card>();
card.decrypt(Arg.any(), Arg.any()).resolves(cardView);
cipher.card = card;
const cipherView = await cipher.decrypt();
expect(cipherView).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: "EncryptedString",
notes: "EncryptedString",
type: 3,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
card: cardView,
attachments: null,
fields: null,
passwordHistory: null,
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
deletedDate: null,
reprompt: 0,
localData: undefined,
});
});
});
describe("IdentityCipher", () => {
let cipherData: CipherData;
beforeEach(() => {
cipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
userId: "userId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Identity,
name: "EncryptedString",
notes: "EncryptedString",
deletedDate: null,
reprompt: CipherRepromptType.None,
identity: {
title: "EncryptedString",
firstName: "EncryptedString",
middleName: "EncryptedString",
lastName: "EncryptedString",
address1: "EncryptedString",
address2: "EncryptedString",
address3: "EncryptedString",
city: "EncryptedString",
state: "EncryptedString",
postalCode: "EncryptedString",
country: "EncryptedString",
company: "EncryptedString",
email: "EncryptedString",
phone: "EncryptedString",
ssn: "EncryptedString",
username: "EncryptedString",
passportNumber: "EncryptedString",
licenseNumber: "EncryptedString",
},
};
});
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
id: "id",
userId: "userId",
organizationId: "orgId",
folderId: "folderId",
name: { encryptedString: "EncryptedString", encryptionType: 0 },
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
type: 4,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
collectionIds: undefined,
localData: null,
deletedDate: null,
reprompt: 0,
identity: {
title: { encryptedString: "EncryptedString", encryptionType: 0 },
firstName: { encryptedString: "EncryptedString", encryptionType: 0 },
middleName: { encryptedString: "EncryptedString", encryptionType: 0 },
lastName: { encryptedString: "EncryptedString", encryptionType: 0 },
address1: { encryptedString: "EncryptedString", encryptionType: 0 },
address2: { encryptedString: "EncryptedString", encryptionType: 0 },
address3: { encryptedString: "EncryptedString", encryptionType: 0 },
city: { encryptedString: "EncryptedString", encryptionType: 0 },
state: { encryptedString: "EncryptedString", encryptionType: 0 },
postalCode: { encryptedString: "EncryptedString", encryptionType: 0 },
country: { encryptedString: "EncryptedString", encryptionType: 0 },
company: { encryptedString: "EncryptedString", encryptionType: 0 },
email: { encryptedString: "EncryptedString", encryptionType: 0 },
phone: { encryptedString: "EncryptedString", encryptionType: 0 },
ssn: { encryptedString: "EncryptedString", encryptionType: 0 },
username: { encryptedString: "EncryptedString", encryptionType: 0 },
passportNumber: { encryptedString: "EncryptedString", encryptionType: 0 },
licenseNumber: { encryptedString: "EncryptedString", encryptionType: 0 },
},
attachments: null,
fields: null,
passwordHistory: null,
});
});
it("toCipherData", () => {
const cipher = new Cipher(cipherData);
expect(cipher.toCipherData("userId")).toEqual(cipherData);
});
it("Decrypt", async () => {
const cipher = new Cipher();
cipher.id = "id";
cipher.organizationId = "orgId";
cipher.folderId = "folderId";
cipher.edit = true;
cipher.viewPassword = true;
cipher.organizationUseTotp = true;
cipher.favorite = false;
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
cipher.type = CipherType.Identity;
cipher.name = mockEnc("EncryptedString");
cipher.notes = mockEnc("EncryptedString");
cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None;
const identityView = new IdentityView();
identityView.firstName = "firstName";
identityView.lastName = "lastName";
const identity = Substitute.for<Identity>();
identity.decrypt(Arg.any(), Arg.any()).resolves(identityView);
cipher.identity = identity;
const cipherView = await cipher.decrypt();
expect(cipherView).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
name: "EncryptedString",
notes: "EncryptedString",
type: 4,
favorite: false,
organizationUseTotp: true,
edit: true,
viewPassword: true,
identity: identityView,
attachments: null,
fields: null,
passwordHistory: null,
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
deletedDate: null,
reprompt: 0,
localData: undefined,
});
});
});
});

View File

@@ -0,0 +1,66 @@
import { CollectionData } from "jslib-common/models/data/collectionData";
import { Collection } from "jslib-common/models/domain/collection";
import { mockEnc } from "../utils";
describe("Collection", () => {
let data: CollectionData;
beforeEach(() => {
data = {
id: "id",
organizationId: "orgId",
name: "encName",
externalId: "extId",
readOnly: true,
};
});
it("Convert from empty", () => {
const data = new CollectionData({} as any);
const card = new Collection(data);
expect(card).toEqual({
externalId: null,
hidePasswords: null,
id: null,
name: null,
organizationId: null,
readOnly: null,
});
});
it("Convert", () => {
const collection = new Collection(data);
expect(collection).toEqual({
id: "id",
organizationId: "orgId",
name: { encryptedString: "encName", encryptionType: 0 },
externalId: "extId",
readOnly: true,
hidePasswords: null,
});
});
it("Decrypt", async () => {
const collection = new Collection();
collection.id = "id";
collection.organizationId = "orgId";
collection.name = mockEnc("encName");
collection.externalId = "extId";
collection.readOnly = false;
collection.hidePasswords = false;
const view = await collection.decrypt();
expect(view).toEqual({
externalId: "extId",
hidePasswords: false,
id: "id",
name: "encName",
organizationId: "orgId",
readOnly: false,
});
});
});

View File

@@ -0,0 +1,195 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EncryptionType } from "jslib-common/enums/encryptionType";
import { EncString } from "jslib-common/models/domain/encString";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { ContainerService } from "jslib-common/services/container.service";
describe("EncString", () => {
afterEach(() => {
(window as any).bitwardenContainerService = undefined;
});
describe("Rsa2048_OaepSha256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
expect(encString).toEqual({
data: "data",
encryptedString: "3.data",
encryptionType: 3,
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("3.data");
expect(encString).toEqual({
data: "data",
encryptedString: "3.data",
encryptionType: 3,
});
});
it("invalid", () => {
const encString = new EncString("3.data|test");
expect(encString).toEqual({
encryptedString: "3.data|test",
encryptionType: 3,
});
});
});
describe("decrypt", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToUtf8(encString, Arg.any()).resolves("decrypted");
beforeEach(() => {
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
});
it("decrypts correctly", async () => {
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("decrypted");
});
it("result should be cached", async () => {
const decrypted = await encString.decrypt(null);
cryptoService.received(1).decryptToUtf8(Arg.any(), Arg.any());
expect(decrypted).toBe("decrypted");
});
});
});
describe("AesCbc256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv");
expect(encString).toEqual({
data: "data",
encryptedString: "0.iv|data",
encryptionType: 0,
iv: "iv",
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("0.iv|data");
expect(encString).toEqual({
data: "data",
encryptedString: "0.iv|data",
encryptionType: 0,
iv: "iv",
});
});
it("invalid", () => {
const encString = new EncString("0.iv|data|mac");
expect(encString).toEqual({
encryptedString: "0.iv|data|mac",
encryptionType: 0,
});
});
});
});
describe("AesCbc256_HmacSha256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
expect(encString).toEqual({
data: "data",
encryptedString: "2.iv|data|mac",
encryptionType: 2,
iv: "iv",
mac: "mac",
});
});
it("valid", () => {
const encString = new EncString("2.iv|data|mac");
expect(encString).toEqual({
data: "data",
encryptedString: "2.iv|data|mac",
encryptionType: 2,
iv: "iv",
mac: "mac",
});
});
it("invalid", () => {
const encString = new EncString("2.iv|data");
expect(encString).toEqual({
encryptedString: "2.iv|data",
encryptionType: 2,
});
});
});
it("Exit early if null", () => {
const encString = new EncString(null);
expect(encString).toEqual({
encryptedString: null,
});
});
describe("decrypt", () => {
it("throws exception when bitwarden container not initialized", async () => {
const encString = new EncString(null);
expect.assertions(1);
try {
await encString.decrypt(null);
} catch (e) {
expect(e.message).toEqual("global bitwardenContainerService not initialized.");
}
});
it("handles value it can't decrypt", async () => {
const encString = new EncString(null);
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToUtf8(encString, Arg.any()).throws("error");
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("[error: cannot decrypt]");
expect(encString).toEqual({
decryptedValue: "[error: cannot decrypt]",
encryptedString: null,
});
});
it("passes along key", async () => {
const encString = new EncString(null);
const key = Substitute.for<SymmetricCryptoKey>();
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
await encString.decrypt(null, key);
cryptoService.received().decryptToUtf8(encString, key);
});
});
});

View File

@@ -0,0 +1,64 @@
import { FieldType } from "jslib-common/enums/fieldType";
import { FieldData } from "jslib-common/models/data/fieldData";
import { Field } from "jslib-common/models/domain/field";
import { mockEnc } from "../utils";
describe("Field", () => {
let data: FieldData;
beforeEach(() => {
data = {
type: FieldType.Text,
name: "encName",
value: "encValue",
linkedId: null,
};
});
it("Convert from empty", () => {
const data = new FieldData();
const field = new Field(data);
expect(field).toEqual({
type: undefined,
name: null,
value: null,
linkedId: undefined,
});
});
it("Convert", () => {
const field = new Field(data);
expect(field).toEqual({
type: FieldType.Text,
name: { encryptedString: "encName", encryptionType: 0 },
value: { encryptedString: "encValue", encryptionType: 0 },
linkedId: null,
});
});
it("toFieldData", () => {
const field = new Field(data);
expect(field.toFieldData()).toEqual(data);
});
it("Decrypt", async () => {
const field = new Field();
field.type = FieldType.Text;
field.name = mockEnc("encName");
field.value = mockEnc("encValue");
const view = await field.decrypt(null);
expect(view).toEqual({
type: 0,
name: "encName",
value: "encValue",
newField: false,
showCount: false,
showValue: false,
});
});
});

View File

@@ -0,0 +1,42 @@
import { FolderData } from "jslib-common/models/data/folderData";
import { Folder } from "jslib-common/models/domain/folder";
import { mockEnc } from "../utils";
describe("Folder", () => {
let data: FolderData;
beforeEach(() => {
data = {
id: "id",
userId: "userId",
name: "encName",
revisionDate: "2022-01-31T12:00:00.000Z",
};
});
it("Convert", () => {
const field = new Folder(data);
expect(field).toEqual({
id: "id",
name: { encryptedString: "encName", encryptionType: 0 },
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
});
});
it("Decrypt", async () => {
const folder = new Folder();
folder.id = "id";
folder.name = mockEnc("encName");
folder.revisionDate = new Date("2022-01-31T12:00:00.000Z");
const view = await folder.decrypt();
expect(view).toEqual({
id: "id",
name: "encName",
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
});
});
});

View File

@@ -0,0 +1,134 @@
import { IdentityData } from "jslib-common/models/data/identityData";
import { Identity } from "jslib-common/models/domain/identity";
import { mockEnc } from "../utils";
describe("Identity", () => {
let data: IdentityData;
beforeEach(() => {
data = {
title: "enctitle",
firstName: "encfirstName",
middleName: "encmiddleName",
lastName: "enclastName",
address1: "encaddress1",
address2: "encaddress2",
address3: "encaddress3",
city: "enccity",
state: "encstate",
postalCode: "encpostalCode",
country: "enccountry",
company: "enccompany",
email: "encemail",
phone: "encphone",
ssn: "encssn",
username: "encusername",
passportNumber: "encpassportNumber",
licenseNumber: "enclicenseNumber",
};
});
it("Convert from empty", () => {
const data = new IdentityData();
const identity = new Identity(data);
expect(identity).toEqual({
address1: null,
address2: null,
address3: null,
city: null,
company: null,
country: null,
email: null,
firstName: null,
lastName: null,
licenseNumber: null,
middleName: null,
passportNumber: null,
phone: null,
postalCode: null,
ssn: null,
state: null,
title: null,
username: null,
});
});
it("Convert", () => {
const identity = new Identity(data);
expect(identity).toEqual({
title: { encryptedString: "enctitle", encryptionType: 0 },
firstName: { encryptedString: "encfirstName", encryptionType: 0 },
middleName: { encryptedString: "encmiddleName", encryptionType: 0 },
lastName: { encryptedString: "enclastName", encryptionType: 0 },
address1: { encryptedString: "encaddress1", encryptionType: 0 },
address2: { encryptedString: "encaddress2", encryptionType: 0 },
address3: { encryptedString: "encaddress3", encryptionType: 0 },
city: { encryptedString: "enccity", encryptionType: 0 },
state: { encryptedString: "encstate", encryptionType: 0 },
postalCode: { encryptedString: "encpostalCode", encryptionType: 0 },
country: { encryptedString: "enccountry", encryptionType: 0 },
company: { encryptedString: "enccompany", encryptionType: 0 },
email: { encryptedString: "encemail", encryptionType: 0 },
phone: { encryptedString: "encphone", encryptionType: 0 },
ssn: { encryptedString: "encssn", encryptionType: 0 },
username: { encryptedString: "encusername", encryptionType: 0 },
passportNumber: { encryptedString: "encpassportNumber", encryptionType: 0 },
licenseNumber: { encryptedString: "enclicenseNumber", encryptionType: 0 },
});
});
it("toIdentityData", () => {
const identity = new Identity(data);
expect(identity.toIdentityData()).toEqual(data);
});
it("Decrypt", async () => {
const identity = new Identity();
identity.title = mockEnc("mockTitle");
identity.firstName = mockEnc("mockFirstName");
identity.middleName = mockEnc("mockMiddleName");
identity.lastName = mockEnc("mockLastName");
identity.address1 = mockEnc("mockAddress1");
identity.address2 = mockEnc("mockAddress2");
identity.address3 = mockEnc("mockAddress3");
identity.city = mockEnc("mockCity");
identity.state = mockEnc("mockState");
identity.postalCode = mockEnc("mockPostalCode");
identity.country = mockEnc("mockCountry");
identity.company = mockEnc("mockCompany");
identity.email = mockEnc("mockEmail");
identity.phone = mockEnc("mockPhone");
identity.ssn = mockEnc("mockSsn");
identity.username = mockEnc("mockUsername");
identity.passportNumber = mockEnc("mockPassportNumber");
identity.licenseNumber = mockEnc("mockLicenseNumber");
const view = await identity.decrypt(null);
expect(view).toEqual({
_firstName: "mockFirstName",
_lastName: "mockLastName",
_subTitle: null,
address1: "mockAddress1",
address2: "mockAddress2",
address3: "mockAddress3",
city: "mockCity",
company: "mockCompany",
country: "mockCountry",
email: "mockEmail",
licenseNumber: "mockLicenseNumber",
middleName: "mockMiddleName",
passportNumber: "mockPassportNumber",
phone: "mockPhone",
postalCode: "mockPostalCode",
ssn: "mockSsn",
state: "mockState",
title: "mockTitle",
username: "mockUsername",
});
});
});

View File

@@ -0,0 +1,101 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { UriMatchType } from "jslib-common/enums/uriMatchType";
import { LoginData } from "jslib-common/models/data/loginData";
import { Login } from "jslib-common/models/domain/login";
import { LoginUri } from "jslib-common/models/domain/loginUri";
import { LoginUriView } from "jslib-common/models/view/loginUriView";
import { mockEnc } from "../utils";
describe("Login DTO", () => {
it("Convert from empty LoginData", () => {
const data = new LoginData();
const login = new Login(data);
expect(login).toEqual({
passwordRevisionDate: null,
autofillOnPageLoad: undefined,
username: null,
password: null,
totp: null,
});
});
it("Convert from full LoginData", () => {
const data: LoginData = {
uris: [{ uri: "uri", match: UriMatchType.Domain }],
username: "username",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "123",
autofillOnPageLoad: false,
};
const login = new Login(data);
expect(login).toEqual({
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
autofillOnPageLoad: false,
username: { encryptedString: "username", encryptionType: 0 },
password: { encryptedString: "password", encryptionType: 0 },
totp: { encryptedString: "123", encryptionType: 0 },
uris: [{ match: 0, uri: { encryptedString: "uri", encryptionType: 0 } }],
});
});
it("Initialize without LoginData", () => {
const login = new Login();
expect(login).toEqual({});
});
it("Decrypts correctly", async () => {
const loginUri = Substitute.for<LoginUri>();
const loginUriView = new LoginUriView();
loginUriView.uri = "decrypted uri";
loginUri.decrypt(Arg.any()).resolves(loginUriView);
const login = new Login();
login.uris = [loginUri];
login.username = mockEnc("encrypted username");
login.password = mockEnc("encrypted password");
login.passwordRevisionDate = new Date("2022-01-31T12:00:00.000Z");
login.totp = mockEnc("encrypted totp");
login.autofillOnPageLoad = true;
const loginView = await login.decrypt(null);
expect(loginView).toEqual({
username: "encrypted username",
password: "encrypted password",
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
totp: "encrypted totp",
uris: [
{
match: null,
_uri: "decrypted uri",
_domain: null,
_hostname: null,
_host: null,
_canLaunch: null,
},
],
autofillOnPageLoad: true,
});
});
it("Converts from LoginData and back", () => {
const data: LoginData = {
uris: [{ uri: "uri", match: UriMatchType.Domain }],
username: "username",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "123",
autofillOnPageLoad: false,
};
const login = new Login(data);
const loginData = login.toLoginData();
expect(loginData).toEqual(data);
});
});

View File

@@ -0,0 +1,57 @@
import { UriMatchType } from "jslib-common/enums/uriMatchType";
import { LoginUriData } from "jslib-common/models/data/loginUriData";
import { LoginUri } from "jslib-common/models/domain/loginUri";
import { mockEnc } from "../utils";
describe("LoginUri", () => {
let data: LoginUriData;
beforeEach(() => {
data = {
uri: "encUri",
match: UriMatchType.Domain,
};
});
it("Convert from empty", () => {
const data = new LoginUriData();
const loginUri = new LoginUri(data);
expect(loginUri).toEqual({
match: null,
uri: null,
});
});
it("Convert", () => {
const loginUri = new LoginUri(data);
expect(loginUri).toEqual({
match: 0,
uri: { encryptedString: "encUri", encryptionType: 0 },
});
});
it("toLoginUriData", () => {
const loginUri = new LoginUri(data);
expect(loginUri.toLoginUriData()).toEqual(data);
});
it("Decrypt", async () => {
const loginUri = new LoginUri();
loginUri.match = UriMatchType.Exact;
loginUri.uri = mockEnc("uri");
const view = await loginUri.decrypt(null);
expect(view).toEqual({
_canLaunch: null,
_domain: null,
_host: null,
_hostname: null,
_uri: "uri",
match: 3,
});
});
});

View File

@@ -0,0 +1,51 @@
import { PasswordHistoryData } from "jslib-common/models/data/passwordHistoryData";
import { Password } from "jslib-common/models/domain/password";
import { mockEnc } from "../utils";
describe("Password", () => {
let data: PasswordHistoryData;
beforeEach(() => {
data = {
password: "encPassword",
lastUsedDate: "2022-01-31T12:00:00.000Z",
};
});
it("Convert from empty", () => {
const data = new PasswordHistoryData();
const password = new Password(data);
expect(password).toMatchObject({
password: null,
});
});
it("Convert", () => {
const password = new Password(data);
expect(password).toEqual({
password: { encryptedString: "encPassword", encryptionType: 0 },
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
});
});
it("toPasswordHistoryData", () => {
const password = new Password(data);
expect(password.toPasswordHistoryData()).toEqual(data);
});
it("Decrypt", async () => {
const password = new Password();
password.password = mockEnc("password");
password.lastUsedDate = new Date("2022-01-31T12:00:00.000Z");
const view = await password.decrypt(null);
expect(view).toEqual({
password: "password",
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
});
});
});

View File

@@ -0,0 +1,46 @@
import { SecureNoteType } from "jslib-common/enums/secureNoteType";
import { SecureNoteData } from "jslib-common/models/data/secureNoteData";
import { SecureNote } from "jslib-common/models/domain/secureNote";
describe("SecureNote", () => {
let data: SecureNoteData;
beforeEach(() => {
data = {
type: SecureNoteType.Generic,
};
});
it("Convert from empty", () => {
const data = new SecureNoteData();
const secureNote = new SecureNote(data);
expect(secureNote).toEqual({
type: undefined,
});
});
it("Convert", () => {
const secureNote = new SecureNote(data);
expect(secureNote).toEqual({
type: 0,
});
});
it("toSecureNoteData", () => {
const secureNote = new SecureNote(data);
expect(secureNote.toSecureNoteData()).toEqual(data);
});
it("Decrypt", async () => {
const secureNote = new SecureNote();
secureNote.type = SecureNoteType.Generic;
const view = await secureNote.decrypt(null);
expect(view).toEqual({
type: 0,
});
});
});

View File

@@ -0,0 +1,144 @@
import Substitute, { Arg, SubstituteOf } from "@fluffy-spoon/substitute";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { SendType } from "jslib-common/enums/sendType";
import { SendData } from "jslib-common/models/data/sendData";
import { EncString } from "jslib-common/models/domain/encString";
import { Send } from "jslib-common/models/domain/send";
import { SendText } from "jslib-common/models/domain/sendText";
import { ContainerService } from "jslib-common/services/container.service";
import { makeStaticByteArray, mockEnc } from "../utils";
describe("Send", () => {
let data: SendData;
beforeEach(() => {
data = {
id: "id",
accessId: "accessId",
userId: "userId",
type: SendType.Text,
name: "encName",
notes: "encNotes",
text: {
text: "encText",
hidden: true,
},
file: null,
key: "encKey",
maxAccessCount: null,
accessCount: 10,
revisionDate: "2022-01-31T12:00:00.000Z",
expirationDate: "2022-01-31T12:00:00.000Z",
deletionDate: "2022-01-31T12:00:00.000Z",
password: "password",
disabled: false,
hideEmail: true,
};
});
it("Convert from empty", () => {
const data = new SendData();
const send = new Send(data);
expect(send).toEqual({
id: null,
accessId: null,
userId: null,
type: undefined,
name: null,
notes: null,
text: undefined,
file: undefined,
key: null,
maxAccessCount: undefined,
accessCount: undefined,
revisionDate: null,
expirationDate: null,
deletionDate: null,
password: undefined,
disabled: undefined,
hideEmail: undefined,
});
});
it("Convert", () => {
const send = new Send(data);
expect(send).toEqual({
id: "id",
accessId: "accessId",
userId: "userId",
type: SendType.Text,
name: { encryptedString: "encName", encryptionType: 0 },
notes: { encryptedString: "encNotes", encryptionType: 0 },
text: {
text: { encryptedString: "encText", encryptionType: 0 },
hidden: true,
},
key: { encryptedString: "encKey", encryptionType: 0 },
maxAccessCount: null,
accessCount: 10,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
deletionDate: new Date("2022-01-31T12:00:00.000Z"),
password: "password",
disabled: false,
hideEmail: true,
});
});
it("Decrypt", async () => {
const text = Substitute.for<SendText>();
text.decrypt(Arg.any()).resolves("textView" as any);
const send = new Send();
send.id = "id";
send.accessId = "accessId";
send.userId = "userId";
send.type = SendType.Text;
send.name = mockEnc("name");
send.notes = mockEnc("notes");
send.text = text;
send.key = mockEnc("key");
send.accessCount = 10;
send.revisionDate = new Date("2022-01-31T12:00:00.000Z");
send.expirationDate = new Date("2022-01-31T12:00:00.000Z");
send.deletionDate = new Date("2022-01-31T12:00:00.000Z");
send.password = "password";
send.disabled = false;
send.hideEmail = true;
const cryptoService = Substitute.for<CryptoService>();
cryptoService.decryptToBytes(send.key, null).resolves(makeStaticByteArray(32));
cryptoService.makeSendKey(Arg.any()).resolves("cryptoKey" as any);
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
const view = await send.decrypt();
text.received(1).decrypt("cryptoKey" as any);
(send.name as SubstituteOf<EncString>).received(1).decrypt(null, "cryptoKey" as any);
expect(view).toMatchObject({
id: "id",
accessId: "accessId",
name: "name",
notes: "notes",
type: 0,
key: expect.anything(),
cryptoKey: "cryptoKey",
file: expect.anything(),
text: "textView",
maxAccessCount: undefined,
accessCount: 10,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
deletionDate: new Date("2022-01-31T12:00:00.000Z"),
password: "password",
disabled: false,
hideEmail: true,
});
});
});

View File

@@ -0,0 +1,84 @@
import Substitute, { Arg } from "@fluffy-spoon/substitute";
import { SendType } from "jslib-common/enums/sendType";
import { SendAccess } from "jslib-common/models/domain/sendAccess";
import { SendText } from "jslib-common/models/domain/sendText";
import { SendAccessResponse } from "jslib-common/models/response/sendAccessResponse";
import { mockEnc } from "../utils";
describe("SendAccess", () => {
let request: SendAccessResponse;
beforeEach(() => {
request = {
id: "id",
type: SendType.Text,
name: "encName",
file: null,
text: {
text: "encText",
hidden: true,
},
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
creatorIdentifier: "creatorIdentifier",
} as SendAccessResponse;
});
it("Convert from empty", () => {
const request = new SendAccessResponse({});
const sendAccess = new SendAccess(request);
expect(sendAccess).toEqual({
id: null,
type: undefined,
name: null,
creatorIdentifier: null,
expirationDate: null,
});
});
it("Convert", () => {
const sendAccess = new SendAccess(request);
expect(sendAccess).toEqual({
id: "id",
type: 0,
name: { encryptedString: "encName", encryptionType: 0 },
text: {
hidden: true,
text: { encryptedString: "encText", encryptionType: 0 },
},
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
creatorIdentifier: "creatorIdentifier",
});
});
it("Decrypt", async () => {
const sendAccess = new SendAccess();
sendAccess.id = "id";
sendAccess.type = SendType.Text;
sendAccess.name = mockEnc("name");
const text = Substitute.for<SendText>();
text.decrypt(Arg.any()).resolves({} as any);
sendAccess.text = text;
sendAccess.expirationDate = new Date("2022-01-31T12:00:00.000Z");
sendAccess.creatorIdentifier = "creatorIdentifier";
const view = await sendAccess.decrypt(null);
text.received(1).decrypt(Arg.any());
expect(view).toEqual({
id: "id",
type: 0,
name: "name",
text: {},
file: expect.anything(),
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
creatorIdentifier: "creatorIdentifier",
});
});
});

View File

@@ -0,0 +1,57 @@
import { SendFileData } from "jslib-common/models/data/sendFileData";
import { SendFile } from "jslib-common/models/domain/sendFile";
import { mockEnc } from "../utils";
describe("SendFile", () => {
let data: SendFileData;
beforeEach(() => {
data = {
id: "id",
size: "1100",
sizeName: "1.1 KB",
fileName: "encFileName",
};
});
it("Convert from empty", () => {
const data = new SendFileData();
const sendFile = new SendFile(data);
expect(sendFile).toEqual({
fileName: null,
id: null,
size: undefined,
sizeName: null,
});
});
it("Convert", () => {
const sendFile = new SendFile(data);
expect(sendFile).toEqual({
id: "id",
size: "1100",
sizeName: "1.1 KB",
fileName: { encryptedString: "encFileName", encryptionType: 0 },
});
});
it("Decrypt", async () => {
const sendFile = new SendFile();
sendFile.id = "id";
sendFile.size = "1100";
sendFile.sizeName = "1.1 KB";
sendFile.fileName = mockEnc("fileName");
const view = await sendFile.decrypt(null);
expect(view).toEqual({
fileName: "fileName",
id: "id",
size: "1100",
sizeName: "1.1 KB",
});
});
});

View File

@@ -0,0 +1,47 @@
import { SendTextData } from "jslib-common/models/data/sendTextData";
import { SendText } from "jslib-common/models/domain/sendText";
import { mockEnc } from "../utils";
describe("SendText", () => {
let data: SendTextData;
beforeEach(() => {
data = {
text: "encText",
hidden: false,
};
});
it("Convert from empty", () => {
const data = new SendTextData();
const secureNote = new SendText(data);
expect(secureNote).toEqual({
hidden: undefined,
text: null,
});
});
it("Convert", () => {
const secureNote = new SendText(data);
expect(secureNote).toEqual({
hidden: false,
text: { encryptedString: "encText", encryptionType: 0 },
});
});
it("Decrypt", async () => {
const secureNote = new SendText();
secureNote.text = mockEnc("text");
secureNote.hidden = true;
const view = await secureNote.decrypt(null);
expect(view).toEqual({
text: "text",
hidden: true,
});
});
});

View File

@@ -0,0 +1,69 @@
import { EncryptionType } from "jslib-common/enums/encryptionType";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { makeStaticByteArray } from "../utils";
describe("SymmetricCryptoKey", () => {
it("errors if no key", () => {
const t = () => {
new SymmetricCryptoKey(null);
};
expect(t).toThrowError("Must provide key");
});
describe("guesses encKey from key length", () => {
it("AesCbc256_B64", () => {
const key = makeStaticByteArray(32);
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
encKey: key,
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 0,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: null,
});
});
it("AesCbc128_HmacSha256_B64", () => {
const key = makeStaticByteArray(32);
const cryptoKey = new SymmetricCryptoKey(key, EncryptionType.AesCbc128_HmacSha256_B64);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 16),
encKeyB64: "AAECAwQFBgcICQoLDA0ODw==",
encType: 1,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: key.slice(16, 32),
macKeyB64: "EBESExQVFhcYGRobHB0eHw==",
});
});
it("AesCbc256_HmacSha256_B64", () => {
const key = makeStaticByteArray(64);
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 32),
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 2,
key: key,
keyB64:
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
macKey: key.slice(32, 64),
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
});
});
it("unknown length", () => {
const t = () => {
new SymmetricCryptoKey(makeStaticByteArray(30));
};
expect(t).toThrowError("Unable to determine encType.");
});
});
});

View File

@@ -0,0 +1,31 @@
import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { BitwardenJsonImporter } from "jslib-common/importers/bitwardenJsonImporter";
import { data as passwordProtectedData } from "./testData/bitwardenJson/passwordProtected.json";
describe("bitwarden json importer", () => {
let sut: BitwardenJsonImporter;
let cryptoService: SubstituteOf<CryptoService>;
let i18nService: SubstituteOf<I18nService>;
beforeEach(() => {
cryptoService = Substitute.for<CryptoService>();
i18nService = Substitute.for<I18nService>();
sut = new BitwardenJsonImporter(cryptoService, i18nService);
});
it("should fail if password is needed", async () => {
expect((await sut.parse(passwordProtectedData)).success).toBe(false);
});
it("should return password needed error message", async () => {
const expected = "Password required error message";
i18nService.t("importPasswordRequired").returns(expected);
expect((await sut.parse(passwordProtectedData)).errorMessage).toEqual(expected);
});
});

View File

@@ -0,0 +1,113 @@
import Substitute, { Arg, SubstituteOf } from "@fluffy-spoon/substitute";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KdfType } from "jslib-common/enums/kdfType";
import { BitwardenPasswordProtectedImporter } from "jslib-common/importers/bitwardenPasswordProtectedImporter";
import { Utils } from "jslib-common/misc/utils";
import { ImportResult } from "jslib-common/models/domain/importResult";
import { data as emptyDecryptedData } from "./testData/bitwardenJson/empty.json";
describe("BitwardenPasswordProtectedImporter", () => {
let importer: BitwardenPasswordProtectedImporter;
let cryptoService: SubstituteOf<CryptoService>;
let i18nService: SubstituteOf<I18nService>;
const password = Utils.newGuid();
const result = new ImportResult();
let jDoc: {
encrypted?: boolean;
passwordProtected?: boolean;
salt?: string;
kdfIterations?: any;
kdfType?: any;
encKeyValidation_DO_NOT_EDIT?: string;
data?: string;
};
beforeEach(() => {
cryptoService = Substitute.for<CryptoService>();
i18nService = Substitute.for<I18nService>();
jDoc = {
encrypted: true,
passwordProtected: true,
salt: "c2FsdA==",
kdfIterations: 100000,
kdfType: KdfType.PBKDF2_SHA256,
encKeyValidation_DO_NOT_EDIT: Utils.newGuid(),
data: Utils.newGuid(),
};
result.success = true;
importer = new BitwardenPasswordProtectedImporter(cryptoService, i18nService, password);
});
describe("Required Json Data", () => {
it("succeeds with default jdoc", async () => {
cryptoService.decryptToUtf8(Arg.any(), Arg.any()).resolves(emptyDecryptedData);
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(true);
});
it("fails if encrypted === false", async () => {
jDoc.encrypted = false;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if encrypted === null", async () => {
jDoc.encrypted = null;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if passwordProtected === false", async () => {
jDoc.passwordProtected = false;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if passwordProtected === null", async () => {
jDoc.passwordProtected = null;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if salt === null", async () => {
jDoc.salt = null;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if kdfIterations === null", async () => {
jDoc.kdfIterations = null;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if kdfIterations is not a number", async () => {
jDoc.kdfIterations = "not a number";
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if kdfType === null", async () => {
jDoc.kdfType = null;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if kdfType is not a string", async () => {
jDoc.kdfType = "not a valid kdf type";
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if kdfType is not a known kdfType", async () => {
jDoc.kdfType = -1;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if encKeyValidation_DO_NOT_EDIT === null", async () => {
jDoc.encKeyValidation_DO_NOT_EDIT = null;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
it("fails if data === null", async () => {
jDoc.data = null;
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
});
});
});

View File

@@ -0,0 +1,367 @@
import { CipherType } from "jslib-common/enums/cipherType";
import { DashlaneCsvImporter as Importer } from "jslib-common/importers/dashlaneImporters/dashlaneCsvImporter";
import { credentialsData } from "./testData/dashlaneCsv/credentials.csv";
import { identityData } from "./testData/dashlaneCsv/id.csv";
import { multiplePersonalInfoData } from "./testData/dashlaneCsv/multiplePersonalInfo.csv";
import { paymentsData } from "./testData/dashlaneCsv/payments.csv";
import { personalInfoData } from "./testData/dashlaneCsv/personalInfo.csv";
import { secureNoteData } from "./testData/dashlaneCsv/securenotes.csv";
describe("Dashlane CSV Importer", () => {
let importer: Importer;
beforeEach(() => {
importer = new Importer();
});
it("should parse login records", async () => {
const result = await importer.parse(credentialsData);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.name).toEqual("example.com");
expect(cipher.login.username).toEqual("jdoe");
expect(cipher.login.password).toEqual("somePassword");
expect(cipher.login.totp).toEqual("someTOTPSeed");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://www.example.com");
expect(cipher.notes).toEqual("some note for example.com");
});
it("should parse an item and create a folder", async () => {
const result = await importer.parse(credentialsData);
expect(result).not.toBeNull();
expect(result.success).toBe(true);
expect(result.folders.length).toBe(1);
expect(result.folders[0].name).toBe("Entertainment");
expect(result.folderRelationships[0]).toEqual([0, 0]);
});
it("should parse payment records", async () => {
const result = await importer.parse(paymentsData);
expect(result).not.toBeNull();
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(2);
// Account
const cipher = result.ciphers.shift();
expect(cipher.type).toBe(CipherType.Card);
expect(cipher.name).toBe("John's savings account");
expect(cipher.card.brand).toBeNull();
expect(cipher.card.cardholderName).toBe("John Doe");
expect(cipher.card.number).toBe("accountNumber");
expect(cipher.card.code).toBeNull();
expect(cipher.card.expMonth).toBeNull();
expect(cipher.card.expYear).toBeNull();
expect(cipher.fields.length).toBe(4);
expect(cipher.fields[0].name).toBe("type");
expect(cipher.fields[0].value).toBe("bank");
expect(cipher.fields[1].name).toBe("routing_number");
expect(cipher.fields[1].value).toBe("routingNumber");
expect(cipher.fields[2].name).toBe("country");
expect(cipher.fields[2].value).toBe("US");
expect(cipher.fields[3].name).toBe("issuing_bank");
expect(cipher.fields[3].value).toBe("US-ALLY");
// CreditCard
const cipher2 = result.ciphers.shift();
expect(cipher2.type).toBe(CipherType.Card);
expect(cipher2.name).toBe("John Doe");
expect(cipher2.card.brand).toBe("Visa");
expect(cipher2.card.cardholderName).toBe("John Doe");
expect(cipher2.card.number).toBe("41111111111111111");
expect(cipher2.card.code).toBe("123");
expect(cipher2.card.expMonth).toBe("01");
expect(cipher2.card.expYear).toBe("23");
expect(cipher2.fields.length).toBe(2);
expect(cipher2.fields[0].name).toBe("type");
expect(cipher2.fields[0].value).toBe("credit_card");
expect(cipher2.fields[1].name).toBe("country");
expect(cipher2.fields[1].value).toBe("US");
});
it("should parse ids records", async () => {
const result = await importer.parse(identityData);
expect(result).not.toBeNull();
expect(result.success).toBe(true);
// Type card
const cipher = result.ciphers.shift();
expect(cipher.type).toBe(CipherType.Identity);
expect(cipher.name).toBe("John Doe card");
expect(cipher.identity.fullName).toBe("John Doe");
expect(cipher.identity.firstName).toBe("John");
expect(cipher.identity.middleName).toBeNull();
expect(cipher.identity.lastName).toBe("Doe");
expect(cipher.identity.licenseNumber).toBe("123123123");
expect(cipher.fields.length).toBe(3);
expect(cipher.fields[0].name).toEqual("type");
expect(cipher.fields[0].value).toEqual("card");
expect(cipher.fields[1].name).toEqual("issue_date");
expect(cipher.fields[1].value).toEqual("2022-1-30");
expect(cipher.fields[2].name).toEqual("expiration_date");
expect(cipher.fields[2].value).toEqual("2032-1-30");
// Type passport
const cipher2 = result.ciphers.shift();
expect(cipher2.type).toBe(CipherType.Identity);
expect(cipher2.name).toBe("John Doe passport");
expect(cipher2.identity.fullName).toBe("John Doe");
expect(cipher2.identity.firstName).toBe("John");
expect(cipher2.identity.middleName).toBeNull();
expect(cipher2.identity.lastName).toBe("Doe");
expect(cipher2.identity.passportNumber).toBe("123123123");
expect(cipher2.fields.length).toBe(4);
expect(cipher2.fields[0].name).toEqual("type");
expect(cipher2.fields[0].value).toEqual("passport");
expect(cipher2.fields[1].name).toEqual("issue_date");
expect(cipher2.fields[1].value).toEqual("2022-1-30");
expect(cipher2.fields[2].name).toEqual("expiration_date");
expect(cipher2.fields[2].value).toEqual("2032-1-30");
expect(cipher2.fields[3].name).toEqual("place_of_issue");
expect(cipher2.fields[3].value).toEqual("somewhere in Germany");
// Type license
const cipher3 = result.ciphers.shift();
expect(cipher3.type).toBe(CipherType.Identity);
expect(cipher3.name).toBe("John Doe license");
expect(cipher3.identity.fullName).toBe("John Doe");
expect(cipher3.identity.firstName).toBe("John");
expect(cipher3.identity.middleName).toBeNull();
expect(cipher3.identity.lastName).toBe("Doe");
expect(cipher3.identity.licenseNumber).toBe("1234556");
expect(cipher3.identity.state).toBe("DC");
expect(cipher3.fields.length).toBe(3);
expect(cipher3.fields[0].name).toEqual("type");
expect(cipher3.fields[0].value).toEqual("license");
expect(cipher3.fields[1].name).toEqual("issue_date");
expect(cipher3.fields[1].value).toEqual("2022-8-10");
expect(cipher3.fields[2].name).toEqual("expiration_date");
expect(cipher3.fields[2].value).toEqual("2022-10-10");
// Type social_security
const cipher4 = result.ciphers.shift();
expect(cipher4.type).toBe(CipherType.Identity);
expect(cipher4.name).toBe("John Doe social_security");
expect(cipher4.identity.fullName).toBe("John Doe");
expect(cipher4.identity.firstName).toBe("John");
expect(cipher4.identity.middleName).toBeNull();
expect(cipher4.identity.lastName).toBe("Doe");
expect(cipher4.identity.ssn).toBe("123123123");
expect(cipher4.fields.length).toBe(1);
expect(cipher4.fields[0].name).toEqual("type");
expect(cipher4.fields[0].value).toEqual("social_security");
// Type tax_number
const cipher5 = result.ciphers.shift();
expect(cipher5.type).toBe(CipherType.Identity);
expect(cipher5.name).toBe("tax_number");
expect(cipher5.identity.licenseNumber).toBe("123123123");
expect(cipher5.fields.length).toBe(1);
expect(cipher5.fields[0].name).toEqual("type");
expect(cipher5.fields[0].value).toEqual("tax_number");
});
it("should parse secureNote records", async () => {
const result = await importer.parse(secureNoteData);
expect(result).not.toBeNull();
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(1);
const cipher = result.ciphers.shift();
expect(cipher.type).toBe(CipherType.SecureNote);
expect(cipher.name).toBe("01");
expect(cipher.notes).toBe("test");
});
it("should parse personal information records (multiple identities)", async () => {
const result = await importer.parse(multiplePersonalInfoData);
expect(result).not.toBeNull();
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(6);
// name
const cipher = result.ciphers.shift();
expect(cipher.type).toBe(CipherType.SecureNote);
expect(cipher.name).toBe("MR John Doe");
expect(cipher.fields.length).toBe(7);
expect(cipher.fields[0].name).toEqual("type");
expect(cipher.fields[0].value).toEqual("name");
expect(cipher.fields[1].name).toEqual("title");
expect(cipher.fields[1].value).toEqual("MR");
expect(cipher.fields[2].name).toEqual("first_name");
expect(cipher.fields[2].value).toEqual("John");
expect(cipher.fields[3].name).toEqual("last_name");
expect(cipher.fields[3].value).toEqual("Doe");
expect(cipher.fields[4].name).toEqual("login");
expect(cipher.fields[4].value).toEqual("jdoe");
expect(cipher.fields[5].name).toEqual("date_of_birth");
expect(cipher.fields[5].value).toEqual("2022-01-30");
expect(cipher.fields[6].name).toEqual("place_of_birth");
expect(cipher.fields[6].value).toEqual("world");
// email
const cipher2 = result.ciphers.shift();
expect(cipher2.type).toBe(CipherType.SecureNote);
expect(cipher2.name).toBe("Johns email");
expect(cipher2.fields.length).toBe(4);
expect(cipher2.fields[0].name).toEqual("type");
expect(cipher2.fields[0].value).toEqual("email");
expect(cipher2.fields[1].name).toEqual("email");
expect(cipher2.fields[1].value).toEqual("jdoe@example.com");
expect(cipher2.fields[2].name).toEqual("email_type");
expect(cipher2.fields[2].value).toEqual("personal");
expect(cipher2.fields[3].name).toEqual("item_name");
expect(cipher2.fields[3].value).toEqual("Johns email");
// number
const cipher3 = result.ciphers.shift();
expect(cipher3.type).toBe(CipherType.SecureNote);
expect(cipher3.name).toBe("John's number");
expect(cipher3.fields.length).toBe(3);
expect(cipher3.fields[0].name).toEqual("type");
expect(cipher3.fields[0].value).toEqual("number");
expect(cipher3.fields[1].name).toEqual("item_name");
expect(cipher3.fields[1].value).toEqual("John's number");
expect(cipher3.fields[2].name).toEqual("phone_number");
expect(cipher3.fields[2].value).toEqual("+49123123123");
// address
const cipher4 = result.ciphers.shift();
expect(cipher4.type).toBe(CipherType.SecureNote);
expect(cipher4.name).toBe("John's home address");
expect(cipher4.fields.length).toBe(12);
expect(cipher4.fields[0].name).toEqual("type");
expect(cipher4.fields[0].value).toEqual("address");
expect(cipher4.fields[1].name).toEqual("item_name");
expect(cipher4.fields[1].value).toEqual("John's home address");
expect(cipher4.fields[2].name).toEqual("address");
expect(cipher4.fields[2].value).toEqual("1 some street");
expect(cipher4.fields[3].name).toEqual("country");
expect(cipher4.fields[3].value).toEqual("de");
expect(cipher4.fields[4].name).toEqual("state");
expect(cipher4.fields[4].value).toEqual("DE-0-NW");
expect(cipher4.fields[5].name).toEqual("city");
expect(cipher4.fields[5].value).toEqual("some city");
expect(cipher4.fields[6].name).toEqual("zip");
expect(cipher4.fields[6].value).toEqual("123123");
expect(cipher4.fields[7].name).toEqual("address_recipient");
expect(cipher4.fields[7].value).toEqual("John");
expect(cipher4.fields[8].name).toEqual("address_building");
expect(cipher4.fields[8].value).toEqual("1");
expect(cipher4.fields[9].name).toEqual("address_apartment");
expect(cipher4.fields[9].value).toEqual("1");
expect(cipher4.fields[10].name).toEqual("address_floor");
expect(cipher4.fields[10].value).toEqual("1");
expect(cipher4.fields[11].name).toEqual("address_door_code");
expect(cipher4.fields[11].value).toEqual("123");
// website
const cipher5 = result.ciphers.shift();
expect(cipher5.type).toBe(CipherType.SecureNote);
expect(cipher5.name).toBe("Website");
expect(cipher5.fields.length).toBe(3);
expect(cipher5.fields[0].name).toEqual("type");
expect(cipher5.fields[0].value).toEqual("website");
expect(cipher5.fields[1].name).toEqual("item_name");
expect(cipher5.fields[1].value).toEqual("Website");
expect(cipher5.fields[2].name).toEqual("url");
expect(cipher5.fields[2].value).toEqual("website.com");
// 2nd name/identity
const cipher6 = result.ciphers.shift();
expect(cipher6.type).toBe(CipherType.SecureNote);
expect(cipher6.name).toBe("Mrs Jane Doe");
expect(cipher6.fields.length).toBe(7);
expect(cipher6.fields[0].name).toEqual("type");
expect(cipher6.fields[0].value).toEqual("name");
expect(cipher6.fields[1].name).toEqual("title");
expect(cipher6.fields[1].value).toEqual("Mrs");
expect(cipher6.fields[2].name).toEqual("first_name");
expect(cipher6.fields[2].value).toEqual("Jane");
expect(cipher6.fields[3].name).toEqual("last_name");
expect(cipher6.fields[3].value).toEqual("Doe");
expect(cipher6.fields[4].name).toEqual("login");
expect(cipher6.fields[4].value).toEqual("jdoe");
expect(cipher6.fields[5].name).toEqual("date_of_birth");
expect(cipher6.fields[5].value).toEqual("2022-01-30");
expect(cipher6.fields[6].name).toEqual("place_of_birth");
expect(cipher6.fields[6].value).toEqual("earth");
});
it("should combine personal information records to one identity if only one identity present", async () => {
const result = await importer.parse(personalInfoData);
expect(result).not.toBeNull();
expect(result.success).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.type).toBe(CipherType.Identity);
expect(cipher.name).toBe("MR John Doe");
expect(cipher.identity.fullName).toBe("MR John Doe");
expect(cipher.identity.title).toBe("MR");
expect(cipher.identity.firstName).toBe("John");
expect(cipher.identity.middleName).toBeNull();
expect(cipher.identity.lastName).toBe("Doe");
expect(cipher.identity.username).toBe("jdoe");
expect(cipher.identity.email).toBe("jdoe@example.com");
expect(cipher.identity.phone).toBe("+49123123123");
expect(cipher.fields.length).toBe(9);
expect(cipher.fields[0].name).toBe("date_of_birth");
expect(cipher.fields[0].value).toBe("2022-01-30");
expect(cipher.fields[1].name).toBe("place_of_birth");
expect(cipher.fields[1].value).toBe("world");
expect(cipher.fields[2].name).toBe("email_type");
expect(cipher.fields[2].value).toBe("personal");
expect(cipher.fields[3].name).toBe("address_recipient");
expect(cipher.fields[3].value).toBe("John");
expect(cipher.fields[4].name).toBe("address_building");
expect(cipher.fields[4].value).toBe("1");
expect(cipher.fields[5].name).toBe("address_apartment");
expect(cipher.fields[5].value).toBe("1");
expect(cipher.fields[6].name).toBe("address_floor");
expect(cipher.fields[6].value).toBe("1");
expect(cipher.fields[7].name).toBe("address_door_code");
expect(cipher.fields[7].value).toBe("123");
expect(cipher.fields[8].name).toBe("url");
expect(cipher.fields[8].value).toBe("website.com");
});
});

View File

@@ -0,0 +1,74 @@
import { FirefoxCsvImporter as Importer } from "jslib-common/importers/firefoxCsvImporter";
import { CipherView } from "jslib-common/models/view/cipherView";
import { LoginUriView } from "jslib-common/models/view/loginUriView";
import { LoginView } from "jslib-common/models/view/loginView";
import { data as firefoxAccountsData } from "./testData/firefoxCsv/firefoxAccountsData.csv";
import { data as simplePasswordData } from "./testData/firefoxCsv/simplePasswordData.csv";
const CipherData = [
{
title: "should parse password",
csv: simplePasswordData,
expected: Object.assign(new CipherView(), {
id: null,
organizationId: null,
folderId: null,
name: "example.com",
login: Object.assign(new LoginView(), {
username: "foo",
password: "bar",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com",
}),
],
}),
notes: null,
type: 1,
}),
},
{
title: 'should skip "chrome://FirefoxAccounts"',
csv: firefoxAccountsData,
expected: Object.assign(new CipherView(), {
id: null,
organizationId: null,
folderId: null,
name: "example.com",
login: Object.assign(new LoginView(), {
username: "foo",
password: "bar",
uris: [
Object.assign(new LoginUriView(), {
uri: "https://example.com",
}),
],
}),
notes: null,
type: 1,
}),
},
];
describe("Firefox CSV Importer", () => {
CipherData.forEach((data) => {
it(data.title, async () => {
const importer = new Importer();
const result = await importer.parse(data.csv);
expect(result != null).toBe(true);
expect(result.ciphers.length).toBeGreaterThan(0);
const cipher = result.ciphers.shift();
let property: keyof typeof data.expected;
for (property in data.expected) {
// eslint-disable-next-line
if (data.expected.hasOwnProperty(property)) {
// eslint-disable-next-line
expect(cipher.hasOwnProperty(property)).toBe(true);
expect(cipher[property]).toEqual(data.expected[property]);
}
}
});
});
});

View File

@@ -0,0 +1,77 @@
import { FSecureFskImporter as Importer } from "jslib-common/importers/fsecureFskImporter";
const TestDataWithStyleSetToWebsite: string = JSON.stringify({
data: {
"8d58b5cf252dd06fbd98f5289e918ab1": {
color: "#00baff",
reatedDate: 1609302913,
creditCvv: "",
creditExpiry: "",
creditNumber: "",
favorite: 0,
modifiedDate: 1609302913,
notes: "note",
password: "word",
passwordList: [],
passwordModifiedDate: 1609302913,
rev: 1,
service: "My first pass",
style: "website",
type: 1,
url: "https://bitwarden.com",
username: "pass",
},
},
});
const TestDataWithStyleSetToGlobe: string = JSON.stringify({
data: {
"8d58b5cf252dd06fbd98f5289e918ab1": {
color: "#00baff",
reatedDate: 1609302913,
creditCvv: "",
creditExpiry: "",
creditNumber: "",
favorite: 0,
modifiedDate: 1609302913,
notes: "note",
password: "word",
passwordList: [],
passwordModifiedDate: 1609302913,
rev: 1,
service: "My first pass",
style: "globe",
type: 1,
url: "https://bitwarden.com",
username: "pass",
},
},
});
describe("FSecure FSK Importer", () => {
it("should parse data with style set to website", async () => {
const importer = new Importer();
const result = await importer.parse(TestDataWithStyleSetToWebsite);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.login.username).toEqual("pass");
expect(cipher.login.password).toEqual("word");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://bitwarden.com");
});
it("should parse data with style set to globe", async () => {
const importer = new Importer();
const result = await importer.parse(TestDataWithStyleSetToGlobe);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.login.username).toEqual("pass");
expect(cipher.login.password).toEqual("word");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://bitwarden.com");
});
});

View File

@@ -0,0 +1,189 @@
import { KeePass2XmlImporter as Importer } from "jslib-common/importers/keepass2XmlImporter";
const TestData = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<KeePassFile>
<Meta>
<Generator>KeePass</Generator>
<DatabaseName />
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
<DatabaseDescription />
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
<DefaultUserName />
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
<Color />
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
<MemoryProtection>
<ProtectTitle>False</ProtectTitle>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>True</ProtectPassword>
<ProtectURL>False</ProtectURL>
<ProtectNotes>False</ProtectNotes>
</MemoryProtection>
<RecycleBinEnabled>True</RecycleBinEnabled>
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
<HistoryMaxItems>10</HistoryMaxItems>
<HistoryMaxSize>6291456</HistoryMaxSize>
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
<Binaries />
<CustomData />
</Meta>
<Root>
<Group>
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
<Name>Root</Name>
<Notes />
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Group>
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
<Name>Folder2</Name>
<Notes>a note about the folder</Notes>
<IconID>48</IconID>
<Times>
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>1</UsageCount>
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
</Times>
<IsExpanded>True</IsExpanded>
<DefaultAutoTypeSequence />
<EnableAutoType>null</EnableAutoType>
<EnableSearching>null</EnableSearching>
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>1</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
</Times>
<String>
<Key>att2</Key>
<Value>att2value</Value>
</String>
<String>
<Key>attr1</Key>
<Value>att1value
line1
line2</Value>
</String>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
<History>
<Entry>
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
<IconID>0</IconID>
<ForegroundColor />
<BackgroundColor />
<OverrideURL />
<Tags />
<Times>
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
<Expires>False</Expires>
<UsageCount>0</UsageCount>
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
</Times>
<String>
<Key>Notes</Key>
<Value>This is a note!!!
line1
line2</Value>
</String>
<String>
<Key>Password</Key>
<Value ProtectInMemory="True">googpass</Value>
</String>
<String>
<Key>Title</Key>
<Value>Google</Value>
</String>
<String>
<Key>URL</Key>
<Value>google.com</Value>
</String>
<String>
<Key>UserName</Key>
<Value>googleuser</Value>
</String>
<AutoType>
<Enabled>True</Enabled>
<DataTransferObfuscation>0</DataTransferObfuscation>
</AutoType>
</Entry>
</History>
</Entry>
</Group>
</Group>
<DeletedObjects />
</Root>
</KeePassFile>`;
describe("KeePass2 Xml Importer", () => {
it("should parse XML data", async () => {
const importer = new Importer();
const result = await importer.parse(TestData);
expect(result != null).toBe(true);
});
});

View File

@@ -0,0 +1,108 @@
import { KeeperJsonImporter as Importer } from "jslib-common/importers/keeperImporters/keeperJsonImporter";
import { Utils } from "jslib-common/misc/utils";
import { testData as TestData } from "./testData/keeperJson/testData";
describe("Keeper Json Importer", () => {
const testDataJson = JSON.stringify(TestData);
let importer: Importer;
beforeEach(() => {
importer = new Importer();
});
it("should parse login data", async () => {
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.name).toEqual("Bank Account 1");
expect(cipher.login.username).toEqual("customer1234");
expect(cipher.login.password).toEqual("4813fJDHF4239fdk");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://chase.com");
expect(cipher.notes).toEqual("These are some notes.");
const cipher2 = result.ciphers.shift();
expect(cipher2.name).toEqual("Bank Account 2");
expect(cipher2.login.username).toEqual("mybankusername");
expect(cipher2.login.password).toEqual("w4k4k193f$^&@#*%2");
expect(cipher2.login.uris.length).toEqual(1);
const uriView2 = cipher2.login.uris.shift();
expect(uriView2.uri).toEqual("https://amex.com");
expect(cipher2.notes).toEqual("Some great information here.");
const cipher3 = result.ciphers.shift();
expect(cipher3.name).toEqual("Some Account");
expect(cipher3.login.username).toEqual("someUserName");
expect(cipher3.login.password).toEqual("w4k4k1wergf$^&@#*%2");
expect(cipher3.notes).toBeNull();
expect(cipher3.fields).toBeNull();
expect(cipher3.login.uris.length).toEqual(1);
const uriView3 = cipher3.login.uris.shift();
expect(uriView3.uri).toEqual("https://example.com");
});
it("should import TOTP when present", async () => {
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.login.totp).toBeNull();
// 2nd Cipher
const cipher2 = result.ciphers.shift();
expect(cipher2.login.totp).toEqual(
"otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30"
);
});
it("should parse custom fields", async () => {
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.fields.length).toBe(1);
expect(cipher.fields[0].name).toEqual("Account Number");
expect(cipher.fields[0].value).toEqual("123-456-789");
// 2nd Cipher
const cipher2 = result.ciphers.shift();
expect(cipher2.fields.length).toBe(2);
expect(cipher2.fields[0].name).toEqual("Security Group");
expect(cipher2.fields[0].value).toEqual("Public");
expect(cipher2.fields[1].name).toEqual("IP Address");
expect(cipher2.fields[1].value).toEqual("12.45.67.8");
});
it("should create folders and assigned ciphers to them", async () => {
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
const folders = result.folders;
expect(folders.length).toBe(2);
expect(folders[0].name).toBe("Optional Private Folder 1");
expect(folders[1].name).toBe("My Customer 1");
expect(result.folderRelationships[0]).toEqual([0, 0]);
expect(result.folderRelationships[1]).toEqual([1, 0]);
expect(result.folderRelationships[2]).toEqual([1, 1]);
});
it("should create collections if part of an organization", async () => {
importer.organizationId = Utils.newGuid();
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
const collections = result.collections;
expect(collections.length).toBe(2);
expect(collections[0].name).toBe("Optional Private Folder 1");
expect(collections[1].name).toBe("My Customer 1");
expect(result.collectionRelationships[0]).toEqual([0, 0]);
expect(result.collectionRelationships[1]).toEqual([1, 0]);
expect(result.collectionRelationships[2]).toEqual([1, 1]);
});
});

View File

@@ -0,0 +1,202 @@
import { CipherType } from "jslib-common/enums/cipherType";
import { FieldType } from "jslib-common/enums/fieldType";
import { LastPassCsvImporter as Importer } from "jslib-common/importers/lastpassCsvImporter";
import { ImportResult } from "jslib-common/models/domain/importResult";
import { CipherView } from "jslib-common/models/view/cipherView";
import { FieldView } from "jslib-common/models/view/fieldView";
function baseExcept(result: ImportResult) {
expect(result).not.toBeNull();
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(1);
}
function expectLogin(cipher: CipherView) {
expect(cipher.type).toBe(CipherType.Login);
expect(cipher.name).toBe("example.com");
expect(cipher.notes).toBe("super secure notes");
expect(cipher.login.uri).toBe("http://example.com");
expect(cipher.login.username).toBe("someUser");
expect(cipher.login.password).toBe("myPassword");
expect(cipher.login.totp).toBe("Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G");
}
const CipherData = [
{
title: "should parse expiration date",
csv: `url,username,password,extra,name,grouping,fav
http://sn,,,"NoteType:Credit Card
Name on Card:John Doe
Type:
Number:1234567812345678
Security Code:123
Start Date:October,2017
Expiration Date:June,2020
Notes:some text
",Credit-card,,0`,
expected: Object.assign(new CipherView(), {
id: null,
organizationId: null,
folderId: null,
name: "Credit-card",
notes: "some text\n",
type: 3,
card: {
cardholderName: "John Doe",
number: "1234567812345678",
code: "123",
expYear: "2020",
expMonth: "6",
},
fields: [
Object.assign(new FieldView(), {
name: "Start Date",
value: "October,2017",
type: FieldType.Text,
}),
],
}),
},
{
title: "should parse blank card note",
csv: `url,username,password,extra,name,grouping,fav
http://sn,,,"NoteType:Credit Card
Name on Card:
Type:
Number:
Security Code:
Start Date:,
Expiration Date:,
Notes:",empty,,0`,
expected: Object.assign(new CipherView(), {
id: null,
organizationId: null,
folderId: null,
name: "empty",
notes: null,
type: 3,
card: {
expMonth: undefined,
},
fields: [
Object.assign(new FieldView(), {
name: "Start Date",
value: ",",
type: FieldType.Text,
}),
],
}),
},
{
title: "should parse card expiration date w/ no exp year",
csv: `url,username,password,extra,name,grouping,fav
http://sn,,,"NoteType:Credit Card
Name on Card:John Doe
Type:Visa
Number:1234567887654321
Security Code:321
Start Date:,
Expiration Date:January,
Notes:",noyear,,0`,
expected: Object.assign(new CipherView(), {
id: null,
organizationId: null,
folderId: null,
name: "noyear",
notes: null,
type: 3,
card: {
cardholderName: "John Doe",
number: "1234567887654321",
code: "321",
expMonth: "1",
},
fields: [
Object.assign(new FieldView(), {
name: "Type",
value: "Visa",
type: FieldType.Text,
}),
Object.assign(new FieldView(), {
name: "Start Date",
value: ",",
type: FieldType.Text,
}),
],
}),
},
{
title: "should parse card expiration date w/ no month",
csv: `url,username,password,extra,name,grouping,fav
http://sn,,,"NoteType:Credit Card
Name on Card:John Doe
Type:Mastercard
Number:8765432112345678
Security Code:987
Start Date:,
Expiration Date:,2020
Notes:",nomonth,,0`,
expected: Object.assign(new CipherView(), {
id: null,
organizationId: null,
folderId: null,
name: "nomonth",
notes: null,
type: 3,
card: {
cardholderName: "John Doe",
number: "8765432112345678",
code: "987",
expYear: "2020",
expMonth: undefined,
},
fields: [
Object.assign(new FieldView(), {
name: "Type",
value: "Mastercard",
type: FieldType.Text,
}),
Object.assign(new FieldView(), {
name: "Start Date",
value: ",",
type: FieldType.Text,
}),
],
}),
},
];
describe("Lastpass CSV Importer", () => {
CipherData.forEach((data) => {
it(data.title, async () => {
const importer = new Importer();
const result = await importer.parse(data.csv);
expect(result != null).toBe(true);
expect(result.ciphers.length).toBeGreaterThan(0);
const cipher = result.ciphers.shift();
let property: keyof typeof data.expected;
for (property in data.expected) {
// eslint-disable-next-line
if (data.expected.hasOwnProperty(property)) {
// eslint-disable-next-line
expect(cipher.hasOwnProperty(property)).toBe(true);
expect(cipher[property]).toEqual(data.expected[property]);
}
}
});
});
it("should parse login with totp", async () => {
const input = `url,username,password,totp,extra,name,grouping,fav
http://example.com,someUser,myPassword,Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G,super secure notes,example.com,,0`;
const importer = new Importer();
const result = await importer.parse(input);
baseExcept(result);
const cipher = result.ciphers[0];
expectLogin(cipher);
});
});

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