1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-01-06 18:44:04 +00:00

Compare commits

..

63 Commits

Author SHA1 Message Date
Bitwarden DevOps
1909194d5a Bumped version to 2024.3.2 (#473)
(cherry picked from commit 1480445d35)
2024-03-27 12:27:27 -04:00
Bitwarden DevOps
fc04964663 Bumped version to 2024.3.1 (#472) 2024-03-25 14:15:46 +00:00
Addison Beck
cc05bcb4a6 Fix warning when attempting a double login in the cli (#468)
* Replace call to `getEntityType` with a static string

* Delete several unused `StateService` methods
2024-03-19 14:30:38 +00:00
Vince Grassia
5ce3b01ff1 Add Cleanup RC Branch workflow (#470) 2024-03-18 11:36:31 -06:00
renovate[bot]
079c3ee840 [deps]: Update chrnorm/deployment-status action to v2.0.3 (#469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-18 10:10:51 -04:00
Vince Grassia
f88ce25b59 DEVOPS-1840 - Update for automatic version bump calculation (#462) 2024-03-13 14:55:55 +01:00
Bitwarden DevOps
599473f6e4 Bumped version to 2024.3.0 (#467) 2024-03-11 20:25:02 +00:00
Opeyemi
df389cbd08 Update Failure Job (#463) 2024-03-06 14:01:16 +01:00
Vince Grassia
051b6dc3cf DEVOPS-1800 - Migrate Secrets (#461) 2024-02-28 11:48:14 -07:00
renovate[bot]
5727dd75cc [deps]: Update https-proxy-agent to v7 (#451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-16 17:38:38 -06:00
renovate[bot]
435f2d10b7 [deps]: Update npm to v10 (#450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-16 16:51:58 -06:00
Opeyemi
dab646675f Remove individual linter file (#449) 2024-02-16 12:37:32 +01:00
renovate[bot]
8dc65ef371 [deps]: Update chrnorm/deployment-action action to v2.0.7 (#441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-12 14:08:39 -05:00
renovate[bot]
9925fdea40 [deps]: Update ncipollo/release-action action to v1.14.0 (#442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-12 14:08:25 -05:00
Vince Grassia
4c61498714 Fix while loop (#448) 2024-02-12 18:16:38 +00:00
Vince Grassia
930f8c84d5 Fix while loop logic (#447) 2024-02-12 07:35:31 -08:00
renovate[bot]
d20818ee49 [deps]: Update actions/setup-node action to v4.0.2 (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-12 10:34:28 -05:00
renovate[bot]
6936c218d1 [deps]: Update actions/upload-artifact action to v4 (#444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-12 10:34:04 -05:00
Bitwarden DevOps
8ef5459801 Bumped version to 2024.2.2 (#446) 2024-02-12 14:51:51 +00:00
renovate[bot]
cb615412aa [AC-2155] [deps]: Update commander to v12 (#439)
* [deps]: Update commander to v12

* Remove duplicate package.json entry

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Addison Beck <hello@addisonbeck.com>
2024-02-05 10:13:42 -06:00
renovate[bot]
2d69d2b791 [deps]: Update eslint-import-resolver-typescript to v3 (#436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-05 09:26:27 -06:00
renovate[bot]
0630b4f52e [deps]: Update actions/setup-node action to v3.8.2 (#430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-05 09:12:18 -05:00
renovate[bot]
690c9cd5cb [deps]: Update crazy-max/ghaction-import-gpg action to v6.1.0 (#431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-05 09:12:06 -05:00
renovate[bot]
3f0454b1d8 [deps]: Update actions/checkout action to v4 (#432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-05 09:11:48 -05:00
Vince Grassia
1a84084b5d Update Version Bump workflow logic (#427) 2024-02-05 08:49:28 -05:00
renovate[bot]
c5fb57576c [deps]: Update eslint-config-prettier to v9 (#435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-02 14:08:53 -06:00
renovate[bot]
165083a245 [deps]: Update rimraf to v5 (#428)
* [deps]: Update rimraf to v5

* Adjust commands to include newly needed options

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Addison Beck <hello@addisonbeck.com>
2024-02-02 07:02:39 -06:00
renovate[bot]
84f1f5b81f [deps]: Update husky to v9 (#426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-01 14:56:32 -06:00
renovate[bot]
9599c66586 [deps]: Update @angular/cdk to v17 (#329)
* [deps]: Update @angular/cdk to v17

* Lets just go to 16 instead

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Addison Beck <hello@addisonbeck.com>
2024-02-01 13:08:56 -06:00
Addison Beck
38b2a13df6 Upgrade angular & friends (v15.2.9 ⮕ v16.2.12) (#424)
* Upgrade @angular packages to 16.2.12

Upgrade a webpack package

* Rebuild package-lock.json

* Remove use of deprecated Guard interfaces

Update route guards to use mapToCanActivate
2024-02-01 11:26:22 -06:00
Addison Beck
1fb4378046 Upgrade electron & friends (v18.3.15 ⮕ v28.2.0) (#418)
* Update versions in package.json

* Rebuild package-lock.json

fix

* Update electron-log usage

* Change necessary logic to get the newest version of the package
  working
* Wrap up `init()` logic in a way that has parity with Bitwarden Desktop

Updates for electron-log

fix

* Update notarize script to match the latest @electron/notarize version

Update notarize step to look more like clients
2024-02-01 16:07:55 +00:00
Bitwarden DevOps
8a661fbc5e Bumped version to 2024.2.0 (#422) 2024-01-29 10:45:06 -05:00
renovate[bot]
cf56b5fb57 [deps]: Update Azure/login action to v1.6.0 (#421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-29 10:32:42 -05:00
Addison Beck
9c88e66a27 Upgrade prettier (#409) 2024-01-27 15:56:40 -06:00
Addison Beck
5b7b68f1cb Upgrade commander (v7.2.0 ⮕ v11.1.0) (#410)
* Upgrade package in package.json

* Rebuild package-lock.json

* Update imports to reflect new types

fixup
2024-01-27 14:24:23 -06:00
Addison Beck
a09473c632 Upgrade typescript-transform-paths (v2.2.4 ⮕ v3.4.6) (#416)
* Update version in package.json

* Rebuild package-lock.json
2024-01-27 13:45:56 -06:00
Addison Beck
71727dae7d Upgrade copy-webpack-plugin (v11.0.0 ⮕ v12.0.2) (#415)
* Update version number in package.json

* Rebuild package-lock.json
2024-01-27 13:40:23 -06:00
Addison Beck
3dbd34ebc3 Upgrade sass-loader (v12.6.0 ⮕ v14.0.0) (#414)
* Update version number in package.json

* Rebuild package-lock.json
2024-01-26 16:21:21 -06:00
Addison Beck
6dd121acc6 Upgrade html-loader (v3.1.2 ⮕ v5.0.0) (#413)
* Upgrade version in package.json

* Rebuild package-lock.json
2024-01-26 16:17:36 -06:00
Addison Beck
d6ddb499f0 Upgrade lint-staged (v12.5.0 ⮕ v15.2.0) (#412)
* Update version number in package.json

* Rebuild package-lock.json
2024-01-26 16:11:55 -06:00
Addison Beck
5b4e09be93 Upgrade dotenv (v9.0.2 ⮕ v16.4.1) (#411)
* Upgrade package in package.json

* Upgrade package-lock.json
2024-01-26 16:08:19 -06:00
Addison Beck
a48e0af042 Peform any minor package upgrades (#401) 2024-01-26 09:46:46 -06:00
Vince Grassia
a133718eb7 Update Renovate config (#407) 2024-01-24 12:46:35 -06:00
Daniel James Smith
37bdd75c67 Tools - Remove importer and exporters (#402)
* Remove all importers and related files

* Remove exporter and all related files

* Remove packages: papaparse and jsdom

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2024-01-17 20:33:24 +01:00
Addison Beck
a9f1d32ce0 Apply any availible patch upgrades to dependencies (#400) 2024-01-17 11:13:24 +10:00
Addison Beck
6e76f23653 [AC-1743] pt. 3 ⮕ Remove unused packages (#399)
* Remove package @angular/cli

* Remove package duo_web_sdk

* Remove package nodemon

* Remove package prebuild-install

* Remove package tapable

* Remove tsconfig-paths

* Remove ttypescript

* Remove typemoq

* Remove package jest-junit

* Remove package ts-node

* Specify dart sass in the webpack sass-loader config

* Add google-auth-library to package.json

* Add dotenv to package.json

* Ignore packages that break depcheck's regex and use case

Ignore @types/jest in depcheck

* Remove redundant config line
2024-01-16 16:01:44 -06:00
Addison Beck
39ed9359fe [AC-1743] pt. 2.5 ⮕ Fix the unit tests for good (#398)
* Remove uneeded redirects to a tsconfig.spec.json

* Got the tests running again

* Delete webCryptoFunctionService

* Delete unused utils tests

* Add a test workflow
2024-01-12 11:05:35 -08:00
renovate[bot]
d1cb92b5e7 [deps]: Pin dependencies (#389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 09:27:16 -06:00
Addison Beck
eacdb6b8a8 [AC-1743] pt. 2: Update eslintrc and fix any errors (#393)
* Sync eslintrc with clients repo

* Autofix one eslint error

* Add type attributes to buttons for eslint

* Properly destroy ApiKeyComponent

* Fix eslint issues related to state

* Fix eslint warnings for default named imports

* Ran prettier

* Be more proactive about an unsubscribe

* Rework subscription
2024-01-05 11:36:19 +10:00
Addison Beck
95f613d61a Remove package.json from src (#395) 2023-12-21 14:00:27 +10:00
Addison Beck
c259962279 [AC-1743] pt. 1: Unpackage-ify jslib (#374)
* Unpackage-ify jslib

* Adjust .tsconfig path for root and apply to jslib

* Rebuild package-lock.json

* Disable husky in CI

* Revert an incorrect find/replace

* Add jslib/shared/.eslintrc rules to root eslintrc

* Revert package.json change to ignore spec files when linting

* Ensure custom matcher gets imported in jslib tests

* Fix small workflow bugs from merging

* Try and get CI builds moving again

* Always sign and notorize builds in CI

* Revert erroneous verion bump
2023-12-20 11:33:33 -05:00
Vince Grassia
9126d4ae59 Merge _cut_rc.yml into version-bump.yml (#392) 2023-12-18 10:58:55 -07:00
Vince Grassia
f3b01afd0b Revert "Bumped version to 2023.12.0 (#387)" (#391)
This reverts commit 187d1e17b1.
2023-12-18 09:35:25 -05:00
Vince Grassia
82b5f9bd04 Update version bump workflow (#388) 2023-12-15 13:30:04 +01:00
Bitwarden DevOps
187d1e17b1 Bumped version to 2023.12.0 (#387) 2023-12-14 17:23:28 -05:00
Joseph Flinn
e1300f585a Fix branch (#385) 2023-12-13 05:56:04 -05:00
Joseph Flinn
11f5e2993a Point workflows to main (#378) 2023-12-12 12:18:16 -08:00
Vince Grassia
ff87907b4e Add token to checkout step (#382) 2023-12-12 09:38:56 -08:00
Vince Grassia
1163f34317 Fix version bump workflow on call (#380) 2023-12-12 08:54:13 -08:00
Vince Grassia
7a2e9ecec6 Update workflow call to use main branch (#376) 2023-12-12 11:03:38 -05:00
Vince Grassia
bab928c07c Update Version Bump workflow (#377) 2023-12-12 10:22:06 -05:00
Oscar Hinton
1546cc2012 Upgrade the jest test suite to match the clients (#379)
* Upgrade the jest test suite to match the clients

* Update makeStaticByteArray
2023-12-12 13:45:12 +01:00
Oscar Hinton
36ab2953b5 Update renovate (#373)
* Update renovate.json

* Update renovate.json

* Update renovate.json
2023-12-05 16:54:49 +01:00
315 changed files with 10787 additions and 15655 deletions

1
.depcheckrc Normal file
View File

@@ -0,0 +1 @@
ignores: ["*-loader", "webpack-cli", "@types/jest"]

View File

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

View File

@@ -2,7 +2,6 @@
"root": true,
"env": {
"browser": true,
"webextensions": true,
"node": true
},
"overrides": [
@@ -11,7 +10,7 @@
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json"],
"project": ["./tsconfig.eslint.json"],
"sourceType": "module",
"ecmaVersion": 2020
},
@@ -53,12 +52,12 @@
"newlines-between": "always",
"pathGroups": [
{
"pattern": "@bitwarden/**",
"pattern": "@/jslib/**/*",
"group": "external",
"position": "after"
},
{
"pattern": "src/**/*",
"pattern": "@/src/**/*",
"group": "parent",
"position": "before"
}
@@ -86,7 +85,11 @@
},
{
"files": ["*.html"],
"parser": "@angular-eslint/template-parser"
"parser": "@angular-eslint/template-parser",
"plugins": ["@angular-eslint/template"],
"rules": {
"@angular-eslint/template/button-has-type": "error"
}
}
]
}

28
.github/renovate.json vendored
View File

@@ -2,40 +2,30 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
"github>bitwarden/renovate-config:pin-actions",
":combinePatchMinorReleases",
":dependencyDashboard",
":maintainLockFilesWeekly",
":pinAllExceptPeerDependencies",
":prConcurrentLimit10",
":rebaseStalePrs",
"schedule:weekends",
":separateMajorReleases"
":separateMajorReleases",
"group:monorepos",
"schedule:weekends"
],
"enabledManagers": ["github-actions", "npm"],
"commitMessagePrefix": "[deps]:",
"commitMessageTopic": "{{depName}}",
"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"
"matchFileNames": ["package.json"],
"description": "Admin Console owns general dependencies",
"reviewers": ["team:team-admin-console-dev"]
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,14 +2,9 @@
name: Build
on:
push:
branches-ignore:
- 'l10n_master'
paths-ignore:
- '.github/workflows/**'
pull_request: {}
workflow_dispatch: {}
jobs:
cloc:
name: CLOC
@@ -56,7 +51,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -124,14 +119,14 @@ jobs:
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -151,7 +146,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -212,14 +207,14 @@ jobs:
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -243,7 +238,7 @@ jobs:
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -337,8 +332,7 @@ jobs:
- name: Zip
shell: cmd
run: |
7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
run: 7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
- name: Version Test
shell: pwsh
@@ -357,14 +351,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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -384,7 +378,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -417,28 +411,28 @@ jobs:
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: latest.yml
path: ./dist/latest.yml
@@ -458,7 +452,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -485,14 +479,14 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
@@ -512,7 +506,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -529,44 +523,43 @@ jobs:
npm --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
shell: bash
- name: Decrypt secrets
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
shell: bash
- name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Get certificates
run: |
mkdir -p $HOME/secrets
mkdir -p $HOME/certificates
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/devid-app-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/devid-app-cert.p12.gpg"
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert |
jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/devid-installer-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/devid-installer-cert.p12.gpg"
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert |
jq -r .value | base64 -d > $HOME/certificates/devid-installer-cert.p12
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/macdev-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/macdev-cert.p12.gpg"
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
- name: Set up keychain
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }}
MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }}
shell: bash
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
security set-keychain-settings -lut 1200 build.keychain
security import "$HOME/secrets/devid-app-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
security import "$HOME/certificates/devid-app-cert.p12" -k build.keychain -P "" \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security import "$HOME/secrets/devid-installer-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
security import "$HOME/certificates/devid-installer-cert.p12" -k build.keychain -P "" \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security import "$HOME/secrets/macdev-cert.p12" -k build.keychain -P $MACDEV_CERT_PASSWORD \
security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
- name: Load package version
@@ -586,30 +579,31 @@ jobs:
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
CSC_FOR_PULL_REQUEST: true
- name: Upload .zip artifact
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml
@@ -630,37 +624,11 @@ jobs:
- macos-gui
steps:
- name: Check if any job failed
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
SETUP_STATUS: ${{ needs.setup.result }}
LINUX_CLI_STATUS: ${{ needs.linux-cli.result }}
MACOS_CLI_STATUS: ${{ needs.macos-cli.result }}
WINDOWS_CLI_STATUS: ${{ needs.windows-cli.result }}
WINDOWS_GUI_STATUS: ${{ needs.windows-gui.result }}
LINUX_GUI_STATUS: ${{ needs.linux-gui.result }}
MACOS_GUI_STATUS: ${{ needs.macos-gui.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$LINUX_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$MACOS_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_GUI_STATUS" = "failure" ]; then
exit 1
elif [ "$LINUX_GUI_STATUS" = "failure" ]; then
exit 1
elif [ "$MACOS_GUI_STATUS" = "failure" ]; then
exit 1
fi
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc') && contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
if: failure()
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

53
.github/workflows/cleanup-rc-branch.yml vendored Normal file
View File

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

View File

@@ -48,7 +48,7 @@ jobs:
needs: setup
steps:
- name: Create GitHub deployment
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
id: deployment
with:
token: '${{ secrets.GITHUB_TOKEN }}'
@@ -71,11 +71,11 @@ jobs:
with:
workflow: build.yml
workflow_conclusion: success
branch: master
branch: main
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
env:
PKG_VERSION: ${{ needs.setup.outputs.release-version }}
with:
@@ -104,7 +104,7 @@ jobs:
- name: Update deployment status to Success
if: ${{ success() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'success'
@@ -112,7 +112,7 @@ jobs:
- name: Update deployment status to Failure
if: ${{ failure() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'failure'

54
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
---
name: Run tests
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc-*"
pull_request: {}
defaults:
run:
shell: bash
jobs:
test:
name: Run tests
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Get Node Version
id: retrieve-node-version
run: |
NODE_NVMRC=$(cat .nvmrc)
NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
- name: Set up Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
- name: Print environment
run: |
node --version
npm --version
- name: Install Node dependencies
run: npm ci
# We use isolatedModules: true which disables typechecking in tests
# Tests in apps/ are typechecked when their app is built, so we just do it here for libs/
# See https://bitwarden.atlassian.net/browse/EC-497
- name: Run typechecking
run: npm run test:types --coverage
- name: Run tests
run: npm run test

View File

@@ -4,20 +4,45 @@ name: Version Bump
on:
workflow_dispatch:
inputs:
version_number:
description: "New Version"
required: true
version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
type: boolean
jobs:
bump_version:
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
name: Bump Version
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.set-final-version-output.outputs.version }}
steps:
- name: Validate version input
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with:
version: ${{ inputs.version_number_override }}
- name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
- name: Login to Azure - Prod Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
- name: Check if RC branch exists
if: ${{ inputs.cut_rc_branch == true }}
run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
fi
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -26,50 +51,119 @@ jobs:
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.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: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
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@main
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./package.json"
- name: Commit files
- name: Setup git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Create Version Branch
id: create-branch
run: |
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Get current version
id: current-version
run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed."
exit 1
fi
# Check if version is newer.
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
if [ $? -eq 0 ]; then
echo "Version check successful."
else
echo "Version check failed."
exit 1
fi
- name: Calculate next release version
if: ${{ inputs.version_number_override == '' }}
id: calculate-next-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-version.outputs.version }}
- name: Bump Version - Package - Version Override
if: ${{ inputs.version_number_override != '' }}
id: bump-version-override
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "./package.json"
version: ${{ inputs.version_number_override }}
- name: Bump Version - Package - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
id: bump-version-automatic
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "./package.json"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Set final version output
id: set-final-version-output
run: |
if [[ "${{ steps.bump-version-override.outcome }}" == "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" == "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed
id: version-changed
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT
else
echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT
echo "No changes to commit!";
fi
- name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
run: git push -u origin $PR_BRANCH
- name: Create Version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: create-pr
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
BASE_BRANCH: master
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: |
gh pr create --title "$TITLE" \
--base "$BASE" \
PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
@@ -82,4 +176,50 @@ jobs:
- [X] Other
## Objective
Automated version bump to ${{ github.event.inputs.version_number }}"
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
steps:
- name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
- name: Verify version has been updated
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
- name: Cut RC branch
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc

View File

@@ -1,11 +0,0 @@
---
name: Workflow Linter
on:
pull_request:
paths:
- .github/workflows/**
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@c970b0fb89bd966749280e832928db62040812bf

42
jest.config.js Normal file
View File

@@ -0,0 +1,42 @@
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
const tsPreset = require("ts-jest/jest-preset");
const angularPreset = require("jest-preset-angular/jest-preset");
const { defaultTransformerOptions } = require("jest-preset-angular/presets");
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
// ...tsPreset,
// ...angularPreset,
preset: "jest-preset-angular",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],
roots: ["<rootDir>"],
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
// Workaround for a memory leak that crashes tests in CI:
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
// Also anecdotally improves performance when run locally
maxWorkers: 3,
transform: {
"^.+\\.tsx?$": [
"jest-preset-angular",
// 'ts-jest',
{
...defaultTransformerOptions,
tsconfig: "./tsconfig.json",
// Further workaround for memory leak, recommended here:
// https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
isolatedModules: true,
},
],
},
};

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

@@ -12,10 +12,10 @@
{{ enforcedPolicyMessage }}
<ul>
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{ "policyInEffectMinComplexity" | i18n : getPasswordScoreAlertDisplay() }}
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{ "policyInEffectMinLength" | i18n : enforcedPolicyOptions?.minLength.toString() }}
{{ "policyInEffectMinLength" | i18n: enforcedPolicyOptions?.minLength.toString() }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{ "policyInEffectUppercase" | i18n }}
@@ -27,7 +27,7 @@
{{ "policyInEffectNumbers" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{ "policyInEffectSpecial" | i18n : "!@#$%^&*" }}
{{ "policyInEffectSpecial" | i18n: "!@#$%^&*" }}
</li>
</ul>
</div>

View File

@@ -19,7 +19,7 @@ export class EnvironmentComponent {
constructor(
protected platformUtilsService: PlatformUtilsService,
protected environmentService: EnvironmentService,
protected i18nService: I18nService
protected i18nService: I18nService,
) {
const urls = this.environmentService.getUrls();

View File

@@ -34,7 +34,10 @@ export class IconComponent implements OnChanges {
private iconsUrl: string;
constructor(environmentService: EnvironmentService, private stateService: StateService) {
constructor(
environmentService: EnvironmentService,
private stateService: StateService,
) {
this.iconsUrl = environmentService.getIconsUrl();
}

View File

@@ -35,7 +35,7 @@ export class DynamicModalComponent implements AfterViewInit, OnDestroy {
private cd: ChangeDetectorRef,
private el: ElementRef<HTMLElement>,
private focusTrapFactory: ConfigurableFocusTrapFactory,
public modalRef: ModalRef
public modalRef: ModalRef,
) {}
ngAfterViewInit() {
@@ -47,7 +47,7 @@ export class DynamicModalComponent implements AfterViewInit, OnDestroy {
this.modalRef.created(this.el.nativeElement);
this.focusTrap = this.focusTrapFactory.create(
this.el.nativeElement.querySelector(".modal-dialog")
this.el.nativeElement.querySelector(".modal-dialog"),
);
if (this.el.nativeElement.querySelector("[appAutoFocus]") == null) {
this.focusTrap.focusFirstTabbableElementWhenReady();

View File

@@ -1,12 +1,15 @@
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
export class ModalInjector implements Injector {
constructor(private _parentInjector: Injector, private _additionalTokens: WeakMap<any, any>) {}
constructor(
private _parentInjector: Injector,
private _additionalTokens: WeakMap<any, any>,
) {}
get<T>(
token: ProviderToken<T>,
notFoundValue: undefined,
options: InjectOptions & { optional?: false }
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;

View File

@@ -1,11 +1,10 @@
import { Directive } from "@angular/core";
import { ModalRef } from "./modal/modal.ref";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { PlatformUtilsService } from "@/jslib/common/src/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.
@@ -20,7 +19,7 @@ export class PasswordRepromptComponent {
private modalRef: ModalRef,
private cryptoService: CryptoService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
private i18nService: I18nService,
) {}
togglePassword() {
@@ -32,7 +31,7 @@ export class PasswordRepromptComponent {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidMasterPassword")
this.i18nService.t("invalidMasterPassword"),
);
return;
}

View File

@@ -62,7 +62,10 @@ import {
preserveWhitespaces: false,
})
export class BitwardenToast extends BaseToast {
constructor(protected toastrService: ToastrService, public toastPackage: ToastPackage) {
constructor(
protected toastrService: ToastrService,
public toastPackage: ToastPackage,
) {
super(toastrService, toastPackage);
}
}

View File

@@ -10,7 +10,10 @@ export class A11yTitleDirective {
private title: string;
constructor(private el: ElementRef, private renderer: Renderer2) {}
constructor(
private el: ElementRef,
private renderer: Renderer2,
) {}
ngOnInit() {
if (!this.el.nativeElement.hasAttribute("title")) {

View File

@@ -1,10 +1,9 @@
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
import { ValidationService } from "../services/validation.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { ErrorResponse } from "@/jslib/common/src/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.
@@ -21,7 +20,7 @@ export class ApiActionDirective implements OnChanges {
constructor(
private el: ElementRef,
private validationService: ValidationService,
private logService: LogService
private logService: LogService,
) {}
ngOnChanges(changes: any) {
@@ -44,7 +43,7 @@ export class ApiActionDirective implements OnChanges {
}
this.logService?.error(`Received API exception: ${e}`);
this.validationService.showError(e);
}
},
);
}
}

View File

@@ -13,7 +13,10 @@ export class AutofocusDirective {
private autofocus: boolean;
constructor(private el: ElementRef, private ngZone: NgZone) {}
constructor(
private el: ElementRef,
private ngZone: NgZone,
) {}
ngOnInit() {
if (!Utils.isMobileBrowser && this.autofocus) {

View File

@@ -13,7 +13,7 @@ export class BoxRowDirective implements OnInit {
ngOnInit(): void {
this.formEls = Array.from(
this.el.querySelectorAll('input:not([type="hidden"]), select, textarea')
this.el.querySelectorAll('input:not([type="hidden"]), select, textarea'),
);
this.formEls.forEach((formEl) => {
formEl.addEventListener(
@@ -21,7 +21,7 @@ export class BoxRowDirective implements OnInit {
() => {
this.el.classList.add("active");
},
false
false,
);
formEl.addEventListener(
@@ -29,7 +29,7 @@ export class BoxRowDirective implements OnInit {
() => {
this.el.classList.remove("active");
},
false
false,
);
});
}

View File

@@ -4,7 +4,8 @@ $icomoon-font-path: "/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"),
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");

View File

@@ -13,7 +13,7 @@ export class AuthGuardService {
private router: Router,
private messagingService: MessagingService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService
private stateService: StateService,
) {}
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {

View File

@@ -1,13 +1,5 @@
import { Injector, LOCALE_ID, NgModule } from "@angular/core";
import { AuthGuardService } from "./auth-guard.service";
import { BroadcasterService } from "./broadcaster.service";
import { LockGuardService } from "./lock-guard.service";
import { ModalService } from "./modal.service";
import { PasswordRepromptService } from "./passwordReprompt.service";
import { UnauthGuardService } from "./unauth-guard.service";
import { ValidationService } from "./validation.service";
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
import { AuditService as AuditServiceAbstraction } from "@/jslib/common/src/abstractions/audit.service";
@@ -19,7 +11,6 @@ import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/ab
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
import { EventService as EventServiceAbstraction } from "@/jslib/common/src/abstractions/event.service";
import { ExportService as ExportServiceAbstraction } from "@/jslib/common/src/abstractions/export.service";
import { FileUploadService as FileUploadServiceAbstraction } from "@/jslib/common/src/abstractions/fileUpload.service";
import { FolderService as FolderServiceAbstraction } from "@/jslib/common/src/abstractions/folder.service";
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
@@ -59,7 +50,6 @@ import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.servic
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
import { EventService } from "@/jslib/common/src/services/event.service";
import { ExportService } from "@/jslib/common/src/services/export.service";
import { FileUploadService } from "@/jslib/common/src/services/fileUpload.service";
import { FolderService } from "@/jslib/common/src/services/folder.service";
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
@@ -80,8 +70,14 @@ import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service"
import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service";
import { UsernameGenerationService } from "@/jslib/common/src/services/usernameGeneration.service";
import { VaultTimeoutService } from "@/jslib/common/src/services/vaultTimeout.service";
import { WebCryptoFunctionService } from "@/jslib/common/src/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: [],
@@ -135,7 +131,7 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
i18nService: I18nServiceAbstraction,
injector: Injector,
logService: LogService,
stateService: StateServiceAbstraction
stateService: StateServiceAbstraction,
) =>
new CipherService(
cryptoService,
@@ -145,7 +141,7 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
i18nService,
() => injector.get(SearchServiceAbstraction),
logService,
stateService
stateService,
),
deps: [
CryptoServiceAbstraction,
@@ -213,14 +209,14 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
platformUtilsService: PlatformUtilsServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
appIdService: AppIdServiceAbstraction
appIdService: AppIdServiceAbstraction,
) =>
new ApiService(
tokenService,
platformUtilsService,
environmentService,
appIdService,
async (expired: boolean) => messagingService.send("logout", { expired: expired })
async (expired: boolean) => messagingService.send("logout", { expired: expired }),
),
deps: [
TokenServiceAbstraction,
@@ -251,7 +247,7 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
keyConnectorService: KeyConnectorServiceAbstraction,
stateService: StateServiceAbstraction,
organizationService: OrganizationServiceAbstraction,
providerService: ProviderServiceAbstraction
providerService: ProviderServiceAbstraction,
) =>
new SyncService(
apiService,
@@ -268,7 +264,7 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
stateService,
organizationService,
providerService,
async (expired: boolean) => messagingService.send("logout", { expired: expired })
async (expired: boolean) => messagingService.send("logout", { expired: expired }),
),
deps: [
ApiServiceAbstraction,
@@ -306,7 +302,7 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
tokenService: TokenServiceAbstraction,
policyService: PolicyServiceAbstraction,
keyConnectorService: KeyConnectorServiceAbstraction,
stateService: StateServiceAbstraction
stateService: StateServiceAbstraction,
) =>
new VaultTimeoutService(
cipherService,
@@ -322,7 +318,7 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
stateService,
null,
async (userId?: string) =>
messagingService.send("logout", { expired: false, userId: userId })
messagingService.send("logout", { expired: false, userId: userId }),
),
deps: [
CipherServiceAbstraction,
@@ -344,14 +340,14 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction,
logService: LogService,
stateMigrationService: StateMigrationServiceAbstraction
stateMigrationService: StateMigrationServiceAbstraction,
) =>
new StateService(
storageService,
secureStorageService,
logService,
stateMigrationService,
new StateFactory(GlobalState, Account)
new StateFactory(GlobalState, Account),
),
deps: [
StorageServiceAbstraction,
@@ -364,25 +360,15 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction
secureStorageService: StorageServiceAbstraction,
) =>
new StateMigrationService(
storageService,
secureStorageService,
new StateFactory(GlobalState, Account)
new StateFactory(GlobalState, Account),
),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
},
{
provide: ExportServiceAbstraction,
useClass: ExportService,
deps: [
FolderServiceAbstraction,
CipherServiceAbstraction,
ApiServiceAbstraction,
CryptoServiceAbstraction,
],
},
{
provide: SearchServiceAbstraction,
useClass: SearchService,
@@ -398,7 +384,7 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
logService: LogService,
stateService: StateServiceAbstraction
stateService: StateServiceAbstraction,
) =>
new NotificationsService(
syncService,
@@ -408,7 +394,7 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
environmentService,
async () => messagingService.send("logout", { expired: true }),
logService,
stateService
stateService,
),
deps: [
SyncServiceAbstraction,
@@ -421,11 +407,6 @@ import { WebCryptoFunctionService } from "@/jslib/common/src/services/webCryptoF
StateServiceAbstraction,
],
},
{
provide: CryptoFunctionServiceAbstraction,
useClass: WebCryptoFunctionService,
deps: ["WINDOW"],
},
{
provide: EventServiceAbstraction,
useClass: EventService,

View File

@@ -11,7 +11,7 @@ export class LockGuardService {
constructor(
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private stateService: StateService
private stateService: StateService,
) {}
async canActivate() {

View File

@@ -31,7 +31,7 @@ export class ModalService {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private applicationRef: ApplicationRef,
private injector: Injector
private injector: Injector,
) {
document.addEventListener("keyup", (event) => {
if (event.key === "Escape" && this.modalCount > 0) {
@@ -51,7 +51,7 @@ export class ModalService {
async openViewRef<T>(
componentType: Type<T>,
viewContainerRef: ViewContainerRef,
setComponentParameters: (component: T) => void = null
setComponentParameters: (component: T) => void = null,
): Promise<[ModalRef, T]> {
const [modalRef, modalComponentRef] = this.openInternal(componentType, null, false);
modalComponentRef.instance.setComponentParameters = setComponentParameters;
@@ -76,7 +76,7 @@ export class ModalService {
registerComponentFactoryResolver<T>(
componentType: Type<T>,
componentFactoryResolver: ComponentFactoryResolver
componentFactoryResolver: ComponentFactoryResolver,
): void {
this.factoryResolvers.set(componentType, componentFactoryResolver);
}
@@ -92,7 +92,7 @@ export class ModalService {
protected openInternal(
componentType: Type<any>,
config?: ModalConfig,
attachToDom?: boolean
attachToDom?: boolean,
): [ModalRef, ComponentRef<DynamicModalComponent>] {
const [modalRef, componentRef] = this.createModalComponent(config);
componentRef.instance.childComponentType = componentType;
@@ -143,7 +143,7 @@ export class ModalService {
dialogEl.style.zIndex = `${this.modalCount}050`;
const modals = Array.from(
el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]')
el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]'),
);
for (const closeElement of modals) {
closeElement.addEventListener("click", () => {
@@ -163,7 +163,7 @@ export class ModalService {
}
protected createModalComponent(
config: ModalConfig
config: ModalConfig,
): [ModalRef, ComponentRef<DynamicModalComponent>] {
const modalRef = new ModalRef();

View File

@@ -1,13 +1,11 @@
import { Injectable } from "@angular/core";
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
import { ModalService } from "./modal.service";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
import { ModalService } from "./modal.service";
/**
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
@@ -19,7 +17,7 @@ export class PasswordRepromptService implements PasswordRepromptServiceAbstracti
constructor(
private modalService: ModalService,
private keyConnectorService: KeyConnectorService
private keyConnectorService: KeyConnectorService,
) {}
protectedFields() {

View File

@@ -10,7 +10,7 @@ export class UnauthGuardService {
constructor(
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private stateService: StateService
private stateService: StateService,
) {}
async canActivate() {

View File

@@ -8,7 +8,7 @@ import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse"
export class ValidationService {
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService
private platformUtilsService: PlatformUtilsService,
) {}
showError(data: any): string[] {

View File

@@ -0,0 +1,83 @@
import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { AttachmentData } from "@/jslib/common/src/models/data/attachmentData";
import { Attachment } from "@/jslib/common/src/models/domain/attachment";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { ContainerService } from "@/jslib/common/src/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/src/models/data/cardData";
import { Card } from "@/jslib/common/src/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/src/enums/cipherRepromptType";
import { CipherType } from "@/jslib/common/src/enums/cipherType";
import { FieldType } from "@/jslib/common/src/enums/fieldType";
import { SecureNoteType } from "@/jslib/common/src/enums/secureNoteType";
import { UriMatchType } from "@/jslib/common/src/enums/uriMatchType";
import { CipherData } from "@/jslib/common/src/models/data/cipherData";
import { Card } from "@/jslib/common/src/models/domain/card";
import { Cipher } from "@/jslib/common/src/models/domain/cipher";
import { Identity } from "@/jslib/common/src/models/domain/identity";
import { Login } from "@/jslib/common/src/models/domain/login";
import { SecureNote } from "@/jslib/common/src/models/domain/secureNote";
import { CardView } from "@/jslib/common/src/models/view/cardView";
import { IdentityView } from "@/jslib/common/src/models/view/identityView";
import { LoginView } from "@/jslib/common/src/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/src/models/data/collectionData";
import { Collection } from "@/jslib/common/src/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/src/abstractions/crypto.service";
import { EncryptionType } from "@/jslib/common/src/enums/encryptionType";
import { EncString } from "@/jslib/common/src/models/domain/encString";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { ContainerService } from "@/jslib/common/src/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/src/enums/fieldType";
import { FieldData } from "@/jslib/common/src/models/data/fieldData";
import { Field } from "@/jslib/common/src/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/src/models/data/folderData";
import { Folder } from "@/jslib/common/src/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/src/models/data/identityData";
import { Identity } from "@/jslib/common/src/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/src/enums/uriMatchType";
import { LoginData } from "@/jslib/common/src/models/data/loginData";
import { Login } from "@/jslib/common/src/models/domain/login";
import { LoginUri } from "@/jslib/common/src/models/domain/loginUri";
import { LoginUriView } from "@/jslib/common/src/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/src/enums/uriMatchType";
import { LoginUriData } from "@/jslib/common/src/models/data/loginUriData";
import { LoginUri } from "@/jslib/common/src/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/src/models/data/passwordHistoryData";
import { Password } from "@/jslib/common/src/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/src/enums/secureNoteType";
import { SecureNoteData } from "@/jslib/common/src/models/data/secureNoteData";
import { SecureNote } from "@/jslib/common/src/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/src/abstractions/crypto.service";
import { SendType } from "@/jslib/common/src/enums/sendType";
import { SendData } from "@/jslib/common/src/models/data/sendData";
import { EncString } from "@/jslib/common/src/models/domain/encString";
import { Send } from "@/jslib/common/src/models/domain/send";
import { SendText } from "@/jslib/common/src/models/domain/sendText";
import { ContainerService } from "@/jslib/common/src/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/src/enums/sendType";
import { SendAccess } from "@/jslib/common/src/models/domain/sendAccess";
import { SendText } from "@/jslib/common/src/models/domain/sendText";
import { SendAccessResponse } from "@/jslib/common/src/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/src/models/data/sendFileData";
import { SendFile } from "@/jslib/common/src/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/src/models/data/sendTextData";
import { SendText } from "@/jslib/common/src/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/src/enums/encryptionType";
import { SymmetricCryptoKey } from "@/jslib/common/src/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,114 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { ApiLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/apiLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
describe("ApiLogInStrategy", () => {
let cryptoService: SubstituteOf<CryptoService>;
let apiService: SubstituteOf<ApiService>;
let tokenService: SubstituteOf<TokenService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let logService: SubstituteOf<LogService>;
let environmentService: SubstituteOf<EnvironmentService>;
let keyConnectorService: SubstituteOf<KeyConnectorService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let apiLogInStrategy: ApiLogInStrategy;
let credentials: ApiLogInCredentials;
const deviceId = Utils.newGuid();
const keyConnectorUrl = "KEY_CONNECTOR_URL";
const apiClientId = "API_CLIENT_ID";
const apiClientSecret = "API_CLIENT_SECRET";
beforeEach(async () => {
cryptoService = Substitute.for<CryptoService>();
apiService = Substitute.for<ApiService>();
tokenService = Substitute.for<TokenService>();
appIdService = Substitute.for<AppIdService>();
platformUtilsService = Substitute.for<PlatformUtilsService>();
messagingService = Substitute.for<MessagingService>();
logService = Substitute.for<LogService>();
environmentService = Substitute.for<EnvironmentService>();
stateService = Substitute.for<StateService>();
keyConnectorService = Substitute.for<KeyConnectorService>();
twoFactorService = Substitute.for<TwoFactorService>();
appIdService.getAppId().resolves(deviceId);
tokenService.getTwoFactorToken().resolves(null);
apiLogInStrategy = new ApiLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
environmentService,
keyConnectorService,
);
credentials = new ApiLogInCredentials(apiClientId, apiClientSecret);
});
it("sends api key credentials to the server", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await apiLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const apiTokenRequest = actual as any;
return (
apiTokenRequest.clientId === apiClientId &&
apiTokenRequest.clientSecret === apiClientSecret &&
apiTokenRequest.device.identifier === deviceId &&
apiTokenRequest.twoFactor.provider == null &&
apiTokenRequest.twoFactor.token == null &&
apiTokenRequest.captchaResponse == null
);
}),
);
});
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await apiLogInStrategy.logIn(credentials);
stateService.received(1).setApiKeyClientId(apiClientId);
stateService.received(1).setApiKeyClientSecret(apiClientSecret);
stateService.received(1).addAccount(Arg.any());
});
it("gets and sets the Key Connector key from environmentUrl", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
await apiLogInStrategy.logIn(credentials);
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
});
});

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,127 @@
import { sequentialize } from "@/jslib/common/src/misc/sequentialize";
describe("sequentialize decorator", () => {
it("should call the function once", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
});
it("should call the function once for each instance of the object", async () => {
const foo = new Foo();
const foo2 = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
promises.push(foo2.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
expect(foo2.calls).toBe(1);
});
it("should call the function once with key function", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.baz(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
});
it("should call the function again when already resolved", async () => {
const foo = new Foo();
await foo.bar(1);
expect(foo.calls).toBe(1);
await foo.bar(1);
expect(foo.calls).toBe(2);
});
it("should call the function again when already resolved with a key function", async () => {
const foo = new Foo();
await foo.baz(1);
expect(foo.calls).toBe(1);
await foo.baz(1);
expect(foo.calls).toBe(2);
});
it("should call the function for each argument", async () => {
const foo = new Foo();
await Promise.all([foo.bar(1), foo.bar(1), foo.bar(2), foo.bar(2), foo.bar(3), foo.bar(3)]);
expect(foo.calls).toBe(3);
});
it("should call the function for each argument with key function", async () => {
const foo = new Foo();
await Promise.all([foo.baz(1), foo.baz(1), foo.baz(2), foo.baz(2), foo.baz(3), foo.baz(3)]);
expect(foo.calls).toBe(3);
});
it("should return correct result for each call", async () => {
const foo = new Foo();
const allRes: number[] = [];
await Promise.all([
foo.bar(1).then((res) => allRes.push(res)),
foo.bar(1).then((res) => allRes.push(res)),
foo.bar(2).then((res) => allRes.push(res)),
foo.bar(2).then((res) => allRes.push(res)),
foo.bar(3).then((res) => allRes.push(res)),
foo.bar(3).then((res) => allRes.push(res)),
]);
expect(foo.calls).toBe(3);
expect(allRes.length).toBe(6);
allRes.sort();
expect(allRes).toEqual([2, 2, 4, 4, 6, 6]);
});
it("should return correct result for each call with key function", async () => {
const foo = new Foo();
const allRes: number[] = [];
await Promise.all([
foo.baz(1).then((res) => allRes.push(res)),
foo.baz(1).then((res) => allRes.push(res)),
foo.baz(2).then((res) => allRes.push(res)),
foo.baz(2).then((res) => allRes.push(res)),
foo.baz(3).then((res) => allRes.push(res)),
foo.baz(3).then((res) => allRes.push(res)),
]);
expect(foo.calls).toBe(3);
expect(allRes.length).toBe(6);
allRes.sort();
expect(allRes).toEqual([3, 3, 6, 6, 9, 9]);
});
});
class Foo {
calls = 0;
@sequentialize((args) => "bar" + args[0])
bar(a: number): Promise<number> {
this.calls++;
return new Promise((res) => {
setTimeout(() => {
res(a * 2);
}, Math.random() * 100);
});
}
@sequentialize((args) => "baz" + args[0])
baz(a: number): Promise<number> {
this.calls++;
return new Promise((res) => {
setTimeout(() => {
res(a * 3);
}, Math.random() * 100);
});
}
}

View File

@@ -0,0 +1,110 @@
import { sequentialize } from "@/jslib/common/src/misc/sequentialize";
import { throttle } from "@/jslib/common/src/misc/throttle";
describe("throttle decorator", () => {
it("should call the function once at a time", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(10);
});
it("should call the function once at a time for each object", async () => {
const foo = new Foo();
const foo2 = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
promises.push(foo2.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(10);
expect(foo2.calls).toBe(10);
});
it("should call the function limit at a time", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.baz(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(10);
});
it("should call the function limit at a time for each object", async () => {
const foo = new Foo();
const foo2 = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.baz(1));
promises.push(foo2.baz(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(10);
expect(foo2.calls).toBe(10);
});
it("should work together with sequentialize", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.qux(Math.floor(i / 2) * 2));
}
await Promise.all(promises);
expect(foo.calls).toBe(5);
});
});
class Foo {
calls = 0;
inflight = 0;
@throttle(1, () => "bar")
bar(a: number) {
this.calls++;
this.inflight++;
return new Promise((res) => {
setTimeout(() => {
expect(this.inflight).toBe(1);
this.inflight--;
res(a * 2);
}, Math.random() * 10);
});
}
@throttle(5, () => "baz")
baz(a: number) {
this.calls++;
this.inflight++;
return new Promise((res) => {
setTimeout(() => {
expect(this.inflight).toBeLessThanOrEqual(5);
this.inflight--;
res(a * 3);
}, Math.random() * 10);
});
}
@sequentialize((args) => "qux" + args[0])
@throttle(1, () => "qux")
qux(a: number) {
this.calls++;
this.inflight++;
return new Promise((res) => {
setTimeout(() => {
expect(this.inflight).toBe(1);
this.inflight--;
res(a * 3);
}, Math.random() * 10);
});
}
}

View File

@@ -0,0 +1,69 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { FileUploadService } from "@/jslib/common/src/abstractions/fileUpload.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { SearchService } from "@/jslib/common/src/abstractions/search.service";
import { SettingsService } from "@/jslib/common/src/abstractions/settings.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import { Cipher } from "@/jslib/common/src/models/domain/cipher";
import { EncArrayBuffer } from "@/jslib/common/src/models/domain/encArrayBuffer";
import { EncString } from "@/jslib/common/src/models/domain/encString";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { CipherService } from "@/jslib/common/src/services/cipher.service";
const ENCRYPTED_TEXT = "This data has been encrypted";
const ENCRYPTED_BYTES = new EncArrayBuffer(Utils.fromUtf8ToArray(ENCRYPTED_TEXT).buffer);
describe("Cipher Service", () => {
let cryptoService: SubstituteOf<CryptoService>;
let stateService: SubstituteOf<StateService>;
let settingsService: SubstituteOf<SettingsService>;
let apiService: SubstituteOf<ApiService>;
let fileUploadService: SubstituteOf<FileUploadService>;
let i18nService: SubstituteOf<I18nService>;
let searchService: SubstituteOf<SearchService>;
let logService: SubstituteOf<LogService>;
let cipherService: CipherService;
beforeEach(() => {
cryptoService = Substitute.for<CryptoService>();
stateService = Substitute.for<StateService>();
settingsService = Substitute.for<SettingsService>();
apiService = Substitute.for<ApiService>();
fileUploadService = Substitute.for<FileUploadService>();
i18nService = Substitute.for<I18nService>();
searchService = Substitute.for<SearchService>();
logService = Substitute.for<LogService>();
cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES);
cryptoService.encrypt(Arg.any(), Arg.any()).resolves(new EncString(ENCRYPTED_TEXT));
cipherService = new CipherService(
cryptoService,
settingsService,
apiService,
fileUploadService,
i18nService,
() => searchService,
logService,
stateService,
);
});
it("attachments upload encrypted file contents", async () => {
const fileName = "filename";
const fileData = new Uint8Array(10).buffer;
cryptoService.getOrgKey(Arg.any()).resolves(new SymmetricCryptoKey(new Uint8Array(32).buffer));
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData);
fileUploadService
.received(1)
.uploadCipherAttachment(Arg.any(), Arg.any(), new EncString(ENCRYPTED_TEXT), ENCRYPTED_BYTES);
});
});

View File

@@ -0,0 +1,102 @@
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
const originalConsole = console;
let caughtMessage: any;
declare let console: any;
export function interceptConsole(interceptions: any): object {
console = {
log: function () {
// eslint-disable-next-line
interceptions.log = arguments;
},
warn: function () {
// eslint-disable-next-line
interceptions.warn = arguments;
},
error: function () {
// eslint-disable-next-line
interceptions.error = arguments;
},
};
return interceptions;
}
export function restoreConsole() {
console = originalConsole;
}
describe("ConsoleLogService", () => {
let logService: ConsoleLogService;
beforeEach(() => {
caughtMessage = {};
interceptConsole(caughtMessage);
logService = new ConsoleLogService(true);
});
afterAll(() => {
restoreConsole();
});
it("filters messages below the set threshold", () => {
logService = new ConsoleLogService(true, () => true);
logService.debug("debug");
logService.info("info");
logService.warning("warning");
logService.error("error");
expect(caughtMessage).toEqual({});
});
it("only writes debug messages in dev mode", () => {
logService = new ConsoleLogService(false);
logService.debug("debug message");
expect(caughtMessage.log).toBeUndefined();
});
it("writes debug/info messages to console.log", () => {
logService.debug("this is a debug message");
expect(caughtMessage).toMatchObject({
log: { "0": "this is a debug message" },
});
logService.info("this is an info message");
expect(caughtMessage).toMatchObject({
log: { "0": "this is an info message" },
});
});
it("writes warning messages to console.warn", () => {
logService.warning("this is a warning message");
expect(caughtMessage).toMatchObject({
warn: { 0: "this is a warning message" },
});
});
it("writes error messages to console.error", () => {
logService.error("this is an error message");
expect(caughtMessage).toMatchObject({
error: { 0: "this is an error message" },
});
});
it("times with output to info", async () => {
logService.time();
await new Promise((r) => setTimeout(r, 250));
const duration = logService.timeEnd();
expect(duration[0]).toBe(0);
expect(duration[1]).toBeGreaterThan(0);
expect(duration[1]).toBeLessThan(500 * 10e6);
expect(caughtMessage).toEqual(expect.arrayContaining([]));
expect(caughtMessage.log.length).toBe(1);
expect(caughtMessage.log[0]).toEqual(expect.stringMatching(/^default: \d+\.?\d*ms$/));
});
it("filters time output", async () => {
logService = new ConsoleLogService(true, () => true);
logService.time();
logService.timeEnd();
expect(caughtMessage).toEqual({});
});
});

View File

@@ -0,0 +1,84 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { Account } from "@/jslib/common/src/models/domain/account";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
const userId = "USER_ID";
describe("State Migration Service", () => {
let storageService: SubstituteOf<StorageService>;
let secureStorageService: SubstituteOf<StorageService>;
let stateFactory: SubstituteOf<StateFactory>;
let stateMigrationService: StateMigrationService;
beforeEach(() => {
storageService = Substitute.for<StorageService>();
secureStorageService = Substitute.for<StorageService>();
stateFactory = Substitute.for<StateFactory>();
stateMigrationService = new StateMigrationService(
storageService,
secureStorageService,
stateFactory,
);
});
describe("StateVersion 3 to 4 migration", async () => {
beforeEach(() => {
const globalVersion3: Partial<GlobalState> = {
stateVersion: StateVersion.Three,
};
storageService.get("global", Arg.any()).resolves(globalVersion3);
storageService.get("authenticatedAccounts", Arg.any()).resolves([userId]);
});
it("clears everBeenUnlocked", async () => {
const accountVersion3: Account = {
profile: {
apiKeyClientId: null,
convertAccountToKeyConnector: null,
email: "EMAIL",
emailVerified: true,
everBeenUnlocked: true,
hasPremiumPersonally: false,
kdfIterations: 100000,
kdfType: 0,
keyHash: "KEY_HASH",
lastSync: "LAST_SYNC",
userId: userId,
usesKeyConnector: false,
forcePasswordReset: false,
},
};
const expectedAccountVersion4: Account = {
profile: {
...accountVersion3.profile,
},
};
delete expectedAccountVersion4.profile.everBeenUnlocked;
storageService.get(userId, Arg.any()).resolves(accountVersion3);
await stateMigrationService.migrate();
storageService.received(1).save(userId, expectedAccountVersion4, Arg.any());
});
it("updates StateVersion number", async () => {
await stateMigrationService.migrate();
storageService.received(1).save(
"global",
Arg.is((globals: GlobalState) => globals.stateVersion === StateVersion.Four),
Arg.any(),
);
});
});
});

View File

@@ -0,0 +1,5 @@
import { webcrypto } from "crypto";
Object.defineProperty(window, "crypto", {
value: webcrypto,
});

View File

@@ -0,0 +1,37 @@
import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { EncString } from "@/jslib/common/src/models/domain/encString";
function newGuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
export function GetUniqueString(prefix = "") {
return prefix + "_" + newGuid();
}
export function BuildTestObject<T, K extends keyof T = keyof T>(
def: Partial<Pick<T, K>> | T,
constructor?: new () => T,
): T {
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
}
export function mockEnc(s: string): EncString {
const mock = Substitute.for<EncString>();
mock.decrypt(Arg.any(), Arg.any()).resolves(s);
return mock;
}
export function makeStaticByteArray(length: number, start = 0) {
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = start + i;
}
return arr;
}

View File

@@ -169,7 +169,7 @@ import { SendAccessView } from "../models/view/sendAccessView";
export abstract class ApiService {
postIdentityToken: (
request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest
request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest,
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
refreshIdentityToken: () => Promise<any>;
@@ -221,7 +221,7 @@ export abstract class ApiService {
postSendAccess: (
id: string,
request: SendAccessRequest,
apiUrl?: string
apiUrl?: string,
) => Promise<SendAccessResponse>;
getSends: () => Promise<ListResponse<SendResponse>>;
postSend: (request: SendRequest) => Promise<SendResponse>;
@@ -238,7 +238,7 @@ export abstract class ApiService {
getSendFileDownloadData: (
send: SendAccessView,
request: SendAccessRequest,
apiUrl?: string
apiUrl?: string,
) => Promise<SendFileDownloadDataResponse>;
renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise<SendFileUploadDataResponse>;
@@ -247,7 +247,7 @@ export abstract class ApiService {
getAttachmentData: (
cipherId: string,
attachmentId: string,
emergencyAccessId?: string
emergencyAccessId?: string,
) => Promise<AttachmentResponse>;
getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>;
postCipher: (request: CipherRequest) => Promise<CipherResponse>;
@@ -268,7 +268,7 @@ export abstract class ApiService {
postImportCiphers: (request: ImportCiphersRequest) => Promise<any>;
postImportOrganizationCiphers: (
organizationId: string,
request: ImportOrganizationCiphersRequest
request: ImportOrganizationCiphersRequest,
) => Promise<any>;
putDeleteCipher: (id: string) => Promise<any>;
putDeleteCipherAdmin: (id: string) => Promise<any>;
@@ -277,7 +277,7 @@ export abstract class ApiService {
putRestoreCipher: (id: string) => Promise<CipherResponse>;
putRestoreCipherAdmin: (id: string) => Promise<CipherResponse>;
putRestoreManyCiphers: (
request: CipherBulkRestoreRequest
request: CipherBulkRestoreRequest,
) => Promise<ListResponse<CipherResponse>>;
/**
@@ -292,7 +292,7 @@ export abstract class ApiService {
postCipherAttachmentAdminLegacy: (id: string, data: FormData) => Promise<CipherResponse>;
postCipherAttachment: (
id: string,
request: AttachmentRequest
request: AttachmentRequest,
) => Promise<AttachmentUploadDataResponse>;
deleteCipherAttachment: (id: string, attachmentId: string) => Promise<any>;
deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise<any>;
@@ -300,40 +300,40 @@ export abstract class ApiService {
id: string,
attachmentId: string,
data: FormData,
organizationId: string
organizationId: string,
) => Promise<any>;
renewAttachmentUploadUrl: (
id: string,
attachmentId: string
attachmentId: string,
) => Promise<AttachmentUploadDataResponse>;
postAttachmentFile: (id: string, attachmentId: string, data: FormData) => Promise<any>;
getCollectionDetails: (
organizationId: string,
id: string
id: string,
) => Promise<CollectionGroupDetailsResponse>;
getUserCollections: () => Promise<ListResponse<CollectionResponse>>;
getCollections: (organizationId: string) => Promise<ListResponse<CollectionResponse>>;
getCollectionUsers: (organizationId: string, id: string) => Promise<SelectionReadOnlyResponse[]>;
postCollection: (
organizationId: string,
request: CollectionRequest
request: CollectionRequest,
) => Promise<CollectionResponse>;
putCollectionUsers: (
organizationId: string,
id: string,
request: SelectionReadOnlyRequest[]
request: SelectionReadOnlyRequest[],
) => Promise<any>;
putCollection: (
organizationId: string,
id: string,
request: CollectionRequest
request: CollectionRequest,
) => Promise<CollectionResponse>;
deleteCollection: (organizationId: string, id: string) => Promise<any>;
deleteCollectionUser: (
organizationId: string,
id: string,
organizationUserId: string
organizationUserId: string,
) => Promise<any>;
getGroupDetails: (organizationId: string, id: string) => Promise<GroupDetailsResponse>;
@@ -351,82 +351,82 @@ export abstract class ApiService {
organizationId: string,
token: string,
email: string,
organizationUserId: string
organizationUserId: string,
) => Promise<ListResponse<PolicyResponse>>;
getPoliciesByInvitedUser: (
organizationId: string,
userId: string
userId: string,
) => Promise<ListResponse<PolicyResponse>>;
putPolicy: (
organizationId: string,
type: PolicyType,
request: PolicyRequest
request: PolicyRequest,
) => Promise<PolicyResponse>;
getOrganizationUser: (
organizationId: string,
id: string
id: string,
) => Promise<OrganizationUserDetailsResponse>;
getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>;
getOrganizationUsers: (
organizationId: string
organizationId: string,
) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
getOrganizationUserResetPasswordDetails: (
organizationId: string,
id: string
id: string,
) => Promise<OrganizationUserResetPasswordDetailsReponse>;
postOrganizationUserInvite: (
organizationId: string,
request: OrganizationUserInviteRequest
request: OrganizationUserInviteRequest,
) => Promise<any>;
postOrganizationUserReinvite: (organizationId: string, id: string) => Promise<any>;
postManyOrganizationUserReinvite: (
organizationId: string,
request: OrganizationUserBulkRequest
request: OrganizationUserBulkRequest,
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
postOrganizationUserAccept: (
organizationId: string,
id: string,
request: OrganizationUserAcceptRequest
request: OrganizationUserAcceptRequest,
) => Promise<any>;
postOrganizationUserConfirm: (
organizationId: string,
id: string,
request: OrganizationUserConfirmRequest
request: OrganizationUserConfirmRequest,
) => Promise<any>;
postOrganizationUsersPublicKey: (
organizationId: string,
request: OrganizationUserBulkRequest
request: OrganizationUserBulkRequest,
) => Promise<ListResponse<OrganizationUserBulkPublicKeyResponse>>;
postOrganizationUserBulkConfirm: (
organizationId: string,
request: OrganizationUserBulkConfirmRequest
request: OrganizationUserBulkConfirmRequest,
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
putOrganizationUser: (
organizationId: string,
id: string,
request: OrganizationUserUpdateRequest
request: OrganizationUserUpdateRequest,
) => Promise<any>;
putOrganizationUserGroups: (
organizationId: string,
id: string,
request: OrganizationUserUpdateGroupsRequest
request: OrganizationUserUpdateGroupsRequest,
) => Promise<any>;
putOrganizationUserResetPasswordEnrollment: (
organizationId: string,
userId: string,
request: OrganizationUserResetPasswordEnrollmentRequest
request: OrganizationUserResetPasswordEnrollmentRequest,
) => Promise<any>;
putOrganizationUserResetPassword: (
organizationId: string,
id: string,
request: OrganizationUserResetPasswordRequest
request: OrganizationUserResetPasswordRequest,
) => Promise<any>;
deleteOrganizationUser: (organizationId: string, id: string) => Promise<any>;
deleteManyOrganizationUsers: (
organizationId: string,
request: OrganizationUserBulkRequest
request: OrganizationUserBulkRequest,
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
getSync: () => Promise<SyncResponse>;
@@ -438,43 +438,43 @@ export abstract class ApiService {
getTwoFactorProviders: () => Promise<ListResponse<TwoFactorProviderResponse>>;
getTwoFactorOrganizationProviders: (
organizationId: string
organizationId: string,
) => Promise<ListResponse<TwoFactorProviderResponse>>;
getTwoFactorAuthenticator: (
request: SecretVerificationRequest
request: SecretVerificationRequest,
) => Promise<TwoFactorAuthenticatorResponse>;
getTwoFactorEmail: (request: SecretVerificationRequest) => Promise<TwoFactorEmailResponse>;
getTwoFactorDuo: (request: SecretVerificationRequest) => Promise<TwoFactorDuoResponse>;
getTwoFactorOrganizationDuo: (
organizationId: string,
request: SecretVerificationRequest
request: SecretVerificationRequest,
) => Promise<TwoFactorDuoResponse>;
getTwoFactorYubiKey: (request: SecretVerificationRequest) => Promise<TwoFactorYubiKeyResponse>;
getTwoFactorWebAuthn: (request: SecretVerificationRequest) => Promise<TwoFactorWebAuthnResponse>;
getTwoFactorWebAuthnChallenge: (request: SecretVerificationRequest) => Promise<ChallengeResponse>;
getTwoFactorRecover: (request: SecretVerificationRequest) => Promise<TwoFactorRecoverResponse>;
putTwoFactorAuthenticator: (
request: UpdateTwoFactorAuthenticatorRequest
request: UpdateTwoFactorAuthenticatorRequest,
) => Promise<TwoFactorAuthenticatorResponse>;
putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise<TwoFactorEmailResponse>;
putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise<TwoFactorDuoResponse>;
putTwoFactorOrganizationDuo: (
organizationId: string,
request: UpdateTwoFactorDuoRequest
request: UpdateTwoFactorDuoRequest,
) => Promise<TwoFactorDuoResponse>;
putTwoFactorYubiKey: (
request: UpdateTwoFactorYubioOtpRequest
request: UpdateTwoFactorYubioOtpRequest,
) => Promise<TwoFactorYubiKeyResponse>;
putTwoFactorWebAuthn: (
request: UpdateTwoFactorWebAuthnRequest
request: UpdateTwoFactorWebAuthnRequest,
) => Promise<TwoFactorWebAuthnResponse>;
deleteTwoFactorWebAuthn: (
request: UpdateTwoFactorWebAuthnDeleteRequest
request: UpdateTwoFactorWebAuthnDeleteRequest,
) => Promise<TwoFactorWebAuthnResponse>;
putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise<TwoFactorProviderResponse>;
putTwoFactorOrganizationDisable: (
organizationId: string,
request: TwoFactorProviderRequest
request: TwoFactorProviderRequest,
) => Promise<TwoFactorProviderResponse>;
postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise<any>;
postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise<any>;
@@ -496,7 +496,7 @@ export abstract class ApiService {
postEmergencyAccessTakeover: (id: string) => Promise<EmergencyAccessTakeoverResponse>;
postEmergencyAccessPassword: (
id: string,
request: EmergencyAccessPasswordRequest
request: EmergencyAccessPasswordRequest,
) => Promise<any>;
postEmergencyAccessView: (id: string) => Promise<EmergencyAccessViewResponse>;
@@ -506,13 +506,13 @@ export abstract class ApiService {
getOrganizationLicense: (id: string, installationId: string) => Promise<any>;
getOrganizationTaxInfo: (id: string) => Promise<TaxInfoResponse>;
getOrganizationAutoEnrollStatus: (
identifier: string
identifier: string,
) => Promise<OrganizationAutoEnrollStatusResponse>;
getOrganizationSso: (id: string) => Promise<OrganizationSsoResponse>;
postOrganization: (request: OrganizationCreateRequest) => Promise<OrganizationResponse>;
putOrganization: (
id: string,
request: OrganizationUpdateRequest
request: OrganizationUpdateRequest,
) => Promise<OrganizationResponse>;
putOrganizationTaxInfo: (id: string, request: OrganizationTaxInfoUpdateRequest) => Promise<any>;
postLeaveOrganization: (id: string) => Promise<any>;
@@ -520,23 +520,23 @@ export abstract class ApiService {
postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise<any>;
postOrganizationApiKey: (
id: string,
request: SecretVerificationRequest
request: SecretVerificationRequest,
) => Promise<ApiKeyResponse>;
postOrganizationRotateApiKey: (
id: string,
request: SecretVerificationRequest
request: SecretVerificationRequest,
) => Promise<ApiKeyResponse>;
postOrganizationSso: (
id: string,
request: OrganizationSsoRequest
request: OrganizationSsoRequest,
) => Promise<OrganizationSsoResponse>;
postOrganizationUpgrade: (
id: string,
request: OrganizationUpgradeRequest
request: OrganizationUpgradeRequest,
) => Promise<PaymentResponse>;
postOrganizationUpdateSubscription: (
id: string,
request: OrganizationSubscriptionUpdateRequest
request: OrganizationSubscriptionUpdateRequest,
) => Promise<void>;
postOrganizationSeat: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
postOrganizationStorage: (id: string, request: StorageRequest) => Promise<any>;
@@ -550,7 +550,7 @@ export abstract class ApiService {
getOrganizationKeys: (id: string) => Promise<OrganizationKeysResponse>;
postOrganizationKeys: (
id: string,
request: OrganizationKeysRequest
request: OrganizationKeysRequest,
) => Promise<OrganizationKeysResponse>;
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
@@ -563,46 +563,46 @@ export abstract class ApiService {
postProviderUserReinvite: (providerId: string, id: string) => Promise<any>;
postManyProviderUserReinvite: (
providerId: string,
request: ProviderUserBulkRequest
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
postProviderUserAccept: (
providerId: string,
id: string,
request: ProviderUserAcceptRequest
request: ProviderUserAcceptRequest,
) => Promise<any>;
postProviderUserConfirm: (
providerId: string,
id: string,
request: ProviderUserConfirmRequest
request: ProviderUserConfirmRequest,
) => Promise<any>;
postProviderUsersPublicKey: (
providerId: string,
request: ProviderUserBulkRequest
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkPublicKeyResponse>>;
postProviderUserBulkConfirm: (
providerId: string,
request: ProviderUserBulkConfirmRequest
request: ProviderUserBulkConfirmRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
putProviderUser: (
providerId: string,
id: string,
request: ProviderUserUpdateRequest
request: ProviderUserUpdateRequest,
) => Promise<any>;
deleteProviderUser: (organizationId: string, id: string) => Promise<any>;
deleteManyProviderUsers: (
providerId: string,
request: ProviderUserBulkRequest
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
getProviderClients: (
providerId: string
providerId: string,
) => Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
postProviderAddOrganization: (
providerId: string,
request: ProviderAddOrganizationRequest
request: ProviderAddOrganizationRequest,
) => Promise<any>;
postProviderCreateOrganization: (
providerId: string,
request: ProviderOrganizationCreateRequest
request: ProviderOrganizationCreateRequest,
) => Promise<ProviderOrganizationResponse>;
deleteProviderOrganization: (providerId: string, organizationId: string) => Promise<any>;
@@ -611,33 +611,33 @@ export abstract class ApiService {
id: string,
start: string,
end: string,
token: string
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsOrganization: (
id: string,
start: string,
end: string,
token: string
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsOrganizationUser: (
organizationId: string,
id: string,
start: string,
end: string,
token: string
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsProvider: (
id: string,
start: string,
end: string,
token: string
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsProviderUser: (
providerId: string,
id: string,
start: string,
end: string,
token: string
token: string,
) => Promise<ListResponse<EventResponse>>;
postEventsCollect: (request: EventRequest[]) => Promise<any>;
@@ -659,21 +659,21 @@ export abstract class ApiService {
postCreateSponsorship: (
sponsorshipOrgId: string,
request: OrganizationSponsorshipCreateRequest
request: OrganizationSponsorshipCreateRequest,
) => Promise<void>;
deleteRevokeSponsorship: (sponsoringOrganizationId: string) => Promise<void>;
deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise<void>;
postPreValidateSponsorshipToken: (sponsorshipToken: string) => Promise<boolean>;
postRedeemSponsorship: (
sponsorshipToken: string,
request: OrganizationSponsorshipRedeemRequest
request: OrganizationSponsorshipRedeemRequest,
) => Promise<void>;
postResendSponsorshipOffer: (sponsoringOrgId: string) => Promise<void>;
getUserKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
postUserKeyToKeyConnector: (
keyConnectorUrl: string,
request: KeyConnectorUserKeyRequest
request: KeyConnectorUserKeyRequest,
) => Promise<void>;
getKeyConnectorAlive: (keyConnectorUrl: string) => Promise<void>;
}

View File

@@ -11,11 +11,11 @@ export abstract class AuthService {
masterPasswordHash: string;
email: string;
logIn: (
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
) => Promise<AuthResult>;
logInTwoFactor: (
twoFactor: TokenRequestTwoFactor,
captchaResponse: string
captchaResponse: string,
) => Promise<AuthResult>;
logOut: (callback: () => void) => void;
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;

View File

@@ -12,7 +12,7 @@ export abstract class CipherService {
encrypt: (
model: CipherView,
key?: SymmetricCryptoKey,
originalCipher?: Cipher
originalCipher?: Cipher,
) => Promise<Cipher>;
encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise<Field[]>;
encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise<Field>;
@@ -23,7 +23,7 @@ export abstract class CipherService {
getAllDecryptedForUrl: (
url: string,
includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchType
defaultMatch?: UriMatchType,
) => Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
@@ -37,23 +37,23 @@ export abstract class CipherService {
shareWithServer: (
cipher: CipherView,
organizationId: string,
collectionIds: string[]
collectionIds: string[],
) => Promise<any>;
shareManyWithServer: (
ciphers: CipherView[],
organizationId: string,
collectionIds: string[]
collectionIds: string[],
) => Promise<any>;
saveAttachmentWithServer: (
cipher: Cipher,
unencryptedFile: any,
admin?: boolean
admin?: boolean,
) => Promise<Cipher>;
saveAttachmentRawWithServer: (
cipher: Cipher,
filename: string,
data: ArrayBuffer,
admin?: boolean
admin?: boolean,
) => Promise<Cipher>;
saveCollectionsWithServer: (cipher: Cipher) => Promise<any>;
upsert: (cipher: CipherData | CipherData[]) => Promise<any>;
@@ -72,7 +72,7 @@ export abstract class CipherService {
softDeleteWithServer: (id: string) => Promise<any>;
softDeleteManyWithServer: (ids: string[]) => Promise<any>;
restore: (
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[]
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) => Promise<any>;
restoreWithServer: (id: string) => Promise<any>;
restoreManyWithServer: (ids: string[]) => Promise<any>;

View File

@@ -15,7 +15,7 @@ export abstract class CryptoService {
setEncPrivateKey: (encPrivateKey: string) => Promise<void>;
setOrgKeys: (
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[]
providerOrgs: ProfileProviderOrganizationResponse[],
) => Promise<void>;
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
@@ -46,14 +46,14 @@ export abstract class CryptoService {
password: string,
salt: string,
kdf: KdfType,
kdfIterations: number
kdfIterations: number,
) => Promise<SymmetricCryptoKey>;
makeKeyFromPin: (
pin: string,
salt: string,
kdf: KdfType,
kdfIterations: number,
protectedKeyCs?: EncString
protectedKeyCs?: EncString,
) => Promise<SymmetricCryptoKey>;
makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>;
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
@@ -61,18 +61,18 @@ export abstract class CryptoService {
pin: string,
salt: string,
kdf: KdfType,
kdfIterations: number
kdfIterations: number,
) => Promise<SymmetricCryptoKey>;
makeSendKey: (keyMaterial: ArrayBuffer) => Promise<SymmetricCryptoKey>;
hashPassword: (
password: string,
key: SymmetricCryptoKey,
hashPurpose?: HashPurpose
hashPurpose?: HashPurpose,
) => Promise<string>;
makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>;
remakeEncKey: (
key: SymmetricCryptoKey,
encKey?: SymmetricCryptoKey
encKey?: SymmetricCryptoKey,
) => Promise<[SymmetricCryptoKey, EncString]>;
encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncString>;
encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;

View File

@@ -6,35 +6,35 @@ export abstract class CryptoFunctionService {
password: string | ArrayBuffer,
salt: string | ArrayBuffer,
algorithm: "sha256" | "sha512",
iterations: number
iterations: number,
) => Promise<ArrayBuffer>;
hkdf: (
ikm: ArrayBuffer,
salt: string | ArrayBuffer,
info: string | ArrayBuffer,
outputByteSize: number,
algorithm: "sha256" | "sha512"
algorithm: "sha256" | "sha512",
) => Promise<ArrayBuffer>;
hkdfExpand: (
prk: ArrayBuffer,
info: string | ArrayBuffer,
outputByteSize: number,
algorithm: "sha256" | "sha512"
algorithm: "sha256" | "sha512",
) => Promise<ArrayBuffer>;
hash: (
value: string | ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512" | "md5"
algorithm: "sha1" | "sha256" | "sha512" | "md5",
) => Promise<ArrayBuffer>;
hmac: (
value: ArrayBuffer,
key: ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512"
algorithm: "sha1" | "sha256" | "sha512",
) => Promise<ArrayBuffer>;
compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise<boolean>;
hmacFast: (
value: ArrayBuffer | string,
key: ArrayBuffer | string,
algorithm: "sha1" | "sha256" | "sha512"
algorithm: "sha1" | "sha256" | "sha512",
) => Promise<ArrayBuffer | string>;
compareFast: (a: ArrayBuffer | string, b: ArrayBuffer | string) => Promise<boolean>;
aesEncrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
@@ -42,19 +42,19 @@ export abstract class CryptoFunctionService {
data: string,
iv: string,
mac: string,
key: SymmetricCryptoKey
key: SymmetricCryptoKey,
) => DecryptParameters<ArrayBuffer | string>;
aesDecryptFast: (parameters: DecryptParameters<ArrayBuffer | string>) => Promise<string>;
aesDecrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
rsaEncrypt: (
data: ArrayBuffer,
publicKey: ArrayBuffer,
algorithm: "sha1" | "sha256"
algorithm: "sha1" | "sha256",
) => Promise<ArrayBuffer>;
rsaDecrypt: (
data: ArrayBuffer,
privateKey: ArrayBuffer,
algorithm: "sha1" | "sha256"
algorithm: "sha1" | "sha256",
) => Promise<ArrayBuffer>;
rsaExtractPublicKey: (privateKey: ArrayBuffer) => Promise<ArrayBuffer>;
rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[ArrayBuffer, ArrayBuffer]>;

View File

@@ -1,11 +0,0 @@
import { EventView } from "../models/view/eventView";
export type ExportFormat = "csv" | "json" | "encrypted_json";
export abstract class ExportService {
getExport: (format?: ExportFormat, organizationId?: string) => Promise<string>;
getPasswordProtectedExport: (password: string, organizationId?: string) => Promise<string>;
getOrganizationExport: (organizationId: string, format?: ExportFormat) => Promise<string>;
getEventExport: (events: EventView[]) => Promise<string>;
getFileName: (prefix?: string, extension?: string) => string;
}

View File

@@ -7,12 +7,12 @@ export abstract class FileUploadService {
uploadSendFile: (
uploadData: SendFileUploadDataResponse,
fileName: EncString,
encryptedFileData: EncArrayBuffer
encryptedFileData: EncArrayBuffer,
) => Promise<any>;
uploadCipherAttachment: (
admin: boolean,
uploadData: AttachmentUploadDataResponse,
fileName: EncString,
encryptedFileData: EncArrayBuffer
encryptedFileData: EncArrayBuffer,
) => Promise<any>;
}

View File

@@ -1,19 +0,0 @@
import { ImportOption, ImportType } from "../enums/importOptions";
import { ImportError } from "../importers/importError";
import { Importer } from "../importers/importer";
export abstract class ImportService {
featuredImportOptions: readonly ImportOption[];
regularImportOptions: readonly ImportOption[];
getImportOptions: () => ImportOption[];
import: (
importer: Importer,
fileContents: string,
organizationId?: string
) => Promise<ImportError>;
getImporter: (
format: ImportType | "bitwardenpasswordprotected",
organizationId: string,
password?: string
) => Importer;
}

View File

@@ -9,7 +9,7 @@ export abstract class KeyConnectorService {
userNeedsMigration: () => Promise<boolean>;
convertNewSsoUserToKeyConnector: (
tokenResponse: IdentityTokenResponse,
orgId: string
orgId: string,
) => Promise<void>;
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
setConvertAccountRequired: (status: boolean) => Promise<void>;

View File

@@ -8,7 +8,7 @@ export abstract class PasswordGenerationService {
generatePassphrase: (options: any) => Promise<string>;
getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>;
enforcePasswordGeneratorPoliciesOnOptions: (
options: any
options: any,
) => Promise<[any, PasswordGeneratorPolicyOptions]>;
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
saveOptions: (options: any) => Promise<any>;

View File

@@ -27,7 +27,7 @@ export abstract class PlatformUtilsService {
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: ToastOptions
options?: ToastOptions,
) => void;
showDialog: (
body: string,
@@ -35,7 +35,7 @@ export abstract class PlatformUtilsService {
confirmText?: string,
cancelText?: string,
type?: string,
bodyIsHtml?: boolean
bodyIsHtml?: boolean,
) => Promise<boolean>;
isDev: () => boolean;
isSelfHost: () => boolean;
@@ -45,7 +45,7 @@ export abstract class PlatformUtilsService {
authenticateBiometric: () => Promise<boolean>;
getDefaultSystemTheme: () => Promise<ThemeType.Light | ThemeType.Dark>;
onDefaultSystemThemeChange: (
callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown
callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown,
) => unknown;
getEffectiveTheme: () => Promise<ThemeType>;
supportsSecureStorage: () => boolean;

View File

@@ -17,16 +17,16 @@ export abstract class PolicyService {
evaluateMasterPassword: (
passwordStrength: number,
newPassword: string,
enforcedPolicyOptions?: MasterPasswordPolicyOptions
enforcedPolicyOptions?: MasterPasswordPolicyOptions,
) => boolean;
getResetPasswordPolicyOptions: (
policies: Policy[],
orgId: string
orgId: string,
) => [ResetPasswordPolicyOptions, boolean];
mapPoliciesFromToken: (policiesResponse: ListResponse<PolicyResponse>) => Policy[];
policyAppliesToUser: (
policyType: PolicyType,
policyFilter?: (policy: Policy) => boolean,
userId?: string
userId?: string,
) => Promise<boolean>;
}

View File

@@ -9,7 +9,7 @@ export abstract class SearchService {
searchCiphers: (
query: string,
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
ciphers?: CipherView[]
ciphers?: CipherView[],
) => Promise<CipherView[]>;
searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[];
searchSends: (sends: SendView[], query: string) => SendView[];

View File

@@ -10,7 +10,7 @@ export abstract class SendService {
model: SendView,
file: File | ArrayBuffer,
password: string,
key?: SymmetricCryptoKey
key?: SymmetricCryptoKey,
) => Promise<[Send, EncArrayBuffer]>;
get: (id: string) => Promise<Send>;
getAll: () => Promise<Send[]>;

View File

@@ -82,23 +82,23 @@ export abstract class StateService<T extends Account = Account> {
getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
setDecryptedCryptoSymmetricKey: (
value: SymmetricCryptoKey,
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getDecryptedFolders: (options?: StorageOptions) => Promise<FolderView[]>;
setDecryptedFolders: (value: FolderView[], options?: StorageOptions) => Promise<void>;
getDecryptedOrganizationKeys: (
options?: StorageOptions
options?: StorageOptions,
) => Promise<Map<string, SymmetricCryptoKey>>;
setDecryptedOrganizationKeys: (
value: Map<string, SymmetricCryptoKey>,
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getDecryptedPasswordGenerationHistory: (
options?: StorageOptions
options?: StorageOptions,
) => Promise<GeneratedPasswordHistory[]>;
setDecryptedPasswordGenerationHistory: (
value: GeneratedPasswordHistory[],
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
@@ -109,7 +109,7 @@ export abstract class StateService<T extends Account = Account> {
getDecryptedProviderKeys: (options?: StorageOptions) => Promise<Map<string, SymmetricCryptoKey>>;
setDecryptedProviderKeys: (
value: Map<string, SymmetricCryptoKey>,
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getDecryptedSends: (options?: StorageOptions) => Promise<SendView[]>;
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
@@ -126,7 +126,7 @@ export abstract class StateService<T extends Account = Account> {
getDisableChangedPasswordNotification: (options?: StorageOptions) => Promise<boolean>;
setDisableChangedPasswordNotification: (
value: boolean,
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
@@ -153,7 +153,7 @@ export abstract class StateService<T extends Account = Account> {
getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise<boolean>;
setEnableBrowserIntegrationFingerprint: (
value: boolean,
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
@@ -170,38 +170,38 @@ export abstract class StateService<T extends Account = Account> {
getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>;
setEncryptedCiphers: (
value: { [id: string]: CipherData },
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getEncryptedCollections: (options?: StorageOptions) => Promise<{ [id: string]: CollectionData }>;
setEncryptedCollections: (
value: { [id: string]: CollectionData },
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
getEncryptedFolders: (options?: StorageOptions) => Promise<{ [id: string]: FolderData }>;
setEncryptedFolders: (
value: { [id: string]: FolderData },
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise<any>;
setEncryptedOrganizationKeys: (
value: Map<string, SymmetricCryptoKey>,
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getEncryptedPasswordGenerationHistory: (
options?: StorageOptions
options?: StorageOptions,
) => Promise<GeneratedPasswordHistory[]>;
setEncryptedPasswordGenerationHistory: (
value: GeneratedPasswordHistory[],
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
getEncryptedPolicies: (options?: StorageOptions) => Promise<{ [id: string]: PolicyData }>;
setEncryptedPolicies: (
value: { [id: string]: PolicyData },
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getEncryptedPrivateKey: (options?: StorageOptions) => Promise<string>;
setEncryptedPrivateKey: (value: string, options?: StorageOptions) => Promise<void>;
@@ -210,9 +210,6 @@ export abstract class StateService<T extends Account = Account> {
getEncryptedSends: (options?: StorageOptions) => Promise<{ [id: string]: SendData }>;
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
getEntityId: (options?: StorageOptions) => Promise<string>;
setEntityId: (value: string, options?: StorageOptions) => Promise<void>;
getEntityType: (options?: StorageOptions) => Promise<any>;
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<any>;
@@ -261,7 +258,7 @@ export abstract class StateService<T extends Account = Account> {
getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>;
setOrganizations: (
value: { [id: string]: OrganizationData },
options?: StorageOptions
options?: StorageOptions,
) => Promise<void>;
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<any>;
setPasswordGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;

View File

@@ -4,7 +4,7 @@ export abstract class TokenService {
setTokens: (
accessToken: string,
refreshToken: string,
clientIdClientSecret: [string, string]
clientIdClientSecret: [string, string],
) => Promise<any>;
setToken: (token: string) => Promise<any>;
getToken: () => Promise<string>;

View File

@@ -5,7 +5,7 @@ export abstract class UserVerificationService {
buildRequest: <T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean
alreadyHashed?: boolean,
) => Promise<T>;
verifyUser: (verification: Verification) => Promise<boolean>;
requestOTP: () => Promise<void>;

View File

@@ -1,74 +0,0 @@
export interface ImportOption {
id: string;
name: string;
}
export const featuredImportOptions = [
{ id: "bitwardenjson", name: "Bitwarden (json)" },
{ id: "bitwardencsv", name: "Bitwarden (csv)" },
{ id: "chromecsv", name: "Chrome (csv)" },
{ id: "dashlanecsv", name: "Dashlane (csv)" },
{ id: "firefoxcsv", name: "Firefox (csv)" },
{ id: "keepass2xml", name: "KeePass 2 (xml)" },
{ id: "lastpasscsv", name: "LastPass (csv)" },
{ id: "safaricsv", name: "Safari and macOS (csv)" },
{ id: "1password1pux", name: "1Password (1pux)" },
] as const;
export const regularImportOptions = [
{ id: "keepassxcsv", name: "KeePassX (csv)" },
{ id: "1password1pif", name: "1Password (1pif)" },
{ id: "1passwordwincsv", name: "1Password 6 and 7 Windows (csv)" },
{ id: "1passwordmaccsv", name: "1Password 6 and 7 Mac (csv)" },
{ id: "dashlanejson", name: "Dashlane (json)" },
{ id: "roboformcsv", name: "RoboForm (csv)" },
{ id: "keepercsv", name: "Keeper (csv)" },
// Temporarily remove this option for the Feb release
// { id: "keeperjson", name: "Keeper (json)" },
{ id: "enpasscsv", name: "Enpass (csv)" },
{ id: "enpassjson", name: "Enpass (json)" },
{ id: "safeincloudxml", name: "SafeInCloud (xml)" },
{ id: "pwsafexml", name: "Password Safe (xml)" },
{ id: "stickypasswordxml", name: "Sticky Password (xml)" },
{ id: "msecurecsv", name: "mSecure (csv)" },
{ id: "truekeycsv", name: "True Key (csv)" },
{ id: "passwordbossjson", name: "Password Boss (json)" },
{ id: "zohovaultcsv", name: "Zoho Vault (csv)" },
{ id: "splashidcsv", name: "SplashID (csv)" },
{ id: "passworddragonxml", name: "Password Dragon (xml)" },
{ id: "padlockcsv", name: "Padlock (csv)" },
{ id: "passboltcsv", name: "Passbolt (csv)" },
{ id: "clipperzhtml", name: "Clipperz (html)" },
{ id: "aviracsv", name: "Avira (csv)" },
{ id: "saferpasscsv", name: "SaferPass (csv)" },
{ id: "upmcsv", name: "Universal Password Manager (csv)" },
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
{ id: "meldiumcsv", name: "Meldium (csv)" },
{ id: "passkeepcsv", name: "PassKeep (csv)" },
{ id: "operacsv", name: "Opera (csv)" },
{ id: "vivaldicsv", name: "Vivaldi (csv)" },
{ id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" },
{ id: "blurcsv", name: "Blur (csv)" },
{ id: "passwordagentcsv", name: "Password Agent (csv)" },
{ id: "passpackcsv", name: "Passpack (csv)" },
{ id: "passmanjson", name: "Passman (json)" },
{ id: "avastcsv", name: "Avast Passwords (csv)" },
{ id: "avastjson", name: "Avast Passwords (json)" },
{ id: "fsecurefsk", name: "F-Secure KEY (fsk)" },
{ id: "kasperskytxt", name: "Kaspersky Password Manager (txt)" },
{ id: "remembearcsv", name: "RememBear (csv)" },
{ id: "passwordwallettxt", name: "PasswordWallet (txt)" },
{ id: "mykicsv", name: "Myki (csv)" },
{ id: "securesafecsv", name: "SecureSafe (csv)" },
{ id: "logmeoncecsv", name: "LogMeOnce (csv)" },
{ id: "blackberrycsv", name: "BlackBerry Password Keeper (csv)" },
{ id: "buttercupcsv", name: "Buttercup (csv)" },
{ id: "codebookcsv", name: "Codebook (csv)" },
{ id: "encryptrcsv", name: "Encryptr (csv)" },
{ id: "yoticsv", name: "Yoti (csv)" },
{ id: "nordpasscsv", name: "Nordpass (csv)" },
] as const;
export type ImportType =
| (typeof featuredImportOptions)[number]["id"]
| (typeof regularImportOptions)[number]["id"];

View File

@@ -6,14 +6,14 @@ import { GlobalStateFactory } from "./globalStateFactory";
export class StateFactory<
TGlobal extends GlobalState = GlobalState,
TAccount extends Account = Account
TAccount extends Account = Account,
> {
private globalStateFactory: GlobalStateFactory<TGlobal>;
private accountFactory: AccountFactory<TAccount>;
constructor(
globalStateConstructor: new (init: Partial<TGlobal>) => TGlobal,
accountConstructor: new (init: Partial<TAccount>) => TAccount
accountConstructor: new (init: Partial<TAccount>) => TAccount,
) {
this.globalStateFactory = new GlobalStateFactory(globalStateConstructor);
this.accountFactory = new AccountFactory(accountConstructor);

View File

@@ -1,59 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class AscendoCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, false);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
if (value.length < 2) {
return;
}
const cipher = this.initLoginCipher();
cipher.notes = this.getValueOrDefault(value[value.length - 1]);
cipher.name = this.getValueOrDefault(value[0], "--");
if (value.length > 2 && value.length % 2 === 0) {
for (let i = 0; i < value.length - 2; i += 2) {
const val: string = value[i + 2];
const field: string = value[i + 1];
if (this.isNullOrWhitespace(val) || this.isNullOrWhitespace(field)) {
continue;
}
const fieldLower = field.toLowerCase();
if (cipher.login.password == null && this.passwordFieldNames.indexOf(fieldLower) > -1) {
cipher.login.password = this.getValueOrDefault(val);
} else if (
cipher.login.username == null &&
this.usernameFieldNames.indexOf(fieldLower) > -1
) {
cipher.login.username = this.getValueOrDefault(val);
} else if (
(cipher.login.uris == null || cipher.login.uris.length === 0) &&
this.uriFieldNames.indexOf(fieldLower) > -1
) {
cipher.login.uris = this.makeUriArray(val);
} else {
this.processKvp(cipher, field, val);
}
}
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,28 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class AvastCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(value.name);
cipher.login.uris = this.makeUriArray(value.web);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.username = this.getValueOrDefault(value.login);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,68 +0,0 @@
import { CipherType } from "../enums/cipherType";
import { SecureNoteType } from "../enums/secureNoteType";
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class AvastJsonImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = JSON.parse(data);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
if (results.logins != null) {
results.logins.forEach((value: any) => {
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(value.custName);
cipher.notes = this.getValueOrDefault(value.note);
cipher.login.uris = this.makeUriArray(value.url);
cipher.login.password = this.getValueOrDefault(value.pwd);
cipher.login.username = this.getValueOrDefault(value.loginName);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
}
if (results.notes != null) {
results.notes.forEach((value: any) => {
const cipher = this.initLoginCipher();
cipher.type = CipherType.SecureNote;
cipher.secureNote.type = SecureNoteType.Generic;
cipher.name = this.getValueOrDefault(value.label);
cipher.notes = this.getValueOrDefault(value.text);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
}
if (results.cards != null) {
results.cards.forEach((value: any) => {
const cipher = this.initLoginCipher();
cipher.type = CipherType.Card;
cipher.name = this.getValueOrDefault(value.custName);
cipher.notes = this.getValueOrDefault(value.note);
cipher.card.cardholderName = this.getValueOrDefault(value.holderName);
cipher.card.number = this.getValueOrDefault(value.cardNumber);
cipher.card.code = this.getValueOrDefault(value.cvv);
cipher.card.brand = this.getCardBrand(cipher.card.number);
if (value.expirationDate != null) {
if (value.expirationDate.month != null) {
cipher.card.expMonth = value.expirationDate.month + "";
}
if (value.expirationDate.year != null) {
cipher.card.expYear = value.expirationDate.year + "";
}
}
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
}
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,41 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class AviraCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(
value.name,
this.getValueOrDefault(this.nameFromUrl(value.website), "--")
);
cipher.login.uris = this.makeUriArray(value.website);
cipher.login.password = this.getValueOrDefault(value.password);
if (
this.isNullOrWhitespace(value.username) &&
!this.isNullOrWhitespace(value.secondary_username)
) {
cipher.login.username = value.secondary_username;
} else {
cipher.login.username = this.getValueOrDefault(value.username);
cipher.notes = this.getValueOrDefault(value.secondary_username);
}
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,466 +0,0 @@
import * as papa from "papaparse";
import { LogService } from "../abstractions/log.service";
import { CipherType } from "../enums/cipherType";
import { FieldType } from "../enums/fieldType";
import { SecureNoteType } from "../enums/secureNoteType";
import { Utils } from "../misc/utils";
import { ImportResult } from "../models/domain/importResult";
import { CipherView } from "../models/view/cipherView";
import { CollectionView } from "../models/view/collectionView";
import { FieldView } from "../models/view/fieldView";
import { FolderView } from "../models/view/folderView";
import { LoginUriView } from "../models/view/loginUriView";
import { LoginView } from "../models/view/loginView";
import { SecureNoteView } from "../models/view/secureNoteView";
import { ConsoleLogService } from "../services/consoleLog.service";
export abstract class BaseImporter {
organizationId: string = null;
protected logService: LogService = new ConsoleLogService(false);
protected newLineRegex = /(?:\r\n|\r|\n)/;
protected passwordFieldNames = [
"password",
"pass word",
"passphrase",
"pass phrase",
"pass",
"code",
"code word",
"codeword",
"secret",
"secret word",
"personpwd",
"key",
"keyword",
"key word",
"keyphrase",
"key phrase",
"form_pw",
"wppassword",
"pin",
"pwd",
"pw",
"pword",
"passwd",
"p",
"serial",
"serial#",
"license key",
"reg #",
// Non-English names
"passwort",
];
protected usernameFieldNames = [
"user",
"name",
"user name",
"username",
"login name",
"email",
"e-mail",
"id",
"userid",
"user id",
"login",
"form_loginname",
"wpname",
"mail",
"loginid",
"login id",
"log",
"personlogin",
"first name",
"last name",
"card#",
"account #",
"member",
"member #",
// Non-English names
"nom",
"benutzername",
];
protected notesFieldNames = [
"note",
"notes",
"comment",
"comments",
"memo",
"description",
"free form",
"freeform",
"free text",
"freetext",
"free",
// Non-English names
"kommentar",
];
protected uriFieldNames: string[] = [
"url",
"hyper link",
"hyperlink",
"link",
"host",
"hostname",
"host name",
"server",
"address",
"hyper ref",
"href",
"web",
"website",
"web site",
"site",
"web-site",
"uri",
// Non-English names
"ort",
"adresse",
];
protected parseCsvOptions = {
encoding: "UTF-8",
skipEmptyLines: false,
};
protected get organization() {
return this.organizationId != null;
}
protected parseXml(data: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(data, "application/xml");
return doc != null && doc.querySelector("parsererror") == null ? doc : null;
}
protected parseCsv(data: string, header: boolean, options: any = {}): any[] {
const parseOptions: papa.ParseConfig<string> = Object.assign(
{ header: header },
this.parseCsvOptions,
options
);
data = this.splitNewLine(data).join("\n").trim();
const result = papa.parse(data, parseOptions);
if (result.errors != null && result.errors.length > 0) {
result.errors.forEach((e) => {
if (e.row != null) {
this.logService.warning("Error parsing row " + e.row + ": " + e.message);
}
});
}
return result.data && result.data.length > 0 ? result.data : null;
}
protected parseSingleRowCsv(rowData: string) {
if (this.isNullOrWhitespace(rowData)) {
return null;
}
const parsedRow = this.parseCsv(rowData, false);
if (parsedRow != null && parsedRow.length > 0 && parsedRow[0].length > 0) {
return parsedRow[0];
}
return null;
}
protected makeUriArray(uri: string | string[]): LoginUriView[] {
if (uri == null) {
return null;
}
if (typeof uri === "string") {
const loginUri = new LoginUriView();
loginUri.uri = this.fixUri(uri);
if (this.isNullOrWhitespace(loginUri.uri)) {
return null;
}
loginUri.match = null;
return [loginUri];
}
if (uri.length > 0) {
const returnArr: LoginUriView[] = [];
uri.forEach((u) => {
const loginUri = new LoginUriView();
loginUri.uri = this.fixUri(u);
if (this.isNullOrWhitespace(loginUri.uri)) {
return;
}
loginUri.match = null;
returnArr.push(loginUri);
});
return returnArr.length === 0 ? null : returnArr;
}
return null;
}
protected fixUri(uri: string) {
if (uri == null) {
return null;
}
uri = uri.trim();
if (uri.indexOf("://") === -1 && uri.indexOf(".") >= 0) {
uri = "http://" + uri;
}
if (uri.length > 1000) {
return uri.substring(0, 1000);
}
return uri;
}
protected nameFromUrl(url: string) {
const hostname = Utils.getHostname(url);
if (this.isNullOrWhitespace(hostname)) {
return null;
}
return hostname.startsWith("www.") ? hostname.replace("www.", "") : hostname;
}
protected isNullOrWhitespace(str: string): boolean {
return Utils.isNullOrWhitespace(str);
}
protected getValueOrDefault(str: string, defaultValue: string = null): string {
if (this.isNullOrWhitespace(str)) {
return defaultValue;
}
return str;
}
protected splitNewLine(str: string): string[] {
return str.split(this.newLineRegex);
}
// ref https://stackoverflow.com/a/5911300
protected getCardBrand(cardNum: string) {
if (this.isNullOrWhitespace(cardNum)) {
return null;
}
// Visa
let re = new RegExp("^4");
if (cardNum.match(re) != null) {
return "Visa";
}
// Mastercard
// Updated for Mastercard 2017 BINs expansion
if (
/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test(
cardNum
)
) {
return "Mastercard";
}
// AMEX
re = new RegExp("^3[47]");
if (cardNum.match(re) != null) {
return "Amex";
}
// Discover
re = new RegExp(
"^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)"
);
if (cardNum.match(re) != null) {
return "Discover";
}
// Diners
re = new RegExp("^36");
if (cardNum.match(re) != null) {
return "Diners Club";
}
// Diners - Carte Blanche
re = new RegExp("^30[0-5]");
if (cardNum.match(re) != null) {
return "Diners Club";
}
// JCB
re = new RegExp("^35(2[89]|[3-8][0-9])");
if (cardNum.match(re) != null) {
return "JCB";
}
// Visa Electron
re = new RegExp("^(4026|417500|4508|4844|491(3|7))");
if (cardNum.match(re) != null) {
return "Visa";
}
return null;
}
protected setCardExpiration(cipher: CipherView, expiration: string): boolean {
if (!this.isNullOrWhitespace(expiration)) {
expiration = expiration.replace(/\s/g, "");
const parts = expiration.split("/");
if (parts.length === 2) {
let month: string = null;
let year: string = null;
if (parts[0].length === 1 || parts[0].length === 2) {
month = parts[0];
if (month.length === 2 && month[0] === "0") {
month = month.substr(1, 1);
}
}
if (parts[1].length === 2 || parts[1].length === 4) {
year = month.length === 2 ? "20" + parts[1] : parts[1];
}
if (month != null && year != null) {
cipher.card.expMonth = month;
cipher.card.expYear = year;
return true;
}
}
}
return false;
}
protected moveFoldersToCollections(result: ImportResult) {
result.folderRelationships.forEach((r) => result.collectionRelationships.push(r));
result.collections = result.folders.map((f) => {
const collection = new CollectionView();
collection.name = f.name;
return collection;
});
result.folderRelationships = [];
result.folders = [];
}
protected querySelectorDirectChild(parentEl: Element, query: string) {
const els = this.querySelectorAllDirectChild(parentEl, query);
return els.length === 0 ? null : els[0];
}
protected querySelectorAllDirectChild(parentEl: Element, query: string) {
return Array.from(parentEl.querySelectorAll(query)).filter((el) => el.parentNode === parentEl);
}
protected initLoginCipher() {
const cipher = new CipherView();
cipher.favorite = false;
cipher.notes = "";
cipher.fields = [];
cipher.login = new LoginView();
cipher.type = CipherType.Login;
return cipher;
}
protected cleanupCipher(cipher: CipherView) {
if (cipher == null) {
return;
}
if (cipher.type !== CipherType.Login) {
cipher.login = null;
}
if (this.isNullOrWhitespace(cipher.name)) {
cipher.name = "--";
}
if (this.isNullOrWhitespace(cipher.notes)) {
cipher.notes = null;
} else {
cipher.notes = cipher.notes.trim();
}
if (cipher.fields != null && cipher.fields.length === 0) {
cipher.fields = null;
}
}
protected processKvp(
cipher: CipherView,
key: string,
value: string,
type: FieldType = FieldType.Text
) {
if (this.isNullOrWhitespace(value)) {
return;
}
if (this.isNullOrWhitespace(key)) {
key = "";
}
if (value.length > 200 || value.trim().search(this.newLineRegex) > -1) {
if (cipher.notes == null) {
cipher.notes = "";
}
cipher.notes += key + ": " + this.splitNewLine(value).join("\n") + "\n";
} else {
if (cipher.fields == null) {
cipher.fields = [];
}
const field = new FieldView();
field.type = type;
field.name = key;
field.value = value;
cipher.fields.push(field);
}
}
protected processFolder(result: ImportResult, folderName: string) {
let folderIndex = result.folders.length;
const hasFolder = !this.isNullOrWhitespace(folderName);
let addFolder = hasFolder;
if (hasFolder) {
for (let i = 0; i < result.folders.length; i++) {
if (result.folders[i].name === folderName) {
addFolder = false;
folderIndex = i;
break;
}
}
}
if (addFolder) {
const f = new FolderView();
f.name = folderName;
result.folders.push(f);
}
if (hasFolder) {
result.folderRelationships.push([result.ciphers.length, folderIndex]);
}
}
protected convertToNoteIfNeeded(cipher: CipherView) {
if (
cipher.type === CipherType.Login &&
this.isNullOrWhitespace(cipher.login.username) &&
this.isNullOrWhitespace(cipher.login.password) &&
(cipher.login.uris == null || cipher.login.uris.length === 0)
) {
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
}
}
protected processFullName(cipher: CipherView, fullName: string) {
if (this.isNullOrWhitespace(fullName)) {
return;
}
const nameParts = fullName.split(" ");
if (nameParts.length > 0) {
cipher.identity.firstName = this.getValueOrDefault(nameParts[0]);
}
if (nameParts.length === 2) {
cipher.identity.lastName = this.getValueOrDefault(nameParts[1]);
} else if (nameParts.length >= 3) {
cipher.identity.middleName = this.getValueOrDefault(nameParts[1]);
cipher.identity.lastName = nameParts.slice(2, nameParts.length).join(" ");
}
}
}

View File

@@ -1,120 +0,0 @@
import { CipherRepromptType } from "../enums/cipherRepromptType";
import { CipherType } from "../enums/cipherType";
import { FieldType } from "../enums/fieldType";
import { SecureNoteType } from "../enums/secureNoteType";
import { ImportResult } from "../models/domain/importResult";
import { CipherView } from "../models/view/cipherView";
import { CollectionView } from "../models/view/collectionView";
import { FieldView } from "../models/view/fieldView";
import { LoginView } from "../models/view/loginView";
import { SecureNoteView } from "../models/view/secureNoteView";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class BitwardenCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
if (this.organization && !this.isNullOrWhitespace(value.collections)) {
const collections = (value.collections as string).split(",");
collections.forEach((col) => {
let addCollection = true;
let collectionIndex = result.collections.length;
for (let i = 0; i < result.collections.length; i++) {
if (result.collections[i].name === col) {
addCollection = false;
collectionIndex = i;
break;
}
}
if (addCollection) {
const collection = new CollectionView();
collection.name = col;
result.collections.push(collection);
}
result.collectionRelationships.push([result.ciphers.length, collectionIndex]);
});
} else if (!this.organization) {
this.processFolder(result, value.folder);
}
const cipher = new CipherView();
cipher.favorite =
!this.organization && this.getValueOrDefault(value.favorite, "0") !== "0" ? true : false;
cipher.type = CipherType.Login;
cipher.notes = this.getValueOrDefault(value.notes);
cipher.name = this.getValueOrDefault(value.name, "--");
try {
cipher.reprompt = parseInt(
this.getValueOrDefault(value.reprompt, CipherRepromptType.None.toString()),
10
);
} catch (e) {
// eslint-disable-next-line
console.error("Unable to parse reprompt value", e);
cipher.reprompt = CipherRepromptType.None;
}
if (!this.isNullOrWhitespace(value.fields)) {
const fields = this.splitNewLine(value.fields);
for (let i = 0; i < fields.length; i++) {
if (this.isNullOrWhitespace(fields[i])) {
continue;
}
const delimPosition = fields[i].lastIndexOf(": ");
if (delimPosition === -1) {
continue;
}
if (cipher.fields == null) {
cipher.fields = [];
}
const field = new FieldView();
field.name = fields[i].substr(0, delimPosition);
field.value = null;
field.type = FieldType.Text;
if (fields[i].length > delimPosition + 2) {
field.value = fields[i].substr(delimPosition + 2);
}
cipher.fields.push(field);
}
}
const valueType = value.type != null ? value.type.toLowerCase() : null;
switch (valueType) {
case "note":
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
break;
default: {
cipher.type = CipherType.Login;
cipher.login = new LoginView();
cipher.login.totp = this.getValueOrDefault(value.login_totp || value.totp);
cipher.login.username = this.getValueOrDefault(value.login_username || value.username);
cipher.login.password = this.getValueOrDefault(value.login_password || value.password);
const uris = this.parseSingleRowCsv(value.login_uri || value.uri);
cipher.login.uris = this.makeUriArray(uris);
break;
}
}
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,179 +0,0 @@
import { CryptoService } from "../abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
import { EncString } from "../models/domain/encString";
import { ImportResult } from "../models/domain/importResult";
import { CipherWithIds } from "../models/export/cipherWithIds";
import { CollectionWithId } from "../models/export/collectionWithId";
import { FolderWithId } from "../models/export/folderWithId";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class BitwardenJsonImporter extends BaseImporter implements Importer {
private results: any;
private result: ImportResult;
constructor(protected cryptoService: CryptoService, protected i18nService: I18nService) {
super();
}
async parse(data: string): Promise<ImportResult> {
this.result = new ImportResult();
this.results = JSON.parse(data);
if (this.results == null || this.results.items == null) {
if (this.results?.passwordProtected) {
this.result.success = false;
this.result.missingPassword = true;
this.result.errorMessage = this.i18nService.t("importPasswordRequired");
return this.result;
}
this.result.success = false;
return this.result;
}
if (this.results.encrypted) {
await this.parseEncrypted();
} else {
this.parseDecrypted();
}
return this.result;
}
private async parseEncrypted() {
if (this.results.encKeyValidation_DO_NOT_EDIT != null) {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const encKeyValidation = new EncString(this.results.encKeyValidation_DO_NOT_EDIT);
const encKeyValidationDecrypt = await this.cryptoService.decryptToUtf8(
encKeyValidation,
orgKey
);
if (encKeyValidationDecrypt === null) {
this.result.success = false;
this.result.errorMessage = this.i18nService.t("importEncKeyError");
return;
}
}
const groupingsMap = new Map<string, number>();
if (this.organization && this.results.collections != null) {
for (const c of this.results.collections as CollectionWithId[]) {
const collection = CollectionWithId.toDomain(c);
if (collection != null) {
collection.id = null;
collection.organizationId = this.organizationId;
const view = await collection.decrypt();
groupingsMap.set(c.id, this.result.collections.length);
this.result.collections.push(view);
}
}
} else if (!this.organization && this.results.folders != null) {
for (const f of this.results.folders as FolderWithId[]) {
const folder = FolderWithId.toDomain(f);
if (folder != null) {
folder.id = null;
const view = await folder.decrypt();
groupingsMap.set(f.id, this.result.folders.length);
this.result.folders.push(view);
}
}
}
for (const c of this.results.items as CipherWithIds[]) {
const cipher = CipherWithIds.toDomain(c);
// reset ids incase they were set for some reason
cipher.id = null;
cipher.folderId = null;
cipher.organizationId = this.organizationId;
cipher.collectionIds = null;
// make sure password history is limited
if (cipher.passwordHistory != null && cipher.passwordHistory.length > 5) {
cipher.passwordHistory = cipher.passwordHistory.slice(0, 5);
}
if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) {
this.result.folderRelationships.push([
this.result.ciphers.length,
groupingsMap.get(c.folderId),
]);
} else if (this.organization && c.collectionIds != null) {
c.collectionIds.forEach((cId) => {
if (groupingsMap.has(cId)) {
this.result.collectionRelationships.push([
this.result.ciphers.length,
groupingsMap.get(cId),
]);
}
});
}
const view = await cipher.decrypt();
this.cleanupCipher(view);
this.result.ciphers.push(view);
}
this.result.success = true;
}
private parseDecrypted() {
const groupingsMap = new Map<string, number>();
if (this.organization && this.results.collections != null) {
this.results.collections.forEach((c: CollectionWithId) => {
const collection = CollectionWithId.toView(c);
if (collection != null) {
collection.id = null;
collection.organizationId = null;
groupingsMap.set(c.id, this.result.collections.length);
this.result.collections.push(collection);
}
});
} else if (!this.organization && this.results.folders != null) {
this.results.folders.forEach((f: FolderWithId) => {
const folder = FolderWithId.toView(f);
if (folder != null) {
folder.id = null;
groupingsMap.set(f.id, this.result.folders.length);
this.result.folders.push(folder);
}
});
}
this.results.items.forEach((c: CipherWithIds) => {
const cipher = CipherWithIds.toView(c);
// reset ids incase they were set for some reason
cipher.id = null;
cipher.folderId = null;
cipher.organizationId = null;
cipher.collectionIds = null;
// make sure password history is limited
if (cipher.passwordHistory != null && cipher.passwordHistory.length > 5) {
cipher.passwordHistory = cipher.passwordHistory.slice(0, 5);
}
if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) {
this.result.folderRelationships.push([
this.result.ciphers.length,
groupingsMap.get(c.folderId),
]);
} else if (this.organization && c.collectionIds != null) {
c.collectionIds.forEach((cId) => {
if (groupingsMap.has(cId)) {
this.result.collectionRelationships.push([
this.result.ciphers.length,
groupingsMap.get(cId),
]);
}
});
}
this.cleanupCipher(cipher);
this.result.ciphers.push(cipher);
});
this.result.success = true;
}
}

View File

@@ -1,81 +0,0 @@
import { CryptoService } from "../abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
import { KdfType } from "../enums/kdfType";
import { EncString } from "../models/domain/encString";
import { ImportResult } from "../models/domain/importResult";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { BitwardenJsonImporter } from "./bitwardenJsonImporter";
import { Importer } from "./importer";
interface BitwardenPasswordProtectedFileFormat {
encrypted: boolean;
passwordProtected: boolean;
salt: string;
kdfIterations: number;
kdfType: number;
encKeyValidation_DO_NOT_EDIT: string;
data: string;
}
export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer {
private key: SymmetricCryptoKey;
constructor(cryptoService: CryptoService, i18nService: I18nService, private password: string) {
super(cryptoService, i18nService);
}
async parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const parsedData = JSON.parse(data);
if (this.cannotParseFile(parsedData)) {
result.success = false;
return result;
}
if (!(await this.checkPassword(parsedData))) {
result.success = false;
result.errorMessage = this.i18nService.t("importEncKeyError");
return result;
}
const encData = new EncString(parsedData.data);
const clearTextData = await this.cryptoService.decryptToUtf8(encData, this.key);
return await super.parse(clearTextData);
}
private async checkPassword(jdoc: BitwardenPasswordProtectedFileFormat): Promise<boolean> {
this.key = await this.cryptoService.makePinKey(
this.password,
jdoc.salt,
KdfType.PBKDF2_SHA256,
jdoc.kdfIterations
);
const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT);
const encKeyValidationDecrypt = await this.cryptoService.decryptToUtf8(
encKeyValidation,
this.key
);
if (encKeyValidationDecrypt === null) {
return false;
}
return true;
}
private cannotParseFile(jdoc: BitwardenPasswordProtectedFileFormat): boolean {
return (
!jdoc ||
!jdoc.encrypted ||
!jdoc.passwordProtected ||
!jdoc.salt ||
!jdoc.kdfIterations ||
typeof jdoc.kdfIterations !== "number" ||
jdoc.kdfType == null ||
KdfType[jdoc.kdfType] == null ||
!jdoc.encKeyValidation_DO_NOT_EDIT ||
!jdoc.data
);
}
}

View File

@@ -1,36 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class BlackBerryCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
if (value.grouping === "list") {
return;
}
const cipher = this.initLoginCipher();
cipher.favorite = value.fav === "1";
cipher.name = this.getValueOrDefault(value.name);
cipher.notes = this.getValueOrDefault(value.extra);
if (value.grouping !== "note") {
cipher.login.uris = this.makeUriArray(value.url);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.username = this.getValueOrDefault(value.username);
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,41 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class BlurCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
if (value.label === "null") {
value.label = null;
}
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(
value.label,
this.getValueOrDefault(this.nameFromUrl(value.domain), "--")
);
cipher.login.uris = this.makeUriArray(value.domain);
cipher.login.password = this.getValueOrDefault(value.password);
if (this.isNullOrWhitespace(value.email) && !this.isNullOrWhitespace(value.username)) {
cipher.login.username = value.username;
} else {
cipher.login.username = this.getValueOrDefault(value.email);
cipher.notes = this.getValueOrDefault(value.username);
}
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,50 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
const OfficialProps = ["!group_id", "!group_name", "title", "username", "password", "URL", "id"];
export class ButtercupCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
this.processFolder(result, this.getValueOrDefault(value["!group_name"]));
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(value.title, "--");
cipher.login.username = this.getValueOrDefault(value.username);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.uris = this.makeUriArray(value.URL);
let processingCustomFields = false;
for (const prop in value) {
// eslint-disable-next-line
if (value.hasOwnProperty(prop)) {
if (!processingCustomFields && OfficialProps.indexOf(prop) === -1) {
processingCustomFields = true;
}
if (processingCustomFields) {
this.processKvp(cipher, prop, value[prop]);
}
}
}
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
if (this.organization) {
this.moveFoldersToCollections(result);
}
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,28 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class ChromeCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(value.name, "--");
cipher.login.username = this.getValueOrDefault(value.username);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.uris = this.makeUriArray(value.url);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,88 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class ClipperzHtmlImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const doc = this.parseXml(data);
if (doc == null) {
result.success = false;
return Promise.resolve(result);
}
const textarea = doc.querySelector("textarea");
if (textarea == null || this.isNullOrWhitespace(textarea.textContent)) {
result.errorMessage = "Missing textarea.";
result.success = false;
return Promise.resolve(result);
}
const entries = JSON.parse(textarea.textContent);
entries.forEach((entry: any) => {
const cipher = this.initLoginCipher();
if (!this.isNullOrWhitespace(entry.label)) {
cipher.name = entry.label.split(" ")[0];
}
if (entry.data != null && !this.isNullOrWhitespace(entry.data.notes)) {
cipher.notes = entry.data.notes.split("\\n").join("\n");
}
if (entry.currentVersion != null && entry.currentVersion.fields != null) {
for (const property in entry.currentVersion.fields) {
// eslint-disable-next-line
if (!entry.currentVersion.fields.hasOwnProperty(property)) {
continue;
}
const field = entry.currentVersion.fields[property];
const actionType = field.actionType != null ? field.actionType.toLowerCase() : null;
switch (actionType) {
case "password":
cipher.login.password = this.getValueOrDefault(field.value);
break;
case "email":
case "username":
case "user":
case "name":
cipher.login.username = this.getValueOrDefault(field.value);
break;
case "url":
cipher.login.uris = this.makeUriArray(field.value);
break;
default: {
const labelLower = field.label != null ? field.label.toLowerCase() : null;
if (
cipher.login.password == null &&
this.passwordFieldNames.indexOf(labelLower) > -1
) {
cipher.login.password = this.getValueOrDefault(field.value);
} else if (
cipher.login.username == null &&
this.usernameFieldNames.indexOf(labelLower) > -1
) {
cipher.login.username = this.getValueOrDefault(field.value);
} else if (
(cipher.login.uris == null || cipher.login.uris.length === 0) &&
this.uriFieldNames.indexOf(labelLower) > -1
) {
cipher.login.uris = this.makeUriArray(field.value);
} else {
this.processKvp(cipher, field.label, field.value);
}
break;
}
}
}
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,47 +0,0 @@
import { ImportResult } from "../models/domain/importResult";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class CodebookCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
this.processFolder(result, this.getValueOrDefault(value.Category));
const cipher = this.initLoginCipher();
cipher.favorite = this.getValueOrDefault(value.Favorite) === "True";
cipher.name = this.getValueOrDefault(value.Entry, "--");
cipher.notes = this.getValueOrDefault(value.Note);
cipher.login.username = this.getValueOrDefault(value.Username, value.Email);
cipher.login.password = this.getValueOrDefault(value.Password);
cipher.login.totp = this.getValueOrDefault(value.TOTP);
cipher.login.uris = this.makeUriArray(value.Website);
if (!this.isNullOrWhitespace(value.Username)) {
this.processKvp(cipher, "Email", value.Email);
}
this.processKvp(cipher, "Phone", value.Phone);
this.processKvp(cipher, "PIN", value.PIN);
this.processKvp(cipher, "Account", value.Account);
this.processKvp(cipher, "Date", value.Date);
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
if (this.organization) {
this.moveFoldersToCollections(result);
}
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,271 +0,0 @@
import { CipherType } from "../../enums/cipherType";
import { SecureNoteType } from "../../enums/secureNoteType";
import { ImportResult } from "../../models/domain/importResult";
import { CardView } from "../../models/view/cardView";
import { CipherView } from "../../models/view/cipherView";
import { IdentityView } from "../../models/view/identityView";
import { LoginView } from "../../models/view/loginView";
import { BaseImporter } from "../baseImporter";
import { Importer } from "../importer";
import {
CredentialsRecord,
IdRecord,
PaymentsRecord,
PersonalInformationRecord,
SecureNoteRecord,
} from "./types/dashlaneCsvTypes";
const _mappedCredentialsColums = new Set([
"title",
"note",
"username",
"password",
"url",
"otpSecret",
"category",
]);
const _mappedPersonalInfoAsIdentiyColumns = new Set([
"type",
"title",
"first_name",
"middle_name",
"last_name",
"login",
"email",
"phone_number",
"address",
"country",
"state",
"city",
"zip",
// Skip item_name as we already have set a combined name
"item_name",
]);
const _mappedSecureNoteColumns = new Set(["title", "note"]);
export class DashlaneCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
if (results[0].type != null && results[0].title != null) {
const personalRecords = results as PersonalInformationRecord[];
// If personalRecords has only one "name" then create an Identity-Cipher
if (personalRecords.filter((x) => x.type === "name").length === 1) {
const cipher = this.initLoginCipher();
cipher.type = CipherType.Identity;
cipher.identity = new IdentityView();
results.forEach((row) => {
this.parsePersonalInformationRecordAsIdentity(cipher, row);
});
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
result.success = true;
return Promise.resolve(result);
}
}
results.forEach((row) => {
const cipher = this.initLoginCipher();
const rowKeys = Object.keys(row);
if (rowKeys[0] === "username") {
this.processFolder(result, row.category);
this.parseCredentialsRecord(cipher, row);
}
if (rowKeys[0] === "type" && rowKeys[1] === "account_name") {
this.parsePaymentRecord(cipher, row);
}
if (rowKeys[0] === "type" && rowKeys[1] === "number") {
this.parseIdRecord(cipher, row);
}
if ((rowKeys[0] === "type") != null && rowKeys[1] === "title") {
this.parsePersonalInformationRecord(cipher, row);
}
if (rowKeys[0] === "title" && rowKeys[1] === "note") {
this.parseSecureNoteRecords(cipher, row);
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
if (this.organization) {
this.moveFoldersToCollections(result);
}
result.success = true;
return Promise.resolve(result);
}
parseCredentialsRecord(cipher: CipherView, row: CredentialsRecord) {
cipher.type = CipherType.Login;
cipher.login = new LoginView();
cipher.name = row.title;
cipher.notes = row.note;
cipher.login.username = row.username;
cipher.login.password = row.password;
cipher.login.totp = row.otpSecret;
cipher.login.uris = this.makeUriArray(row.url);
this.importUnmappedFields(cipher, row, _mappedCredentialsColums);
}
parsePaymentRecord(cipher: CipherView, row: PaymentsRecord) {
cipher.type = CipherType.Card;
cipher.card = new CardView();
cipher.name = row.account_name;
let mappedValues: string[] = [];
switch (row.type) {
case "credit_card":
cipher.card.cardholderName = row.account_name;
cipher.card.number = row.cc_number;
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.code = row.code;
cipher.card.expMonth = row.expiration_month;
cipher.card.expYear = row.expiration_year.substring(2, 4);
// If you add more mapped fields please extend this
mappedValues = [
"account_name",
"account_holder",
"cc_number",
"code",
"expiration_month",
"expiration_year",
];
break;
case "bank":
cipher.card.cardholderName = row.account_holder;
cipher.card.number = row.account_number;
// If you add more mapped fields please extend this
mappedValues = ["account_name", "account_holder", "account_number"];
break;
default:
break;
}
this.importUnmappedFields(cipher, row, new Set(mappedValues));
}
parseIdRecord(cipher: CipherView, row: IdRecord) {
cipher.type = CipherType.Identity;
cipher.identity = new IdentityView();
const mappedValues: string[] = ["name", "number"];
switch (row.type) {
case "card":
cipher.name = `${row.name} ${row.type}`;
this.processFullName(cipher, row.name);
cipher.identity.licenseNumber = row.number;
break;
case "passport":
cipher.name = `${row.name} ${row.type}`;
this.processFullName(cipher, row.name);
cipher.identity.passportNumber = row.number;
break;
case "license":
cipher.name = `${row.name} ${row.type}`;
this.processFullName(cipher, row.name);
cipher.identity.licenseNumber = row.number;
cipher.identity.state = row.state;
mappedValues.push("state");
break;
case "social_security":
cipher.name = `${row.name} ${row.type}`;
this.processFullName(cipher, row.name);
cipher.identity.ssn = row.number;
break;
case "tax_number":
cipher.name = row.type;
cipher.identity.licenseNumber = row.number;
break;
default:
break;
}
// If you add more mapped fields please extend this
this.importUnmappedFields(cipher, row, new Set(mappedValues));
}
parsePersonalInformationRecord(cipher: CipherView, row: PersonalInformationRecord) {
cipher.type = CipherType.SecureNote;
cipher.secureNote.type = SecureNoteType.Generic;
if (row.type === "name") {
cipher.name = `${row.title} ${row.first_name} ${row.middle_name} ${row.last_name}`
.replace(" ", " ")
.trim();
} else {
cipher.name = row.item_name;
}
const dataRow = row as any;
Object.keys(row).forEach((key) => {
this.processKvp(cipher, key, dataRow[key]);
});
}
parsePersonalInformationRecordAsIdentity(cipher: CipherView, row: PersonalInformationRecord) {
switch (row.type) {
case "name":
this.processFullName(cipher, `${row.first_name} ${row.middle_name} ${row.last_name}`);
cipher.identity.title = row.title;
cipher.name = cipher.identity.fullName;
cipher.identity.username = row.login;
break;
case "email":
cipher.identity.email = row.email;
break;
case "number":
cipher.identity.phone = row.phone_number;
break;
case "address":
cipher.identity.address1 = row.address;
cipher.identity.city = row.city;
cipher.identity.postalCode = row.zip;
cipher.identity.state = row.state;
cipher.identity.country = row.country;
break;
default:
break;
}
this.importUnmappedFields(cipher, row, _mappedPersonalInfoAsIdentiyColumns);
}
parseSecureNoteRecords(cipher: CipherView, row: SecureNoteRecord) {
cipher.type = CipherType.SecureNote;
cipher.secureNote.type = SecureNoteType.Generic;
cipher.name = row.title;
cipher.notes = row.note;
this.importUnmappedFields(cipher, row, _mappedSecureNoteColumns);
}
importUnmappedFields(cipher: CipherView, row: any, mappedValues: Set<string>) {
const unmappedFields = Object.keys(row).filter((x) => !mappedValues.has(x));
unmappedFields.forEach((key) => {
const item = row as any;
this.processKvp(cipher, key, item[key]);
});
}
}

View File

@@ -1,171 +0,0 @@
import { CipherType } from "../../enums/cipherType";
import { SecureNoteType } from "../../enums/secureNoteType";
import { ImportResult } from "../../models/domain/importResult";
import { CardView } from "../../models/view/cardView";
import { CipherView } from "../../models/view/cipherView";
import { IdentityView } from "../../models/view/identityView";
import { SecureNoteView } from "../../models/view/secureNoteView";
import { BaseImporter } from "../baseImporter";
import { Importer } from "../importer";
const HandledResults = new Set([
"ADDRESS",
"AUTHENTIFIANT",
"BANKSTATEMENT",
"IDCARD",
"IDENTITY",
"PAYMENTMEANS_CREDITCARD",
"PAYMENTMEAN_PAYPAL",
"EMAIL",
]);
export class DashlaneJsonImporter extends BaseImporter implements Importer {
private result: ImportResult;
parse(data: string): Promise<ImportResult> {
this.result = new ImportResult();
const results = JSON.parse(data);
if (results == null || results.length === 0) {
this.result.success = false;
return Promise.resolve(this.result);
}
if (results.ADDRESS != null) {
this.processAddress(results.ADDRESS);
}
if (results.AUTHENTIFIANT != null) {
this.processAuth(results.AUTHENTIFIANT);
}
if (results.BANKSTATEMENT != null) {
this.processNote(results.BANKSTATEMENT, "BankAccountName");
}
if (results.IDCARD != null) {
this.processNote(results.IDCARD, "Fullname");
}
if (results.PAYMENTMEANS_CREDITCARD != null) {
this.processCard(results.PAYMENTMEANS_CREDITCARD);
}
if (results.IDENTITY != null) {
this.processIdentity(results.IDENTITY);
}
for (const key in results) {
// eslint-disable-next-line
if (results.hasOwnProperty(key) && !HandledResults.has(key)) {
this.processNote(results[key], null, "Generic Note");
}
}
this.result.success = true;
return Promise.resolve(this.result);
}
private processAuth(results: any[]) {
results.forEach((credential: any) => {
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(credential.title);
cipher.login.username = this.getValueOrDefault(
credential.login,
this.getValueOrDefault(credential.secondaryLogin)
);
if (this.isNullOrWhitespace(cipher.login.username)) {
cipher.login.username = this.getValueOrDefault(credential.email);
} else if (!this.isNullOrWhitespace(credential.email)) {
cipher.notes = "Email: " + credential.email + "\n";
}
cipher.login.password = this.getValueOrDefault(credential.password);
cipher.login.uris = this.makeUriArray(credential.domain);
cipher.notes += this.getValueOrDefault(credential.note, "");
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
this.result.ciphers.push(cipher);
});
}
private processIdentity(results: any[]) {
results.forEach((obj: any) => {
const cipher = new CipherView();
cipher.identity = new IdentityView();
cipher.type = CipherType.Identity;
cipher.name = this.getValueOrDefault(obj.fullName, "");
const nameParts = cipher.name.split(" ");
if (nameParts.length > 0) {
cipher.identity.firstName = this.getValueOrDefault(nameParts[0]);
}
if (nameParts.length === 2) {
cipher.identity.lastName = this.getValueOrDefault(nameParts[1]);
} else if (nameParts.length === 3) {
cipher.identity.middleName = this.getValueOrDefault(nameParts[1]);
cipher.identity.lastName = this.getValueOrDefault(nameParts[2]);
}
cipher.identity.username = this.getValueOrDefault(obj.pseudo);
this.cleanupCipher(cipher);
this.result.ciphers.push(cipher);
});
}
private processAddress(results: any[]) {
results.forEach((obj: any) => {
const cipher = new CipherView();
cipher.identity = new IdentityView();
cipher.type = CipherType.Identity;
cipher.name = this.getValueOrDefault(obj.addressName);
cipher.identity.address1 = this.getValueOrDefault(obj.addressFull);
cipher.identity.city = this.getValueOrDefault(obj.city);
cipher.identity.state = this.getValueOrDefault(obj.state);
cipher.identity.postalCode = this.getValueOrDefault(obj.zipcode);
cipher.identity.country = this.getValueOrDefault(obj.country);
if (cipher.identity.country != null) {
cipher.identity.country = cipher.identity.country.toUpperCase();
}
this.cleanupCipher(cipher);
this.result.ciphers.push(cipher);
});
}
private processCard(results: any[]) {
results.forEach((obj: any) => {
const cipher = new CipherView();
cipher.card = new CardView();
cipher.type = CipherType.Card;
cipher.name = this.getValueOrDefault(obj.bank);
cipher.card.number = this.getValueOrDefault(obj.cardNumber);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.cardholderName = this.getValueOrDefault(obj.owner);
if (!this.isNullOrWhitespace(cipher.card.brand)) {
if (this.isNullOrWhitespace(cipher.name)) {
cipher.name = cipher.card.brand;
} else {
cipher.name += " - " + cipher.card.brand;
}
}
this.cleanupCipher(cipher);
this.result.ciphers.push(cipher);
});
}
private processNote(results: any[], nameProperty: string, name: string = null) {
results.forEach((obj: any) => {
const cipher = new CipherView();
cipher.secureNote = new SecureNoteView();
cipher.type = CipherType.SecureNote;
cipher.secureNote.type = SecureNoteType.Generic;
if (name != null) {
cipher.name = name;
} else {
cipher.name = this.getValueOrDefault(obj[nameProperty]);
}
for (const key in obj) {
// eslint-disable-next-line
if (obj.hasOwnProperty(key) && key !== nameProperty) {
this.processKvp(cipher, key, obj[key].toString());
}
}
this.cleanupCipher(cipher);
this.result.ciphers.push(cipher);
});
}
}

View File

@@ -1,68 +0,0 @@
// tslint:disable
export class CredentialsRecord {
username: string;
username2: string;
username3: string;
title: string;
password: string;
note: string;
url: string;
category: string;
otpSecret: string;
}
export class PaymentsRecord {
type: string;
account_name: string;
account_holder: string;
cc_number: string;
code: string;
expiration_month: string;
expiration_year: string;
routing_number: string;
account_number: string;
country: string;
issuing_bank: string;
}
export class IdRecord {
type: string;
number: string;
name: string;
issue_date: string;
expiration_date: string;
place_of_issue: string;
state: string;
}
export class PersonalInformationRecord {
type: string;
title: string;
first_name: string;
middle_name: string;
last_name: string;
login: string;
date_of_birth: string;
place_of_birth: string;
email: string;
email_type: string;
item_name: string;
phone_number: string;
address: string;
country: string;
state: string;
city: string;
zip: string;
address_recipient: string;
address_building: string;
address_apartment: string;
address_floor: string;
address_door_code: string;
job_title: string;
url: string;
}
export class SecureNoteRecord {
title: string;
note: string;
}

View File

@@ -1,60 +0,0 @@
import { CipherType } from "../enums/cipherType";
import { ImportResult } from "../models/domain/importResult";
import { CardView } from "../models/view/cardView";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class EncryptrCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, true);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
results.forEach((value) => {
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(value.Label, "--");
cipher.notes = this.getValueOrDefault(value.Notes);
const text = this.getValueOrDefault(value.Text);
if (!this.isNullOrWhitespace(text)) {
if (this.isNullOrWhitespace(cipher.notes)) {
cipher.notes = text;
} else {
cipher.notes += "\n\n" + text;
}
}
const type = value["Entry Type"];
if (type === "Password") {
cipher.login.username = this.getValueOrDefault(value.Username);
cipher.login.password = this.getValueOrDefault(value.Password);
cipher.login.uris = this.makeUriArray(value["Site URL"]);
} else if (type === "Credit Card") {
cipher.type = CipherType.Card;
cipher.card = new CardView();
cipher.card.cardholderName = this.getValueOrDefault(value["Name on card"]);
cipher.card.number = this.getValueOrDefault(value["Card Number"]);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.code = this.getValueOrDefault(value.CVV);
const expiry = this.getValueOrDefault(value.Expiry);
if (!this.isNullOrWhitespace(expiry)) {
const expParts = expiry.split("/");
if (expParts.length > 1) {
cipher.card.expMonth = parseInt(expParts[0], null).toString();
cipher.card.expYear = (2000 + parseInt(expParts[1], null)).toString();
}
}
}
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -1,133 +0,0 @@
import { CipherType } from "../enums/cipherType";
import { SecureNoteType } from "../enums/secureNoteType";
import { ImportResult } from "../models/domain/importResult";
import { CardView } from "../models/view/cardView";
import { SecureNoteView } from "../models/view/secureNoteView";
import { BaseImporter } from "./baseImporter";
import { Importer } from "./importer";
export class EnpassCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, false);
if (results == null) {
result.success = false;
return Promise.resolve(result);
}
let firstRow = true;
results.forEach((value) => {
if (value.length < 2 || (firstRow && (value[0] === "Title" || value[0] === "title"))) {
firstRow = false;
return;
}
const cipher = this.initLoginCipher();
cipher.notes = this.getValueOrDefault(value[value.length - 1]);
cipher.name = this.getValueOrDefault(value[0], "--");
if (
value.length === 2 ||
(!this.containsField(value, "username") &&
!this.containsField(value, "password") &&
!this.containsField(value, "email") &&
!this.containsField(value, "url"))
) {
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
cipher.secureNote.type = SecureNoteType.Generic;
}
if (
this.containsField(value, "cardholder") &&
this.containsField(value, "number") &&
this.containsField(value, "expiry date")
) {
cipher.type = CipherType.Card;
cipher.card = new CardView();
}
if (value.length > 2 && value.length % 2 === 0) {
for (let i = 0; i < value.length - 2; i += 2) {
const fieldValue: string = value[i + 2];
if (this.isNullOrWhitespace(fieldValue)) {
continue;
}
const fieldName: string = value[i + 1];
const fieldNameLower = fieldName.toLowerCase();
if (cipher.type === CipherType.Login) {
if (
fieldNameLower === "url" &&
(cipher.login.uris == null || cipher.login.uris.length === 0)
) {
cipher.login.uris = this.makeUriArray(fieldValue);
continue;
} else if (
(fieldNameLower === "username" || fieldNameLower === "email") &&
this.isNullOrWhitespace(cipher.login.username)
) {
cipher.login.username = fieldValue;
continue;
} else if (
fieldNameLower === "password" &&
this.isNullOrWhitespace(cipher.login.password)
) {
cipher.login.password = fieldValue;
continue;
} else if (fieldNameLower === "totp" && this.isNullOrWhitespace(cipher.login.totp)) {
cipher.login.totp = fieldValue;
continue;
}
} else if (cipher.type === CipherType.Card) {
if (
fieldNameLower === "cardholder" &&
this.isNullOrWhitespace(cipher.card.cardholderName)
) {
cipher.card.cardholderName = fieldValue;
continue;
} else if (fieldNameLower === "number" && this.isNullOrWhitespace(cipher.card.number)) {
cipher.card.number = fieldValue;
cipher.card.brand = this.getCardBrand(fieldValue);
continue;
} else if (fieldNameLower === "cvc" && this.isNullOrWhitespace(cipher.card.code)) {
cipher.card.code = fieldValue;
continue;
} else if (
fieldNameLower === "expiry date" &&
this.isNullOrWhitespace(cipher.card.expMonth) &&
this.isNullOrWhitespace(cipher.card.expYear)
) {
if (this.setCardExpiration(cipher, fieldValue)) {
continue;
}
} else if (fieldNameLower === "type") {
// Skip since brand was determined from number above
continue;
}
}
this.processKvp(cipher, fieldName, fieldValue);
}
}
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
private containsField(fields: any[], name: string) {
if (fields == null || name == null) {
return false;
}
return (
fields.filter((f) => !this.isNullOrWhitespace(f) && f.toLowerCase() === name.toLowerCase())
.length > 0
);
}
}

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